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 }