Public Access
FileZilla works, every copy of its settings fails: the <Pass> value in an exported sitemanager XML is Base64, not the password. Copied out verbatim it is rejected by the server every time, which is exactly the "works in the GUI, fails in a script" shape we have been chasing. The test can now read the export itself - listing sites by name so the right practice is picked out of the nine - and decodes the password, reporting both lengths so the difference is visible. A master-password protected export (encoding="crypt") cannot be read, and says so. A typed password that is still Base64 is flagged the same way, since pasting it by hand is the other route to the same mistake. Guarded against false positives: it must decode to printable ASCII, so ordinary passwords are not accused.
427 lines
18 KiB
PowerShell
427 lines
18 KiB
PowerShell
# 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
|
|
|
|
# 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
|
|
}
|
|
|
|
$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 { }
|
|
try {
|
|
Add-Type -Path $winscpDll
|
|
} catch {
|
|
Write-Host "Could not load WinSCPnet.dll - $($_.Exception.Message)" -ForegroundColor Red
|
|
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
|
|
exit 1
|
|
}
|
|
|
|
function Read-Number($question, $min, $max, $default) {
|
|
while ($true) {
|
|
Write-Host "$question " -NoNewline -ForegroundColor White
|
|
Write-Host "[$default]" -NoNewline -ForegroundColor DarkGray
|
|
Write-Host ": " -NoNewline -ForegroundColor White
|
|
$a = Read-Host
|
|
if ([string]::IsNullOrWhiteSpace($a)) { $a = [string]$default }
|
|
$n = 0
|
|
if ([int]::TryParse(([string]$a).Trim(), [ref]$n) -and $n -ge $min -and $n -le $max) { return $n }
|
|
Write-Host " (a number between $min and $max)" -ForegroundColor Yellow
|
|
}
|
|
}
|
|
|
|
# FileZilla keeps site passwords Base64-encoded, so the value sitting in the
|
|
# exported XML is not the password - it has to be decoded first. Copying it
|
|
# out verbatim fails against the server every time, which is exactly what a
|
|
# working FileZilla login plus a failing script looks like.
|
|
function Import-FileZillaSite {
|
|
Write-Host ""
|
|
Write-Host "In FileZilla: File > Export, tick 'Site Manager entries', save the .xml." -ForegroundColor Gray
|
|
$path = Read-Answer "Path to the exported .xml" $null
|
|
$path = $path.Trim('"').Trim()
|
|
if (-not (Test-Path $path)) {
|
|
Write-Host " Nothing at that path." -ForegroundColor Red
|
|
return $null
|
|
}
|
|
|
|
try {
|
|
$xml = [xml](Get-Content $path -Raw)
|
|
} catch {
|
|
Write-Host " That file is not readable XML - $($_.Exception.Message)" -ForegroundColor Red
|
|
return $null
|
|
}
|
|
|
|
$servers = @($xml.SelectNodes("//Server"))
|
|
if ($servers.Count -eq 0) {
|
|
Write-Host " No sites found in that file." -ForegroundColor Red
|
|
return $null
|
|
}
|
|
|
|
$node = $servers[0]
|
|
if ($servers.Count -gt 1) {
|
|
Write-Host ""
|
|
Write-Host "Sites in that file:" -ForegroundColor White
|
|
for ($i = 0; $i -lt $servers.Count; $i++) {
|
|
$n = $servers[$i]
|
|
Write-Host (" {0}. {1} {2}@{3}:{4}" -f ($i + 1), $n.Name, $n.User, $n.Host, $n.Port)
|
|
}
|
|
$node = $servers[(Read-Number "Which one" 1 $servers.Count 1) - 1]
|
|
}
|
|
|
|
$pass = $null
|
|
$passNode = $node.SelectSingleNode("Pass")
|
|
if ($passNode) {
|
|
$encoding = $passNode.GetAttribute("encoding")
|
|
if ($encoding -eq "base64") {
|
|
try {
|
|
$pass = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($passNode.InnerText))
|
|
Write-Host ""
|
|
Write-Host " Decoded the password from Base64." -ForegroundColor Green
|
|
Write-Host " The XML holds $($passNode.InnerText.Length) characters; the real" -ForegroundColor Gray
|
|
Write-Host " password is $($pass.Length). Using the decoded one." -ForegroundColor Gray
|
|
} catch {
|
|
Write-Host " The password in the XML is not valid Base64." -ForegroundColor Yellow
|
|
}
|
|
} elseif ($encoding -eq "crypt") {
|
|
Write-Host ""
|
|
Write-Host " This export is protected by a FileZilla master password," -ForegroundColor Yellow
|
|
Write-Host " so the password cannot be read out of it. Type it instead." -ForegroundColor Yellow
|
|
} else {
|
|
$pass = $passNode.InnerText
|
|
}
|
|
}
|
|
|
|
# FileZilla's protocol numbering: 1 is SFTP, 0 is plain FTP, and the
|
|
# TLS variants use higher values. Only used as the menu default.
|
|
$proto = 1
|
|
switch ([string]$node.Protocol) {
|
|
"0" { $proto = 3 }
|
|
"1" { $proto = 1 }
|
|
"3" { $proto = 2 }
|
|
"4" { $proto = 2 }
|
|
}
|
|
|
|
return @{
|
|
Host = [string]$node.Host
|
|
Port = [string]$node.Port
|
|
User = [string]$node.User
|
|
Password = $pass
|
|
Proto = $proto
|
|
}
|
|
}
|
|
|
|
$fromXml = $null
|
|
Write-Host ""
|
|
Write-Host "Where are the details coming from?" -ForegroundColor White
|
|
Write-Host " 1. I will type them in"
|
|
Write-Host " 2. Read them from a FileZilla site export (.xml)"
|
|
if ((Read-Number "Choose 1-2" 1 2 1) -eq 2) {
|
|
$fromXml = Import-FileZillaSite
|
|
if (-not $fromXml) {
|
|
Write-Host ""
|
|
Write-Host "Falling back to typing them in." -ForegroundColor Yellow
|
|
}
|
|
}
|
|
|
|
Write-Host ""
|
|
$serverHost = Read-Answer "Server" $(if ($fromXml) { $fromXml.Host } else { $null })
|
|
$serverHost = ($serverHost -replace '^\w+://', '').Split('/')[0].Trim()
|
|
|
|
Write-Host ""
|
|
Write-Host "Protocol:" -ForegroundColor White
|
|
Write-Host " 1. SFTP"
|
|
Write-Host " 2. FTPS"
|
|
Write-Host " 3. FTP"
|
|
$protoDefault = if ($fromXml) { $fromXml.Proto } else { 1 }
|
|
$protoChoice = Read-Number "Choose 1-3" 1 3 $protoDefault
|
|
$protocol = @("sftp", "ftps", "ftp")[$protoChoice - 1]
|
|
|
|
$defaultPort = if ($fromXml -and $fromXml.Port) { $fromXml.Port }
|
|
elseif ($protocol -eq "sftp") { 22 } else { 21 }
|
|
$port = Read-Number "Port" 1 65535 $defaultPort
|
|
$username = Read-Answer "Username" $(if ($fromXml) { $fromXml.User } else { $null })
|
|
|
|
if ($fromXml -and $fromXml.Password) {
|
|
$password = $fromXml.Password
|
|
} else {
|
|
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)
|
|
|
|
# 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
|
|
}
|
|
}
|
|
|
|
# -- 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
|
|
}
|
|
# A password copied straight out of a FileZilla export is still Base64.
|
|
if (-not $fromXml -and $password.Length -ge 8 -and $password -cmatch '^[A-Za-z0-9+/]+={0,2}$' -and ($password.Length % 4) -eq 0) {
|
|
$decoded = $null
|
|
try { $decoded = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($password)) } catch { }
|
|
if ($decoded -and $decoded -match '^[\x20-\x7E]+$') {
|
|
Write-Host " !! This looks like Base64, not a password." -ForegroundColor Yellow
|
|
Write-Host " FileZilla stores passwords Base64-encoded in its exported" -ForegroundColor Yellow
|
|
Write-Host " XML. Decoded, this is $($decoded.Length) characters - that is" -ForegroundColor Yellow
|
|
Write-Host " probably the real password. Re-run and choose option 2 to" -ForegroundColor Yellow
|
|
Write-Host " read the export directly, or paste the decoded value." -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)
|
|
|
|
$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
|
|
}
|
|
|
|
$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
|
|
}
|
|
|
|
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 ""
|