2026-08-30 20:28:32 -05:00
|
|
|
# Standalone SFTP/FTP login test. Nothing is read from a config file - you type
|
|
|
|
|
# the details in - so it isolates the account from whatever a script is doing
|
|
|
|
|
# with it. Called by test-login.bat.
|
|
|
|
|
|
|
|
|
|
$scriptRoot = Split-Path -Parent $PSCommandPath
|
|
|
|
|
|
|
|
|
|
function Read-Answer($question, $default) {
|
|
|
|
|
while ($true) {
|
|
|
|
|
if ($default) {
|
|
|
|
|
Write-Host "$question " -NoNewline -ForegroundColor White
|
|
|
|
|
Write-Host "[$default]" -NoNewline -ForegroundColor DarkGray
|
|
|
|
|
Write-Host ": " -NoNewline -ForegroundColor White
|
|
|
|
|
} else {
|
|
|
|
|
Write-Host "${question}: " -NoNewline -ForegroundColor White
|
|
|
|
|
}
|
|
|
|
|
$answer = Read-Host
|
|
|
|
|
if ([string]::IsNullOrWhiteSpace($answer)) {
|
|
|
|
|
if ($default) { return $default }
|
|
|
|
|
Write-Host " (required)" -ForegroundColor Yellow
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
return $answer
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Write-Host ""
|
|
|
|
|
Write-Host ("=" * 64) -ForegroundColor Cyan
|
|
|
|
|
Write-Host " Login test" -ForegroundColor Cyan
|
|
|
|
|
Write-Host ("=" * 64) -ForegroundColor Cyan
|
|
|
|
|
Write-Host ""
|
|
|
|
|
Write-Host "Type the details exactly as they work in WinSCP or FileZilla." -ForegroundColor Gray
|
|
|
|
|
|
2026-08-30 20:33:03 -05:00
|
|
|
# Find WinSCPnet.dll wherever it happens to live - this script is meant to be
|
|
|
|
|
# dropped next to whatever uploader is in use, not just this one.
|
|
|
|
|
function Find-WinSCPDll {
|
|
|
|
|
$parent = Split-Path -Parent $scriptRoot
|
|
|
|
|
$candidates = @(
|
|
|
|
|
(Join-Path $scriptRoot "WinSCP\WinSCPnet.dll"),
|
|
|
|
|
(Join-Path $scriptRoot "WinSCPnet.dll"),
|
|
|
|
|
(Join-Path $parent "WinSCP\WinSCPnet.dll"),
|
|
|
|
|
(Join-Path $parent "WinSCPnet.dll"),
|
|
|
|
|
"C:\Program Files (x86)\WinSCP\WinSCPnet.dll",
|
|
|
|
|
"C:\Program Files\WinSCP\WinSCPnet.dll"
|
|
|
|
|
)
|
|
|
|
|
foreach ($c in $candidates) {
|
|
|
|
|
if (Test-Path $c) { return $c }
|
|
|
|
|
}
|
|
|
|
|
# Anywhere under the folder this script sits in.
|
|
|
|
|
$found = Get-ChildItem -Path $scriptRoot -Filter "WinSCPnet.dll" -Recurse -File -ErrorAction SilentlyContinue |
|
|
|
|
|
Select-Object -First 1
|
|
|
|
|
if ($found) { return $found.FullName }
|
|
|
|
|
return $null
|
2026-08-30 20:28:32 -05:00
|
|
|
}
|
2026-08-30 20:33:03 -05:00
|
|
|
|
|
|
|
|
$winscpDll = Find-WinSCPDll
|
|
|
|
|
if (-not $winscpDll) {
|
|
|
|
|
Write-Host ""
|
|
|
|
|
Write-Host "Could not find WinSCPnet.dll automatically." -ForegroundColor Yellow
|
|
|
|
|
Write-Host "It sits either in a WinSCP folder next to your upload script, or" -ForegroundColor Gray
|
|
|
|
|
Write-Host "in C:\Program Files (x86)\WinSCP\ if WinSCP is installed normally." -ForegroundColor Gray
|
|
|
|
|
Write-Host ""
|
|
|
|
|
$winscpDll = Read-Answer "Full path to WinSCPnet.dll" $null
|
|
|
|
|
if (-not (Test-Path $winscpDll)) {
|
|
|
|
|
Write-Host "Nothing at that path." -ForegroundColor Red
|
|
|
|
|
exit 1
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Write-Host ""
|
|
|
|
|
Write-Host "Using $winscpDll" -ForegroundColor DarkGray
|
|
|
|
|
|
|
|
|
|
# Windows tags files copied from elsewhere as untrusted, which stops Add-Type
|
|
|
|
|
# loading them. Harmless everywhere else, so failure here is not worth a word.
|
|
|
|
|
try {
|
|
|
|
|
Get-ChildItem -Path (Split-Path -Parent $winscpDll) -Recurse -File -ErrorAction SilentlyContinue |
|
|
|
|
|
Unblock-File -ErrorAction SilentlyContinue
|
|
|
|
|
} catch { }
|
2026-08-30 20:28:32 -05:00
|
|
|
try {
|
|
|
|
|
Add-Type -Path $winscpDll
|
|
|
|
|
} catch {
|
|
|
|
|
Write-Host "Could not load WinSCPnet.dll - $($_.Exception.Message)" -ForegroundColor Red
|
2026-08-30 20:33:03 -05:00
|
|
|
Write-Host "If it says the operation is not supported, Windows has the file" -ForegroundColor Yellow
|
|
|
|
|
Write-Host "blocked: right-click it, Properties, tick Unblock, OK." -ForegroundColor Yellow
|
2026-08-30 20:28:32 -05:00
|
|
|
exit 1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Write-Host ""
|
|
|
|
|
$serverHost = Read-Answer "Server" $null
|
|
|
|
|
$serverHost = ($serverHost -replace '^\w+://', '').Split('/')[0].Trim()
|
|
|
|
|
|
|
|
|
|
$protoChoice = 0
|
|
|
|
|
Write-Host ""
|
|
|
|
|
Write-Host "Protocol:" -ForegroundColor White
|
|
|
|
|
Write-Host " 1. SFTP"
|
|
|
|
|
Write-Host " 2. FTPS"
|
|
|
|
|
Write-Host " 3. FTP"
|
|
|
|
|
while ($protoChoice -lt 1 -or $protoChoice -gt 3) {
|
|
|
|
|
Write-Host "Choose 1-3 " -NoNewline -ForegroundColor White
|
|
|
|
|
Write-Host "[1]" -NoNewline -ForegroundColor DarkGray
|
|
|
|
|
Write-Host ": " -NoNewline -ForegroundColor White
|
|
|
|
|
$a = Read-Host
|
|
|
|
|
if ([string]::IsNullOrWhiteSpace($a)) { $a = "1" }
|
|
|
|
|
[int]::TryParse($a.Trim(), [ref]$protoChoice) | Out-Null
|
|
|
|
|
}
|
|
|
|
|
$protocol = @("sftp", "ftps", "ftp")[$protoChoice - 1]
|
|
|
|
|
$defaultPort = if ($protocol -eq "sftp") { 22 } else { 21 }
|
|
|
|
|
|
|
|
|
|
$port = [int](Read-Answer "Port" $defaultPort)
|
|
|
|
|
$username = Read-Answer "Username" $null
|
|
|
|
|
Write-Host ""
|
|
|
|
|
Write-Host "Paste the password. It is not echoed and not saved anywhere." -ForegroundColor DarkGray
|
|
|
|
|
Write-Host "Password: " -NoNewline -ForegroundColor White
|
|
|
|
|
$secure = Read-Host -AsSecureString
|
|
|
|
|
$bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure)
|
|
|
|
|
$password = [Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr)
|
|
|
|
|
[Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)
|
|
|
|
|
|
2026-08-30 20:50:26 -05:00
|
|
|
# Nothing echoes while typing, so a slipped paste looks identical to a good
|
|
|
|
|
# one. Typing it twice is the only way to be sure of what actually went in.
|
|
|
|
|
Write-Host "Type it once more to check: " -NoNewline -ForegroundColor White
|
|
|
|
|
$secure2 = Read-Host -AsSecureString
|
|
|
|
|
$bstr2 = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure2)
|
|
|
|
|
$password2 = [Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr2)
|
|
|
|
|
[Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr2)
|
|
|
|
|
|
|
|
|
|
if ($password -ne $password2) {
|
|
|
|
|
Write-Host ""
|
|
|
|
|
Write-Host " !! The two did not match." -ForegroundColor Red
|
|
|
|
|
Write-Host " First entry: $($password.Length) characters. Second: $($password2.Length)." -ForegroundColor Red
|
|
|
|
|
Write-Host " One of them is wrong, so this test would prove nothing." -ForegroundColor Red
|
|
|
|
|
Write-Host " Start over and paste carefully, or type it by hand." -ForegroundColor Yellow
|
|
|
|
|
Write-Host ""
|
|
|
|
|
exit 1
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-30 20:28:32 -05:00
|
|
|
# -- What the credentials actually contain ------------------------------------
|
|
|
|
|
Write-Host ""
|
|
|
|
|
Write-Host ("-" * 64)
|
|
|
|
|
Write-Host "What was entered" -ForegroundColor Cyan
|
|
|
|
|
Write-Host ("-" * 64)
|
|
|
|
|
Write-Host " Server : [$serverHost] port $port over $($protocol.ToUpper())"
|
|
|
|
|
Write-Host " Username : [$username] ($($username.Length) characters)"
|
|
|
|
|
Write-Host " Password : $($password.Length) characters"
|
|
|
|
|
|
|
|
|
|
$warned = $false
|
|
|
|
|
if ($username -ne $username.Trim()) {
|
|
|
|
|
Write-Host " !! The username has a leading or trailing space." -ForegroundColor Yellow
|
|
|
|
|
$warned = $true
|
|
|
|
|
}
|
|
|
|
|
if ($password -ne $password.Trim()) {
|
|
|
|
|
Write-Host " !! The password has a leading or trailing space." -ForegroundColor Yellow
|
|
|
|
|
$warned = $true
|
|
|
|
|
}
|
|
|
|
|
if ($password -match '[\$`]') {
|
|
|
|
|
Write-Host " !! The password contains a `$ or a backtick." -ForegroundColor Yellow
|
|
|
|
|
Write-Host " In a PowerShell script these MUST be in single quotes:" -ForegroundColor Yellow
|
|
|
|
|
Write-Host " `$Password = 'the`$password' <- correct" -ForegroundColor Yellow
|
|
|
|
|
Write-Host " `$Password = `"the`$password`" <- WRONG, PowerShell eats `$password" -ForegroundColor Yellow
|
|
|
|
|
Write-Host " This is the most common reason a login works in the WinSCP" -ForegroundColor Yellow
|
|
|
|
|
Write-Host " window but fails from a script with the same credentials." -ForegroundColor Yellow
|
|
|
|
|
$warned = $true
|
|
|
|
|
}
|
|
|
|
|
if ($password -match '[^\x20-\x7E]') {
|
|
|
|
|
Write-Host " !! The password contains a non-standard character." -ForegroundColor Yellow
|
|
|
|
|
$warned = $true
|
|
|
|
|
}
|
|
|
|
|
if (-not $warned) {
|
|
|
|
|
Write-Host " Nothing unusual about either string." -ForegroundColor Green
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# -- Try it -------------------------------------------------------------------
|
|
|
|
|
$logFile = Join-Path $scriptRoot "login-test.log"
|
|
|
|
|
if (Test-Path $logFile) { Remove-Item $logFile -Force }
|
|
|
|
|
|
|
|
|
|
$opts = New-Object WinSCP.SessionOptions
|
|
|
|
|
$opts.HostName = $serverHost
|
|
|
|
|
$opts.PortNumber = $port
|
|
|
|
|
$opts.UserName = $username
|
|
|
|
|
$opts.Password = $password
|
|
|
|
|
if ($protocol -eq "sftp") {
|
|
|
|
|
$opts.Protocol = [WinSCP.Protocol]::Sftp
|
|
|
|
|
$opts.GiveUpSecurityAndAcceptAnySshHostKey = $true
|
|
|
|
|
} else {
|
|
|
|
|
$opts.Protocol = [WinSCP.Protocol]::Ftp
|
|
|
|
|
if ($protocol -eq "ftps") {
|
|
|
|
|
$opts.FtpSecure = [WinSCP.FtpSecure]::Explicit
|
|
|
|
|
$opts.GiveUpSecurityAndAcceptAnyTlsHostCertificate = $true
|
|
|
|
|
} else {
|
|
|
|
|
$opts.FtpSecure = [WinSCP.FtpSecure]::None
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Write-Host ""
|
|
|
|
|
Write-Host ("-" * 64)
|
|
|
|
|
Write-Host "Connecting" -ForegroundColor Cyan
|
|
|
|
|
Write-Host ("-" * 64)
|
|
|
|
|
|
|
|
|
|
$session = New-Object WinSCP.Session
|
|
|
|
|
$session.SessionLogPath = $logFile
|
|
|
|
|
$connected = $false
|
|
|
|
|
try {
|
|
|
|
|
$session.Open($opts)
|
|
|
|
|
$connected = $true
|
|
|
|
|
Write-Host " CONNECTED" -ForegroundColor Green
|
|
|
|
|
} catch {
|
|
|
|
|
Write-Host " FAILED" -ForegroundColor Red
|
|
|
|
|
Write-Host ""
|
|
|
|
|
Write-Host " $($_.Exception.Message)" -ForegroundColor Red
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($connected) {
|
|
|
|
|
try {
|
|
|
|
|
Write-Host ""
|
|
|
|
|
Write-Host " Home directory: $($session.HomePath)" -ForegroundColor Gray
|
|
|
|
|
Write-Host " Folders visible at the top level:" -ForegroundColor Gray
|
|
|
|
|
$listing = $session.ListDirectory($session.HomePath)
|
|
|
|
|
$shown = 0
|
|
|
|
|
foreach ($item in $listing.Files) {
|
|
|
|
|
if ($item.Name -eq "." -or $item.Name -eq "..") { continue }
|
|
|
|
|
$kind = if ($item.IsDirectory) { "[dir] " } else { " " }
|
|
|
|
|
Write-Host " $kind$($item.Name)"
|
|
|
|
|
$shown++
|
|
|
|
|
if ($shown -ge 25) { Write-Host " ... (more)"; break }
|
|
|
|
|
}
|
|
|
|
|
if ($shown -eq 0) { Write-Host " (nothing - the account may be locked to an empty folder)" }
|
|
|
|
|
} catch {
|
|
|
|
|
Write-Host " Connected, but could not list the folder: $($_.Exception.Message)" -ForegroundColor Yellow
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Write-Host ""
|
|
|
|
|
$checkPath = Read-Answer "Remote folder to check (or . to skip)" "."
|
|
|
|
|
if ($checkPath -ne ".") {
|
|
|
|
|
try {
|
|
|
|
|
if ($session.FileExists($checkPath)) {
|
|
|
|
|
Write-Host " $checkPath ... found" -ForegroundColor Green
|
|
|
|
|
} else {
|
|
|
|
|
Write-Host " $checkPath ... NOT THERE" -ForegroundColor Red
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
Write-Host " Could not check: $($_.Exception.Message)" -ForegroundColor Yellow
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
$session.Dispose()
|
|
|
|
|
|
|
|
|
|
# -- The authentication conversation ------------------------------------------
|
|
|
|
|
if (-not $connected -and (Test-Path $logFile)) {
|
|
|
|
|
Write-Host ""
|
|
|
|
|
Write-Host ("-" * 64)
|
|
|
|
|
Write-Host "What the server actually said" -ForegroundColor Cyan
|
|
|
|
|
Write-Host ("-" * 64)
|
2026-08-30 20:50:26 -05:00
|
|
|
|
|
|
|
|
$log = Get-Content $logFile -ErrorAction SilentlyContinue
|
|
|
|
|
|
|
|
|
|
$server = $log | Select-String -Pattern "Remote version:" | Select-Object -First 1
|
|
|
|
|
if ($server) { Write-Host " $(($server.Line -split 'Remote version:')[1].Trim())" -ForegroundColor Gray }
|
|
|
|
|
|
|
|
|
|
$methodLine = $log | Select-String -Pattern "Server offered these authentication methods:" | Select-Object -First 1
|
|
|
|
|
$methods = ""
|
|
|
|
|
if ($methodLine) {
|
|
|
|
|
$methods = (($methodLine.Line -split "authentication methods:")[1]).Trim()
|
|
|
|
|
Write-Host " Methods the server accepts: $methods" -ForegroundColor Gray
|
2026-08-30 20:28:32 -05:00
|
|
|
}
|
2026-08-30 20:50:26 -05:00
|
|
|
|
|
|
|
|
$sentPassword = ($log | Select-String -Pattern "Sent password").Count -gt 0
|
|
|
|
|
$pwRejected = ($log | Select-String -Pattern "Password authentication failed").Count -gt 0
|
|
|
|
|
|
|
|
|
|
Write-Host ""
|
|
|
|
|
if ($pwRejected -or ($sentPassword -and -not $connected)) {
|
|
|
|
|
Write-Host " The password reached the server and the server rejected it." -ForegroundColor Yellow
|
|
|
|
|
Write-Host ""
|
|
|
|
|
Write-Host " That rules out the script, the protocol, the port and the host" -ForegroundColor Yellow
|
|
|
|
|
Write-Host " key - all of those worked. Only three things are left:" -ForegroundColor Yellow
|
|
|
|
|
Write-Host ""
|
|
|
|
|
Write-Host " 1. The password is not the one this account expects." -ForegroundColor White
|
|
|
|
|
Write-Host " Check it is this practice's own password, not another's." -ForegroundColor Gray
|
|
|
|
|
Write-Host " 2. The account is locked out from repeated failed tries," -ForegroundColor White
|
|
|
|
|
Write-Host " or expired. Log in with the WinSCP window right now - if" -ForegroundColor Gray
|
|
|
|
|
Write-Host " that fails too, it is the account, not anything here." -ForegroundColor Gray
|
|
|
|
|
Write-Host " 3. The account is set to require a key rather than a" -ForegroundColor White
|
|
|
|
|
Write-Host " password, even though the server allows passwords" -ForegroundColor Gray
|
|
|
|
|
Write-Host " generally. Only the partner can tell you that." -ForegroundColor Gray
|
|
|
|
|
} elseif ($methods -and $methods -notmatch "password") {
|
|
|
|
|
Write-Host " The server never offered password authentication." -ForegroundColor Yellow
|
|
|
|
|
Write-Host " It accepts: $methods" -ForegroundColor Yellow
|
|
|
|
|
Write-Host " A password will never work here - the partner has to enable" -ForegroundColor Yellow
|
|
|
|
|
Write-Host " password auth or issue an SSH key for this account." -ForegroundColor Yellow
|
|
|
|
|
} else {
|
|
|
|
|
Write-Host " It failed before any password was sent - so this is about" -ForegroundColor Yellow
|
|
|
|
|
Write-Host " reaching the server, not about the credentials." -ForegroundColor Yellow
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-30 20:28:32 -05:00
|
|
|
Write-Host ""
|
|
|
|
|
Write-Host " Full log: $logFile" -ForegroundColor Cyan
|
|
|
|
|
Write-Host " It contains no password. Send it to Brad." -ForegroundColor Cyan
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Write-Host ""
|
|
|
|
|
Write-Host ("=" * 64) -ForegroundColor Cyan
|
|
|
|
|
if ($connected) {
|
|
|
|
|
Write-Host " Login works. The account is fine - so if a script using these" -ForegroundColor Green
|
|
|
|
|
Write-Host " same details still fails, the script is mangling one of them." -ForegroundColor Green
|
|
|
|
|
} else {
|
|
|
|
|
Write-Host " Login failed. Compare against the WinSCP window: File > Session" -ForegroundColor Yellow
|
|
|
|
|
Write-Host " > Generate Session URL/Code > .NET assembly code. That shows the" -ForegroundColor Yellow
|
|
|
|
|
Write-Host " exact settings the working connection uses." -ForegroundColor Yellow
|
|
|
|
|
}
|
|
|
|
|
Write-Host ("=" * 64) -ForegroundColor Cyan
|
|
|
|
|
Write-Host ""
|