1
0
mirror of https://github.com/winscp/winscp.git synced 2026-10-24 02:28:55 +03:00
Files
winscp/source/core/SecureShell.cpp
Martin Prikryl 0a567870cf Thread safe replacement of handles_by_evtomain global
Before it was used for local proxy command only, so probability of problems was relatively small. In PuTTY 0.75 it's used even for Pageant communication.

(cherry picked from commit 675cfb552b)

Source commit: e95234c0c1ab3db66d29fa4b321596cac02539e7
2021-11-23 21:03:38 +01:00

2802 lines
88 KiB
C++

//---------------------------------------------------------------------------
#include <vcl.h>
#pragma hdrstop
#include "Common.h"
#include "PuttyIntf.h"
#include "Exceptions.h"
#include "Interface.h"
#include "SecureShell.h"
#include "TextsCore.h"
#include "HelpCore.h"
#include "CoreMain.h"
#include <StrUtils.hpp>
#ifndef AUTO_WINSOCK
#include <winsock2.h>
#endif
#include <ws2ipdef.h>
//---------------------------------------------------------------------------
#pragma package(smart_init)
//---------------------------------------------------------------------------
#define MAX_BUFSIZE 32768
//---------------------------------------------------------------------------
const wchar_t HostKeyDelimiter = L';';
static std::unique_ptr<TCriticalSection> PuttyStorageSection(TraceInitPtr(new TCriticalSection()));
//---------------------------------------------------------------------------
struct TPuttyTranslation
{
const wchar_t * Original;
int Translation;
UnicodeString HelpKeyword;
};
//---------------------------------------------------------------------------
struct ScpLogPolicy : public LogPolicy
{
TSecureShell * SecureShell;
struct Seat * Seat;
};
//---------------------------------------------------------------------------
Seat * get_log_seat(LogContext * logctx)
{
ScpLogPolicy * ALogPolicy = static_cast<ScpLogPolicy *>(log_get_logpolicy(logctx));
return ALogPolicy->Seat;
}
//---------------------------------------------------------------------------
struct callback_set * get_log_callback_set(LogContext * logctx)
{
ScpLogPolicy * ALogPolicy = static_cast<ScpLogPolicy *>(log_get_logpolicy(logctx));
return ALogPolicy->SecureShell->GetCallbackSet();
}
//---------------------------------------------------------------------------
__fastcall TSecureShell::TSecureShell(TSessionUI* UI,
TSessionData * SessionData, TSessionLog * Log, TConfiguration * Configuration)
{
FUI = UI;
FSessionData = SessionData;
FLog = Log;
FConfiguration = Configuration;
FActive = false;
FWaiting = 0;
FOpened = false;
FSshImplementation = sshiUnknown;
OutPtr = NULL;
Pending = NULL;
FBackendHandle = NULL;
FLogPolicy = NULL;
FSeat = NULL;
FLogCtx = NULL;
ResetConnection();
FOnCaptureOutput = NULL;
FOnReceive = NULL;
FSocket = INVALID_SOCKET;
FSocketEvent = CreateEvent(NULL, false, false, NULL);
FFrozen = false;
FSimple = false;
FCollectPrivateKeyUsage = false;
FWaitingForData = 0;
FCallbackSet.reset(new callback_set());
memset(FCallbackSet.get(), 0, sizeof(callback_set));
FCallbackSet->handles_by_evtomain = new_handles_by_evtomain();
}
//---------------------------------------------------------------------------
__fastcall TSecureShell::~TSecureShell()
{
freetree234(FCallbackSet->handles_by_evtomain);
FCallbackSet->handles_by_evtomain = NULL;
DebugAssert(FWaiting == 0);
Active = false;
ResetConnection();
CloseHandle(FSocketEvent);
}
//---------------------------------------------------------------------------
void __fastcall TSecureShell::ResetConnection()
{
FreeBackend();
ClearStdError();
PendLen = 0;
PendSize = 0;
sfree(Pending);
Pending = NULL;
FCWriteTemp = L"";
ResetSessionInfo();
FAuthenticating = false;
FAuthenticated = false;
FStoredPasswordTried = false;
FStoredPasswordTriedForKI = false;
FStoredPassphraseTried = false;
delete FLogPolicy;
FLogPolicy = NULL;
delete FSeat;
FSeat = NULL;
if (FLogCtx != NULL)
{
log_free(FLogCtx);
}
FLogCtx = NULL;
FClosed = false;
}
//---------------------------------------------------------------------------
void __fastcall TSecureShell::ResetSessionInfo()
{
FSessionInfoValid = false;
}
//---------------------------------------------------------------------------
inline void __fastcall TSecureShell::UpdateSessionInfo()
{
if (!FSessionInfoValid)
{
DebugAssert(get_ssh_version(FBackendHandle) == 2);
FSessionInfo.ProtocolBaseName = L"SSH";
FSessionInfo.ProtocolName =
FORMAT(L"%s-%d", (FSessionInfo.ProtocolBaseName, get_ssh_version(FBackendHandle)));
FSessionInfo.SecurityProtocolName = FSessionInfo.ProtocolName;
FSessionInfo.CSCipher = GetCipherName(get_cscipher(FBackendHandle));
FSessionInfo.SCCipher = GetCipherName(get_sccipher(FBackendHandle));
FSessionInfo.CSCompression = GetCompressorName(get_cscomp(FBackendHandle));
FSessionInfo.SCCompression = GetDecompressorName(get_sccomp(FBackendHandle));
FSessionInfoValid = true;
}
}
//---------------------------------------------------------------------------
const TSessionInfo & __fastcall TSecureShell::GetSessionInfo()
{
if (!FSessionInfoValid)
{
UpdateSessionInfo();
}
return FSessionInfo;
}
//---------------------------------------------------------------------
void __fastcall TSecureShell::GetHostKeyFingerprint(UnicodeString & SHA256, UnicodeString & MD5)
{
SHA256 = FSessionInfo.HostKeyFingerprintSHA256;
MD5 = FSessionInfo.HostKeyFingerprintMD5;
}
//---------------------------------------------------------------------
Conf * __fastcall TSecureShell::StoreToConfig(TSessionData * Data, bool Simple)
{
Conf * conf = conf_new();
PuttyDefaults(conf);
// user-configurable settings
conf_set_str(conf, CONF_host, AnsiString(Data->HostNameExpanded).c_str());
conf_set_str(conf, CONF_username, UTF8String(Data->UserNameExpanded).c_str());
conf_set_int(conf, CONF_port, Data->PortNumber);
conf_set_int(conf, CONF_protocol, PROT_SSH);
conf_set_bool(conf, CONF_change_password, Data->ChangePassword);
// always set 0, as we will handle keepalives ourselves to avoid
// multi-threaded issues in putty timer list
conf_set_int(conf, CONF_ping_interval, 0);
conf_set_bool(conf, CONF_compression, Data->Compression);
conf_set_bool(conf, CONF_tryagent, Data->TryAgent);
conf_set_bool(conf, CONF_agentfwd, Data->AgentFwd);
conf_set_int(conf, CONF_addressfamily, Data->AddressFamily);
conf_set_str(conf, CONF_ssh_rekey_data, AnsiString(Data->RekeyData).c_str());
conf_set_int(conf, CONF_ssh_rekey_time, Data->RekeyTime);
DebugAssert(CIPHER_MAX == CIPHER_COUNT);
for (int c = 0; c < CIPHER_COUNT; c++)
{
int pcipher;
switch (Data->Cipher[c]) {
case cipWarn: pcipher = CIPHER_WARN; break;
case cip3DES: pcipher = CIPHER_3DES; break;
case cipBlowfish: pcipher = CIPHER_BLOWFISH; break;
case cipAES: pcipher = CIPHER_AES; break;
case cipDES: pcipher = CIPHER_DES; break;
case cipArcfour: pcipher = CIPHER_ARCFOUR; break;
case cipChaCha20: pcipher = CIPHER_CHACHA20; break;
default: DebugFail();
}
conf_set_int_int(conf, CONF_ssh_cipherlist, c, pcipher);
}
DebugAssert(KEX_MAX == KEX_COUNT);
for (int k = 0; k < KEX_COUNT; k++)
{
int pkex;
switch (Data->Kex[k]) {
case kexWarn: pkex = KEX_WARN; break;
case kexDHGroup1: pkex = KEX_DHGROUP1; break;
case kexDHGroup14: pkex = KEX_DHGROUP14; break;
case kexDHGEx: pkex = KEX_DHGEX; break;
case kexRSA: pkex = KEX_RSA; break;
case kexECDH: pkex = KEX_ECDH; break;
default: DebugFail();
}
conf_set_int_int(conf, CONF_ssh_kexlist, k, pkex);
}
DebugAssert(HK_MAX == HOSTKEY_COUNT);
for (int h = 0; h < HOSTKEY_COUNT; h++)
{
int phk;
switch (Data->HostKeys[h]) {
case hkWarn: phk = HK_WARN; break;
case hkRSA: phk = HK_RSA; break;
case hkDSA: phk = hkDSA; break;
case hkECDSA: phk = HK_ECDSA; break;
case hkED25519: phk = HK_ED25519; break;
case hkED448: phk = HK_ED448; break;
default: DebugFail();
}
conf_set_int_int(conf, CONF_ssh_hklist, h, phk);
}
DebugAssert(ngsslibs == GSSLIB_COUNT);
for (int g = 0; g < GSSLIB_COUNT; g++)
{
int pgsslib;
switch (Data->GssLib[g]) {
case gssGssApi32: pgsslib = 0; break;
case gssSspi: pgsslib = 1; break;
case gssCustom: pgsslib = 2; break;
default: DebugFail();
}
conf_set_int_int(conf, CONF_ssh_gsslist, g, pgsslib);
}
Filename * GssLibCustomFileName = filename_from_str(UTF8String(Data->GssLibCustom).c_str());
conf_set_filename(conf, CONF_ssh_gss_custom, GssLibCustomFileName);
filename_free(GssLibCustomFileName);
UnicodeString SPublicKeyFile = Data->PublicKeyFile;
if (SPublicKeyFile.IsEmpty()) SPublicKeyFile = Configuration->DefaultKeyFile;
// StripPathQuotes should not be needed as we do not feed quotes anymore
SPublicKeyFile = StripPathQuotes(ExpandEnvironmentVariables(SPublicKeyFile));
Filename * KeyFileFileName = filename_from_str(UTF8String(SPublicKeyFile).c_str());
conf_set_filename(conf, CONF_keyfile, KeyFileFileName);
filename_free(KeyFileFileName);
conf_set_bool(conf, CONF_ssh2_des_cbc, Data->Ssh2DES);
conf_set_bool(conf, CONF_ssh_no_userauth, Data->SshNoUserAuth);
conf_set_bool(conf, CONF_try_ki_auth, Data->AuthKI);
conf_set_bool(conf, CONF_try_gssapi_auth, Data->AuthGSSAPI);
conf_set_bool(conf, CONF_try_gssapi_kex, Data->AuthGSSAPIKEX);
conf_set_bool(conf, CONF_gssapifwd, Data->GSSAPIFwdTGT);
conf_set_bool(conf, CONF_change_username, Data->ChangeUsername);
conf_set_int(conf, CONF_proxy_type, Data->ProxyMethod);
conf_set_str(conf, CONF_proxy_host, AnsiString(Data->ProxyHost).c_str());
conf_set_int(conf, CONF_proxy_port, Data->ProxyPort);
conf_set_str(conf, CONF_proxy_username, UTF8String(Data->ProxyUsername).c_str());
conf_set_str(conf, CONF_proxy_password, UTF8String(Data->ProxyPassword).c_str());
if (Data->ProxyMethod == pmCmd)
{
conf_set_str(conf, CONF_proxy_telnet_command, AnsiString(Data->ProxyLocalCommand).c_str());
}
else
{
conf_set_str(conf, CONF_proxy_telnet_command, AnsiString(Data->ProxyTelnetCommand).c_str());
}
conf_set_int(conf, CONF_proxy_dns, Data->ProxyDNS);
conf_set_bool(conf, CONF_even_proxy_localhost, Data->ProxyLocalhost);
conf_set_int(conf, CONF_sshbug_hmac2, Data->Bug[sbHMAC2]);
conf_set_int(conf, CONF_sshbug_derivekey2, Data->Bug[sbDeriveKey2]);
conf_set_int(conf, CONF_sshbug_rsapad2, Data->Bug[sbRSAPad2]);
conf_set_int(conf, CONF_sshbug_rekey2, Data->Bug[sbRekey2]);
conf_set_int(conf, CONF_sshbug_pksessid2, Data->Bug[sbPKSessID2]);
conf_set_int(conf, CONF_sshbug_maxpkt2, Data->Bug[sbMaxPkt2]);
conf_set_int(conf, CONF_sshbug_ignore2, Data->Bug[sbIgnore2]);
conf_set_int(conf, CONF_sshbug_winadj, Data->Bug[sbWinAdj]);
conf_set_int(conf, CONF_sshbug_oldgex2, Data->Bug[sbOldGex2]);
conf_set_int(conf, CONF_sshbug_chanreq, Data->Bug[sbChanReq]);
if (!Data->TunnelPortFwd.IsEmpty())
{
DebugAssert(!Simple);
UnicodeString TunnelPortFwd = Data->TunnelPortFwd;
while (!TunnelPortFwd.IsEmpty())
{
UnicodeString Buf = CutToChar(TunnelPortFwd, L',', true);
AnsiString Key = AnsiString(CutToChar(Buf, L'\t', true));
AnsiString Value = AnsiString(Buf);
conf_set_str_str(conf, CONF_portfwd, Key.c_str(), Value.c_str());
}
// when setting up a tunnel, do not open shell/sftp
conf_set_bool(conf, CONF_ssh_no_shell, TRUE);
}
else
{
DebugAssert(Simple);
conf_set_bool(conf, CONF_ssh_simple, Data->SshSimple && Simple);
if (Data->FSProtocol == fsSCPonly)
{
conf_set_bool(conf, CONF_ssh_subsys, FALSE);
if (Data->Shell.IsEmpty())
{
// Following forces Putty to open default shell
// see ssh.c: do_ssh2_authconn() and ssh1_protocol()
conf_set_str(conf, CONF_remote_cmd, "");
}
else
{
conf_set_str(conf, CONF_remote_cmd, AnsiString(Data->Shell).c_str());
}
conf_set_bool(conf, CONF_force_remote_cmd2, 0);
}
else
{
if (Data->SftpServer.IsEmpty())
{
conf_set_bool(conf, CONF_ssh_subsys, TRUE);
conf_set_str(conf, CONF_remote_cmd, "sftp");
}
else
{
conf_set_bool(conf, CONF_ssh_subsys, FALSE);
conf_set_str(conf, CONF_remote_cmd, AnsiString(Data->SftpServer).c_str());
}
if (Data->FSProtocol != fsSFTPonly)
{
conf_set_bool(conf, CONF_ssh_subsys2, FALSE);
if (Data->Shell.IsEmpty())
{
// Following forces Putty to open default shell
// see ssh.c: do_ssh2_authconn() and ssh1_protocol()
conf_set_str(conf, CONF_remote_cmd2, "");
// PuTTY ignores CONF_remote_cmd2 set to "",
// so we have to enforce it
// (CONF_force_remote_cmd2 is our config option)
conf_set_bool(conf, CONF_force_remote_cmd2, 1);
}
else
{
conf_set_str(conf, CONF_remote_cmd2, AnsiString(Data->Shell).c_str());
}
}
else
{
if (Data->SftpServer.IsEmpty())
{
// see psftp_connect() from psftp.c
conf_set_bool(conf, CONF_ssh_subsys2, FALSE);
conf_set_str(conf, CONF_remote_cmd2,
"test -x /usr/lib/sftp-server && exec /usr/lib/sftp-server\n"
"test -x /usr/local/lib/sftp-server && exec /usr/local/lib/sftp-server\n"
"exec sftp-server");
}
else
{
conf_set_bool(conf, CONF_force_remote_cmd2, 0);
}
}
}
}
conf_set_int(conf, CONF_connect_timeout, Data->Timeout * MSecsPerSec);
conf_set_int(conf, CONF_sndbuf, Data->SendBuf);
conf_set_str(conf, CONF_srcaddr, AnsiString(Data->SourceAddress).c_str());
// permanent settings
conf_set_bool(conf, CONF_nopty, TRUE);
conf_set_bool(conf, CONF_tcp_keepalives, 0);
conf_set_bool(conf, CONF_ssh_show_banner, TRUE);
conf_set_int(conf, CONF_proxy_log_to_term, FORCE_OFF);
conf_set_str(conf, CONF_loghost, AnsiString(Data->LogicalHostName).c_str());
return conf;
}
//---------------------------------------------------------------------------
static void eventlog(LogPolicy * ALogPolicy, const char * string)
{
static_cast<ScpLogPolicy *>(ALogPolicy)->SecureShell->PuttyLogEvent(string);
}
//---------------------------------------------------------------------------
static const LogPolicyVtable ScpLogPolicyVTable =
{
eventlog,
NULL, // Should never be called
NULL, // Should never be called
null_lp_verbose_no, // Should never be called
};
//---------------------------------------------------------------------------
void __fastcall TSecureShell::Open()
{
ResetConnection();
FAuthenticating = false;
FAuthenticated = false;
FLastSendBufferUpdate = 0;
// do not use UTF-8 until decided otherwise (see TSCPFileSystem::DetectUtf())
FUtfStrings = false;
Active = false;
FAuthenticationLog = L"";
FNoConnectionResponse = false;
FUI->Information(LoadStr(STATUS_LOOKUPHOST), true);
try
{
char * RealHost;
FreeBackend(); // in case we are reconnecting
const char * InitError;
Conf * conf = StoreToConfig(FSessionData, Simple);
FSendBuf = FSessionData->SendBuf;
FSeat = new ScpSeat(this);
FLogPolicy = new ScpLogPolicy();
FLogPolicy->vt = &ScpLogPolicyVTable;
FLogPolicy->SecureShell = this;
FLogPolicy->Seat = FSeat;
try
{
FLogCtx = log_init(FLogPolicy, conf);
InitError = backend_init(&ssh_backend, FSeat, &FBackendHandle, FLogCtx, conf,
AnsiString(FSessionData->HostNameExpanded).c_str(), FSessionData->PortNumber, &RealHost,
(FSessionData->TcpNoDelay ? 1 : 0),
conf_get_bool(conf, CONF_tcp_keepalives));
}
__finally
{
conf_free(conf);
}
sfree(RealHost);
if (InitError)
{
PuttyFatalError(InitError);
}
FUI->Information(LoadStr(STATUS_CONNECT), true);
Init();
CheckConnection(CONNECTION_FAILED);
}
catch (Exception & E)
{
if (FNoConnectionResponse && TryFtp())
{
Configuration->Usage->Inc(L"ProtocolSuggestions");
// HELP_FTP_SUGGESTION won't be used as all errors that set
// FNoConnectionResponse have already their own help keyword
FUI->FatalError(&E, LoadStr(FTP_SUGGESTION), HELP_FTP_SUGGESTION);
}
else
{
throw;
}
}
FLastDataSent = Now();
FSessionInfo.LoginTime = Now();
FAuthenticating = false;
FAuthenticated = true;
FUI->Information(LoadStr(STATUS_AUTHENTICATED), true);
ResetSessionInfo();
DebugAssert(!FSessionInfo.SshImplementation.IsEmpty());
FOpened = true;
UnicodeString SshImplementation = GetSessionInfo().SshImplementation;
if (IsOpenSSH(SshImplementation))
{
FSshImplementation = sshiOpenSSH;
}
// e.g. "mod_sftp/0.9.8"
else if (SshImplementation.Pos(L"mod_sftp") == 1)
{
FSshImplementation = sshiProFTPD;
}
// e.g. "5.25 FlowSsh: Bitvise SSH Server (WinSSHD) 6.07: free only for personal non-commercial use"
else if (SshImplementation.Pos(L"FlowSsh") > 0)
{
FSshImplementation = sshiBitvise;
}
// e.g. "srtSSHServer_10.00"
else if (ContainsText(SshImplementation, L"srtSSHServer"))
{
FSshImplementation = sshiTitan;
}
else if (ContainsText(FSessionInfo.SshImplementation, L"OpenVMS"))
{
FSshImplementation = sshiOpenVMS;
}
else if (ContainsText(FSessionInfo.SshImplementation, L"CerberusFTPServer"))
{
FSshImplementation = sshiCerberus;
}
else
{
FSshImplementation = sshiUnknown;
}
}
//---------------------------------------------------------------------------
bool __fastcall TSecureShell::TryFtp()
{
bool Result;
if (!FConfiguration->TryFtpWhenSshFails)
{
Result = false;
}
else
{
if (((FSessionData->FSProtocol != fsSFTP) && (FSessionData->FSProtocol != fsSFTPonly)) ||
(FSessionData->PortNumber != SshPortNumber) ||
FSessionData->Tunnel || (FSessionData->ProxyMethod != ::pmNone))
{
LogEvent(L"Using non-standard protocol or port, tunnel or proxy, will not knock FTP port.");
Result = false;
}
else
{
LogEvent(L"Knocking FTP port.");
SOCKET Socket = socket(AF_INET, SOCK_STREAM, 0);
Result = (Socket != INVALID_SOCKET);
if (Result)
{
LPHOSTENT HostEntry = gethostbyname(AnsiString(FSessionData->HostNameExpanded).c_str());
Result = (HostEntry != NULL);
if (Result)
{
SOCKADDR_IN Address;
memset(&Address, 0, sizeof(Address));
Address.sin_family = AF_INET;
int Port = FtpPortNumber;
Address.sin_port = htons(static_cast<short>(Port));
Address.sin_addr.s_addr = *((unsigned long *)*HostEntry->h_addr_list);
HANDLE Event = CreateEvent(NULL, false, false, NULL);
Result = (WSAEventSelect(Socket, (WSAEVENT)Event, FD_CONNECT | FD_CLOSE) != SOCKET_ERROR);
if (Result)
{
Result =
(connect(Socket, reinterpret_cast<sockaddr *>(&Address), sizeof(Address)) != SOCKET_ERROR) ||
(WSAGetLastError() == WSAEWOULDBLOCK);
if (Result)
{
Result = (WaitForSingleObject(Event, 2000) == WAIT_OBJECT_0);
}
}
CloseHandle(Event);
}
closesocket(Socket);
}
if (Result)
{
LogEvent(L"FTP port opened, will suggest using FTP protocol.");
}
else
{
LogEvent(L"FTP port did not open.");
}
}
}
return Result;
}
//---------------------------------------------------------------------------
void __fastcall TSecureShell::Init()
{
try
{
try
{
// Recent pscp checks backend_exitcode(FBackendHandle) in the loop
// (see comment in putty revision 8110)
// It seems that we do not need to do it.
while (!winscp_query(FBackendHandle, WINSCP_QUERY_MAIN_CHANNEL))
{
if (Configuration->ActualLogProtocol >= 1)
{
LogEvent(L"Waiting for the server to continue with the initialization");
}
WaitForData();
}
}
catch(Exception & E)
{
if (FAuthenticating && !FAuthenticationLog.IsEmpty())
{
FUI->FatalError(&E, FMTLOAD(AUTHENTICATION_LOG, (FAuthenticationLog)));
}
else
{
throw;
}
}
}
catch(Exception & E)
{
if (FAuthenticating)
{
FUI->FatalError(&E, LoadStr(AUTHENTICATION_FAILED));
}
else
{
throw;
}
}
}
//---------------------------------------------------------------------------
struct callback_set * TSecureShell::GetCallbackSet()
{
return FCallbackSet.get();
}
//---------------------------------------------------------------------------
UnicodeString __fastcall TSecureShell::ConvertFromPutty(const char * Str, int Length)
{
int BomLength = strlen(WINSCP_BOM);
if ((Length >= BomLength) &&
(strncmp(Str, WINSCP_BOM, BomLength) == 0))
{
return UTF8ToString(Str + BomLength, Length - BomLength);
}
else
{
return AnsiToString(Str, Length);
}
}
//---------------------------------------------------------------------------
const UnicodeString ServerVersionMsg(L"Remote version: ");
const UnicodeString ForwardingFailureMsg(L"Forwarded connection refused by remote");
const UnicodeString LocalPortMsg(L"Local port ");
const UnicodeString ForwadingToMsg(L" forwarding to ");
const UnicodeString FailedMsg(L" failed:");
//---------------------------------------------------------------------------
void __fastcall TSecureShell::PuttyLogEvent(const char * AStr)
{
UnicodeString Str = ConvertFromPutty(AStr, strlen(AStr));
if (Str.Pos(L"failed") > 0)
{
Str += L"";
}
// Gross hack
if (StartsStr(ServerVersionMsg, Str))
{
FSessionInfo.SshVersionString = RightStr(Str, Str.Length() - ServerVersionMsg.Length());
const wchar_t * Ptr = wcschr(FSessionInfo.SshVersionString.c_str(), L'-');
if (Ptr != NULL)
{
Ptr = wcschr(Ptr + 1, L'-');
}
FSessionInfo.SshImplementation = (Ptr != NULL) ? Ptr + 1 : L"";
}
else if (StartsStr(ForwardingFailureMsg, Str))
{
if (ForwardingFailureMsg == Str)
{
FLastTunnelError = Str;
}
else
{
FLastTunnelError = RightStr(Str, Str.Length() - ForwardingFailureMsg.Length());
UnicodeString Prefix(L": ");
if (StartsStr(Prefix, FLastTunnelError))
{
FLastTunnelError.Delete(1, Prefix.Length());
}
static const TPuttyTranslation Translation[] = {
{ L"Administratively prohibited [%]", PFWD_TRANSL_ADMIN },
{ L"Connect failed [%]", PFWD_TRANSL_CONNECT },
};
TranslatePuttyMessage(Translation, LENOF(Translation), FLastTunnelError);
}
}
else if (StartsStr(LocalPortMsg, Str) && ContainsStr(Str, ForwadingToMsg) && ContainsStr(Str, FailedMsg))
{
FLastTunnelError = Str;
}
LogEvent(Str);
}
//---------------------------------------------------------------------------
TPromptKind __fastcall TSecureShell::IdentifyPromptKind(UnicodeString & Name)
{
// beware of changing order
static const TPuttyTranslation NameTranslation[] = {
{ L"SSH login name", USERNAME_TITLE },
{ L"SSH key passphrase", PASSPHRASE_TITLE },
{ L"SSH TIS authentication", SERVER_PROMPT_TITLE },
{ L"SSH CryptoCard authentication", SERVER_PROMPT_TITLE },
{ L"SSH server: %", SERVER_PROMPT_TITLE2 },
{ L"SSH server authentication", SERVER_PROMPT_TITLE },
{ L"SSH password", PASSWORD_TITLE },
{ L"New SSH password", NEW_PASSWORD_TITLE },
};
int Index = TranslatePuttyMessage(NameTranslation, LENOF(NameTranslation), Name);
TPromptKind PromptKind;
if (Index == 0) // username
{
PromptKind = pkUserName;
}
else if (Index == 1) // passphrase
{
PromptKind = pkPassphrase;
}
else if (Index == 2) // TIS
{
PromptKind = pkTIS;
}
else if (Index == 3) // CryptoCard
{
PromptKind = pkCryptoCard;
}
else if ((Index == 4) || (Index == 5))
{
PromptKind = pkKeybInteractive;
}
else if (Index == 6)
{
PromptKind = pkPassword;
}
else if (Index == 7)
{
PromptKind = pkNewPassword;
}
else
{
PromptKind = pkPrompt;
DebugFail();
}
return PromptKind;
}
//---------------------------------------------------------------------------
bool __fastcall TSecureShell::PromptUser(bool /*ToServer*/,
UnicodeString AName, bool /*NameRequired*/,
UnicodeString Instructions, bool InstructionsRequired,
TStrings * Prompts, TStrings * Results)
{
// there can be zero prompts!
DebugAssert(Results->Count == Prompts->Count);
UnicodeString Name = AName;
TPromptKind PromptKind = IdentifyPromptKind(Name);
const TPuttyTranslation * InstructionTranslation = NULL;
const TPuttyTranslation * PromptTranslation = NULL;
size_t PromptTranslationCount = 1;
UnicodeString PromptDesc;
if (PromptKind == pkUserName)
{
static const TPuttyTranslation UsernamePromptTranslation[] = {
{ L"login as: ", USERNAME_PROMPT2 },
};
PromptTranslation = UsernamePromptTranslation;
PromptDesc = L"username";
}
else if (PromptKind == pkPassphrase)
{
static const TPuttyTranslation PassphrasePromptTranslation[] = {
{ L"Passphrase for key \"%\": ", PROMPT_KEY_PASSPHRASE },
};
PromptTranslation = PassphrasePromptTranslation;
PromptDesc = L"passphrase";
}
else if (PromptKind == pkTIS)
{
static const TPuttyTranslation TISInstructionTranslation[] = {
{ L"Using TIS authentication.%", TIS_INSTRUCTION },
};
static const TPuttyTranslation TISPromptTranslation[] = {
{ L"Response: ", PROMPT_PROMPT },
};
InstructionTranslation = TISInstructionTranslation;
PromptTranslation = TISPromptTranslation;
PromptDesc = L"tis";
}
else if (PromptKind == pkCryptoCard)
{
static const TPuttyTranslation CryptoCardInstructionTranslation[] = {
{ L"Using CryptoCard authentication.%", CRYPTOCARD_INSTRUCTION },
};
static const TPuttyTranslation CryptoCardPromptTranslation[] = {
{ L"Response: ", PROMPT_PROMPT },
};
InstructionTranslation = CryptoCardInstructionTranslation;
PromptTranslation = CryptoCardPromptTranslation;
PromptDesc = L"cryptocard";
}
else if (PromptKind == pkKeybInteractive)
{
static const TPuttyTranslation KeybInteractiveInstructionTranslation[] = {
{ L"Using keyboard-interactive authentication.%", KEYBINTER_INSTRUCTION },
};
static const TPuttyTranslation KeybInteractivePromptTranslation[] = {
// as used by Linux-PAM (pam_exec/pam_exec.c, libpam/pam_get_authtok.c,
// pam_unix/pam_unix_auth.c, pam_userdb/pam_userdb.c)
{ L"Password: ", PASSWORD_PROMPT },
};
InstructionTranslation = KeybInteractiveInstructionTranslation;
PromptTranslation = KeybInteractivePromptTranslation;
PromptDesc = L"keyboard interactive";
}
else if (PromptKind == pkPassword)
{
DebugAssert(Prompts->Count == 1);
Prompts->Strings[0] = LoadStr(PASSWORD_PROMPT);
PromptDesc = L"password";
}
else if (PromptKind == pkNewPassword)
{
// Can be tested with WS_FTP server
static const TPuttyTranslation NewPasswordPromptTranslation[] = {
{ L"Current password (blank for previously entered password): ", NEW_PASSWORD_CURRENT_PROMPT },
{ L"Enter new password: ", NEW_PASSWORD_NEW_PROMPT },
{ L"Confirm new password: ", NEW_PASSWORD_CONFIRM_PROMPT },
};
PromptTranslation = NewPasswordPromptTranslation;
PromptTranslationCount = LENOF(NewPasswordPromptTranslation);
PromptDesc = L"new password";
}
else
{
PromptDesc = L"unknown";
DebugFail();
}
UnicodeString InstructionsLog =
(Instructions.IsEmpty() ? UnicodeString(L"<no instructions>") : FORMAT(L"\"%s\"", (Instructions)));
UnicodeString PromptsLog =
(Prompts->Count > 0 ? FORMAT(L"\"%s\"", (Prompts->Strings[0])) : UnicodeString(L"<no prompt>")) +
(Prompts->Count > 1 ? FORMAT(L"%d more", (Prompts->Count - 1)) : UnicodeString());
LogEvent(FORMAT(L"Prompt (%s, \"%s\", %s, %s)", (PromptDesc, AName, InstructionsLog, PromptsLog)));
Name = Name.Trim();
if (InstructionTranslation != NULL)
{
TranslatePuttyMessage(InstructionTranslation, 1, Instructions);
}
// some servers add leading or trailing blank line to make the prompt look prettier
// on terminal console
Instructions = Instructions.Trim();
for (int Index = 0; Index < Prompts->Count; Index++)
{
UnicodeString Prompt = Prompts->Strings[Index];
if (PromptTranslation != NULL)
{
TranslatePuttyMessage(PromptTranslation, PromptTranslationCount, Prompt);
}
// some servers add leading blank line to make the prompt look prettier
// on terminal console
Prompts->Strings[Index] = Prompt.Trim();
}
bool Result = false;
if ((PromptKind == pkTIS) || (PromptKind == pkCryptoCard) ||
(PromptKind == pkKeybInteractive))
{
if (FSessionData->AuthKIPassword && !FSessionData->Password.IsEmpty() &&
!FStoredPasswordTriedForKI && (Prompts->Count == 1) &&
FLAGCLEAR(int(Prompts->Objects[0]), pupEcho))
{
LogEvent(L"Using stored password.");
FUI->Information(LoadStr(AUTH_PASSWORD), false);
Result = true;
Results->Strings[0] = NormalizeString(FSessionData->Password);
FStoredPasswordTriedForKI = true;
}
else if (Instructions.IsEmpty() && !InstructionsRequired && (Prompts->Count == 0))
{
LogEvent(L"Ignoring empty SSH server authentication request");
Result = true;
}
}
else if (PromptKind == pkPassword)
{
if (!FSessionData->Password.IsEmpty() && !FStoredPasswordTried)
{
LogEvent(L"Using stored password.");
FUI->Information(LoadStr(AUTH_PASSWORD), false);
Result = true;
Results->Strings[0] = NormalizeString(FSessionData->Password);
FStoredPasswordTried = true;
}
}
else if (PromptKind == pkPassphrase)
{
if (!FSessionData->Passphrase.IsEmpty() && !FStoredPassphraseTried)
{
LogEvent(L"Using configured passphrase.");
Result = true;
Results->Strings[0] = FSessionData->Passphrase;
FStoredPassphraseTried = true;
}
}
else if (PromptKind == pkNewPassword)
{
if (FSessionData->ChangePassword)
{
FUI->Information(LoadStr(AUTH_CHANGING_PASSWORD), false);
if (!FSessionData->Password.IsEmpty() && !FSessionData->NewPassword.IsEmpty() && !FStoredPasswordTried)
{
LogEvent(L"Using stored password and new password.");
Result = true;
DebugAssert(Results->Count == 3);
Results->Strings[0] = NormalizeString(FSessionData->Password);
Results->Strings[1] = NormalizeString(FSessionData->NewPassword);
Results->Strings[2] = NormalizeString(FSessionData->NewPassword);
FStoredPasswordTried = true;
}
}
}
if (!Result)
{
LogEvent(L"Prompting user for the credentials.");
Result = FUI->PromptUser(FSessionData,
PromptKind, Name, Instructions, Prompts, Results);
if (!Result)
{
LogEvent(L"Prompt cancelled.");
}
else
{
if ((Prompts->Count >= 1) &&
(FLAGSET(int(Prompts->Objects[0]), pupEcho) || Configuration->LogSensitive))
{
LogEvent(FORMAT(L"Response: \"%s\"", (Results->Strings[0])));
}
else
{
LogEvent("Prompt responded.");
}
if ((PromptKind == pkUserName) && (Prompts->Count == 1))
{
FUserName = Results->Strings[0];
}
}
}
return Result;
}
//---------------------------------------------------------------------------
void __fastcall TSecureShell::GotHostKey()
{
// due to re-key GotHostKey() may be called again later during session
if (!FAuthenticating && !FAuthenticated)
{
FAuthenticating = true;
if (!FSessionData->ChangePassword)
{
FUI->Information(LoadStr(STATUS_AUTHENTICATE), true);
}
}
}
//---------------------------------------------------------------------------
void __fastcall TSecureShell::CWrite(const char * Data, size_t Length)
{
// some messages to stderr may indicate that something has changed with the
// session, so reset the session info
ResetSessionInfo();
// We send only whole line at once, so we have to cache incoming data
FCWriteTemp += DeleteChar(ConvertFromPutty(Data, Length), L'\r');
UnicodeString Line;
// Do we have at least one complete line in std error cache?
while (FCWriteTemp.Pos(L"\n") > 0)
{
UnicodeString Line = CutToChar(FCWriteTemp, L'\n', false);
FLog->Add(llStdError, Line);
if (FAuthenticating)
{
TranslateAuthenticationMessage(Line);
FAuthenticationLog += (FAuthenticationLog.IsEmpty() ? L"" : L"\n") + Line;
}
FUI->Information(Line, false);
}
}
//---------------------------------------------------------------------------
void __fastcall TSecureShell::RegisterReceiveHandler(TNotifyEvent Handler)
{
DebugAssert(FOnReceive == NULL);
FOnReceive = Handler;
}
//---------------------------------------------------------------------------
void __fastcall TSecureShell::UnregisterReceiveHandler(TNotifyEvent Handler)
{
DebugAssert(FOnReceive == Handler);
DebugUsedParam(Handler);
FOnReceive = NULL;
}
//---------------------------------------------------------------------------
void __fastcall TSecureShell::FromBackend(const unsigned char * Data, size_t Length)
{
// Note that we do not apply ConvertFromPutty to Data yet (as opposite to CWrite).
// as there's no use for this atm.
CheckConnection();
if (Configuration->ActualLogProtocol >= 1)
{
LogEvent(FORMAT(L"Received %u bytes", (static_cast<int>(Length))));
}
// Following is taken from scp.c from_backend() and modified
const unsigned char *p = Data;
unsigned Len = Length;
// with event-select mechanism we can now receive data even before we
// actually expect them (OutPtr can be NULL)
if ((OutPtr != NULL) && (OutLen > 0) && (Len > 0))
{
unsigned Used = OutLen;
if (Used > Len) Used = Len;
memmove(OutPtr, p, Used);
OutPtr += Used; OutLen -= Used;
p += Used; Len -= Used;
}
if (Len > 0)
{
if (PendSize < PendLen + Len)
{
PendSize = PendLen + Len + 4096;
Pending = (unsigned char *)
(Pending ? srealloc(Pending, PendSize) : smalloc(PendSize));
if (!Pending) FatalError(L"Out of memory");
}
memmove(Pending + PendLen, p, Len);
PendLen += Len;
}
if (FOnReceive != NULL)
{
if (!FFrozen)
{
FFrozen = true;
try
{
do
{
FDataWhileFrozen = false;
FOnReceive(NULL);
}
while (FDataWhileFrozen);
}
__finally
{
FFrozen = false;
}
}
else
{
FDataWhileFrozen = true;
}
}
}
//---------------------------------------------------------------------------
bool __fastcall TSecureShell::Peek(unsigned char *& Buf, int Len)
{
bool Result = (int(PendLen) >= Len);
if (Result)
{
Buf = Pending;
}
return Result;
}
//---------------------------------------------------------------------------
Integer __fastcall TSecureShell::Receive(unsigned char * Buf, Integer Len)
{
CheckConnection();
if (Len > 0)
{
// Following is taken from scp.c ssh_scp_recv() and modified
OutPtr = Buf;
OutLen = Len;
try
{
/*
* See if the pending-input block contains some of what we
* need.
*/
if (PendLen > 0)
{
unsigned PendUsed = PendLen;
if (PendUsed > OutLen)
{
PendUsed = OutLen;
}
memmove(OutPtr, Pending, PendUsed);
memmove(Pending, Pending + PendUsed, PendLen - PendUsed);
OutPtr += PendUsed;
OutLen -= PendUsed;
PendLen -= PendUsed;
if (PendLen == 0)
{
PendSize = 0;
sfree(Pending);
Pending = NULL;
}
}
while (OutLen > 0)
{
if (Configuration->ActualLogProtocol >= 1)
{
LogEvent(FORMAT(L"Waiting for another %u bytes", (static_cast<int>(OutLen))));
}
WaitForData();
}
// This seems ambiguous
if (Len <= 0) FatalError(LoadStr(LOST_CONNECTION));
}
__finally
{
OutPtr = NULL;
}
}
if (Configuration->ActualLogProtocol >= 1)
{
LogEvent(FORMAT(L"Read %u bytes (%d pending)",
(static_cast<int>(Len), static_cast<int>(PendLen))));
}
return Len;
}
//---------------------------------------------------------------------------
UnicodeString __fastcall TSecureShell::ReceiveLine()
{
unsigned Index;
RawByteString Line;
Boolean EOL = False;
do
{
// If there is any buffer of received chars
if (PendLen > 0)
{
Index = 0;
// Repeat until we walk thru whole buffer or reach end-of-line
while ((Index < PendLen) && (!Index || (Pending[Index-1] != '\n')))
{
Index++;
}
EOL = (Boolean)(Index && (Pending[Index-1] == '\n'));
Integer PrevLen = Line.Length();
Line.SetLength(PrevLen + Index);
Receive(reinterpret_cast<unsigned char *>(Line.c_str()) + PrevLen, Index);
}
// If buffer don't contain end-of-line character
// we read one more which causes receiving new buffer of chars
if (!EOL)
{
unsigned char Ch;
Receive(&Ch, 1);
Line += static_cast<char>(Ch);
EOL = (static_cast<char>(Ch) == '\n');
}
}
while (!EOL);
// We don't want end-of-line character
Line.SetLength(Line.Length()-1);
UnicodeString Result = ConvertInput(Line);
CaptureOutput(llOutput, Result);
return Result;
}
//---------------------------------------------------------------------------
UnicodeString __fastcall TSecureShell::ConvertInput(const RawByteString & Input)
{
UnicodeString Result;
if (UtfStrings)
{
Result = UTF8ToString(Input);
}
else
{
Result = AnsiToString(Input);
}
return Result;
}
//---------------------------------------------------------------------------
void __fastcall TSecureShell::SendSpecial(int Code)
{
if (Configuration->ActualLogProtocol >= 0)
{
LogEvent(FORMAT(L"Sending special code: %d", (Code)));
}
CheckConnection();
backend_special(FBackendHandle, (SessionSpecialCode)Code, 0);
CheckConnection();
FLastDataSent = Now();
}
//---------------------------------------------------------------------------
unsigned int __fastcall TSecureShell::TimeoutPrompt(TQueryParamsTimerEvent PoolEvent)
{
FWaiting++;
unsigned int Answer;
try
{
TQueryParams Params(qpFatalAbort | qpAllowContinueOnError | qpIgnoreAbort);
Params.HelpKeyword = HELP_MESSAGE_HOST_IS_NOT_COMMUNICATING;
Params.Timer = 500;
Params.TimerEvent = PoolEvent;
Params.TimerMessage = MainInstructionsFirstParagraph(FMTLOAD(TIMEOUT_STILL_WAITING3, (FSessionData->Timeout)));
Params.TimerAnswers = qaAbort;
Params.TimerQueryType = qtInformation;
if (FConfiguration->SessionReopenAutoStall > 0)
{
Params.Timeout = FConfiguration->SessionReopenAutoStall;
Params.TimeoutAnswer = qaAbort;
Params.TimeoutResponse = qaNo;
}
Answer = FUI->QueryUser(MainInstructions(FMTLOAD(CONFIRM_PROLONG_TIMEOUT3, (FSessionData->Timeout))),
NULL, qaRetry | qaAbort, &Params);
}
__finally
{
FWaiting--;
}
return Answer;
}
//---------------------------------------------------------------------------
void __fastcall TSecureShell::SendBuffer(unsigned int & Result)
{
// for comments see PoolForData
if (!Active)
{
Result = qaRetry;
}
else
{
try
{
if (backend_sendbuffer(FBackendHandle) <= MAX_BUFSIZE)
{
Result = qaOK;
}
}
catch(...)
{
Result = qaRetry;
}
}
}
//---------------------------------------------------------------------------
void TSecureShell::TimeoutAbort(unsigned int Answer)
{
FatalError(MainInstructions(LoadStr(Answer == qaAbort ? USER_TERMINATED : TIMEOUT_ERROR)));
}
//---------------------------------------------------------------------------
void __fastcall TSecureShell::DispatchSendBuffer(int BufSize)
{
TDateTime Start = Now();
do
{
CheckConnection();
if (Configuration->ActualLogProtocol >= 1)
{
LogEvent(FORMAT(L"There are %u bytes remaining in the send buffer, "
"need to send at least another %u bytes",
(BufSize, BufSize - MAX_BUFSIZE)));
}
EventSelectLoop(100, false, NULL);
BufSize = backend_sendbuffer(FBackendHandle);
if (Configuration->ActualLogProtocol >= 1)
{
LogEvent(FORMAT(L"There are %u bytes remaining in the send buffer", (BufSize)));
}
if (Now() - Start > FSessionData->TimeoutDT)
{
LogEvent(L"Waiting for dispatching send buffer timed out, asking user what to do.");
unsigned int Answer = TimeoutPrompt(SendBuffer);
switch (Answer)
{
case qaRetry:
Start = Now();
break;
case qaOK:
BufSize = 0;
break;
default:
DebugFail();
// fallthru
case qaAbort:
case qaNo:
TimeoutAbort(Answer);
break;
}
}
}
while (BufSize > MAX_BUFSIZE);
}
//---------------------------------------------------------------------------
void __fastcall TSecureShell::Send(const unsigned char * Buf, Integer Len)
{
CheckConnection();
int BufSize = backend_send(FBackendHandle, const_cast<char *>(reinterpret_cast<const char *>(Buf)), Len);
if (Configuration->ActualLogProtocol >= 1)
{
LogEvent(FORMAT(L"Sent %u bytes", (static_cast<int>(Len))));
LogEvent(FORMAT(L"There are %u bytes remaining in the send buffer", (BufSize)));
}
FLastDataSent = Now();
// among other forces receive of pending data to free the servers's send buffer
EventSelectLoop(0, false, NULL);
if (BufSize > MAX_BUFSIZE)
{
DispatchSendBuffer(BufSize);
}
CheckConnection();
}
//---------------------------------------------------------------------------
void __fastcall TSecureShell::SendNull()
{
LogEvent(L"Sending NULL.");
unsigned char Null = 0;
Send(&Null, 1);
}
//---------------------------------------------------------------------------
void __fastcall TSecureShell::SendLine(const UnicodeString & Line)
{
CheckConnection();
RawByteString Buf;
if (UtfStrings)
{
Buf = RawByteString(UTF8String(Line));
}
else
{
Buf = RawByteString(AnsiString(Line));
}
Buf += "\n";
FLog->Add(llInput, Line);
Send(reinterpret_cast<const unsigned char *>(Buf.c_str()), Buf.Length());
}
//---------------------------------------------------------------------------
int __fastcall TSecureShell::TranslatePuttyMessage(
const TPuttyTranslation * Translation, size_t Count, UnicodeString & Message,
UnicodeString * HelpKeyword)
{
int Result = -1;
for (unsigned int Index = 0; Index < Count; Index++)
{
const wchar_t * Original = Translation[Index].Original;
const wchar_t * Div = wcschr(Original, L'%');
if (Div == NULL)
{
if (wcscmp(Message.c_str(), Original) == 0)
{
Message = LoadStr(Translation[Index].Translation);
Result = int(Index);
break;
}
}
else
{
size_t OriginalLen = wcslen(Original);
size_t PrefixLen = Div - Original;
size_t SuffixLen = OriginalLen - PrefixLen - 1;
if (((size_t)Message.Length() >= OriginalLen - 1) &&
(wcsncmp(Message.c_str(), Original, PrefixLen) == 0) &&
(wcsncmp(Message.c_str() + Message.Length() - SuffixLen, Div + 1, SuffixLen) == 0))
{
Message = FMTLOAD(Translation[Index].Translation,
(Message.SubString(PrefixLen + 1, Message.Length() - PrefixLen - SuffixLen).TrimRight()));
Result = int(Index);
break;
}
}
}
if ((HelpKeyword != NULL) && (Result >= 0))
{
*HelpKeyword = Translation[Result].HelpKeyword;
}
return Result;
}
//---------------------------------------------------------------------------
int __fastcall TSecureShell::TranslateAuthenticationMessage(
UnicodeString & Message, UnicodeString * HelpKeyword)
{
static const TPuttyTranslation Translation[] = {
{ L"Using username \"%\".", AUTH_TRANSL_USERNAME },
{ L"Using keyboard-interactive authentication.", AUTH_TRANSL_KEYB_INTER }, // not used anymore
{ L"Authenticating with public key \"%\" from agent", AUTH_TRANSL_PUBLIC_KEY_AGENT },
{ L"Authenticating with public key \"%\"", AUTH_TRANSL_PUBLIC_KEY },
{ L"Authenticated using RSA key \"%\" from agent", AUTH_TRANSL_PUBLIC_KEY_AGENT },
{ L"Wrong passphrase", AUTH_TRANSL_WRONG_PASSPHRASE },
{ L"Wrong passphrase.", AUTH_TRANSL_WRONG_PASSPHRASE },
{ L"Access denied", AUTH_TRANSL_ACCESS_DENIED },
{ L"Trying public key authentication.", AUTH_TRANSL_TRY_PUBLIC_KEY },
{ L"Server refused our public key.", AUTH_TRANSL_KEY_REFUSED, HELP_AUTH_TRANSL_KEY_REFUSED }, // help mapping probably never used
{ L"Server refused our key", AUTH_TRANSL_KEY_REFUSED, HELP_AUTH_TRANSL_KEY_REFUSED }
};
int Result = TranslatePuttyMessage(Translation, LENOF(Translation), Message, HelpKeyword);
if ((Result == 2) || (Result == 3) || (Result == 4))
{
FCollectPrivateKeyUsage = true;
}
return Result;
}
//---------------------------------------------------------------------------
void __fastcall TSecureShell::AddStdError(const char * Data, size_t Length)
{
UnicodeString Str = ConvertInput(RawByteString(Data, Length));
FStdError += Str;
Integer P;
Str = DeleteChar(Str, L'\r');
// We send only whole line at once to log, so we have to cache
// incoming std error data
FStdErrorTemp += Str;
UnicodeString Line;
// Do we have at least one complete line in std error cache?
while ((P = FStdErrorTemp.Pos(L"\n")) > 0)
{
Line = FStdErrorTemp.SubString(1, P-1);
FStdErrorTemp.Delete(1, P);
AddStdErrorLine(Line);
}
}
//---------------------------------------------------------------------------
void __fastcall TSecureShell::AddStdErrorLine(const UnicodeString & Str)
{
if (FAuthenticating)
{
FAuthenticationLog += (FAuthenticationLog.IsEmpty() ? L"" : L"\n") + Str;
}
CaptureOutput(llStdError, Str);
}
//---------------------------------------------------------------------------
const UnicodeString & __fastcall TSecureShell::GetStdError()
{
return FStdError;
}
//---------------------------------------------------------------------------
void __fastcall TSecureShell::ClearStdError()
{
// Flush std error cache
if (!FStdErrorTemp.IsEmpty())
{
if (FAuthenticating)
{
FAuthenticationLog +=
(FAuthenticationLog.IsEmpty() ? L"" : L"\n") + FStdErrorTemp;
}
CaptureOutput(llStdError, FStdErrorTemp);
FStdErrorTemp = L"";
}
FStdError = L"";
}
//---------------------------------------------------------------------------
void __fastcall TSecureShell::CaptureOutput(TLogLineType Type,
const UnicodeString & Line)
{
if (FOnCaptureOutput != NULL)
{
FOnCaptureOutput(Line, (Type == llStdError) ? cotError : cotOutput);
}
FLog->Add(Type, Line);
}
//---------------------------------------------------------------------------
int __fastcall TSecureShell::TranslateErrorMessage(
UnicodeString & Message, UnicodeString * HelpKeyword)
{
static const TPuttyTranslation Translation[] = {
{ L"Remote side unexpectedly closed network connection", UNEXPECTED_CLOSE_ERROR, HELP_UNEXPECTED_CLOSE_ERROR },
{ L"Network error: Connection refused", NET_TRANSL_REFUSED2, HELP_NET_TRANSL_REFUSED },
{ L"Network error: Connection reset by peer", NET_TRANSL_RESET, HELP_NET_TRANSL_RESET },
{ L"Network error: Connection timed out", NET_TRANSL_TIMEOUT2, HELP_NET_TRANSL_TIMEOUT },
{ L"Network error: No route to host", NET_TRANSL_NO_ROUTE2, HELP_NET_TRANSL_NO_ROUTE },
{ L"Network error: Software caused connection abort", NET_TRANSL_CONN_ABORTED, HELP_NET_TRANSL_CONN_ABORTED },
{ L"Host does not exist", NET_TRANSL_HOST_NOT_EXIST2, HELP_NET_TRANSL_HOST_NOT_EXIST },
{ L"Incoming packet was garbled on decryption", NET_TRANSL_PACKET_GARBLED, HELP_NET_TRANSL_PACKET_GARBLED },
};
int Index = TranslatePuttyMessage(Translation, LENOF(Translation), Message, HelpKeyword);
if ((Index == 0) || (Index == 1) || (Index == 2) || (Index == 3))
{
FNoConnectionResponse = true;
}
Message = ReplaceStr(Message, L"%HOST%", FSessionData->HostNameExpanded);
return Index;
}
//---------------------------------------------------------------------------
void __fastcall TSecureShell::PuttyFatalError(UnicodeString Error)
{
UnicodeString HelpKeyword;
TranslateErrorMessage(Error, &HelpKeyword);
if (!FClosed)
{
FatalError(Error, HelpKeyword);
}
else
{
LogEvent(FORMAT(L"Ignoring an error from the server while closing: %s", (Error)));
}
}
//---------------------------------------------------------------------------
void __fastcall TSecureShell::FatalError(UnicodeString Error, UnicodeString HelpKeyword)
{
FUI->FatalError(NULL, Error, HelpKeyword);
}
//---------------------------------------------------------------------------
void __fastcall inline TSecureShell::LogEvent(const UnicodeString & Str)
{
if (FLog->Logging)
{
FLog->Add(llMessage, Str);
}
}
//---------------------------------------------------------------------------
void __fastcall TSecureShell::SocketEventSelect(SOCKET Socket, HANDLE Event, bool Enable)
{
int Events;
if (Enable)
{
Events = (FD_CONNECT | FD_READ | FD_WRITE | FD_OOB | FD_CLOSE | FD_ACCEPT);
}
else
{
Events = 0;
}
if (Configuration->ActualLogProtocol >= 2)
{
LogEvent(FORMAT(L"Selecting events %d for socket %d", (int(Events), int(Socket))));
}
if (WSAEventSelect(Socket, (WSAEVENT)Event, Events) == SOCKET_ERROR)
{
if (Configuration->ActualLogProtocol >= 2)
{
LogEvent(FORMAT(L"Error selecting events %d for socket %d", (int(Events), int(Socket))));
}
if (Enable)
{
FatalError(FMTLOAD(EVENT_SELECT_ERROR, (WSAGetLastError())));
}
}
}
//---------------------------------------------------------------------------
void __fastcall TSecureShell::UpdateSocket(SOCKET value, bool Enable)
{
if (!FActive && !Enable)
{
// no-op
// Remove the branch eventually:
// When TCP connection fails, PuTTY does not release the memory allocated for
// socket. As a simple hack we call sk_tcp_close() in ssh.c to release the memory,
// until they fix it better. Unfortunately sk_tcp_close calls do_select,
// so we must filter that out.
}
else
{
DebugAssert(value);
DebugAssert((FActive && (FSocket == value)) || (!FActive && Enable));
// filter our "local proxy" connection, which have no socket
if (value != INVALID_SOCKET)
{
SocketEventSelect(value, FSocketEvent, Enable);
}
else
{
DebugAssert(FSessionData->ProxyMethod == pmCmd);
}
if (Enable)
{
FSocket = value;
FActive = true;
}
else
{
FSocket = INVALID_SOCKET;
Discard();
}
}
}
//---------------------------------------------------------------------------
void __fastcall TSecureShell::UpdatePortFwdSocket(SOCKET value, bool Enable)
{
if (Configuration->ActualLogProtocol >= 2)
{
LogEvent(FORMAT(L"Updating forwarding socket %d (%d)", (int(value), int(Enable))));
}
SocketEventSelect(value, FSocketEvent, Enable);
if (Enable)
{
FPortFwdSockets.insert(value);
}
else
{
FPortFwdSockets.erase(value);
}
}
//---------------------------------------------------------------------------
void __fastcall TSecureShell::SetActive(bool value)
{
if (FActive != value)
{
if (value)
{
Open();
}
else
{
Close();
}
}
}
//---------------------------------------------------------------------------
void __fastcall TSecureShell::FreeBackend()
{
if (FBackendHandle != NULL)
{
backend_free(FBackendHandle);
FBackendHandle = NULL;
// After destroying backend, ic_pktin_free should be the only remaining callback.
if (is_idempotent_callback_pending(FCallbackSet.get(), FCallbackSet->ic_pktin_free))
{
// This releases the callback and should be noop otherwise.
run_toplevel_callbacks(FCallbackSet.get());
}
sfree(FCallbackSet->ic_pktin_free);
FCallbackSet->ic_pktin_free = NULL;
// Not checking that cbcurr is NULL. It may be non-null, when (fatal?) exception occurs, while the callback is called.
FCallbackSet->cbcurr = NULL;
DebugAssert(FCallbackSet->cbhead == NULL);
DebugAssert(FCallbackSet->cbtail == NULL);
if (FCallbackSet->pktin_freeq_head != NULL)
{
DebugAssert(FCallbackSet->pktin_freeq_head->next == FCallbackSet->pktin_freeq_head);
sfree(FCallbackSet->pktin_freeq_head);
FCallbackSet->pktin_freeq_head = NULL;
}
}
}
//---------------------------------------------------------------------------
void __fastcall TSecureShell::Discard()
{
bool WasActive = FActive;
FActive = false;
FOpened = false;
if (WasActive)
{
FUI->Closed();
}
}
//---------------------------------------------------------------------------
void __fastcall TSecureShell::Close()
{
LogEvent(L"Closing connection.");
DebugAssert(FActive);
FClosed = true;
// Without main channel SS_EOF is ignored and would get stuck waiting for exit code.
if ((backend_exitcode(FBackendHandle) < 0) && winscp_query(FBackendHandle, WINSCP_QUERY_MAIN_CHANNEL))
{
// this is particularly necessary when using local proxy command
// (e.g. plink), otherwise it hangs in sk_localproxy_close
SendSpecial(SS_EOF);
// Try waiting for the EOF exchange to complete (among other to avoid packet queue memory leaks)
int Timeout = 500;
while ((backend_exitcode(FBackendHandle) < 0) && (Timeout > 0))
{
const int Step = 100;
if (!EventSelectLoop(Step, false, NULL))
{
Timeout -= Step;
}
}
}
FreeBackend();
Discard();
}
//---------------------------------------------------------------------------
void inline __fastcall TSecureShell::CheckConnection(int Message)
{
if (!FActive || (backend_exitcode(FBackendHandle) >= 0))
{
UnicodeString Str;
UnicodeString HelpKeyword;
if (Message >= 0)
{
Str = LoadStr(Message);
}
else
{
Str = LoadStr(NOT_CONNECTED);
HelpKeyword = HELP_NOT_CONNECTED;
}
Str = MainInstructions(Str);
int ExitCode = backend_exitcode(FBackendHandle);
if (ExitCode >= 0)
{
Str += L" " + FMTLOAD(SSH_EXITCODE, (ExitCode));
}
FatalError(Str, HelpKeyword);
}
}
//---------------------------------------------------------------------------
void __fastcall TSecureShell::PoolForData(WSANETWORKEVENTS & Events, unsigned int & Result)
{
if (!Active)
{
// see comment below
Result = qaRetry;
}
else
{
try
{
if (Configuration->ActualLogProtocol >= 2)
{
LogEvent(L"Pooling for data in case they finally arrives");
}
// in extreme condition it may happen that send buffer is full, but there
// will be no data coming and we may not empty the send buffer because we
// do not process FD_WRITE until we receive any FD_READ
if (EventSelectLoop(0, false, &Events))
{
LogEvent(L"Data has arrived, closing query to user.");
Result = qaOK;
}
}
catch(...)
{
// if we let the exception out, it may popup another message dialog
// in whole event loop, another call to PoolForData from original dialog
// would be invoked, leading to an infinite loop.
// by retrying we hope (that probably fatal) error would repeat in WaitForData.
// anyway now once no actual work is done in EventSelectLoop,
// hardly any exception can occur actually
Result = qaRetry;
}
}
}
//---------------------------------------------------------------------------
class TPoolForDataEvent
{
public:
__fastcall TPoolForDataEvent(TSecureShell * SecureShell, WSANETWORKEVENTS & Events) :
FSecureShell(SecureShell),
FEvents(Events)
{
}
void __fastcall PoolForData(unsigned int & Result)
{
FSecureShell->PoolForData(FEvents, Result);
}
private:
TSecureShell * FSecureShell;
WSANETWORKEVENTS & FEvents;
};
//---------------------------------------------------------------------------
void __fastcall TSecureShell::WaitForData()
{
// see winsftp.c
bool IncomingData;
do
{
if (Configuration->ActualLogProtocol >= 2)
{
LogEvent(L"Looking for incoming data");
}
IncomingData = EventSelectLoop(FSessionData->Timeout * MSecsPerSec, true, NULL);
if (!IncomingData)
{
DebugAssert(FWaitingForData == 0);
TAutoNestingCounter NestingCounter(FWaitingForData);
WSANETWORKEVENTS Events;
memset(&Events, 0, sizeof(Events));
TPoolForDataEvent Event(this, Events);
LogEvent(L"Waiting for data timed out, asking user what to do.");
unsigned int Answer = TimeoutPrompt(Event.PoolForData);
switch (Answer)
{
case qaRetry:
// noop
break;
case qaOK:
// read event was already captured in PoolForData(),
// make sure we do not try to select it again as it would timeout
// unless another read event occurs
IncomingData = true;
HandleNetworkEvents(FSocket, Events);
break;
default:
DebugFail();
// fallthru
case qaAbort:
case qaNo:
TimeoutAbort(Answer);
break;
}
}
}
while (!IncomingData);
}
//---------------------------------------------------------------------------
bool __fastcall TSecureShell::SshFallbackCmd() const
{
return ssh_fallback_cmd(FBackendHandle);
}
//---------------------------------------------------------------------------
bool __fastcall TSecureShell::EnumNetworkEvents(SOCKET Socket, WSANETWORKEVENTS & Events)
{
if (Configuration->ActualLogProtocol >= 2)
{
LogEvent(FORMAT(L"Enumerating network events for socket %d", (int(Socket))));
}
// see winplink.c
WSANETWORKEVENTS AEvents;
if (WSAEnumNetworkEvents(Socket, NULL, &AEvents) == 0)
{
noise_ultralight(NOISE_SOURCE_IOID, Socket);
noise_ultralight(NOISE_SOURCE_IOID, AEvents.lNetworkEvents);
Events.lNetworkEvents |= AEvents.lNetworkEvents;
for (int Index = 0; Index < FD_MAX_EVENTS; Index++)
{
if (AEvents.iErrorCode[Index] != 0)
{
Events.iErrorCode[Index] = AEvents.iErrorCode[Index];
}
}
if (Configuration->ActualLogProtocol >= 2)
{
LogEvent(FORMAT(L"Enumerated %d network events making %d cumulative events for socket %d",
(int(AEvents.lNetworkEvents), int(Events.lNetworkEvents), int(Socket))));
}
}
else
{
if (Configuration->ActualLogProtocol >= 2)
{
LogEvent(FORMAT(L"Error enumerating network events for socket %d", (int(Socket))));
}
}
bool Result =
FLAGSET(Events.lNetworkEvents, FD_READ) ||
FLAGSET(Events.lNetworkEvents, FD_CLOSE);
return Result;
}
//---------------------------------------------------------------------------
void __fastcall TSecureShell::HandleNetworkEvents(SOCKET Socket, WSANETWORKEVENTS & Events)
{
static const struct { int Bit, Mask; const wchar_t * Desc; } EventTypes[] =
{
{ FD_WRITE_BIT, FD_WRITE, L"write" },
{ FD_OOB_BIT, FD_OOB, L"oob" },
{ FD_ACCEPT_BIT, FD_ACCEPT, L"accept" },
{ FD_CONNECT_BIT, FD_CONNECT, L"connect" },
{ FD_CLOSE_BIT, FD_CLOSE, L"close" },
// Read goes last, as it can cause an exception.
// Though a correct solution would be to process all events, even if one causes exception
{ FD_READ_BIT, FD_READ, L"read" },
};
for (unsigned int Event = 0; Event < LENOF(EventTypes); Event++)
{
if (FLAGSET(Events.lNetworkEvents, EventTypes[Event].Mask))
{
int Err = Events.iErrorCode[EventTypes[Event].Bit];
if (Configuration->ActualLogProtocol >= 2)
{
LogEvent(FORMAT(L"Handling network %s event on socket %d with error %d",
(EventTypes[Event].Desc, int(Socket), Err)));
}
#pragma option push -w-prc
LPARAM SelectEvent = WSAMAKESELECTREPLY(EventTypes[Event].Mask, Err);
#pragma option pop
select_result((WPARAM)Socket, SelectEvent);
CheckConnection();
}
}
}
//---------------------------------------------------------------------------
bool __fastcall TSecureShell::ProcessNetworkEvents(SOCKET Socket)
{
WSANETWORKEVENTS Events;
memset(&Events, 0, sizeof(Events));
bool Result = EnumNetworkEvents(Socket, Events);
HandleNetworkEvents(Socket, Events);
return Result;
}
//---------------------------------------------------------------------------
bool __fastcall TSecureShell::EventSelectLoop(unsigned int MSec, bool ReadEventRequired,
WSANETWORKEVENTS * Events)
{
CheckConnection();
bool Result = false;
do
{
if (Configuration->ActualLogProtocol >= 2)
{
LogEvent(L"Looking for network events");
}
unsigned int TicksBefore = GetTickCount();
int HandleCount;
HANDLE * Handles = NULL;
try
{
unsigned int Timeout = MSec;
unsigned int WaitResult;
do
{
CheckConnection();
unsigned int TimeoutStep = std::min(GUIUpdateInterval, Timeout);
if (toplevel_callback_pending(GetCallbackSet()))
{
TimeoutStep = 0;
}
Timeout -= TimeoutStep;
if (Handles != NULL)
{
sfree(Handles);
}
// It returns only busy handles, so the set can change with every call to run_toplevel_callbacks.
Handles = handle_get_events(FCallbackSet->handles_by_evtomain, &HandleCount);
Handles = sresize(Handles, HandleCount + 1, HANDLE);
Handles[HandleCount] = FSocketEvent;
WaitResult = WaitForMultipleObjects(HandleCount + 1, Handles, FALSE, TimeoutStep);
FUI->ProcessGUI();
// run_toplevel_callbacks can cause processing of pending raw data, so:
// 1) Check for changes in our pending buffer - wait criteria in Receive()
int PrevDataLen = (-static_cast<int>(OutLen) + static_cast<int>(PendLen));
// 2) Changes in session state - wait criteria in Init()
unsigned int HadMainChannel = winscp_query(FBackendHandle, WINSCP_QUERY_MAIN_CHANNEL);
if (run_toplevel_callbacks(GetCallbackSet()) &&
(((-static_cast<int>(OutLen) + static_cast<int>(PendLen)) > PrevDataLen) ||
(HadMainChannel != winscp_query(FBackendHandle, WINSCP_QUERY_MAIN_CHANNEL))))
{
// Note that we still may process new network event now
Result = true;
}
} while ((WaitResult == WAIT_TIMEOUT) && (Timeout > 0) && !Result);
if (WaitResult < WAIT_OBJECT_0 + HandleCount)
{
if (handle_got_event(FCallbackSet->handles_by_evtomain, Handles[WaitResult - WAIT_OBJECT_0]))
{
Result = true;
}
}
else if (WaitResult == WAIT_OBJECT_0 + HandleCount)
{
if (Configuration->ActualLogProtocol >= 1)
{
LogEvent(L"Detected network event");
}
if (Events == NULL)
{
if (ProcessNetworkEvents(FSocket))
{
Result = true;
}
}
else
{
if (EnumNetworkEvents(FSocket, *Events))
{
Result = true;
}
}
{
TSockets::iterator i = FPortFwdSockets.begin();
while (i != FPortFwdSockets.end())
{
ProcessNetworkEvents(*i);
i++;
}
}
}
else if (WaitResult == WAIT_TIMEOUT)
{
if (Configuration->ActualLogProtocol >= 2)
{
LogEvent(L"Timeout waiting for network events");
}
MSec = 0;
}
else
{
if (Configuration->ActualLogProtocol >= 2)
{
LogEvent(FORMAT(L"Unknown waiting result %d", (int(WaitResult))));
}
MSec = 0;
}
}
__finally
{
sfree(Handles);
}
unsigned int TicksAfter = GetTickCount();
// ticks wraps once in 49.7 days
if (TicksBefore < TicksAfter)
{
unsigned int Ticks = TicksAfter - TicksBefore;
if (Ticks > MSec)
{
MSec = 0;
}
else
{
MSec -= Ticks;
}
}
if ((FSendBuf > 0) && (TicksAfter - FLastSendBufferUpdate >= 1000))
{
DWORD BufferLen = 0;
DWORD OutLen = 0;
if (WSAIoctl(FSocket, SIO_IDEAL_SEND_BACKLOG_QUERY, NULL, 0, &BufferLen, sizeof(BufferLen), &OutLen, 0, 0) == 0)
{
DebugAssert(OutLen == sizeof(BufferLen));
if (FSendBuf < static_cast<int>(BufferLen))
{
LogEvent(FORMAT(L"Increasing send buffer from %d to %d", (FSendBuf, static_cast<int>(BufferLen))));
FSendBuf = BufferLen;
setsockopt(FSocket, SOL_SOCKET, SO_SNDBUF, reinterpret_cast<const char *>(&BufferLen), sizeof(BufferLen));
}
}
FLastSendBufferUpdate = TicksAfter;
}
}
while (ReadEventRequired && (MSec > 0) && !Result);
return Result;
}
//---------------------------------------------------------------------------
void __fastcall TSecureShell::Idle(unsigned int MSec)
{
noise_regular();
winscp_query(FBackendHandle, WINSCP_QUERY_TIMER);
// if we are actively waiting for data in WaitForData,
// do not read here, otherwise we swallow read event and never wake
if (FWaitingForData <= 0)
{
EventSelectLoop(MSec, false, NULL);
}
}
//---------------------------------------------------------------------------
void __fastcall TSecureShell::KeepAlive()
{
if (FActive && (FWaiting == 0))
{
LogEvent(L"Sending null packet to keep session alive.");
SendSpecial(SS_PING);
}
else
{
// defer next keepalive attempt
FLastDataSent = Now();
}
}
//---------------------------------------------------------------------------
unsigned long __fastcall TSecureShell::MaxPacketSize()
{
if (!FSessionInfoValid)
{
UpdateSessionInfo();
}
return winscp_query(FBackendHandle, WINSCP_QUERY_REMMAXPKT);
}
//---------------------------------------------------------------------------
UnicodeString __fastcall TSecureShell::FormatKeyStr(UnicodeString KeyStr)
{
int Index = 1;
int Digits = 0;
while (Index <= KeyStr.Length())
{
if (IsHex(KeyStr[Index]))
{
Digits++;
if (Digits >= 16)
{
KeyStr.Insert(L" ", Index + 1);
Index++;
Digits = 0;
}
}
else
{
Digits = 0;
}
Index++;
}
return KeyStr;
}
//---------------------------------------------------------------------------
void __fastcall TSecureShell::GetRealHost(UnicodeString & Host, int & Port)
{
if (FSessionData->Tunnel)
{
// Now that we set the CONF_loghost, the hostname is correct already
Host = FSessionData->OrigHostName;
Port = FSessionData->OrigPortNumber;
}
}
//---------------------------------------------------------------------------
bool TSecureShell::HaveAcceptNewHostKeyPolicy()
{
return SameText(FSessionData->HostKey.Trim(), L"acceptnew");
}
//---------------------------------------------------------------------------
THierarchicalStorage * TSecureShell::GetHostKeyStorage()
{
if (!Configuration->Persistent && HaveAcceptNewHostKeyPolicy())
{
return Configuration->CreateConfigRegistryStorage();
}
else
{
return Configuration->CreateConfigStorage();
}
}
//---------------------------------------------------------------------------
UnicodeString __fastcall TSecureShell::RetrieveHostKey(const UnicodeString & Host, int Port, const UnicodeString & KeyType)
{
std::unique_ptr<THierarchicalStorage> Storage(GetHostKeyStorage());
Storage->AccessMode = smRead;
TGuard Guard(PuttyStorageSection.get());
DebugAssert(PuttyStorage == NULL);
TValueRestorer<THierarchicalStorage *> StorageRestorer(PuttyStorage);
PuttyStorage = Storage.get();
AnsiString AnsiStoredKeys;
AnsiStoredKeys.SetLength(10240);
UnicodeString Result;
if (retrieve_host_key(AnsiString(Host).c_str(), Port, AnsiString(KeyType).c_str(),
AnsiStoredKeys.c_str(), AnsiStoredKeys.Length()) == 0)
{
PackStr(AnsiStoredKeys);
Result = UnicodeString(AnsiStoredKeys);
}
return Result;
}
//---------------------------------------------------------------------------
static bool DoVerifyFingerprint(const UnicodeString & AFingerprintFromUser, const UnicodeString & AFingerprintFromHost, bool Base64)
{
UnicodeString FingerprintFromUser = AFingerprintFromUser;
UnicodeString FingerprintFromHost = AFingerprintFromHost;
UnicodeString FingerprintFromUserName, FingerprintFromHostName;
NormalizeFingerprint(FingerprintFromUser, FingerprintFromUserName);
NormalizeFingerprint(FingerprintFromHost, FingerprintFromHostName);
bool Result;
if (DebugAlwaysFalse(FingerprintFromHostName.IsEmpty()))
{
Result = false;
}
else
{
// In all below three formats, the checksum can be any of these formats:
// MD5 (case insensitive):
// xx:xx:xx
// xx-xx-xx
// SHA-256 (case sensitive):
// xxxx+xx/xxx=
// xxxx+xx/xxx
// xxxx-xx_xxx=
// xxxx-xx_xxx
// Full fingerprint format "type bits checksum"
if (!FingerprintFromUserName.IsEmpty())
{
Result =
SameText(FingerprintFromUserName, FingerprintFromHostName) &&
SameChecksum(FingerprintFromUser, FingerprintFromHost, Base64);
}
else
{
// normalized format "type-checksum"
UnicodeString NormalizedPrefix = FingerprintFromHost + NormalizedFingerprintSeparator;
if (StartsText(NormalizedPrefix, FingerprintFromUser))
{
FingerprintFromUser.Delete(1, NormalizedPrefix.Length());
Result = SameChecksum(FingerprintFromUser, FingerprintFromHost, Base64);
}
else
{
// last resort: "checksum" only
Result = SameChecksum(FingerprintFromUser, FingerprintFromHost, Base64);
}
}
}
return Result;
}
//---------------------------------------------------------------------------
static bool VerifyFingerprint(
const UnicodeString & FingerprintFromUser, const UnicodeString & FingerprintFromHostMD5, const UnicodeString & FingerprintFromHostSHA256)
{
return
DoVerifyFingerprint(FingerprintFromUser, FingerprintFromHostMD5, false) ||
DoVerifyFingerprint(FingerprintFromUser, FingerprintFromHostSHA256, true);
}
//---------------------------------------------------------------------------
struct TPasteKeyHandler
{
UnicodeString KeyStr;
UnicodeString FingerprintMD5;
UnicodeString FingerprintSHA256;
TSessionUI * UI;
void __fastcall Paste(TObject * Sender, unsigned int & Answer);
};
//---------------------------------------------------------------------------
void __fastcall TPasteKeyHandler::Paste(TObject * /*Sender*/, unsigned int & Answer)
{
UnicodeString ClipboardText;
if (TextFromClipboard(ClipboardText, true))
{
if (VerifyFingerprint(ClipboardText, FingerprintMD5, FingerprintSHA256) ||
SameText(ClipboardText, KeyStr))
{
Answer = qaYes;
}
else
{
const struct ssh_keyalg * Algorithm;
try
{
UnicodeString Key = ParseOpenSshPubLine(ClipboardText, Algorithm);
if (Key == KeyStr)
{
Answer = qaYes;
}
}
catch (...)
{
// swallow
}
}
}
if (Answer == 0)
{
UI->QueryUser(LoadStr(HOSTKEY_NOT_MATCH_CLIPBOARD), NULL, qaOK, NULL, qtError);
}
}
//---------------------------------------------------------------------------
bool TSecureShell::VerifyCachedHostKey(
const UnicodeString & StoredKeys, const UnicodeString & KeyStr, const UnicodeString & FingerprintMD5, const UnicodeString & FingerprintSHA256)
{
bool Result = false;
UnicodeString Buf = StoredKeys;
while (!Result && !Buf.IsEmpty())
{
UnicodeString StoredKey = CutToChar(Buf, HostKeyDelimiter, false);
// skip leading ECDH subtype identification
int P = StoredKey.Pos(L",");
// Start from beginning or after the comma, if there's any.
// If it does not start with 0x, it's probably a fingerprint (stored by TSessionData::CacheHostKey).
bool Fingerprint = (StoredKey.SubString(P + 1, 2) != L"0x");
if (!Fingerprint && (StoredKey == KeyStr))
{
LogEvent(L"Host key matches cached key");
Result = true;
}
else if (Fingerprint && VerifyFingerprint(StoredKey, FingerprintMD5, FingerprintSHA256))
{
LogEvent(L"Host key matches cached key fingerprint");
Result = true;
}
else
{
if (Configuration->ActualLogProtocol >= 1)
{
UnicodeString FormattedKey = Fingerprint ? StoredKey : FormatKeyStr(StoredKey);
LogEvent(FORMAT(L"Host key does not match cached key %s", (FormattedKey)));
}
else
{
LogEvent(L"Host key does not match cached key");
}
}
}
return Result;
}
//---------------------------------------------------------------------------
UnicodeString TSecureShell::StoreHostKey(
const UnicodeString & Host, int Port, const UnicodeString & KeyType, const UnicodeString & KeyStr)
{
TGuard Guard(PuttyStorageSection.get());
DebugAssert(PuttyStorage == NULL);
TValueRestorer<THierarchicalStorage *> StorageRestorer(PuttyStorage);
std::unique_ptr<THierarchicalStorage> Storage(GetHostKeyStorage());
Storage->AccessMode = smReadWrite;
PuttyStorage = Storage.get();
store_host_key(AnsiString(Host).c_str(), Port, AnsiString(KeyType).c_str(), AnsiString(KeyStr).c_str());
return Storage->Source;
}
//---------------------------------------------------------------------------
void TSecureShell::ParseFingerprint(const UnicodeString & Fingerprint, UnicodeString & SignKeyType, UnicodeString & Hash)
{
UnicodeString Buf = Fingerprint;
UnicodeString SignKeyAlg = CutToChar(Buf, L' ', false);
UnicodeString SignKeySize = CutToChar(Buf, L' ', false);
SignKeyType = SignKeyAlg + L' ' + SignKeySize;
Hash = Buf;
}
//---------------------------------------------------------------------------
void __fastcall TSecureShell::VerifyHostKey(
const UnicodeString & AHost, int Port, const UnicodeString & KeyType, const UnicodeString & KeyStr,
const UnicodeString & AFingerprintSHA256, const UnicodeString & AFingerprintMD5)
{
if (Configuration->ActualLogProtocol >= 1)
{
LogEvent(FORMAT(L"Verifying host key %s %s with fingerprints %s, %s", (KeyType, FormatKeyStr(KeyStr), AFingerprintSHA256, AFingerprintMD5)));
}
GotHostKey();
DebugAssert(KeyStr.Pos(HostKeyDelimiter) == 0);
UnicodeString Host = AHost;
GetRealHost(Host, Port);
UnicodeString SignKeyType, MD5, SHA256;
ParseFingerprint(AFingerprintMD5, SignKeyType, MD5);
DebugAssert(get_ssh_version(FBackendHandle) == 2);
UnicodeString SignKeyTypeSHA256;
ParseFingerprint(AFingerprintSHA256, SignKeyTypeSHA256, SHA256);
DebugAssert(SignKeyTypeSHA256 == SignKeyType);
if (DebugAlwaysTrue(StartsText(L"SHA256:", SHA256)))
{
CutToChar(SHA256, L':', false);
}
UnicodeString FingerprintMD5 = SignKeyType + L' ' + MD5;
DebugAssert(AFingerprintMD5 == FingerprintMD5);
UnicodeString FingerprintSHA256 = SignKeyType + L' ' + SHA256;
DebugAssert(ReplaceStr(AFingerprintSHA256, L"SHA256:", EmptyStr) == FingerprintSHA256);
FSessionInfo.HostKeyFingerprintSHA256 = FingerprintSHA256;
FSessionInfo.HostKeyFingerprintMD5 = FingerprintMD5;
if (FSessionData->FingerprintScan)
{
Abort();
}
bool AcceptNew = HaveAcceptNewHostKeyPolicy();
UnicodeString ConfigHostKey;
if (!AcceptNew)
{
ConfigHostKey = FSessionData->HostKey;
}
UnicodeString StoredKeys = RetrieveHostKey(Host, Port, KeyType);
bool Result = VerifyCachedHostKey(StoredKeys, KeyStr, FingerprintMD5, FingerprintSHA256);
if (!Result && AcceptNew)
{
if (!StoredKeys.IsEmpty()) // optimization + avoiding the log message
{
AcceptNew = false;
}
else if (have_any_ssh2_hostkey(FSeat, AnsiString(Host).c_str(), Port))
{
LogEvent(L"Host key not found in the cache, but other key types found, cannot accept new key");
AcceptNew = false;
}
}
bool ConfiguredKeyNotMatch = false;
if (!Result && !ConfigHostKey.IsEmpty() &&
// Should test have_any_ssh2_hostkey + No need to bother with AcceptNew, as we never get here
(StoredKeys.IsEmpty() || FSessionData->OverrideCachedHostKey))
{
UnicodeString Buf = ConfigHostKey;
while (!Result && !Buf.IsEmpty())
{
UnicodeString ExpectedKey = CutToChar(Buf, HostKeyDelimiter, false);
if (ExpectedKey == L"*")
{
UnicodeString Message = LoadStr(ANY_HOSTKEY);
FUI->Information(Message, true);
FLog->Add(llException, Message);
Result = true;
}
else if (VerifyFingerprint(ExpectedKey, FingerprintMD5, FingerprintSHA256))
{
LogEvent(L"Host key matches configured key fingerprint");
Result = true;
}
else
{
LogEvent(FORMAT(L"Host key does not match configured key fingerprint %s", (ExpectedKey)));
}
}
if (!Result)
{
ConfiguredKeyNotMatch = true;
}
}
if (!Result && AcceptNew && DebugAlwaysTrue(ConfigHostKey.IsEmpty()))
{
try
{
UnicodeString StorageSource = StoreHostKey(Host, Port, KeyType, KeyStr);
UnicodeString StoredKeys = RetrieveHostKey(Host, Port, KeyType);
if (StoredKeys != KeyStr)
{
throw Exception(UnicodeString());
}
Configuration->Usage->Inc(L"HostKeyNewAccepted");
LogEvent(FORMAT(L"Warning: Stored new host key to %s - This should occur only on the first connection", (StorageSource)));
Result = true;
}
catch (Exception & E)
{
FUI->FatalError(&E, LoadStr(STORE_NEW_HOSTKEY_ERROR));
}
}
if (!Result)
{
bool Verified;
if (ConfiguredKeyNotMatch || Configuration->DisableAcceptingHostKeys)
{
Verified = false;
}
// no point offering manual verification, if we cannot persist the verified key
else if (!Configuration->Persistent && Configuration->Scripting)
{
Verified = false;
}
else
{
// We should not offer caching if !Configuration->Persistent,
// but as scripting mode is handled earlier and in GUI it hardly happens,
// it's a small issue.
TClipboardHandler ClipboardHandler;
ClipboardHandler.Text = FingerprintSHA256 + L"\n" + FingerprintMD5;
TPasteKeyHandler PasteKeyHandler;
PasteKeyHandler.KeyStr = KeyStr;
PasteKeyHandler.FingerprintMD5 = FingerprintMD5;
PasteKeyHandler.FingerprintSHA256 = FingerprintSHA256;
PasteKeyHandler.UI = FUI;
bool Unknown = StoredKeys.IsEmpty();
int Answers;
int AliasesCount;
TQueryButtonAlias Aliases[4];
Aliases[0].Button = qaRetry;
Aliases[0].Alias = LoadStr(COPY_KEY_BUTTON);
Aliases[0].ActionAlias = LoadStr(COPY_KEY_ACTION);
Aliases[0].OnSubmit = &ClipboardHandler.Copy;
Aliases[1].Button = qaIgnore;
Aliases[1].Alias = LoadStr(PASTE_KEY_BUTTON);
Aliases[1].OnSubmit = &PasteKeyHandler.Paste;
Aliases[1].GroupWith = qaYes;
Answers = qaYes | qaCancel | qaRetry | qaIgnore;
AliasesCount = 2;
if (!Unknown)
{
Aliases[2].Button = qaYes;
Aliases[2].Alias = LoadStr(UPDATE_KEY_BUTTON);
Aliases[3].Button = qaOK;
Aliases[3].Alias = LoadStr(ADD_KEY_BUTTON);
AliasesCount += 2;
Answers |= qaSkip | qaOK;
}
else
{
Answers |= qaNo;
}
TQueryParams Params(qpWaitInBatch);
Params.NoBatchAnswers = qaYes | qaRetry | qaSkip | qaOK;
Params.HelpKeyword = (Unknown ? HELP_UNKNOWN_KEY : HELP_DIFFERENT_KEY);
Params.Aliases = Aliases;
Params.AliasesCount = AliasesCount;
UnicodeString KeyTypeHuman = GetKeyTypeHuman(KeyType);
UnicodeString KeyDetails = FMTLOAD(KEY_DETAILS, (SignKeyType, SHA256, MD5));
UnicodeString Message = FMTLOAD((Unknown ? UNKNOWN_KEY4 : DIFFERENT_KEY5), (KeyTypeHuman, KeyDetails));
if (Configuration->Scripting)
{
AddToList(Message, LoadStr(SCRIPTING_USE_HOSTKEY), L"\n");
}
unsigned int R =
FUI->QueryUser(Message, NULL, Answers, &Params, qtWarning);
UnicodeString StoreKeyStr = KeyStr;
switch (R) {
case qaOK:
DebugAssert(!Unknown);
StoreKeyStr = (StoredKeys + HostKeyDelimiter + StoreKeyStr);
// fall thru
case qaYes:
StoreHostKey(Host, Port, KeyType, StoreKeyStr);
Verified = true;
break;
case qaCancel:
Verified = false;
break;
default:
Verified = true;
break;
}
}
if (!Verified)
{
Configuration->Usage->Inc(L"HostNotVerified");
UnicodeString Message;
if (ConfiguredKeyNotMatch)
{
Message = FMTLOAD(CONFIGURED_KEY_NOT_MATCH, (ConfigHostKey));
}
else if (!Configuration->Persistent && Configuration->Scripting)
{
Message = LoadStr(HOSTKEY_NOT_CONFIGURED);
}
else
{
Message = LoadStr(KEY_NOT_VERIFIED);
}
Exception * E = new Exception(MainInstructions(Message));
try
{
FUI->FatalError(E, FMTLOAD(HOSTKEY, (FingerprintSHA256)));
}
__finally
{
delete E;
}
}
}
Configuration->RememberLastFingerprint(FSessionData->SiteKey, SshFingerprintType, FingerprintSHA256);
}
//---------------------------------------------------------------------------
bool __fastcall TSecureShell::HaveHostKey(UnicodeString Host, int Port, const UnicodeString KeyType)
{
// Return true, if we have any host key fingerprint of a particular type
GetRealHost(Host, Port);
UnicodeString StoredKeys = RetrieveHostKey(Host, Port, KeyType);
bool Result = !StoredKeys.IsEmpty();
if (!FSessionData->HostKey.IsEmpty())
{
UnicodeString Buf = FSessionData->HostKey;
while (!Result && !Buf.IsEmpty())
{
UnicodeString ExpectedKey = CutToChar(Buf, HostKeyDelimiter, false);
UnicodeString ExpectedKeyType = KeyTypeFromFingerprint(ExpectedKey);
Result = SameText(ExpectedKeyType, KeyType);
}
}
if (Result)
{
LogEvent(FORMAT(L"Have a known host key of type %s", (KeyType)));
}
return Result;
}
//---------------------------------------------------------------------------
void __fastcall TSecureShell::AskAlg(UnicodeString AlgType, UnicodeString AlgName)
{
// beware of changing order
static const TPuttyTranslation AlgTranslation[] = {
{ L"cipher", CIPHER_TYPE_BOTH2 },
{ L"client-to-server cipher", CIPHER_TYPE_CS2 },
{ L"server-to-client cipher", CIPHER_TYPE_SC2 },
{ L"key-exchange algorithm", KEY_EXCHANGE_ALG },
{ L"hostkey type", KEYKEY_TYPE },
};
TranslatePuttyMessage(AlgTranslation, LENOF(AlgTranslation), AlgType);
UnicodeString Msg = FMTLOAD(ALG_BELOW_TRESHOLD, (AlgType, AlgName));
if (FUI->QueryUser(Msg, NULL, qaYes | qaNo, NULL, qtWarning) == qaNo)
{
UnicodeString Error = FMTLOAD(ALG_NOT_VERIFIED, (AlgType, AlgName));
FUI->FatalError(NULL, Error);
}
}
//---------------------------------------------------------------------------
void __fastcall TSecureShell::DisplayBanner(const UnicodeString & Banner)
{
FUI->DisplayBanner(Banner);
}
//---------------------------------------------------------------------------
void __fastcall TSecureShell::OldKeyfileWarning()
{
// actually never called, see Net.cpp
FUI->QueryUser(LoadStr(OLD_KEY), NULL, qaOK, NULL, qtWarning);
}
//---------------------------------------------------------------------------
bool __fastcall TSecureShell::GetStoredCredentialsTried()
{
return FStoredPasswordTried || FStoredPasswordTriedForKI || FStoredPassphraseTried;
}
//---------------------------------------------------------------------------
bool __fastcall TSecureShell::GetReady()
{
return FOpened && (FWaiting == 0);
}
//---------------------------------------------------------------------------
void __fastcall TSecureShell::CollectUsage()
{
if (FCollectPrivateKeyUsage)
{
Configuration->Usage->Inc(L"OpenedSessionsPrivateKey2");
}
Configuration->Usage->Inc(L"OpenedSessionsSSH2");
if (SshImplementation == sshiOpenSSH)
{
Configuration->Usage->Inc(L"OpenedSessionsSSHOpenSSH");
}
else if (SshImplementation == sshiProFTPD)
{
Configuration->Usage->Inc(L"OpenedSessionsSSHProFTPD");
}
else if (SshImplementation == sshiBitvise)
{
Configuration->Usage->Inc(L"OpenedSessionsSSHBitvise");
}
else if (SshImplementation == sshiTitan)
{
Configuration->Usage->Inc(L"OpenedSessionsSSHTitan");
}
else if (SshImplementation == sshiOpenVMS)
{
Configuration->Usage->Inc(L"OpenedSessionsSSHOpenVMS");
}
else if (ContainsText(FSessionInfo.SshImplementation, L"Serv-U"))
{
Configuration->Usage->Inc(L"OpenedSessionsSSHServU");
}
else if (SshImplementation == sshiCerberus)
{
// Ntb, Cerberus can also be detected using vendor-id extension
// Cerberus FTP Server 7.0.5.3 (70005003) by Cerberus, LLC
Configuration->Usage->Inc(L"OpenedSessionsSSHCerberus");
}
else if (ContainsText(FSessionInfo.SshImplementation, L"WS_FTP"))
{
Configuration->Usage->Inc(L"OpenedSessionsSSHWSFTP");
}
// SSH-2.0-1.36_sshlib GlobalSCAPE
else if (ContainsText(FSessionInfo.SshImplementation, L"GlobalSCAPE"))
{
Configuration->Usage->Inc(L"OpenedSessionsSSHGlobalScape");
}
// SSH-2.0-CompleteFTP-8.1.3
else if (ContainsText(FSessionInfo.SshImplementation, L"CompleteFTP"))
{
Configuration->Usage->Inc(L"OpenedSessionsSSHComplete");
}
// SSH-2.0-CoreFTP-0.3.3
else if (ContainsText(FSessionInfo.SshImplementation, L"CoreFTP"))
{
Configuration->Usage->Inc(L"OpenedSessionsSSHCore");
}
// SSH-2.0-SSHD-CORE-0.11.0 (value is configurable, this is a default)
// (Apache Mina SSHD, e.g. on brickftp.com)
else if (ContainsText(FSessionInfo.SshImplementation, L"SSHD-CORE"))
{
Configuration->Usage->Inc(L"OpenedSessionsSSHApache");
}
// SSH-2.0-Syncplify_Me_Server
else if (ContainsText(FSessionInfo.SshImplementation, L"Syncplify"))
{
Configuration->Usage->Inc(L"OpenedSessionsSSHSyncplify");
}
else if (ContainsText(FSessionInfo.SshImplementation, L"zFTPServer"))
{
Configuration->Usage->Inc(L"OpenedSessionsSSHzFTP");
}
else
{
Configuration->Usage->Inc(L"OpenedSessionsSSHOther");
}
}
//---------------------------------------------------------------------------
bool __fastcall TSecureShell::CanChangePassword()
{
return
// These major SSH servers explicitly do not support password change.
(SshImplementation != sshiOpenSSH) && // See userauth_passwd
(SshImplementation != sshiProFTPD); // See sftp_auth_password
}