mirror of
https://github.com/winscp/winscp.git
synced 2026-10-24 02:28:55 +03:00
3023 lines
96 KiB
C++
3023 lines
96 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>
|
|
#include <Consts.hpp>
|
|
|
|
#ifndef AUTO_WINSOCK
|
|
#include <winsock2.h>
|
|
#endif
|
|
#include <ws2ipdef.h>
|
|
//---------------------------------------------------------------------------
|
|
#pragma package(smart_init)
|
|
//---------------------------------------------------------------------------
|
|
#define MAX_BUFSIZE 32768
|
|
//---------------------------------------------------------------------------
|
|
const wchar_t HostKeyDelimiter = L';';
|
|
//---------------------------------------------------------------------------
|
|
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 = new callback_set();
|
|
memset(FCallbackSet, 0, sizeof(*FCallbackSet));
|
|
FCallbackSet->ready_event = INVALID_HANDLE_VALUE;
|
|
Mode = ssmNone;
|
|
}
|
|
//---------------------------------------------------------------------------
|
|
__fastcall TSecureShell::~TSecureShell()
|
|
{
|
|
DebugAssert(FWaiting == 0);
|
|
if (Active)
|
|
{
|
|
Close();
|
|
}
|
|
ResetConnection();
|
|
CloseHandle(FSocketEvent);
|
|
delete FCallbackSet;
|
|
}
|
|
//---------------------------------------------------------------------------
|
|
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;
|
|
FAuthenticationCancelled = false;
|
|
delete FLogPolicy;
|
|
FLogPolicy = NULL;
|
|
delete FSeat;
|
|
FSeat = NULL;
|
|
if (FLogCtx != NULL)
|
|
{
|
|
log_free(FLogCtx);
|
|
}
|
|
FLogCtx = NULL;
|
|
FClosed = false;
|
|
FLoggedKnownHostKeys.clear();
|
|
}
|
|
//---------------------------------------------------------------------------
|
|
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;
|
|
// Update also CollectUsage
|
|
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;
|
|
case cipAESGCM: pcipher = CIPHER_AESGCM; 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 kexDHGroup15: pkex = KEX_DHGROUP15; break;
|
|
case kexDHGroup16: pkex = KEX_DHGROUP16; break;
|
|
case kexDHGroup17: pkex = KEX_DHGROUP17; break;
|
|
case kexDHGroup18: pkex = KEX_DHGROUP18; break;
|
|
case kexDHGEx: pkex = KEX_DHGEX; break;
|
|
case kexRSA: pkex = KEX_RSA; break;
|
|
case kexECDH: pkex = KEX_ECDH; break;
|
|
case kexNTRUHybrid: pkex = KEX_NTRU_HYBRID; break;
|
|
case kexMLKEM25519Hybrid: pkex = KEX_MLKEM_25519_HYBRID; break;
|
|
case kexMLKEMNISTHybrid: pkex = KEX_MLKEM_NIST_HYBRID; 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 = HostKeyToPutty(Data->HostKeys[h]);
|
|
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_utf8(UTF8String(Data->GssLibCustom).c_str());
|
|
conf_set_filename(conf, CONF_ssh_gss_custom, GssLibCustomFileName);
|
|
filename_free(GssLibCustomFileName);
|
|
|
|
Filename * AFileName = filename_from_utf8(UTF8String(Data->ResolvePublicKeyFile()).c_str());
|
|
conf_set_filename(conf, CONF_keyfile, AFileName);
|
|
filename_free(AFileName);
|
|
|
|
AFileName = filename_from_utf8(UTF8String(ExpandEnvironmentVariables(Data->DetachedCertificate)).c_str());
|
|
conf_set_filename(conf, CONF_detached_cert, AFileName);
|
|
filename_free(AFileName);
|
|
|
|
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;
|
|
|
|
FActive = 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);
|
|
FAuthenticationCancelled = false;
|
|
if (!Active && DebugAlwaysTrue(HasLocalProxy()))
|
|
{
|
|
FActive = 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;
|
|
}
|
|
//---------------------------------------------------------------------------
|
|
UnicodeString __fastcall TSecureShell::ConvertFromPutty(const char * Str, int Length)
|
|
{
|
|
int BomLength = strlen(WINSCP_BOM);
|
|
if ((Length >= BomLength) &&
|
|
(strncmp(Str, WINSCP_BOM, BomLength) == 0))
|
|
{
|
|
return UTF8ArrayToString(Str + BomLength, Length - BomLength - 1);
|
|
}
|
|
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));
|
|
// 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 },
|
|
{ L"SOCKS proxy authentication", PROXY_AUTH_TITLE },
|
|
{ L"HTTP proxy authentication", PROXY_AUTH_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 if ((Index == 8) || (Index == 9))
|
|
{
|
|
PromptKind = pkProxyAuth;
|
|
}
|
|
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 if (PromptKind == pkProxyAuth)
|
|
{
|
|
static const TPuttyTranslation ProxyAuthPromptTranslation[] = {
|
|
{ L"Proxy username: ", PROXY_AUTH_USERNAME_PROMPT },
|
|
{ L"Proxy password: ", PROXY_AUTH_PASSWORD_PROMPT },
|
|
};
|
|
PromptTranslation = ProxyAuthPromptTranslation;
|
|
PromptTranslationCount = LENOF(ProxyAuthPromptTranslation);
|
|
PromptDesc = L"proxy authentication";
|
|
}
|
|
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.");
|
|
FAuthenticationCancelled = true;
|
|
}
|
|
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, bool Sending)
|
|
{
|
|
UnicodeString CounterName;
|
|
if (Mode == ssmUploading)
|
|
{
|
|
CounterName = Sending ? L"TimeoutUploadingSending" : L"TimeoutUploadingReceiving";
|
|
}
|
|
else if (Mode == ssmDownloading)
|
|
{
|
|
CounterName = Sending ? L"TimeoutDownloadingSending" : L"TimeoutDownloadingReceiving";
|
|
}
|
|
if (!CounterName.IsEmpty())
|
|
{
|
|
Configuration->Usage->Inc(CounterName);
|
|
}
|
|
|
|
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, true);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
while (BufSize > MAX_BUFSIZE);
|
|
}
|
|
//---------------------------------------------------------------------------
|
|
void __fastcall TSecureShell::Send(const unsigned char * Buf, Integer Len)
|
|
{
|
|
CheckConnection();
|
|
backend_send(FBackendHandle, const_cast<char *>(reinterpret_cast<const char *>(Buf)), Len);
|
|
int BufSize = backend_sendbuffer(FBackendHandle);
|
|
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())));
|
|
}
|
|
}
|
|
}
|
|
//---------------------------------------------------------------------------
|
|
bool TSecureShell::HasLocalProxy()
|
|
{
|
|
return (FSessionData->ProxyMethod == pmCmd);
|
|
}
|
|
//---------------------------------------------------------------------------
|
|
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(HasLocalProxy());
|
|
}
|
|
|
|
if (Enable)
|
|
{
|
|
FSocket = value;
|
|
FActive = true;
|
|
}
|
|
else
|
|
{
|
|
FSocket = INVALID_SOCKET;
|
|
Discard();
|
|
}
|
|
}
|
|
}
|
|
//---------------------------------------------------------------------------
|
|
void __fastcall TSecureShell::UpdatePortFwdSocket(SOCKET value, bool Enable)
|
|
{
|
|
if (value != INVALID_SOCKET)
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
DebugAssert(FSessionData->AgentFwd);
|
|
}
|
|
}
|
|
//---------------------------------------------------------------------------
|
|
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, FCallbackSet->ic_pktin_free))
|
|
{
|
|
// This releases the callback and should be noop otherwise.
|
|
run_toplevel_callbacks(FCallbackSet);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
if (FCallbackSet->handlewaits_tree_real != NULL)
|
|
{
|
|
DebugAssert(count234(FCallbackSet->handlewaits_tree_real) <= 1);
|
|
while (count234(FCallbackSet->handlewaits_tree_real) > 0)
|
|
{
|
|
HandleWait * AHandleWait = static_cast<HandleWait *>(index234(FCallbackSet->handlewaits_tree_real, 0));
|
|
delete_handle_wait(FCallbackSet, AHandleWait);
|
|
}
|
|
|
|
freetree234(FCallbackSet->handlewaits_tree_real);
|
|
FCallbackSet->handlewaits_tree_real = NULL;
|
|
}
|
|
|
|
if (FCallbackSet->ready_event != INVALID_HANDLE_VALUE)
|
|
{
|
|
CloseHandle(FCallbackSet->ready_event);
|
|
FCallbackSet->ready_event = INVALID_HANDLE_VALUE;
|
|
}
|
|
}
|
|
}
|
|
//---------------------------------------------------------------------------
|
|
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;
|
|
|
|
int ExitCode = backend_exitcode(FBackendHandle);
|
|
if (Message >= 0)
|
|
{
|
|
Str = LoadStr(Message);
|
|
}
|
|
else
|
|
{
|
|
if ((ExitCode == 0) && FAuthenticationCancelled)
|
|
{
|
|
// This should be improved to check if the prompt for specific credential
|
|
// was cancelled after it was unsuccessfully answered before
|
|
if (GetStoredCredentialsTried())
|
|
{
|
|
Str = LoadStr(AUTHENTICATION_FAILED);
|
|
}
|
|
else
|
|
{
|
|
Str = LoadStr(CREDENTIALS_NOT_SPECIFIED);
|
|
}
|
|
// The 0 code is not coming from the server
|
|
ExitCode = -1;
|
|
}
|
|
else
|
|
{
|
|
Str = LoadStr(NOT_CONNECTED);
|
|
HelpKeyword = HELP_NOT_CONNECTED;
|
|
}
|
|
}
|
|
|
|
Str = MainInstructions(Str);
|
|
|
|
if (ExitCode >= 0)
|
|
{
|
|
Str += L" " + FMTLOAD(SSH_EXITCODE, (ExitCode));
|
|
}
|
|
if (!FClosed)
|
|
{
|
|
FatalError(Str, HelpKeyword);
|
|
}
|
|
else
|
|
{
|
|
LogEvent(FORMAT(L"Ignoring closed connection: %s", (Str)));
|
|
}
|
|
}
|
|
}
|
|
//---------------------------------------------------------------------------
|
|
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, false);
|
|
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)
|
|
{
|
|
bool Result = false;
|
|
|
|
do
|
|
{
|
|
if (Configuration->ActualLogProtocol >= 2)
|
|
{
|
|
LogEvent(L"Looking for network events");
|
|
}
|
|
unsigned int TicksBefore = GetTickCount();
|
|
HandleWaitList * WaitList = 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 (WaitList != NULL)
|
|
{
|
|
handle_wait_list_free(WaitList);
|
|
}
|
|
// It returns only busy handles, so the set can change with every call to run_toplevel_callbacks.
|
|
WaitList = get_handle_wait_list(FCallbackSet);
|
|
DebugAssert(WaitList->nhandles < MAXIMUM_WAIT_OBJECTS);
|
|
WaitList->handles[WaitList->nhandles] = FSocketEvent;
|
|
WaitResult = WaitForMultipleObjects(WaitList->nhandles + 1, WaitList->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 + WaitList->nhandles)
|
|
{
|
|
if (handle_wait_activate(FCallbackSet, WaitList, WaitResult - WAIT_OBJECT_0))
|
|
{
|
|
Result = true;
|
|
}
|
|
}
|
|
else if (WaitResult == WAIT_OBJECT_0 + WaitList->nhandles)
|
|
{
|
|
if (Configuration->ActualLogProtocol >= 1)
|
|
{
|
|
LogEvent(L"Detected network event");
|
|
}
|
|
|
|
DebugAssert(FSocket != INVALID_SOCKET);
|
|
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
|
|
{
|
|
if (WaitList != NULL)
|
|
{
|
|
handle_wait_list_free(WaitList);
|
|
}
|
|
}
|
|
|
|
|
|
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 ((FSocket != INVALID_SOCKET) &&
|
|
(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, 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);
|
|
std::unique_ptr<THierarchicalStorage> Storage(GetHostKeyStorage());
|
|
Storage->AccessMode = smReadWrite;
|
|
TValueRestorer<THierarchicalStorage *> StorageRestorer(PuttyStorage, Storage.get());
|
|
store_host_key(FSeat, 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,
|
|
bool IsCertificate, int CACount, bool AlreadyVerified)
|
|
{
|
|
if (Configuration->ActualLogProtocol >= 1)
|
|
{
|
|
UnicodeString HostKeyAction = AlreadyVerified ? L"Got" : L"Verifying";
|
|
LogEvent(FORMAT(L"%s host key %s %s with fingerprints %s, %s", (HostKeyAction, 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();
|
|
}
|
|
|
|
if (!AlreadyVerified)
|
|
{
|
|
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();
|
|
|
|
UnicodeString AcceptButton = LoadStr(HOSTKEY_ACCEPT_BUTTON);
|
|
UnicodeString OnceButton = LoadStr(HOSTKEY_ONCE_BUTTON);
|
|
UnicodeString CancelButton = Vcl_Consts_SMsgDlgCancel;
|
|
UnicodeString UpdateButton = LoadStr(UPDATE_KEY_BUTTON);
|
|
UnicodeString AddButton = LoadStr(ADD_KEY_BUTTON);
|
|
int Answers;
|
|
std::vector<TQueryButtonAlias> Aliases;
|
|
|
|
TQueryButtonAlias CopyAlias;
|
|
CopyAlias.Button = qaRetry;
|
|
CopyAlias.Alias = LoadStr(COPY_KEY_BUTTON);
|
|
CopyAlias.ActionAlias = LoadStr(COPY_KEY_ACTION);
|
|
CopyAlias.OnSubmit = &ClipboardHandler.Copy;
|
|
Aliases.push_back(CopyAlias);
|
|
|
|
TQueryButtonAlias PasteAlias;
|
|
PasteAlias.Button = qaIgnore;
|
|
PasteAlias.Alias = LoadStr(PASTE_KEY_BUTTON);
|
|
PasteAlias.OnSubmit = &PasteKeyHandler.Paste;
|
|
PasteAlias.GroupWith = qaYes;
|
|
Aliases.push_back(PasteAlias);
|
|
|
|
TQueryButtonAlias OnceAlias;
|
|
OnceAlias.Button = qaOK;
|
|
OnceAlias.Alias = OnceButton;
|
|
OnceAlias.GroupWith = qaYes;
|
|
Aliases.push_back(OnceAlias);
|
|
|
|
Answers = qaYes | qaOK | qaCancel | qaRetry | qaIgnore;
|
|
if (!Unknown)
|
|
{
|
|
TQueryButtonAlias UpdateAlias;
|
|
UpdateAlias.Button = qaYes;
|
|
UpdateAlias.Alias = UpdateButton;
|
|
Aliases.push_back(UpdateAlias);
|
|
|
|
TQueryButtonAlias AddAlias;
|
|
AddAlias.Button = qaNo;
|
|
AddAlias.Alias = AddButton;
|
|
AddAlias.GroupWith = qaYes;
|
|
Aliases.push_back(AddAlias);
|
|
|
|
Answers |= qaNo;
|
|
}
|
|
else
|
|
{
|
|
TQueryButtonAlias AcceptAlias;
|
|
AcceptAlias.Button = qaYes;
|
|
AcceptAlias.Alias = AcceptButton;
|
|
Aliases.push_back(AcceptAlias);
|
|
}
|
|
|
|
TQueryParams Params(qpWaitInBatch);
|
|
Params.NoBatchAnswers = qaYes | qaNo | qaRetry | qaIgnore | qaOK;
|
|
Params.HelpKeyword = (Unknown ? HELP_UNKNOWN_KEY : HELP_DIFFERENT_KEY);
|
|
Params.Aliases = &Aliases[0];
|
|
Params.AliasesCount = Aliases.size();
|
|
|
|
UnicodeString NewLine = L"\n";
|
|
UnicodeString Para = NewLine + NewLine;
|
|
UnicodeString Message;
|
|
UnicodeString ServerPara = FMTLOAD(HOSTKEY_SERVER, (Host, Port)) + Para;
|
|
UnicodeString Nbsp = L"\xA0";
|
|
UnicodeString Indent = Nbsp + Nbsp + Nbsp + Nbsp;
|
|
UnicodeString FingerprintPara =
|
|
Indent + FMTLOAD(HOSTKEY_FINGERPRINT, (KeyType)) + NewLine +
|
|
Indent + ReplaceStr(FingerprintSHA256, L" ", Nbsp) + Para;
|
|
UnicodeString AdministratorChangerOrAnotherComputerExplanationPara =
|
|
FMTLOAD(HOSTKEY_TWO_EXPLANATIONS, (LoadStr(HOSTKEY_ADMINISTRATOR_CHANGED), LoadStr(HOSTKEY_ANOTHER_COMPUTER))) + Para;
|
|
UnicodeString CertifiedTrustLine;
|
|
if (IsCertificate)
|
|
{
|
|
Message =
|
|
MainInstructions(LoadStr(HOSTKEY_SECURITY_BREACH)) + Para +
|
|
LoadStr(HOSTKEY_CERTIFIED1) + NewLine +
|
|
ServerPara;
|
|
if (CACount > 0)
|
|
{
|
|
Message +=
|
|
LoadStr(HOSTKEY_CERTIFIED2) + Para;
|
|
if (!Unknown)
|
|
{
|
|
Message += LoadStr(HOSTKEY_CERTIFIED_DOESNT_MATCH_ALSO) + Para;
|
|
}
|
|
Message +=
|
|
FMTLOAD(HOSTKEY_TWO_EXPLANATIONS, (LoadStr(HOSTKEY_CERTIFIED_ANOTHER), LoadStr(HOSTKEY_ANOTHER_COMPUTER))) + Para;
|
|
}
|
|
else
|
|
{
|
|
Message +=
|
|
LoadStr(HOSTKEY_CERTIFIED_DOESNT_MATCH) + Para +
|
|
AdministratorChangerOrAnotherComputerExplanationPara;
|
|
}
|
|
|
|
CertifiedTrustLine = LoadStr(HOSTKEY_CERTIFIED_TRUST) + NewLine;
|
|
}
|
|
else if (Unknown)
|
|
{
|
|
Message =
|
|
MainInstructions(LoadStr(HOSTKEY_UNKNOWN)) + Para +
|
|
LoadStr(HOSTKEY_NOT_CACHED) + NewLine +
|
|
ServerPara +
|
|
LoadStr(HOSTKEY_NO_GUARANTEE) + Para;
|
|
}
|
|
else
|
|
{
|
|
Message =
|
|
MainInstructions(LoadStr(HOSTKEY_SECURITY_BREACH)) + Para +
|
|
LoadStr(HOSTKEY_DOESNT_MATCH) + NewLine +
|
|
ServerPara +
|
|
AdministratorChangerOrAnotherComputerExplanationPara;
|
|
}
|
|
|
|
Message += FingerprintPara;
|
|
|
|
if (Unknown)
|
|
{
|
|
Message +=
|
|
FMTLOAD(HOSTKEY_ACCEPT_NEW, (StripHotkey(AcceptButton))) + NewLine +
|
|
CertifiedTrustLine +
|
|
FMTLOAD(HOSTKEY_ONCE_NEW, (StripHotkey(OnceButton))) + NewLine +
|
|
FMTLOAD(HOSTKEY_CANCEL_NEW, (StripHotkey(CancelButton)));
|
|
}
|
|
else
|
|
{
|
|
Message +=
|
|
FMTLOAD(HOSTKEY_ACCEPT_CHANGE, (StripHotkey(UpdateButton), StripHotkey(AddButton))) + NewLine +
|
|
CertifiedTrustLine +
|
|
FMTLOAD(HOSTKEY_ONCE_CHANGE, (StripHotkey(OnceButton))) + NewLine +
|
|
FMTLOAD(HOSTKEY_CANCEL_CHANGE, (StripHotkey(CancelButton), StripHotkey(CancelButton)));
|
|
}
|
|
|
|
if (Configuration->Scripting)
|
|
{
|
|
AddToList(Message, LoadStr(SCRIPTING_USE_HOSTKEY), Para);
|
|
}
|
|
|
|
unsigned int R =
|
|
FUI->QueryUser(Message, NULL, Answers, &Params, qtWarning);
|
|
UnicodeString StoreKeyStr = KeyStr;
|
|
|
|
switch (R) {
|
|
case qaNo:
|
|
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 &&
|
|
(FLoggedKnownHostKeys.find(KeyType) == FLoggedKnownHostKeys.end()))
|
|
{
|
|
LogEvent(FORMAT(L"Have a known host key of type %s", (KeyType)));
|
|
// This is called multiple times for the same cached key since PuTTY 0.76 (support for rsa-sha2*?).
|
|
// Avoid repeated log entries.
|
|
FLoggedKnownHostKeys.insert(KeyType);
|
|
}
|
|
|
|
return Result;
|
|
}
|
|
//---------------------------------------------------------------------------
|
|
void TSecureShell::AskAlg(const UnicodeString & AAlgType, const UnicodeString & AlgName, int WeakCryptoReason)
|
|
{
|
|
// 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 },
|
|
};
|
|
|
|
UnicodeString AlgType = AAlgType;
|
|
TranslatePuttyMessage(AlgTranslation, LENOF(AlgTranslation), AlgType);
|
|
|
|
UnicodeString Msg;
|
|
UnicodeString NewLine = UnicodeString(sLineBreak);
|
|
switch (WeakCryptoReason)
|
|
{
|
|
default:
|
|
DebugFail();
|
|
case 0:
|
|
Msg = FMTLOAD(ALG_BELOW_TRESHOLD2, (AlgType, AlgName));
|
|
break;
|
|
|
|
case 1:
|
|
case 2:
|
|
Msg = FMTLOAD(CIPHER_TERRAPIN, (AlgType, AlgName));
|
|
if (WeakCryptoReason == 2)
|
|
{
|
|
Msg += NewLine + NewLine + FMTLOAD(CIPHER_TERRAPIN_AVOID, (AlgName));
|
|
}
|
|
break;
|
|
}
|
|
|
|
Msg =
|
|
MainInstructions(LoadStr(WEAK_ALG_TITLE)) + NewLine + NewLine +
|
|
Msg + NewLine + NewLine +
|
|
LoadStr(WEAK_ALG_RISK_CONFIRM);
|
|
|
|
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)
|
|
{
|
|
// Since 0.77 PuTTY calls this again with CRLF if the actual banner does not end with one.
|
|
if (!Banner.Trim().IsEmpty())
|
|
{
|
|
FUI->DisplayBanner(Banner);
|
|
}
|
|
}
|
|
//---------------------------------------------------------------------------
|
|
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");
|
|
}
|
|
|
|
int CipherGroup = GetCipherGroup(get_cscipher(FBackendHandle));
|
|
switch (CipherGroup)
|
|
{
|
|
case CIPHER_3DES: Configuration->Usage->Inc(L"OpenedSessionsSSHCipher3DES"); break;
|
|
case CIPHER_BLOWFISH: Configuration->Usage->Inc(L"OpenedSessionsSSHCipherBlowfish"); break;
|
|
case CIPHER_AES: Configuration->Usage->Inc(L"OpenedSessionsSSHCipherAES"); break;
|
|
// All following miss "Cipher"
|
|
case CIPHER_DES: Configuration->Usage->Inc(L"OpenedSessionsSSHDES"); break;
|
|
case CIPHER_ARCFOUR: Configuration->Usage->Inc(L"OpenedSessionsSSHArcfour"); break;
|
|
case CIPHER_CHACHA20: Configuration->Usage->Inc(L"OpenedSessionsSSHChaCha20"); break;
|
|
case CIPHER_AESGCM: Configuration->Usage->Inc(L"OpenedSessionsSSHAESGCM"); break;
|
|
default: DebugFail(); break;
|
|
}
|
|
}
|
|
//---------------------------------------------------------------------------
|
|
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
|
|
}
|