wolfssh/.github/workflows/windows-sftp.yml

817 lines
33 KiB
YAML

name: Windows wolfsshd SFTP Test
# This workflow tests wolfsshd, SFTP, and SCP on Windows with:
# 1. Basic test: wolfsshd + SFTP client (pwd, ls, put/get small file)
# 2. Recursive SCP test: pull a directory tree with "scp -O -r"
# 3. Large file test: WOLFSSH_NO_SFTP_TIMEOUT, WOLFSSH_MAX_SFTP_RW=10485760,
# WOLFSSH_MAX_CHN_NAMESZ=4200 - get and put a 3GB file
# 3. No-profile test: a user with no Windows profile at all, so wolfsshd has to
# build one with LoadUserProfileW instead of reading ProfileList
on:
push:
branches: [ 'master', 'main', 'release/**' ]
pull_request:
branches: [ '*' ]
env:
WOLFSSL_SOLUTION_FILE_PATH: wolfssl64.sln
SOLUTION_FILE_PATH: wolfssh.sln
USER_SETTINGS_H_NEW: wolfssh/ide/winvs/user_settings.h
USER_SETTINGS_H: wolfssl/IDE/WIN/user_settings.h
INCLUDE_DIR: wolfssh
WOLFSSL_BUILD_CONFIGURATION: Release
WOLFSSH_BUILD_CONFIGURATION: Release
BUILD_PLATFORM: x64
TARGET_PLATFORM: 10
TEST_PORT: 22222
jobs:
build:
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
include:
- test_type: basic
artifact_name: wolfssh-windows-build
- test_type: large_rw
artifact_name: wolfssh-windows-build-large-rw
steps:
- uses: actions/checkout@v6
with:
repository: wolfssl/wolfssl
path: wolfssl
- uses: actions/checkout@v6
with:
path: wolfssh
- name: Add MSBuild to PATH
uses: microsoft/setup-msbuild@v1
- name: Update user_settings.h for wolfSSL build
working-directory: ${{env.GITHUB_WORKSPACE}}
shell: bash
run: |
sed -i 's/#if 0/#if 1/g' ${{env.USER_SETTINGS_H_NEW}}
cp ${{env.USER_SETTINGS_H_NEW}} ${{env.USER_SETTINGS_H}}
- name: Restore wolfSSL NuGet packages
working-directory: ${{ github.workspace }}\wolfssl
run: nuget restore ${{env.WOLFSSL_SOLUTION_FILE_PATH}}
- name: Build wolfssl library
working-directory: ${{ github.workspace }}\wolfssl
run: msbuild /m /p:PlatformToolset=v142 /p:Platform=${{env.BUILD_PLATFORM}} /p:Configuration=${{env.WOLFSSL_BUILD_CONFIGURATION}} /t:wolfssl ${{env.WOLFSSL_SOLUTION_FILE_PATH}}
- name: Upload wolfSSL build artifacts
if: matrix.test_type == 'basic'
uses: actions/upload-artifact@v7
with:
name: wolfssl-windows-build
path: |
wolfssl/IDE/WIN/${{env.WOLFSSL_BUILD_CONFIGURATION}}/${{env.BUILD_PLATFORM}}/**
wolfssl/IDE/WIN/${{env.WOLFSSL_BUILD_CONFIGURATION}}/**
wolfssl/${{env.WOLFSSL_BUILD_CONFIGURATION}}/${{env.BUILD_PLATFORM}}/**
wolfssl/${{env.WOLFSSL_BUILD_CONFIGURATION}}/**
- name: Update user_settings.h for sshd and SFTP
working-directory: ${{env.GITHUB_WORKSPACE}}
shell: bash
run: |
# Enable SSHD, SFTP support (second #if 0 block)
sed -i 's/#if 0/#if 1/g' ${{env.USER_SETTINGS_H_NEW}}
cp ${{env.USER_SETTINGS_H_NEW}} ${{env.USER_SETTINGS_H}}
# For large_rw test: add SFTP large file defines
if [ "${{ matrix.test_type }}" = "large_rw" ]; then
echo "" >> ${{env.USER_SETTINGS_H_NEW}}
echo "/* SFTP large file test defines */" >> ${{env.USER_SETTINGS_H_NEW}}
echo "#define WOLFSSH_NO_SFTP_TIMEOUT" >> ${{env.USER_SETTINGS_H_NEW}}
echo "#define WOLFSSH_MAX_SFTP_RW 10485760" >> ${{env.USER_SETTINGS_H_NEW}}
echo "#define WOLFSSH_MAX_CHN_NAMESZ 4200" >> ${{env.USER_SETTINGS_H_NEW}}
echo "Added WOLFSSH_NO_SFTP_TIMEOUT, WOLFSSH_MAX_SFTP_RW=10485760, WOLFSSH_MAX_CHN_NAMESZ=4200"
cp ${{env.USER_SETTINGS_H_NEW}} ${{env.USER_SETTINGS_H}}
fi
- name: Restore NuGet packages
working-directory: ${{ github.workspace }}\wolfssh\ide\winvs
run: nuget restore ${{env.SOLUTION_FILE_PATH}}
- name: Build wolfssh
working-directory: ${{ github.workspace }}\wolfssh\ide\winvs
run: msbuild /m /p:PlatformToolset=v142 /p:Platform=${{env.BUILD_PLATFORM}} /p:WindowsTargetPlatformVersion=${{env.TARGET_PLATFORM}} /p:Configuration=${{env.WOLFSSH_BUILD_CONFIGURATION}} ${{env.SOLUTION_FILE_PATH}}
- name: Upload wolfSSH build artifacts
uses: actions/upload-artifact@v7
with:
name: ${{ matrix.artifact_name }}
if-no-files-found: error
path: |
wolfssh/ide/winvs/**/Release/**
test:
needs: build
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
include:
- test_type: basic
artifact_name: wolfssh-windows-build
- test_type: large_rw
artifact_name: wolfssh-windows-build-large-rw
- test_type: no_profile
artifact_name: wolfssh-windows-build
steps:
- uses: actions/checkout@v6
with:
path: wolfssh
- name: Download wolfSSH build artifacts
uses: actions/download-artifact@v8
with:
name: ${{ matrix.artifact_name }}
path: .
- name: Download wolfSSL build artifacts
uses: actions/download-artifact@v8
with:
name: wolfssl-windows-build
path: .
- name: Create Windows user testuser and authorized_keys
if: matrix.test_type != 'no_profile'
working-directory: ${{ github.workspace }}\wolfssh
shell: pwsh
run: |
$homeDir = "C:\Users\testuser"
$sshDir = "$homeDir\.ssh"
$authKeysFile = "$sshDir\authorized_keys"
$pw = 'T3stP@ss!xY9'
$o = net user testuser $pw /add /homedir:$homeDir 2>&1
if ($LASTEXITCODE -ne 0) {
if ($o -match "already exists") {
net user testuser /homedir:$homeDir 2>$null
} else {
Write-Host "net user failed: $o"
exit 1
}
}
Add-Content -Path $env:GITHUB_ENV -Value "TESTUSER_PASSWORD=$pw"
# Log the user on once so Windows builds a real profile: a directory
# with NTUSER.DAT plus the matching ProfileList entry.
$sec = ConvertTo-SecureString $pw -AsPlainText -Force
$cred = New-Object System.Management.Automation.PSCredential("testuser", $sec)
# -WorkingDirectory has to be readable by testuser.
Start-Process -FilePath "cmd.exe" -ArgumentList "/c", "exit" `
-Credential $cred -WorkingDirectory "C:\" -Wait -ErrorAction Stop
foreach ($i in 1..120) {
if (Test-Path "$homeDir\NTUSER.DAT") { break }
Start-Sleep -Milliseconds 500
}
if (-not (Test-Path "$homeDir\NTUSER.DAT")) {
Write-Host "ERROR: no profile was built for testuser"
Get-ChildItem -Path "C:\Users"
exit 1
}
# Later steps use $homeDir literally, so the profile has to be there.
$sid = (New-Object System.Security.Principal.NTAccount("testuser")).Translate([System.Security.Principal.SecurityIdentifier]).Value
$profKey = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\$sid"
$imagePath = (Get-ItemProperty -Path $profKey -Name ProfileImagePath -ErrorAction SilentlyContinue).ProfileImagePath
if ($imagePath -ne $homeDir) {
Write-Host "ERROR: testuser's profile is at '$imagePath', expected '$homeDir'"
exit 1
}
Write-Host "testuser profile built at $imagePath"
New-Item -ItemType Directory -Path $sshDir -Force | Out-Null
"" | Out-File -FilePath $authKeysFile -Encoding ASCII -NoNewline
icacls $authKeysFile /grant "testuser:R" /q
- name: Create SCP test key and directory tree
if: matrix.test_type == 'basic'
working-directory: ${{ github.workspace }}\wolfssh
shell: pwsh
run: |
# scp cannot supply a password non-interactively, so the recursive
# SCP test authenticates with a key.
$keyFile = Join-Path $env:RUNNER_TEMP "scp_id_ecdsa"
ssh-keygen -q -t ecdsa -b 256 -f $keyFile -N ""
Get-Content "$keyFile.pub" |
Add-Content -Path "C:\Users\testuser\.ssh\authorized_keys" -Encoding ASCII
icacls $keyFile /inheritance:r /grant:r "$($env:USERNAME):R" /q
Add-Content -Path $env:GITHUB_ENV -Value "SCP_KEY=$keyFile"
# A nested subdirectory and an empty one. The empty directory is the
# case where "." and ".." are the only entries.
$src = "C:\Users\testuser\scp_src"
New-Item -ItemType Directory -Path "$src\nested" -Force | Out-Null
New-Item -ItemType Directory -Path "$src\emptydir" -Force | Out-Null
Set-Content -Path "$src\alpha.txt" -Value "alpha contents"
Set-Content -Path "$src\beta.txt" -Value "beta contents"
Set-Content -Path "$src\nested\gamma.txt" -Value "gamma contents"
icacls $src /grant "testuser:(OI)(CI)F" /T /q
# No home directory, no ACL grant and no ProfileList entry, which is what a
# user that has never logged on to the machine looks like.
- name: Create Windows user testuser with no profile
if: matrix.test_type == 'no_profile'
shell: pwsh
run: |
$pw = 'T3stP@ss!xY9'
Add-Content -Path $env:GITHUB_ENV -Value "TESTUSER_PASSWORD=$pw"
# testuser2 is for the concurrent-session test, which needs a user
# whose profile no earlier step has built.
foreach ($u in @('testuser', 'testuser2')) {
$o = net user $u $pw /add 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Host "net user $u failed: $o"
exit 1
}
$sid = (New-Object System.Security.Principal.NTAccount($u)).Translate([System.Security.Principal.SecurityIdentifier]).Value
$profKey = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\$sid"
if (Test-Path $profKey) {
Write-Host "ERROR: $u already has a ProfileList entry"
exit 1
}
if (Test-Path "C:\Users\$u") {
Write-Host "ERROR: C:\Users\$u already exists"
exit 1
}
Write-Host "$u created with no profile, SID $sid"
if ($u -eq 'testuser') {
Add-Content -Path $env:GITHUB_ENV -Value "TESTUSER_SID=$sid"
}
else {
Add-Content -Path $env:GITHUB_ENV -Value "TESTUSER2_SID=$sid"
}
}
- name: Create wolfSSHd config file
working-directory: ${{ github.workspace }}\wolfssh
shell: pwsh
run: |
$keyPath = Join-Path "${{ github.workspace }}" "wolfssh\keys\server-key.pem"
$keyPathFull = (Resolve-Path $keyPath -ErrorAction Stop)
$configContent = @"
Port ${{env.TEST_PORT}}
PasswordAuthentication yes
PermitRootLogin yes
HostKey $($keyPathFull.Path)
AuthorizedKeysFile C:\Users\testuser\.ssh\authorized_keys
"@
$configContent | Out-File -FilePath sshd_config_test -Encoding ASCII
Get-Content sshd_config_test
- name: Find wolfSSH executables
working-directory: ${{ github.workspace }}\wolfssh
shell: pwsh
run: |
$searchRoot = "${{ github.workspace }}"
$sshdExe = Get-ChildItem -Path $searchRoot -Recurse -Filter "wolfsshd.exe" -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -like "*Release*" } | Select-Object -First 1
if ($sshdExe) {
Add-Content -Path $env:GITHUB_ENV -Value "SSHD_PATH=$($sshdExe.FullName)"
} else {
Write-Host "ERROR: wolfsshd.exe not found"
exit 1
}
$sftpExe = Get-ChildItem -Path $searchRoot -Recurse -Filter "wolfsftp.exe" -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -like "*Release*" } | Select-Object -First 1
if (-not $sftpExe) {
$sftpExe = Get-ChildItem -Path $searchRoot -Recurse -Filter "wolfsftp-client.exe" -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -like "*Release*" } | Select-Object -First 1
}
if ($sftpExe) {
Add-Content -Path $env:GITHUB_ENV -Value "SFTP_PATH=$($sftpExe.FullName)"
} else {
Write-Host "ERROR: SFTP client exe not found"
exit 1
}
$clientExe = Get-ChildItem -Path $searchRoot -Recurse -Filter "client.exe" -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -like "*Release*" } | Select-Object -First 1
if ($clientExe) {
Add-Content -Path $env:GITHUB_ENV -Value "CLIENT_PATH=$($clientExe.FullName)"
}
- name: Copy wolfSSL DLL to executable directory
working-directory: ${{ github.workspace }}
shell: pwsh
run: |
$sshdPath = $env:SSHD_PATH
$sshdDir = Split-Path -Parent $sshdPath
if (Test-Path (Join-Path $sshdDir "wolfssl.lib")) { exit 0 }
$wolfsslDll = Get-ChildItem -Path "${{ github.workspace }}\wolfssl" -Recurse -Filter "wolfssl.dll" -ErrorAction SilentlyContinue | Select-Object -First 1
if ($wolfsslDll) {
Copy-Item -Path $wolfsslDll.FullName -Destination (Join-Path $sshdDir "wolfssl.dll") -Force
}
- name: Grant service access to config and keys
working-directory: ${{ github.workspace }}\wolfssh
shell: pwsh
run: |
icacls (Get-Location).Path /grant "NT AUTHORITY\SYSTEM:(OI)(CI)RX" /T /q
- name: Start wolfSSHd as Windows service
working-directory: ${{ github.workspace }}\wolfssh
shell: pwsh
run: |
$sshdPath = $env:SSHD_PATH
$configPathFull = (Resolve-Path "sshd_config_test").Path
$serviceName = "wolfsshd"
$existingService = Get-Service -Name $serviceName -ErrorAction SilentlyContinue
if ($existingService) {
if ($existingService.Status -eq 'Running') { Stop-Service -Name $serviceName -Force }
sc.exe delete $serviceName | Out-Null
Start-Sleep -Seconds 2
}
$binPath = "`"$sshdPath`" -f `"$configPathFull`" -p ${{env.TEST_PORT}}"
sc.exe create $serviceName binPath= $binPath
sc.exe start $serviceName
Start-Sleep -Seconds 5
$service = Get-Service -Name $serviceName -ErrorAction SilentlyContinue
if ($service.Status -ne 'Running') {
Write-Host "ERROR: Service failed to start"
sc.exe query $serviceName
exit 1
}
Add-Content -Path $env:GITHUB_ENV -Value "SSHD_SERVICE_NAME=$serviceName"
- name: Test SFTP get non-existent file (no hang, correct error)
if: matrix.test_type != 'no_profile'
working-directory: ${{ github.workspace }}\wolfssh
shell: pwsh
timeout-minutes: 1
run: |
$sftpPath = $env:SFTP_PATH
$destFile = Join-Path $env:TEMP "copy.dat"
$getCommands = "get /this_file_does_not_exist_xyz $destFile`nquit"
$getCommands | Out-File -FilePath sftp_get_nonexistent_commands.txt -Encoding ASCII
$proc = Start-Process -FilePath $sftpPath `
-ArgumentList "-u", "testuser", "-P", $env:TESTUSER_PASSWORD, "-h", "localhost", "-p", "${{env.TEST_PORT}}" `
-RedirectStandardInput "sftp_get_nonexistent_commands.txt" `
-RedirectStandardOutput "sftp_get_nonexistent_out.txt" `
-RedirectStandardError "sftp_get_nonexistent_err.txt" `
-Wait -NoNewWindow -PassThru
Write-Host "=== SFTP Output ==="
$output = ""
if (Test-Path sftp_get_nonexistent_out.txt) {
$output = Get-Content sftp_get_nonexistent_out.txt -Raw
Write-Host $output
}
Write-Host "=== SFTP Error ==="
if (Test-Path sftp_get_nonexistent_err.txt) { Get-Content sftp_get_nonexistent_err.txt }
# Verify file was NOT created
if (Test-Path $destFile) {
Write-Host "ERROR: $destFile was created despite non-existent source file"
exit 1
}
# Verify error message was emitted
if ($output -notmatch "Error getting file") {
Write-Host "ERROR: Expected 'Error getting file' in output"
exit 1
}
Write-Host "PASS: SFTP get non-existent file did not create file, reported error correctly, did not hang"
# First connection for this user, so it is the one that builds the profile.
# An exec session goes through the shell subsystem, whose cleanup unloads
# it.
- name: Test exec session (no profile)
if: matrix.test_type == 'no_profile'
working-directory: ${{ github.workspace }}\wolfssh
shell: pwsh
timeout-minutes: 2
run: |
if (-not $env:CLIENT_PATH) {
Write-Host "ERROR: client.exe not found"
exit 1
}
"" | Out-File -FilePath exec_stdin.txt -Encoding ASCII
# Nothing may have built the profile before this connection, or the
# checks below would not be about the exec session.
if (Test-Path "C:\Users\testuser\NTUSER.DAT") {
Write-Host "ERROR: testuser already has a profile before the exec session"
exit 1
}
$proc = Start-Process -FilePath $env:CLIENT_PATH `
-ArgumentList "-u", "testuser", "-P", $env:TESTUSER_PASSWORD, "-h", "localhost", "-p", "${{env.TEST_PORT}}", "-c", "whoami" `
-RedirectStandardInput "exec_stdin.txt" `
-RedirectStandardOutput "exec_out.txt" `
-RedirectStandardError "exec_err.txt" `
-Wait -NoNewWindow -PassThru
Write-Host "=== exec output ==="
if (Test-Path exec_out.txt) { Get-Content exec_out.txt }
Write-Host "=== exec error ==="
if (Test-Path exec_err.txt) { Get-Content exec_err.txt }
if ($proc.ExitCode -ne 0) {
Write-Host "ERROR: exec session failed with exit $($proc.ExitCode)"
exit 1
}
if ((Get-Content exec_out.txt -Raw) -notmatch "testuser") {
Write-Host "ERROR: exec output does not name testuser"
exit 1
}
# NTUSER.DAT only exists if a real profile was built for the user.
if (-not (Test-Path "C:\Users\testuser\NTUSER.DAT")) {
Write-Host "ERROR: the exec session did not build a profile"
Get-ChildItem -Path "C:\Users"
exit 1
}
# The server thread tears the connection down after the client exits.
$loaded = $true
foreach ($i in 1..120) {
if (-not (Test-Path "Registry::HKEY_USERS\$env:TESTUSER_SID")) {
$loaded = $false
break
}
Start-Sleep -Milliseconds 500
}
if ($loaded) {
Write-Host "ERROR: testuser's hive is still loaded after the exec session"
exit 1
}
Write-Host "PASS: exec session built the profile and released the hive"
# Session A builds the profile and holds the only handle to the hive,
# session B finds the profile already there. Ending A must not unmount the
# hive while B is still running against it.
- name: Test concurrent sessions for one profile-less user
if: matrix.test_type == 'no_profile'
working-directory: ${{ github.workspace }}\wolfssh
shell: pwsh
timeout-minutes: 5
run: |
if (-not $env:CLIENT_PATH) {
Write-Host "ERROR: client.exe not found"
exit 1
}
# Each session holds itself open by pinging. No spaces in the paths, so
# -c stays a single argument. The commands end on their own, which
# leaves no child process holding the hive open.
$bMark = "C:\Users\Public\b_started.txt"
Remove-Item $bMark -Force -ErrorAction SilentlyContinue
Set-Content -Path "C:\Users\Public\hold_a.cmd" -Value "ping -n 25 127.0.0.1 > nul" -Encoding ASCII
Set-Content -Path "C:\Users\Public\hold_b.cmd" -Value @(
"echo started > $bMark", "ping -n 60 127.0.0.1 > nul") -Encoding ASCII
"" | Out-File -FilePath hold_stdin.txt -Encoding ASCII
$hive = "Registry::HKEY_USERS\$env:TESTUSER2_SID"
function Show-Hives($why) {
Write-Host "--- $why"
Write-Host "loaded hives:"
(Get-ChildItem Registry::HKEY_USERS -ErrorAction SilentlyContinue).Name
Write-Host "profile dir: $(Test-Path 'C:\Users\testuser2\NTUSER.DAT')"
}
if (Test-Path "C:\Users\testuser2\NTUSER.DAT") {
Write-Host "ERROR: testuser2 already has a profile"
exit 1
}
function Test-Running($proc) {
return $null -ne (Get-Process -Id $proc.Id -ErrorAction SilentlyContinue)
}
function Start-Hold($script, $tag) {
return Start-Process -FilePath $env:CLIENT_PATH -ArgumentList @(
"-u", "testuser2", "-P", $env:TESTUSER_PASSWORD,
"-h", "localhost", "-p", "${{env.TEST_PORT}}", "-c", $script) `
-RedirectStandardInput "hold_stdin.txt" `
-RedirectStandardOutput "hold_${tag}_out.txt" `
-RedirectStandardError "hold_${tag}_err.txt" `
-NoNewWindow -PassThru
}
$procA = Start-Hold "C:\Users\Public\hold_a.cmd" "a"
# Watch from the start: the hive can appear and go away again inside a
# fixed sleep, which reads the same as never appearing.
$sawHive = $false
foreach ($i in 1..40) {
if (Test-Path $hive) {
$sawHive = $true
Write-Host "hive appeared after $($i * 500) ms"
break
}
Start-Sleep -Milliseconds 500
}
if (-not (Test-Path "C:\Users\testuser2\NTUSER.DAT")) {
Write-Host "ERROR: session A did not build the profile"
Show-Hives "no profile"
Get-Content hold_a_out.txt, hold_a_err.txt -ErrorAction SilentlyContinue
exit 1
}
if (-not $sawHive) {
Write-Host "ERROR: testuser2's hive never appeared during session A"
Show-Hives "hive never seen"
Get-Content hold_a_out.txt, hold_a_err.txt -ErrorAction SilentlyContinue
exit 1
}
if (-not (Test-Running $procA)) {
Write-Host "ERROR: session A ended before the hive check"
Show-Hives "A gone"
Get-Content hold_a_out.txt, hold_a_err.txt -ErrorAction SilentlyContinue
exit 1
}
if (-not (Test-Path $hive)) {
Write-Host "ERROR: testuser2's hive went away while session A was live"
Show-Hives "hive dropped under A"
exit 1
}
Write-Host "session A built the profile and holds the hive"
$procB = Start-Hold "C:\Users\Public\hold_b.cmd" "b"
foreach ($i in 1..60) {
if (Test-Path $bMark) { break }
Start-Sleep -Milliseconds 500
}
if (-not (Test-Path $bMark)) {
Write-Host "ERROR: session B's command never ran"
Get-Content hold_b_out.txt, hold_b_err.txt -ErrorAction SilentlyContinue
exit 1
}
if (-not (Test-Running $procB)) {
Write-Host "ERROR: session B ended early"
Get-Content hold_b_out.txt, hold_b_err.txt
exit 1
}
Write-Host "session B is running alongside session A"
Wait-Process -Id $procA.Id -Timeout 120 -ErrorAction SilentlyContinue
if (Test-Running $procA) {
Write-Host "ERROR: session A did not end"
exit 1
}
# This asserts the hive does not go away, so settle rather than poll.
Start-Sleep -Seconds 15
if (-not (Test-Running $procB)) {
Write-Host "ERROR: session B ended before the hive check"
Get-Content hold_b_out.txt, hold_b_err.txt
exit 1
}
if (-not (Test-Path $hive)) {
Write-Host "ERROR: ending session A unmounted the hive under session B"
Show-Hives "hive dropped under B"
exit 1
}
Write-Host "hive survived session A ending"
Wait-Process -Id $procB.Id -Timeout 240 -ErrorAction SilentlyContinue
if (Test-Running $procB) {
Write-Host "ERROR: session B did not end"
exit 1
}
$loaded = $true
foreach ($i in 1..120) {
if (-not (Test-Path $hive)) {
$loaded = $false
break
}
Start-Sleep -Milliseconds 500
}
if ($loaded) {
Write-Host "ERROR: testuser2's hive is still loaded after both sessions ended"
Show-Hives "hive still loaded"
exit 1
}
Write-Host "PASS: the hive outlived session A and went away with session B"
- name: Test SFTP connection (basic)
if: matrix.test_type == 'basic' || matrix.test_type == 'no_profile'
working-directory: ${{ github.workspace }}\wolfssh
shell: pwsh
run: |
$sftpPath = $env:SFTP_PATH
$testCommands = "pwd`nls`nquit"
$testCommands | Out-File -FilePath sftp_commands.txt -Encoding ASCII
$process = Start-Process -FilePath $sftpPath `
-ArgumentList "-u", "testuser", "-P", $env:TESTUSER_PASSWORD, "-h", "localhost", "-p", "${{env.TEST_PORT}}" `
-RedirectStandardInput "sftp_commands.txt" `
-RedirectStandardOutput "sftp_output.txt" `
-RedirectStandardError "sftp_error.txt" `
-Wait -NoNewWindow -PassThru
Get-Content sftp_output.txt
Get-Content sftp_error.txt
if ($process.ExitCode -ne 0) {
Write-Host "ERROR: SFTP basic test failed with exit $($process.ExitCode)"
exit 1
}
Write-Host "Basic SFTP test passed"
- name: Test SCP recursive directory pull
if: matrix.test_type == 'basic'
working-directory: ${{ github.workspace }}\wolfssh
shell: pwsh
timeout-minutes: 2
run: |
$scp = Get-Command scp.exe -ErrorAction SilentlyContinue
if (-not $scp) {
Write-Host "ERROR: scp.exe not found on runner image"
exit 1
}
$out = Join-Path $env:RUNNER_TEMP "scp_out"
New-Item -ItemType Directory -Path $out -Force | Out-Null
# -O forces the legacy SCP protocol. Without it OpenSSH 9 and later
# run the transfer over SFTP and never reach the SCP server code.
$scpArgs = @(
"-O", "-r", "-P", "${{env.TEST_PORT}}", "-i", $env:SCP_KEY,
"-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=NUL",
"-o", "IdentitiesOnly=yes", "-o", "BatchMode=yes",
"testuser@localhost:C:/Users/testuser/scp_src", $out
)
$process = Start-Process -FilePath $scp.Source -ArgumentList $scpArgs `
-RedirectStandardOutput "scp_output.txt" `
-RedirectStandardError "scp_error.txt" `
-Wait -NoNewWindow -PassThru
Get-Content scp_output.txt
Get-Content scp_error.txt
if ($process.ExitCode -ne 0) {
Write-Host "ERROR: SCP recursive test failed with exit $($process.ExitCode)"
exit 1
}
# The whole tree must arrive. A server that treats end of directory as
# fatal aborts partway and leaves nested entries behind.
$expected = @(
"scp_src\alpha.txt", "scp_src\beta.txt",
"scp_src\nested", "scp_src\nested\gamma.txt", "scp_src\emptydir"
)
$missing = $expected | Where-Object { -not (Test-Path (Join-Path $out $_)) }
Get-ChildItem -Recurse $out | ForEach-Object { $_.FullName }
if ($missing) {
Write-Host "ERROR: missing from transfer: $($missing -join ', ')"
exit 1
}
Write-Host "Recursive SCP test passed"
- name: Verify the SFTP session used and released testuser's profile
if: matrix.test_type == 'no_profile'
working-directory: ${{ github.workspace }}\wolfssh
shell: pwsh
run: |
$output = Get-Content sftp_output.txt -Raw
if ($output -match "systemprofile") {
Write-Host "ERROR: session landed in the service account's profile"
Write-Host $output
exit 1
}
if ($output -notmatch "testuser") {
Write-Host "ERROR: 'testuser' missing from pwd output"
Write-Host $output
exit 1
}
# An SFTP session reaches the unload through wolfSSHD_AuthFreeUser at
# connection teardown rather than through the shell subsystem.
$loaded = $true
foreach ($i in 1..120) {
if (-not (Test-Path "Registry::HKEY_USERS\$env:TESTUSER_SID")) {
$loaded = $false
break
}
Start-Sleep -Milliseconds 500
}
if ($loaded) {
Write-Host "ERROR: testuser's hive is still loaded after the SFTP session"
(Get-ChildItem Registry::HKEY_USERS -ErrorAction SilentlyContinue).Name
exit 1
}
Write-Host "PASS: SFTP session used testuser's own home directory and released the hive"
- name: Create 3GB test file and run SFTP get/put
if: matrix.test_type == 'large_rw'
working-directory: ${{ github.workspace }}\wolfssh
shell: pwsh
timeout-minutes: 25
run: |
$sftpPath = $env:SFTP_PATH
$workDir = Join-Path $env:GITHUB_WORKSPACE "wolfssh"
$largeFile = Join-Path $workDir "large_test.dat"
$getDestPath = Join-Path $workDir "large_test_copy.dat"
# Create 3GB file: one random 10MB chunk repeated 307x + 2MB
Write-Host "Creating 3GB test file..."
$chunkSize = 10485760 # 10MB
$totalSize = [long]3221225472 # 3GB
$rng = New-Object System.Security.Cryptography.RNGCryptoServiceProvider
$chunk = New-Object byte[] $chunkSize
$rng.GetBytes($chunk)
$fs = [System.IO.File]::Create($largeFile)
$remaining = $totalSize
while ($remaining -gt 0) {
$toWrite = [int][Math]::Min([long]$chunkSize, $remaining)
$fs.Write($chunk, 0, $toWrite)
$remaining -= $toWrite
}
$fs.Close()
$hash = Get-FileHash -Path $largeFile -Algorithm SHA256
$hash.Hash | Out-File -FilePath (Join-Path $workDir "large_test.dat.sha256")
Write-Host "Created 3GB file, SHA256: $($hash.Hash)"
# SFTP PUT (upload)
# Use a relative remote path (no leading /) so the server resolves it
# under testuser's home directory. An absolute /large_test.dat maps to
# the drive root (C:\) where testuser has no write permission; relative
# large_test.dat is prefixed by workingDir (C:\Users\testuser) on the
# client and becomes C:\Users\testuser\large_test.dat on the server.
Write-Host "SFTP PUT 3GB file..."
$putCommands = "put $largeFile large_test.dat`nquit"
$putCommands | Out-File -FilePath (Join-Path $workDir "sftp_put_commands.txt") -Encoding ASCII
$proc = Start-Process -FilePath $sftpPath `
-ArgumentList "-u", "testuser", "-P", $env:TESTUSER_PASSWORD, "-h", "localhost", "-p", "${{env.TEST_PORT}}" `
-WorkingDirectory $workDir `
-RedirectStandardInput (Join-Path $workDir "sftp_put_commands.txt") `
-RedirectStandardOutput (Join-Path $workDir "sftp_put_out.txt") `
-RedirectStandardError (Join-Path $workDir "sftp_put_err.txt") `
-Wait -NoNewWindow -PassThru
$putOut = Get-Content (Join-Path $workDir "sftp_put_out.txt") -Raw -ErrorAction SilentlyContinue
Write-Host "=== SFTP PUT output ==="; Write-Host $putOut
if ($proc.ExitCode -ne 0 -or $putOut -match "Error pushing file") {
Get-Content (Join-Path $workDir "sftp_put_err.txt") -ErrorAction SilentlyContinue
Write-Host "ERROR: SFTP PUT failed"
exit 1
}
Write-Host "PUT succeeded"
# SFTP GET (download) - relative remote and local paths.
# Remote large_test.dat resolves to C:\Users\testuser\large_test.dat.
# Local large_test_copy.dat is relative to wolfsftp's CWD ($workDir).
Write-Host "SFTP GET 3GB file..."
$getCommands = "get large_test.dat large_test_copy.dat`nquit"
$getCommands | Out-File -FilePath (Join-Path $workDir "sftp_get_commands.txt") -Encoding ASCII
$proc2 = Start-Process -FilePath $sftpPath `
-ArgumentList "-u", "testuser", "-P", $env:TESTUSER_PASSWORD, "-h", "localhost", "-p", "${{env.TEST_PORT}}" `
-WorkingDirectory $workDir `
-RedirectStandardInput (Join-Path $workDir "sftp_get_commands.txt") `
-RedirectStandardOutput (Join-Path $workDir "sftp_get_out.txt") `
-RedirectStandardError (Join-Path $workDir "sftp_get_err.txt") `
-Wait -NoNewWindow -PassThru
$getOut = Get-Content (Join-Path $workDir "sftp_get_out.txt") -Raw -ErrorAction SilentlyContinue
Write-Host "=== SFTP GET output ==="; Write-Host $getOut
if ($proc2.ExitCode -ne 0 -or $getOut -match "Error getting file") {
Get-Content (Join-Path $workDir "sftp_get_err.txt") -ErrorAction SilentlyContinue
Write-Host "ERROR: SFTP GET failed"
exit 1
}
Write-Host "GET succeeded"
# Verify integrity (file is in $workDir from GET with relative path)
$expectedHash = (Get-Content (Join-Path $workDir "large_test.dat.sha256")).Trim()
$actualHash = (Get-FileHash -Path $getDestPath -Algorithm SHA256).Hash
Write-Host "File: $getDestPath"
Write-Host "Expected SHA256: $expectedHash"
Write-Host "Actual SHA256: $actualHash"
if ($expectedHash -ne $actualHash) {
Write-Host "ERROR: SHA256 mismatch - PUT/GET corruption"
exit 1
}
Write-Host "PASS: 3GB SFTP get/put succeeded"
- name: Cleanup
if: always()
working-directory: ${{ github.workspace }}\wolfssh
shell: pwsh
run: |
$serviceName = $env:SSHD_SERVICE_NAME
if (-not $serviceName) { $serviceName = "wolfsshd" }
$service = Get-Service -Name $serviceName -ErrorAction SilentlyContinue
if ($service) {
if ($service.Status -eq 'Running') { Stop-Service -Name $serviceName -Force }
sc.exe delete $serviceName | Out-Null
}