mirror of
https://github.com/2dust/v2rayN.git
synced 2026-11-03 02:55:01 +03:00
Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 11c8125286 | |||
| 0f1f82af4e | |||
| 3d0f45f6a8 | |||
| 19850f92be | |||
| 4c4449e74d | |||
| 59487dca75 | |||
| d4880dbe02 | |||
| 0856ffd7d8 | |||
| c436b998b4 | |||
| 7a52be4150 | |||
| 8c02b5f2f6 | |||
| 743eb323c2 | |||
| ebabf79017 | |||
| 756916978d | |||
| a2a8a77ff6 | |||
| 2bc79bc5a1 | |||
| 4c8b865d03 | |||
| 3d44ba8ec8 | |||
| aac4520f59 | |||
| b13ae2592a | |||
| bf2584c9a9 | |||
| e044b90c83 | |||
| 3dce503a08 | |||
| 70ab27e436 | |||
| 9593b289b9 | |||
| 5664270be7 | |||
| 926790b071 | |||
| e1c9633535 | |||
| 36184045bb | |||
| fcd313007a | |||
| 7e14cf9773 | |||
| ad3e24b6b5 | |||
| 4169c12a1f | |||
| 8b93d35766 | |||
| 78b0b2224b | |||
| e2f4292321 | |||
| 57f17d5939 | |||
| 66dd7da6e6 | |||
| 5e0ea3275d | |||
| 673283da4b | |||
| 1f083adbe4 | |||
| 80ab14d057 | |||
| 95f438abc9 | |||
| f1023e08e1 | |||
| 1883178b5f | |||
| 1cde521d75 | |||
| 2407d44f4f | |||
| fbf822d74c | |||
| 4869a6cbba | |||
| b501efbc45 | |||
| 5ea8ae6420 | |||
| fa9d4a4769 | |||
| 771ef3192a |
@@ -0,0 +1,941 @@
|
||||
name: build v2rayN.WebAPI
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
release_tag:
|
||||
required: false
|
||||
type: string
|
||||
default: ''
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_tag:
|
||||
description: 'Release tag (for example 7.x.y). Empty builds a development API package.'
|
||||
required: false
|
||||
default: ''
|
||||
type: string
|
||||
pull_request:
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'v2rayN/v2rayN.WebAPI/**'
|
||||
- 'v2rayN/ServiceLib/**'
|
||||
- 'v2rayN/ServiceLib.Tests/**'
|
||||
- 'v2rayN/Directory.Build.*'
|
||||
- 'v2rayN/Directory.Packages.props'
|
||||
- 'v2rayN/v2rayN.slnx'
|
||||
- 'global.json'
|
||||
- '.github/workflows/build-WebAPI.yml'
|
||||
- '.github/workflows/release-WebAPI.yml'
|
||||
- '.github/workflows/build-all.yml'
|
||||
- '.github/workflows/upload-sign.yml'
|
||||
push:
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'v2rayN/v2rayN.WebAPI/**'
|
||||
- 'v2rayN/ServiceLib/**'
|
||||
- 'v2rayN/ServiceLib.Tests/**'
|
||||
- 'v2rayN/Directory.Build.*'
|
||||
- 'v2rayN/Directory.Packages.props'
|
||||
- 'v2rayN/v2rayN.slnx'
|
||||
- 'global.json'
|
||||
- '.github/workflows/build-WebAPI.yml'
|
||||
- '.github/workflows/release-WebAPI.yml'
|
||||
- '.github/workflows/build-all.yml'
|
||||
- '.github/workflows/upload-sign.yml'
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
macos:
|
||||
name: macOS WebAPI and ServiceLib tests
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 15
|
||||
env:
|
||||
TUNIT_MAX_PARALLEL_TESTS: 1
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-dotnet@v6
|
||||
with:
|
||||
dotnet-version: '10.x'
|
||||
- name: Run ServiceLib.Tests
|
||||
run: dotnet test --project v2rayN/ServiceLib.Tests/ServiceLib.Tests.csproj -c Release
|
||||
- name: Run WebAPI.Tests
|
||||
run: dotnet test --project v2rayN/v2rayN.WebAPI/Tests/v2rayN.WebAPI.Tests.csproj -c Release
|
||||
|
||||
identity:
|
||||
name: Resolve API build identity
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
version: ${{ steps.resolve.outputs.version }}
|
||||
tag: ${{ steps.resolve.outputs.tag }}
|
||||
release: ${{ steps.resolve.outputs.release }}
|
||||
build_date: ${{ steps.resolve.outputs.build_date }}
|
||||
steps:
|
||||
- name: Resolve release identity
|
||||
id: resolve
|
||||
shell: bash
|
||||
env:
|
||||
RELEASE_TAG: ${{ inputs.release_tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
build_date="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
if [[ -z "$RELEASE_TAG" ]]; then
|
||||
{
|
||||
echo "version=0.0.0-dev"
|
||||
echo "tag="
|
||||
echo "release=false"
|
||||
echo "build_date=$build_date"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
[[ "$RELEASE_TAG" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || {
|
||||
echo "Invalid release tag: $RELEASE_TAG" >&2
|
||||
exit 1
|
||||
}
|
||||
{
|
||||
echo "version=$RELEASE_TAG"
|
||||
echo "tag=$RELEASE_TAG"
|
||||
echo "release=true"
|
||||
echo "build_date=$build_date"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
verify:
|
||||
name: Backend, ServiceLib, package smoke
|
||||
runs-on: ubuntu-24.04
|
||||
env:
|
||||
TUNIT_MAX_PARALLEL_TESTS: 1
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-dotnet@v6
|
||||
with:
|
||||
dotnet-version: '10.x'
|
||||
- name: Verify API build, backend and ServiceLib tests, native startup and package boundaries
|
||||
run: bash v2rayN/v2rayN.WebAPI/Scripts/verify.sh "$RUNNER_TEMP/v2rayN.WebAPI-verify"
|
||||
- name: Check patch whitespace
|
||||
if: github.event_name == 'pull_request' || github.event_name == 'push' || github.event_name == 'workflow_dispatch'
|
||||
shell: bash
|
||||
env:
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before || '' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
base_sha="$BASE_SHA"
|
||||
if [[ -z "$base_sha" ]]; then
|
||||
base_sha="$(git rev-parse HEAD^)"
|
||||
fi
|
||||
if [[ "$base_sha" =~ ^0+$ ]]; then
|
||||
empty_tree="$(git hash-object -t tree -w /dev/null)"
|
||||
git diff --check "$empty_tree" HEAD
|
||||
else
|
||||
git diff --check "$base_sha...HEAD"
|
||||
fi
|
||||
|
||||
windows:
|
||||
name: Windows WebAPI build and runtime smoke
|
||||
needs: identity
|
||||
runs-on: windows-latest
|
||||
env:
|
||||
TUNIT_MAX_PARALLEL_TESTS: 1
|
||||
WEB_VERSION: ${{ needs.identity.outputs.version }}
|
||||
WEB_COMMIT: ${{ github.sha }}
|
||||
WEB_BUILD_DATE: ${{ needs.identity.outputs.build_date }}
|
||||
WEB_REPOSITORY: ${{ github.repository }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-dotnet@v6
|
||||
with:
|
||||
dotnet-version: '10.x'
|
||||
- uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: '3.x'
|
||||
- name: Restore WebAPI
|
||||
shell: pwsh
|
||||
run: dotnet restore v2rayN/v2rayN.WebAPI/v2rayN.WebAPI.csproj
|
||||
- name: Restore WebAPI.Tests
|
||||
shell: pwsh
|
||||
run: dotnet restore v2rayN/v2rayN.WebAPI/Tests/v2rayN.WebAPI.Tests.csproj
|
||||
- name: Restore ServiceLib.Tests
|
||||
shell: pwsh
|
||||
run: dotnet restore v2rayN/ServiceLib.Tests/ServiceLib.Tests.csproj
|
||||
- name: Build WebAPI
|
||||
shell: pwsh
|
||||
run: dotnet build v2rayN/v2rayN.WebAPI/v2rayN.WebAPI.csproj --configuration Release --no-restore
|
||||
- name: Run WebAPI.Tests
|
||||
shell: pwsh
|
||||
run: dotnet test --project v2rayN/v2rayN.WebAPI/Tests/v2rayN.WebAPI.Tests.csproj --configuration Release --no-restore
|
||||
- name: Run ServiceLib.Tests
|
||||
shell: pwsh
|
||||
run: dotnet test --project v2rayN/ServiceLib.Tests/ServiceLib.Tests.csproj --configuration Release --no-restore
|
||||
- name: Publish win-x64 self-contained single-file WebAPI
|
||||
shell: pwsh
|
||||
run: |
|
||||
$publishDirectory = Join-Path $env:RUNNER_TEMP 'v2rayN.WebAPI-win-x64'
|
||||
dotnet publish v2rayN/v2rayN.WebAPI/v2rayN.WebAPI.csproj `
|
||||
--configuration Release `
|
||||
--runtime win-x64 `
|
||||
--self-contained true `
|
||||
-p:PublishSingleFile=true `
|
||||
-p:IncludeNativeLibrariesForSelfExtract=true `
|
||||
-p:WebVersion=$env:WEB_VERSION `
|
||||
-p:WebCommit=$env:WEB_COMMIT `
|
||||
-p:WebBuildDate=$env:WEB_BUILD_DATE `
|
||||
-p:WebRepository=$env:WEB_REPOSITORY `
|
||||
--output $publishDirectory
|
||||
$identity = [ordered]@{
|
||||
product = 'v2rayN.WebAPI'
|
||||
version = $env:WEB_VERSION
|
||||
commit = $env:WEB_COMMIT
|
||||
buildDate = $env:WEB_BUILD_DATE
|
||||
rid = 'win-x64'
|
||||
}
|
||||
$json = $identity | ConvertTo-Json -Compress
|
||||
[IO.File]::WriteAllText((Join-Path $publishDirectory 'v2rayN.WebAPI.build.json'), $json, [Text.UTF8Encoding]::new($false))
|
||||
- name: Verify single-file publish output
|
||||
shell: pwsh
|
||||
run: |
|
||||
$publishDirectory = Join-Path $env:RUNNER_TEMP 'v2rayN.WebAPI-win-x64'
|
||||
$executable = Join-Path $publishDirectory 'v2rayN.WebAPI.exe'
|
||||
if (-not (Test-Path -LiteralPath $executable -PathType Leaf)) {
|
||||
throw "dotnet publish did not produce $executable"
|
||||
}
|
||||
if ((Get-Item -LiteralPath $executable).Length -eq 0) {
|
||||
throw "Published executable is empty: $executable"
|
||||
}
|
||||
$looseDlls = @(Get-ChildItem -LiteralPath $publishDirectory -Filter '*.dll' -File -Recurse)
|
||||
if ($looseDlls.Count -gt 0) {
|
||||
throw "Single-file publish left loose DLLs: $($looseDlls.FullName -join ', ')"
|
||||
}
|
||||
- name: Smoke-test first startup, health endpoint and graceful shutdown
|
||||
shell: pwsh
|
||||
run: |
|
||||
$publishDirectory = Join-Path $env:RUNNER_TEMP 'v2rayN.WebAPI-win-x64'
|
||||
& ./v2rayN/v2rayN.WebAPI/Tests/Integration/Test-Windows.ps1 -Mode startup -PublishDirectory $publishDirectory
|
||||
- name: Publish previous Windows fixture for update regression
|
||||
if: inputs.release_tag != ''
|
||||
shell: pwsh
|
||||
run: |
|
||||
$publishDirectory = Join-Path $env:RUNNER_TEMP 'v2rayN.WebAPI-win-x64-previous'
|
||||
dotnet publish v2rayN/v2rayN.WebAPI/v2rayN.WebAPI.csproj `
|
||||
--configuration Release `
|
||||
--runtime win-x64 `
|
||||
--self-contained true `
|
||||
-p:PublishSingleFile=true `
|
||||
-p:IncludeNativeLibrariesForSelfExtract=true `
|
||||
-p:WebVersion=0.0.1-regression `
|
||||
-p:WebCommit=$env:WEB_COMMIT `
|
||||
-p:WebBuildDate=$env:WEB_BUILD_DATE `
|
||||
-p:WebRepository=$env:WEB_REPOSITORY `
|
||||
--output $publishDirectory
|
||||
$identity = [ordered]@{
|
||||
product = 'v2rayN.WebAPI'
|
||||
version = '0.0.1-regression'
|
||||
commit = $env:WEB_COMMIT
|
||||
buildDate = $env:WEB_BUILD_DATE
|
||||
rid = 'win-x64'
|
||||
}
|
||||
$json = $identity | ConvertTo-Json -Compress
|
||||
[IO.File]::WriteAllText((Join-Path $publishDirectory 'v2rayN.WebAPI.build.json'), $json, [Text.UTF8Encoding]::new($false))
|
||||
- name: Add the matching Windows Core bundle
|
||||
if: inputs.release_tag != ''
|
||||
shell: pwsh
|
||||
run: |
|
||||
$bundle = Join-Path $env:RUNNER_TEMP 'v2rayN-windows-64.zip'
|
||||
$expanded = Join-Path $env:RUNNER_TEMP 'v2rayN-windows-64-core'
|
||||
$publishDirectory = Join-Path $env:RUNNER_TEMP 'v2rayN.WebAPI-win-x64'
|
||||
$previousDirectory = Join-Path $env:RUNNER_TEMP 'v2rayN.WebAPI-win-x64-previous'
|
||||
Invoke-WebRequest -Uri 'https://raw.githubusercontent.com/2dust/v2rayN-core-bin/refs/heads/master/v2rayN-windows-64.zip' -OutFile $bundle
|
||||
Expand-Archive -LiteralPath $bundle -DestinationPath $expanded -Force
|
||||
$sourceBin = Join-Path $expanded 'v2rayN-windows-64\bin'
|
||||
if (-not (Test-Path -LiteralPath $sourceBin -PathType Container)) {
|
||||
throw "Windows Core bundle is missing $sourceBin"
|
||||
}
|
||||
foreach ($directory in @($publishDirectory, $previousDirectory)) {
|
||||
$destinationBin = Join-Path $directory 'bin'
|
||||
New-Item -ItemType Directory -Path $destinationBin -Force | Out-Null
|
||||
Copy-Item -Path (Join-Path $sourceBin '*') -Destination $destinationBin -Recurse -Force
|
||||
}
|
||||
foreach ($directory in @($publishDirectory, $previousDirectory)) {
|
||||
$checkBin = Join-Path $directory 'bin'
|
||||
foreach ($relative in @('xray\xray.exe','sing_box\sing-box.exe','mihomo\mihomo.exe','geoip.dat','geosite.dat','geoip.metadb','Country.mmdb')) {
|
||||
$required = Join-Path $checkBin $relative
|
||||
if (-not (Test-Path -LiteralPath $required -PathType Leaf)) { throw "Missing Windows Core asset: $required" }
|
||||
}
|
||||
}
|
||||
$candidateBin = Join-Path $publishDirectory 'bin'
|
||||
& (Join-Path $candidateBin 'xray\xray.exe') -version
|
||||
if ($LASTEXITCODE -ne 0) { throw "xray.exe smoke failed: $LASTEXITCODE" }
|
||||
& (Join-Path $candidateBin 'sing_box\sing-box.exe') version
|
||||
if ($LASTEXITCODE -ne 0) { throw "sing-box.exe smoke failed: $LASTEXITCODE" }
|
||||
& (Join-Path $candidateBin 'mihomo\mihomo.exe') -v
|
||||
if ($LASTEXITCODE -ne 0) { throw "mihomo.exe smoke failed: $LASTEXITCODE" }
|
||||
- name: Smoke-test Windows backup restore handoff and Core intent recovery
|
||||
if: inputs.release_tag != ''
|
||||
shell: pwsh
|
||||
run: |
|
||||
& ./v2rayN/v2rayN.WebAPI/Tests/Integration/Test-Windows.ps1 `
|
||||
-Mode restore `
|
||||
-PublishDirectory (Join-Path $env:RUNNER_TEMP 'v2rayN.WebAPI-win-x64')
|
||||
- name: Smoke-test Windows self-update replacement and rollback
|
||||
if: inputs.release_tag != ''
|
||||
shell: pwsh
|
||||
run: |
|
||||
& ./v2rayN/v2rayN.WebAPI/Tests/Integration/Test-Windows.ps1 `
|
||||
-Mode update `
|
||||
-PreviousPublishDirectory (Join-Path $env:RUNNER_TEMP 'v2rayN.WebAPI-win-x64-previous') `
|
||||
-CandidatePublishDirectory (Join-Path $env:RUNNER_TEMP 'v2rayN.WebAPI-win-x64')
|
||||
- name: Package and smoke-test Windows full-install and app-only ZIPs
|
||||
if: inputs.release_tag != ''
|
||||
shell: pwsh
|
||||
run: |
|
||||
$publishDirectory = Join-Path $env:RUNNER_TEMP 'v2rayN.WebAPI-win-x64'
|
||||
$dist = Join-Path $env:RUNNER_TEMP 'v2rayN.WebAPI-win-x64-dist'
|
||||
python v2rayN/v2rayN.WebAPI/Scripts/web-release-assets.py package --rid win-x64 --publish $publishDirectory --dist $dist
|
||||
$fullZip = Join-Path $dist 'v2rayN-windows-64-WebAPI.zip'
|
||||
$appOnlyZip = Join-Path $dist 'v2rayN-windows-64-WebAPI-update.zip'
|
||||
if (-not (Test-Path -LiteralPath $fullZip -PathType Leaf) -or -not (Test-Path -LiteralPath $appOnlyZip -PathType Leaf)) {
|
||||
throw 'Windows release ZIPs were not created.'
|
||||
}
|
||||
$extracted = Join-Path $env:RUNNER_TEMP 'v2rayN-windows-64-WebAPI-smoke'
|
||||
Expand-Archive -LiteralPath $fullZip -DestinationPath $extracted -Force
|
||||
& ./v2rayN/v2rayN.WebAPI/Tests/Integration/Test-Windows.ps1 -Mode startup -PublishDirectory $extracted
|
||||
"Windows full-install ZIP: $fullZip"
|
||||
"Windows app-only update ZIP: $appOnlyZip"
|
||||
- uses: actions/upload-artifact@v7
|
||||
if: inputs.release_tag != ''
|
||||
with:
|
||||
name: v2rayN.WebAPI-win-x64
|
||||
path: ${{ runner.temp }}/v2rayN.WebAPI-win-x64-dist/*.zip
|
||||
if-no-files-found: error
|
||||
|
||||
# The self-update replacement smoke stays on win-x64: the updater logic is RID-generic
|
||||
# and the per-RID PE/ELF checks are covered by unit tests. This job proves the win-arm64
|
||||
# build, first startup, health endpoint and graceful shutdown on native ARM64 Windows.
|
||||
windows-arm64:
|
||||
name: Windows ARM64 WebAPI build and runtime smoke
|
||||
needs: identity
|
||||
runs-on: windows-11-arm
|
||||
env:
|
||||
TUNIT_MAX_PARALLEL_TESTS: 1
|
||||
WEB_VERSION: ${{ needs.identity.outputs.version }}
|
||||
WEB_COMMIT: ${{ github.sha }}
|
||||
WEB_BUILD_DATE: ${{ needs.identity.outputs.build_date }}
|
||||
WEB_REPOSITORY: ${{ github.repository }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-dotnet@v6
|
||||
with:
|
||||
dotnet-version: '10.x'
|
||||
- uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: '3.x'
|
||||
- name: Restore WebAPI
|
||||
shell: pwsh
|
||||
run: dotnet restore v2rayN/v2rayN.WebAPI/v2rayN.WebAPI.csproj
|
||||
- name: Restore WebAPI.Tests
|
||||
shell: pwsh
|
||||
run: dotnet restore v2rayN/v2rayN.WebAPI/Tests/v2rayN.WebAPI.Tests.csproj
|
||||
- name: Restore ServiceLib.Tests
|
||||
shell: pwsh
|
||||
run: dotnet restore v2rayN/ServiceLib.Tests/ServiceLib.Tests.csproj
|
||||
- name: Build WebAPI
|
||||
shell: pwsh
|
||||
run: dotnet build v2rayN/v2rayN.WebAPI/v2rayN.WebAPI.csproj --configuration Release --no-restore
|
||||
- name: Run WebAPI.Tests
|
||||
shell: pwsh
|
||||
run: dotnet test --project v2rayN/v2rayN.WebAPI/Tests/v2rayN.WebAPI.Tests.csproj --configuration Release --no-restore
|
||||
- name: Run ServiceLib.Tests
|
||||
shell: pwsh
|
||||
run: dotnet test --project v2rayN/ServiceLib.Tests/ServiceLib.Tests.csproj --configuration Release --no-restore
|
||||
- name: Publish win-arm64 self-contained single-file WebAPI
|
||||
shell: pwsh
|
||||
run: |
|
||||
$publishDirectory = Join-Path $env:RUNNER_TEMP 'v2rayN.WebAPI-win-arm64'
|
||||
dotnet publish v2rayN/v2rayN.WebAPI/v2rayN.WebAPI.csproj `
|
||||
--configuration Release `
|
||||
--runtime win-arm64 `
|
||||
--self-contained true `
|
||||
-p:PublishSingleFile=true `
|
||||
-p:IncludeNativeLibrariesForSelfExtract=true `
|
||||
-p:WebVersion=$env:WEB_VERSION `
|
||||
-p:WebCommit=$env:WEB_COMMIT `
|
||||
-p:WebBuildDate=$env:WEB_BUILD_DATE `
|
||||
-p:WebRepository=$env:WEB_REPOSITORY `
|
||||
--output $publishDirectory
|
||||
$identity = [ordered]@{
|
||||
product = 'v2rayN.WebAPI'
|
||||
version = $env:WEB_VERSION
|
||||
commit = $env:WEB_COMMIT
|
||||
buildDate = $env:WEB_BUILD_DATE
|
||||
rid = 'win-arm64'
|
||||
}
|
||||
$json = $identity | ConvertTo-Json -Compress
|
||||
[IO.File]::WriteAllText((Join-Path $publishDirectory 'v2rayN.WebAPI.build.json'), $json, [Text.UTF8Encoding]::new($false))
|
||||
- name: Smoke-test first startup, health endpoint and graceful shutdown
|
||||
shell: pwsh
|
||||
run: |
|
||||
$publishDirectory = Join-Path $env:RUNNER_TEMP 'v2rayN.WebAPI-win-arm64'
|
||||
& ./v2rayN/v2rayN.WebAPI/Tests/Integration/Test-Windows.ps1 -Mode startup -PublishDirectory $publishDirectory
|
||||
- name: Add the matching Windows ARM64 Core bundle
|
||||
if: inputs.release_tag != ''
|
||||
shell: pwsh
|
||||
run: |
|
||||
$bundle = Join-Path $env:RUNNER_TEMP 'v2rayN-windows-arm64.zip'
|
||||
$expanded = Join-Path $env:RUNNER_TEMP 'v2rayN-windows-arm64-core'
|
||||
$publishDirectory = Join-Path $env:RUNNER_TEMP 'v2rayN.WebAPI-win-arm64'
|
||||
Invoke-WebRequest -Uri 'https://raw.githubusercontent.com/2dust/v2rayN-core-bin/refs/heads/master/v2rayN-windows-arm64.zip' -OutFile $bundle
|
||||
Expand-Archive -LiteralPath $bundle -DestinationPath $expanded -Force
|
||||
$sourceBin = Join-Path $expanded 'v2rayN-windows-arm64\bin'
|
||||
if (-not (Test-Path -LiteralPath $sourceBin -PathType Container)) {
|
||||
throw "Windows ARM64 Core bundle is missing $sourceBin"
|
||||
}
|
||||
$destinationBin = Join-Path $publishDirectory 'bin'
|
||||
New-Item -ItemType Directory -Path $destinationBin -Force | Out-Null
|
||||
Copy-Item -Path (Join-Path $sourceBin '*') -Destination $destinationBin -Recurse -Force
|
||||
foreach ($relative in @('xray\xray.exe','sing_box\sing-box.exe','mihomo\mihomo.exe','geoip.dat','geosite.dat','geoip.metadb','Country.mmdb')) {
|
||||
$required = Join-Path $destinationBin $relative
|
||||
if (-not (Test-Path -LiteralPath $required -PathType Leaf)) { throw "Missing Windows ARM64 Core asset: $required" }
|
||||
}
|
||||
& (Join-Path $destinationBin 'xray\xray.exe') -version
|
||||
if ($LASTEXITCODE -ne 0) { throw "xray.exe smoke failed: $LASTEXITCODE" }
|
||||
& (Join-Path $destinationBin 'sing_box\sing-box.exe') version
|
||||
if ($LASTEXITCODE -ne 0) { throw "sing-box.exe smoke failed: $LASTEXITCODE" }
|
||||
& (Join-Path $destinationBin 'mihomo\mihomo.exe') -v
|
||||
if ($LASTEXITCODE -ne 0) { throw "mihomo.exe smoke failed: $LASTEXITCODE" }
|
||||
- name: Package Windows ARM64 full-install and app-only ZIPs
|
||||
if: inputs.release_tag != ''
|
||||
shell: pwsh
|
||||
run: |
|
||||
$publishDirectory = Join-Path $env:RUNNER_TEMP 'v2rayN.WebAPI-win-arm64'
|
||||
$dist = Join-Path $env:RUNNER_TEMP 'v2rayN.WebAPI-win-arm64-dist'
|
||||
python v2rayN/v2rayN.WebAPI/Scripts/web-release-assets.py package --rid win-arm64 --publish $publishDirectory --dist $dist
|
||||
$fullZip = Join-Path $dist 'v2rayN-windows-arm64-WebAPI.zip'
|
||||
$appOnlyZip = Join-Path $dist 'v2rayN-windows-arm64-WebAPI-update.zip'
|
||||
if (-not (Test-Path -LiteralPath $fullZip -PathType Leaf) -or -not (Test-Path -LiteralPath $appOnlyZip -PathType Leaf)) {
|
||||
throw 'Windows ARM64 release ZIPs were not created.'
|
||||
}
|
||||
$extracted = Join-Path $env:RUNNER_TEMP 'v2rayN-windows-arm64-WebAPI-smoke'
|
||||
Expand-Archive -LiteralPath $fullZip -DestinationPath $extracted -Force
|
||||
& ./v2rayN/v2rayN.WebAPI/Tests/Integration/Test-Windows.ps1 -Mode startup -PublishDirectory $extracted
|
||||
"Windows ARM64 full-install ZIP: $fullZip"
|
||||
"Windows ARM64 app-only update ZIP: $appOnlyZip"
|
||||
- uses: actions/upload-artifact@v7
|
||||
if: inputs.release_tag != ''
|
||||
with:
|
||||
name: v2rayN.WebAPI-win-arm64
|
||||
path: ${{ runner.temp }}/v2rayN.WebAPI-win-arm64-dist/*.zip
|
||||
if-no-files-found: error
|
||||
|
||||
native:
|
||||
name: Native API build (${{ matrix.rid }})
|
||||
needs: identity
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# Development builds keep x64 coverage; releases also validate ARM64.
|
||||
# Only tested Linux RIDs are enabled; native RID/runner pairs are independent
|
||||
# of Desktop workflows and the container platform matrix.
|
||||
include: >-
|
||||
${{ fromJSON(needs.identity.outputs.release == 'true'
|
||||
&& '[{"rid":"linux-x64","runner":"ubuntu-24.04"},{"rid":"linux-arm64","runner":"ubuntu-24.04-arm"}]'
|
||||
|| '[{"rid":"linux-x64","runner":"ubuntu-24.04"}]') }}
|
||||
runs-on: ${{ matrix.runner }}
|
||||
env:
|
||||
RID: ${{ matrix.rid }}
|
||||
V2RAYN_WEB_VERSION: ${{ needs.identity.outputs.version }}
|
||||
V2RAYN_WEB_REPOSITORY: ${{ github.repository }}
|
||||
V2RAYN_WEB_COMMIT: ${{ github.sha }}
|
||||
V2RAYN_WEB_BUILD_DATE: ${{ needs.identity.outputs.build_date }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-dotnet@v6
|
||||
with:
|
||||
dotnet-version: '10.x'
|
||||
- name: Publish native API
|
||||
run: bash v2rayN/v2rayN.WebAPI/Scripts/publish-native.sh "$RID"
|
||||
- name: Verify native executable
|
||||
run: test -x "v2rayN/v2rayN.WebAPI/publish/$RID/v2rayN.WebAPI"
|
||||
- name: Add the matching Linux Core bundle
|
||||
if: needs.identity.outputs.release == 'true'
|
||||
run: sh v2rayN/v2rayN.WebAPI/Scripts/fetch-core-bundle.sh "$RID" "v2rayN/v2rayN.WebAPI/publish/$RID/bin"
|
||||
- name: Package API install and self-update ZIPs
|
||||
if: needs.identity.outputs.release == 'true'
|
||||
run: python3 v2rayN/v2rayN.WebAPI/Scripts/web-release-assets.py package --rid "$RID" --publish "v2rayN/v2rayN.WebAPI/publish/$RID" --dist dist
|
||||
- name: Smoke-test bundled Cores
|
||||
if: needs.identity.outputs.release == 'true'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
publish="v2rayN/v2rayN.WebAPI/publish/$RID"
|
||||
"$publish/bin/xray/xray" -version
|
||||
"$publish/bin/sing_box/sing-box" version
|
||||
"$publish/bin/mihomo/mihomo" -v
|
||||
- uses: actions/upload-artifact@v7
|
||||
if: needs.identity.outputs.release == 'true'
|
||||
with:
|
||||
name: v2rayN.WebAPI-${{ matrix.rid }}
|
||||
path: dist/*.zip
|
||||
if-no-files-found: error
|
||||
|
||||
# linux-riscv64 runs natively on the RISE RISC-V runners with the community
|
||||
# xujiegb/dotnet-riscv SDK; single-file publish is verified to work there.
|
||||
native-riscv64:
|
||||
name: Native API build (linux-riscv64)
|
||||
needs: identity
|
||||
if: needs.identity.outputs.release == 'true'
|
||||
runs-on: ubuntu-24.04-riscv
|
||||
container: debian:13
|
||||
env:
|
||||
RID: linux-riscv64
|
||||
V2RAYN_WEB_VERSION: ${{ needs.identity.outputs.version }}
|
||||
V2RAYN_WEB_REPOSITORY: ${{ github.repository }}
|
||||
V2RAYN_WEB_COMMIT: ${{ github.sha }}
|
||||
V2RAYN_WEB_BUILD_DATE: ${{ needs.identity.outputs.build_date }}
|
||||
steps:
|
||||
- name: Prepare tools (Debian)
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update
|
||||
apt-get install -y sudo git rsync findutils tar gzip unzip which curl jq wget file ca-certificates python3 libc6-dev libgcc-s1 libstdc++6 zlib1g libatomic1 libicu76 || \
|
||||
apt-get install -y sudo git rsync findutils tar gzip unzip which curl jq wget file ca-certificates python3 libc6-dev libgcc-s1 libstdc++6 zlib1g libatomic1 libicu72
|
||||
- name: Checkout repo
|
||||
shell: bash
|
||||
env: { GITHUB_TOKEN: "${{ github.token }}" }
|
||||
run: |
|
||||
set -euo pipefail
|
||||
rm -rf ./*
|
||||
git init .
|
||||
git config --global --add safe.directory "$PWD"
|
||||
git remote add origin "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
|
||||
git fetch --depth=1 origin "${GITHUB_SHA}"
|
||||
git checkout FETCH_HEAD
|
||||
- name: Install community .NET SDK (linux-riscv64)
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
SDK=dotnet-sdk-10.0.111-linux-riscv64.tar.gz
|
||||
curl -fL "https://github.com/xujiegb/dotnet-riscv/releases/download/10.0.111/$SDK" -o /tmp/$SDK
|
||||
mkdir -p "$HOME/.dotnet"
|
||||
tar -C "$HOME/.dotnet" -xzf /tmp/$SDK
|
||||
echo "DOTNET_ROOT=$HOME/.dotnet" >> "$GITHUB_ENV"
|
||||
echo "$HOME/.dotnet" >> "$GITHUB_PATH"
|
||||
export DOTNET_ROOT="$HOME/.dotnet"
|
||||
export PATH="$DOTNET_ROOT:$PATH"
|
||||
dotnet --info
|
||||
- name: Publish native API
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
export DOTNET_ROOT="$HOME/.dotnet"
|
||||
export PATH="$DOTNET_ROOT:$PATH"
|
||||
bash v2rayN/v2rayN.WebAPI/Scripts/publish-native.sh "$RID"
|
||||
test -x "v2rayN/v2rayN.WebAPI/publish/$RID/v2rayN.WebAPI"
|
||||
- name: Add the matching Linux Core bundle
|
||||
shell: bash
|
||||
run: sh v2rayN/v2rayN.WebAPI/Scripts/fetch-core-bundle.sh "$RID" "v2rayN/v2rayN.WebAPI/publish/$RID/bin"
|
||||
- name: Package API install and self-update ZIPs
|
||||
shell: bash
|
||||
run: python3 v2rayN/v2rayN.WebAPI/Scripts/web-release-assets.py package --rid "$RID" --publish "v2rayN/v2rayN.WebAPI/publish/$RID" --dist dist
|
||||
- name: Smoke-test bundled Cores
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
publish="v2rayN/v2rayN.WebAPI/publish/$RID"
|
||||
"$publish/bin/xray/xray" -version
|
||||
"$publish/bin/sing_box/sing-box" version
|
||||
"$publish/bin/mihomo/mihomo" -v
|
||||
- name: Smoke-test the published API (native riscv64)
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
publish="v2rayN/v2rayN.WebAPI/publish/$RID"
|
||||
"$publish/v2rayN.WebAPI" --help | head -n 5
|
||||
export V2RAYN_WEB_API_KEY=ci-smoke-key-0123456789abcdef
|
||||
export ASPNETCORE_URLS=http://127.0.0.1:5080
|
||||
cd "$publish"
|
||||
./v2rayN.WebAPI --foreground --no-open > /tmp/smoke.log 2>&1 &
|
||||
app_pid=$!
|
||||
echo "$app_pid" > /tmp/smoke.pid
|
||||
echo "riscv64 API smoke application PID: $app_pid"
|
||||
ok=0
|
||||
for i in $(seq 1 90); do
|
||||
if curl --noproxy '*' -fsS http://127.0.0.1:5080/api/health >/dev/null 2>&1; then ok=1; break; fi
|
||||
if ! kill -0 "$app_pid" 2>/dev/null; then break; fi
|
||||
sleep 1
|
||||
done
|
||||
if [ "$ok" != 1 ]; then
|
||||
echo 'riscv64 API smoke did not become healthy'
|
||||
tail -n 60 /tmp/smoke.log || true
|
||||
kill -TERM "$app_pid" 2>/dev/null || true
|
||||
sleep 1
|
||||
kill -KILL "$app_pid" 2>/dev/null || true
|
||||
exit 1
|
||||
fi
|
||||
kill -TERM "$app_pid"
|
||||
stopped=0
|
||||
for i in $(seq 1 30); do
|
||||
if ! kill -0 "$app_pid" 2>/dev/null; then stopped=1; break; fi
|
||||
sleep 1
|
||||
done
|
||||
if [ "$stopped" != 1 ]; then
|
||||
echo 'riscv64 API smoke did not exit within 30s after SIGTERM'
|
||||
tail -n 60 /tmp/smoke.log || true
|
||||
kill -KILL "$app_pid" 2>/dev/null || true
|
||||
exit 1
|
||||
fi
|
||||
set +e
|
||||
wait "$app_pid"
|
||||
exit_code=$?
|
||||
set -e
|
||||
if [ "$exit_code" -ne 0 ]; then
|
||||
echo "riscv64 API smoke exited with status $exit_code after SIGTERM"
|
||||
tail -n 60 /tmp/smoke.log || true
|
||||
exit 1
|
||||
fi
|
||||
echo 'linux-riscv64 native API smoke OK'
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: v2rayN.WebAPI-linux-riscv64
|
||||
path: dist/*.zip
|
||||
if-no-files-found: error
|
||||
|
||||
# linux-loongarch64 publishes inside the upstream QEMU loong64 VM with the official
|
||||
# Loongson .NET SDK; the VM runs the native smoke before the host packages the ZIPs.
|
||||
native-loong64:
|
||||
name: Native API build (linux-loongarch64)
|
||||
needs: identity
|
||||
if: needs.identity.outputs.release == 'true'
|
||||
runs-on: ubuntu-26.04-arm
|
||||
env:
|
||||
RID: linux-loongarch64
|
||||
V2RAYN_WEB_VERSION: ${{ needs.identity.outputs.version }}
|
||||
V2RAYN_WEB_REPOSITORY: ${{ github.repository }}
|
||||
V2RAYN_WEB_COMMIT: ${{ github.sha }}
|
||||
V2RAYN_WEB_BUILD_DATE: ${{ needs.identity.outputs.build_date }}
|
||||
QCOW2_URL: https://github.com/xujiegb/debian-loong64-qcow2/releases/download/13.5/debian13-loong64.qcow2
|
||||
EFI_CODE_URL: https://github.com/xujiegb/debian-loong64-qcow2/releases/download/13.5/edk2-loongarch64-code.fd
|
||||
EFI_VARS_URL: https://github.com/xujiegb/debian-loong64-qcow2/releases/download/13.5/edk2-loongarch64-vars.fd
|
||||
QCOW2_IMAGE: debian13-loong64.qcow2
|
||||
EFI_CODE: edk2-loongarch64-code.fd
|
||||
EFI_VARS: edk2-loongarch64-vars.fd
|
||||
QEMU_VERSION: 10.2.4
|
||||
steps:
|
||||
- name: Prepare host tools
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y git rsync qemu-utils qemu-system-common expect wget curl ca-certificates libfdt1 libglib2.0-0 libpixman-1-0 libslirp0 virtiofsd
|
||||
sudo ln -sf /usr/libexec/virtiofsd /usr/local/bin/virtiofsd
|
||||
virtiofsd --version || virtiofsd --help | head
|
||||
- name: Host clone repo
|
||||
shell: bash
|
||||
env: { GITHUB_TOKEN: "${{ github.token }}" }
|
||||
run: |
|
||||
set -euo pipefail
|
||||
rm -rf repo
|
||||
git init repo
|
||||
git -C repo config --global --add safe.directory "$PWD/repo" || true
|
||||
git -C repo remote add origin "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
|
||||
git -C repo fetch --depth=1 origin "${GITHUB_SHA}"
|
||||
git -C repo checkout FETCH_HEAD
|
||||
- name: Download QEMU prebuild
|
||||
run: |
|
||||
set -euo pipefail
|
||||
wget -O qemu-linux-arm64-v8.2.tar.gz "https://github.com/xujiegb/qemu-linux-prebuild/releases/download/${QEMU_VERSION}/qemu-linux-arm64-v8.2.tar.gz"
|
||||
tar -xzf qemu-linux-arm64-v8.2.tar.gz
|
||||
mkdir -p "$HOME/qemu-install"
|
||||
rsync -a qemu-linux-arm64-v8.2/ "$HOME/qemu-install/"
|
||||
"$HOME/qemu-install/bin/qemu-system-loongarch64" --version
|
||||
- name: Download loong64 qcow2 and EFI firmware
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
wget -O "$QCOW2_IMAGE" "$QCOW2_URL"
|
||||
wget -O "$EFI_CODE" "$EFI_CODE_URL"
|
||||
wget -O "$EFI_VARS" "$EFI_VARS_URL"
|
||||
qemu-img info "$QCOW2_IMAGE"
|
||||
- name: Stage the VM build script and the matching Core bundle
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cat > webapi-loong.env <<EOF
|
||||
WEB_VERSION=${V2RAYN_WEB_VERSION}
|
||||
WEB_REPOSITORY=${V2RAYN_WEB_REPOSITORY}
|
||||
WEB_COMMIT=${V2RAYN_WEB_COMMIT}
|
||||
WEB_BUILD_DATE=${V2RAYN_WEB_BUILD_DATE}
|
||||
EOF
|
||||
cat > webapi-loong-run.sh <<'SCRIPT'
|
||||
#!/usr/bin/env bash
|
||||
set +e
|
||||
set -o pipefail
|
||||
source /workspace/webapi-loong.env
|
||||
export DOTNET_ROOT=/root/.dotnet
|
||||
export PATH="$DOTNET_ROOT:$PATH"
|
||||
mkdir -p /root/.dotnet
|
||||
SDK=dotnet-sdk-10.0.111-linux-loongarch64.tar.gz
|
||||
curl -fL "https://github.com/loongson/dotnet/releases/download/v10.0.111-loongarch64/$SDK" -o /tmp/$SDK || exit 9
|
||||
tar -C /root/.dotnet -xzf /tmp/$SDK || exit 9
|
||||
dotnet --info >/dev/null 2>&1 || exit 9
|
||||
cd /root/v2rayN-src || exit 9
|
||||
mkdir -p /workspace/publish/linux-loongarch64
|
||||
V2RAYN_WEB_VERSION="$WEB_VERSION" V2RAYN_WEB_REPOSITORY="$WEB_REPOSITORY" V2RAYN_WEB_COMMIT="$WEB_COMMIT" V2RAYN_WEB_BUILD_DATE="$WEB_BUILD_DATE" \
|
||||
bash v2rayN/v2rayN.WebAPI/Scripts/publish-native.sh linux-loongarch64 /workspace/publish/linux-loongarch64 2>&1 | tee /tmp/publish.log
|
||||
rc=$?
|
||||
if [ $rc -eq 0 ]; then
|
||||
mkdir -p /workspace/publish/linux-loongarch64/bin
|
||||
cp -a /workspace/core-bundle/. /workspace/publish/linux-loongarch64/bin/
|
||||
# Smoke a VM-local copy: the shared publish tree must stay exactly the packaging
|
||||
# input, without runtime lock/data files owned by the VM user.
|
||||
rm -rf /root/websmoke
|
||||
cp -a /workspace/publish/linux-loongarch64 /root/websmoke
|
||||
cd /root/websmoke || exit 9
|
||||
./bin/xray/xray -version >/dev/null || rc=9
|
||||
./bin/sing_box/sing-box version >/dev/null || rc=9
|
||||
./bin/mihomo/mihomo -v >/dev/null || rc=9
|
||||
./v2rayN.WebAPI --help >/dev/null || rc=9
|
||||
V2RAYN_WEB_API_KEY=ci-smoke-key-0123456789abcdef ASPNETCORE_URLS=http://127.0.0.1:5080 ./v2rayN.WebAPI --foreground --no-open > /tmp/smoke.log 2>&1 &
|
||||
APP=$!
|
||||
echo "$APP" > /tmp/smoke.pid
|
||||
ok=0
|
||||
for i in $(seq 1 90); do
|
||||
if curl --noproxy '*' -fsS http://127.0.0.1:5080/api/health >/dev/null 2>&1; then ok=1; break; fi
|
||||
if ! kill -0 "$APP" 2>/dev/null; then break; fi
|
||||
sleep 1
|
||||
done
|
||||
if [ "$ok" != 1 ]; then
|
||||
echo 'loongarch64 API smoke did not become healthy'
|
||||
tail -n 60 /tmp/smoke.log
|
||||
kill -KILL "$APP" 2>/dev/null || true
|
||||
rc=9
|
||||
fi
|
||||
if [ "$rc" = 0 ]; then
|
||||
kill -TERM "$APP"
|
||||
exited=0
|
||||
for i in $(seq 1 60); do
|
||||
if ! kill -0 "$APP" 2>/dev/null; then exited=1; break; fi
|
||||
sleep 1
|
||||
done
|
||||
if [ "$exited" != 1 ]; then
|
||||
echo 'loongarch64 API smoke did not exit within 60s after SIGTERM'
|
||||
tail -n 60 /tmp/smoke.log
|
||||
kill -KILL "$APP" 2>/dev/null || true
|
||||
rc=9
|
||||
else
|
||||
wait "$APP"
|
||||
app_rc=$?
|
||||
if [ "$app_rc" != 0 ]; then
|
||||
echo "loongarch64 API smoke exited with status $app_rc after SIGTERM"
|
||||
tail -n 60 /tmp/smoke.log
|
||||
rc=9
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
if [ "$rc" != 0 ]; then echo '=== publish.log ==='; tail -n 120 /tmp/publish.log 2>/dev/null; fi
|
||||
echo "__WEBAPI_DONE__$rc"
|
||||
SCRIPT
|
||||
bash repo/v2rayN/v2rayN.WebAPI/Scripts/fetch-core-bundle.sh "$RID" "$GITHUB_WORKSPACE/core-bundle"
|
||||
ls -la "$GITHUB_WORKSPACE/core-bundle" | head -n 15
|
||||
- name: Build WebAPI loongarch64 in the VM (publish + native smoke)
|
||||
shell: bash
|
||||
timeout-minutes: 180
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p "$GITHUB_WORKSPACE/dist"
|
||||
export VIRTIOFSD_SOCKET="$RUNNER_TEMP/virtiofs-webapi-loongarch64.sock"
|
||||
rm -f "$VIRTIOFSD_SOCKET"
|
||||
|
||||
sudo virtiofsd --socket-path="$VIRTIOFSD_SOCKET" --shared-dir="$GITHUB_WORKSPACE" --cache=auto &
|
||||
|
||||
VIRTIOFSD_PID=$!
|
||||
trap 'sudo kill "$VIRTIOFSD_PID" 2>/dev/null || true; rm -f "$VIRTIOFSD_SOCKET"' EXIT
|
||||
|
||||
for i in {1..100}; do
|
||||
[[ -S "$VIRTIOFSD_SOCKET" ]] && break
|
||||
sleep 0.1
|
||||
done
|
||||
[[ -S "$VIRTIOFSD_SOCKET" ]] || { echo "virtiofsd socket not created"; exit 1; }
|
||||
sudo chown "$USER:$(id -gn)" "$VIRTIOFSD_SOCKET" || true
|
||||
sudo chmod 660 "$VIRTIOFSD_SOCKET" || true
|
||||
expect <<'EOF'
|
||||
log_user 1
|
||||
set timeout 10800
|
||||
set qemu_bin "$env(HOME)/qemu-install/bin/qemu-system-loongarch64"
|
||||
set qemu_rom_dir "$env(HOME)/qemu-install/share/qemu"
|
||||
set virtiofs_socket $env(VIRTIOFSD_SOCKET)
|
||||
set qcow2 $env(QCOW2_IMAGE)
|
||||
set efi_code $env(EFI_CODE)
|
||||
set efi_vars $env(EFI_VARS)
|
||||
proc wait_prompt {} {
|
||||
expect {
|
||||
-re "__CI_PROMPT__# " {}
|
||||
timeout { exit 1 }
|
||||
eof { exit 1 }
|
||||
}
|
||||
}
|
||||
proc run_cmd {cmd} {
|
||||
send -- "$cmd\r"
|
||||
wait_prompt
|
||||
}
|
||||
spawn $qemu_bin \
|
||||
-L $qemu_rom_dir \
|
||||
-machine virt \
|
||||
-accel tcg,thread=multi,tb-size=2048 \
|
||||
-cpu la464 \
|
||||
-m 9216 \
|
||||
-object memory-backend-memfd,id=mem0,size=9216M,share=on \
|
||||
-numa node,memdev=mem0 \
|
||||
-smp 4 \
|
||||
-drive if=pflash,format=raw,unit=0,file=$efi_code,readonly=on \
|
||||
-drive if=pflash,format=raw,unit=1,file=$efi_vars \
|
||||
-device virtio-blk-pci,drive=hd0,bootindex=1 \
|
||||
-drive if=none,media=disk,id=hd0,file=$qcow2,format=qcow2 \
|
||||
-netdev user,id=net0 \
|
||||
-device virtio-net-pci-non-transitional,netdev=net0 \
|
||||
-chardev socket,id=charfs0,path=$virtiofs_socket \
|
||||
-device vhost-user-fs-pci,chardev=charfs0,tag=workspace \
|
||||
-display none \
|
||||
-serial stdio \
|
||||
-monitor none
|
||||
expect -re "login:|debian login:"
|
||||
send -- "root\r"
|
||||
expect -re "Password:|密码:|密码:"
|
||||
send -- "password\r"
|
||||
expect {
|
||||
-re "# " {}
|
||||
timeout { exit 1 }
|
||||
eof { exit 1 }
|
||||
}
|
||||
send -- "export TERM=dumb; export PS1='__CI_PROMPT__# '\r"
|
||||
wait_prompt
|
||||
run_cmd "mkdir -p /workspace"
|
||||
run_cmd "mount -t virtiofs workspace /workspace"
|
||||
run_cmd "IFACE=\$(ip -o link show | awk -F': ' '\$2 != \"lo\" {print \$2; exit}') ; ip link set \$IFACE up || true"
|
||||
run_cmd "dhclient \$IFACE || true"
|
||||
run_cmd "printf 'nameserver 10.0.2.3\nnameserver 1.1.1.1\n' >/etc/resolv.conf"
|
||||
run_cmd "apt-get update || apt-get update || apt-get update"
|
||||
run_cmd "DEBIAN_FRONTEND=noninteractive apt-get install -y sudo git rsync findutils tar gzip unzip which curl jq wget file ca-certificates python3 libc6-dev libgcc-s1 libstdc++6 zlib1g libatomic1 libicu76 || DEBIAN_FRONTEND=noninteractive apt-get install -y sudo git rsync findutils tar gzip unzip which curl jq wget file ca-certificates python3 libc6-dev libgcc-s1 libstdc++6 zlib1g libatomic1"
|
||||
run_cmd "rm -rf /root/v2rayN-src"
|
||||
run_cmd "rsync -a /workspace/repo/ /root/v2rayN-src/"
|
||||
run_cmd "cd /root/v2rayN-src && git config --global --add safe.directory /root/v2rayN-src"
|
||||
send -- "bash /workspace/webapi-loong-run.sh\r"
|
||||
expect {
|
||||
-re "__WEBAPI_DONE__0" { send -- "poweroff\r" }
|
||||
-re "__WEBAPI_DONE__" { exit 1 }
|
||||
timeout { exit 1 }
|
||||
eof { exit 1 }
|
||||
}
|
||||
EOF
|
||||
- name: Verify the loongarch64 publish tree
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
publish="$GITHUB_WORKSPACE/publish/linux-loongarch64"
|
||||
test -x "$publish/v2rayN.WebAPI"
|
||||
test -f "$publish/v2rayN.WebAPI.build.json"
|
||||
unreadable="$(find "$publish" ! -readable -print -quit)"
|
||||
if [ -n "$unreadable" ]; then
|
||||
echo "The loongarch64 publish tree contains unreadable entries:"
|
||||
find "$publish" ! -readable -print | head -n 20
|
||||
exit 1
|
||||
fi
|
||||
echo "loongarch64 publish tree is complete and readable"
|
||||
- name: Package API install and self-update ZIPs
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python3 repo/v2rayN/v2rayN.WebAPI/Scripts/web-release-assets.py package --rid "$RID" --publish "$GITHUB_WORKSPACE/publish/linux-loongarch64" --dist "$GITHUB_WORKSPACE/dist"
|
||||
ls -la "$GITHUB_WORKSPACE/dist"
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: v2rayN.WebAPI-linux-loongarch64
|
||||
path: dist/*.zip
|
||||
if-no-files-found: error
|
||||
|
||||
container:
|
||||
name: API Containerfile (${{ matrix.rid }})
|
||||
needs: identity
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# Development builds keep amd64 coverage; releases also validate ARM64.
|
||||
include: >-
|
||||
${{ fromJSON(needs.identity.outputs.release == 'true'
|
||||
&& '[{"rid":"linux-x64","platform":"linux/amd64"},{"rid":"linux-arm64","platform":"linux/arm64"}]'
|
||||
|| '[{"rid":"linux-x64","platform":"linux/amd64"}]') }}
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: docker/setup-buildx-action@v4
|
||||
- name: Build API container (no push)
|
||||
uses: docker/build-push-action@v7
|
||||
env:
|
||||
DOCKER_BUILD_RECORD_UPLOAD: false
|
||||
with:
|
||||
context: .
|
||||
file: ./v2rayN/v2rayN.WebAPI/Containerfile
|
||||
platforms: ${{ matrix.platform }}
|
||||
tags: v2rayn-webapi:ci
|
||||
build-args: |
|
||||
RID=${{ matrix.rid }}
|
||||
WEB_VERSION=${{ needs.identity.outputs.version }}
|
||||
WEB_REPOSITORY=${{ github.repository }}
|
||||
WEB_COMMIT=${{ github.sha }}
|
||||
WEB_BUILD_DATE=${{ needs.identity.outputs.build_date }}
|
||||
push: false
|
||||
|
||||
release-assets:
|
||||
name: Assemble API release assets
|
||||
if: needs.identity.outputs.release == 'true'
|
||||
needs: [identity, verify, windows, windows-arm64, native, native-riscv64, native-loong64, container]
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: v2rayN.WebAPI-*
|
||||
path: release-assets
|
||||
merge-multiple: true
|
||||
- name: Build and verify WebAPI-update.json
|
||||
env:
|
||||
RELEASE_TAG: ${{ needs.identity.outputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python3 v2rayN/v2rayN.WebAPI/Scripts/web-update-manifest.py write \
|
||||
--version "$RELEASE_TAG" \
|
||||
--repository "${{ github.repository }}" \
|
||||
--commit "${{ github.sha }}" \
|
||||
--build-date "${{ needs.identity.outputs.build_date }}" \
|
||||
--dist release-assets \
|
||||
--output release-assets/WebAPI-update.json
|
||||
python3 v2rayN/v2rayN.WebAPI/Scripts/web-update-manifest.py verify \
|
||||
--manifest release-assets/WebAPI-update.json \
|
||||
--dist release-assets \
|
||||
--repository "${{ github.repository }}"
|
||||
- name: Smoke-test full API ZIP
|
||||
run: bash v2rayN/v2rayN.WebAPI/Tests/Integration/test-full-zip-runtime.sh release-assets/v2rayN-linux-64-WebAPI.zip
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: WebAPI-release-packages
|
||||
path: release-assets/*.zip
|
||||
if-no-files-found: error
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: WebAPI-release-manifest
|
||||
path: release-assets/WebAPI-update.json
|
||||
if-no-files-found: error
|
||||
@@ -20,6 +20,13 @@ jobs:
|
||||
secrets:
|
||||
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
|
||||
release-WebAPI:
|
||||
if: inputs.release_tag != ''
|
||||
uses: ./.github/workflows/release-WebAPI.yml
|
||||
with:
|
||||
release_tag: ${{ inputs.release_tag }}
|
||||
secrets: inherit
|
||||
|
||||
update:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
@@ -29,6 +29,22 @@ jobs:
|
||||
release_tag: ${{ inputs.release_tag }}
|
||||
secrets: inherit
|
||||
|
||||
build-aot:
|
||||
uses: ./.github/workflows/build.yml
|
||||
with:
|
||||
target: linux
|
||||
isAot: true
|
||||
|
||||
release-aot-zip:
|
||||
if: inputs.release_tag != ''
|
||||
needs: build-aot
|
||||
uses: ./.github/workflows/package-zip.yml
|
||||
with:
|
||||
target: linux
|
||||
release_tag: ${{ inputs.release_tag }}
|
||||
isAot: true
|
||||
secrets: inherit
|
||||
|
||||
deb:
|
||||
name: build and release deb x64 & arm64
|
||||
if: (github.event_name == 'workflow_dispatch' && inputs.release_tag != '') || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
|
||||
@@ -205,149 +221,45 @@ jobs:
|
||||
deb-loong64:
|
||||
name: build and release deb loong64
|
||||
if: (github.event_name == 'workflow_dispatch' && inputs.release_tag != '') || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
|
||||
runs-on: ubuntu-26.04-arm
|
||||
runs-on: ubuntu-26.04
|
||||
env:
|
||||
RELEASE_TAG: ${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}
|
||||
QCOW2_URL: https://github.com/xujiegb/debian-loong64-qcow2/releases/download/13.5/debian13-loong64.qcow2
|
||||
EFI_CODE_URL: https://github.com/xujiegb/debian-loong64-qcow2/releases/download/13.5/edk2-loongarch64-code.fd
|
||||
EFI_VARS_URL: https://github.com/xujiegb/debian-loong64-qcow2/releases/download/13.5/edk2-loongarch64-vars.fd
|
||||
QCOW2_IMAGE: debian13-loong64.qcow2
|
||||
EFI_CODE: edk2-loongarch64-code.fd
|
||||
EFI_VARS: edk2-loongarch64-vars.fd
|
||||
QEMU_VERSION: 10.2.4
|
||||
DOCKER_IMAGE: ghcr.io/loong64/debian:trixie
|
||||
|
||||
steps:
|
||||
- name: Prepare host tools
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y git rsync qemu-utils qemu-system-common expect wget curl ca-certificates libfdt1 libglib2.0-0 libpixman-1-0 libslirp0 virtiofsd
|
||||
sudo ln -sf /usr/libexec/virtiofsd /usr/local/bin/virtiofsd
|
||||
virtiofsd --version || virtiofsd --help | head
|
||||
- name: Host clone repo
|
||||
shell: bash
|
||||
env: { GITHUB_TOKEN: "${{ github.token }}" }
|
||||
run: |
|
||||
set -euo pipefail
|
||||
rm -rf repo
|
||||
git clone --no-single-branch --tags "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" repo
|
||||
- name: Download QEMU prebuild
|
||||
run: |
|
||||
set -euo pipefail
|
||||
wget -O qemu-linux-arm64-v8.2.tar.gz "https://github.com/xujiegb/qemu-linux-prebuild/releases/download/${QEMU_VERSION}/qemu-linux-arm64-v8.2.tar.gz"
|
||||
tar -xzf qemu-linux-arm64-v8.2.tar.gz
|
||||
mkdir -p "$HOME/qemu-install"
|
||||
rsync -a qemu-linux-arm64-v8.2/ "$HOME/qemu-install/"
|
||||
"$HOME/qemu-install/bin/qemu-system-loongarch64" --version
|
||||
- name: Download loong64 qcow2 and EFI firmware
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
wget -O "$QCOW2_IMAGE" "$QCOW2_URL"
|
||||
wget -O "$EFI_CODE" "$EFI_CODE_URL"
|
||||
wget -O "$EFI_VARS" "$EFI_VARS_URL"
|
||||
qemu-img info "$QCOW2_IMAGE"
|
||||
- name: Build loong64 DEB in VM through serial console
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
- name: Setup QEMU
|
||||
uses: docker/setup-qemu-action@v4
|
||||
- name: Package DEB (Debian-family)
|
||||
shell: bash
|
||||
timeout-minutes: 180
|
||||
env: { RELEASE_TAG: "${{ env.RELEASE_TAG }}" }
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p "$GITHUB_WORKSPACE/dist/deb-loong64"
|
||||
|
||||
export VIRTIOFSD_SOCKET="$RUNNER_TEMP/virtiofs-deb-loong64.sock"
|
||||
rm -f "$VIRTIOFSD_SOCKET"
|
||||
|
||||
sudo virtiofsd --socket-path="$VIRTIOFSD_SOCKET" --shared-dir="$GITHUB_WORKSPACE" --cache=auto &
|
||||
|
||||
VIRTIOFSD_PID=$!
|
||||
trap 'sudo kill "$VIRTIOFSD_PID" 2>/dev/null || true; rm -f "$VIRTIOFSD_SOCKET"' EXIT
|
||||
|
||||
for i in {1..100}; do
|
||||
[[ -S "$VIRTIOFSD_SOCKET" ]] && break
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
[[ -S "$VIRTIOFSD_SOCKET" ]] || { echo "virtiofsd socket not created"; exit 1; }
|
||||
sudo chown "$USER:$(id -gn)" "$VIRTIOFSD_SOCKET" || true
|
||||
sudo chmod 660 "$VIRTIOFSD_SOCKET" || true
|
||||
expect <<'EOF'
|
||||
log_user 1
|
||||
set timeout -1
|
||||
set release_tag $env(RELEASE_TAG)
|
||||
set qemu_bin "$env(HOME)/qemu-install/bin/qemu-system-loongarch64"
|
||||
set qemu_rom_dir "$env(HOME)/qemu-install/share/qemu"
|
||||
set virtiofs_socket $env(VIRTIOFSD_SOCKET)
|
||||
set qcow2 $env(QCOW2_IMAGE)
|
||||
set efi_code $env(EFI_CODE)
|
||||
set efi_vars $env(EFI_VARS)
|
||||
proc wait_prompt {} {
|
||||
expect {
|
||||
-re "__CI_PROMPT__# " {}
|
||||
timeout { exit 1 }
|
||||
eof { exit 1 }
|
||||
}
|
||||
}
|
||||
proc run_cmd {cmd} {
|
||||
send -- "$cmd\r"
|
||||
wait_prompt
|
||||
}
|
||||
spawn $qemu_bin \
|
||||
-L $qemu_rom_dir \
|
||||
-machine virt \
|
||||
-accel tcg,thread=multi,tb-size=2048 \
|
||||
-cpu la464 \
|
||||
-m 9216 \
|
||||
-object memory-backend-memfd,id=mem0,size=9216M,share=on \
|
||||
-numa node,memdev=mem0 \
|
||||
-smp 4 \
|
||||
-drive if=pflash,format=raw,unit=0,file=$efi_code,readonly=on \
|
||||
-drive if=pflash,format=raw,unit=1,file=$efi_vars \
|
||||
-device virtio-blk-pci,drive=hd0,bootindex=1 \
|
||||
-drive if=none,media=disk,id=hd0,file=$qcow2,format=qcow2 \
|
||||
-netdev user,id=net0 \
|
||||
-device virtio-net-pci-non-transitional,netdev=net0 \
|
||||
-chardev socket,id=charfs0,path=$virtiofs_socket \
|
||||
-device vhost-user-fs-pci,chardev=charfs0,tag=workspace \
|
||||
-display none \
|
||||
-serial stdio \
|
||||
-monitor none
|
||||
expect -re "login:|debian login:"
|
||||
send -- "root\r"
|
||||
expect -re "Password:|密码:|密码:"
|
||||
send -- "password\r"
|
||||
expect {
|
||||
-re "# " {}
|
||||
timeout { exit 1 }
|
||||
eof { exit 1 }
|
||||
}
|
||||
send -- "export TERM=dumb; export PS1='__CI_PROMPT__# '\r"
|
||||
wait_prompt
|
||||
run_cmd "mkdir -p /workspace"
|
||||
run_cmd "mount -t virtiofs workspace /workspace"
|
||||
run_cmd "IFACE=\$(ip -o link show | awk -F': ' '\$2 != \"lo\" {print \$2; exit}') ; ip link set \$IFACE up || true"
|
||||
run_cmd "dhclient \$IFACE || true"
|
||||
run_cmd "printf 'nameserver 10.0.2.3\nnameserver 1.1.1.1\n' >/etc/resolv.conf"
|
||||
run_cmd "apt-get update || apt-get update || apt-get update"
|
||||
run_cmd "DEBIAN_FRONTEND=noninteractive apt-get install -y sudo git rsync findutils tar gzip unzip which curl jq wget file ca-certificates desktop-file-utils xdg-utils fakeroot dpkg-dev gcc make libc6-dev libgcc-s1 libstdc++6 zlib1g libatomic1 fuse3"
|
||||
run_cmd "rm -rf /root/v2rayN-src"
|
||||
run_cmd "rsync -a --info=progress2 /workspace/repo/ /root/v2rayN-src/"
|
||||
run_cmd "cd /root/v2rayN-src && git config --global --add safe.directory /root/v2rayN-src"
|
||||
run_cmd "cd /root/v2rayN-src && chmod 755 package-debian-loong.sh"
|
||||
send -- "cd /root/v2rayN-src; cat >/tmp/run-loong-build.sh <<'SCRIPT'\nset +e\nset -o pipefail\nbash -x ./package-debian-loong.sh \"\$RELEASE_TAG\" 2>&1 | tee /tmp/build.log\nrc=\$?\nmkdir -p /workspace/dist/deb-loong64\ncp -av /root/debbuild/*.deb /workspace/dist/deb-loong64/ 2>/dev/null || true\necho __BUILD_DONE__\$rc\nSCRIPT\nRELEASE_TAG=\"$release_tag\" bash /tmp/run-loong-build.sh\r"
|
||||
expect {
|
||||
-re "__BUILD_DONE__0" { send -- "poweroff\r" }
|
||||
default { exit 1 }
|
||||
}
|
||||
EOF
|
||||
- name: Collect DEBs
|
||||
shell: bash
|
||||
docker run --rm \
|
||||
--privileged \
|
||||
--platform linux/loong64 \
|
||||
--env DEBIAN_FRONTEND=noninteractive \
|
||||
--env DOTNET_EnableWriteXorExecute=0 \
|
||||
--volume "${{ github.workspace }}:/root/v2rayN-src" \
|
||||
--volume "$HOME/debbuild:/root/debbuild" \
|
||||
--workdir /root/v2rayN-src \
|
||||
"$DOCKER_IMAGE" bash -c "
|
||||
apt-get update && \
|
||||
apt-get install -y sudo git rsync findutils tar gzip unzip which curl jq wget file ca-certificates desktop-file-utils xdg-utils fakeroot dpkg-dev gcc make libc6-dev libgcc-s1 libstdc++6 zlib1g libatomic1 && \
|
||||
git config --global --add safe.directory /root/v2rayN-src && \
|
||||
./package-debian-loong.sh ${RELEASE_TAG}"
|
||||
- name: Collect DEBs into workspace
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p "$GITHUB_WORKSPACE/dist/deb-loong64"
|
||||
rsync -av "$HOME/debbuild/" "$GITHUB_WORKSPACE/dist/deb-loong64/" || true
|
||||
find "$GITHUB_WORKSPACE/dist/deb-loong64" -name "*.deb" -exec mv {} "$GITHUB_WORKSPACE/dist/deb-loong64/v2rayN-linux-loong64.deb" \; || true
|
||||
echo "==== Dist tree ===="
|
||||
ls -R "$GITHUB_WORKSPACE/dist/deb-loong64"
|
||||
ls -R "$GITHUB_WORKSPACE/dist/deb-loong64" || true
|
||||
- name: Upload DEB artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with: { name: release-deb-loong64, path: "dist/deb-loong64/**/*.deb", if-no-files-found: error }
|
||||
@@ -355,149 +267,43 @@ jobs:
|
||||
rpm-loong64:
|
||||
name: build and release rpm loong64
|
||||
if: (github.event_name == 'workflow_dispatch' && inputs.release_tag != '') || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
|
||||
runs-on: ubuntu-26.04-arm
|
||||
runs-on: ubuntu-26.04
|
||||
env:
|
||||
RELEASE_TAG: ${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}
|
||||
QCOW2_URL: https://github.com/xujiegb/fedora-loong64-qcow2/releases/download/43/fedora43-loong64.qcow2
|
||||
EFI_CODE_URL: https://github.com/xujiegb/fedora-loong64-qcow2/releases/download/43/edk2-loongarch64-code.fd
|
||||
EFI_VARS_URL: https://github.com/xujiegb/fedora-loong64-qcow2/releases/download/43/edk2-loongarch64-vars.fd
|
||||
QCOW2_IMAGE: fedora43-loong64.qcow2
|
||||
EFI_CODE: edk2-loongarch64-code.fd
|
||||
EFI_VARS: edk2-loongarch64-vars.fd
|
||||
QEMU_VERSION: 10.2.4
|
||||
DOCKER_IMAGE: ghcr.io/fedora-remix-loongarch/mini-image:f43
|
||||
steps:
|
||||
- name: Prepare host tools
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y git rsync qemu-utils qemu-system-common expect wget curl ca-certificates libfdt1 libglib2.0-0 libpixman-1-0 libslirp0 virtiofsd
|
||||
sudo ln -sf /usr/libexec/virtiofsd /usr/local/bin/virtiofsd
|
||||
virtiofsd --version || virtiofsd --help | head
|
||||
- name: Host clone repo
|
||||
shell: bash
|
||||
env: { GITHUB_TOKEN: "${{ github.token }}" }
|
||||
run: |
|
||||
set -euo pipefail
|
||||
rm -rf repo
|
||||
git clone --no-single-branch --tags "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" repo
|
||||
- name: Download QEMU prebuild
|
||||
run: |
|
||||
set -euo pipefail
|
||||
wget -O qemu-linux-arm64-v8.2.tar.gz "https://github.com/xujiegb/qemu-linux-prebuild/releases/download/${QEMU_VERSION}/qemu-linux-arm64-v8.2.tar.gz"
|
||||
tar -xzf qemu-linux-arm64-v8.2.tar.gz
|
||||
mkdir -p "$HOME/qemu-install"
|
||||
rsync -a qemu-linux-arm64-v8.2/ "$HOME/qemu-install/"
|
||||
"$HOME/qemu-install/bin/qemu-system-loongarch64" --version
|
||||
- name: Download loong64 qcow2 and EFI firmware
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
wget -O "$QCOW2_IMAGE" "$QCOW2_URL"
|
||||
wget -O "$EFI_CODE" "$EFI_CODE_URL"
|
||||
wget -O "$EFI_VARS" "$EFI_VARS_URL"
|
||||
qemu-img info "$QCOW2_IMAGE"
|
||||
- name: Build loong64 RPM in VM through serial console
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
- name: Setup QEMU
|
||||
uses: docker/setup-qemu-action@v4
|
||||
- name: Build loong64 RPM
|
||||
shell: bash
|
||||
timeout-minutes: 180
|
||||
env: { RELEASE_TAG: "${{ env.RELEASE_TAG }}" }
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p "$GITHUB_WORKSPACE/dist/rpm-loong64"
|
||||
|
||||
export VIRTIOFSD_SOCKET="$RUNNER_TEMP/virtiofs-rpm-loong64.sock"
|
||||
rm -f "$VIRTIOFSD_SOCKET"
|
||||
|
||||
sudo virtiofsd --socket-path="$VIRTIOFSD_SOCKET" --shared-dir="$GITHUB_WORKSPACE" --cache=auto &
|
||||
|
||||
VIRTIOFSD_PID=$!
|
||||
trap 'sudo kill "$VIRTIOFSD_PID" 2>/dev/null || true; rm -f "$VIRTIOFSD_SOCKET"' EXIT
|
||||
|
||||
for i in {1..100}; do
|
||||
[[ -S "$VIRTIOFSD_SOCKET" ]] && break
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
[[ -S "$VIRTIOFSD_SOCKET" ]] || { echo "virtiofsd socket not created"; exit 1; }
|
||||
sudo chown "$USER:$(id -gn)" "$VIRTIOFSD_SOCKET" || true
|
||||
sudo chmod 660 "$VIRTIOFSD_SOCKET" || true
|
||||
expect <<'EOF'
|
||||
log_user 1
|
||||
set timeout -1
|
||||
set release_tag $env(RELEASE_TAG)
|
||||
set qemu_bin "$env(HOME)/qemu-install/bin/qemu-system-loongarch64"
|
||||
set qemu_rom_dir "$env(HOME)/qemu-install/share/qemu"
|
||||
set virtiofs_socket $env(VIRTIOFSD_SOCKET)
|
||||
set qcow2 $env(QCOW2_IMAGE)
|
||||
set efi_code $env(EFI_CODE)
|
||||
set efi_vars $env(EFI_VARS)
|
||||
proc wait_prompt {} {
|
||||
expect {
|
||||
-re "__CI_PROMPT__# " {}
|
||||
timeout { exit 1 }
|
||||
eof { exit 1 }
|
||||
}
|
||||
}
|
||||
proc run_cmd {cmd} {
|
||||
send -- "$cmd\r"
|
||||
wait_prompt
|
||||
}
|
||||
spawn $qemu_bin \
|
||||
-L $qemu_rom_dir \
|
||||
-machine virt \
|
||||
-accel tcg,thread=multi,tb-size=2048 \
|
||||
-cpu la464 \
|
||||
-m 9216 \
|
||||
-object memory-backend-memfd,id=mem0,size=9216M,share=on \
|
||||
-numa node,memdev=mem0 \
|
||||
-smp 4 \
|
||||
-drive if=pflash,format=raw,unit=0,file=$efi_code,readonly=on \
|
||||
-drive if=pflash,format=raw,unit=1,file=$efi_vars \
|
||||
-device virtio-blk-pci,drive=hd0,bootindex=1 \
|
||||
-drive if=none,media=disk,id=hd0,file=$qcow2,format=qcow2 \
|
||||
-netdev user,id=net0 \
|
||||
-device virtio-net-pci-non-transitional,netdev=net0 \
|
||||
-chardev socket,id=charfs0,path=$virtiofs_socket \
|
||||
-device vhost-user-fs-pci,chardev=charfs0,tag=workspace \
|
||||
-display none \
|
||||
-serial stdio \
|
||||
-monitor none
|
||||
expect -re "login:|fedora login:"
|
||||
send -- "root\r"
|
||||
expect -re "Password:|密码:|密码:"
|
||||
send -- "password\r"
|
||||
expect {
|
||||
-re "# " {}
|
||||
timeout { exit 1 }
|
||||
eof { exit 1 }
|
||||
}
|
||||
send -- "export TERM=dumb; export PS1='__CI_PROMPT__# '\r"
|
||||
wait_prompt
|
||||
run_cmd "mkdir -p /workspace"
|
||||
run_cmd "mount -t virtiofs workspace /workspace"
|
||||
run_cmd "IFACE=\$(ip -o link show | awk -F': ' '\$2 != \"lo\" {print \$2; exit}') ; ip link set \$IFACE up || true"
|
||||
run_cmd "dhclient \$IFACE || true"
|
||||
run_cmd "printf 'nameserver 10.0.2.3\nnameserver 1.1.1.1\n' >/etc/resolv.conf"
|
||||
run_cmd "dnf -y makecache || dnf -y makecache || dnf -y makecache"
|
||||
run_cmd "dnf -y install sudo git rpm-build rpmdevtools dnf-plugins-core rsync findutils tar gzip unzip which curl jq file ca-certificates fuse3 --nogpgcheck"
|
||||
run_cmd "rm -rf /root/v2rayN-src"
|
||||
run_cmd "rsync -a --info=progress2 /workspace/repo/ /root/v2rayN-src/"
|
||||
run_cmd "cd /root/v2rayN-src && git config --global --add safe.directory /root/v2rayN-src"
|
||||
run_cmd "cd /root/v2rayN-src && chmod 755 package-rhel-loong.sh"
|
||||
send -- "cd /root/v2rayN-src; cat >/tmp/run-loong-rpm-build.sh <<'SCRIPT'\nset +e\nset -o pipefail\nbash -x ./package-rhel-loong.sh \"\$RELEASE_TAG\" 2>&1 | tee /tmp/build.log\nrc=\$?\nmkdir -p /workspace/dist/rpm-loong64\nfind /root/rpmbuild/RPMS -name '*.rpm' -exec cp -av {} /workspace/dist/rpm-loong64/ \\; 2>/dev/null || true\necho __BUILD_DONE__\$rc\nSCRIPT\nRELEASE_TAG=\"$release_tag\" bash /tmp/run-loong-rpm-build.sh\r"
|
||||
expect {
|
||||
-re "__BUILD_DONE__0" { send -- "poweroff\r" }
|
||||
default { exit 1 }
|
||||
}
|
||||
EOF
|
||||
- name: Collect RPMs
|
||||
shell: bash
|
||||
docker run --rm \
|
||||
--privileged \
|
||||
--platform linux/loong64 \
|
||||
--env DOTNET_EnableWriteXorExecute=0 \
|
||||
--volume "${{ github.workspace }}:/root/v2rayN-src" \
|
||||
--volume "$HOME/rpmbuild/RPMS:/root/rpmbuild/RPMS" \
|
||||
--workdir /root/v2rayN-src \
|
||||
"$DOCKER_IMAGE" bash -c "
|
||||
dnf -y makecache && \
|
||||
dnf -y install sudo git rpm-build rpmdevtools dnf-plugins-core rsync findutils tar gzip unzip which curl jq file ca-certificates fuse3 && \
|
||||
git config --global --add safe.directory /root/v2rayN-src && \
|
||||
./package-rhel-loong.sh ${RELEASE_TAG}"
|
||||
- name: Collect RPMs into workspace
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p "$GITHUB_WORKSPACE/dist/rpm-loong64"
|
||||
rsync -av "$HOME/rpmbuild/RPMS/" "$GITHUB_WORKSPACE/dist/rpm-loong64/" || true
|
||||
find "$GITHUB_WORKSPACE/dist/rpm-loong64" -name "*.loongarch64.rpm" -exec mv {} "$GITHUB_WORKSPACE/dist/rpm-loong64/v2rayN-linux-rhel-loong64.rpm" \; || true
|
||||
echo "==== Dist tree ===="
|
||||
ls -R "$GITHUB_WORKSPACE/dist/rpm-loong64"
|
||||
ls -R "$GITHUB_WORKSPACE/dist/rpm-loong64" || true
|
||||
- name: Upload RPM artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with: { name: release-rpm-loong64, path: "dist/rpm-loong64/**/*.rpm", if-no-files-found: error }
|
||||
|
||||
@@ -28,7 +28,23 @@ jobs:
|
||||
target: macos
|
||||
release_tag: ${{ inputs.release_tag }}
|
||||
secrets: inherit
|
||||
|
||||
|
||||
build-aot:
|
||||
uses: ./.github/workflows/build.yml
|
||||
with:
|
||||
target: macos
|
||||
isAot: true
|
||||
|
||||
release-aot-zip:
|
||||
if: inputs.release_tag != ''
|
||||
needs: build-aot
|
||||
uses: ./.github/workflows/package-zip.yml
|
||||
with:
|
||||
target: macos
|
||||
release_tag: ${{ inputs.release_tag }}
|
||||
isAot: true
|
||||
secrets: inherit
|
||||
|
||||
dmg:
|
||||
name: package and release macOS dmg
|
||||
if: inputs.release_tag != ''
|
||||
|
||||
@@ -28,3 +28,19 @@ jobs:
|
||||
target: windows-desktop
|
||||
release_tag: ${{ inputs.release_tag }}
|
||||
secrets: inherit
|
||||
|
||||
build-aot:
|
||||
uses: ./.github/workflows/build.yml
|
||||
with:
|
||||
target: windows
|
||||
isAot: true
|
||||
|
||||
release-aot-zip:
|
||||
if: inputs.release_tag != ''
|
||||
needs: build-aot
|
||||
uses: ./.github/workflows/package-zip.yml
|
||||
with:
|
||||
target: windows-desktop
|
||||
release_tag: ${{ inputs.release_tag }}
|
||||
isAot: true
|
||||
secrets: inherit
|
||||
|
||||
+54
-10
@@ -10,6 +10,10 @@ on:
|
||||
required: false
|
||||
type: string
|
||||
default: './v2rayN.Desktop/v2rayN.Desktop.csproj'
|
||||
isAot:
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -27,7 +31,6 @@ jobs:
|
||||
)
|
||||
}}
|
||||
env:
|
||||
Output: "${{ github.workspace }}/${{ matrix.arch }}"
|
||||
RID: |-
|
||||
${{
|
||||
case(
|
||||
@@ -41,6 +44,7 @@ jobs:
|
||||
${{
|
||||
case(
|
||||
inputs.target == 'windows', '-p:EnableWindowsTargeting=true',
|
||||
inputs.target == 'linux', (matrix.arch == 'arm64' && '-p:LinkerFlavor=lld -p:ObjCopyName=llvm-objcopy' || ''),
|
||||
''
|
||||
)
|
||||
}}
|
||||
@@ -71,25 +75,65 @@ jobs:
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v6
|
||||
with:
|
||||
dotnet-version: 10.0.1xx
|
||||
|
||||
- name: Build v2rayN
|
||||
dotnet-version: 10.0.x
|
||||
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
working-directory: ./v2rayN
|
||||
run: dotnet publish $Project -c Release -r $RID -p:SelfContained=true $ExtOpt -o $Output
|
||||
if: inputs.target == 'linux' && inputs.isAot
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y clang zlib1g-dev build-essential
|
||||
|
||||
- name: Install dependencies cross-arch
|
||||
shell: bash
|
||||
if: inputs.target == 'linux' && inputs.isAot && matrix.arch == 'arm64'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y lld llvm binutils-aarch64-linux-gnu gcc-aarch64-linux-gnu g++-aarch64-linux-gnu libc6-dev-arm64-cross
|
||||
|
||||
- name: Set AOT Environment Variable
|
||||
shell: bash
|
||||
run: |
|
||||
if [ "${{ inputs.isAot }}" = "true" ]; then
|
||||
echo "DOTNET_AOT_ARG=-p:IsAot=true" >> $GITHUB_ENV
|
||||
else
|
||||
echo "DOTNET_AOT_ARG=-p:IsAot=false" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Set OUTPUT Environment Variable
|
||||
shell: bash
|
||||
run: |
|
||||
RAW_PATH="${{ github.workspace }}/${{ matrix.arch }}${{ inputs.isAot && '-aot' || '' }}"
|
||||
echo "OUTPUT=${RAW_PATH//\\//}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Build AmazTool
|
||||
shell: bash
|
||||
working-directory: ./v2rayN
|
||||
run: dotnet publish ./AmazTool/AmazTool.csproj -c Release -r $RID -p:SelfContained=true -p:PublishTrimmed=true $ExtOpt -o $Output
|
||||
run: dotnet publish ./AmazTool/AmazTool.csproj -c Release -r $RID -p:SelfContained=true -p:PublishTrimmed=true $ExtOpt ${{ env.DOTNET_AOT_ARG }} -o ${{ env.OUTPUT }}
|
||||
|
||||
- name: Build v2rayN WPF
|
||||
shell: bash
|
||||
if: inputs.project == './v2rayN/v2rayN.csproj'
|
||||
working-directory: ./v2rayN
|
||||
run: dotnet publish $Project -c Release -r $RID -p:SelfContained=true $ExtOpt -o ${{ env.OUTPUT }}
|
||||
|
||||
- name: Build v2rayN Avalonia
|
||||
shell: bash
|
||||
if: inputs.project == './v2rayN.Desktop/v2rayN.Desktop.csproj'
|
||||
working-directory: ./v2rayN
|
||||
run: dotnet publish $Project -c Release -r $RID -p:SelfContained=true $ExtOpt ${{ env.DOTNET_AOT_ARG }} -o ${{ env.OUTPUT }}
|
||||
|
||||
- name: Remove Debug Symbols
|
||||
shell: bash
|
||||
run: |
|
||||
find ${{ matrix.arch }} -type f -name '*.pdb' -delete
|
||||
find ${{ env.OUTPUT }} -type f -name '*.pdb' -delete
|
||||
find ${{ env.OUTPUT }} -type f -name '*.debug' -delete
|
||||
find ${{ env.OUTPUT }} -type f -name '*.dbg' -delete
|
||||
find ${{ env.OUTPUT }} -type f -name '*.sym' -delete
|
||||
find ${{ env.OUTPUT }} -name '*.dSYM' -exec rm -rf {} +
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ matrix.arch }}
|
||||
path: ${{ matrix.arch }}
|
||||
name: ${{ matrix.arch }}${{ inputs.isAot && '-aot' || '' }}
|
||||
path: ${{ env.OUTPUT }}
|
||||
|
||||
@@ -9,6 +9,10 @@ on:
|
||||
target: # windows linux macos windows-desktop
|
||||
required: true
|
||||
type: string
|
||||
isAot:
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
secrets:
|
||||
GPG_PRIVATE_KEY:
|
||||
required: true
|
||||
@@ -43,27 +47,62 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Set package variables
|
||||
id: package-vars
|
||||
shell: bash
|
||||
env:
|
||||
INPUT_TARGET: ${{ inputs.target }}
|
||||
INPUT_IS_AOT: ${{ inputs.isAot }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
package_dir="v2rayN-${Target}-${Arch}"
|
||||
package_file="${package_dir}.zip"
|
||||
build_artifact_name="${{ matrix.arch }}"
|
||||
rename_suffix=""
|
||||
if [[ "$INPUT_IS_AOT" == "true" ]]; then
|
||||
build_artifact_name="${build_artifact_name}-aot"
|
||||
rename_suffix="-aot"
|
||||
fi
|
||||
if [[ "$INPUT_TARGET" == "windows-desktop" ]]; then
|
||||
rename_suffix="-desktop${rename_suffix}"
|
||||
fi
|
||||
final_package_file="${package_dir}${rename_suffix}.zip"
|
||||
|
||||
{
|
||||
echo "build_artifact_name=${build_artifact_name}"
|
||||
echo "package_dir=${package_dir}"
|
||||
echo "package_file=${package_file}"
|
||||
echo "final_package_file=${final_package_file}"
|
||||
echo "rename_suffix=${rename_suffix}"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Restore build artifacts
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: ${{ matrix.arch }}
|
||||
path: v2rayN-${{ env.Target }}-${{ env.Arch }}
|
||||
|
||||
name: ${{ steps.package-vars.outputs.build_artifact_name }}
|
||||
path: ${{ steps.package-vars.outputs.package_dir }}
|
||||
|
||||
- name: Get v2rayN-core-bin
|
||||
run: wget -nv -O v2rayN-$Target-$Arch.zip "https://github.com/2dust/v2rayN-core-bin/raw/refs/heads/master/v2rayN-$Target-$Arch.zip"
|
||||
|
||||
run: |
|
||||
wget -nv -O "${{ steps.package-vars.outputs.package_file }}" "https://github.com/2dust/v2rayN-core-bin/raw/refs/heads/master/v2rayN-$Target-$Arch.zip"
|
||||
|
||||
- name: Package zip archive
|
||||
run: 7z a -tZip v2rayN-$Target-$Arch.zip v2rayN-$Target-$Arch -mx1
|
||||
shell: bash
|
||||
run: |
|
||||
7z a -tZip "${{ steps.package-vars.outputs.package_file }}" "${{ steps.package-vars.outputs.package_dir }}" -mx1
|
||||
|
||||
- name: Rename package archive
|
||||
if: steps.package-vars.outputs.rename_suffix != ''
|
||||
shell: bash
|
||||
run: |
|
||||
mv "${{ steps.package-vars.outputs.package_file }}" \
|
||||
"${{ steps.package-vars.outputs.package_dir }}${{ steps.package-vars.outputs.rename_suffix }}.zip"
|
||||
|
||||
- name: Rename windows-desktop
|
||||
if: inputs.target == 'windows-desktop'
|
||||
run: mv "v2rayN-$Target-$Arch.zip" "v2rayN-$Target-$Arch-desktop.zip"
|
||||
|
||||
- name: Upload zip artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: release-${{ inputs.target }}-${{ matrix.arch }}
|
||||
path: v2rayN*.zip
|
||||
name: release-${{ inputs.target }}-${{ matrix.arch }}${{ inputs.isAot && '-aot' || '' }}
|
||||
path: ${{ steps.package-vars.outputs.final_package_file }}
|
||||
if-no-files-found: error
|
||||
|
||||
release:
|
||||
@@ -75,7 +114,7 @@ jobs:
|
||||
uses: ./.github/workflows/upload-sign.yml
|
||||
with:
|
||||
release_tag: ${{ inputs.release_tag }}
|
||||
artifact_name: release-${{ inputs.target }}-${{ matrix.arch }}
|
||||
artifact_name: release-${{ inputs.target }}-${{ matrix.arch }}${{ inputs.isAot && '-aot' || '' }}
|
||||
file_pattern: '*.zip'
|
||||
secrets:
|
||||
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
name: release v2rayN.WebAPI
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
release_tag:
|
||||
required: true
|
||||
type: string
|
||||
secrets:
|
||||
GPG_PRIVATE_KEY:
|
||||
required: false
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_tag:
|
||||
description: 'Release tag (for example 7.x.y).'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
uses: ./.github/workflows/build-WebAPI.yml
|
||||
permissions:
|
||||
contents: read
|
||||
with:
|
||||
release_tag: ${{ inputs.release_tag }}
|
||||
|
||||
release-web-packages:
|
||||
name: sign and upload Web API packages
|
||||
# Upload through the shared signing flow, exactly like the desktop packages.
|
||||
if: inputs.release_tag != ''
|
||||
needs: build
|
||||
uses: ./.github/workflows/upload-sign.yml
|
||||
with:
|
||||
release_tag: ${{ inputs.release_tag }}
|
||||
artifact_name: WebAPI-release-packages
|
||||
file_pattern: 'v2rayN-*-WebAPI*.zip'
|
||||
secrets: { GPG_PRIVATE_KEY: "${{ secrets.GPG_PRIVATE_KEY }}" }
|
||||
|
||||
release-web-manifest:
|
||||
name: sign and upload Web update manifest
|
||||
if: inputs.release_tag != ''
|
||||
# Advertise the update only after its packages and manifest have been uploaded.
|
||||
needs: [build, release-web-packages]
|
||||
uses: ./.github/workflows/upload-sign.yml
|
||||
with:
|
||||
release_tag: ${{ inputs.release_tag }}
|
||||
artifact_name: WebAPI-release-manifest
|
||||
file_pattern: 'WebAPI-update.json'
|
||||
secrets: { GPG_PRIVATE_KEY: "${{ secrets.GPG_PRIVATE_KEY }}" }
|
||||
@@ -0,0 +1,36 @@
|
||||
name: Update Certificate
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
update-cert:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v6
|
||||
with:
|
||||
dotnet-version: 10.0.x
|
||||
|
||||
- name: Update Certificate
|
||||
run: |
|
||||
dotnet run .github/scripts/UpdateCert.cs
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v8
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: "Update certificate"
|
||||
title: "Update certificate"
|
||||
body: "This pull request updates the certificate."
|
||||
branch: github-actions/update-certificate
|
||||
delete-branch: true
|
||||
@@ -19,7 +19,9 @@ jobs:
|
||||
$wingetPackage = "2dust.v2rayN"
|
||||
$gitToken = "${{ secrets.PT_WINGET }}"
|
||||
|
||||
$github = Invoke-RestMethod -uri "https://api.github.com/repos/2dust/v2rayN/releases"
|
||||
$headers = @{ Authorization = "Bearer $gitToken"; Accept = "application/vnd.github+json" }
|
||||
|
||||
$github = Invoke-RestMethod -Uri "https://api.github.com/repos/2dust/v2rayN/releases" -Headers $headers
|
||||
|
||||
$targetRelease = $github | Where-Object -Property prerelease -match 'False' | Select -First 1
|
||||
|
||||
|
||||
Regular → Executable
+2
@@ -220,6 +220,8 @@ apply_channel_or_keep() {
|
||||
VERSION="${VERSION#v}"
|
||||
return 0
|
||||
fi
|
||||
git remote add upstream https://github.com/2dust/v2rayN || true
|
||||
git fetch upstream
|
||||
|
||||
echo "[*] Resolving ${ch} tag from GitHub releases..."
|
||||
|
||||
|
||||
Regular → Executable
Regular → Executable
+9
@@ -676,6 +676,15 @@ EOF
|
||||
find "$stage/opt/v2rayN" -type f -exec chmod 0644 {} +
|
||||
[[ -f "$stage/opt/v2rayN/v2rayN" ]] && chmod 0755 "$stage/opt/v2rayN/v2rayN" || true
|
||||
|
||||
# Core binaries must be executable before installation: ordinary users cannot
|
||||
# chmod the root-owned files installed by dpkg.
|
||||
local core
|
||||
for core in xray/xray sing_box/sing-box mihomo/mihomo; do
|
||||
if [[ -f "$stage/opt/v2rayN/bin/$core" ]]; then
|
||||
chmod 0755 "$stage/opt/v2rayN/bin/$core"
|
||||
fi
|
||||
done
|
||||
|
||||
deb_out="$OUTPUT_DIR/v2rayn_${VERSION}_${deb_arch}.deb"
|
||||
dpkg-deb --root-owner-group --build "$stage" "$deb_out"
|
||||
|
||||
|
||||
Regular → Executable
+5
-3
@@ -73,12 +73,12 @@ detect_environment() {
|
||||
HOST_ARCH="$(uname -m)"
|
||||
|
||||
case "$OS_ID" in
|
||||
rhel|rocky|almalinux|fedora|centos)
|
||||
rhel|rocky|almalinux|fedora|centos|anolis)
|
||||
echo "Detected supported system: ${OS_NAME:-$OS_ID} ${OS_VERSION_ID:-}"
|
||||
;;
|
||||
*)
|
||||
die "Unsupported system: ${OS_NAME:-unknown} (${OS_ID:-unknown}).
|
||||
This script only supports: RHEL / Rocky / AlmaLinux / Fedora / CentOS."
|
||||
This script only supports: RHEL / Rocky / AlmaLinux / Fedora / CentOS / Anolis OS."
|
||||
;;
|
||||
esac
|
||||
|
||||
@@ -216,6 +216,8 @@ apply_channel_or_keep() {
|
||||
VERSION="${VERSION#v}"
|
||||
return 0
|
||||
fi
|
||||
git remote add upstream https://github.com/2dust/v2rayN || true
|
||||
git fetch upstream
|
||||
|
||||
echo "[*] Resolving ${ch} tag from GitHub releases..."
|
||||
|
||||
@@ -521,7 +523,7 @@ Requires: freetype >= 2.13
|
||||
%description
|
||||
v2rayN Linux for Red Hat Enterprise Linux
|
||||
Support vless / vmess / Trojan / http / socks / Anytls / Hysteria2 / Shadowsocks / tuic / WireGuard
|
||||
Support Red Hat Enterprise Linux / Fedora Linux / Rocky Linux / AlmaLinux / CentOS
|
||||
Support Red Hat Enterprise Linux / Fedora Linux / Rocky Linux / AlmaLinux / CentOS / Anolis OS
|
||||
For more information, Please visit our website
|
||||
https://github.com/2dust/v2rayN
|
||||
|
||||
|
||||
Regular → Executable
Regular → Executable
@@ -5,16 +5,31 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Update="Resx\Resource.resx">
|
||||
<Generator>ResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>Resource.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
|
||||
<Compile Update="Resx\Resource.Designer.cs">
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>Resource.resx</DependentUpon>
|
||||
</Compile>
|
||||
<EmbeddedResource Update="Resx\Resource.resx">
|
||||
<SubType>Designer</SubType>
|
||||
<LastGenOutput>Resource.Designer.cs</LastGenOutput>
|
||||
<Generator>ResXFileCodeGenerator</Generator>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Update="Resx\ResUI.zh-Hans.resx">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>ResXFileCodeGenerator</Generator>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Update="Resx\ResUI.zh-Hant.resx">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>ResXFileCodeGenerator</Generator>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="RemoveAotSatelliteOutputs" AfterTargets="ComputeFilesToPublish" Condition="'$(PublishAot)' == 'true'">
|
||||
<ItemGroup>
|
||||
<ResolvedFileToPublish Remove="@(ResolvedFileToPublish)"
|
||||
Condition="$([System.String]::Copy('%(ResolvedFileToPublish.RelativePath)').Contains('resources.dll'))" />
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
+1
-1
@@ -19,7 +19,7 @@ namespace AmazTool.Resx {
|
||||
// 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。
|
||||
// 若要添加或移除成员,请编辑 .ResX 文件,然后重新运行 ResGen
|
||||
// (以 /str 作为命令选项),或重新生成 VS 项目。
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
internal class Resource {
|
||||
|
||||
@@ -45,6 +45,8 @@ internal class UpgradeApp
|
||||
var thisAppOldFile = $"{Utils.GetExePath()}.tmp";
|
||||
File.Delete(thisAppOldFile);
|
||||
var splitKey = "/";
|
||||
var allowedBaseDir = Utils.StartupPath();
|
||||
var pathComparison = Utils.GetPathComparison();
|
||||
|
||||
using var archive = ZipFile.OpenRead(fileName);
|
||||
foreach (var entry in archive.Entries)
|
||||
@@ -65,16 +67,21 @@ internal class UpgradeApp
|
||||
}
|
||||
|
||||
var fullName = string.Join(splitKey, lst[1..lst.Length]);
|
||||
var entryOutputPath = Utils.GetPath(fullName);
|
||||
if (!Utils.IsPathUnderDirectory(allowedBaseDir, entryOutputPath))
|
||||
{
|
||||
Console.WriteLine($"{Resx.Resource.FailedUpgrade} blocked potential path traversal: {entry.FullName}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.Equals(Utils.GetExePath(), Utils.GetPath(fullName), StringComparison.OrdinalIgnoreCase))
|
||||
if (string.Equals(Utils.GetExePath(), entryOutputPath, pathComparison))
|
||||
{
|
||||
File.Move(Utils.GetExePath(), thisAppOldFile);
|
||||
}
|
||||
|
||||
var entryOutputPath = Utils.GetPath(fullName);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(entryOutputPath)!);
|
||||
//In the bin folder, if the file already exists, it will be skipped
|
||||
if (fullName.StartsWith("bin") && File.Exists(entryOutputPath))
|
||||
if (fullName.StartsWith("bin", pathComparison) && File.Exists(entryOutputPath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,28 @@ internal class Utils
|
||||
return Path.Combine(startupPath, fileName);
|
||||
}
|
||||
|
||||
public static bool IsPathUnderDirectory(string baseDir, string targetPath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(baseDir) || string.IsNullOrWhiteSpace(targetPath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var baseFull = Path.GetFullPath(baseDir);
|
||||
var targetFull = Path.GetFullPath(targetPath);
|
||||
|
||||
baseFull = baseFull.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar;
|
||||
|
||||
return targetFull.StartsWith(baseFull, GetPathComparison());
|
||||
}
|
||||
|
||||
public static StringComparison GetPathComparison()
|
||||
{
|
||||
return OperatingSystem.IsWindows()
|
||||
? StringComparison.OrdinalIgnoreCase
|
||||
: StringComparison.Ordinal;
|
||||
}
|
||||
|
||||
public static string V2rayN => "v2rayN";
|
||||
|
||||
public static void StartV2RayN()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project>
|
||||
|
||||
<PropertyGroup>
|
||||
<Version>7.25.5</Version>
|
||||
<Version>7.25.6</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
@@ -29,4 +29,14 @@
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
<PublishReadyToRun>false</PublishReadyToRun>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<IsAot Condition="'$(IsAot)' == ''">false</IsAot>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(IsAot)' == 'true'">
|
||||
<PublishAot>true</PublishAot>
|
||||
<PublishTrimmed>true</PublishTrimmed>
|
||||
<DefineConstants>$(DefineConstants);V2RAYN_IS_AOT</DefineConstants>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
||||
@@ -6,32 +6,30 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Avalonia.AvaloniaEdit" Version="12.0.0" />
|
||||
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="12.1.2" />
|
||||
<PackageVersion Include="Avalonia.Desktop" Version="12.1.2" />
|
||||
<PackageVersion Include="Avalonia.Desktop" Version="12.1.4" />
|
||||
<PackageVersion Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.3" />
|
||||
<PackageVersion Include="DialogHost.Avalonia" Version="0.12.3" />
|
||||
<PackageVersion Include="IPNetwork2" Version="4.3.0" />
|
||||
<PackageVersion Include="ReactiveUI.Avalonia" Version="12.1.2" />
|
||||
<PackageVersion Include="IPNetwork2" Version="5.0.1" />
|
||||
<PackageVersion Include="ProDataGrid" Version="12.1.0.6" />
|
||||
<PackageVersion Include="ReactiveUI.Avalonia" Version="12.1.6" />
|
||||
<PackageVersion Include="CliWrap" Version="3.10.5" />
|
||||
<PackageVersion Include="Downloader" Version="5.9.8" />
|
||||
<PackageVersion Include="Downloader" Version="5.10.2" />
|
||||
<PackageVersion Include="H.NotifyIcon.Wpf" Version="2.4.1" />
|
||||
<PackageVersion Include="MaterialDesignThemes" Version="5.3.2" />
|
||||
<PackageVersion Include="MaterialDesignThemes" Version="5.4.0" />
|
||||
<PackageVersion Include="QRCoder" Version="1.8.0" />
|
||||
<PackageVersion Include="ReactiveUI" Version="24.3.0" />
|
||||
<PackageVersion Include="ReactiveUI.SourceGenerators" Version="3.2.0" />
|
||||
<PackageVersion Include="ReactiveUI.WPF" Version="24.2.0" />
|
||||
<PackageVersion Include="ReactiveUI" Version="26.0.1" />
|
||||
<PackageVersion Include="ReactiveUI.WPF" Version="26.0.1" />
|
||||
<PackageVersion Include="Semi.Avalonia" Version="12.1.0.1" />
|
||||
<PackageVersion Include="Semi.Avalonia.AvaloniaEdit" Version="12.0.0" />
|
||||
<PackageVersion Include="Semi.Avalonia.DataGrid" Version="12.1.0.1" />
|
||||
<PackageVersion Include="NLog" Version="6.2.1" />
|
||||
<PackageVersion Include="NLog" Version="6.2.2" />
|
||||
<PackageVersion Include="sqlite-net-e" Version="1.11.285" />
|
||||
<PackageVersion Include="Repobot.SQLite.Unofficial" Version="3.53.4.1" />
|
||||
<PackageVersion Include="TaskScheduler" Version="2.12.2" />
|
||||
<PackageVersion Include="TUnit" Version="1.72.16" />
|
||||
<PackageVersion Include="Repobot.SQLite.Unofficial" Version="3.54.1" />
|
||||
<PackageVersion Include="TUnit" Version="1.73.19" />
|
||||
<PackageVersion Include="TUnit.Assertions.Should" Version="1.72.16-beta" />
|
||||
<PackageVersion Include="WebDav.Client" Version="2.9.0" />
|
||||
<PackageVersion Include="YamlDotNet" Version="18.1.0" />
|
||||
<PackageVersion Include="ZXing.Net.Bindings.SkiaSharp" Version="0.16.22" />
|
||||
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.4" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
+1
-1
Submodule v2rayN/GlobalHotKeys updated: 162d401dfe...e507a76804
@@ -0,0 +1,42 @@
|
||||
namespace ServiceLib.Tests.Common;
|
||||
|
||||
public class UnixFileModeTests
|
||||
{
|
||||
[Test]
|
||||
public async Task AlreadyExecutableSystemBinary_ShouldNotRequireOwnership()
|
||||
{
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// /bin/sh is executable but owned by root on the Unix CI runners.
|
||||
// A regular user must not need chmod permission just to launch it.
|
||||
var mode = File.GetUnixFileMode("/bin/sh");
|
||||
await Utils.SetUnixFileMode("/bin/sh").Should().BeTrue();
|
||||
await File.GetUnixFileMode("/bin/sh").Should().BeEqualTo(mode);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task NonExecutableOwnedFile_ShouldGainExecuteBitsWithoutLosingOtherPermissions()
|
||||
{
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var path = Path.GetTempFileName();
|
||||
try
|
||||
{
|
||||
var mode = UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.GroupRead;
|
||||
File.SetUnixFileMode(path, mode);
|
||||
|
||||
await Utils.SetUnixFileMode(path).Should().BeTrue();
|
||||
await File.GetUnixFileMode(path).Should().BeEqualTo(mode | UnixFileMode.UserExecute | UnixFileMode.GroupExecute | UnixFileMode.OtherExecute);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
namespace ServiceLib.Tests.CoreConfig.V2ray;
|
||||
|
||||
public class CoreConfigV2rayIPNetworkTests
|
||||
{
|
||||
[Test]
|
||||
[Arguments("0.0.0.0/0", "::/0")]
|
||||
[Arguments("::/0", "0.0.0.0/0")]
|
||||
[Arguments("0.0.0.0/1", "128.0.0.0/1,::/0")]
|
||||
[Arguments("::/1", "0.0.0.0/0,8000::/1")]
|
||||
public async Task GenerateClientConfigContent_TunExclusion_ShouldPreserveRemainingRoutes(
|
||||
string excludedNetwork, string expectedRoutes)
|
||||
{
|
||||
var config = CoreConfigTestFactory.CreateConfigWithTun(ECoreType.Xray, true);
|
||||
config.TunModeItem.RouteExcludeAddress = [excludedNetwork];
|
||||
var generated = await GenerateConfig(config);
|
||||
|
||||
var routes = generated.inbounds.Single(i => i.protocol == "tun").settings.autoSystemRoutingTable;
|
||||
await routes.Should().BeEquivalentTo(expectedRoutes.Split(','), StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GenerateClientConfigContent_TunExclusions_ShouldHandleOverlappingAndDuplicateNetworks()
|
||||
{
|
||||
var config = CoreConfigTestFactory.CreateConfigWithTun(ECoreType.Xray, true);
|
||||
config.TunModeItem.RouteExcludeAddress =
|
||||
[
|
||||
"0.0.0.0/1", "128.0.0.1/2", "128.0.0.1/2", "128.0.0.1/32",
|
||||
"::/1", "8000::1/2", "8000::1/128"
|
||||
];
|
||||
var generated = await GenerateConfig(config);
|
||||
|
||||
var routes = generated.inbounds.Single(i => i.protocol == "tun").settings.autoSystemRoutingTable;
|
||||
await routes.Should().BeEquivalentTo(new[] { "192.0.0.0/2", "c000::/2" }, StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GenerateClientConfigContent_TunExclusions_ShouldAllowExcludingBothAddressFamilies()
|
||||
{
|
||||
var config = CoreConfigTestFactory.CreateConfigWithTun(ECoreType.Xray, true);
|
||||
config.TunModeItem.RouteExcludeAddress = ["0.0.0.0/0", "::/0"];
|
||||
var generated = await GenerateConfig(config);
|
||||
|
||||
var routes = generated.inbounds.Single(i => i.protocol == "tun").settings.autoSystemRoutingTable;
|
||||
await routes.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GenerateClientConfigContent_TunHostExclusions_ShouldPreserveNeighbouringAddresses()
|
||||
{
|
||||
var config = CoreConfigTestFactory.CreateConfigWithTun(ECoreType.Xray, true);
|
||||
config.TunModeItem.RouteExcludeAddress =
|
||||
[
|
||||
"0.0.0.0/32", "255.255.255.255/32",
|
||||
"::/128", "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128"
|
||||
];
|
||||
var generated = await GenerateConfig(config);
|
||||
var routes = generated.inbounds.Single(i => i.protocol == "tun").settings.autoSystemRoutingTable
|
||||
.Select(IPNetwork.Parse).ToArray();
|
||||
|
||||
foreach (var address in new[]
|
||||
{
|
||||
"0.0.0.0", "255.255.255.255", "::", "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"
|
||||
})
|
||||
{
|
||||
await routes.Any(route => route.Contains(IPAddress.Parse(address))).Should().BeFalse();
|
||||
}
|
||||
foreach (var address in new[]
|
||||
{
|
||||
"0.0.0.1", "255.255.255.254", "::1", "ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffe"
|
||||
})
|
||||
{
|
||||
await routes.Any(route => route.Contains(IPAddress.Parse(address))).Should().BeTrue();
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Arguments("198.18.0.0/15", 131071L)]
|
||||
[Arguments("0.0.0.0/0", 4294967295L)]
|
||||
[Arguments("198.18.0.0/31", 1L)]
|
||||
[Arguments("198.18.0.1/32", 65535L)]
|
||||
[Arguments("fd00::/64", long.MaxValue)]
|
||||
[Arguments("fd00::/66", 4611686018427387903L)]
|
||||
[Arguments("fd00::/127", 1L)]
|
||||
[Arguments("fd00::1/128", 65535L)]
|
||||
[Arguments("invalid-network", 65535L)]
|
||||
[Arguments("198.18.0.0/33", 65535L)]
|
||||
[Arguments("fd00::/129", 65535L)]
|
||||
public async Task GenerateClientConfigContent_FakeDns_ShouldDerivePoolSizeOrUseFallback(
|
||||
string range, long expectedPoolSize)
|
||||
{
|
||||
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
|
||||
config.SimpleDNSItem.FakeIP = true;
|
||||
config.SimpleDNSItem.FakeIPRange = range;
|
||||
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||
var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray);
|
||||
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.Xray);
|
||||
context.RoutingItem!.RuleSet = JsonUtils.Serialize(new List<RulesItem>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Enabled = true,
|
||||
RuleType = ERuleType.DNS,
|
||||
OutboundTag = Global.ProxyTag,
|
||||
Domain = ["full:example.com"],
|
||||
}
|
||||
});
|
||||
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
|
||||
await result.Success.Should().BeTrue();
|
||||
var generated = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
|
||||
|
||||
await generated.fakedns.Should().NotBeNull();
|
||||
await generated.fakedns!.ipPool.Should().BeEqualTo(range);
|
||||
await generated.fakedns.poolSize.Should().BeEqualTo(expectedPoolSize);
|
||||
}
|
||||
|
||||
private static async Task<V2rayConfig> GenerateConfig(Config config)
|
||||
{
|
||||
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||
var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray);
|
||||
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.Xray);
|
||||
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
|
||||
|
||||
await result.Success.Should().BeTrue();
|
||||
var generated = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString());
|
||||
await generated.Should().NotBeNull();
|
||||
return generated!;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,43 @@ namespace ServiceLib.Tests.CoreConfig.V2ray;
|
||||
|
||||
public class CoreConfigV2rayServiceTests
|
||||
{
|
||||
[Test]
|
||||
[Arguments(null)]
|
||||
[Arguments("")]
|
||||
[Arguments("AGH+DQBdAAAgACBAi73MnT3WWXWj4M9MUiTygpjSPUfM/Rj3Iasve4uFQAAkAAEAAQABAAIAAQADAAIAAQACAAIAAgADAAMAAQADAAIAAwADAA5vdXRlci5lY2gudGVzdAAA")]
|
||||
[Arguments("https://dns.example.com/dns-query")]
|
||||
[Arguments("udp://127.0.0.1:5353")]
|
||||
[Arguments("ech.example.com+https://dns.example.com/dns-query")]
|
||||
public async Task GenerateClientConfigContent_Tls_ShouldPreserveEchConfigWithoutLegacyQueryOption(string? echConfigList)
|
||||
{
|
||||
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
|
||||
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||
var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray);
|
||||
node.StreamSecurity = Global.StreamSecurity;
|
||||
node.Sni = "ech.example.com";
|
||||
node.EchConfigList = echConfigList;
|
||||
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.Xray);
|
||||
|
||||
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
|
||||
|
||||
await result.Success.Should().BeTrue();
|
||||
var generatedConfig = JsonNode.Parse(result.Data!.ToString()!)!;
|
||||
var proxy = generatedConfig["outbounds"]!.AsArray()
|
||||
.First(outbound => outbound!["tag"]!.GetValue<string>() == Global.ProxyTag)!;
|
||||
var tls = proxy["streamSettings"]!["tlsSettings"]!.AsObject();
|
||||
|
||||
await tls["serverName"]!.GetValue<string>().Should().BeEqualTo("ech.example.com");
|
||||
if (string.IsNullOrEmpty(echConfigList))
|
||||
{
|
||||
await tls.ContainsKey("echConfigList").Should().BeFalse();
|
||||
}
|
||||
else
|
||||
{
|
||||
await tls["echConfigList"]!.GetValue<string>().Should().BeEqualTo(echConfigList);
|
||||
}
|
||||
await tls.ContainsKey("echForceQuery").Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GenerateClientConfigContent_ShouldGenerateBasicProxyConfig()
|
||||
{
|
||||
@@ -21,6 +58,61 @@ public class CoreConfigV2rayServiceTests
|
||||
await v2rayConfig.inbounds.Should().Contain(i => i.protocol == nameof(EInboundProtocol.mixed));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GenerateClientConfigContent_RealityFragment_ShouldUsePacketRangeOnlyForReality()
|
||||
{
|
||||
static ProfileItem CreateVlessNode(string indexId, string streamSecurity)
|
||||
{
|
||||
var node = new ProfileItem
|
||||
{
|
||||
IndexId = indexId,
|
||||
ConfigType = EConfigType.VLESS,
|
||||
CoreType = ECoreType.Xray,
|
||||
Remarks = indexId,
|
||||
Address = "example.com",
|
||||
Port = 443,
|
||||
Password = "01234567-89ab-cdef-0123-456789abcdef",
|
||||
Network = nameof(ETransport.raw),
|
||||
StreamSecurity = streamSecurity,
|
||||
PublicKey = "test-public-key",
|
||||
Sni = "example.com",
|
||||
Fingerprint = "chrome",
|
||||
};
|
||||
node.SetProtocolExtra(new ProtocolExtraItem { VlessEncryption = Global.None });
|
||||
node.SetTransportExtra(new TransportExtraItem { RawHeaderType = Global.None });
|
||||
return node;
|
||||
}
|
||||
|
||||
static string GetFragmentPackets(Outbounds4Ray outbound)
|
||||
{
|
||||
var finalmask = JsonUtils.ParseJson(JsonUtils.Serialize(outbound.streamSettings!.finalmask));
|
||||
return finalmask!["tcp"]![0]!["settings"]!["packets"]!.GetValue<string>();
|
||||
}
|
||||
|
||||
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
|
||||
config.CoreBasicItem.EnableFragment = true;
|
||||
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||
|
||||
var tlsNode = CreateVlessNode("tls-node", Global.StreamSecurity);
|
||||
var realityNode = CreateVlessNode("reality-node", Global.StreamSecurityReality);
|
||||
var group = CoreConfigTestFactory.CreatePolicyGroupNode(ECoreType.Xray, "group", "group",
|
||||
[tlsNode.IndexId, realityNode.IndexId]);
|
||||
var context = CoreConfigTestFactory.CreateContext(config, group, ECoreType.Xray);
|
||||
context.AllProxiesMap[tlsNode.IndexId] = tlsNode;
|
||||
context.AllProxiesMap[realityNode.IndexId] = realityNode;
|
||||
context.AllProxiesMap[group.IndexId] = group;
|
||||
|
||||
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
|
||||
|
||||
await result.Success.Should().BeTrue();
|
||||
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
|
||||
var tlsOutbound = cfg.outbounds.Single(o => o.streamSettings?.security == Global.StreamSecurity);
|
||||
var realityOutbound = cfg.outbounds.Single(o => o.streamSettings?.security == Global.StreamSecurityReality);
|
||||
|
||||
await GetFragmentPackets(tlsOutbound).Should().BeEqualTo("tlshello");
|
||||
await GetFragmentPackets(realityOutbound).Should().BeEqualTo("1-3");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GenerateClientConfigContent_HttpOutbound_ShouldEmitHeadersInSettings()
|
||||
{
|
||||
@@ -587,13 +679,11 @@ public class CoreConfigV2rayServiceTests
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Arguments(true)]
|
||||
[Arguments(false)]
|
||||
public async Task GenerateClientConfigContent_Tun_ShouldSkipIPv6RouteWithoutGlobalIPv6(bool enableIPv6Address)
|
||||
public async Task GenerateClientConfigContent_Tun_ShouldSkipIPv6RouteWithoutGlobalIPv6()
|
||||
{
|
||||
// A host without a global IPv6 address has no IPv6 traffic that could bypass the tunnel,
|
||||
// while ::/0 would pull IPv6 attempts into a tunnel they cannot leave.
|
||||
var config = CoreConfigTestFactory.CreateConfigWithTun(ECoreType.Xray, enableIPv6Address);
|
||||
var config = CoreConfigTestFactory.CreateConfigWithTun(ECoreType.Xray, enableIPv6Address: false);
|
||||
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||
|
||||
var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray, "n-main", "main");
|
||||
@@ -611,6 +701,26 @@ public class CoreConfigV2rayServiceTests
|
||||
await ipv6Routes.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GenerateClientConfigContent_Tun_ShouldRouteIPv6WhenEnabledWithoutGlobalIPv6()
|
||||
{
|
||||
// Hosts behind NAT66 hold only ULA addresses, so EnableIPv6Address must still route ::/0.
|
||||
var config = CoreConfigTestFactory.CreateConfigWithTun(ECoreType.Xray, enableIPv6Address: true);
|
||||
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||
|
||||
var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray, "n-main", "main");
|
||||
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.Xray, hasGlobalIPv6Address: false);
|
||||
|
||||
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
|
||||
|
||||
await result.Success.Should().BeTrue();
|
||||
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
|
||||
var tunInbound = cfg.inbounds.FirstOrDefault(i => i.protocol == "tun");
|
||||
|
||||
await tunInbound.Should().NotBeNull();
|
||||
await tunInbound!.settings.autoSystemRoutingTable.Should().Contain("::/0");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GenerateClientConfigContent_TunRouteExcludeAddress_ShouldSkipIPv6RangesWithoutGlobalIPv6()
|
||||
{
|
||||
|
||||
@@ -27,16 +27,51 @@ public class ConfigHandlerCustomSubscriptionTests
|
||||
Url = "http://127.0.0.1/sub",
|
||||
PreSocksPort = 10808,
|
||||
});
|
||||
var oldProfile = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray, $"profile-{Guid.NewGuid():N}", "old");
|
||||
oldProfile.Subid = subId;
|
||||
oldProfile.IsSub = true;
|
||||
await SQLiteHelper.Instance.InsertAsync(oldProfile);
|
||||
|
||||
var count = await ConfigHandler.AddBatchServers(config, FullXrayConfigs, subId, true);
|
||||
var profiles = await AppManager.Instance.ProfileItems(subId) ?? [];
|
||||
|
||||
await count.Should().BeEqualTo(2);
|
||||
await profiles.Should().HaveCount(2);
|
||||
await profiles.Any(profile => profile.IndexId == oldProfile.IndexId).Should().BeFalse();
|
||||
foreach (var profile in profiles)
|
||||
{
|
||||
await profile.ConfigType.Should().BeEqualTo(EConfigType.Custom);
|
||||
await profile.PreSocksPort.Should().BeEqualTo(10808);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task AddBatchServers_UnsuccessfulSubscriptionUpdate_PreservesExistingProfiles()
|
||||
{
|
||||
var config = CoreConfigTestFactory.CreateConfig();
|
||||
CoreConfigTestFactory.BindAppManagerConfig(config);
|
||||
SQLiteHelper.Instance.CreateTable<SubItem>();
|
||||
SQLiteHelper.Instance.CreateTable<ProfileItem>();
|
||||
|
||||
var subId = $"sub-failed-{Guid.NewGuid():N}";
|
||||
await SQLiteHelper.Instance.ReplaceAsync(new SubItem
|
||||
{
|
||||
Id = subId,
|
||||
Remarks = "sub",
|
||||
Url = "http://127.0.0.1/sub",
|
||||
});
|
||||
|
||||
var existing = CoreConfigTestFactory.CreateVmessNode(ECoreType.Xray, $"profile-{Guid.NewGuid():N}", "existing");
|
||||
existing.Subid = subId;
|
||||
existing.IsSub = true;
|
||||
await SQLiteHelper.Instance.InsertAsync(existing);
|
||||
|
||||
var invalidSubscription = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes("not a valid profile"));
|
||||
var count = await ConfigHandler.AddBatchServers(config, invalidSubscription, subId, true);
|
||||
var profiles = await AppManager.Instance.ProfileItems(subId) ?? [];
|
||||
|
||||
await (count < 1).Should().BeTrue();
|
||||
await profiles.Should().HaveCount(1);
|
||||
await profiles[0].IndexId.Should().BeEqualTo(existing.IndexId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
namespace ServiceLib.Tests.Manager;
|
||||
|
||||
public class ProfileExManagerConcurrencyTests
|
||||
{
|
||||
[Test]
|
||||
public async Task SetTestDelay_FromManyThreads_ShouldNotThrow()
|
||||
{
|
||||
var manager = CreateManager();
|
||||
var exceptions = new List<Exception>();
|
||||
var threads = Environment.ProcessorCount * 2;
|
||||
|
||||
//Hammer the same manager from several streams at once: the queue is mutated while
|
||||
//other threads keep pushing into it
|
||||
var barrier = new Barrier(threads);
|
||||
Parallel.For(0, threads, t =>
|
||||
{
|
||||
barrier.SignalAndWait();
|
||||
for (var i = 0; i < 5000; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
manager.SetTestDelay($"index{i % 500}", i);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
lock (exceptions)
|
||||
{
|
||||
exceptions.Add(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await exceptions.Count.Should().BeEqualTo(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task SetTestDelay_FromManyThreads_ShouldNotLoseQueuedIds()
|
||||
{
|
||||
var manager = CreateManager();
|
||||
var ids = 500;
|
||||
|
||||
Parallel.For(0, Environment.ProcessorCount * 2, t =>
|
||||
{
|
||||
for (var i = 0; i < 5000; i++)
|
||||
{
|
||||
manager.SetTestDelay($"id{i % ids}", i);
|
||||
}
|
||||
});
|
||||
|
||||
var drained = Drain(manager);
|
||||
await drained.Count.Should().BeGreaterThanOrEqualTo(ids);
|
||||
await drained.Distinct().Count().Should().BeEqualTo(ids);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task SetTestDelay_ShouldCreateOneEntryPerIndex()
|
||||
{
|
||||
var manager = CreateManager();
|
||||
|
||||
await RunConcurrentUpdates(8, 10, (_, index) => manager.SetTestDelay($"same{index}", index));
|
||||
|
||||
var items = await manager.GetProfileExs();
|
||||
await items.Count.Should().BeEqualTo(10);
|
||||
await items.Select(t => t.IndexId).Distinct().Count().Should().BeEqualTo(10);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task SetTestResults_ConcurrentUpdates_ShouldKeepAllValuesOnOneEntry()
|
||||
{
|
||||
var manager = CreateManager();
|
||||
Action<string>[] updates =
|
||||
[
|
||||
indexId => manager.SetTestDelay(indexId, 123),
|
||||
indexId => manager.SetTestSpeed(indexId, 45.6m),
|
||||
indexId => manager.SetTestMessage(indexId, "result"),
|
||||
indexId => manager.SetTestIpInfo(indexId, "203.0.113.1"),
|
||||
indexId => manager.SetSort(indexId, 7)
|
||||
];
|
||||
|
||||
await RunConcurrentUpdates(updates.Length, 100, (worker, index) => updates[worker]($"profile{index}"));
|
||||
|
||||
var items = await manager.GetProfileExs();
|
||||
await items.Count.Should().BeEqualTo(100);
|
||||
await items.Select(item => item.IndexId).Distinct().Count().Should().BeEqualTo(100);
|
||||
foreach (var item in items)
|
||||
{
|
||||
await item.Delay.Should().BeEqualTo(123);
|
||||
await item.Speed.Should().BeEqualTo(45.6m);
|
||||
await item.Message.Should().BeEqualTo("result");
|
||||
await item.IpInfo.Should().BeEqualTo("203.0.113.1");
|
||||
await item.Sort.Should().BeEqualTo(7);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task RunConcurrentUpdates(int workers, int indexes, Action<int, int> update)
|
||||
{
|
||||
using var barrier = new Barrier(workers);
|
||||
var tasks = Enumerable.Range(0, workers).Select(worker => Task.Factory.StartNew(() =>
|
||||
{
|
||||
for (var index = 0; index < indexes; index++)
|
||||
{
|
||||
if (!barrier.SignalAndWait(TimeSpan.FromSeconds(10)))
|
||||
{
|
||||
throw new TimeoutException("Concurrent update workers did not synchronize.");
|
||||
}
|
||||
update(worker, index);
|
||||
}
|
||||
}, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default)).ToArray();
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
private static ProfileExManager CreateManager()
|
||||
{
|
||||
return (ProfileExManager)Activator.CreateInstance(typeof(ProfileExManager), nonPublic: true)!;
|
||||
}
|
||||
|
||||
private static List<string> Drain(ProfileExManager manager)
|
||||
{
|
||||
var queue = (System.Collections.Concurrent.ConcurrentQueue<string>)typeof(ProfileExManager)
|
||||
.GetField("_queIndexIds", BindingFlags.Instance | BindingFlags.NonPublic)!
|
||||
.GetValue(manager)!;
|
||||
|
||||
var result = new List<string>();
|
||||
while (queue.TryDequeue(out var id))
|
||||
{
|
||||
result.Add(id);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
namespace ServiceLib.Tests.Manager;
|
||||
|
||||
public class ProfileExManagerPersistenceTests
|
||||
{
|
||||
[Test]
|
||||
public async Task SaveTo_NewProfilesWithRepeatedResults_ShouldPersistEveryProfile()
|
||||
{
|
||||
SQLiteHelper.Instance.CreateTable<ProfileExItem>();
|
||||
var manager = new ProfileExManager();
|
||||
var ids = Enumerable.Range(0, 3).Select(_ => Guid.NewGuid().ToString("N")).ToArray();
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var id in ids)
|
||||
{
|
||||
manager.SetTestDelay(id, 42);
|
||||
manager.SetTestSpeed(id, 12.5m);
|
||||
manager.SetTestMessage(id, "completed");
|
||||
manager.SetTestIpInfo(id, "192.0.2.1");
|
||||
manager.SetSort(id, 7);
|
||||
}
|
||||
|
||||
await manager.SaveTo();
|
||||
|
||||
foreach (var id in ids)
|
||||
{
|
||||
var saved = await SQLiteHelper.Instance.TableAsync<ProfileExItem>()
|
||||
.Where(t => t.IndexId == id).ToListAsync();
|
||||
await saved.Count.Should().BeEqualTo(1);
|
||||
await saved[0].Delay.Should().BeEqualTo(42);
|
||||
await saved[0].Speed.Should().BeEqualTo(12.5m);
|
||||
await saved[0].Message.Should().BeEqualTo("completed");
|
||||
await saved[0].IpInfo.Should().BeEqualTo("192.0.2.1");
|
||||
await saved[0].Sort.Should().BeEqualTo(7);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
foreach (var id in ids)
|
||||
{
|
||||
await SQLiteHelper.Instance.DeleteAsync(new ProfileExItem { IndexId = id });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task SaveTo_MixedNewAndExistingProfiles_ShouldInsertAndUpdateResults()
|
||||
{
|
||||
SQLiteHelper.Instance.CreateTable<ProfileExItem>();
|
||||
var manager = new ProfileExManager();
|
||||
var existingId = Guid.NewGuid().ToString("N");
|
||||
var newId = Guid.NewGuid().ToString("N");
|
||||
|
||||
try
|
||||
{
|
||||
await SQLiteHelper.Instance.InsertAsync(new ProfileExItem
|
||||
{
|
||||
IndexId = existingId,
|
||||
Delay = 1,
|
||||
Speed = 2,
|
||||
});
|
||||
manager.SetTestDelay(existingId, 100);
|
||||
manager.SetTestSpeed(existingId, 20);
|
||||
manager.SetTestDelay(newId, 200);
|
||||
manager.SetTestIpInfo(newId, "192.0.2.2");
|
||||
|
||||
await manager.SaveTo();
|
||||
|
||||
var existing = await SQLiteHelper.Instance.TableAsync<ProfileExItem>()
|
||||
.Where(t => t.IndexId == existingId).FirstAsync();
|
||||
await existing.Delay.Should().BeEqualTo(100);
|
||||
await existing.Speed.Should().BeEqualTo(20m);
|
||||
var inserted = await SQLiteHelper.Instance.TableAsync<ProfileExItem>()
|
||||
.Where(t => t.IndexId == newId).ToListAsync();
|
||||
await inserted.Count.Should().BeEqualTo(1);
|
||||
await inserted[0].Delay.Should().BeEqualTo(200);
|
||||
await inserted[0].IpInfo.Should().BeEqualTo("192.0.2.2");
|
||||
|
||||
manager.SetTestDelay(newId, 300);
|
||||
await manager.SaveTo();
|
||||
var updated = await SQLiteHelper.Instance.TableAsync<ProfileExItem>()
|
||||
.Where(t => t.IndexId == newId).FirstAsync();
|
||||
await updated.Delay.Should().BeEqualTo(300);
|
||||
await updated.IpInfo.Should().BeEqualTo("192.0.2.2");
|
||||
}
|
||||
finally
|
||||
{
|
||||
await SQLiteHelper.Instance.DeleteAsync(new ProfileExItem { IndexId = existingId });
|
||||
await SQLiteHelper.Instance.DeleteAsync(new ProfileExItem { IndexId = newId });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
<OutputType>Exe</OutputType>
|
||||
<IsPackable>false</IsPackable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<PublishAot>true</PublishAot>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
namespace ServiceLib.Tests.Services;
|
||||
|
||||
public class SpeedtestServiceBatchTests
|
||||
{
|
||||
[Test]
|
||||
[Arguments(0)]
|
||||
[Arguments(1000)]
|
||||
public async Task EmptySelection_ShouldReturnNoBatches(int pageSize)
|
||||
{
|
||||
await GetBatches([], pageSize).Count.Should().BeEqualTo(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Arguments(0)]
|
||||
[Arguments(-1)]
|
||||
public async Task NonPositivePageSize_ShouldKeepEveryEligibleProfile(int pageSize)
|
||||
{
|
||||
var profiles = CreateProfiles();
|
||||
var batches = GetBatches(profiles, pageSize);
|
||||
|
||||
await batches.Count.Should().BeEqualTo(profiles.Count);
|
||||
await batches.SelectMany(batch => batch).Count().Should().BeEqualTo(profiles.Count);
|
||||
foreach (var profile in profiles)
|
||||
{
|
||||
await batches.SelectMany(batch => batch).Count(item => ReferenceEquals(item, profile)).Should().BeEqualTo(1);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task NormalBatches_ShouldRespectPageSizeAndKeepCoreTypesSeparate()
|
||||
{
|
||||
var profiles = CreateProfiles();
|
||||
var batches = GetBatches(profiles, 2);
|
||||
|
||||
await batches.Count.Should().BeEqualTo(3);
|
||||
await batches.SelectMany(batch => batch).Count().Should().BeEqualTo(profiles.Count);
|
||||
foreach (var batch in batches)
|
||||
{
|
||||
await (batch.Count > 0 && batch.Count <= 2).Should().BeTrue();
|
||||
await batch.Select(item => item.CoreType).Distinct().Count().Should().BeEqualTo(1);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<ServerTestItem> CreateProfiles() =>
|
||||
[
|
||||
new() { CoreType = ECoreType.Xray },
|
||||
new() { CoreType = ECoreType.Xray },
|
||||
new() { CoreType = ECoreType.Xray },
|
||||
new() { CoreType = ECoreType.sing_box },
|
||||
new() { CoreType = ECoreType.sing_box }
|
||||
];
|
||||
|
||||
private static List<List<ServerTestItem>> GetBatches(List<ServerTestItem> profiles, int pageSize)
|
||||
{
|
||||
var service = new SpeedtestService(new Config { SpeedTestItem = new() }, _ => Task.CompletedTask);
|
||||
var method = typeof(SpeedtestService).GetMethod("GetTestBatchItem", BindingFlags.Instance | BindingFlags.NonPublic)!;
|
||||
return (List<List<ServerTestItem>>)method.Invoke(service, [profiles, pageSize])!;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
namespace ServiceLib.Tests.Services;
|
||||
|
||||
public class SpeedtestServiceResultTests
|
||||
{
|
||||
[Test]
|
||||
[Arguments(ESpeedActionType.Speedtest, 0)]
|
||||
[Arguments(ESpeedActionType.Mixedtest, 0)]
|
||||
[Arguments(ESpeedActionType.Realping, 12.5)]
|
||||
public async Task StartingATest_ShouldClearTheLastSpeedOfSpeedTests(ESpeedActionType actionType, double expected)
|
||||
{
|
||||
var indexId = Guid.NewGuid().ToString();
|
||||
ProfileExManager.Instance.SetTestSpeed(indexId, 12.5m);
|
||||
|
||||
await SetTestResult([new() { IndexId = indexId }], actionType, ResUI.Speedtesting);
|
||||
|
||||
var profileEx = (await ProfileExManager.Instance.GetProfileExs()).First(t => t.IndexId == indexId);
|
||||
await profileEx.Speed.Should().BeEqualTo((decimal)expected);
|
||||
}
|
||||
|
||||
private static Task SetTestResult(List<ServerTestItem> items, ESpeedActionType actionType, string message)
|
||||
{
|
||||
var service = new SpeedtestService(new Config { SpeedTestItem = new() }, _ => Task.CompletedTask);
|
||||
var method = typeof(SpeedtestService).GetMethod("SetTestResultAsync", BindingFlags.Instance | BindingFlags.NonPublic)!;
|
||||
return (Task)method.Invoke(service, [items, actionType, message])!;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
using ReactiveUI.Builder;
|
||||
using ServiceLib.Tests.CoreConfig;
|
||||
using ServiceLib.ViewModels;
|
||||
|
||||
namespace ServiceLib.Tests.ViewModels;
|
||||
|
||||
[NotInParallel]
|
||||
public class ClashProxiesViewModelTests
|
||||
{
|
||||
static ClashProxiesViewModelTests()
|
||||
{
|
||||
RxAppBuilder.CreateReactiveUIBuilder().WithCoreServices().BuildApp();
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Arguments("Selector", true)]
|
||||
[Arguments("URLTest", true)]
|
||||
[Arguments("LoadBalance", true)]
|
||||
[Arguments("Fallback", true)]
|
||||
[Arguments("selector", true)]
|
||||
[Arguments("SELECTOR", true)]
|
||||
[Arguments("Direct", false)]
|
||||
[Arguments(null, false)]
|
||||
public async Task GroupDelay_ShouldTestAllowedGroupTypes(string? type, bool allowed)
|
||||
{
|
||||
var configField = typeof(AppManager).GetField("_config", BindingFlags.Instance | BindingFlags.NonPublic)!;
|
||||
var portField = typeof(AppManager).GetField("_statePort2", BindingFlags.Instance | BindingFlags.NonPublic)!;
|
||||
var viewConfigField = typeof(MyReactiveObject).GetField("_config", BindingFlags.Static | BindingFlags.NonPublic)!;
|
||||
var previousConfig = configField.GetValue(AppManager.Instance);
|
||||
var previousPort = portField.GetValue(AppManager.Instance);
|
||||
var previousViewConfig = viewConfigField.GetValue(null);
|
||||
await using var server = new DelayServer();
|
||||
try
|
||||
{
|
||||
CoreConfigTestFactory.BindAppManagerConfig(CoreConfigTestFactory.CreateConfig());
|
||||
portField.SetValue(AppManager.Instance, server.Port);
|
||||
var viewModel = new ClashProxiesViewModel();
|
||||
var item = new ClashItem();
|
||||
item.Proxies.Add("test-group", new() { type = type, all = ["test-node"] });
|
||||
item.Proxies.Add("test-node", new() { type = "Shadowsocks" });
|
||||
typeof(ClashProxiesViewModel).GetField("_clashItem", BindingFlags.Instance | BindingFlags.NonPublic)!
|
||||
.SetValue(viewModel, item);
|
||||
viewModel.SelectedGroup = new ClashProxyModel { Name = "test-group" };
|
||||
|
||||
var method = typeof(ClashProxiesViewModel).GetMethod("TestGroupProxiesDelay", BindingFlags.Instance | BindingFlags.NonPublic)!;
|
||||
await ((Task)method.Invoke(viewModel, null)!).WaitAsync(TimeSpan.FromSeconds(10));
|
||||
|
||||
await server.Requests.Count.Should().BeEqualTo(allowed ? 1 : 0);
|
||||
await viewModel.ProxyDetails.Single().DelayName.Should().BeEqualTo(allowed ? "42ms" : string.Empty);
|
||||
if (allowed)
|
||||
{
|
||||
await server.Requests.Single().StartsWith("GET /proxies/test-node/delay?").Should().BeTrue();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
configField.SetValue(AppManager.Instance, previousConfig);
|
||||
portField.SetValue(AppManager.Instance, previousPort);
|
||||
viewConfigField.SetValue(null, previousViewConfig);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class DelayServer : IAsyncDisposable
|
||||
{
|
||||
private readonly TcpListener _listener = new(IPAddress.Loopback, 0);
|
||||
private readonly CancellationTokenSource _cancellation = new();
|
||||
private readonly Task _serverTask;
|
||||
public int Port => ((IPEndPoint)_listener.LocalEndpoint).Port;
|
||||
public ConcurrentQueue<string> Requests { get; } = new();
|
||||
|
||||
public DelayServer()
|
||||
{
|
||||
_listener.Start();
|
||||
_serverTask = ServeAsync();
|
||||
}
|
||||
|
||||
private async Task ServeAsync()
|
||||
{
|
||||
var token = _cancellation.Token;
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
using var client = await _listener.AcceptTcpClientAsync(token);
|
||||
await using var stream = client.GetStream();
|
||||
using var reader = new StreamReader(stream, Encoding.ASCII, leaveOpen: true);
|
||||
var request = await reader.ReadLineAsync(token);
|
||||
while (await reader.ReadLineAsync(token) is { Length: > 0 })
|
||||
{
|
||||
}
|
||||
Requests.Enqueue(request!);
|
||||
const string body = "{\"delay\":42}";
|
||||
var response = $"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {body.Length}\r\nConnection: close\r\n\r\n{body}";
|
||||
await stream.WriteAsync(Encoding.ASCII.GetBytes(response), token);
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await _cancellation.CancelAsync();
|
||||
_listener.Stop();
|
||||
try
|
||||
{
|
||||
await _serverTask;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
finally
|
||||
{
|
||||
_cancellation.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -135,16 +135,13 @@ public class UdpTestService
|
||||
roundTripTime = currentRoundTripTime;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested)
|
||||
catch (OperationCanceledException) when (ct.IsCancellationRequested)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
if (attempt == 1 && roundTripTime == TimeSpan.MaxValue)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
// Timeout, continue to next attempt
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -143,7 +143,7 @@ public static class Extension
|
||||
[CallerFilePath] string filePath = "",
|
||||
[CallerLineNumber] int lineNumber = 0)
|
||||
{
|
||||
return Signal.Defer(() => interaction.Handle(input))
|
||||
return Signal.Defer(() => interaction.WhenHandled(input))
|
||||
.Catch<TOutput, UnhandledInteractionException<TInput, TOutput>>(ex =>
|
||||
{
|
||||
Logging.SaveLog($"Unhandled interaction exception in {memberName} at {filePath}:{lineNumber}", ex);
|
||||
|
||||
@@ -7,40 +7,45 @@ public class JsonUtils
|
||||
private static readonly JsonSerializerOptions _defaultDeserializeOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
ReadCommentHandling = JsonCommentHandling.Skip
|
||||
ReadCommentHandling = JsonCommentHandling.Skip,
|
||||
TypeInfoResolver = AppJsonContext.Default,
|
||||
};
|
||||
|
||||
private static readonly JsonSerializerOptions _defaultSerializeOptions = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
||||
TypeInfoResolver = AppJsonContext.Default,
|
||||
};
|
||||
|
||||
private static readonly JsonSerializerOptions _defaultSerializeNoIndentedOptions = new()
|
||||
{
|
||||
WriteIndented = false,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
||||
TypeInfoResolver = AppJsonContext.Default,
|
||||
};
|
||||
|
||||
private static readonly JsonSerializerOptions _nullValueSerializeOptions = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
||||
TypeInfoResolver = AppJsonContext.Default,
|
||||
};
|
||||
|
||||
private static readonly JsonSerializerOptions _nullValueSerializeNoIndentedOptions = new()
|
||||
{
|
||||
WriteIndented = false,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
||||
TypeInfoResolver = AppJsonContext.Default,
|
||||
};
|
||||
|
||||
private static readonly JsonDocumentOptions _defaultDocumentOptions = new()
|
||||
{
|
||||
CommentHandling = JsonCommentHandling.Skip
|
||||
CommentHandling = JsonCommentHandling.Skip,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
@@ -109,7 +114,7 @@ public class JsonUtils
|
||||
/// <param name="indented"></param>
|
||||
/// <param name="nullValue"></param>
|
||||
/// <returns></returns>
|
||||
public static string Serialize(object? obj, bool indented = true, bool nullValue = false)
|
||||
public static string Serialize<T>(T? obj, bool indented = true, bool nullValue = false)
|
||||
{
|
||||
var result = string.Empty;
|
||||
try
|
||||
@@ -140,7 +145,7 @@ public class JsonUtils
|
||||
/// <param name="obj"></param>
|
||||
/// <param name="options"></param>
|
||||
/// <returns></returns>
|
||||
public static string Serialize(object? obj, JsonSerializerOptions? options)
|
||||
public static string SerializeCamelCase<T>(T? obj)
|
||||
{
|
||||
var result = string.Empty;
|
||||
try
|
||||
@@ -149,7 +154,14 @@ public class JsonUtils
|
||||
{
|
||||
return result;
|
||||
}
|
||||
result = JsonSerializer.Serialize(obj, options ?? _defaultSerializeOptions);
|
||||
var options = new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
TypeInfoResolver = AppJsonContext.Default,
|
||||
};
|
||||
result = JsonSerializer.Serialize(obj, options);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -162,9 +174,108 @@ public class JsonUtils
|
||||
/// SerializeToNode
|
||||
/// </summary>
|
||||
/// <param name="obj"></param>
|
||||
/// <param name="nullValue"></param>
|
||||
/// <returns></returns>
|
||||
public static JsonNode? SerializeToNode(object? obj, JsonSerializerOptions? options = null)
|
||||
public static JsonNode? SerializeToNode<T>(T? obj, bool nullValue = false)
|
||||
{
|
||||
var options = nullValue ? _nullValueSerializeOptions : _defaultSerializeOptions;
|
||||
return JsonSerializer.SerializeToNode(obj, options);
|
||||
}
|
||||
|
||||
public static JsonArray? SerializeToArray(IEnumerable<string>? values)
|
||||
{
|
||||
if (values is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var array = new JsonArray();
|
||||
foreach (var value in values)
|
||||
{
|
||||
var valueNode = JsonSerializer.SerializeToNode(value, _defaultSerializeOptions);
|
||||
array.Add(valueNode);
|
||||
}
|
||||
return array;
|
||||
}
|
||||
}
|
||||
|
||||
[JsonSerializable(typeof(Config))]
|
||||
[JsonSerializable(typeof(RulesItem))]
|
||||
[JsonSerializable(typeof(ProfileItem))]
|
||||
[JsonSerializable(typeof(List<RulesItem>))]
|
||||
[JsonSerializable(typeof(List<ProfileItem>))]
|
||||
[JsonSerializable(typeof(ProfileItemModel))]
|
||||
[JsonSerializable(typeof(List<ProfileItemModel>))]
|
||||
[JsonSerializable(typeof(RoutingTemplate))]
|
||||
[JsonSerializable(typeof(DNSItem))]
|
||||
[JsonSerializable(typeof(SimpleDNSItem))]
|
||||
[JsonSerializable(typeof(IPAPIInfo))]
|
||||
[JsonSerializable(typeof(SsSIP008))]
|
||||
[JsonSerializable(typeof(List<SsServer>))]
|
||||
[JsonSerializable(typeof(SsServer))]
|
||||
[JsonSerializable(typeof(VmessQRCode))]
|
||||
[JsonSerializable(typeof(ClashProxies))]
|
||||
[JsonSerializable(typeof(ClashProviders))]
|
||||
[JsonSerializable(typeof(ClashConnections))]
|
||||
[JsonSerializable(typeof(ProtocolExtraItem))]
|
||||
[JsonSerializable(typeof(TransportExtraItem))]
|
||||
[JsonSerializable(typeof(TrafficItem))]
|
||||
[JsonSerializable(typeof(V2rayMetricsVars))]
|
||||
[JsonSerializable(typeof(V2rayMetricsVarsLink))]
|
||||
[JsonSerializable(typeof(GitHubRelease))]
|
||||
[JsonSerializable(typeof(List<GitHubRelease>))]
|
||||
[JsonSerializable(typeof(ServerStatItem))]
|
||||
[JsonSerializable(typeof(WebDavItem))]
|
||||
[JsonSerializable(typeof(KeyEventItem))]
|
||||
[JsonSerializable(typeof(SubItem))]
|
||||
[JsonSerializable(typeof(SingboxConfig))]
|
||||
[JsonSerializable(typeof(Dns4Sbox))]
|
||||
[JsonSerializable(typeof(Inbound4Sbox))]
|
||||
[JsonSerializable(typeof(List<Inbound4Sbox>))]
|
||||
[JsonSerializable(typeof(Endpoints4Sbox))]
|
||||
[JsonSerializable(typeof(List<Endpoints4Sbox>))]
|
||||
[JsonSerializable(typeof(Outbound4Sbox))]
|
||||
[JsonSerializable(typeof(List<Outbound4Sbox>))]
|
||||
[JsonSerializable(typeof(Rule4Sbox))]
|
||||
[JsonSerializable(typeof(Ruleset4Sbox))]
|
||||
[JsonSerializable(typeof(List<Rule4Sbox>))]
|
||||
[JsonSerializable(typeof(List<Ruleset4Sbox>))]
|
||||
[JsonSerializable(typeof(V2rayConfig))]
|
||||
[JsonSerializable(typeof(Dns4Ray))]
|
||||
[JsonSerializable(typeof(DnsServer4Ray))]
|
||||
[JsonSerializable(typeof(Inbounds4Ray))]
|
||||
[JsonSerializable(typeof(List<Inbounds4Ray>))]
|
||||
[JsonSerializable(typeof(Outbounds4Ray))]
|
||||
[JsonSerializable(typeof(List<Outbounds4Ray>))]
|
||||
[JsonSerializable(typeof(RulesItem4Ray))]
|
||||
[JsonSerializable(typeof(List<RulesItem4Ray>))]
|
||||
[JsonSerializable(typeof(Finalmask4Ray))]
|
||||
[JsonSerializable(typeof(string))]
|
||||
[JsonSerializable(typeof(int))]
|
||||
[JsonSerializable(typeof(long))]
|
||||
[JsonSerializable(typeof(double))]
|
||||
[JsonSerializable(typeof(decimal))]
|
||||
[JsonSerializable(typeof(bool))]
|
||||
[JsonSerializable(typeof(Guid))]
|
||||
[JsonSerializable(typeof(DateTime))]
|
||||
[JsonSerializable(typeof(DateTimeOffset))]
|
||||
[JsonSerializable(typeof(TimeSpan))]
|
||||
[JsonSerializable(typeof(int?))]
|
||||
[JsonSerializable(typeof(long?))]
|
||||
[JsonSerializable(typeof(double?))]
|
||||
[JsonSerializable(typeof(bool?))]
|
||||
[JsonSerializable(typeof(DateTime?))]
|
||||
[JsonSerializable(typeof(Guid?))]
|
||||
[JsonSerializable(typeof(string[]))]
|
||||
[JsonSerializable(typeof(int[]))]
|
||||
[JsonSerializable(typeof(byte[]))]
|
||||
[JsonSerializable(typeof(List<string>))]
|
||||
[JsonSerializable(typeof(List<int>))]
|
||||
[JsonSerializable(typeof(Dictionary<string, string>))]
|
||||
[JsonSerializable(typeof(Dictionary<string, object>))]
|
||||
[JsonSerializable(typeof(object))]
|
||||
[JsonSerializable(typeof(JsonElement))]
|
||||
[JsonSerializable(typeof(JsonDocument))]
|
||||
[JsonSerializable(typeof(JsonNode))]
|
||||
[JsonSerializable(typeof(JsonObject))]
|
||||
[JsonSerializable(typeof(JsonArray))]
|
||||
internal partial class AppJsonContext : JsonSerializerContext;
|
||||
|
||||
@@ -348,10 +348,9 @@ public class Utils
|
||||
.ToList());
|
||||
}
|
||||
|
||||
public static List<string> GetEnumNames<TEnum>() where TEnum : Enum
|
||||
public static List<string> GetEnumNames<TEnum>() where TEnum : struct, Enum
|
||||
{
|
||||
return Enum.GetValues(typeof(TEnum))
|
||||
.Cast<TEnum>()
|
||||
return Enum.GetValues<TEnum>()
|
||||
.Select(e => e.ToString())
|
||||
.ToList();
|
||||
}
|
||||
@@ -1131,7 +1130,7 @@ public class Utils
|
||||
}
|
||||
|
||||
//Check if it is installed by Windows WinGet
|
||||
if (IsWindows() && basePath.Contains("Users") && basePath.Contains("WinGet"))
|
||||
if (IsWindows() && basePath.Contains("WinGet"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -1441,7 +1440,11 @@ public class Utils
|
||||
if (File.Exists(fileName))
|
||||
{
|
||||
var currentMode = File.GetUnixFileMode(fileName);
|
||||
File.SetUnixFileMode(fileName, currentMode | UnixFileMode.UserExecute | UnixFileMode.GroupExecute | UnixFileMode.OtherExecute);
|
||||
var executableMode = currentMode | UnixFileMode.UserExecute | UnixFileMode.GroupExecute | UnixFileMode.OtherExecute;
|
||||
if (currentMode != executableMode)
|
||||
{
|
||||
File.SetUnixFileMode(fileName, executableMode);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using YamlDotNet.Core;
|
||||
using YamlDotNet.Serialization;
|
||||
using YamlDotNet.Serialization.NamingConventions;
|
||||
using YamlDotNet.RepresentationModel;
|
||||
|
||||
namespace ServiceLib.Common;
|
||||
|
||||
@@ -13,45 +12,57 @@ public class YamlUtils
|
||||
/// <summary>
|
||||
/// Deserialize
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="str"></param>
|
||||
/// <returns></returns>
|
||||
public static T FromYaml<T>(string str)
|
||||
public static YamlMappingNode FromYaml(string str)
|
||||
{
|
||||
var deserializer = new DeserializerBuilder()
|
||||
.WithNamingConvention(PascalCaseNamingConvention.Instance)
|
||||
.Build();
|
||||
//var deserializer = new DeserializerBuilder()
|
||||
// .WithNamingConvention(PascalCaseNamingConvention.Instance)
|
||||
// .Build();
|
||||
//try
|
||||
//{
|
||||
// var obj = deserializer.Deserialize<T>(str);
|
||||
// return obj;
|
||||
//}
|
||||
//catch (Exception ex)
|
||||
//{
|
||||
// Logging.SaveLog(_tag, ex);
|
||||
// return deserializer.Deserialize<T>("");
|
||||
//}
|
||||
try
|
||||
{
|
||||
var obj = deserializer.Deserialize<T>(str);
|
||||
return obj;
|
||||
var yaml = new YamlStream();
|
||||
using var reader = new StringReader(str);
|
||||
yaml.Load(reader);
|
||||
var rootNode = (YamlMappingNode)yaml.Documents[0].RootNode;
|
||||
return rootNode;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
return deserializer.Deserialize<T>("");
|
||||
return new YamlMappingNode();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serialize
|
||||
/// </summary>
|
||||
/// <param name="obj"></param>
|
||||
/// <param name="node"></param>
|
||||
/// <returns></returns>
|
||||
public static string ToYaml(object? obj)
|
||||
public static string ToYaml(YamlMappingNode? node)
|
||||
{
|
||||
var result = string.Empty;
|
||||
if (obj == null)
|
||||
if (node == null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
var serializer = new SerializerBuilder()
|
||||
.WithNamingConvention(HyphenatedNamingConvention.Instance)
|
||||
.Build();
|
||||
|
||||
try
|
||||
{
|
||||
result = serializer.Serialize(obj);
|
||||
var doc = new YamlDocument(node);
|
||||
var stream = new YamlStream(doc);
|
||||
using var writer = new StringWriter();
|
||||
stream.Save(writer, assignAnchors: false);
|
||||
result = writer.ToString();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -64,9 +75,15 @@ public class YamlUtils
|
||||
{
|
||||
try
|
||||
{
|
||||
var mergingParser = new MergingParser(new Parser(new StringReader(str)));
|
||||
var obj = new DeserializerBuilder().Build().Deserialize(mergingParser);
|
||||
return ToYaml(obj);
|
||||
using var reader = new StringReader(str);
|
||||
var parser = new Parser(reader);
|
||||
var mergingParser = new MergingParser(parser);
|
||||
var yamlStream = new YamlStream();
|
||||
yamlStream.Load(mergingParser);
|
||||
using var writer = new StringWriter();
|
||||
yamlStream.Save(writer, assignAnchors: false);
|
||||
|
||||
return writer.ToString();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -42,6 +42,7 @@ public class Global
|
||||
public const string SingboxFakeIPFilterFileName = NamespaceSample + "singbox_fakeip_filter";
|
||||
public const string ChromeRootCertFileName = NamespaceSample + "chrome_roots_pem";
|
||||
public const string MozillaRootCertFileName = NamespaceSample + "mozilla_roots_pem";
|
||||
public const string TaskXmlTemplateFileName = NamespaceSample + "TaskXmlTemplate";
|
||||
|
||||
public const string ChromeRootProvider = "chrome";
|
||||
public const string MozillaRootProvider = "mozilla";
|
||||
@@ -498,6 +499,20 @@ public class Global
|
||||
"localhost"
|
||||
];
|
||||
|
||||
public static readonly List<string> DomainDNS4PreResolveAddress =
|
||||
[
|
||||
"",
|
||||
"https://doh.pub/dns-query",
|
||||
"https://dns.alidns.com/dns-query",
|
||||
"119.29.29.29",
|
||||
"223.5.5.5",
|
||||
"localhost",
|
||||
"https://cloudflare-dns.com/dns-query",
|
||||
"https://dns.google/dns-query",
|
||||
"8.8.8.8",
|
||||
"1.1.1.1",
|
||||
];
|
||||
|
||||
public static readonly List<LanguageOption> LanguageOptions =
|
||||
[
|
||||
new("zh-Hans", "简体中文"),
|
||||
@@ -656,10 +671,10 @@ public class Global
|
||||
{ ECoreType.Xray, "XTLS/Xray-core" },
|
||||
{ ECoreType.sing_box, "SagerNet/sing-box" },
|
||||
{ ECoreType.mihomo, "MetaCubeX/mihomo" },
|
||||
{ ECoreType.hysteria, "apernet/hysteria" },
|
||||
{ ECoreType.hysteria2, "apernet/hysteria" },
|
||||
{ ECoreType.hysteria, "HyNetworks/hysteria" },
|
||||
{ ECoreType.hysteria2, "HyNetworks/hysteria" },
|
||||
{ ECoreType.naiveproxy, "klzgrad/naiveproxy" },
|
||||
{ ECoreType.tuic, "EAimTY/tuic" },
|
||||
{ ECoreType.tuic, "tuic-protocol/tuic" },
|
||||
{ ECoreType.juicity, "juicity/juicity" },
|
||||
{ ECoreType.brook, "txthinking/brook" },
|
||||
{ ECoreType.overtls, "ShadowsocksR-Live/overtls" },
|
||||
|
||||
@@ -87,6 +87,7 @@ public static class AutoStartupHandler
|
||||
[SupportedOSPlatform("windows")]
|
||||
public static void AutoStartTaskService(string taskName, string fileName, string description)
|
||||
{
|
||||
#if false
|
||||
if (taskName.IsNullOrEmpty())
|
||||
{
|
||||
return;
|
||||
@@ -117,6 +118,78 @@ public static class AutoStartupHandler
|
||||
task.Actions.Add(new Microsoft.Win32.TaskScheduler.ExecAction(fileName.AppendQuotes(), null, Path.GetDirectoryName(fileName)));
|
||||
|
||||
taskService.RootFolder.RegisterTaskDefinition(taskName, task);
|
||||
#endif
|
||||
if (string.IsNullOrEmpty(taskName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RunSchtasks($"/Delete /TN \"{taskName}\" /F");
|
||||
|
||||
if (string.IsNullOrEmpty(fileName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var workingDir = Path.GetDirectoryName(fileName) ?? string.Empty;
|
||||
|
||||
var taskXml = EmbedUtils.GetEmbedText(Global.TaskXmlTemplateFileName);
|
||||
|
||||
taskXml = taskXml
|
||||
.Replace("{{DESCRIPTION}}", System.Security.SecurityElement.Escape(description ?? ""))
|
||||
.Replace("{{EXEC_PATH}}", System.Security.SecurityElement.Escape(fileName ?? ""))
|
||||
.Replace("{{ARGUMENTS}}", string.Empty)
|
||||
.Replace("{{WORKING_DIRECTORY}}", System.Security.SecurityElement.Escape(workingDir ?? ""));
|
||||
|
||||
var taskXmlFileName = Utils.GetTempPath($"{Utils.GetGuid(false)}.xml");
|
||||
|
||||
File.WriteAllText(taskXmlFileName, taskXml);
|
||||
|
||||
RunSchtasks(
|
||||
"/Create",
|
||||
"/TN", taskName,
|
||||
"/XML", taskXmlFileName,
|
||||
"/F"
|
||||
);
|
||||
}
|
||||
|
||||
private static bool RunSchtasks(params string[] arguments)
|
||||
{
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = "schtasks.exe",
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
};
|
||||
|
||||
foreach (var argument in arguments)
|
||||
{
|
||||
psi.ArgumentList.Add(argument);
|
||||
}
|
||||
|
||||
using var process = Process.Start(psi);
|
||||
|
||||
if (process == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
process.WaitForExit();
|
||||
|
||||
var output = process.StandardOutput.ReadToEnd();
|
||||
var error = process.StandardError.ReadToEnd();
|
||||
if (!string.IsNullOrEmpty(output))
|
||||
{
|
||||
Logging.SaveLog($"RunSchtasks output: {output}");
|
||||
}
|
||||
if (!string.IsNullOrEmpty(error))
|
||||
{
|
||||
Logging.SaveLog($"RunSchtasks error: {error}");
|
||||
}
|
||||
|
||||
return process.ExitCode == 0;
|
||||
}
|
||||
|
||||
private static string GetAutoRunNameWindows()
|
||||
|
||||
@@ -540,6 +540,67 @@ public static class ConfigHandler
|
||||
return await Task.FromResult(0);
|
||||
}
|
||||
|
||||
public static Task<int> MoveServersTo(
|
||||
Config config,
|
||||
IReadOnlyList<string> orderedProfileIds,
|
||||
IReadOnlyList<int> sourceIndices,
|
||||
int insertIndex)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(orderedProfileIds);
|
||||
ArgumentNullException.ThrowIfNull(sourceIndices);
|
||||
|
||||
if (orderedProfileIds.Count == 0 ||
|
||||
sourceIndices.Count == 0 ||
|
||||
insertIndex < 0 ||
|
||||
insertIndex > orderedProfileIds.Count)
|
||||
{
|
||||
return Task.FromResult(-1);
|
||||
}
|
||||
|
||||
int[] movingIndices = sourceIndices
|
||||
.Distinct()
|
||||
.OrderBy(index => index)
|
||||
.ToArray();
|
||||
|
||||
if (movingIndices.Length != sourceIndices.Count ||
|
||||
movingIndices.Any(index =>
|
||||
index < 0 || index >= orderedProfileIds.Count))
|
||||
{
|
||||
return Task.FromResult(-1);
|
||||
}
|
||||
|
||||
var movingIndexSet = movingIndices.ToHashSet();
|
||||
|
||||
var movingIds = movingIndices
|
||||
.Select(index => orderedProfileIds[index])
|
||||
.ToList();
|
||||
|
||||
var remainingIds = orderedProfileIds
|
||||
.Where((_, index) => !movingIndexSet.Contains(index))
|
||||
.ToList();
|
||||
|
||||
if (insertIndex > remainingIds.Count)
|
||||
{
|
||||
return Task.FromResult(-1);
|
||||
}
|
||||
|
||||
remainingIds.InsertRange(insertIndex, movingIds);
|
||||
|
||||
if (remainingIds.Count != orderedProfileIds.Count)
|
||||
{
|
||||
return Task.FromResult(-1);
|
||||
}
|
||||
|
||||
for (var index = 0; index < remainingIds.Count; index++)
|
||||
{
|
||||
ProfileExManager.Instance.SetSort(
|
||||
remainingIds[index],
|
||||
(index + 1) * 10);
|
||||
}
|
||||
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a custom server configuration from a file
|
||||
/// Copies the configuration file to the app's config directory
|
||||
@@ -1000,10 +1061,6 @@ public static class ConfigHandler
|
||||
profileItem.Address = profileItem.Address.TrimEx();
|
||||
profileItem.Username = profileItem.Username.TrimEx();
|
||||
profileItem.Password = profileItem.Password.TrimEx();
|
||||
profileItem.Fingerprint = string.Empty;
|
||||
profileItem.Alpn = string.Empty;
|
||||
profileItem.Network = string.Empty;
|
||||
profileItem.AllowInsecure = string.Empty;
|
||||
if (profileItem.StreamSecurity.IsNullOrEmpty())
|
||||
{
|
||||
profileItem.StreamSecurity = Global.StreamSecurity;
|
||||
@@ -1656,11 +1713,16 @@ public static class ConfigHandler
|
||||
}
|
||||
|
||||
var subFilter = string.Empty;
|
||||
var preResolver = string.Empty;
|
||||
if (isSub && subid.IsNotEmpty())
|
||||
{
|
||||
subFilter = (await AppManager.Instance.GetSubItem(subid))?.Filter ?? "";
|
||||
preResolver = (await AppManager.Instance.GetSubItem(subid))?.PreResolver ?? "";
|
||||
}
|
||||
|
||||
var dnsClient = preResolver.IsNullOrEmpty() ? null : EndpointResolveManager.BuildDnsClient(preResolver);
|
||||
var dnsCache = new Dictionary<string, string>();
|
||||
|
||||
var countServers = 0;
|
||||
List<ProfileItem> lstAdd = [];
|
||||
var arrData = strData.Split(Environment.NewLine.ToCharArray()).Where(t => !t.IsNullOrEmpty());
|
||||
@@ -1696,6 +1758,21 @@ public static class ConfigHandler
|
||||
profileItem.Subid = subid;
|
||||
profileItem.IsSub = isSub;
|
||||
|
||||
if (dnsClient is not null && profileItem.Address.IsNotEmpty() && !Utils.IsIpAddress(profileItem.Address))
|
||||
{
|
||||
if (!dnsCache.TryGetValue(profileItem.Address, out var resolvedAddress))
|
||||
{
|
||||
var ipList = await dnsClient.ResolveIpv46Async(profileItem.Address);
|
||||
resolvedAddress = ipList.FirstOrDefault()?.ToString() ?? profileItem.Address;
|
||||
dnsCache[profileItem.Address] = resolvedAddress;
|
||||
}
|
||||
if (!resolvedAddress.IsNullOrEmpty())
|
||||
{
|
||||
profileItem.Address = resolvedAddress;
|
||||
}
|
||||
// TODO: Override the host and sni if empty
|
||||
}
|
||||
|
||||
var addStatus = profileItem.ConfigType switch
|
||||
{
|
||||
EConfigType.VMess => await AddVMessServer(config, profileItem, false),
|
||||
@@ -2073,7 +2150,6 @@ public static class ConfigHandler
|
||||
{
|
||||
lstOriSub = await AppManager.Instance.ProfileItems(subid);
|
||||
activeProfile = lstOriSub?.FirstOrDefault(t => t.IndexId == config.IndexId);
|
||||
await RemoveServersViaSubid(config, subid, true);
|
||||
}
|
||||
|
||||
var counter = 0;
|
||||
@@ -2133,6 +2209,11 @@ public static class ConfigHandler
|
||||
counter = await AddBatchServers4Custom(config, strData, subid, isSub);
|
||||
}
|
||||
|
||||
if (counter > 0 && lstOriSub is { Count: > 0 })
|
||||
{
|
||||
await RemoveServers(config, lstOriSub.Where(item => item.IsSub).ToList());
|
||||
}
|
||||
|
||||
//Select active node
|
||||
if (activeProfile != null)
|
||||
{
|
||||
@@ -2236,6 +2317,7 @@ public static class ConfigHandler
|
||||
item.PreSocksPort = subItem.PreSocksPort;
|
||||
item.Memo = subItem.Memo;
|
||||
item.CustomCoreType = subItem.CustomCoreType;
|
||||
item.PreResolver = subItem.PreResolver;
|
||||
}
|
||||
|
||||
if (item.Id.IsNullOrEmpty())
|
||||
|
||||
@@ -80,12 +80,7 @@ public class BaseFmt
|
||||
{
|
||||
var node = JsonUtils.ParseJson(item.Finalmask);
|
||||
var finalmask = node != null
|
||||
? JsonUtils.Serialize(node, new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = false,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||
})
|
||||
? JsonUtils.Serialize(node, indented: false, nullValue: true)
|
||||
: item.Finalmask;
|
||||
dicQuery.Add("fm", Utils.UrlEncode(finalmask));
|
||||
}
|
||||
@@ -154,12 +149,7 @@ public class BaseFmt
|
||||
{
|
||||
var node = JsonUtils.ParseJson(transport.XhttpExtra);
|
||||
var extra = node != null
|
||||
? JsonUtils.Serialize(node, new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = false,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||
})
|
||||
? JsonUtils.Serialize(node, indented: false, nullValue: true)
|
||||
: transport.XhttpExtra;
|
||||
dicQuery.Add("extra", UrlEncodeSafe(extra));
|
||||
}
|
||||
@@ -215,12 +205,7 @@ public class BaseFmt
|
||||
{
|
||||
var node = JsonUtils.ParseJson(finalmaskDecoded);
|
||||
item.Finalmask = node != null
|
||||
? JsonUtils.Serialize(node, new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||
})
|
||||
? JsonUtils.Serialize(node, indented: true, nullValue: true)
|
||||
: finalmaskDecoded;
|
||||
}
|
||||
else
|
||||
@@ -278,12 +263,7 @@ public class BaseFmt
|
||||
var node = JsonUtils.ParseJson(xhttpExtra);
|
||||
if (node != null)
|
||||
{
|
||||
xhttpExtra = JsonUtils.Serialize(node, new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||
});
|
||||
xhttpExtra = JsonUtils.Serialize(node, indented: true, nullValue: true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -179,12 +179,7 @@ public class InnerFmt : BaseFmt
|
||||
if (jsonObj.TryGetPropertyValue("CustomOutboundObj", out var customOutboundNode)
|
||||
&& customOutboundNode is JsonObject customOutboundObj)
|
||||
{
|
||||
var customOutboundContent = JsonUtils.Serialize(customOutboundObj, new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||
});
|
||||
var customOutboundContent = JsonUtils.Serialize(customOutboundObj, indented: true, nullValue: true);
|
||||
customOutboundFilePath = WriteAllText(customOutboundContent);
|
||||
jsonObj.Remove("CustomOutboundObj");
|
||||
}
|
||||
|
||||
@@ -29,12 +29,7 @@ public class WireguardFmt : BaseFmt
|
||||
{
|
||||
var node = JsonUtils.ParseJson(finalmaskDecoded);
|
||||
item.Finalmask = node != null
|
||||
? JsonUtils.Serialize(node, new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||
})
|
||||
? JsonUtils.Serialize(node, indented: true, nullValue: true)
|
||||
: finalmaskDecoded;
|
||||
}
|
||||
else
|
||||
@@ -98,12 +93,7 @@ public class WireguardFmt : BaseFmt
|
||||
{
|
||||
var node = JsonUtils.ParseJson(item.Finalmask);
|
||||
var finalmask = node != null
|
||||
? JsonUtils.Serialize(node, new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = false,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||
})
|
||||
? JsonUtils.Serialize(node, indented: false, nullValue: true)
|
||||
: item.Finalmask;
|
||||
dicQuery.Add("fm", Utils.UrlEncode(finalmask));
|
||||
}
|
||||
|
||||
@@ -133,7 +133,7 @@ public static class ProxySettingWindows
|
||||
list.dwOptionCount = options.Length;
|
||||
list.dwOptionError = 0;
|
||||
|
||||
var optSize = Marshal.SizeOf(typeof(InternetConnectionOption));
|
||||
var optSize = Marshal.SizeOf<InternetConnectionOption>();
|
||||
// make a pointer out of all that ...
|
||||
var optionsPtr = Marshal.AllocCoTaskMem(optSize * options.Length); // !! remember to deallocate memory 4
|
||||
// copy the array over into that spot in memory ...
|
||||
@@ -207,17 +207,17 @@ public static class ProxySettingWindows
|
||||
var entries = 0;
|
||||
// attempt to query with 1 entry buffer
|
||||
var rasEntryNames = new RASENTRYNAME[1];
|
||||
var bufferSize = Marshal.SizeOf(typeof(RASENTRYNAME));
|
||||
rasEntryNames[0].dwSize = Marshal.SizeOf(typeof(RASENTRYNAME));
|
||||
var bufferSize = Marshal.SizeOf<RASENTRYNAME>();
|
||||
rasEntryNames[0].dwSize = Marshal.SizeOf<RASENTRYNAME>();
|
||||
|
||||
var result = NativeMethods.RasEnumEntries(null, null, rasEntryNames, ref bufferSize, ref entries);
|
||||
// increase buffer if the buffer is not large enough
|
||||
if (result == (uint)ErrorCode.ERROR_BUFFER_TOO_SMALL)
|
||||
{
|
||||
rasEntryNames = new RASENTRYNAME[bufferSize / Marshal.SizeOf(typeof(RASENTRYNAME))];
|
||||
rasEntryNames = new RASENTRYNAME[bufferSize / Marshal.SizeOf<RASENTRYNAME>()];
|
||||
for (var i = 0; i < rasEntryNames.Length; i++)
|
||||
{
|
||||
rasEntryNames[i].dwSize = Marshal.SizeOf(typeof(RASENTRYNAME));
|
||||
rasEntryNames[i].dwSize = Marshal.SizeOf<RASENTRYNAME>();
|
||||
}
|
||||
|
||||
result = NativeMethods.RasEnumEntries(null, null, rasEntryNames, ref bufferSize, ref entries);
|
||||
@@ -258,7 +258,7 @@ public static class ProxySettingWindows
|
||||
|
||||
static InternetConnectionOption()
|
||||
{
|
||||
Size = Marshal.SizeOf(typeof(InternetConnectionOption));
|
||||
Size = Marshal.SizeOf<InternetConnectionOption>();
|
||||
}
|
||||
|
||||
// Nested Types
|
||||
|
||||
@@ -28,6 +28,11 @@ public sealed class SQLiteHelper
|
||||
return await _dbAsync.InsertAllAsync(models, runInTransaction: true).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<int> ReplaceAllAsync(IEnumerable models)
|
||||
{
|
||||
return await _dbAsync.InsertAllAsync(models, extra: "OR REPLACE", runInTransaction: true).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<int> InsertAsync(object model)
|
||||
{
|
||||
return await _dbAsync.InsertAsync(model);
|
||||
|
||||
@@ -0,0 +1,523 @@
|
||||
using System.Net.Http.Headers;
|
||||
|
||||
namespace ServiceLib.Manager;
|
||||
|
||||
public class EndpointResolveManager
|
||||
{
|
||||
public static IDnsResolver BuildDnsClient(string dnsServer)
|
||||
{
|
||||
if (string.IsNullOrEmpty(dnsServer))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(dnsServer));
|
||||
}
|
||||
|
||||
if (dnsServer is "local" or "localhost" or "system" or "dhcp" ||
|
||||
dnsServer.StartsWith("127.0.0.1") ||
|
||||
dnsServer.StartsWith("::1"))
|
||||
{
|
||||
return new SystemDnsResolver();
|
||||
}
|
||||
|
||||
var (domain, scheme, port, path) = Utils.ParseUrl(dnsServer);
|
||||
|
||||
if (string.IsNullOrEmpty(scheme))
|
||||
{
|
||||
return new UdpDnsResolver(new IPEndPoint(IPAddress.Parse(domain), 53));
|
||||
}
|
||||
|
||||
var processedScheme = scheme.ToLowerInvariant().Replace("+local", "");
|
||||
|
||||
switch (processedScheme)
|
||||
{
|
||||
case "udp":
|
||||
return new UdpDnsResolver(new IPEndPoint(IPAddress.Parse(domain), port != 0 ? port : 53));
|
||||
|
||||
case "tcp":
|
||||
return new TcpDnsResolver(new IPEndPoint(IPAddress.Parse(domain), port != 0 ? port : 53));
|
||||
|
||||
case "doh" or "https":
|
||||
{
|
||||
var uri = new Uri($"https://{domain}:{(port != 0 ? port : 443)}{path}");
|
||||
var httpClient = new HttpClient();
|
||||
return new DohDnsResolver(httpClient, uri);
|
||||
}
|
||||
|
||||
default:
|
||||
throw new NotSupportedException($"Unsupported DNS scheme: {scheme}");
|
||||
}
|
||||
}
|
||||
|
||||
public class DnsWire
|
||||
{
|
||||
#region Build DNS Query
|
||||
|
||||
public static byte[] BuildQuery(string host, ushort queryType)
|
||||
{
|
||||
if (queryType is not (1 or 28))
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(queryType));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(host))
|
||||
{
|
||||
throw new ArgumentException("Host is empty.", nameof(host));
|
||||
}
|
||||
|
||||
host = host.TrimEnd('.');
|
||||
|
||||
if (host.Length == 0)
|
||||
{
|
||||
host = ".";
|
||||
}
|
||||
|
||||
using var stream = new MemoryStream();
|
||||
|
||||
// Header
|
||||
WriteUInt16(stream, 0);
|
||||
WriteUInt16(stream, 0x0100);
|
||||
WriteUInt16(stream, 1);
|
||||
WriteUInt16(stream, 0);
|
||||
WriteUInt16(stream, 0);
|
||||
WriteUInt16(stream, 0);
|
||||
|
||||
// Question: QNAME
|
||||
if (host != ".")
|
||||
{
|
||||
var labels = host.Split('.');
|
||||
foreach (var label in labels)
|
||||
{
|
||||
if (label.Length is < 1 or > 63)
|
||||
{
|
||||
throw new ArgumentException($"Invalid DNS label length: {label.Length}.", nameof(host));
|
||||
}
|
||||
|
||||
foreach (var c in label)
|
||||
{
|
||||
if (c > 0x7F)
|
||||
{
|
||||
throw new ArgumentException("Non-ASCII domain names must be converted to A-labels first.",
|
||||
nameof(host));
|
||||
}
|
||||
}
|
||||
|
||||
stream.WriteByte((byte)label.Length);
|
||||
|
||||
var labelBytes = Encoding.ASCII.GetBytes(label);
|
||||
stream.Write(labelBytes, 0, labelBytes.Length);
|
||||
}
|
||||
}
|
||||
|
||||
stream.WriteByte(0);
|
||||
|
||||
// Question: QTYPE & QCLASS
|
||||
WriteUInt16(stream, queryType);
|
||||
WriteUInt16(stream, 1);
|
||||
|
||||
return stream.ToArray();
|
||||
}
|
||||
|
||||
private static void WriteUInt16(Stream stream, ushort value)
|
||||
{
|
||||
stream.WriteByte((byte)(value >> 8));
|
||||
stream.WriteByte((byte)(value & 0xFF));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Parse DNS Response
|
||||
|
||||
public static List<IPAddress> ParseAddresses(
|
||||
ReadOnlySpan<byte> message,
|
||||
ushort expectedType)
|
||||
{
|
||||
return ParseResponse(message, expectedType).Addresses;
|
||||
}
|
||||
|
||||
public static DnsResponse ParseResponse(
|
||||
ReadOnlySpan<byte> message,
|
||||
ushort expectedType)
|
||||
{
|
||||
if (expectedType is not (1 or 28))
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(expectedType));
|
||||
}
|
||||
|
||||
if (message.Length < 12)
|
||||
{
|
||||
throw new InvalidDataException("DNS message is too short.");
|
||||
}
|
||||
|
||||
var flags = ReadUInt16(message, 2);
|
||||
var questionCount = ReadUInt16(message, 4);
|
||||
var answerCount = ReadUInt16(message, 6);
|
||||
|
||||
// QR = 1
|
||||
if ((flags & 0x8000) == 0)
|
||||
{
|
||||
throw new InvalidDataException("Not a DNS response.");
|
||||
}
|
||||
|
||||
// RCODE
|
||||
var rcode = flags & 0x000F;
|
||||
if (rcode != 0)
|
||||
{
|
||||
return new DnsResponse([], null);
|
||||
}
|
||||
|
||||
var offset = 12;
|
||||
|
||||
// Parse the Question section to obtain the query name.
|
||||
string? queryName = null;
|
||||
for (var i = 0; i < questionCount; i++)
|
||||
{
|
||||
var name = ReadName(message, offset, out offset);
|
||||
queryName ??= NormalizeName(name);
|
||||
|
||||
if (offset + 4 > message.Length)
|
||||
{
|
||||
throw new InvalidDataException("Invalid DNS question.");
|
||||
}
|
||||
|
||||
offset += 4; // QTYPE + QCLASS
|
||||
}
|
||||
|
||||
var addresses = new List<IPAddress>();
|
||||
string? cname = null;
|
||||
|
||||
for (var i = 0; i < answerCount; i++)
|
||||
{
|
||||
// NAME
|
||||
var ownerName = ReadName(message, offset, out offset);
|
||||
|
||||
if (offset + 10 > message.Length)
|
||||
{
|
||||
throw new InvalidDataException("Invalid DNS answer.");
|
||||
}
|
||||
|
||||
var type = ReadUInt16(message, offset);
|
||||
var @class = ReadUInt16(message, offset + 2);
|
||||
// TTL
|
||||
var rdLength = ReadUInt16(message, offset + 8);
|
||||
|
||||
offset += 10;
|
||||
|
||||
if (offset + rdLength > message.Length)
|
||||
{
|
||||
throw new InvalidDataException("Invalid DNS RDATA.");
|
||||
}
|
||||
|
||||
if (@class != 1)
|
||||
{
|
||||
offset += rdLength;
|
||||
continue;
|
||||
}
|
||||
|
||||
var ownerMatchesQuery = NamesEqual(ownerName, queryName);
|
||||
var ownerMatchesCname = NamesEqual(ownerName, cname);
|
||||
|
||||
if (type == expectedType)
|
||||
{
|
||||
if (ownerMatchesQuery || ownerMatchesCname)
|
||||
{
|
||||
if (expectedType == 1 && rdLength == 4)
|
||||
{
|
||||
addresses.Add(new IPAddress(
|
||||
message.Slice(offset, 4)));
|
||||
}
|
||||
else if (expectedType == 28 && rdLength == 16)
|
||||
{
|
||||
addresses.Add(new IPAddress(
|
||||
message.Slice(offset, 16)));
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (type == 5 && ownerMatchesQuery && cname is null)
|
||||
{
|
||||
cname = ReadName(message, offset, out _);
|
||||
}
|
||||
|
||||
offset += rdLength;
|
||||
}
|
||||
|
||||
return new DnsResponse(addresses, cname);
|
||||
}
|
||||
|
||||
public static async Task<List<IPAddress>> ResolveWithCnameAsync(
|
||||
string host,
|
||||
bool ipv6,
|
||||
Func<string, ushort, CancellationToken, Task<byte[]>> queryAsync,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(host))
|
||||
{
|
||||
throw new ArgumentException("Host is empty.", nameof(host));
|
||||
}
|
||||
|
||||
var queryType = ipv6 ? (ushort)28 : (ushort)1;
|
||||
var currentName = host.TrimEnd('.');
|
||||
var visited = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
const int maxHops = 8;
|
||||
|
||||
for (var hop = 0; hop < maxHops; hop++)
|
||||
{
|
||||
if (!visited.Add(currentName))
|
||||
{
|
||||
// CNAME loop detected.
|
||||
return [];
|
||||
}
|
||||
|
||||
var response = await queryAsync(currentName, queryType, cancellationToken);
|
||||
var result = ParseResponse(response, queryType);
|
||||
|
||||
if (result.Addresses.Count > 0)
|
||||
{
|
||||
return result.Addresses;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(result.Cname))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
currentName = result.Cname.TrimEnd('.');
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
public static string ReadName(
|
||||
ReadOnlySpan<byte> message,
|
||||
int offset,
|
||||
out int nextOffset)
|
||||
{
|
||||
var name = new StringBuilder();
|
||||
var position = offset;
|
||||
var endOffset = -1;
|
||||
var pointerCount = 0;
|
||||
|
||||
while (true)
|
||||
{
|
||||
if ((uint)position >= (uint)message.Length)
|
||||
{
|
||||
throw new InvalidDataException("Invalid DNS name.");
|
||||
}
|
||||
|
||||
var length = message[position];
|
||||
|
||||
// End of name.
|
||||
if (length == 0)
|
||||
{
|
||||
position++;
|
||||
if (endOffset < 0)
|
||||
{
|
||||
endOffset = position;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Compression pointer.
|
||||
if ((length & 0xC0) == 0xC0)
|
||||
{
|
||||
if (position + 1 >= message.Length)
|
||||
{
|
||||
throw new InvalidDataException("Invalid DNS pointer.");
|
||||
}
|
||||
|
||||
if (++pointerCount > 64)
|
||||
{
|
||||
throw new InvalidDataException("Too many DNS compression pointers.");
|
||||
}
|
||||
|
||||
if (endOffset < 0)
|
||||
{
|
||||
endOffset = position + 2;
|
||||
}
|
||||
|
||||
var pointer = ((length & 0x3F) << 8) | message[position + 1];
|
||||
position = pointer;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Reserved label types.
|
||||
if ((length & 0xC0) != 0)
|
||||
{
|
||||
throw new InvalidDataException("Invalid DNS label.");
|
||||
}
|
||||
|
||||
position++;
|
||||
if (position + length > message.Length)
|
||||
{
|
||||
throw new InvalidDataException("Invalid DNS label length.");
|
||||
}
|
||||
|
||||
if (name.Length > 0)
|
||||
{
|
||||
name.Append('.');
|
||||
}
|
||||
|
||||
name.Append(Encoding.ASCII.GetString(message.Slice(position, length)));
|
||||
position += length;
|
||||
}
|
||||
|
||||
nextOffset = endOffset;
|
||||
return name.ToString();
|
||||
}
|
||||
|
||||
public sealed record DnsResponse(List<IPAddress> Addresses, string? Cname);
|
||||
|
||||
private static string? NormalizeName(string name)
|
||||
{
|
||||
name = name.TrimEnd('.');
|
||||
return name.Length == 0 ? null : name;
|
||||
}
|
||||
|
||||
private static bool NamesEqual(string? left, string? right)
|
||||
{
|
||||
return left is not null &&
|
||||
right is not null &&
|
||||
string.Equals(
|
||||
left.TrimEnd('.'),
|
||||
right.TrimEnd('.'),
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static ushort ReadUInt16(
|
||||
ReadOnlySpan<byte> data,
|
||||
int offset)
|
||||
{
|
||||
return (ushort)(
|
||||
(data[offset] << 8) |
|
||||
data[offset + 1]);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
public interface IDnsResolver
|
||||
{
|
||||
public Task<List<IPAddress>> ResolveAsync(
|
||||
string host,
|
||||
bool ipv6,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
public async Task<List<IPAddress>> ResolveIpv46Async(
|
||||
string host,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var ipv4Task = ResolveAsync(host, false, cancellationToken);
|
||||
var ipv6Task = ResolveAsync(host, true, cancellationToken);
|
||||
var results = await Task.WhenAll(ipv4Task, ipv6Task);
|
||||
return results.SelectMany(x => x).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public class UdpDnsResolver(IPEndPoint dnsServer) : IDnsResolver
|
||||
{
|
||||
public async Task<List<IPAddress>> ResolveAsync(
|
||||
string host,
|
||||
bool ipv6,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await DnsWire.ResolveWithCnameAsync(
|
||||
host,
|
||||
ipv6,
|
||||
async (name, queryType, ct) =>
|
||||
{
|
||||
var query = DnsWire.BuildQuery(name, queryType);
|
||||
using var udpClient = new UdpClient();
|
||||
udpClient.Connect(dnsServer);
|
||||
await udpClient.SendAsync(query, query.Length);
|
||||
var result = await udpClient.ReceiveAsync(ct);
|
||||
return result.Buffer;
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
public class TcpDnsResolver(IPEndPoint dnsServer) : IDnsResolver
|
||||
{
|
||||
public async Task<List<IPAddress>> ResolveAsync(
|
||||
string host,
|
||||
bool ipv6,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await DnsWire.ResolveWithCnameAsync(
|
||||
host,
|
||||
ipv6,
|
||||
async (name, queryType, ct) =>
|
||||
{
|
||||
var query = DnsWire.BuildQuery(name, queryType);
|
||||
using var tcpClient = new TcpClient();
|
||||
await tcpClient.ConnectAsync(dnsServer.Address, dnsServer.Port, ct);
|
||||
await using var networkStream = tcpClient.GetStream();
|
||||
await networkStream.WriteAsync(query, 0, query.Length, ct);
|
||||
// Read the length of the response (2 bytes)
|
||||
var lengthBuffer = new byte[2];
|
||||
await networkStream.ReadExactlyAsync(lengthBuffer, 0, 2, ct);
|
||||
var responseLength = (lengthBuffer[0] << 8) | lengthBuffer[1];
|
||||
// Read the actual response
|
||||
var responseBuffer = new byte[responseLength];
|
||||
await networkStream.ReadExactlyAsync(responseBuffer, 0, responseLength, ct);
|
||||
return responseBuffer;
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
public class DohDnsResolver(HttpClient httpClient, Uri dohServer) : IDnsResolver
|
||||
{
|
||||
public async Task<List<IPAddress>> ResolveAsync(
|
||||
string host,
|
||||
bool ipv6,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await DnsWire.ResolveWithCnameAsync(
|
||||
host,
|
||||
ipv6,
|
||||
async (name, queryType, ct) =>
|
||||
{
|
||||
var query = DnsWire.BuildQuery(name, queryType);
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, dohServer);
|
||||
request.Content = new ByteArrayContent(query)
|
||||
{
|
||||
Headers =
|
||||
{
|
||||
ContentType = new MediaTypeHeaderValue("application/dns-message"),
|
||||
},
|
||||
};
|
||||
|
||||
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/dns-message"));
|
||||
|
||||
using var response =
|
||||
await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, ct);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
return await response.Content.ReadAsByteArrayAsync(ct);
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
public class SystemDnsResolver : IDnsResolver
|
||||
{
|
||||
public async Task<List<IPAddress>> ResolveAsync(
|
||||
string host,
|
||||
bool ipv6,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var addresses = await Dns.GetHostAddressesAsync(host, cancellationToken);
|
||||
return addresses.Where(ip => (!ipv6 && ip.AddressFamily == AddressFamily.InterNetwork) ||
|
||||
(ipv6 && ip.AddressFamily == AddressFamily.InterNetworkV6))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public async Task<List<IPAddress>> ResolveIpv46Async(
|
||||
string host,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var addresses = await Dns.GetHostAddressesAsync(host, cancellationToken);
|
||||
return addresses.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,11 +65,17 @@ public class PacManager
|
||||
{
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
using var client = await listener.AcceptTcpClientAsync(token).ConfigureAwait(false);
|
||||
await using var stream = client.GetStream();
|
||||
_ = await stream.ReadAsync(buffer, token).ConfigureAwait(false);
|
||||
await stream.WriteAsync(_writeContent, token).ConfigureAwait(false);
|
||||
await stream.FlushAsync(token).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
using var client = await listener.AcceptTcpClientAsync(token).ConfigureAwait(false);
|
||||
await using var stream = client.GetStream();
|
||||
_ = await stream.ReadAsync(buffer, token).ConfigureAwait(false);
|
||||
await stream.WriteAsync(_writeContent, token).ConfigureAwait(false);
|
||||
await stream.FlushAsync(token).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (token.IsCancellationRequested)
|
||||
|
||||
@@ -4,7 +4,8 @@ public class ProfileExManager
|
||||
{
|
||||
private static readonly Lazy<ProfileExManager> _instance = new(() => new());
|
||||
private ConcurrentBag<ProfileExItem> _lstProfileEx = [];
|
||||
private readonly Queue<string> _queIndexIds = new();
|
||||
private readonly ConcurrentQueue<string> _queIndexIds = new();
|
||||
private readonly Lock _profileExLock = new();
|
||||
public static ProfileExManager Instance => _instance.Value;
|
||||
private static readonly string _tag = "ProfileExHandler";
|
||||
|
||||
@@ -27,56 +28,51 @@ public class ProfileExManager
|
||||
{
|
||||
await SQLiteHelper.Instance.ExecuteAsync($"delete from ProfileExItem where indexId not in ( select indexId from ProfileItem )");
|
||||
|
||||
_lstProfileEx = new(await SQLiteHelper.Instance.TableAsync<ProfileExItem>().ToListAsync());
|
||||
var lstProfileEx = new ConcurrentBag<ProfileExItem>(await SQLiteHelper.Instance.TableAsync<ProfileExItem>().ToListAsync());
|
||||
lock (_profileExLock)
|
||||
{
|
||||
_lstProfileEx = lstProfileEx;
|
||||
}
|
||||
}
|
||||
|
||||
private void IndexIdEnqueue(string indexId)
|
||||
{
|
||||
if (indexId.IsNotEmpty() && !_queIndexIds.Contains(indexId))
|
||||
if (indexId.IsNotEmpty())
|
||||
{
|
||||
//Speed tests touch this from many threads; the save loop deduplicates IDs.
|
||||
_queIndexIds.Enqueue(indexId);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SaveQueueIndexIds()
|
||||
{
|
||||
var cnt = _queIndexIds.Count;
|
||||
if (cnt > 0)
|
||||
var lstIds = new HashSet<string>();
|
||||
while (_queIndexIds.TryDequeue(out var id))
|
||||
{
|
||||
var lstExists = await SQLiteHelper.Instance.TableAsync<ProfileExItem>().ToListAsync();
|
||||
List<ProfileExItem> lstInserts = [];
|
||||
List<ProfileExItem> lstUpdates = [];
|
||||
lstIds.Add(id);
|
||||
}
|
||||
|
||||
for (var i = 0; i < cnt; i++)
|
||||
if (lstIds.Count > 0)
|
||||
{
|
||||
List<ProfileExItem> lstSaves = [];
|
||||
|
||||
foreach (var id in lstIds)
|
||||
{
|
||||
var id = _queIndexIds.Dequeue();
|
||||
var item = lstExists.FirstOrDefault(t => t.IndexId == id);
|
||||
var itemNew = _lstProfileEx?.FirstOrDefault(t => t.IndexId == id);
|
||||
if (itemNew is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item is not null)
|
||||
{
|
||||
lstUpdates.Add(itemNew);
|
||||
}
|
||||
else
|
||||
{
|
||||
lstInserts.Add(itemNew);
|
||||
}
|
||||
lstSaves.Add(itemNew);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (lstInserts.Count > 0)
|
||||
if (lstSaves.Count > 0)
|
||||
{
|
||||
await SQLiteHelper.Instance.InsertAllAsync(lstInserts);
|
||||
}
|
||||
|
||||
if (lstUpdates.Count > 0)
|
||||
{
|
||||
await SQLiteHelper.Instance.UpdateAllAsync(lstUpdates);
|
||||
//Another save may have inserted these rows since the queue was drained.
|
||||
await SQLiteHelper.Instance.ReplaceAllAsync(lstSaves);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -103,13 +99,26 @@ public class ProfileExManager
|
||||
|
||||
private ProfileExItem GetProfileExItem(string? indexId)
|
||||
{
|
||||
return _lstProfileEx.FirstOrDefault(t => t.IndexId == indexId) ?? AddProfileEx(indexId);
|
||||
var profileEx = _lstProfileEx.FirstOrDefault(t => t.IndexId == indexId);
|
||||
if (profileEx is not null)
|
||||
{
|
||||
return profileEx;
|
||||
}
|
||||
|
||||
lock (_profileExLock)
|
||||
{
|
||||
profileEx = _lstProfileEx.FirstOrDefault(t => t.IndexId == indexId);
|
||||
return profileEx ?? AddProfileEx(indexId);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ClearAll()
|
||||
{
|
||||
await SQLiteHelper.Instance.ExecuteAsync($"delete from ProfileExItem ");
|
||||
_lstProfileEx = [];
|
||||
lock (_profileExLock)
|
||||
{
|
||||
_lstProfileEx = [];
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SaveTo()
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
namespace ServiceLib.Models.Configs;
|
||||
|
||||
[Serializable]
|
||||
public class Config
|
||||
{
|
||||
#region property
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
namespace ServiceLib.Models.Configs;
|
||||
|
||||
[Serializable]
|
||||
public class CoreBasicItem
|
||||
{
|
||||
public bool LogEnabled { get; set; }
|
||||
@@ -22,7 +21,6 @@ public class CoreBasicItem
|
||||
public bool EnableCacheFile4Sbox { get; set; } = true;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class InItem
|
||||
{
|
||||
public int LocalPort { get; set; }
|
||||
@@ -38,7 +36,6 @@ public class InItem
|
||||
public bool SecondLocalPortEnabled { get; set; }
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class KcpItem
|
||||
{
|
||||
public int Mtu { get; set; }
|
||||
@@ -54,7 +51,6 @@ public class KcpItem
|
||||
public int MaxSendingWindow { get; set; }
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class GrpcItem
|
||||
{
|
||||
public int? IdleTimeout { get; set; }
|
||||
@@ -63,7 +59,6 @@ public class GrpcItem
|
||||
public int? InitialWindowsSize { get; set; }
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class GUIItem
|
||||
{
|
||||
public bool AutoRun { get; set; }
|
||||
@@ -77,14 +72,12 @@ public class GUIItem
|
||||
public string? RootCertProvider { get; set; }
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class MsgUIItem
|
||||
{
|
||||
public string? MainMsgFilter { get; set; }
|
||||
public bool? AutoRefresh { get; set; }
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class UIItem
|
||||
{
|
||||
public bool EnableAutoAdjustMainLvColWidth { get; set; }
|
||||
@@ -106,7 +99,6 @@ public class UIItem
|
||||
public bool HideColumnIpInfo { get; set; }
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class ConstItem
|
||||
{
|
||||
public string? SubConvertUrl { get; set; }
|
||||
@@ -115,7 +107,6 @@ public class ConstItem
|
||||
public string? RouteRulesTemplateSourceUrl { get; set; }
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class KeyEventItem
|
||||
{
|
||||
public EGlobalHotkey EGlobalHotkey { get; set; }
|
||||
@@ -129,7 +120,6 @@ public class KeyEventItem
|
||||
public int? KeyCode { get; set; }
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class CoreTypeItem
|
||||
{
|
||||
public EConfigType ConfigType { get; set; }
|
||||
@@ -137,7 +127,6 @@ public class CoreTypeItem
|
||||
public ECoreType CoreType { get; set; }
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class TunModeItem
|
||||
{
|
||||
public bool EnableTun { get; set; }
|
||||
@@ -153,7 +142,6 @@ public class TunModeItem
|
||||
public string IPv6Address { get; set; }
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class SpeedTestItem
|
||||
{
|
||||
public int SpeedTestTimeout { get; set; }
|
||||
@@ -166,7 +154,6 @@ public class SpeedTestItem
|
||||
public int? SpeedTestDelayInterval { get; set; }
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class RoutingBasicItem
|
||||
{
|
||||
public string DomainStrategy { get; set; }
|
||||
@@ -174,7 +161,6 @@ public class RoutingBasicItem
|
||||
public string RoutingIndexId { get; set; }
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class ColumnItem
|
||||
{
|
||||
public string Name { get; set; }
|
||||
@@ -182,7 +168,6 @@ public class ColumnItem
|
||||
public int Index { get; set; }
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class Mux4RayItem
|
||||
{
|
||||
public int? Concurrency { get; set; }
|
||||
@@ -190,7 +175,6 @@ public class Mux4RayItem
|
||||
public string? XudpProxyUDP443 { get; set; }
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class Mux4SboxItem
|
||||
{
|
||||
public string Protocol { get; set; }
|
||||
@@ -198,7 +182,6 @@ public class Mux4SboxItem
|
||||
public bool? Padding { get; set; }
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class HysteriaItem
|
||||
{
|
||||
public int UpMbps { get; set; }
|
||||
@@ -206,7 +189,6 @@ public class HysteriaItem
|
||||
public int HopInterval { get; set; } = Global.Hysteria2DefaultHopInt;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class ClashUIItem
|
||||
{
|
||||
public bool EnableIPv6 { get; set; }
|
||||
@@ -219,7 +201,6 @@ public class ClashUIItem
|
||||
public List<ColumnItem> ConnectionsColumnItem { get; set; }
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class SystemProxyItem
|
||||
{
|
||||
public ESysProxyType SysProxyType { get; set; }
|
||||
@@ -230,7 +211,6 @@ public class SystemProxyItem
|
||||
public string? CustomSystemProxyScriptPath { get; set; }
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class WebDavItem
|
||||
{
|
||||
public string? Url { get; set; }
|
||||
@@ -239,7 +219,6 @@ public class WebDavItem
|
||||
public string? DirName { get; set; }
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class CheckUpdateItem
|
||||
{
|
||||
public bool UpdateViaProxy { get; set; } = true;
|
||||
@@ -247,7 +226,6 @@ public class CheckUpdateItem
|
||||
public List<string>? CheckPreReleaseCoreTypes { get; set; }
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class Fragment4RayItem
|
||||
{
|
||||
public string? Packets { get; set; }
|
||||
@@ -262,7 +240,6 @@ public class Fragment4RayItem
|
||||
// migration end
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class WindowSizeItem
|
||||
{
|
||||
public string TypeName { get; set; }
|
||||
@@ -270,7 +247,6 @@ public class WindowSizeItem
|
||||
public int Height { get; set; }
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class SimpleDNSItem
|
||||
{
|
||||
public bool? UseSystemHosts { get; set; }
|
||||
@@ -293,7 +269,6 @@ public class SimpleDNSItem
|
||||
public bool? EnableHappyEyeballs { get; set; }
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class HappyEyeballs4RayItem
|
||||
{
|
||||
public int? TryDelayMs { get; set; }
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
namespace ServiceLib.Models.CoreConfigs;
|
||||
|
||||
[Serializable]
|
||||
public class CoreInfo
|
||||
{
|
||||
public ECoreType CoreType { get; set; }
|
||||
|
||||
@@ -44,7 +44,6 @@ public class Route4Sbox
|
||||
public string? final { get; set; }
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class Rule4Sbox
|
||||
{
|
||||
public string? outbound { get; set; }
|
||||
@@ -103,7 +102,6 @@ public class Rule4Sbox
|
||||
public bool? speculative { get; set; }
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class Inbound4Sbox
|
||||
{
|
||||
public string type { get; set; }
|
||||
@@ -232,7 +230,7 @@ public class Reality4Sbox
|
||||
public class Transport4Sbox
|
||||
{
|
||||
public string? type { get; set; }
|
||||
public object? host { get; set; }
|
||||
public JsonNode? host { get; set; }
|
||||
public string? path { get; set; }
|
||||
public Headers4Sbox? headers { get; set; }
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ namespace ServiceLib.Models.CoreConfigs;
|
||||
public class V2rayConfig
|
||||
{
|
||||
public Log4Ray log { get; set; }
|
||||
public object dns { get; set; }
|
||||
public JsonObject? dns { get; set; }
|
||||
public FakeDns4Ray? fakedns { get; set; }
|
||||
public List<Inbounds4Ray> inbounds { get; set; }
|
||||
public List<Outbounds4Ray> outbounds { get; set; }
|
||||
@@ -130,7 +130,7 @@ public class Outboundsettings4Ray
|
||||
|
||||
public string? secretKey { get; set; }
|
||||
|
||||
public object? address { get; set; }
|
||||
public JsonNode? address { get; set; }
|
||||
|
||||
public int? port { get; set; }
|
||||
|
||||
@@ -142,7 +142,7 @@ public class Outboundsettings4Ray
|
||||
|
||||
public string? email { get; set; }
|
||||
|
||||
public object? headers { get; set; }
|
||||
public JsonNode? headers { get; set; }
|
||||
|
||||
public List<WireguardPeer4Ray>? peers { get; set; }
|
||||
|
||||
@@ -199,8 +199,8 @@ public class Response4Ray
|
||||
|
||||
public class Dns4Ray
|
||||
{
|
||||
public Dictionary<string, object>? hosts { get; set; }
|
||||
public List<object> servers { get; set; }
|
||||
public Dictionary<string, JsonNode>? hosts { get; set; }
|
||||
public List<JsonNode> servers { get; set; }
|
||||
public bool? serveStale { get; set; }
|
||||
public bool? enableParallelQuery { get; set; }
|
||||
public string? queryStrategy { get; set; }
|
||||
@@ -214,6 +214,7 @@ public class DnsServer4Ray
|
||||
public List<string>? domains { get; set; }
|
||||
public bool? skipFallback { get; set; }
|
||||
public List<string>? expectedIPs { get; set; }
|
||||
public bool? finalQuery { get; set; }
|
||||
public string? tag { get; set; }
|
||||
}
|
||||
|
||||
@@ -226,7 +227,6 @@ public class Routing4Ray
|
||||
public List<BalancersItem4Ray>? balancers { get; set; }
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class RulesItem4Ray
|
||||
{
|
||||
public string? type { get; set; }
|
||||
@@ -321,7 +321,7 @@ public class StreamSettings4Ray
|
||||
public HysteriaSettings4Ray? hysteriaSettings { get; set; }
|
||||
public MasqueSettings4Ray? masqueSettings { get; set; }
|
||||
|
||||
public object? finalmask { get; set; }
|
||||
public JsonObject? finalmask { get; set; }
|
||||
|
||||
public Sockopt4Ray? sockopt { get; set; }
|
||||
}
|
||||
@@ -344,7 +344,6 @@ public class TlsSettings4Ray
|
||||
public string? pinnedPeerCertSha256 { get; set; }
|
||||
public bool? disableSystemRoot { get; set; }
|
||||
public string? echConfigList { get; set; }
|
||||
public string? echForceQuery { get; set; }
|
||||
public Sockopt4Ray? echSockopt { get; set; }
|
||||
}
|
||||
|
||||
@@ -363,9 +362,9 @@ public class Header4Ray
|
||||
{
|
||||
public string type { get; set; }
|
||||
|
||||
public object request { get; set; }
|
||||
public JsonObject request { get; set; }
|
||||
|
||||
public object response { get; set; }
|
||||
public JsonObject response { get; set; }
|
||||
}
|
||||
|
||||
public class KcpSettings4Ray
|
||||
@@ -411,7 +410,7 @@ public class XhttpSettings4Ray
|
||||
public string? path { get; set; }
|
||||
public string? host { get; set; }
|
||||
public string? mode { get; set; }
|
||||
public object? extra { get; set; }
|
||||
public JsonObject? extra { get; set; }
|
||||
}
|
||||
|
||||
public class HttpSettings4Ray
|
||||
@@ -452,6 +451,14 @@ public class MasqueSettings4Ray
|
||||
{
|
||||
public string? path { get; set; }
|
||||
public Dictionary<string, string>? headers { get; set; }
|
||||
public MasqueWarpSettings4Ray? warp { get; set; }
|
||||
}
|
||||
|
||||
public class MasqueWarpSettings4Ray
|
||||
{
|
||||
public string? privateKey { get; set; }
|
||||
public string? publicKey { get; set; }
|
||||
public List<string>? address { get; set; }
|
||||
}
|
||||
|
||||
public class UdpHop4Ray
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
namespace ServiceLib.Models.Dto;
|
||||
|
||||
[Serializable]
|
||||
public partial class ClashProxyModel : ReactiveObject
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
namespace ServiceLib.Models.Dto;
|
||||
|
||||
[Serializable]
|
||||
public partial class ProfileItemModel : ReactiveObject
|
||||
{
|
||||
public bool IsActive { get; set; }
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
namespace ServiceLib.Models.Dto;
|
||||
|
||||
[Serializable]
|
||||
public class RoutingItemModel : RoutingItem
|
||||
{
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
namespace ServiceLib.Models.Dto;
|
||||
|
||||
[Serializable]
|
||||
public class RoutingTemplate
|
||||
{
|
||||
public string Version { get; set; }
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
namespace ServiceLib.Models.Dto;
|
||||
|
||||
[Serializable]
|
||||
public class RulesItemModel : RulesItem
|
||||
{
|
||||
public string InboundTags { get; set; }
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
namespace ServiceLib.Models.Dto;
|
||||
|
||||
[Serializable]
|
||||
public class ServerSpeedItem : ServerStatItem
|
||||
{
|
||||
public long ProxyUp { get; set; }
|
||||
@@ -12,7 +11,6 @@ public class ServerSpeedItem : ServerStatItem
|
||||
public long DirectDown { get; set; }
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class TrafficItem
|
||||
{
|
||||
public ulong Up { get; set; }
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
namespace ServiceLib.Models.Dto;
|
||||
|
||||
[Serializable]
|
||||
public class ServerTestItem
|
||||
{
|
||||
public string? IndexId { get; set; }
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
namespace ServiceLib.Models.Dto;
|
||||
|
||||
[Serializable]
|
||||
public class SpeedTestResult
|
||||
{
|
||||
public string? IndexId { get; set; }
|
||||
|
||||
@@ -5,7 +5,6 @@ public class SsSIP008
|
||||
public List<SsServer>? servers { get; set; }
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class SsServer
|
||||
{
|
||||
public string? remarks { get; set; }
|
||||
|
||||
@@ -3,7 +3,6 @@ namespace ServiceLib.Models.Dto;
|
||||
/// <summary>
|
||||
/// https://github.com/2dust/v2rayN/wiki/
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class VmessQRCode
|
||||
{
|
||||
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)]
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
namespace ServiceLib.Models.Entities;
|
||||
|
||||
[Serializable]
|
||||
public class DNSItem
|
||||
{
|
||||
[PrimaryKey]
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
namespace ServiceLib.Models.Entities;
|
||||
|
||||
[Serializable]
|
||||
public class FullConfigTemplateItem
|
||||
{
|
||||
[PrimaryKey]
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
namespace ServiceLib.Models.Entities;
|
||||
|
||||
[Serializable]
|
||||
public class ProfileExItem
|
||||
{
|
||||
[PrimaryKey]
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
namespace ServiceLib.Models.Entities;
|
||||
|
||||
[Obsolete("Use ProtocolExtraItem instead.")]
|
||||
[Serializable]
|
||||
public class ProfileGroupItem
|
||||
{
|
||||
[PrimaryKey]
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
namespace ServiceLib.Models.Entities;
|
||||
|
||||
[Serializable]
|
||||
public class ProfileItem
|
||||
{
|
||||
private ProtocolExtraItem? _protocolExtraCache;
|
||||
|
||||
@@ -49,6 +49,9 @@ public record ProtocolExtraItem
|
||||
// MASQUE
|
||||
public string? MasquePath { get; init; }
|
||||
public string? MasqueHeaders { get; init; }
|
||||
public string? WarpPublicKey { get; init; }
|
||||
public string? WarpPrivateKey { get; init; }
|
||||
public string? WarpAddress { get; init; }
|
||||
|
||||
// group profile
|
||||
public string? GroupType { get; init; }
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
namespace ServiceLib.Models.Entities;
|
||||
|
||||
[Serializable]
|
||||
public class RoutingItem
|
||||
{
|
||||
[PrimaryKey]
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
namespace ServiceLib.Models.Entities;
|
||||
|
||||
[Serializable]
|
||||
public class RulesItem
|
||||
{
|
||||
public string Id { get; set; }
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
namespace ServiceLib.Models.Entities;
|
||||
|
||||
[Serializable]
|
||||
public class ServerStatItem
|
||||
{
|
||||
[PrimaryKey]
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
namespace ServiceLib.Models.Entities;
|
||||
|
||||
[Serializable]
|
||||
public class SubItem
|
||||
{
|
||||
[PrimaryKey]
|
||||
@@ -37,4 +36,6 @@ public class SubItem
|
||||
public string? Memo { get; set; }
|
||||
|
||||
public ECoreType? CustomCoreType { get; set; }
|
||||
|
||||
public string? PreResolver { get; set; }
|
||||
}
|
||||
|
||||
Generated
+27
@@ -528,6 +528,15 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Pre-resolve server domains 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string LvPreResolve {
|
||||
get {
|
||||
return ResourceManager.GetString("LvPreResolve", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Previous proxy remarks 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -978,6 +987,15 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Columns 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string menuColumns {
|
||||
get {
|
||||
return ResourceManager.GetString("menuColumns", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Close Connection 的本地化字符串。
|
||||
/// </summary>
|
||||
@@ -5157,6 +5175,15 @@ namespace ServiceLib.Resx {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Warp 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbWarp {
|
||||
get {
|
||||
return ResourceManager.GetString("TbWarp", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Xray Only 的本地化字符串。
|
||||
/// </summary>
|
||||
|
||||
@@ -1905,4 +1905,13 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
|
||||
<data name="menuAddMasqueServer" xml:space="preserve">
|
||||
<value>Add [MASQUE]</value>
|
||||
</data>
|
||||
<data name="menuColumns" xml:space="preserve">
|
||||
<value>Columns</value>
|
||||
</data>
|
||||
<data name="TbWarp" xml:space="preserve">
|
||||
<value>Warp</value>
|
||||
</data>
|
||||
<data name="LvPreResolve" xml:space="preserve">
|
||||
<value>Pre-resolve server domains</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1897,4 +1897,13 @@
|
||||
<data name="menuAddMasqueServer" xml:space="preserve">
|
||||
<value>添加 [MASQUE]</value>
|
||||
</data>
|
||||
<data name="menuColumns" xml:space="preserve">
|
||||
<value>列</value>
|
||||
</data>
|
||||
<data name="TbWarp" xml:space="preserve">
|
||||
<value>Warp</value>
|
||||
</data>
|
||||
<data name="LvPreResolve" xml:space="preserve">
|
||||
<value>预解析节点域名</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-16"?>
|
||||
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
||||
<RegistrationInfo>
|
||||
<Description>{{DESCRIPTION}}</Description>
|
||||
</RegistrationInfo>
|
||||
<Triggers>
|
||||
<LogonTrigger>
|
||||
<Enabled>true</Enabled>
|
||||
</LogonTrigger>
|
||||
</Triggers>
|
||||
<Principals>
|
||||
<Principal id="Author">
|
||||
<LogonType>InteractiveToken</LogonType>
|
||||
<RunLevel>HighestAvailable</RunLevel>
|
||||
</Principal>
|
||||
</Principals>
|
||||
<Actions Context="Author">
|
||||
<Exec>
|
||||
<Command>{{EXEC_PATH}}</Command>
|
||||
<Arguments>{{ARGUMENTS}}</Arguments>
|
||||
<WorkingDirectory>{{WORKING_DIRECTORY}}</WorkingDirectory>
|
||||
</Exec>
|
||||
</Actions>
|
||||
</Task>
|
||||
@@ -171,70 +171,6 @@ ivAwhZTJryQCL2/W3Wf+47BVTwSYT6RBVuKT0Gro1vP7ZeDOdcQxWQzugsgMYDNK
|
||||
GbqEZycPvEJdvSRUDewdcAZfpLz6IHxV
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
-----BEGIN CERTIFICATE-----
|
||||
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
|
||||
Y11aWOIv4x3kqdbQCtCev9eBCfHJxyYNrJgWVqA=
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
-----BEGIN CERTIFICATE-----
|
||||
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
|
||||
4/g7u9xN12TyUb7mqqta6THuBrxzvxNiCp/HuZc=
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIFRzCCAy+gAwIBAgIJEQA0tk7GNi02MA0GCSqGSIb3DQEBCwUAMEExCzAJBgNV
|
||||
BAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJR04g
|
||||
@@ -300,40 +236,6 @@ AAoACxGV2lZFA4gKn2fQ1XmxqI1AbQ3CekD6819kR5LLU7m7Wc5P/dAVUwHY3+vZ
|
||||
5nbv0CO7O6l5s9UCKc2Jo5YPSjXnTkLAdc0Hz+Ys63su
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
-----BEGIN CERTIFICATE-----
|
||||
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
|
||||
hNQ+IIX3Sj0rnP0qCglN6oH4EZw=
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEMzCCAxugAwIBAgIDCYPzMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNVBAYTAkRF
|
||||
MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBD
|
||||
@@ -434,43 +336,6 @@ e9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4p
|
||||
TpPDpFQUWw==
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIGWzCCBEOgAwIBAgIRAMrpG4nxVQMNo+ZBbcTjpuEwDQYJKoZIhvcNAQELBQAw
|
||||
WjELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCURoaW15b3RpczEcMBoGA1UECwwTMDAw
|
||||
MiA0ODE0NjMwODEwMDAzNjEZMBcGA1UEAwwQQ2VydGlnbmEgUm9vdCBDQTAeFw0x
|
||||
MzEwMDEwODMyMjdaFw0zMzEwMDEwODMyMjdaMFoxCzAJBgNVBAYTAkZSMRIwEAYD
|
||||
VQQKDAlEaGlteW90aXMxHDAaBgNVBAsMEzAwMDIgNDgxNDYzMDgxMDAwMzYxGTAX
|
||||
BgNVBAMMEENlcnRpZ25hIFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw
|
||||
ggIKAoICAQDNGDllGlmx6mQWDoyUJJV8g9PFOSbcDO8WV43X2KyjQn+Cyu3NW9sO
|
||||
ty3tRQgXstmzy9YXUnIo245Onoq2C/mehJpNdt4iKVzSs9IGPjA5qXSjklYcoW9M
|
||||
CiBtnyN6tMbaLOQdLNyzKNAT8kxOAkmhVECe5uUFoC2EyP+YbNDrihqECB63aCPu
|
||||
I9Vwzm1RaRDuoXrC0SIxwoKF0vJVdlB8JXrJhFwLrN1CTivngqIkicuQstDuI7pm
|
||||
TLtipPlTWmR7fJj6o0ieD5Wupxj0auwuA0Wv8HT4Ks16XdG+RCYyKfHx9WzMfgIh
|
||||
C59vpD++nVPiz32pLHxYGpfhPTc3GGYo0kDFUYqMwy3OU4gkWGQwFsWq4NYKpkDf
|
||||
ePb1BHxpE4S80dGnBs8B92jAqFe7OmGtBIyT46388NtEbVncSVmurJqZNjBBe3Yz
|
||||
IoejwpKGbvlw7q6Hh5UbxHq9MfPU0uWZ/75I7HX1eBYdpnDBfzwboZL7z8g81sWT
|
||||
Co/1VTp2lc5ZmIoJlXcymoO6LAQ6l73UL77XbJuiyn1tJslV1c/DeVIICZkHJC1k
|
||||
JWumIWmbat10TWuXekG9qxf5kBdIjzb5LdXF2+6qhUVB+s06RbFo5jZMm5BX7CO5
|
||||
hwjCxAnxl4YqKE3idMDaxIzb3+KhF1nOJFl0Mdp//TBt2dzhauH8XwIDAQABo4IB
|
||||
GjCCARYwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE
|
||||
FBiHVuBud+4kNTxOc5of1uHieX4rMB8GA1UdIwQYMBaAFBiHVuBud+4kNTxOc5of
|
||||
1uHieX4rMEQGA1UdIAQ9MDswOQYEVR0gADAxMC8GCCsGAQUFBwIBFiNodHRwczov
|
||||
L3d3d3cuY2VydGlnbmEuZnIvYXV0b3JpdGVzLzBtBgNVHR8EZjBkMC+gLaArhilo
|
||||
dHRwOi8vY3JsLmNlcnRpZ25hLmZyL2NlcnRpZ25hcm9vdGNhLmNybDAxoC+gLYYr
|
||||
aHR0cDovL2NybC5kaGlteW90aXMuY29tL2NlcnRpZ25hcm9vdGNhLmNybDANBgkq
|
||||
hkiG9w0BAQsFAAOCAgEAlLieT/DjlQgi581oQfccVdV8AOItOoldaDgvUSILSo3L
|
||||
6btdPrtcPbEo/uRTVRPPoZAbAh1fZkYJMyjhDSSXcNMQH+pkV5a7XdrnxIxPTGRG
|
||||
HVyH41neQtGbqH6mid2PHMkwgu07nM3A6RngatgCdTer9zQoKJHyBApPNeNgJgH6
|
||||
0BGM+RFq7q89w1DTj18zeTyGqHNFkIwgtnJzFyO+B2XleJINugHA64wcZr+shncB
|
||||
lA2c5uk5jR+mUYyZDDl34bSb+hxnV29qao6pK0xXeXpXIs/NX2NGjVxZOob4Mkdi
|
||||
o2cNGJHc+6Zr9UhhcyNZjgKnvETq9Emd8VRY+WCv2hikLyhF3HqgiIZd8zvn/yk1
|
||||
gPxkQ5Tm4xxvvq0OKmOZK8l+hfZx6AYDlf7ej0gcWtSS6Cvu5zHbugRqh5jnxV/v
|
||||
faci9wHYTfmJ0A6aBVmknpjZbyvKcL5kwlWj9Omvw5Ip3IgWJJk8jSaYtlu3zM63
|
||||
Nwf9JtmYhST/WSMDmu2dnajkXjjO11INb9I/bbEFa0nOipFGc/T2L/Coc3cOZayh
|
||||
jWZSaX5LaAzHHjcng6WMxwLkFM1JAbBzs/3GkDpv0mztO+7skb6iQ12LAEpmJURw
|
||||
3kAP+HwV96LOPNdeE4yBFxgX0b3xdxA61GU5wSesVywlVP+i2k+KYTlerj1KjL0=
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICPzCCAcWgAwIBAgIQBVVWvPJepDU1w6QP1atFcjAKBggqhkjOPQQDAzBhMQsw
|
||||
CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu
|
||||
@@ -565,22 +430,6 @@ RQuQ+q7hv53yrlc8pa6yVvSLZUDp/TGBLPQ5Cdjua6e0ph0VpZj3AYHYhX3zUVxx
|
||||
iN66zB+Afko=
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICTjCCAdOgAwIBAgIKPPYHqWhwDtqLhDAKBggqhkjOPQQDAzBrMQswCQYDVQQG
|
||||
EwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNo
|
||||
bm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0g
|
||||
RzMwHhcNMTgwMjE4MTgzMDAwWhcNNDMwMjE4MTgzMDAwWjBrMQswCQYDVQQGEwJJ
|
||||
TjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9s
|
||||
b2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0gRzMw
|
||||
djAQBgcqhkjOPQIBBgUrgQQAIgNiAAQjpQy4LRL1KPOxst3iAhKAnjlfSU2fySU0
|
||||
WXTsuwYc58Byr+iuL+FBVIcUqEqy6HyC5ltqtdyzdc6LBtCGI79G1Y4PPwT01xyS
|
||||
fvalY8L1X44uT6EYGQIrMgqCZH0Wk9GjQjBAMB0GA1UdDgQWBBR8XQKEE9TMipuB
|
||||
zhccLikenEhjQjAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggq
|
||||
hkjOPQQDAwNpADBmAjEAvvNhzwIQHWSVB7gYboiFBS+DCBeQyh+KTOgNG3qxrdWB
|
||||
CUfvO6wIBHxcmbHtRwfSAjEAnbpV/KlK6O3t5nYBQnvI+GDZjVGLVTv7jHvrZQnD
|
||||
+JbNR6iC8hZVdyR+EhCVBCyj
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIFiDCCA3CgAwIBAgIIfQmX/vBH6nowDQYJKoZIhvcNAQELBQAwYjELMAkGA1UE
|
||||
BhMCQ04xMjAwBgNVBAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZ
|
||||
@@ -732,20 +581,6 @@ LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI
|
||||
4uJEvlz36hz1
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICCTCCAY6gAwIBAgINAgPluILrIPglJ209ZjAKBggqhkjOPQQDAzBHMQswCQYD
|
||||
VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG
|
||||
A1UEAxMLR1RTIFJvb3QgUjMwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw
|
||||
WjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2Vz
|
||||
IExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjMwdjAQBgcqhkjOPQIBBgUrgQQAIgNi
|
||||
AAQfTzOHMymKoYTey8chWEGJ6ladK0uFxh1MJ7x/JlFyb+Kf1qPKzEUURout736G
|
||||
jOyxfi//qXGdGIRFBEFVbivqJn+7kAHjSxm65FSWRQmx1WyRRK2EE46ajA2ADDL2
|
||||
4CejQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW
|
||||
BBTB8Sa6oC2uhYHP0/EqEr24Cmf9vDAKBggqhkjOPQQDAwNpADBmAjEA9uEglRR7
|
||||
VKOQFhG/hMjqb2sXnh5GmCCbn9MN2azTL818+FsuVbu/3ZL3pAzcMeGiAjEA/Jdm
|
||||
ZuVDFhOD3cffL74UOO0BzrEXGhF16b0DjyZ+hOXJYKaV11RZt+cRLInUue4X
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIFVzCCAz+gAwIBAgINAgPlk28xsBNJiGuiFzANBgkqhkiG9w0BAQwFADBHMQsw
|
||||
CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU
|
||||
@@ -805,38 +640,6 @@ HQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUVLB7rUW44kB/
|
||||
bmF0774BxL4YSFlhgjICICadVGNA3jdgUM/I2O2dgq43mLyjj0xMqTQrbO/7lZsm
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
-----BEGIN CERTIFICATE-----
|
||||
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
|
||||
6WLAYv7YTVWW4tAR+kg0Eeye7QUd5MjWHYbL
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIFzzCCA7egAwIBAgIUCBZfikyl7ADJk0DfxMauI7gcWqQwDQYJKoZIhvcNAQEL
|
||||
BQAwbzELMAkGA1UEBhMCSEsxEjAQBgNVBAgTCUhvbmcgS29uZzESMBAGA1UEBxMJ
|
||||
@@ -1189,31 +992,6 @@ AHFiRSdLOkKEW39lt2c0Ui2cFmuqqNh7o0JMcccMyj6D5KbvtwEwXlGjefVwaaZB
|
||||
RA+GsCyRxj3qrg+E
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQG
|
||||
EwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFTATBgNVBAoMDE5ldExvY2sgS2Z0LjE3
|
||||
MDUGA1UECwwuVGFuw7pzw610dsOhbnlraWFkw7NrIChDZXJ0aWZpY2F0aW9uIFNl
|
||||
cnZpY2VzKTE1MDMGA1UEAwwsTmV0TG9jayBBcmFueSAoQ2xhc3MgR29sZCkgRsWR
|
||||
dGFuw7pzw610dsOhbnkwHhcNMDgxMjExMTUwODIxWhcNMjgxMjA2MTUwODIxWjCB
|
||||
pzELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRUwEwYDVQQKDAxOZXRM
|
||||
b2NrIEtmdC4xNzA1BgNVBAsMLlRhbsO6c8OtdHbDoW55a2lhZMOzayAoQ2VydGlm
|
||||
aWNhdGlvbiBTZXJ2aWNlcykxNTAzBgNVBAMMLE5ldExvY2sgQXJhbnkgKENsYXNz
|
||||
IEdvbGQpIEbFkXRhbsO6c8OtdHbDoW55MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
|
||||
MIIBCgKCAQEAxCRec75LbRTDofTjl5Bu0jBFHjzuZ9lk4BqKf8owyoPjIMHj9DrT
|
||||
lF8afFttvzBPhCf2nx9JvMaZCpDyD/V/Q4Q3Y1GLeqVw/HpYzY6b7cNGbIRwXdrz
|
||||
AZAj/E4wqX7hJ2Pn7WQ8oLjJM2P+FpD/sLj916jAwJRDC7bVWaaeVtAkH3B5r9s5
|
||||
VA1lddkVQZQBr17s9o3x/61k/iCa11zr/qYfCGSji3ZVrR47KGAuhyXoqq8fxmRG
|
||||
ILdwfzzeSNuWU7c5d+Qa4scWhHaXWy+7GRWF+GmF9ZmnqfI0p6m2pgP8b4Y9VHx2
|
||||
BJtr+UBdADTHLpl1neWIA6pN+APSQnbAGwIDAKiLo0UwQzASBgNVHRMBAf8ECDAG
|
||||
AQH/AgEEMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUzPpnk/C2uNClwB7zU/2M
|
||||
U9+D15YwDQYJKoZIhvcNAQELBQADggEBAKt/7hwWqZw8UQCgwBEIBaeZ5m8BiFRh
|
||||
bvG5GK1Krf6BQCOUL/t1fC8oS2IkgYIL9WHxHG64YTjrgfpioTtaYtOUZcTh5m2C
|
||||
+C8lcLIhJsFyUR+MLMOEkMNaj7rP9KdlpeuY0fsFskZ1FSNqb4VjMIDw1Z4fKRzC
|
||||
bLBQWV2QWzuoDTDPv31/zvGdg73JRm4gpvlhUbohL3u+pRVjodSVh/GeufOJ8z2F
|
||||
uLjbvrW5KfnaNwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2
|
||||
XjG4Kvte9nHfRCaexOYNkbQudZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E=
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDtTCCAp2gAwIBAgIQdrEgUnTwhYdGs/gjGvbCwDANBgkqhkiG9w0BAQsFADBt
|
||||
MQswCQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUg
|
||||
@@ -1827,38 +1605,6 @@ xw/ogM4cKGR0GQjTQuPOAF1/sdwTsOEFy9EgqoZ0njnnkf3/W9b3raYvAwtt41dU
|
||||
63ZTGI0RmLo=
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
-----BEGIN CERTIFICATE-----
|
||||
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
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw
|
||||
CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg
|
||||
|
||||
@@ -104,59 +104,6 @@ RQuQ+q7hv53yrlc8pa6yVvSLZUDp/TGBLPQ5Cdjua6e0ph0VpZj3AYHYhX3zUVxx
|
||||
iN66zB+Afko=
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICTjCCAdOgAwIBAgIKPPYHqWhwDtqLhDAKBggqhkjOPQQDAzBrMQswCQYDVQQG
|
||||
EwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNo
|
||||
bm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0g
|
||||
RzMwHhcNMTgwMjE4MTgzMDAwWhcNNDMwMjE4MTgzMDAwWjBrMQswCQYDVQQGEwJJ
|
||||
TjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9s
|
||||
b2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0gRzMw
|
||||
djAQBgcqhkjOPQIBBgUrgQQAIgNiAAQjpQy4LRL1KPOxst3iAhKAnjlfSU2fySU0
|
||||
WXTsuwYc58Byr+iuL+FBVIcUqEqy6HyC5ltqtdyzdc6LBtCGI79G1Y4PPwT01xyS
|
||||
fvalY8L1X44uT6EYGQIrMgqCZH0Wk9GjQjBAMB0GA1UdDgQWBBR8XQKEE9TMipuB
|
||||
zhccLikenEhjQjAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggq
|
||||
hkjOPQQDAwNpADBmAjEAvvNhzwIQHWSVB7gYboiFBS+DCBeQyh+KTOgNG3qxrdWB
|
||||
CUfvO6wIBHxcmbHtRwfSAjEAnbpV/KlK6O3t5nYBQnvI+GDZjVGLVTv7jHvrZQnD
|
||||
+JbNR6iC8hZVdyR+EhCVBCyj
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDczCCAlugAwIBAgILAK7PALrEzzL4Q7IwDQYJKoZIhvcNAQELBQAwVjELMAkG
|
||||
A1UEBhMCVVMxEzARBgNVBAsTCmVtU2lnbiBQS0kxFDASBgNVBAoTC2VNdWRocmEg
|
||||
SW5jMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEMxMB4XDTE4MDIxODE4MzAw
|
||||
MFoXDTQzMDIxODE4MzAwMFowVjELMAkGA1UEBhMCVVMxEzARBgNVBAsTCmVtU2ln
|
||||
biBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMRwwGgYDVQQDExNlbVNpZ24gUm9v
|
||||
dCBDQSAtIEMxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz+upufGZ
|
||||
BczYKCFK83M0UYRWEPWgTywS4/oTmifQz/l5GnRfHXk5/Fv4cI7gklL35CX5VIPZ
|
||||
HdPIWoU/Xse2B+4+wM6ar6xWQio5JXDWv7V7Nq2s9nPczdcdioOl+yuQFTdrHCZH
|
||||
3DspVpNqs8FqOp099cGXOFgFixwR4+S0uF2FHYP+eF8LRWgYSKVGczQ7/g/IdrvH
|
||||
GPMF0Ybzhe3nudkyrVWIzqa2kbBPrH4VI5b2P/AgNBbeCsbEBEV5f6f9vtKppa+c
|
||||
xSMq9zwhbL2vj07FOrLzNBL834AaSaTUqZX3noleoomslMuoaJuvimUnzYnu3Yy1
|
||||
aylwQ6BpC+S5DwIDAQABo0IwQDAdBgNVHQ4EFgQU/qHgcB4qAzlSWkK+XJGFehiq
|
||||
TbUwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL
|
||||
BQADggEBAMJKVvoVIXsoounlHfv4LcQ5lkFMOycsxGwYFYDGrK9HWS8mC+M2sO87
|
||||
/kOXSTKZEhVb3xEp/6tT+LvBeA+snFOvV71ojD1pM/CjoCNjO2RnIkSt1XHLVip4
|
||||
kqNPEjE2NuLe/gDEo2APJ62gsIq1NnpSob0n9CAnYuhNlCQT5AoE6TyrLshDCUrG
|
||||
YQTlSTR+08TI9Q/Aqum6VF7zYytPT1DU/rl7mYw9wC68AivTxEDkigcxHpvOJpkT
|
||||
+xHqmiIMERnHXhuBUDDIlhJu58tBf5E7oke3VIAb3ADMmpDqw8NQBmIMMMAVSKeo
|
||||
WXzhriKi4gp6D/piq1JM4fHfyr6DDUI=
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICKzCCAbGgAwIBAgIKe3G2gla4EnycqDAKBggqhkjOPQQDAzBaMQswCQYDVQQG
|
||||
EwJVUzETMBEGA1UECxMKZW1TaWduIFBLSTEUMBIGA1UEChMLZU11ZGhyYSBJbmMx
|
||||
IDAeBgNVBAMTF2VtU2lnbiBFQ0MgUm9vdCBDQSAtIEMzMB4XDTE4MDIxODE4MzAw
|
||||
MFoXDTQzMDIxODE4MzAwMFowWjELMAkGA1UEBhMCVVMxEzARBgNVBAsTCmVtU2ln
|
||||
biBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMSAwHgYDVQQDExdlbVNpZ24gRUND
|
||||
IFJvb3QgQ0EgLSBDMzB2MBAGByqGSM49AgEGBSuBBAAiA2IABP2lYa57JhAd6bci
|
||||
MK4G9IGzsUJxlTm801Ljr6/58pc1kjZGDoeVjbk5Wum739D+yAdBPLtVb4Ojavti
|
||||
sIGJAnB9SMVK4+kiVCJNk7tCDK93nCOmfddhEc5lx/h//vXyqaNCMEAwHQYDVR0O
|
||||
BBYEFPtaSNCAIEDyqOkAB2kZd6fmw/TPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMB
|
||||
Af8EBTADAQH/MAoGCCqGSM49BAMDA2gAMGUCMQC02C8Cif22TGK6Q04ThHK1rt0c
|
||||
3ta13FaPWEBaLd4gTCKDypOofu4SQMfWh0/434UCMBwUZOR8loMRnLDRWmFLpg9J
|
||||
0wD8ofzkpf9/rdcw0Md3f76BB1UwUCAU9Vc4CqgxUQ==
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIFzzCCA7egAwIBAgIUCBZfikyl7ADJk0DfxMauI7gcWqQwDQYJKoZIhvcNAQEL
|
||||
BQAwbzELMAkGA1UEBhMCSEsxEjAQBgNVBAgTCUhvbmcgS29uZzESMBAGA1UEBxMJ
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Library</OutputType>
|
||||
@@ -10,10 +10,6 @@
|
||||
<PackageReference Include="ReactiveUI">
|
||||
<TreatAsUsed>true</TreatAsUsed>
|
||||
</PackageReference>
|
||||
<PackageReference Include="ReactiveUI.SourceGenerators">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="sqlite-net-e" />
|
||||
<PackageReference Include="Repobot.SQLite.Unofficial" />
|
||||
<PackageReference Include="NLog" />
|
||||
@@ -22,7 +18,6 @@
|
||||
<PackageReference Include="QRCoder" />
|
||||
<PackageReference Include="CliWrap" />
|
||||
<PackageReference Include="ZXing.Net.Bindings.SkiaSharp" />
|
||||
<PackageReference Include="TaskScheduler" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -49,6 +44,7 @@
|
||||
<EmbeddedResource Include="Sample\SampleTunRules" />
|
||||
<EmbeddedResource Include="Sample\SingboxSampleClientConfig" />
|
||||
<EmbeddedResource Include="Sample\SingboxSampleOutbound" />
|
||||
<EmbeddedResource Include="Sample\TaskXmlTemplate" />
|
||||
<EmbeddedResource Include="Sample\tun_singbox_dns" />
|
||||
<EmbeddedResource Include="Sample\tun_singbox_inbound" />
|
||||
<EmbeddedResource Include="Sample\tun_singbox_rules" />
|
||||
|
||||
@@ -61,10 +61,10 @@ public class CoreConfigClashService(Config config, bool isTunEnabled)
|
||||
//YAML anchors
|
||||
if (txtFile.Contains("<<:") && txtFile.Contains('*') && txtFile.Contains('&'))
|
||||
{
|
||||
txtFile = YamlUtils.PreprocessYaml(txtFile);
|
||||
txtFile = YamlUtils.PreprocessYaml(txtFile) ?? "";
|
||||
}
|
||||
|
||||
var fileContent = YamlUtils.FromYaml<Dictionary<string, object>>(txtFile);
|
||||
var fileContent = YamlUtils.FromYaml(txtFile);
|
||||
if (fileContent == null)
|
||||
{
|
||||
ret.Msg = ResUI.FailedConversionConfiguration;
|
||||
@@ -72,29 +72,38 @@ public class CoreConfigClashService(Config config, bool isTunEnabled)
|
||||
}
|
||||
|
||||
//mixed-port
|
||||
fileContent["mixed-port"] = AppManager.Instance.GetLocalPort(EInboundProtocol.socks);
|
||||
//fileContent["mixed-port"] = AppManager.Instance.GetLocalPort(EInboundProtocol.socks);
|
||||
fileContent.Children[new YamlScalarNode("mixed-port")] = new YamlScalarNode(AppManager.Instance.GetLocalPort(EInboundProtocol.socks).ToString());
|
||||
//log-level
|
||||
fileContent["log-level"] = GetLogLevel(config.CoreBasicItem.Loglevel);
|
||||
//fileContent["log-level"] = GetLogLevel(config.CoreBasicItem.Loglevel);
|
||||
fileContent.Children[new YamlScalarNode("log-level")] = new YamlScalarNode(GetLogLevel(config.CoreBasicItem.Loglevel));
|
||||
|
||||
//external-controller
|
||||
fileContent["external-controller"] = $"{Global.Loopback}:{AppManager.Instance.StatePort2}";
|
||||
fileContent.Remove("secret");
|
||||
//fileContent["external-controller"] = $"{Global.Loopback}:{AppManager.Instance.StatePort2}";
|
||||
fileContent.Children[new YamlScalarNode("external-controller")] = new YamlScalarNode($"{Global.Loopback}:{AppManager.Instance.StatePort2}");
|
||||
//fileContent.Remove("secret");
|
||||
fileContent.Children.Remove(new YamlScalarNode("secret"));
|
||||
//allow-lan
|
||||
if (config.Inbound.First().AllowLANConn)
|
||||
{
|
||||
fileContent["allow-lan"] = "true";
|
||||
fileContent["bind-address"] = "*";
|
||||
//fileContent["allow-lan"] = "true";
|
||||
fileContent.Children[new YamlScalarNode("allow-lan")] = new YamlScalarNode("true");
|
||||
//fileContent["bind-address"] = "*";
|
||||
fileContent.Children[new YamlScalarNode("bind-address")] = new YamlScalarNode("*");
|
||||
}
|
||||
else
|
||||
{
|
||||
fileContent["allow-lan"] = "false";
|
||||
//fileContent["allow-lan"] = "false";
|
||||
fileContent.Children[new YamlScalarNode("allow-lan")] = new YamlScalarNode("false");
|
||||
}
|
||||
|
||||
//ipv6
|
||||
fileContent["ipv6"] = config.ClashUIItem.EnableIPv6;
|
||||
//fileContent["ipv6"] = config.ClashUIItem.EnableIPv6;
|
||||
fileContent.Children[new YamlScalarNode("ipv6")] = new YamlScalarNode(config.ClashUIItem.EnableIPv6.ToString().ToLowerInvariant());
|
||||
|
||||
//mode
|
||||
fileContent.TryAdd("mode", nameof(ERuleMode.Rule));
|
||||
//fileContent.TryAdd("mode", nameof(ERuleMode.Rule));
|
||||
fileContent.Children.TryAdd(new YamlScalarNode("mode"), new YamlScalarNode(nameof(ERuleMode.Rule)));
|
||||
|
||||
//enable tun mode
|
||||
if (isTunEnabled)
|
||||
@@ -102,10 +111,11 @@ public class CoreConfigClashService(Config config, bool isTunEnabled)
|
||||
var tun = EmbedUtils.GetEmbedText(Global.ClashTunYaml);
|
||||
if (tun.IsNotEmpty())
|
||||
{
|
||||
var tunContent = YamlUtils.FromYaml<Dictionary<string, object>>(tun);
|
||||
var tunContent = YamlUtils.FromYaml(tun);
|
||||
if (tunContent != null)
|
||||
{
|
||||
fileContent["tun"] = tunContent["tun"];
|
||||
//fileContent["tun"] = tunContent["tun"];
|
||||
fileContent.Children[new YamlScalarNode("tun")] = tunContent.Children[new YamlScalarNode("tun")];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -120,27 +130,27 @@ public class CoreConfigClashService(Config config, bool isTunEnabled)
|
||||
Logging.SaveLog($"{_tag}-Mixin", ex);
|
||||
}
|
||||
|
||||
// Mihomo parses plain values such as 815458e4 as floats, so quote REALITY short IDs.
|
||||
var originalRealityShortIds = new List<(Dictionary<object, object> RealityOptions, string ShortId)>();
|
||||
if (fileContent.GetValueOrDefault("proxies") is List<object> proxies)
|
||||
{
|
||||
foreach (var proxy in proxies.OfType<Dictionary<object, object>>())
|
||||
{
|
||||
if (proxy.GetValueOrDefault("reality-opts") is Dictionary<object, object> realityOptions
|
||||
&& realityOptions.GetValueOrDefault("short-id") is string shortId
|
||||
&& !shortId.StartsWith(tagYamlStr2, StringComparison.Ordinal))
|
||||
{
|
||||
originalRealityShortIds.Add((realityOptions, shortId));
|
||||
realityOptions["short-id"] = new YamlScalarNode(shortId) { Style = ScalarStyle.DoubleQuoted };
|
||||
}
|
||||
}
|
||||
}
|
||||
//// Mihomo parses plain values such as 815458e4 as floats, so quote REALITY short IDs.
|
||||
//var originalRealityShortIds = new List<(Dictionary<object, object> RealityOptions, string ShortId)>();
|
||||
//if (fileContent.GetValueOrDefault("proxies") is List<object> proxies)
|
||||
//{
|
||||
// foreach (var proxy in proxies.OfType<Dictionary<object, object>>())
|
||||
// {
|
||||
// if (proxy.GetValueOrDefault("reality-opts") is Dictionary<object, object> realityOptions
|
||||
// && realityOptions.GetValueOrDefault("short-id") is string shortId
|
||||
// && !shortId.StartsWith(tagYamlStr2, StringComparison.Ordinal))
|
||||
// {
|
||||
// originalRealityShortIds.Add((realityOptions, shortId));
|
||||
// realityOptions["short-id"] = new YamlScalarNode(shortId) { Style = ScalarStyle.DoubleQuoted };
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
var txtFileNew = YamlUtils.ToYaml(fileContent).Replace(tagYamlStr2, tagYamlStr3);
|
||||
foreach (var (realityOptions, shortId) in originalRealityShortIds)
|
||||
{
|
||||
realityOptions["short-id"] = shortId;
|
||||
}
|
||||
//foreach (var (realityOptions, shortId) in originalRealityShortIds)
|
||||
//{
|
||||
// realityOptions["short-id"] = shortId;
|
||||
//}
|
||||
await File.WriteAllTextAsync(fileName, txtFileNew);
|
||||
//check again
|
||||
if (!File.Exists(fileName))
|
||||
@@ -161,7 +171,7 @@ public class CoreConfigClashService(Config config, bool isTunEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
private async Task MixinContent(Dictionary<string, object> fileContent, ProfileItem node)
|
||||
private async Task MixinContent(YamlMappingNode fileContent, ProfileItem node)
|
||||
{
|
||||
if (!config.ClashUIItem.EnableMixinContent)
|
||||
{
|
||||
@@ -177,80 +187,116 @@ public class CoreConfigClashService(Config config, bool isTunEnabled)
|
||||
|
||||
var txtFile = await File.ReadAllTextAsync(Utils.GetConfigPath(Global.ClashMixinConfigFileName));
|
||||
|
||||
var mixinContent = YamlUtils.FromYaml<Dictionary<string, object>>(txtFile);
|
||||
var mixinContent = YamlUtils.FromYaml(txtFile);
|
||||
if (mixinContent == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
foreach (var item in mixinContent)
|
||||
foreach (var (keyNode, valueNode) in mixinContent.Children)
|
||||
{
|
||||
if (!isTunEnabled && item.Key == "tun")
|
||||
if (keyNode is not YamlScalarNode scalarKey)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var key = scalarKey.Value ?? "";
|
||||
if (!isTunEnabled && key == "tun")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item.Key.StartsWith("prepend-")
|
||||
|| item.Key.StartsWith("append-")
|
||||
|| item.Key.StartsWith("removed-"))
|
||||
if (key.StartsWith("prepend-")
|
||||
|| key.StartsWith("append-")
|
||||
|| key.StartsWith("removed-"))
|
||||
{
|
||||
ModifyContentMerge(fileContent, item.Key, item.Value);
|
||||
ModifyContentMerge(fileContent, key, valueNode);
|
||||
}
|
||||
else
|
||||
{
|
||||
fileContent[item.Key] = item.Value;
|
||||
fileContent.Children[keyNode] = valueNode;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
private void ModifyContentMerge(Dictionary<string, object> fileContent, string key, object value)
|
||||
private void ModifyContentMerge(YamlMappingNode fileContent, YamlNode keyNode, YamlNode valueNode)
|
||||
{
|
||||
if (keyNode is not YamlScalarNode { Value: string key })
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var blPrepend = false;
|
||||
var blRemoved = false;
|
||||
string realKey;
|
||||
|
||||
if (key.StartsWith("prepend-"))
|
||||
{
|
||||
blPrepend = true;
|
||||
key = key.Replace("prepend-", "");
|
||||
realKey = key["prepend-".Length..];
|
||||
}
|
||||
else if (key.StartsWith("append-"))
|
||||
{
|
||||
blPrepend = false;
|
||||
key = key.Replace("append-", "");
|
||||
realKey = key["append-".Length..];
|
||||
}
|
||||
else if (key.StartsWith("removed-"))
|
||||
{
|
||||
blRemoved = true;
|
||||
key = key.Replace("removed-", "");
|
||||
realKey = key["removed-".Length..];
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!blRemoved && fileContent.TryAdd(key, value))
|
||||
var targetKeyNode = new YamlScalarNode(realKey);
|
||||
|
||||
if (!blRemoved && fileContent.Children.TryAdd(targetKeyNode, valueNode))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!fileContent.Children.TryGetValue(targetKeyNode, out var existingNode) ||
|
||||
existingNode is not YamlSequenceNode lstOri ||
|
||||
valueNode is not YamlSequenceNode lstValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var lstOri = (List<object>)fileContent[key];
|
||||
var lstValue = (List<object>)value;
|
||||
|
||||
if (blRemoved)
|
||||
{
|
||||
foreach (var item in lstValue)
|
||||
var removePrefixes = lstValue.Children
|
||||
.OfType<YamlScalarNode>()
|
||||
.Select(s => s.Value)
|
||||
.Where(v => !string.IsNullOrEmpty(v))
|
||||
.ToList();
|
||||
|
||||
for (var i = lstOri.Children.Count - 1; i >= 0; i--)
|
||||
{
|
||||
lstOri.RemoveAll(t => t.ToString().StartsWith(item.ToString()));
|
||||
if (lstOri.Children[i] is YamlScalarNode { Value: not null } itemScalar)
|
||||
{
|
||||
if (removePrefixes.Any(prefix => itemScalar.Value.StartsWith(prefix!)))
|
||||
{
|
||||
lstOri.Children.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (blPrepend)
|
||||
{
|
||||
lstValue.Reverse();
|
||||
lstValue.ForEach(item => lstOri.Insert(0, item));
|
||||
// Insert in reverse order to maintain original sequence at index 0
|
||||
for (var i = lstValue.Children.Count - 1; i >= 0; i--)
|
||||
{
|
||||
lstOri.Children.Insert(0, lstValue.Children[i]);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
lstValue.ForEach(lstOri.Add);
|
||||
foreach (var item in lstValue.Children)
|
||||
{
|
||||
lstOri.Children.Add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -356,6 +356,8 @@ public partial class CoreConfigSingboxService
|
||||
{
|
||||
case EConfigType.WireGuard:
|
||||
{
|
||||
endpoint.server = null;
|
||||
endpoint.server_port = null;
|
||||
endpoint.address = Utils.String2List(protocolExtra.WgInterfaceAddress)?.Select(s => s.Trim()).ToList() ?? ["172.16.0.2/32"];
|
||||
var peer = new Peer4Sbox
|
||||
{
|
||||
@@ -523,7 +525,7 @@ public partial class CoreConfigSingboxService
|
||||
transport.type = nameof(ETransport.http);
|
||||
transport.host = transportExtra.Host.IsNullOrEmpty()
|
||||
? null
|
||||
: Utils.String2List(transportExtra.Host);
|
||||
: JsonUtils.SerializeToArray(Utils.String2List(transportExtra.Host));
|
||||
transport.path = transportExtra.Path.NullIfEmpty();
|
||||
if (!useragentValue.IsNullOrEmpty())
|
||||
{
|
||||
|
||||
@@ -176,13 +176,13 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
|
||||
var tag = Global.ProxyTag + inbound.port.ToString();
|
||||
var isBalancer = false;
|
||||
//outbound
|
||||
var proxyOutbounds =
|
||||
new CoreConfigV2rayService(context with { Node = item }).BuildAllProxyOutbounds(tag);
|
||||
var service = new CoreConfigV2rayService(context with { Node = item });
|
||||
var proxyOutbounds = service.BuildAllProxyOutbounds(tag);
|
||||
_coreConfig.outbounds.AddRange(proxyOutbounds);
|
||||
if (proxyOutbounds.Count(n => n.tag.StartsWith(tag)) > 1)
|
||||
{
|
||||
isBalancer = true;
|
||||
var multipleLoad = _node.GetProtocolExtra().MultipleLoad ?? EMultipleLoad.LeastPing;
|
||||
var multipleLoad = service.FindFirstPolicyGroupMultipleLoad() ?? EMultipleLoad.LeastPing;
|
||||
GenObservatory(multipleLoad, tag);
|
||||
GenBalancer(multipleLoad, tag);
|
||||
}
|
||||
|
||||
@@ -17,14 +17,14 @@ public partial class CoreConfigV2rayService
|
||||
}
|
||||
|
||||
// DNS routing
|
||||
var dnsObj = JsonUtils.SerializeToNode(_coreConfig.dns);
|
||||
if (dnsObj == null)
|
||||
var dnsNode = JsonUtils.SerializeToNode(_coreConfig.dns);
|
||||
if (dnsNode is not JsonObject dnsObj)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
dnsObj["tag"] = Global.DnsTag;
|
||||
_coreConfig.dns = JsonUtils.Deserialize<Dns4Ray>(JsonUtils.Serialize(dnsObj));
|
||||
_coreConfig.dns = dnsObj;
|
||||
_coreConfig.routing.rules.Add(new RulesItem4Ray
|
||||
{
|
||||
type = "field",
|
||||
@@ -34,7 +34,7 @@ public partial class CoreConfigV2rayService
|
||||
return;
|
||||
}
|
||||
var simpleDnsItem = context.SimpleDnsItem;
|
||||
var dnsItem = _coreConfig.dns as Dns4Ray ?? new Dns4Ray();
|
||||
var dnsItem = new Dns4Ray();
|
||||
|
||||
var strategy4Freedom = simpleDnsItem?.Strategy4Freedom ?? Global.AsIs;
|
||||
//Outbound Freedom domainStrategy
|
||||
@@ -117,7 +117,7 @@ public partial class CoreConfigV2rayService
|
||||
balancerTag = finalRule.balancerTag,
|
||||
});
|
||||
|
||||
_coreConfig.dns = dnsItem;
|
||||
_coreConfig.dns = (JsonObject)JsonUtils.SerializeToNode(dnsItem);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -317,7 +317,7 @@ public partial class CoreConfigV2rayService
|
||||
|
||||
if (!useDirectDns)
|
||||
{
|
||||
dnsItem.servers.AddRange(remoteDNSAddress);
|
||||
dnsItem.servers.AddRange(JsonUtils.SerializeToArray(remoteDNSAddress));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -326,8 +326,7 @@ public partial class CoreConfigV2rayService
|
||||
var dnsServer = CreateDnsServer(dns, []);
|
||||
dnsServer.tag = $"{Global.DirectDnsTag}-{directDnsTagIndex++}";
|
||||
dnsServer.skipFallback = false;
|
||||
dnsItem.servers.Add(JsonUtils.SerializeToNode(dnsServer,
|
||||
new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }));
|
||||
dnsItem.servers.Add(JsonUtils.SerializeToNode(dnsServer));
|
||||
}
|
||||
}
|
||||
return;
|
||||
@@ -375,15 +374,21 @@ public partial class CoreConfigV2rayService
|
||||
{
|
||||
return;
|
||||
}
|
||||
foreach (var dnsAddress in dnsAddresses)
|
||||
//foreach (var dnsAddress in dnsAddresses)
|
||||
for (var i = 0; i < dnsAddresses.Count; i++)
|
||||
{
|
||||
var dnsAddress = dnsAddresses[i];
|
||||
var dnsServer = CreateDnsServer(dnsAddress, domains, expectedIPs);
|
||||
if (isDirectDns)
|
||||
{
|
||||
dnsServer.tag = $"{Global.DirectDnsTag}-{directDnsTagIndex++}";
|
||||
}
|
||||
var dnsServerNode = JsonUtils.SerializeToNode(dnsServer,
|
||||
new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull });
|
||||
if (expectedIPs is not null
|
||||
&& i == expectedIPs.Count - 1)
|
||||
{
|
||||
dnsServer.finalQuery = true;
|
||||
}
|
||||
var dnsServerNode = JsonUtils.SerializeToNode(dnsServer);
|
||||
dnsItem.servers.Add(dnsServerNode);
|
||||
}
|
||||
}
|
||||
@@ -396,12 +401,12 @@ public partial class CoreConfigV2rayService
|
||||
{
|
||||
return;
|
||||
}
|
||||
dnsItem.hosts ??= new Dictionary<string, object>();
|
||||
dnsItem.hosts ??= new Dictionary<string, JsonNode>();
|
||||
if (simpleDNSItem.AddCommonHosts == true)
|
||||
{
|
||||
dnsItem.hosts = Global.PredefinedHosts.ToDictionary(
|
||||
kvp => kvp.Key,
|
||||
kvp => (object)kvp.Value
|
||||
kvp => (JsonNode)JsonUtils.SerializeToArray(kvp.Value)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -414,14 +419,14 @@ public partial class CoreConfigV2rayService
|
||||
{
|
||||
foreach (var host in systemHosts)
|
||||
{
|
||||
normalHost.TryAdd(host.Key, new List<string> { host.Value });
|
||||
normalHost.TryAdd(host.Key, JsonUtils.SerializeToArray(new List<string> { host.Value }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var kvp in Utils.ParseHostsToDictionary(simpleDNSItem.Hosts))
|
||||
{
|
||||
dnsItem.hosts[kvp.Key] = kvp.Value;
|
||||
dnsItem.hosts[kvp.Key] = JsonUtils.SerializeToArray(kvp.Value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -449,8 +454,8 @@ public partial class CoreConfigV2rayService
|
||||
}
|
||||
}
|
||||
|
||||
var obj = JsonUtils.ParseJson(customDNS);
|
||||
if (obj is null)
|
||||
var node = JsonUtils.ParseJson(customDNS);
|
||||
if (node is not JsonObject obj)
|
||||
{
|
||||
List<string> servers = [];
|
||||
var arrDNS = customDNS.Split(',');
|
||||
@@ -458,8 +463,7 @@ public partial class CoreConfigV2rayService
|
||||
{
|
||||
servers.Add(str);
|
||||
}
|
||||
obj = JsonUtils.ParseJson("{}");
|
||||
obj["servers"] = JsonUtils.SerializeToNode(servers);
|
||||
obj = new JsonObject { ["servers"] = JsonUtils.SerializeToNode(servers) };
|
||||
}
|
||||
|
||||
// Append to dns settings
|
||||
|
||||
@@ -67,12 +67,10 @@ public partial class CoreConfigV2rayService
|
||||
|
||||
var address = _config.TunModeItem.IPv4Address.NullIfEmpty() ?? Global.TunIPv4Address.First();
|
||||
tunInbound.settings.gateway = [address];
|
||||
// Route both families into the tunnel regardless of EnableIPv6Address. That option only
|
||||
// controls whether the interface gets an IPv6 address; leaving ::/0 out of the routing
|
||||
// table makes IPv6 follow the system default route and bypass the tunnel entirely.
|
||||
// A host without a global IPv6 address is the exception: it has nothing to leak,
|
||||
// and IPv6 sent into the tunnel would have no way back out.
|
||||
tunInbound.settings.autoSystemRoutingTable = context.HasGlobalIPv6Address
|
||||
// Without ::/0, IPv6 bypasses the tunnel. Skip it only when the host has no global IPv6
|
||||
// address and EnableIPv6Address is off; NAT66 hosts have only ULA addresses.
|
||||
var routeIPv6 = _config.TunModeItem.EnableIPv6Address || context.HasGlobalIPv6Address;
|
||||
tunInbound.settings.autoSystemRoutingTable = routeIPv6
|
||||
? ["0.0.0.0/0", "::/0"]
|
||||
: ["0.0.0.0/0"];
|
||||
if (_config.TunModeItem.EnableIPv6Address == true)
|
||||
@@ -86,7 +84,7 @@ public partial class CoreConfigV2rayService
|
||||
{
|
||||
tunInbound.settings.autoOutboundsInterface = bindInterface;
|
||||
}
|
||||
tunInbound.sniffing = inbound.sniffing;
|
||||
tunInbound.sniffing = JsonUtils.DeepCopy(inbound.sniffing);
|
||||
// tunInbound.sniffing.routeOnly = inbound.sniffing.routeOnly;
|
||||
tunInbound.sniffing.routeOnly = true;
|
||||
|
||||
@@ -108,7 +106,7 @@ public partial class CoreConfigV2rayService
|
||||
.Where(x => x != null).ToList();
|
||||
|
||||
var includeList = new List<IPNetwork2> { wholeInternet };
|
||||
var includeListV6 = context.HasGlobalIPv6Address
|
||||
var includeListV6 = routeIPv6
|
||||
? new List<IPNetwork2> { wholeInternetV6 }
|
||||
: new List<IPNetwork2>();
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ public partial class CoreConfigV2rayService
|
||||
_coreConfig.outbounds.InsertRange(0, proxyOutboundList);
|
||||
if (proxyOutboundList.Count(n => n.tag.StartsWith(Global.ProxyTag)) > 1)
|
||||
{
|
||||
var multipleLoad = _node.GetProtocolExtra().MultipleLoad ?? EMultipleLoad.LeastPing;
|
||||
var multipleLoad = FindFirstPolicyGroupMultipleLoad() ?? EMultipleLoad.LeastPing;
|
||||
GenObservatory(multipleLoad);
|
||||
GenBalancer(multipleLoad);
|
||||
}
|
||||
@@ -203,7 +203,7 @@ public partial class CoreConfigV2rayService
|
||||
};
|
||||
var setting = new Outboundsettings4Ray
|
||||
{
|
||||
address = Utils.String2List(protocolExtra.WgInterfaceAddress)?.Select(s => s.Trim()).ToList() ?? ["172.16.0.2/32"],
|
||||
address = JsonUtils.SerializeToArray(Utils.String2List(protocolExtra.WgInterfaceAddress)?.Select(s => s.Trim()).ToList() ?? ["172.16.0.2/32"]),
|
||||
secretKey = _node.Password,
|
||||
reserved = Utils.String2List(protocolExtra.WgReserved)?.Select(s => s.Trim()).Select(int.Parse).ToList(),
|
||||
mtu = protocolExtra.WgMtu > 0 ? protocolExtra.WgMtu : Global.TunMtus.First(),
|
||||
@@ -337,11 +337,6 @@ public partial class CoreConfigV2rayService
|
||||
{
|
||||
tlsSettings.serverName = Utils.String2List(host)?.First();
|
||||
}
|
||||
if (!tlsSettings.echConfigList.IsNullOrEmpty())
|
||||
{
|
||||
// For legacy xray compatibility, remove this in the future
|
||||
tlsSettings.echForceQuery = "full";
|
||||
}
|
||||
var certs = CertPemManager.ParsePemChain(_node.Cert);
|
||||
if (certs.Count > 0)
|
||||
{
|
||||
@@ -428,7 +423,7 @@ public partial class CoreConfigV2rayService
|
||||
}
|
||||
kcpFinalmask.udp?.Reverse();
|
||||
streamSettings.kcpSettings = kcpSettings;
|
||||
streamSettings.finalmask = kcpFinalmask;
|
||||
streamSettings.finalmask = (JsonObject)JsonUtils.SerializeToNode(kcpFinalmask);
|
||||
break;
|
||||
//ws
|
||||
case nameof(ETransport.ws):
|
||||
@@ -489,7 +484,7 @@ public partial class CoreConfigV2rayService
|
||||
}
|
||||
if (xhttpExtra.IsNotEmpty())
|
||||
{
|
||||
xhttpSettings.extra = JsonUtils.ParseJson(xhttpExtra);
|
||||
xhttpSettings.extra = JsonUtils.ParseJson(xhttpExtra) as JsonObject;
|
||||
}
|
||||
|
||||
streamSettings.xhttpSettings = xhttpSettings;
|
||||
@@ -581,7 +576,7 @@ public partial class CoreConfigV2rayService
|
||||
auth = _node.Password,
|
||||
};
|
||||
hy2Finalmask.udp?.Reverse();
|
||||
streamSettings.finalmask = hy2Finalmask;
|
||||
streamSettings.finalmask = (JsonObject)JsonUtils.SerializeToNode(hy2Finalmask);
|
||||
break;
|
||||
|
||||
case "masque":
|
||||
@@ -603,11 +598,21 @@ public partial class CoreConfigV2rayService
|
||||
}
|
||||
headers = userHeaders;
|
||||
}
|
||||
streamSettings.masqueSettings = new()
|
||||
var masqueSettings = new MasqueSettings4Ray
|
||||
{
|
||||
path = protocolExtra?.MasquePath.NullIfEmpty(),
|
||||
headers = headers.Count > 0 ? headers : null,
|
||||
};
|
||||
if (!protocolExtra.WarpPrivateKey.IsNullOrEmpty())
|
||||
{
|
||||
masqueSettings.warp = new MasqueWarpSettings4Ray
|
||||
{
|
||||
publicKey = protocolExtra.WarpPublicKey,
|
||||
privateKey = protocolExtra.WarpPrivateKey,
|
||||
address = Utils.String2List(protocolExtra.WarpAddress)?.Select(s => s.Trim()).ToList(),
|
||||
};
|
||||
}
|
||||
streamSettings.masqueSettings = masqueSettings;
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -637,7 +642,7 @@ public partial class CoreConfigV2rayService
|
||||
pathHttp = string.Join(",".AppendQuotes(), arrPath);
|
||||
}
|
||||
request = request.Replace("$requestPath$", $"{pathHttp.AppendQuotes()}");
|
||||
rawSettings.header.request = JsonUtils.Deserialize<object>(request);
|
||||
rawSettings.header.request = JsonUtils.SerializeToNode(request) as JsonObject;
|
||||
|
||||
streamSettings.rawSettings = rawSettings;
|
||||
}
|
||||
@@ -646,7 +651,7 @@ public partial class CoreConfigV2rayService
|
||||
|
||||
if (!_node.Finalmask.IsNullOrEmpty())
|
||||
{
|
||||
streamSettings.finalmask = JsonUtils.ParseJson(_node.Finalmask);
|
||||
streamSettings.finalmask = JsonUtils.ParseJson(_node.Finalmask) as JsonObject;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -809,8 +814,6 @@ public partial class CoreConfigV2rayService
|
||||
&& (n.streamSettings?.sockopt?.dialerProxy?.IsNullOrEmpty() ?? true))
|
||||
.ToList();
|
||||
|
||||
var fragmentMask = BuildFragmentsMasks();
|
||||
|
||||
foreach (var outbound in actOutboundWithTlsList)
|
||||
{
|
||||
var finalMaskJsonObj = JsonUtils.ParseJson(JsonUtils.Serialize(outbound.streamSettings?.finalmask)) as JsonObject ?? new JsonObject();
|
||||
@@ -818,6 +821,13 @@ public partial class CoreConfigV2rayService
|
||||
var tcpFinalmaskList = finalMaskJsonObj["tcp"] as JsonArray ?? [];
|
||||
if (tcpFinalmaskList.Count == 0)
|
||||
{
|
||||
var fragmentMask = BuildFragmentsMasks();
|
||||
// Xray's tlshello fragment is incompatible with REALITY.
|
||||
if (outbound.streamSettings.security == Global.StreamSecurityReality
|
||||
&& fragmentMask.settings?.packets == "tlshello")
|
||||
{
|
||||
fragmentMask.settings!.packets = "1-3";
|
||||
}
|
||||
tcpFinalmaskList.Add(JsonUtils.SerializeToNode(fragmentMask));
|
||||
finalMaskJsonObj["tcp"] = tcpFinalmaskList;
|
||||
}
|
||||
@@ -837,10 +847,10 @@ public partial class CoreConfigV2rayService
|
||||
protocol = "freedom",
|
||||
streamSettings = new StreamSettings4Ray
|
||||
{
|
||||
finalmask = new Finalmask4Ray
|
||||
finalmask = JsonUtils.SerializeToNode(new Finalmask4Ray
|
||||
{
|
||||
tcp = [fragmentMask],
|
||||
},
|
||||
}) as JsonObject,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -914,4 +924,32 @@ public partial class CoreConfigV2rayService
|
||||
}
|
||||
return clonedOutbounds;
|
||||
}
|
||||
|
||||
private EMultipleLoad? FindFirstPolicyGroupMultipleLoad()
|
||||
{
|
||||
if (!_node.ConfigType.IsGroupType())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// BFS: the nearest policy group in the group tree decides the multiple load strategy.
|
||||
var queue = new Queue<ProfileItem>([_node]);
|
||||
while (queue.Count > 0)
|
||||
{
|
||||
var node = queue.Dequeue();
|
||||
if (node.ConfigType == EConfigType.PolicyGroup)
|
||||
{
|
||||
return node.GetProtocolExtra().MultipleLoad;
|
||||
}
|
||||
foreach (var childId in Utils.String2List(node.GetProtocolExtra().ChildItems) ?? [])
|
||||
{
|
||||
if (context.AllProxiesMap.TryGetValue(childId, out var childNode)
|
||||
&& childNode.ConfigType.IsGroupType())
|
||||
{
|
||||
queue.Enqueue(childNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,11 +200,12 @@ public partial class CoreConfigV2rayService
|
||||
return tag;
|
||||
}
|
||||
|
||||
var proxyOutbounds = new CoreConfigV2rayService(context with { Node = node, }).BuildAllProxyOutbounds(tag);
|
||||
var service = new CoreConfigV2rayService(context with { Node = node, });
|
||||
var proxyOutbounds = service.BuildAllProxyOutbounds(tag);
|
||||
_coreConfig.outbounds.AddRange(proxyOutbounds);
|
||||
if (proxyOutbounds.Count(n => n.tag.StartsWith(tag)) > 1)
|
||||
{
|
||||
var multipleLoad = node.GetProtocolExtra().MultipleLoad ?? EMultipleLoad.LeastPing;
|
||||
var multipleLoad = service.FindFirstPolicyGroupMultipleLoad() ?? EMultipleLoad.LeastPing;
|
||||
GenObservatory(multipleLoad, tag);
|
||||
GenBalancer(multipleLoad, tag);
|
||||
}
|
||||
|
||||
@@ -188,10 +188,13 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
|
||||
break;
|
||||
|
||||
case ESpeedActionType.Speedtest:
|
||||
// A skipped or failed test must not leave the last speed behind for sorting.
|
||||
ProfileExManager.Instance.SetTestSpeed(it.IndexId, 0);
|
||||
await UpdateFunc(it.IndexId, "", message);
|
||||
break;
|
||||
|
||||
case ESpeedActionType.Mixedtest:
|
||||
ProfileExManager.Instance.SetTestSpeed(it.IndexId, 0);
|
||||
await UpdateFunc(it.IndexId, message, message);
|
||||
break;
|
||||
}
|
||||
@@ -538,8 +541,20 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
|
||||
private async Task<int> DoUdpTest(ServerTestItem it,
|
||||
ConcurrentDictionary<string, byte> completedIds, CancellationToken ct = default)
|
||||
{
|
||||
var udpService = UdpTestService.CreateFromTarget(_config?.SpeedTestItem.UdpTestTarget, out var udpTestUrl);
|
||||
var responseTime = (int)(await udpService.SendUdpRequestAsync(udpTestUrl, it.Port, ct)).TotalMilliseconds;
|
||||
var responseTime = -1;
|
||||
try
|
||||
{
|
||||
var udpService = UdpTestService.CreateFromTarget(_config?.SpeedTestItem.UdpTestTarget, out var udpTestUrl);
|
||||
responseTime = (int)(await udpService.SendUdpRequestAsync(udpTestUrl, it.Port, ct)).TotalMilliseconds;
|
||||
}
|
||||
catch (OperationCanceledException) when (ct.IsCancellationRequested)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore
|
||||
}
|
||||
|
||||
ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime);
|
||||
await UpdateFunc(it.IndexId, responseTime.ToString());
|
||||
@@ -587,6 +602,12 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
|
||||
private List<List<ServerTestItem>> GetTestBatchItem(List<ServerTestItem> lstSelected, int pageSize)
|
||||
{
|
||||
List<List<ServerTestItem>> lstTest = [];
|
||||
if (lstSelected.Count == 0)
|
||||
{
|
||||
return lstTest;
|
||||
}
|
||||
|
||||
pageSize = Math.Max(pageSize, 1);
|
||||
var lst1 = lstSelected.Where(t => t.CoreType == ECoreType.Xray).ToList();
|
||||
var lst2 = lstSelected.Where(t => t.CoreType == ECoreType.sing_box).ToList();
|
||||
|
||||
|
||||
@@ -335,9 +335,13 @@ public partial class UpdateService(Config config, Func<bool, string, Task> updat
|
||||
//Check for avalonia desktop windows version
|
||||
if (File.Exists(Path.Combine(Utils.GetBaseDirectory(), "libHarfBuzzSharp.dll")))
|
||||
{
|
||||
return url?.Replace(".zip", "-desktop.zip");
|
||||
url = url?.Replace(".zip", "-desktop.zip");
|
||||
}
|
||||
|
||||
#if V2RAYN_IS_AOT
|
||||
url = url?.Replace(".zip", "-aot.zip");
|
||||
#endif
|
||||
|
||||
return url;
|
||||
}
|
||||
else if (Utils.IsLinux())
|
||||
|
||||
@@ -24,7 +24,7 @@ public sealed class WindowsJobService : IDisposable
|
||||
|
||||
try
|
||||
{
|
||||
var length = Marshal.SizeOf(typeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION));
|
||||
var length = Marshal.SizeOf<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>();
|
||||
extendedInfoPtr = Marshal.AllocHGlobal(length);
|
||||
Marshal.StructureToPtr(extendedInfo, extendedInfoPtr, false);
|
||||
|
||||
|
||||
@@ -103,6 +103,18 @@ public partial class AddServerViewModel : MyReactiveObject, ICloseable
|
||||
[Reactive]
|
||||
public partial string MasqueHeaders { get; set; }
|
||||
|
||||
[Reactive]
|
||||
public partial string WarpPublicKey { get; set; }
|
||||
|
||||
[Reactive]
|
||||
public partial string WarpPrivateKey { get; set; }
|
||||
|
||||
[Reactive]
|
||||
public partial string WarpAddress { get; set; }
|
||||
|
||||
[Reactive]
|
||||
public partial string Network { get; set; }
|
||||
|
||||
[Reactive]
|
||||
public partial string RawHeaderType { get; set; }
|
||||
|
||||
@@ -274,7 +286,7 @@ public partial class AddServerViewModel : MyReactiveObject, ICloseable
|
||||
this.WhenAnyValue(x => x.CertSha)
|
||||
.Subscribe(_ => UpdateCertTip());
|
||||
|
||||
this.WhenAnyValue(x => x.SelectedSource.Network)
|
||||
this.WhenAnyValue(x => x.Network)
|
||||
.Subscribe(_ =>
|
||||
{
|
||||
this.RaisePropertyChanged(nameof(TransportHeaderType));
|
||||
@@ -299,6 +311,7 @@ public partial class AddServerViewModel : MyReactiveObject, ICloseable
|
||||
CoreType = SelectedSource?.CoreType?.ToString();
|
||||
AllowInsecure = SelectedSource?.GetAllowInsecure() == true;
|
||||
MuxEnabled = SelectedSource?.MuxEnabled == true;
|
||||
Network = SelectedSource?.Network ?? Global.DefaultNetwork;
|
||||
Cert = SelectedSource?.Cert ?? string.Empty;
|
||||
CertSha = SelectedSource?.CertSha ?? string.Empty;
|
||||
|
||||
@@ -330,6 +343,9 @@ public partial class AddServerViewModel : MyReactiveObject, ICloseable
|
||||
GeckoMaxPacketSize = protocolExtra.GeckoMaxPacketSize.ToInt();
|
||||
MasquePath = protocolExtra.MasquePath ?? string.Empty;
|
||||
MasqueHeaders = protocolExtra.MasqueHeaders ?? string.Empty;
|
||||
WarpPublicKey = protocolExtra.WarpPublicKey ?? string.Empty;
|
||||
WarpPrivateKey = protocolExtra.WarpPrivateKey ?? string.Empty;
|
||||
WarpAddress = protocolExtra.WarpAddress ?? string.Empty;
|
||||
|
||||
RawHeaderType = transport.RawHeaderType ?? Global.None;
|
||||
Host = transport.Host ?? string.Empty;
|
||||
@@ -405,10 +421,11 @@ public partial class AddServerViewModel : MyReactiveObject, ICloseable
|
||||
SelectedSource.MuxEnabled = MuxEnabled;
|
||||
SelectedSource.Cert = Cert.IsNullOrEmpty() ? string.Empty : Cert;
|
||||
SelectedSource.CertSha = CertSha.IsNullOrEmpty() ? string.Empty : CertSha;
|
||||
if (!Global.Networks.Contains(SelectedSource.Network))
|
||||
if (!Global.Networks.Contains(Network))
|
||||
{
|
||||
SelectedSource.Network = Global.DefaultNetwork;
|
||||
Network = Global.DefaultNetwork;
|
||||
}
|
||||
SelectedSource.Network = Network;
|
||||
|
||||
var transport = new TransportExtraItem
|
||||
{
|
||||
@@ -453,6 +470,9 @@ public partial class AddServerViewModel : MyReactiveObject, ICloseable
|
||||
GeckoMaxPacketSize = GeckoMaxPacketSize > 0 ? GeckoMaxPacketSize.ToString() : null,
|
||||
MasquePath = MasquePath.NullIfEmpty(),
|
||||
MasqueHeaders = MasqueHeaders.NullIfEmpty(),
|
||||
WarpPublicKey = WarpPublicKey.NullIfEmpty(),
|
||||
WarpPrivateKey = WarpPrivateKey.NullIfEmpty(),
|
||||
WarpAddress = WarpAddress.NullIfEmpty(),
|
||||
});
|
||||
SelectedSource.SetTransportExtra(transport);
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ namespace ServiceLib.ViewModels;
|
||||
|
||||
public partial class ClashProxiesViewModel : MyReactiveObject
|
||||
{
|
||||
private readonly int _delayTimeout = 99999999;
|
||||
private const int _delayTimeout = 99999999;
|
||||
private ClashItem _clashItem = new();
|
||||
|
||||
public ClashProxiesViewModel()
|
||||
@@ -235,6 +235,11 @@ public partial class ClashProxiesViewModel : MyReactiveObject
|
||||
{
|
||||
return;
|
||||
}
|
||||
var dicCurrentDetails = new Dictionary<string, ClashProxyModel>();
|
||||
foreach (var item in ProxyDetails)
|
||||
{
|
||||
dicCurrentDetails.TryAdd(item.Name, item);
|
||||
}
|
||||
var lstDetails = new List<ClashProxyModel>();
|
||||
foreach (var item in proxy.all)
|
||||
{
|
||||
@@ -243,15 +248,29 @@ public partial class ClashProxiesViewModel : MyReactiveObject
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var delay = proxy2.history?.Count > 0 ? proxy2.history.Last().delay : -1;
|
||||
var delay = -1;
|
||||
var hasData = false;
|
||||
|
||||
if (proxy2.history is { Count: > 0 })
|
||||
{
|
||||
delay = proxy2.history.Last().delay;
|
||||
hasData = true;
|
||||
}
|
||||
else if (dicCurrentDetails.TryGetValue(item, out var detail))
|
||||
{
|
||||
delay = detail.Delay;
|
||||
hasData = true;
|
||||
}
|
||||
|
||||
var isValidDelay = delay is > 0 and not _delayTimeout;
|
||||
|
||||
lstDetails.Add(new ClashProxyModel
|
||||
{
|
||||
IsActive = item == proxy.now,
|
||||
Name = item,
|
||||
Type = proxy2.type,
|
||||
Delay = delay <= 0 ? _delayTimeout : delay,
|
||||
DelayName = delay <= 0 ? string.Empty : $"{delay}ms",
|
||||
Delay = isValidDelay ? delay : _delayTimeout,
|
||||
DelayName = !hasData ? string.Empty : (isValidDelay ? $"{delay}ms" : "-1"),
|
||||
});
|
||||
}
|
||||
// sort
|
||||
@@ -340,7 +359,7 @@ public partial class ClashProxiesViewModel : MyReactiveObject
|
||||
private async Task TestGroupProxiesDelay()
|
||||
{
|
||||
var groupProxy = TryGetProxy(SelectedGroup.Name);
|
||||
if (!Global.allowSelectType.Contains(groupProxy?.type))
|
||||
if (!Global.allowSelectType.Contains(groupProxy?.type, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -362,8 +381,9 @@ public partial class ClashProxiesViewModel : MyReactiveObject
|
||||
{
|
||||
return;
|
||||
}
|
||||
detail.Delay = Convert.ToInt32(result.Delay);
|
||||
detail.DelayName = $"{detail.Delay}ms";
|
||||
var isValid = int.TryParse(result.Delay, out var delay) && delay > 0 && delay != _delayTimeout;
|
||||
detail.Delay = isValid ? delay : _delayTimeout;
|
||||
detail.DelayName = isValid ? $"{delay}ms" : "-1";
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ public partial class DNSSettingViewModel : MyReactiveObject, ICloseable
|
||||
[Reactive] public partial bool RayCustomDNSEnableCompatible { get; set; }
|
||||
[Reactive] public partial bool SBCustomDNSEnableCompatible { get; set; }
|
||||
|
||||
public bool IsSimpleDNSEnabled => !(RayCustomDNSEnableCompatible && SBCustomDNSEnableCompatible);
|
||||
[ObservableAsProperty] public partial bool IsSimpleDNSEnabled { get; }
|
||||
|
||||
public ReactiveCommand<RxVoid, RxVoid> SaveCmd { get; }
|
||||
public ReactiveCommand<RxVoid, RxVoid> ImportDefConfig4V2rayCompatibleCmd { get; }
|
||||
@@ -60,8 +60,11 @@ public partial class DNSSettingViewModel : MyReactiveObject, ICloseable
|
||||
await Task.CompletedTask;
|
||||
});
|
||||
|
||||
this.WhenAnyValue(x => x.RayCustomDNSEnableCompatible, x => x.SBCustomDNSEnableCompatible)
|
||||
.Subscribe(_ => this.RaisePropertyChanged(nameof(IsSimpleDNSEnabled)));
|
||||
_isSimpleDNSEnabledHelper = this.WhenAnyValue(
|
||||
x => x.RayCustomDNSEnableCompatible,
|
||||
x => x.SBCustomDNSEnableCompatible,
|
||||
(a, b) => !(a && b))
|
||||
.ToProperty(this, nameof(IsSimpleDNSEnabled));
|
||||
|
||||
_ = Init();
|
||||
}
|
||||
|
||||
@@ -527,7 +527,7 @@ public partial class MainWindowViewModel : MyReactiveObject
|
||||
public async Task AddServerViaImageAsync()
|
||||
{
|
||||
var imageFileName = await BrowseImageFileInteraction.HandleSafe(RxVoid.Default);
|
||||
await AddScanResultAsync(imageFileName);
|
||||
await ScanImageResult(imageFileName);
|
||||
}
|
||||
|
||||
public async Task ScanImageResult(string fileName)
|
||||
|
||||
@@ -448,7 +448,7 @@ public partial class ProfilesViewModel : MyReactiveObject
|
||||
return null;
|
||||
}
|
||||
|
||||
var orderProfiles = SelectedProfiles?.OrderBy(t => t.Sort);
|
||||
var orderProfiles = SelectedProfiles?.OrderBy(t => t.Sort).ToList();
|
||||
if (latest)
|
||||
{
|
||||
lstSelected.AddRange(await AppManager.Instance.GetProfileItemsOrderedByIndexIds(orderProfiles.Select(sp => sp?.IndexId)));
|
||||
@@ -702,6 +702,33 @@ public partial class ProfilesViewModel : MyReactiveObject
|
||||
}
|
||||
}
|
||||
|
||||
public async Task MoveServersTo(
|
||||
IReadOnlyList<int> sourceIndices,
|
||||
int insertIndex)
|
||||
{
|
||||
if (sourceIndices.Count == 0 ||
|
||||
insertIndex < 0 ||
|
||||
insertIndex > ProfileItems.Count)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var orderedProfileIds = ProfileItems
|
||||
.Select(item => item.IndexId)
|
||||
.ToList();
|
||||
|
||||
var result = await ConfigHandler.MoveServersTo(
|
||||
_config,
|
||||
orderedProfileIds,
|
||||
sourceIndices,
|
||||
insertIndex);
|
||||
|
||||
if (result == 0)
|
||||
{
|
||||
await RefreshServers();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ServerSpeedtest(ESpeedActionType actionType)
|
||||
{
|
||||
List<ProfileItem>? lstSelected;
|
||||
@@ -712,7 +739,7 @@ public partial class ProfilesViewModel : MyReactiveObject
|
||||
actionType = ESpeedActionType.Realping;
|
||||
}
|
||||
|
||||
lstSelected = JsonUtils.Deserialize<List<ProfileItem>>(JsonUtils.Serialize(ProfileItems?.OrderBy(t => t.Sort)));
|
||||
lstSelected = JsonUtils.Deserialize<List<ProfileItem>>(JsonUtils.Serialize(ProfileItems?.OrderBy(t => t.Sort).ToList()));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -191,13 +191,7 @@ public partial class RoutingRuleSettingViewModel : MyReactiveObject, ICloseable
|
||||
}
|
||||
if (lst.Count > 0)
|
||||
{
|
||||
var options = new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
};
|
||||
await SetClipboardDataInteraction.HandleSafe(JsonUtils.Serialize(lst, options));
|
||||
await SetClipboardDataInteraction.HandleSafe(JsonUtils.SerializeCamelCase(lst));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -219,7 +219,8 @@ public partial class StatusBarViewModel : MyReactiveObject
|
||||
await ConfigHandler.InitBuiltinRouting(_config);
|
||||
await RefreshRoutingsMenu();
|
||||
await InboundDisplayStatus();
|
||||
await ChangeSystemProxyAsync(_config.SystemProxyItem.SysProxyType, true);
|
||||
// The view refreshes its icon on initialization; interaction handlers are not registered yet.
|
||||
await ChangeSystemProxyAsync(_config.SystemProxyItem.SysProxyType, false);
|
||||
|
||||
BlRouting = true;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
xmlns:dialogHost="clr-namespace:DialogHostAvalonia;assembly=DialogHost.Avalonia"
|
||||
xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib"
|
||||
xmlns:semi="https://irihi.tech/semi"
|
||||
xmlns:service="clr-namespace:ServiceLib;assembly=ServiceLib"
|
||||
xmlns:vms="clr-namespace:ServiceLib.ViewModels;assembly=ServiceLib"
|
||||
Name="v2rayN"
|
||||
x:DataType="vms:StatusBarViewModel"
|
||||
@@ -37,7 +38,7 @@
|
||||
<TrayIcon
|
||||
Command="{Binding NotifyLeftClickCmd}"
|
||||
Icon="/Assets/NotifyIcon1.ico"
|
||||
ToolTipText="{Binding RunningServerToolTipText}">
|
||||
ToolTipText="{Binding RunningServerToolTipText, FallbackValue={x:Static service:Global.AppName}}">
|
||||
<TrayIcon.Menu>
|
||||
<NativeMenu>
|
||||
<NativeMenuItem
|
||||
|
||||
@@ -69,7 +69,7 @@ public partial class QRCodeAvaloniaUtils
|
||||
|
||||
var bmi = new BITMAPINFO
|
||||
{
|
||||
biSize = Marshal.SizeOf(typeof(BITMAPINFO)),
|
||||
biSize = Marshal.SizeOf<BITMAPINFO>(),
|
||||
biWidth = width,
|
||||
biHeight = -height,
|
||||
biPlanes = 1,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user