fix(shell): preserve Windows launcher paths and elevation arguments
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$auditRoot = Join-Path ([IO.Path]::GetTempPath()) 'napcat-windows-launcher-tests'
|
||||
$sourceRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '../../../packages/napcat-shell-loader'))
|
||||
New-Item -ItemType Directory -Path $auditRoot -Force | Out-Null
|
||||
$compiledStub = Join-Path $auditRoot ('stub-' + [guid]::NewGuid().ToString('N') + '.exe')
|
||||
& (Join-Path $env:WINDIR 'Microsoft.NET/Framework64/v4.0.30319/csc.exe') /nologo ("/out:" + $compiledStub) (Join-Path $PSScriptRoot 'windows-launcher-stub.cs')
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Failed to compile launcher fixture' }
|
||||
$stubDirectory = Join-Path $auditRoot 'tools'
|
||||
New-Item -ItemType Directory -Path $stubDirectory -Force | Out-Null
|
||||
foreach ($stubName in @('reg.exe', 'net.exe', 'powershell.exe')) {
|
||||
Copy-Item -LiteralPath $compiledStub -Destination (Join-Path $stubDirectory $stubName) -Force
|
||||
}
|
||||
$results = [Collections.Generic.List[object]]::new()
|
||||
foreach ($launcher in @('launcher.bat', 'launcher-win10.bat', 'launcher-user.bat', 'launcher-win10-user.bat')) {
|
||||
foreach ($pathKind in @('plain', 'space', 'special', 'unquoted', 'missing-registry', 'boot-error', 'elevation')) {
|
||||
if ($pathKind -eq 'elevation' -and $launcher -like '*-user.bat') { continue }
|
||||
$directoryName = switch ($pathKind) {
|
||||
plain { 'plain' }
|
||||
space { 'space path' }
|
||||
special { "路径 & () ! # ' test" }
|
||||
default { "路径 & () ! # ' $pathKind" }
|
||||
}
|
||||
$fixture = Join-Path $auditRoot ($launcher.Replace('.bat', '') + '\' + $directoryName)
|
||||
New-Item -ItemType Directory -Path $fixture -Force | Out-Null
|
||||
Copy-Item -LiteralPath (Join-Path $sourceRoot $launcher) -Destination $fixture -Force
|
||||
Copy-Item -LiteralPath $compiledStub -Destination (Join-Path $fixture 'NapCatWinBootMain.exe') -Force
|
||||
New-Item -ItemType File -Path (Join-Path $fixture 'QQ.exe') -Force | Out-Null
|
||||
$output = Join-Path $fixture 'arguments.txt'
|
||||
if (Test-Path -LiteralPath $output) { Remove-Item -LiteralPath $output }
|
||||
$startInfo = [Diagnostics.ProcessStartInfo]::new()
|
||||
$startInfo.FileName = $env:ComSpec
|
||||
$startInfo.Arguments = '/d /s /c ""' + (Join-Path $fixture $launcher) + '" -q "123456" "argument with spaces" "literal!value""'
|
||||
$startInfo.WorkingDirectory = $auditRoot
|
||||
$startInfo.UseShellExecute = $false
|
||||
$startInfo.CreateNoWindow = $true
|
||||
$startInfo.RedirectStandardInput = $true
|
||||
$startInfo.RedirectStandardOutput = $true
|
||||
$startInfo.RedirectStandardError = $true
|
||||
$startInfo.EnvironmentVariables['PATH'] = $stubDirectory + ';' + $env:PATH
|
||||
$startInfo.EnvironmentVariables['NAPCAT_AUDIT_UNINSTALL'] = if ($pathKind -eq 'missing-registry') { '' } elseif ($pathKind -eq 'unquoted') { Join-Path $fixture 'Uninstall.exe' } else { '"' + (Join-Path $fixture 'Uninstall.exe') + '"' }
|
||||
$startInfo.EnvironmentVariables['NAPCAT_AUDIT_NET_STATUS'] = if ($pathKind -eq 'elevation') { '1' } else { '0' }
|
||||
$startInfo.EnvironmentVariables['NAPCAT_AUDIT_BOOT_STATUS'] = if ($pathKind -eq 'boot-error') { '31' } else { '0' }
|
||||
$startInfo.EnvironmentVariables['NAPCAT_AUDIT_OUTPUT'] = $output
|
||||
$process = [Diagnostics.Process]::Start($startInfo)
|
||||
$stdout = $process.StandardOutput.ReadToEndAsync()
|
||||
$stderr = $process.StandardError.ReadToEndAsync()
|
||||
$process.StandardInput.WriteLine('')
|
||||
$process.StandardInput.Close()
|
||||
if (-not $process.WaitForExit(10000)) {
|
||||
& taskkill.exe /PID $process.Id /T /F | Out-Null
|
||||
throw "$launcher $pathKind timed out"
|
||||
}
|
||||
$expected = @((Join-Path $fixture 'QQ.exe'), (Join-Path $fixture 'NapCatWinBootHook.dll'), '-q', '123456', 'argument with spaces', 'literal!value')
|
||||
if ($pathKind -eq 'missing-registry') { $expected = @() }
|
||||
if ($pathKind -eq 'elevation') {
|
||||
$terminal = if ($launcher -eq 'launcher.bat') { 'wt.exe' } else { 'powershell.exe' }
|
||||
$expected = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', (Join-Path $fixture 'launcher-elevate.ps1'), $terminal, (Join-Path $fixture $launcher), '-q', '123456', 'argument with spaces', 'literal!value')
|
||||
}
|
||||
$actual = @()
|
||||
if (Test-Path -LiteralPath $output) {
|
||||
$actual = @(Get-Content -LiteralPath $output -Encoding UTF8 | Select-Object -Skip 1 | ForEach-Object { [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($_)) })
|
||||
}
|
||||
$passed = $actual.Count -eq $expected.Count
|
||||
for ($argumentIndex = 0; $argumentIndex -lt $expected.Count -and $passed; $argumentIndex++) {
|
||||
$passed = $actual[$argumentIndex] -ceq $expected[$argumentIndex]
|
||||
}
|
||||
$expectedExit = if ($pathKind -eq 'missing-registry') { 1 } elseif ($pathKind -eq 'boot-error') { 31 } else { 0 }
|
||||
$passed = $passed -and $process.ExitCode -eq $expectedExit
|
||||
$result = [PSCustomObject]@{ launcher=$launcher; path=$pathKind; passed=$passed; exit=$process.ExitCode; actual=$actual; stdout=$stdout.Result; stderr=$stderr.Result }
|
||||
$results.Add($result)
|
||||
$result | Select-Object launcher,path,passed,exit | ConvertTo-Json -Compress
|
||||
}
|
||||
}
|
||||
function Start-Process {
|
||||
param([string]$FilePath, [string]$ArgumentList, [string]$Verb)
|
||||
$global:napcatAuditElevationRequest = @{ file=$FilePath; arguments=$ArgumentList; verb=$Verb }
|
||||
}
|
||||
foreach ($terminal in @('wt.exe', 'powershell.exe')) {
|
||||
$testScript = "C:\路径 & () ! # '\launcher.bat"
|
||||
$testArguments = @('-q', '123456', 'argument with spaces', 'literal!value', 'literal$()', 'quote"value')
|
||||
& (Join-Path $sourceRoot 'launcher-elevate.ps1') $terminal $testScript @testArguments
|
||||
$encodedCommand = ($global:napcatAuditElevationRequest.arguments -split ' ')[-1]
|
||||
$command = [Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($encodedCommand))
|
||||
$parseTokens = $null
|
||||
$parseErrors = $null
|
||||
[Management.Automation.Language.Parser]::ParseInput($command, [ref]$parseTokens, [ref]$parseErrors) | Out-Null
|
||||
$match = [regex]::Match($command, "FromBase64String\('([A-Za-z0-9+/=]+)'\)")
|
||||
$payload = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($match.Groups[1].Value)) | ConvertFrom-Json
|
||||
$passed = $parseErrors.Count -eq 0 -and $payload.script -ceq $testScript -and $global:napcatAuditElevationRequest.verb -eq 'RunAs'
|
||||
$passed = $passed -and ($payload.arguments | ConvertTo-Json -Compress) -ceq ($testArguments | ConvertTo-Json -Compress)
|
||||
$result = [PSCustomObject]@{ launcher='launcher-elevate.ps1'; path=$terminal; passed=$passed; exit=0 }
|
||||
$results.Add($result)
|
||||
$result | ConvertTo-Json -Compress
|
||||
}
|
||||
$results | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $auditRoot 'results.json') -Encoding UTF8
|
||||
if ($results.Where({ -not $_.passed }).Count) { exit 1 }
|
||||
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
public class NapCatLauncherAudit {
|
||||
public static int Main(string[] arguments) {
|
||||
string name = Path.GetFileNameWithoutExtension(Process.GetCurrentProcess().MainModule.FileName);
|
||||
if (name == "reg") {
|
||||
string uninstall = Environment.GetEnvironmentVariable("NAPCAT_AUDIT_UNINSTALL");
|
||||
if (String.IsNullOrEmpty(uninstall)) return 1;
|
||||
Console.OutputEncoding = new UTF8Encoding(false);
|
||||
Console.WriteLine(" UninstallString REG_SZ " + uninstall);
|
||||
return 0;
|
||||
}
|
||||
if (name == "net") return Int32.Parse(Environment.GetEnvironmentVariable("NAPCAT_AUDIT_NET_STATUS"));
|
||||
string output = Environment.GetEnvironmentVariable("NAPCAT_AUDIT_OUTPUT");
|
||||
using (StreamWriter writer = new StreamWriter(output, false, new UTF8Encoding(false))) {
|
||||
writer.WriteLine(name);
|
||||
foreach (string argument in arguments) writer.WriteLine(Convert.ToBase64String(Encoding.UTF8.GetBytes(argument)));
|
||||
}
|
||||
return Int32.Parse(Environment.GetEnvironmentVariable("NAPCAT_AUDIT_BOOT_STATUS") ?? "0");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
name: Windows launcher
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- packages/napcat-shell-loader/launcher*
|
||||
- .github/tests/windows-launcher/**
|
||||
- .github/workflows/windows-launcher.yml
|
||||
pull_request:
|
||||
paths:
|
||||
- packages/napcat-shell-loader/launcher*
|
||||
- .github/tests/windows-launcher/**
|
||||
- .github/workflows/windows-launcher.yml
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
launchers:
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Check paths, registry, arguments and elevation
|
||||
shell: pwsh
|
||||
run: ./.github/tests/windows-launcher/validation.ps1
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: windows-launcher-results
|
||||
path: ${{ runner.temp }}/napcat-windows-launcher-tests/results.json
|
||||
@@ -0,0 +1,20 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$terminal = $args[0]
|
||||
$payload = @{
|
||||
script = $args[1]
|
||||
arguments = @($args | Select-Object -Skip 2)
|
||||
} | ConvertTo-Json -Compress
|
||||
$encodedPayload = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($payload))
|
||||
$command = @"
|
||||
`$payload = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('$encodedPayload')) | ConvertFrom-Json
|
||||
`$launchArguments = @(`$payload.arguments)
|
||||
& `$payload.script @launchArguments
|
||||
exit `$LASTEXITCODE
|
||||
"@
|
||||
$encodedCommand = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($command))
|
||||
$commandArguments = "-NoProfile -ExecutionPolicy Bypass -EncodedCommand $encodedCommand"
|
||||
if ($terminal -eq 'wt.exe') {
|
||||
Start-Process -FilePath $terminal -ArgumentList "powershell.exe $commandArguments" -Verb RunAs
|
||||
} else {
|
||||
Start-Process -FilePath $terminal -ArgumentList $commandArguments -Verb RunAs
|
||||
}
|
||||
@@ -7,11 +7,14 @@ set "NAPCAT_LOAD_PATH=%cd%\loadNapCat.js"
|
||||
set "NAPCAT_INJECT_PATH=%cd%\NapCatWinBootHook.dll"
|
||||
set "NAPCAT_LAUNCHER_PATH=%cd%\NapCatWinBootMain.exe"
|
||||
set "NAPCAT_MAIN_PATH=%cd%\napcat.mjs"
|
||||
:loop_read
|
||||
set "RetString="
|
||||
for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" /v "UninstallString"') do (
|
||||
set "RetString=%%~b"
|
||||
goto :napcat_boot
|
||||
)
|
||||
echo QQ installation was not found in the registry.
|
||||
pause
|
||||
exit /b 1
|
||||
|
||||
:napcat_boot
|
||||
for %%a in ("%RetString%") do (
|
||||
@@ -23,9 +26,10 @@ set "QQPath=%pathWithoutUninstall%QQ.exe"
|
||||
if not exist "%QQpath%" (
|
||||
echo provided QQ path is invalid
|
||||
pause
|
||||
exit /b
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
"%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" %*
|
||||
|
||||
set "napcat_exit_code=%ERRORLEVEL%"
|
||||
pause
|
||||
exit /b %napcat_exit_code%
|
||||
|
||||
@@ -7,11 +7,14 @@ set "NAPCAT_LOAD_PATH=%cd%\loadNapCat.js"
|
||||
set "NAPCAT_INJECT_PATH=%cd%\NapCatWinBootHook.dll"
|
||||
set "NAPCAT_LAUNCHER_PATH=%cd%\NapCatWinBootMain.exe"
|
||||
set "NAPCAT_MAIN_PATH=%cd%\napcat.mjs"
|
||||
:loop_read
|
||||
set "RetString="
|
||||
for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" /v "UninstallString"') do (
|
||||
set "RetString=%%~b"
|
||||
goto :napcat_boot
|
||||
)
|
||||
echo QQ installation was not found in the registry.
|
||||
pause
|
||||
exit /b 1
|
||||
|
||||
:napcat_boot
|
||||
for %%a in ("%RetString%") do (
|
||||
@@ -23,11 +26,13 @@ set "QQPath=%pathWithoutUninstall%QQ.exe"
|
||||
if not exist "%QQpath%" (
|
||||
echo provided QQ path is invalid
|
||||
pause
|
||||
exit /b
|
||||
exit /b 1
|
||||
)
|
||||
"%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" %*
|
||||
set "napcat_exit_code=%ERRORLEVEL%"
|
||||
|
||||
REM Optional: -q <QQ_NUMBER> for quick login, omit for QR code login
|
||||
REM Example: "%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" -q 123456
|
||||
|
||||
pause
|
||||
exit /b %napcat_exit_code%
|
||||
|
||||
@@ -3,24 +3,26 @@ setlocal DisableDelayedExpansion
|
||||
cd /d "%~dp0" || exit /b 1
|
||||
chcp 65001 >nul
|
||||
net session >nul 2>&1
|
||||
if %ERRORLEVEL% == 0 (
|
||||
echo Administrator mode detected.
|
||||
) else (
|
||||
echo Please run this script in administrator mode.
|
||||
powershell -Command "Start-Process 'cmd.exe' -ArgumentList '/c cd /d \"%cd%\" && \"%~f0\" %*' -Verb runAs"
|
||||
exit
|
||||
)
|
||||
if not errorlevel 1 goto :napcat_admin
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0launcher-elevate.ps1" powershell.exe "%~f0" %*
|
||||
exit /b %ERRORLEVEL%
|
||||
|
||||
:napcat_admin
|
||||
echo Administrator mode detected.
|
||||
|
||||
set "NAPCAT_PATCH_PACKAGE=%cd%\qqnt.json"
|
||||
set "NAPCAT_LOAD_PATH=%cd%\loadNapCat.js"
|
||||
set "NAPCAT_INJECT_PATH=%cd%\NapCatWinBootHook.dll"
|
||||
set "NAPCAT_LAUNCHER_PATH=%cd%\NapCatWinBootMain.exe"
|
||||
set "NAPCAT_MAIN_PATH=%cd%\napcat.mjs"
|
||||
:loop_read
|
||||
set "RetString="
|
||||
for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" /v "UninstallString"') do (
|
||||
set "RetString=%%~b"
|
||||
goto :napcat_boot
|
||||
)
|
||||
echo QQ installation was not found in the registry.
|
||||
pause
|
||||
exit /b 1
|
||||
|
||||
:napcat_boot
|
||||
for %%a in ("%RetString%") do (
|
||||
@@ -32,9 +34,10 @@ set "QQPath=%pathWithoutUninstall%QQ.exe"
|
||||
if not exist "%QQPath%" (
|
||||
echo provided QQ path is invalid
|
||||
pause
|
||||
exit /b
|
||||
exit /b 1
|
||||
)
|
||||
"%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" %*
|
||||
exit /b %ERRORLEVEL%
|
||||
|
||||
REM Optional: -q <QQ_NUMBER> for quick login, omit for QR code login
|
||||
REM Example: "%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" -q 123456
|
||||
|
||||
@@ -3,24 +3,26 @@ setlocal DisableDelayedExpansion
|
||||
cd /d "%~dp0" || exit /b 1
|
||||
chcp 65001 >nul
|
||||
net session >nul 2>&1
|
||||
if %ERRORLEVEL% == 0 (
|
||||
echo Administrator mode detected.
|
||||
) else (
|
||||
echo Please run this script in administrator mode.
|
||||
powershell -Command "Start-Process 'wt.exe' -ArgumentList 'cmd /c cd /d \"%cd%\" && \"%~f0\" %*' -Verb runAs"
|
||||
exit
|
||||
)
|
||||
if not errorlevel 1 goto :napcat_admin
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0launcher-elevate.ps1" wt.exe "%~f0" %*
|
||||
exit /b %ERRORLEVEL%
|
||||
|
||||
:napcat_admin
|
||||
echo Administrator mode detected.
|
||||
|
||||
set "NAPCAT_PATCH_PACKAGE=%cd%\qqnt.json"
|
||||
set "NAPCAT_LOAD_PATH=%cd%\loadNapCat.js"
|
||||
set "NAPCAT_INJECT_PATH=%cd%\NapCatWinBootHook.dll"
|
||||
set "NAPCAT_LAUNCHER_PATH=%cd%\NapCatWinBootMain.exe"
|
||||
set "NAPCAT_MAIN_PATH=%cd%\napcat.mjs"
|
||||
:loop_read
|
||||
set "RetString="
|
||||
for /f "tokens=2*" %%a in ('reg query "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\QQ" /v "UninstallString"') do (
|
||||
set "RetString=%%~b"
|
||||
goto :napcat_boot
|
||||
)
|
||||
echo QQ installation was not found in the registry.
|
||||
pause
|
||||
exit /b 1
|
||||
|
||||
:napcat_boot
|
||||
for %%a in ("%RetString%") do (
|
||||
@@ -32,7 +34,8 @@ set "QQPath=%pathWithoutUninstall%QQ.exe"
|
||||
if not exist "%QQPath%" (
|
||||
echo provided QQ path is invalid
|
||||
pause
|
||||
exit /b
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
"%NAPCAT_LAUNCHER_PATH%" "%QQPath%" "%NAPCAT_INJECT_PATH%" %*
|
||||
exit /b %ERRORLEVEL%
|
||||
|
||||
Reference in New Issue
Block a user