Files
2025-12-10 14:38:26 -08:00

2210 lines
106 KiB
C++

// *****************************************************************************
// * This file is part of the FreeFileSync project. It is distributed under *
// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 *
// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved *
// *****************************************************************************
#include "sftp.h"
#include <array>
#include <zen/sys_error.h>
#include <zen/thread.h>
#include <zen/globals.h>
#include <zen/file_io.h>
#include <zen/socket.h>
#include <zen/open_ssl.h>
#include <zen/resolve_path.h>
#include <libssh2/libssh2_wrap.h> //DON'T include <libssh2_sftp.h> directly!
#include "init_curl_libssh2.h"
#include "ftp_common.h"
#include "abstract_impl.h"
#include <poll.h>
using namespace zen;
using namespace fff;
using AFS = AbstractFileSystem;
namespace
{
/*
SFTP specification version 3 (implemented by libssh2): https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt
libssh2: prefer OpenSSL over WinCNG backend:
WinCNG supports the following ciphers:
rijndael-cbc@lysator.liu.se
aes256-cbc
aes192-cbc
aes128-cbc
arcfour128
arcfour
3des-cbc
OpenSSL supports the same ciphers like WinCNG plus the following:
aes256-ctr
aes192-ctr
aes128-ctr
cast128-cbc
blowfish-cbc */
constexpr ZstringView sftpPrefix = Zstr("sftp:");
constexpr std::chrono::seconds SFTP_SESSION_MAX_IDLE_TIME (20);
constexpr std::chrono::seconds SFTP_SESSION_CLEANUP_INTERVAL (4); //facilitate default of 5-seconds delay for error retry
constexpr std::chrono::seconds SFTP_CHANNEL_LIMIT_DETECTION_TIME_OUT(30);
//permissions for new files: rw- rw- rw- [0666] => consider umask! (e.g. 0022 for ffs.org)
const long SFTP_DEFAULT_PERMISSION_FILE = LIBSSH2_SFTP_S_IRUSR | LIBSSH2_SFTP_S_IWUSR |
LIBSSH2_SFTP_S_IRGRP | LIBSSH2_SFTP_S_IWGRP |
LIBSSH2_SFTP_S_IROTH | LIBSSH2_SFTP_S_IWOTH;
//permissions for new folders: rwx rwx rwx [0777] => consider umask! (e.g. 0022 for ffs.org)
const long SFTP_DEFAULT_PERMISSION_FOLDER = LIBSSH2_SFTP_S_IRWXU |
LIBSSH2_SFTP_S_IRWXG |
LIBSSH2_SFTP_S_IRWXO;
//attention: if operation fails due to time out, e.g. file copy, the cleanup code may hang, too => total delay = 2 x time out interval
const size_t SFTP_OPTIMAL_BLOCK_SIZE_READ = 16 * MAX_SFTP_READ_SIZE; //https://github.com/libssh2/libssh2/issues/90
const size_t SFTP_OPTIMAL_BLOCK_SIZE_WRITE = 16 * MAX_SFTP_OUTGOING_SIZE; //need large buffer to mitigate libssh2 stupidly waiting on "acks": https://www.libssh2.org/libssh2_sftp_write.html
static_assert(MAX_SFTP_READ_SIZE == 30000 && MAX_SFTP_OUTGOING_SIZE == 30000, "reevaluate optimal block sizes if these constants change!");
/* Perf Test, Sourceforge frs, SFTP upload, compressed 25 MB test file:
SFTP_OPTIMAL_BLOCK_SIZE_READ: SFTP_OPTIMAL_BLOCK_SIZE_WRITE:
multiples of multiples of
MAX_SFTP_READ_SIZE KB/s MAX_SFTP_OUTGOING_SIZE KB/s
1 650 1 140
2 1000 2 280
4 1800 4 320
8 1800 8 320
16 1800 16 320
32 1800 32 320
Filezilla download speed: 1800 KB/s Filezilla upload speed: 560 KB/s
DSL maximum download speed: 3060 KB/s DSL maximum upload speed: 620 KB/s
Perf Test 2: FFS hompage (2022-09-22)
SFTP_OPTIMAL_BLOCK_SIZE_READ: SFTP_OPTIMAL_BLOCK_SIZE_WRITE:
multiples of multiples of
MAX_SFTP_READ_SIZE MB/s MAX_SFTP_OUTGOING_SIZE MB/s
1 0,77 1 0.25
2 1,63 2 0.50
4 3,43 4 0.97
8 6,93 8 1.86
16 9,41 16 3.60
32 9,58 32 3.83
Filezilla download speed: 12,2 MB/s Filezilla upload speed: 4.4 MB/s -> unfair comparison: FFS seems slower because it includes setup work, e.g. open file handle
DSL maximum download speed: 12,9 MB/s DSL maximum upload speed: 4,7 MB/s
=> libssh2_sftp_read/libssh2_sftp_write may take quite long for 16x and larger => use smallest multiple that fills bandwidth! */
inline
uint16_t getEffectivePort(int portOption)
{
if (portOption > 0)
return static_cast<uint16_t>(portOption);
return DEFAULT_PORT_SFTP;
}
struct SshDeviceId //= what defines a unique SFTP location
{
/*explicit*/ SshDeviceId(const SftpLogin& login) :
server(login.server),
port(getEffectivePort(login.portCfg)),
username(login.username) {}
Zstring server;
uint16_t port; //must be valid port!
Zstring username;
};
std::weak_ordering operator<=>(const SshDeviceId& lhs, const SshDeviceId& rhs)
{
//exactly the type of case insensitive comparison we need for server names! https://docs.microsoft.com/en-us/windows/win32/api/ws2tcpip/nf-ws2tcpip-getaddrinfow#IDNs
if (const std::weak_ordering cmp = compareAsciiNoCase(lhs.server, rhs.server);
cmp != std::weak_ordering::equivalent)
return cmp;
return std::tie(lhs.port, lhs.username) <=> //username: case sensitive!
std::tie(rhs.port, rhs.username);
}
//also needed by compareDeviceSameAfsType(), so can't just replace with hash and use std::unordered_map
struct SshSessionCfg //= config for buffered SFTP session
{
SshDeviceId deviceId;
SftpAuthType authType = SftpAuthType::password;
Zstring password; //authType == password or keyFile
Zstring privateKeyFilePath; //authType == keyFile: use PEM-encoded private key (protected by password) for authentication
bool allowZlib = false;
};
bool operator==(const SshSessionCfg& lhs, const SshSessionCfg& rhs)
{
if (lhs.deviceId <=> rhs.deviceId != std::weak_ordering::equivalent)
return false;
if (std::tie(lhs.authType, lhs.allowZlib) !=
std::tie(rhs.authType, rhs.allowZlib))
return false;
switch (lhs.authType)
{
case SftpAuthType::password:
return lhs.password == rhs.password; //case sensitive!
case SftpAuthType::keyFile:
return std::tie(lhs.password, lhs.privateKeyFilePath) == //case sensitive!
std::tie(rhs.password, rhs.privateKeyFilePath); //
case SftpAuthType::agent:
return true;
}
assert(false);
return true;
}
Zstring concatenateSftpFolderPathPhrase(const SftpLogin& login, const AfsPath& itemPath); //noexcept
std::string getLibssh2Path(const AfsPath& itemPath)
{
return utfTo<std::string>(getServerRelPath(itemPath));
}
std::wstring getSftpDisplayPath(const SshDeviceId& deviceId, const AfsPath& itemPath)
{
Zstring displayPath = Zstring(sftpPrefix) + Zstr("//");
if (!deviceId.username.empty()) //show username! consider AFS::compareDeviceSameAfsType()
displayPath += deviceId.username + Zstr('@');
//if (parseIpv6Address(deviceId.server) && deviceId.port != DEFAULT_PORT_SFTP)
// displayPath += Zstr('[') + deviceId.server + Zstr(']');
//else
displayPath += deviceId.server;
//if (deviceId.port != DEFAULT_PORT_SFTP)
// displayPath += Zstr(':') + numberTo<Zstring>(deviceId.port);
const Zstring& relPath = getServerRelPath(itemPath);
if (relPath != Zstr("/"))
displayPath += relPath;
return utfTo<std::wstring>(displayPath);
}
//===========================================================================================================================
//=> most likely *not* a connection issue
struct SysErrorSftpProtocol : public zen::SysError
{
SysErrorSftpProtocol(const std::wstring& msg, unsigned long sftpError) : SysError(msg), sftpErrorCode(sftpError) {}
const unsigned long sftpErrorCode;
};
DEFINE_NEW_SYS_ERROR(SysErrorPassword)
constinit Global<UniSessionCounter> globalSftpSessionCount;
GLOBAL_RUN_ONCE(globalSftpSessionCount.set(createUniSessionCounter()));
class SshSession
{
public:
SshSession(const SshSessionCfg& sessionCfg, int timeoutSec) : //throw SysError, SysErrorPassword
sessionCfg_(sessionCfg)
{
ZEN_ON_SCOPE_FAIL(cleanup()); //destructor call would lead to member double clean-up!!!
const Zstring& serviceName = numberTo<Zstring>(sessionCfg_.deviceId.port);
socket_.emplace(sessionCfg_.deviceId.server, serviceName, timeoutSec); //throw SysError
sshSession_ = ::libssh2_session_init();
if (!sshSession_) //does not set ssh last error; source: only memory allocation may fail
throw SysError(formatSystemError("libssh2_session_init", formatSshStatusCode(LIBSSH2_ERROR_ALLOC), L""));
//if zlib compression causes trouble, make it a user setting: https://freefilesync.org/forum/viewtopic.php?t=6663
//=> surprise: it IS causing trouble: slow-down in local syncs: https://freefilesync.org/forum/viewtopic.php?t=7244#p24250
if (sessionCfg_.allowZlib)
if (const int rc = ::libssh2_session_flag(sshSession_, LIBSSH2_FLAG_COMPRESS, 1);
rc != 0) //does not set SSH last error
throw SysError(formatSystemError("libssh2_session_flag", formatSshStatusCode(rc), L""));
::libssh2_session_set_blocking(sshSession_, 1);
//we don't consider the timeout part of the session when it comes to reuse! but we already require it during initialization
::libssh2_session_set_timeout(sshSession_, timeoutSec * 1000 /*ms*/);
if (::libssh2_session_handshake(sshSession_, socket_->get()) != 0)
throw SysError(formatLastSshError("libssh2_session_handshake", nullptr));
//evaluate fingerprint = libssh2_hostkey_hash(sshSession_, LIBSSH2_HOSTKEY_HASH_SHA1) ???
const auto usernameUtf8 = utfTo<std::string>(sessionCfg_.deviceId.username);
const auto passwordUtf8 = utfTo<std::string>(sessionCfg_.password);
const char* authList = ::libssh2_userauth_list(sshSession_, usernameUtf8);
if (!authList)
{
if (::libssh2_userauth_authenticated(sshSession_) != 1)
throw SysError(formatLastSshError("libssh2_userauth_list", nullptr));
//else: SSH_USERAUTH_NONE has authenticated successfully => we're already done
}
else
{
bool supportAuthPassword = false;
bool supportAuthKeyfile = false;
bool supportAuthInteractive = false;
split(authList, ',', [&](std::string_view authMethod)
{
authMethod = trimCpy(authMethod);
if (!authMethod.empty())
{
if (authMethod == "password")
supportAuthPassword = true;
else if (authMethod == "publickey")
supportAuthKeyfile = true;
else if (authMethod == "keyboard-interactive")
supportAuthInteractive = true;
}
});
switch (sessionCfg_.authType)
{
case SftpAuthType::password:
{
if (supportAuthPassword)
{
if (::libssh2_userauth_password(sshSession_, usernameUtf8, passwordUtf8) != 0)
throw SysErrorPassword(formatLastSshError("libssh2_userauth_password", nullptr));
}
else if (supportAuthInteractive) //some servers, e.g. web.sourceforge.net, support "keyboard-interactive", but not "password"
{
std::wstring unexpectedPrompts;
auto authCallback = [&](int num_prompts, const LIBSSH2_USERAUTH_KBDINT_PROMPT* prompts, LIBSSH2_USERAUTH_KBDINT_RESPONSE* responses)
{
//note: FileZilla assumes password requests when it finds "num_prompts == 1" and "!echo" -> prompt may be localized!
//test case: sourceforge.net sends a single "Password: " prompt with "!echo"
if (num_prompts == 1 && prompts[0].echo == 0)
{
responses[0].text = //pass ownership; will be ::free()d
::strdup(passwordUtf8.c_str());
responses[0].length = static_cast<unsigned int>(passwordUtf8.size());
}
else
for (int i = 0; i < num_prompts; ++i)
unexpectedPrompts += (unexpectedPrompts.empty() ? L"" : L"|") + utfTo<std::wstring>(makeStringView(reinterpret_cast<const char*>(prompts[i].text), prompts[i].length));
};
using AuthCbType = decltype(authCallback);
auto authCallbackWrapper = [](const char* name, int name_len, const char* instruction, int instruction_len,
int num_prompts, const LIBSSH2_USERAUTH_KBDINT_PROMPT* prompts, LIBSSH2_USERAUTH_KBDINT_RESPONSE* responses, void** abstract)
{
try
{
AuthCbType* callback = *reinterpret_cast<AuthCbType**>(abstract); //free this poor little C-API from its shackles and redirect to a proper lambda
(*callback)(num_prompts, prompts, responses); //name, instruction are nullptr for sourceforge.net
}
catch (...) { assert(false); }
};
if (*::libssh2_session_abstract(sshSession_))
throw SysError(L"libssh2_session_abstract: non-null value");
*reinterpret_cast<AuthCbType**>(::libssh2_session_abstract(sshSession_)) = &authCallback;
ZEN_ON_SCOPE_EXIT(*::libssh2_session_abstract(sshSession_) = nullptr);
if (::libssh2_userauth_keyboard_interactive(sshSession_, usernameUtf8, authCallbackWrapper) != 0)
throw SysErrorPassword(formatLastSshError("libssh2_userauth_keyboard_interactive", nullptr) +
(unexpectedPrompts.empty() ? L"" : L"\nUnexpected prompts: " + unexpectedPrompts));
}
else
throw SysError(replaceCpy(_("The server does not support authentication via %x."), L"%x", L"\"username/password\"") +
L'\n' +_("Required:") + L' ' + utfTo<std::wstring>(authList));
}
break;
case SftpAuthType::keyFile:
{
if (!supportAuthKeyfile)
throw SysError(replaceCpy(_("The server does not support authentication via %x."), L"%x", L"\"key file\"") +
L'\n' +_("Required:") + L' ' + utfTo<std::wstring>(authList));
std::string passphrase = passwordUtf8;
std::string pkStream;
try
{
pkStream = getFileContent(sessionCfg_.privateKeyFilePath, nullptr /*notifyUnbufferedIO*/); //throw FileError
trim(pkStream);
}
catch (const FileError& e) { throw SysError(replaceCpy(e.toString(), L"\n\n", L'\n')); } //errors should be further enriched by context info => SysError
//libssh2 doesn't support the PuTTY key file format, but we do!
if (isPuttyKeyStream(pkStream))
try
{
pkStream = convertPuttyKeyToPkix(pkStream, passphrase); //throw SysError
passphrase.clear();
}
catch (const SysError& e) //add context
{
throw SysErrorPassword(replaceCpy(_("Cannot read file %x."), L"%x", fmtPath(sessionCfg_.privateKeyFilePath)) + L' ' + e.toString());
}
if (::libssh2_userauth_publickey_frommemory(sshSession_, usernameUtf8, pkStream, passphrase) != 0) //const char* passphrase
{
//libssh2_userauth_publickey_frommemory()'s "Unable to extract public key from private key" isn't exactly *helpful*
//=> detect invalid key files and give better error message:
const wchar_t* invalidKeyFormat = [&]() -> const wchar_t*
{
//"-----BEGIN PUBLIC KEY-----" OpenSSH SSH-2 public key (X.509 SubjectPublicKeyInfo) = PKIX
//"-----BEGIN RSA PUBLIC KEY-----" OpenSSH SSH-2 public key (PKCS#1 RSAPublicKey)
//"---- BEGIN SSH2 PUBLIC KEY ----" SSH-2 public key (RFC 4716 format)
const std::string_view firstLine = makeStringView(pkStream.begin(), std::find_if(pkStream.begin(), pkStream.end(), isLineBreak<char>));
if (contains(firstLine, "PUBLIC KEY"))
return L"OpenSSH public key";
if (startsWith(pkStream, "rsa-") || //rsa-sha2-256, rsa-sha2-512
startsWith(pkStream, "ssh-") || //ssh-rsa, ssh-dss, ssh-ed25519, ssh-ed448
startsWith(pkStream, "ecdsa-")) //ecdsa-sha2-nistp256, ecdsa-sha2-nistp384, ecdsa-sha2-nistp521
return L"OpenSSH public key"; //OpenSSH SSH-2 public key
if (std::count(pkStream.begin(), pkStream.end(), ' ') == 2 &&
/**/std::all_of(pkStream.begin(), pkStream.end(), [](const char c) { return isDigit(c) || c == ' '; }))
return L"SSH-1 public key";
//"-----BEGIN PRIVATE KEY-----" => OpenSSH SSH-2 private key (PKCS#8 PrivateKeyInfo) => should work
//"-----BEGIN ENCRYPTED PRIVATE KEY-----" => OpenSSH SSH-2 private key (PKCS#8 EncryptedPrivateKeyInfo) => should work
//"-----BEGIN RSA PRIVATE KEY-----" => OpenSSH SSH-2 private key (PKCS#1 RSAPrivateKey) => should work
//"-----BEGIN DSA PRIVATE KEY-----" => OpenSSH SSH-2 private key (PKCS#1 DSAPrivateKey) => should work
//"-----BEGIN EC PRIVATE KEY-----" => OpenSSH SSH-2 private key (PKCS#1 ECPrivateKey) => should work
//"-----BEGIN OPENSSH PRIVATE KEY-----" => OpenSSH SSH-2 private key (new format) => should work
//"---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ----" => ssh.com SSH-2 private key => unclear
//"SSH PRIVATE KEY FILE FORMAT 1.1" => SSH-1 private key => unclear
return nullptr; //other: maybe invalid, maybe not
}();
if (invalidKeyFormat)
throw SysError(_("Authentication failed.") + L' ' +
replaceCpy<std::wstring>(L"%x is not an OpenSSH or PuTTY private key file.", L"%x",
fmtPath(sessionCfg_.privateKeyFilePath) + L" [" + invalidKeyFormat + L']'));
if (isPuttyKeyStream(pkStream))
throw SysError(formatLastSshError("libssh2_userauth_publickey_frommemory", nullptr));
else
//can't rely on LIBSSH2_ERROR_AUTHENTICATION_FAILED: https://github.com/libssh2/libssh2/pull/789
throw SysErrorPassword(formatLastSshError("libssh2_userauth_publickey_frommemory", nullptr));
}
}
break;
case SftpAuthType::agent:
{
LIBSSH2_AGENT* sshAgent = ::libssh2_agent_init(sshSession_);
if (!sshAgent)
throw SysError(formatLastSshError("libssh2_agent_init", nullptr));
ZEN_ON_SCOPE_EXIT(::libssh2_agent_free(sshAgent));
if (::libssh2_agent_connect(sshAgent) != 0)
throw SysError(formatLastSshError("libssh2_agent_connect", nullptr));
ZEN_ON_SCOPE_EXIT(::libssh2_agent_disconnect(sshAgent));
if (::libssh2_agent_list_identities(sshAgent) != 0)
throw SysError(formatLastSshError("libssh2_agent_list_identities", nullptr));
for (libssh2_agent_publickey* prev = nullptr;;)
{
libssh2_agent_publickey* identity = nullptr;
const int rc = ::libssh2_agent_get_identity(sshAgent, &identity, prev);
if (rc == 0) //public key returned
;
else if (rc == 1) //no more public keys
throw SysError(L"SSH agent contains no matching public key.");
else
throw SysError(formatLastSshError("libssh2_agent_get_identity", nullptr));
if (::libssh2_agent_userauth(sshAgent, usernameUtf8.c_str(), identity) == 0)
break; //authentication successful
//else: failed => try next public key
prev = identity;
}
}
break;
}
}
lastSuccessfulUseTime_ = std::chrono::steady_clock::now();
}
~SshSession() { cleanup(); }
const SshSessionCfg& getSessionCfg() const
{
static_assert(std::is_const_v<decltype(sessionCfg_)>, "keep this function thread-safe!");
return sessionCfg_;
}
bool isHealthy() const
{
for (const SftpChannelInfo& ci : sftpChannels_)
if (ci.nbInfo.commandPending)
return false;
if (nbInfo_.commandPending)
return false;
if (possiblyCorrupted_)
return false;
if (std::chrono::steady_clock::now() > lastSuccessfulUseTime_ + SFTP_SESSION_MAX_IDLE_TIME)
return false;
return true;
}
void markAsCorrupted() { possiblyCorrupted_ = true; }
struct Details
{
LIBSSH2_SESSION* sshSession;
LIBSSH2_SFTP* sftpChannel;
};
size_t getSftpChannelCount() const { return sftpChannels_.size(); }
//return "false" if pending
bool tryNonBlocking(size_t channelNo, std::chrono::steady_clock::time_point commandStartTime, const char* functionName,
const std::function<int(const SshSession::Details& sd)>& sftpCommand /*noexcept!*/, int timeoutSec) //throw SysError, SysErrorSftpProtocol
{
assert(::libssh2_session_get_blocking(sshSession_));
::libssh2_session_set_blocking(sshSession_, 0);
ZEN_ON_SCOPE_EXIT(::libssh2_session_set_blocking(sshSession_, 1));
//yes, we're non-blocking, still won't hurt to set the timeout in case libssh2 decides to use it nevertheless
::libssh2_session_set_timeout(sshSession_, timeoutSec * 1000 /*ms*/);
LIBSSH2_SFTP* sftpChannel = channelNo < sftpChannels_.size() ? sftpChannels_[channelNo].sftpChannel : nullptr;
SftpNonBlockInfo& nbInfo = channelNo < sftpChannels_.size() ? sftpChannels_[channelNo].nbInfo : nbInfo_;
if (!nbInfo.commandPending)
assert(nbInfo.commandStartTime != commandStartTime);
else if (nbInfo.commandStartTime == commandStartTime && nbInfo.functionName == functionName)
; //continue pending SFTP call
else
{
assert(false); //pending sftp command is not completed by client: e.g. libssh2_sftp_close() cleaning up after a timed-out libssh2_sftp_read()
possiblyCorrupted_ = true; //=> start new command (with new start time), but remember to not trust this session anymore!
}
nbInfo.commandPending = true;
nbInfo.commandStartTime = commandStartTime;
nbInfo.functionName = functionName;
int rc = LIBSSH2_ERROR_NONE;
try
{
rc = sftpCommand({sshSession_, sftpChannel}); //noexcept
}
catch (...) { assert(false); rc = LIBSSH2_ERROR_BAD_USE; }
assert(rc >= 0 || ::libssh2_session_last_errno(sshSession_) == rc);
if (rc < 0 && ::libssh2_session_last_errno(sshSession_) != rc) //when libssh2 fails to properly set last error; e.g. https://github.com/libssh2/libssh2/pull/123
::libssh2_session_set_last_error(sshSession_, rc, nullptr);
if (rc >= LIBSSH2_ERROR_NONE ||
(rc == LIBSSH2_ERROR_SFTP_PROTOCOL && ::libssh2_sftp_last_error(sftpChannel) != LIBSSH2_FX_OK))
//libssh2 source: LIBSSH2_ERROR_SFTP_PROTOCOL *without* setting LIBSSH2_SFTP::last_errno indicates a corrupted connection!
{
nbInfo.commandPending = false; //
lastSuccessfulUseTime_ = std::chrono::steady_clock::now(); //[!] LIBSSH2_ERROR_SFTP_PROTOCOL is NOT an SSH error => the SSH session is just fine!
if (rc == LIBSSH2_ERROR_SFTP_PROTOCOL)
throw SysErrorSftpProtocol(formatLastSshError(functionName, sftpChannel), ::libssh2_sftp_last_error(sftpChannel));
return true;
}
else if (rc == LIBSSH2_ERROR_EAGAIN)
{
if (std::chrono::steady_clock::now() > nbInfo.commandStartTime + std::chrono::seconds(timeoutSec))
//consider SSH session corrupted! => isHealthy() will see pending command
throw SysError(formatSystemError(functionName, formatSshStatusCode(LIBSSH2_ERROR_TIMEOUT),
_P("Operation timed out after 1 second.", "Operation timed out after %x seconds.", timeoutSec)));
return false;
}
else //=> SSH session errors only (hopefully!) e.g. LIBSSH2_ERROR_SOCKET_RECV
//consider SSH session corrupted! => isHealthy() will see pending command
throw SysError(formatLastSshError(functionName, sftpChannel));
}
//returns when traffic is available or time out: both cases are handled by next tryNonBlocking() call
static void waitForTraffic(const std::vector<SshSession*>& sshSessions, int timeoutSec) //throw SysError
{
//reference: session.c: _libssh2_wait_socket()
std::vector<pollfd> fds;
std::chrono::steady_clock::time_point startTimeMin = std::chrono::steady_clock::time_point::max();
for (SshSession* session : sshSessions)
{
assert(::libssh2_session_last_errno(session->sshSession_) == LIBSSH2_ERROR_EAGAIN);
assert(session->nbInfo_.commandPending || std::any_of(session->sftpChannels_.begin(), session->sftpChannels_.end(), [](SftpChannelInfo& ci) { return ci.nbInfo.commandPending; }));
pollfd pfd{.fd = session->socket_->get()};
const int dir = ::libssh2_session_block_directions(session->sshSession_);
assert(dir != 0); //we assert a blocked direction after libssh2 returned LIBSSH2_ERROR_EAGAIN!
if (dir & LIBSSH2_SESSION_BLOCK_INBOUND)
pfd.events |= POLLIN;
if (dir & LIBSSH2_SESSION_BLOCK_OUTBOUND)
pfd.events |= POLLOUT;
if (pfd.events != 0)
fds.push_back(pfd);
for (const SftpChannelInfo& ci : session->sftpChannels_)
if (ci.nbInfo.commandPending)
startTimeMin = std::min(startTimeMin, ci.nbInfo.commandStartTime);
if (session->nbInfo_.commandPending)
startTimeMin = std::min(startTimeMin, session->nbInfo_.commandStartTime);
}
if (!fds.empty())
{
assert(startTimeMin != std::chrono::steady_clock::time_point::max());
const auto now = std::chrono::steady_clock::now();
const auto stopTime = startTimeMin + std::chrono::seconds(timeoutSec);
if (now >= stopTime)
return; //time-out! => let next tryNonBlocking() call fail with detailed error!
const auto waitTimeMs = std::chrono::duration_cast<std::chrono::milliseconds>(stopTime - now).count();
//is poll() on macOS broken? https://daniel.haxx.se/blog/2016/10/11/poll-on-mac-10-12-is-broken/
// it seems Daniel only takes issue with "empty" input handling!? => not an issue for us
const char* functionName = "poll";
const int rv = ::poll(fds.data(), //struct pollfd* fds
fds.size(), //nfds_t nfds
waitTimeMs); //int timeout [ms]
if (rv < 0) //consider SSH sessions corrupted! => isHealthy() will see pending commands
throw SysError(formatSystemError(functionName, getLastError()));
if (rv == 0) //time-out! => let next tryNonBlocking() call fail with detailed error!
return;
}
else assert(false);
}
static void addSftpChannel(const std::vector<SshSession*>& sshSessions, int timeoutSec) //throw SysError
{
auto addChannelDetails = [](const std::wstring& msg, SshSession& sshSession) //when hitting the server's SFTP channel limit, inform user about channel number
{
if (sshSession.sftpChannels_.empty())
return msg;
return msg + L' ' + replaceCpy(_("Failed to open SFTP channel number %x."), L"%x", formatNumber(sshSession.sftpChannels_.size() + 1));
};
std::optional<SysError> firstSysError;
std::vector<SshSession*> pendingSessions = sshSessions;
const auto sftpCommandStartTime = std::chrono::steady_clock::now();
for (;;)
{
//create all SFTP sessions in parallel => non-blocking
//note: each libssh2_sftp_init() consists of multiple round-trips => poll until all sessions are finished, don't just init and then block on each!
for (size_t pos = pendingSessions.size(); pos-- > 0 ; ) //CAREFUL WITH THESE ERASEs (invalidate positions!!!)
try
{
if (pendingSessions[pos]->tryNonBlocking(static_cast<size_t>(-1), sftpCommandStartTime, "libssh2_sftp_init",
[&](const SshSession::Details& sd) //noexcept!
{
LIBSSH2_SFTP* sftpChannelNew = ::libssh2_sftp_init(sd.sshSession);
if (!sftpChannelNew)
return std::min(::libssh2_session_last_errno(sd.sshSession), LIBSSH2_ERROR_SOCKET_NONE);
//just in case libssh2 failed to properly set last error; e.g. https://github.com/libssh2/libssh2/pull/123
pendingSessions[pos]->sftpChannels_.emplace_back(sftpChannelNew);
return LIBSSH2_ERROR_NONE;
}, timeoutSec)) //throw SysError, (SysErrorSftpProtocol)
pendingSessions.erase(pendingSessions.begin() + pos); //= not pending
}
catch (const SysError& e)
{
if (!firstSysError) //don't throw yet and corrupt other valid, but pending SshSessions! We also don't want to leak LIBSSH2_SFTP* waiting in libssh2 code
firstSysError = SysError(addChannelDetails(e.toString(), *pendingSessions[pos]));
//SysErrorSftpProtocol? unexpected during libssh2_sftp_init()
//-> still occuring for whatever reason!? => "slice" down to SysError
pendingSessions.erase(pendingSessions.begin() + pos);
}
if (pendingSessions.empty())
{
if (firstSysError)
throw* firstSysError;
return;
}
waitForTraffic(pendingSessions, timeoutSec); //throw SysError
}
}
private:
SshSession (const SshSession&) = delete;
SshSession& operator=(const SshSession&) = delete;
void cleanup() //attention: may block heavily after error!
{
for (SftpChannelInfo& ci : sftpChannels_)
//ci.nbInfo.commandPending? => may "legitimately" happen when an SFTP command times out
if (::libssh2_sftp_shutdown(ci.sftpChannel) != LIBSSH2_ERROR_NONE)
assert(false);
if (sshSession_)
{
//*INDENT-OFF*
if (!nbInfo_.commandPending && std::all_of(sftpChannels_.begin(), sftpChannels_.end(),
[](const SftpChannelInfo& ci) { return !ci.nbInfo.commandPending; }))
if (::libssh2_session_disconnect(sshSession_, "FreeFileSync says \"bye\"!") != LIBSSH2_ERROR_NONE) //= server notification only! no local cleanup apparently
assert(false);
//else: avoid further stress on the broken SSH session and take French leave
//nbInfo_.commandPending? => have to clean up, no matter what!
if (::libssh2_session_free(sshSession_) != LIBSSH2_ERROR_NONE)
assert(false);
//*INDENT-ON*
}
}
std::wstring formatLastSshError(const char* functionName, LIBSSH2_SFTP* sftpChannel /*optional*/) const
{
char* lastErrorMsg = nullptr; //owned by "sshSession"
const int sshStatusCode = ::libssh2_session_last_error(sshSession_, &lastErrorMsg, nullptr, false /*want_buf*/);
assert(lastErrorMsg);
std::wstring errorMsg;
if (lastErrorMsg)
errorMsg = trimCpy(utfTo<std::wstring>(lastErrorMsg));
//LIBSSH2_ERROR_SFTP_PROTOCOL does *not* mean libssh2_sftp_last_error() is also available!
//But if it's not, we have a broken connection, and lastErrorMsg contains meaningful details!
if (sshStatusCode == LIBSSH2_ERROR_SFTP_PROTOCOL && ::libssh2_sftp_last_error(sftpChannel) != LIBSSH2_FX_OK)
{
if (errorMsg == L"SFTP Protocol Error") //that's trite!
errorMsg.clear();
return formatSystemError(functionName, formatSftpStatusCode(::libssh2_sftp_last_error(sftpChannel)), errorMsg);
}
return formatSystemError(functionName, formatSshStatusCode(sshStatusCode), errorMsg);
}
struct SftpNonBlockInfo
{
bool commandPending = false;
std::chrono::steady_clock::time_point commandStartTime; //specified by client, try to detect libssh2 usage errors
std::string functionName;
};
struct SftpChannelInfo
{
explicit SftpChannelInfo(LIBSSH2_SFTP* sc) : sftpChannel(sc) {}
LIBSSH2_SFTP* sftpChannel = nullptr;
SftpNonBlockInfo nbInfo;
};
std::optional<Socket> socket_; //*bound* after constructor has run
LIBSSH2_SESSION* sshSession_ = nullptr;
std::vector<SftpChannelInfo> sftpChannels_;
bool possiblyCorrupted_ = false;
SftpNonBlockInfo nbInfo_; //for SSH session, e.g. libssh2_sftp_init()
const SshSessionCfg sessionCfg_;
const std::shared_ptr<UniCounterCookie> libsshCurlUnifiedInitCookie_{(getLibsshCurlUnifiedInitCookie(globalSftpSessionCount))}; //throw SysError
std::chrono::steady_clock::time_point lastSuccessfulUseTime_; //...of the SSH session (but not necessarily the SFTP functionality!)
};
//===========================================================================================================================
//===========================================================================================================================
class SftpSessionManager //reuse (healthy) SFTP sessions globally
{
struct SshSessionCache;
public:
SftpSessionManager() : sessionCleaner_([this]
{
setCurrentThreadName(Zstr("Session Cleaner[SFTP]"));
runGlobalSessionCleanUp(); /*throw ThreadStopRequest*/
}) {}
struct ReUseOnDelete
{
void operator()(SshSession* s) const;
};
class SshSessionShared
{
public:
SshSessionShared(std::unique_ptr<SshSession, ReUseOnDelete>&& idleSession, int timeoutSec) :
session_(std::move(idleSession)) /*bound!*/, timeoutSec_(timeoutSec) { /*assert(session_->isHealthy());*/ }
//we need two-step initialization: 1. constructor is FAST and noexcept 2. init() is SLOW and throws
void initSftpChannel() //throw SysError
{
if (session_->getSftpChannelCount() == 0) //make sure the SSH session contains at least one SFTP channel
SshSession::addSftpChannel({session_.get()}, timeoutSec_); //throw SysError
}
void executeBlocking(const char* functionName, const std::function<int(const SshSession::Details& sd)>& sftpCommand /*noexcept!*/) //throw SysError, SysErrorSftpProtocol
{
assert(threadId_ == std::this_thread::get_id());
assert(session_->getSftpChannelCount() > 0);
const auto sftpCommandStartTime = std::chrono::steady_clock::now();
for (;;)
if (session_->tryNonBlocking(0 /*channelNo*/, sftpCommandStartTime, functionName, sftpCommand, timeoutSec_)) //throw SysError, SysErrorSftpProtocol
return;
else //pending
SshSession::waitForTraffic({session_.get()}, timeoutSec_); //throw SysError
}
const SshSessionCfg& getSessionCfg() const { return session_->getSessionCfg(); } //thread-safe
private:
std::unique_ptr<SshSession, ReUseOnDelete> session_; //bound!
const std::thread::id threadId_ = std::this_thread::get_id();
const int timeoutSec_;
};
class SshSessionExclusive
{
public:
SshSessionExclusive(std::unique_ptr<SshSession, ReUseOnDelete>&& idleSession, int timeoutSec) :
session_(std::move(idleSession)) /*bound!*/, timeoutSec_(timeoutSec) { /*assert(session_->isHealthy());*/ }
bool tryNonBlocking(size_t channelNo, std::chrono::steady_clock::time_point commandStartTime, const char* functionName, //throw SysError, SysErrorSftpProtocol
const std::function<int(const SshSession::Details& sd)>& sftpCommand /*noexcept!*/)
{
return session_->tryNonBlocking(channelNo, commandStartTime, functionName, sftpCommand, timeoutSec_); //throw SysError, SysErrorSftpProtocol
}
void waitForTraffic() //throw SysError
{
SshSession::waitForTraffic({session_.get()}, timeoutSec_); //throw SysError
}
size_t getSftpChannelCount() const { return session_->getSftpChannelCount(); }
void markAsCorrupted() { session_->markAsCorrupted(); }
static void addSftpChannel(const std::vector<SshSessionExclusive*>& exSessions) //throw SysError
{
std::vector<SshSession*> sshSessions;
for (SshSessionExclusive* exSession : exSessions)
sshSessions.push_back(exSession->session_.get());
int timeoutSec = 0;
for (SshSessionExclusive* exSession : exSessions)
timeoutSec = std::max(timeoutSec, exSession->timeoutSec_);
SshSession::addSftpChannel(sshSessions, timeoutSec); //throw SysError
}
static void waitForTraffic(const std::vector<SshSessionExclusive*>& exSessions) //throw SysError
{
std::vector<SshSession*> sshSessions;
for (SshSessionExclusive* exSession : exSessions)
sshSessions.push_back(exSession->session_.get());
int timeoutSec = 0;
for (SshSessionExclusive* exSession : exSessions)
timeoutSec = std::max(timeoutSec, exSession->timeoutSec_);
SshSession::waitForTraffic(sshSessions, timeoutSec); //throw SysError
}
const SshSessionCfg& getSessionCfg() const { return session_->getSessionCfg(); } //thread-safe
private:
std::unique_ptr<SshSession, ReUseOnDelete> session_; //bound!
const int timeoutSec_;
};
std::shared_ptr<SshSessionShared> getSharedSession(const SftpLogin& login) //throw SysError, SysErrorPassword
{
Protected<SshSessionCache>& sessionCache = getSessionCache(login);
const std::thread::id threadId = std::this_thread::get_id();
std::shared_ptr<SshSessionShared> sharedSession; //either or
std::optional<SshSessionCfg> sessionCfg; //
sessionCache.access([&](SshSessionCache& cache)
{
if (!cache.activeCfg) //AFS::authenticateAccess() not called => authenticate implicitly!
setActiveConfig(cache, login);
std::weak_ptr<SshSessionShared>& sharedSessionWeak = cache.sshSessionsWithThreadAffinity[threadId]; //get or create
if (auto session = sharedSessionWeak.lock())
//dereference session ONLY after affinity to THIS thread was confirmed!!!
//assume "isHealthy()" to avoid hitting server connection limits: (clean up of !isHealthy() after use; idle sessions via worker thread)
sharedSession = session;
if (!sharedSession)
//assume "isHealthy()" to avoid hitting server connection limits: (clean up of !isHealthy() after use; idle sessions via worker thread)
if (!cache.idleSshSessions.empty())
{
std::unique_ptr<SshSession, ReUseOnDelete> sshSession(cache.idleSshSessions.back().release());
/**/ cache.idleSshSessions.pop_back();
sharedSessionWeak = sharedSession = std::make_shared<SshSessionShared>(std::move(sshSession), login.timeoutSec); //still holding lock => constructor must be *fast*!
}
if (!sharedSession)
sessionCfg = *cache.activeCfg;
});
//create new SFTP session outside the lock: 1. don't block other threads 2. non-atomic regarding "sessionCache"! => one session too many is not a problem!
if (!sharedSession)
{
sharedSession = std::make_shared<SshSessionShared>(std::unique_ptr<SshSession, ReUseOnDelete>(new SshSession(*sessionCfg, login.timeoutSec)), login.timeoutSec); //throw SysError, SysErrorPassword
sessionCache.access([&](SshSessionCache& cache)
{
if (sharedSession->getSessionCfg() == *cache.activeCfg) //created outside the lock => check *again*
cache.sshSessionsWithThreadAffinity[threadId] = sharedSession;
});
}
//finish two-step initialization outside the lock: BLOCKING!
sharedSession->initSftpChannel(); //throw SysError
return sharedSession;
}
std::unique_ptr<SshSessionExclusive> getExclusiveSession(const SftpLogin& login) //throw SysError
{
std::unique_ptr<SshSession, ReUseOnDelete> sshSession; //either or
std::optional<SshSessionCfg> sessionCfg; //
getSessionCache(login).access([&](SshSessionCache& cache)
{
if (!cache.activeCfg) //AFS::authenticateAccess() not called => authenticate implicitly!
setActiveConfig(cache, login);
//assume "isHealthy()" to avoid hitting server connection limits: (clean up of !isHealthy() after use, idle sessions via worker thread)
if (!cache.idleSshSessions.empty())
{
sshSession.reset(cache.idleSshSessions.back().release());
/**/ cache.idleSshSessions.pop_back();
}
else
sessionCfg = *cache.activeCfg;
});
//create new SFTP session outside the lock: 1. don't block other threads 2. non-atomic regarding "sessionCache"! => one session too many is not a problem!
if (!sshSession)
sshSession.reset(new SshSession(*sessionCfg, login.timeoutSec)); //throw SysError, SysErrorPassword
return std::make_unique<SshSessionExclusive>(std::move(sshSession), login.timeoutSec);
}
void setActiveConfig(const SftpLogin& login)
{
getSessionCache(login).access([&](SshSessionCache& cache) { setActiveConfig(cache, login); });
}
void setSessionPassword(const SftpLogin& login, const Zstring& password, SftpAuthType authType)
{
getSessionCache(login).access([&](SshSessionCache& cache)
{
(authType == SftpAuthType::password ? cache.sessionPassword : cache.sessionPassphrase) = password;
setActiveConfig(cache, login);
});
}
private:
SftpSessionManager (const SftpSessionManager&) = delete;
SftpSessionManager& operator=(const SftpSessionManager&) = delete;
Protected<SshSessionCache>& getSessionCache(const SshDeviceId& deviceId)
{
//single global session store per login; life-time bound to globalInstance => never remove a sessionCache!!!
Protected<SshSessionCache>* sessionCache = nullptr;
globalSessionCache_.access([&](GlobalSshSessions& sessionsById)
{
sessionCache = &sessionsById[deviceId]; //get or create
});
static_assert(std::is_same_v<GlobalSshSessions, std::map<SshDeviceId, Protected<SshSessionCache>>>, "require std::map so that the pointers we return remain stable");
return *sessionCache;
}
void setActiveConfig(SshSessionCache& cache, const SftpLogin& login)
{
const Zstring password = [&]
{
if (login.authType == SftpAuthType::password ||
login.authType == SftpAuthType::keyFile)
{
if (login.password)
return *login.password;
return login.authType == SftpAuthType::password ? cache.sessionPassword : cache.sessionPassphrase;
}
return Zstring();
}();
if (cache.activeCfg)
{
assert(std::all_of(cache.idleSshSessions.begin(), cache.idleSshSessions.end(),
[&](const std::unique_ptr<SshSession>& session) { return session->getSessionCfg() == cache.activeCfg; }));
assert(std::all_of(cache.sshSessionsWithThreadAffinity.begin(), cache.sshSessionsWithThreadAffinity.end(), [&](const auto& v)
{
if (std::shared_ptr<SshSessionShared> sharedSession = v.second.lock())
return sharedSession->getSessionCfg() /*thread-safe!*/ == cache.activeCfg;
return true;
}));
}
else
assert(cache.idleSshSessions.empty() && cache.sshSessionsWithThreadAffinity.empty());
const std::optional<SshSessionCfg> prevCfg = cache.activeCfg;
cache.activeCfg =
{
.deviceId{login},
.authType = login.authType,
.password = password,
.privateKeyFilePath = login.privateKeyFilePath,
.allowZlib = login.allowZlib,
};
/* remove incompatible sessions:
- avoid hitting FTP connection limit if some config uses TLS, but not the other: https://freefilesync.org/forum/viewtopic.php?t=8532
- logically consistent with AFS::compareDevice()
- don't allow different authentication methods, when authenticateAccess() is called *once* per device in getFolderStatusParallel()
- what user expects, e.g. when tesing changed settings in SFTP login dialog */
if (cache.activeCfg != prevCfg)
{
cache.idleSshSessions .clear(); //run ~SshSession *inside* the lock! => avoid hitting server limits!
cache.sshSessionsWithThreadAffinity.clear(); //
//=> incompatible sessions will be deleted by ReUseOnDelete(); until then: additionally counts towards SFTP connection limit :(
}
}
//run a dedicated clean-up thread => it's unclear when the server let's a connection time out, so we do it preemptively
//context of worker thread:
void runGlobalSessionCleanUp() //throw ThreadStopRequest
{
std::chrono::steady_clock::time_point lastCleanupTime;
for (;;)
{
const auto now = std::chrono::steady_clock::now();
if (now < lastCleanupTime + SFTP_SESSION_CLEANUP_INTERVAL)
interruptibleSleep(lastCleanupTime + SFTP_SESSION_CLEANUP_INTERVAL - now); //throw ThreadStopRequest
lastCleanupTime = std::chrono::steady_clock::now();
std::vector<Protected<SshSessionCache>*> sessionCaches; //pointers remain stable, thanks to std::map<>
globalSessionCache_.access([&](GlobalSshSessions& sessionsById)
{
for (auto& [sessionId, idleSession] : sessionsById)
sessionCaches.push_back(&idleSession);
});
for (Protected<SshSessionCache>* sessionCache : sessionCaches)
for (;;)
{
bool done = false;
sessionCache->access([&](SshSessionCache& cache)
{
for (std::unique_ptr<SshSession>& sshSession : cache.idleSshSessions)
if (!sshSession->isHealthy()) //!isHealthy() sessions are destroyed after use => in this context this means they have been idle for too long
{
sshSession.swap(cache.idleSshSessions.back());
/**/ cache.idleSshSessions.pop_back(); //run ~SshSession *inside* the lock! => avoid hitting server limits!
return; //don't hold lock for too long: delete only one session at a time, then yield...
}
std::erase_if(cache.sshSessionsWithThreadAffinity, [](const auto& v) { return v.second.expired(); }); //clean up dangling weak pointer
done = true;
});
if (done)
break;
std::this_thread::yield(); //outside the lock
}
}
}
struct SshSessionCache
{
//invariant: all cached sessions correspond to activeCfg at any time!
std::vector<std::unique_ptr<SshSession>> idleSshSessions; //extract *temporarily* from this list during use
std::unordered_map<std::thread::id, std::weak_ptr<SshSessionShared>> sshSessionsWithThreadAffinity; //Win32 thread IDs may be REUSED! still, shouldn't be a problem...
std::optional<SshSessionCfg> activeCfg;
Zstring sessionPassword; //user/password
Zstring sessionPassphrase; //keyfile/passphrase
};
using GlobalSshSessions = std::map<SshDeviceId, Protected<SshSessionCache>>;
Protected<GlobalSshSessions> globalSessionCache_;
InterruptibleThread sessionCleaner_;
};
//--------------------------------------------------------------------------------------
UniInitializer globalInitSftp(*globalSftpSessionCount.get());
constinit Global<SftpSessionManager> globalSftpSessionManager; //caveat: life time must be subset of static UniInitializer!
//--------------------------------------------------------------------------------------
void SftpSessionManager::ReUseOnDelete::operator()(SshSession* session) const
{
//assert(session); -> custom deleter is only called on non-null pointer
if (session->isHealthy()) //thread that created the "!isHealthy()" session is responsible for clean up (avoid hitting server connection limits!)
if (std::shared_ptr<SftpSessionManager> mgr = globalSftpSessionManager.get())
mgr->getSessionCache(session->getSessionCfg().deviceId).access([&](SshSessionCache& cache)
{
assert(cache.activeCfg);
if (cache.activeCfg && session->getSessionCfg() == *cache.activeCfg)
cache.idleSshSessions.emplace_back(std::exchange(session, nullptr)); //pass ownership
});
delete session;
}
std::shared_ptr<SftpSessionManager::SshSessionShared> getSharedSftpSession(const SftpLogin& login) //throw SysError
{
if (const std::shared_ptr<SftpSessionManager> mgr = globalSftpSessionManager.get())
return mgr->getSharedSession(login); //throw SysError, SysErrorPassword
throw SysError(formatSystemError("getSharedSftpSession", L"", L"Function call not allowed during init/shutdown."));
}
std::unique_ptr<SftpSessionManager::SshSessionExclusive> getExclusiveSftpSession(const SftpLogin& login) //throw SysError
{
if (const std::shared_ptr<SftpSessionManager> mgr = globalSftpSessionManager.get())
return mgr->getExclusiveSession(login); //throw SysError
throw SysError(formatSystemError("getExclusiveSftpSession", L"", L"Function call not allowed during init/shutdown."));
}
void runSftpCommand(const SftpLogin& login, const char* functionName,
const std::function<int(const SshSession::Details& sd)>& sftpCommand /*noexcept!*/) //throw SysError, SysErrorSftpProtocol
{
std::shared_ptr<SftpSessionManager::SshSessionShared> asyncSession = getSharedSftpSession(login); //throw SysError
//no need to protect against concurrency: shared session is (temporarily) bound to current thread
asyncSession->executeBlocking(functionName, sftpCommand); //throw SysError, SysErrorSftpProtocol
}
//===========================================================================================================================
//===========================================================================================================================
struct SftpItemDetails
{
AFS::ItemType type;
uint64_t fileSize;
time_t modTime;
};
struct SftpItem
{
Zstring itemName;
SftpItemDetails details;
};
std::vector<SftpItem> getDirContentFlat(const SftpLogin& login, const AfsPath& dirPath) //throw FileError
{
LIBSSH2_SFTP_HANDLE* dirHandle = nullptr;
try
{
runSftpCommand(login, "libssh2_sftp_opendir", //throw SysError, SysErrorSftpProtocol
[&](const SshSession::Details& sd) //noexcept!
{
dirHandle = ::libssh2_sftp_opendir(sd.sftpChannel, getLibssh2Path(dirPath));
if (!dirHandle)
return std::min(::libssh2_session_last_errno(sd.sshSession), LIBSSH2_ERROR_SOCKET_NONE);
return LIBSSH2_ERROR_NONE;
});
}
catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot open directory %x."), L"%x", fmtPath(getSftpDisplayPath(login, dirPath))), e.toString()); }
ZEN_ON_SCOPE_EXIT(try
{
runSftpCommand(login, "libssh2_sftp_closedir", //throw SysError, SysErrorSftpProtocol
[&](const SshSession::Details& sd) { return ::libssh2_sftp_closedir(dirHandle); }); //noexcept!
}
catch (const SysError& e) { logExtraError(replaceCpy(_("Cannot read directory %x."), L"%x", fmtPath(getSftpDisplayPath(login, dirPath))) + L"\n\n" + e.toString()); });
std::vector<SftpItem> output;
for (;;)
{
std::array<char, 1024> buf; //libssh2 sample code uses 512; in practice NAME_MAX(255)+1 should suffice: https://serverfault.com/questions/9546/filename-length-limits-on-linux
LIBSSH2_SFTP_ATTRIBUTES attribs = {};
int rc = 0;
try
{
runSftpCommand(login, "libssh2_sftp_readdir", //throw SysError, SysErrorSftpProtocol
[&](const SshSession::Details& sd) { return rc = ::libssh2_sftp_readdir(dirHandle, buf.data(), buf.size(), &attribs); }); //noexcept!
}
catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot read directory %x."), L"%x", fmtPath(getSftpDisplayPath(login, dirPath))), e.toString()); }
if (rc == 0) //no more items
return output;
const std::string_view sftpItemName = makeStringView(buf.data(), rc);
if (sftpItemName == "." || sftpItemName == "..") //check needed for SFTP, too!
continue;
const Zstring& itemName = utfTo<Zstring>(sftpItemName);
const AfsPath itemPath(appendPath(dirPath.value, itemName));
if ((attribs.flags & LIBSSH2_SFTP_ATTR_PERMISSIONS) == 0) //server probably does not support these attributes => fail at folder level
throw FileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(getSftpDisplayPath(login, itemPath))), L"File attributes not available.");
if (LIBSSH2_SFTP_S_ISLNK(attribs.permissions))
{
if ((attribs.flags & LIBSSH2_SFTP_ATTR_ACMODTIME) == 0) //server probably does not support these attributes => fail at folder level
throw FileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(getSftpDisplayPath(login, itemPath))), L"Modification time not supported.");
output.push_back({itemName, {AFS::ItemType::symlink, 0, static_cast<time_t>(attribs.mtime)}});
}
else if (LIBSSH2_SFTP_S_ISDIR(attribs.permissions))
output.push_back({itemName, {AFS::ItemType::folder, 0, static_cast<time_t>(attribs.mtime)}});
else //a file or named pipe, ect: LIBSSH2_SFTP_S_ISREG, LIBSSH2_SFTP_S_ISCHR, LIBSSH2_SFTP_S_ISBLK, LIBSSH2_SFTP_S_ISFIFO, LIBSSH2_SFTP_S_ISSOCK
{
if ((attribs.flags & LIBSSH2_SFTP_ATTR_ACMODTIME) == 0) //server probably does not support these attributes => fail at folder level
throw FileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(getSftpDisplayPath(login, itemPath))), L"Modification time not supported.");
if ((attribs.flags & LIBSSH2_SFTP_ATTR_SIZE) == 0)
throw FileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(getSftpDisplayPath(login, itemPath))), L"File size not supported.");
output.push_back({itemName, {AFS::ItemType::file, attribs.filesize, static_cast<time_t>(attribs.mtime)}});
}
}
}
SftpItemDetails getSymlinkTargetDetails(const SftpLogin& login, const AfsPath& linkPath) //throw FileError
{
LIBSSH2_SFTP_ATTRIBUTES attribsTrg = {};
try
{
runSftpCommand(login, "libssh2_sftp_stat", //throw SysError, SysErrorSftpProtocol
[&](const SshSession::Details& sd) { return ::libssh2_sftp_stat(sd.sftpChannel, getLibssh2Path(linkPath), &attribsTrg); }); //noexcept!
}
catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot resolve symbolic link %x."), L"%x", fmtPath(getSftpDisplayPath(login, linkPath))), e.toString()); }
if ((attribsTrg.flags & LIBSSH2_SFTP_ATTR_PERMISSIONS) == 0)
throw FileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(getSftpDisplayPath(login, linkPath))), L"File attributes not available.");
if (LIBSSH2_SFTP_S_ISDIR(attribsTrg.permissions))
return {AFS::ItemType::folder, 0, static_cast<time_t>(attribsTrg.mtime)};
else
{
if ((attribsTrg.flags & LIBSSH2_SFTP_ATTR_ACMODTIME) == 0) //server probably does not support these attributes => should fail at folder level!
throw FileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(getSftpDisplayPath(login, linkPath))), L"Modification time not supported.");
if ((attribsTrg.flags & LIBSSH2_SFTP_ATTR_SIZE) == 0)
throw FileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(getSftpDisplayPath(login, linkPath))), L"File size not supported.");
return {AFS::ItemType::file, attribsTrg.filesize, static_cast<time_t>(attribsTrg.mtime)};
}
}
class SingleFolderTraverser
{
public:
using WorkItem = std::pair<AfsPath, std::shared_ptr<AFS::TraverserCallback>>;
SingleFolderTraverser(const SftpLogin& login, const std::vector<std::pair<AfsPath, std::shared_ptr<AFS::TraverserCallback>>>& workload /*throw X*/) :
login_(login)
{
for (const auto& [folderPath, cb] : workload)
workload_.push_back(WorkItem{folderPath, cb});
while (!workload_.empty())
{
auto wi = std::move(workload_. front()); //yes, no strong exception guarantee (std::bad_alloc)
/**/ workload_.pop_front(); //
const auto& [folderPath, cb] = wi;
tryReportingDirError([&] //throw X
{
traverseWithException(folderPath, *cb); //throw FileError, X
}, *cb);
}
}
private:
SingleFolderTraverser (const SingleFolderTraverser&) = delete;
SingleFolderTraverser& operator=(const SingleFolderTraverser&) = delete;
void traverseWithException(const AfsPath& dirPath, AFS::TraverserCallback& cb) //throw FileError, X
{
for (const SftpItem& item : getDirContentFlat(login_, dirPath)) //throw FileError
{
const AfsPath itemPath(appendPath(dirPath.value, item.itemName));
switch (item.details.type)
{
case AFS::ItemType::file:
cb.onFile({item.itemName, item.details.fileSize, item.details.modTime, AFS::FingerPrint() /*not supported by SFTP*/, false /*isFollowedSymlink*/}); //throw X
break;
case AFS::ItemType::folder:
if (std::shared_ptr<AFS::TraverserCallback> cbSub = cb.onFolder({item.itemName, false /*isFollowedSymlink*/})) //throw X
workload_.push_back(WorkItem{itemPath, std::move(cbSub)});
break;
case AFS::ItemType::symlink:
switch (cb.onSymlink({item.itemName, item.details.modTime})) //throw X
{
case AFS::TraverserCallback::HandleLink::follow:
{
SftpItemDetails targetDetails = {};
if (!tryReportingItemError([&] //throw X
{
targetDetails = getSymlinkTargetDetails(login_, itemPath); //throw FileError
}, cb, item.itemName))
continue;
if (targetDetails.type == AFS::ItemType::folder)
{
if (std::shared_ptr<AFS::TraverserCallback> cbSub = cb.onFolder({item.itemName, true /*isFollowedSymlink*/})) //throw X
workload_.push_back(WorkItem{itemPath, std::move(cbSub)});
}
else //a file or named pipe, etc.
cb.onFile({item.itemName, targetDetails.fileSize, targetDetails.modTime, AFS::FingerPrint() /*not supported by SFTP*/, true /*isFollowedSymlink*/}); //throw X
}
break;
case AFS::TraverserCallback::HandleLink::skip:
break;
}
break;
}
}
}
const SftpLogin login_;
RingBuffer<WorkItem> workload_;
};
void traverseFolderRecursiveSftp(const SftpLogin& login, const std::vector<std::pair<AfsPath, std::shared_ptr<AFS::TraverserCallback>>>& workload /*throw X*/, size_t) //throw X
{
SingleFolderTraverser dummy(login, workload); //throw X
}
//===========================================================================================================================
struct InputStreamSftp : public AFS::InputStream
{
InputStreamSftp(const SftpLogin& login, const AfsPath& filePath) : //throw FileError
displayPath_(getSftpDisplayPath(login, filePath))
{
try
{
session_ = getSharedSftpSession(login); //throw SysError
session_->executeBlocking("libssh2_sftp_open", //throw SysError, SysErrorSftpProtocol
[&](const SshSession::Details& sd) //noexcept!
{
fileHandle_ = ::libssh2_sftp_open(sd.sftpChannel, getLibssh2Path(filePath), LIBSSH2_FXF_READ, 0);
if (!fileHandle_)
return std::min(::libssh2_session_last_errno(sd.sshSession), LIBSSH2_ERROR_SOCKET_NONE);
return LIBSSH2_ERROR_NONE;
});
}
catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot open file %x."), L"%x", fmtPath(displayPath_)), e.toString()); }
}
~InputStreamSftp()
{
try
{
session_->executeBlocking("libssh2_sftp_close", //throw SysError, SysErrorSftpProtocol
[&](const SshSession::Details& sd) { return ::libssh2_sftp_close(fileHandle_); }); //noexcept!
}
catch (const SysError& e) { logExtraError(replaceCpy(_("Cannot read file %x."), L"%x", fmtPath(displayPath_)) + L"\n\n" + e.toString()); }
}
size_t getBlockSize() override { return SFTP_OPTIMAL_BLOCK_SIZE_READ; } //throw (FileError); non-zero block size is AFS contract!
//may return short; only 0 means EOF! CONTRACT: bytesToRead > 0!
size_t tryRead(void* buffer, size_t bytesToRead, const IoCallback& notifyUnbufferedIO /*throw X*/) override //throw FileError, (ErrorFileLocked), X
{
//libssh2_sftp_read has same semantics as Posix read:
if (bytesToRead == 0) //"read() with a count of 0 returns zero" => indistinguishable from end of file! => check!
throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!");
assert(bytesToRead % getBlockSize() == 0);
ssize_t bytesRead = 0;
try
{
session_->executeBlocking("libssh2_sftp_read", //throw SysError, SysErrorSftpProtocol
[&](const SshSession::Details& sd) //noexcept!
{
bytesRead = ::libssh2_sftp_read(fileHandle_, static_cast<char*>(buffer), bytesToRead);
return static_cast<int>(bytesRead);
});
ASSERT_SYSERROR(makeUnsigned(bytesRead) <= bytesToRead); //better safe than sorry (user should never see this)
}
catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot read file %x."), L"%x", fmtPath(displayPath_)), e.toString()); }
if (notifyUnbufferedIO) notifyUnbufferedIO(bytesRead); //throw X
return bytesRead; //"zero indicates end of file"
}
std::optional<AFS::StreamAttributes> tryGetAttributesFast() override { return {}; }//throw FileError
//although we have an SFTP stream handle, attribute access requires an extra (expensive) round-trip!
//PERF: test case 148 files, 1MB: overall copy time increases by 20% if libssh2_sftp_fstat() gets called per each file
private:
const std::wstring displayPath_;
LIBSSH2_SFTP_HANDLE* fileHandle_ = nullptr;
std::shared_ptr<SftpSessionManager::SshSessionShared> session_;
};
//===========================================================================================================================
//libssh2_sftp_open fails with generic LIBSSH2_FX_FAILURE if already existing
struct OutputStreamSftp : public AFS::OutputStreamImpl
{
OutputStreamSftp(const SftpLogin& login, //throw FileError
const AfsPath& filePath,
std::optional<time_t> modTime) :
login_(login),
filePath_(filePath),
modTime_(modTime)
{
try
{
session_ = getSharedSftpSession(login); //throw SysError
session_->executeBlocking("libssh2_sftp_open", //throw SysError, SysErrorSftpProtocol
[&](const SshSession::Details& sd) //noexcept!
{
fileHandle_ = ::libssh2_sftp_open(sd.sftpChannel, getLibssh2Path(filePath),
LIBSSH2_FXF_WRITE | LIBSSH2_FXF_CREAT | LIBSSH2_FXF_EXCL,
SFTP_DEFAULT_PERMISSION_FILE); //note: server may also apply umask! (e.g. 0022 for ffs.org)
if (!fileHandle_)
return std::min(::libssh2_session_last_errno(sd.sshSession), LIBSSH2_ERROR_SOCKET_NONE);
return LIBSSH2_ERROR_NONE;
});
}
catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(getSftpDisplayPath(login_, filePath_))), e.toString()); }
//NOTE: fileHandle_ still unowned until end of constructor!!!
//pre-allocate file space? not supported
}
~OutputStreamSftp()
{
if (fileHandle_) //=> cleanup non-finalized output file
{
if (!closeFailed_) //otherwise there's no much point in calling libssh2_sftp_close() a second time => let it leak!?
try { close(); /*throw FileError*/ }
catch (const FileError& e) { logExtraError(e.toString()); }
session_.reset(); //reset before file deletion to potentially get new session if !SshSession::isHealthy()
try //see removeFilePlain()
{
runSftpCommand(login_, "libssh2_sftp_unlink", //throw SysError, SysErrorSftpProtocol
[&](const SshSession::Details& sd) { return ::libssh2_sftp_unlink(sd.sftpChannel, getLibssh2Path(filePath_)); }); //noexcept!
}
catch (const SysError& e)
{
logExtraError(replaceCpy(_("Cannot delete file %x."), L"%x", fmtPath(getSftpDisplayPath(login_, filePath_))) + L"\n\n" + e.toString());
}
}
}
size_t getBlockSize() override { return SFTP_OPTIMAL_BLOCK_SIZE_WRITE; } //throw (FileError)
size_t tryWrite(const void* buffer, size_t bytesToWrite, const IoCallback& notifyUnbufferedIO /*throw X*/) override //throw FileError, X; may return short! CONTRACT: bytesToWrite > 0
{
if (bytesToWrite == 0)
throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!");
assert(bytesToWrite % getBlockSize() == 0 || bytesToWrite < getBlockSize());
ssize_t bytesWritten = 0;
try
{
session_->executeBlocking("libssh2_sftp_write", //throw SysError, SysErrorSftpProtocol
[&](const SshSession::Details& sd) //noexcept!
{
bytesWritten = ::libssh2_sftp_write(fileHandle_, static_cast<const char*>(buffer), bytesToWrite);
/* "If this function returns zero it should not be considered an error, but simply that there was no error but yet no payload data got sent to the other end."
=> sounds like BS, but is it really true!?
From the libssh2_sftp_write code it appears that the function always waits for at least one "ack", unless we give it so much data _libssh2_channel_write() can't sent it all! */
assert(bytesWritten != 0);
return static_cast<int>(bytesWritten);
});
ASSERT_SYSERROR(makeUnsigned(bytesWritten) <= bytesToWrite); //better safe than sorry
}
catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(getSftpDisplayPath(login_, filePath_))), e.toString()); }
if (notifyUnbufferedIO) notifyUnbufferedIO(bytesWritten); //throw X!
return bytesWritten;
}
AFS::FinalizeResult finalize(const IoCallback& notifyUnbufferedIO /*throw X*/) override //throw FileError, X
{
close(); //throw FileError
//output finalized => no more exceptions from here on!
//--------------------------------------------------------------------
AFS::FinalizeResult result;
//result.filePrint = ... -> not supported by SFTP
try
{
setModTimeIfAvailable(); //throw FileError, follows symlinks
/* is setting modtime after closing the file handle a pessimization?
SFTP: no, needed for functional correctness (synology server), same as for Native */
}
catch (const FileError& e) { result.errorModTime = e; /*slicing?*/ }
return result;
}
private:
void close() //throw FileError
{
if (!fileHandle_)
throw std::logic_error(std::string(__FILE__) + '[' + numberTo<std::string>(__LINE__) + "] Contract violation!");
try
{
session_->executeBlocking("libssh2_sftp_close", //throw SysError, SysErrorSftpProtocol
[&](const SshSession::Details& sd) { return ::libssh2_sftp_close(fileHandle_); }); //noexcept!
fileHandle_ = nullptr;
}
catch (const SysError& e)
{
closeFailed_ = true;
throw FileError(replaceCpy(_("Cannot write file %x."), L"%x", fmtPath(getSftpDisplayPath(login_, filePath_))), e.toString());
}
}
void setModTimeIfAvailable() const //throw FileError, follows symlinks
{
assert(!fileHandle_);
if (modTime_)
{
LIBSSH2_SFTP_ATTRIBUTES attribNew = {};
attribNew.flags = LIBSSH2_SFTP_ATTR_ACMODTIME;
attribNew.mtime = static_cast<decltype(attribNew.mtime)>(*modTime_); //32-bit target! loss of data!
attribNew.atime = static_cast<decltype(attribNew.atime)>(::time(nullptr)); //
//it seems libssh2_sftp_fsetstat() triggers bugs on synology server => set mtime by path! https://freefilesync.org/forum/viewtopic.php?t=1281
try
{
session_->executeBlocking("libssh2_sftp_setstat", //throw SysError, SysErrorSftpProtocol
[&](const SshSession::Details& sd) { return ::libssh2_sftp_setstat(sd.sftpChannel, getLibssh2Path(filePath_), &attribNew); }); //noexcept!
}
catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot write modification time of %x."), L"%x", fmtPath(getSftpDisplayPath(login_, filePath_))), e.toString()); }
}
}
const SftpLogin login_;
const AfsPath filePath_;
const std::optional<time_t> modTime_;
LIBSSH2_SFTP_HANDLE* fileHandle_ = nullptr;
bool closeFailed_ = false;
std::shared_ptr<SftpSessionManager::SshSessionShared> session_;
};
//===========================================================================================================================
class SftpFileSystem : public AbstractFileSystem
{
public:
explicit SftpFileSystem(const SftpLogin& login) : login_(login) {}
const SftpLogin& getLogin() const { return login_; }
AfsPath getHomePath() const //throw FileError
{
try
{
//we never ever change the SFTP working directory, right? ...right?
return getServerRealPath("."); //throw SysError
//use "~" instead? NO: libssh2_sftp_realpath() fails with LIBSSH2_FX_NO_SUCH_FILE
}
catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot determine final path for %x."), L"%x", fmtPath(getDisplayPath(AfsPath(Zstr("~"))))), e.toString()); }
}
private:
Zstring getInitPathPhrase(const AfsPath& itemPath) const override { return concatenateSftpFolderPathPhrase(login_, itemPath); }
std::vector<Zstring> getPathPhraseAliases(const AfsPath& itemPath) const override
{
std::vector<Zstring> pathAliases;
if (login_.authType != SftpAuthType::keyFile || login_.privateKeyFilePath.empty())
pathAliases.push_back(concatenateSftpFolderPathPhrase(login_, itemPath));
else //why going crazy with key path aliases!? because we can...
for (const Zstring& pathPhrase : ::getPathPhraseAliases(login_.privateKeyFilePath))
{
auto loginTmp = login_;
loginTmp.privateKeyFilePath = pathPhrase;
pathAliases.push_back(concatenateSftpFolderPathPhrase(loginTmp, itemPath));
}
return pathAliases;
}
std::wstring getDisplayPath(const AfsPath& itemPath) const override { return getSftpDisplayPath(login_, itemPath); }
bool isNullFileSystem() const override { return login_.server.empty(); }
std::weak_ordering compareDeviceSameAfsType(const AbstractFileSystem& afsRhs) const override
{
const SftpLogin& lhs = login_;
const SftpLogin& rhs = static_cast<const SftpFileSystem&>(afsRhs).login_;
return SshDeviceId(lhs) <=> SshDeviceId(rhs);
}
//----------------------------------------------------------------------------------------------------------------
ItemType getItemTypeImpl(const AfsPath& itemPath) const //throw SysError, SysErrorSftpProtocol
{
LIBSSH2_SFTP_ATTRIBUTES attr = {};
runSftpCommand(login_, "libssh2_sftp_lstat", //throw SysError, SysErrorSftpProtocol
[&](const SshSession::Details& sd) { return ::libssh2_sftp_lstat(sd.sftpChannel, getLibssh2Path(itemPath), &attr); }); //noexcept!
if ((attr.flags & LIBSSH2_SFTP_ATTR_PERMISSIONS) == 0)
throw SysError(formatSystemError("libssh2_sftp_lstat", L"", L"File attributes not available."));
if (LIBSSH2_SFTP_S_ISLNK(attr.permissions))
return ItemType::symlink;
if (LIBSSH2_SFTP_S_ISDIR(attr.permissions))
return ItemType::folder;
return ItemType::file; //LIBSSH2_SFTP_S_ISREG || LIBSSH2_SFTP_S_ISCHR || LIBSSH2_SFTP_S_ISBLK || LIBSSH2_SFTP_S_ISFIFO || LIBSSH2_SFTP_S_ISSOCK
}
ItemType getItemType(const AfsPath& itemPath) const override //throw FileError
{
try
{
return getItemTypeImpl(itemPath); //throw SysError, SysErrorSftpProtocol
}
catch (const SysError& e)
{
throw FileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(getDisplayPath(itemPath))), e.toString());
}
}
std::optional<ItemType> getItemTypeIfExists(const AfsPath& itemPath) const override //throw FileError
{
try
{
try
{
//fast check: 1. perf 2. expected by getFolderStatusNonBlocking() 3. traversing non-existing folder below MIGHT NOT FAIL (e.g. for SFTP on AWS)
return getItemTypeImpl(itemPath); //throw SysError, SysErrorSftpProtocol
}
catch (const SysErrorSftpProtocol& e)
{
const std::optional<AfsPath> parentPath = getParentPath(itemPath);
if (!parentPath) //device root => quick access test
throw;
//let's dig deeper, but *only* for SysErrorSftpProtocol, not for general connection issues
//+ check if SFTP error code sounds like "not existing"
if (e.sftpErrorCode == LIBSSH2_FX_NO_SUCH_FILE ||
e.sftpErrorCode == LIBSSH2_FX_NO_SUCH_PATH) //-> not seen yet, but sounds reasonable
{
if (const std::optional<ItemType> parentType = getItemTypeIfExists(*parentPath)) //throw FileError
{
if (*parentType == ItemType::file /*obscure, but possible*/)
throw SysError(replaceCpy(_("The name %x is already used by another item."), L"%x", fmtPath(getItemName(*parentPath))));
const Zstring itemName = getItemName(itemPath);
assert(!itemName.empty());
traverseFolder(*parentPath, //throw FileError
[&](const FileInfo& fi) { if (fi.itemName == itemName) throw SysError(_("Temporary access error:") + L' ' + e.toString()); },
[&](const FolderInfo& fi) { if (fi.itemName == itemName) throw SysError(_("Temporary access error:") + L' ' + e.toString()); },
[&](const SymlinkInfo& si) { if (si.itemName == itemName) throw SysError(_("Temporary access error:") + L' ' + e.toString()); });
//- case-sensitive comparison! itemPath must be normalized!
//- finding the item after getItemType() previously failed is exceptional
}
return std::nullopt;
}
else
throw;
}
}
catch (const SysError& e)
{
throw FileError(replaceCpy(_("Cannot read file attributes of %x."), L"%x", fmtPath(getDisplayPath(itemPath))), e.toString());
}
}
//----------------------------------------------------------------------------------------------------------------
//already existing: fail
void createFolderPlain(const AfsPath& folderPath) const override //throw FileError
{
try
{
//fails with obscure LIBSSH2_FX_FAILURE if already existing
runSftpCommand(login_, "libssh2_sftp_mkdir", //throw SysError, SysErrorSftpProtocol
[&](const SshSession::Details& sd) //noexcept!
{
return ::libssh2_sftp_mkdir(sd.sftpChannel, getLibssh2Path(folderPath), SFTP_DEFAULT_PERMISSION_FOLDER);
//less explicit variant: return ::libssh2_sftp_mkdir(sd.sftpChannel, getLibssh2Path(folderPath), LIBSSH2_SFTP_DEFAULT_MODE);
});
}
catch (const SysError& e) //libssh2_sftp_mkdir reports generic LIBSSH2_FX_FAILURE if existing
{
throw FileError(replaceCpy(_("Cannot create directory %x."), L"%x", fmtPath(getDisplayPath(folderPath))), e.toString());
}
}
void removeFilePlain(const AfsPath& filePath) const override //throw FileError
{
try
{
runSftpCommand(login_, "libssh2_sftp_unlink", //throw SysError, SysErrorSftpProtocol
[&](const SshSession::Details& sd) { return ::libssh2_sftp_unlink(sd.sftpChannel, getLibssh2Path(filePath)); }); //noexcept!
}
catch (const SysError& e)
{
throw FileError(replaceCpy(_("Cannot delete file %x."), L"%x", fmtPath(getDisplayPath(filePath))), e.toString());
}
}
void removeSymlinkPlain(const AfsPath& linkPath) const override //throw FileError
{
try
{
runSftpCommand(login_, "libssh2_sftp_unlink", //throw SysError, SysErrorSftpProtocol
[&](const SshSession::Details& sd) { return ::libssh2_sftp_unlink(sd.sftpChannel, getLibssh2Path(linkPath)); }); //noexcept!
}
catch (const SysError& e)
{
throw FileError(replaceCpy(_("Cannot delete symbolic link %x."), L"%x", fmtPath(getDisplayPath(linkPath))), e.toString());
}
}
void removeFolderPlain(const AfsPath& folderPath) const override //throw FileError
{
try
{
//libssh2_sftp_rmdir fails for symlinks! (LIBSSH2_ERROR_SFTP_PROTOCOL: LIBSSH2_FX_NO_SUCH_FILE)
runSftpCommand(login_, "libssh2_sftp_rmdir", //throw SysError, SysErrorSftpProtocol
[&](const SshSession::Details& sd) { return ::libssh2_sftp_rmdir(sd.sftpChannel, getLibssh2Path(folderPath)); }); //noexcept!
}
catch (const SysError& e)
{
throw FileError(replaceCpy(_("Cannot delete directory %x."), L"%x", fmtPath(getDisplayPath(folderPath))), e.toString());
}
}
void removeFolderIfExistsRecursion(const AfsPath& folderPath, //throw FileError
const std::function<void(const std::wstring& displayPath)>& onBeforeFileDeletion /*throw X*/,
const std::function<void(const std::wstring& displayPath)>& onBeforeSymlinkDeletion/*throw X*/,
const std::function<void(const std::wstring& displayPath)>& onBeforeFolderDeletion /*throw X*/) const override
{
//default implementation: folder traversal
AFS::removeFolderIfExistsRecursion(folderPath, onBeforeFileDeletion, onBeforeSymlinkDeletion, onBeforeFolderDeletion); //throw FileError, X
}
//----------------------------------------------------------------------------------------------------------------
AfsPath getServerRealPath(const std::string& sftpPath) const //throw SysError
{
const size_t bufSize = 10000;
std::vector<char> buf(bufSize + 1); //ensure buffer is always null-terminated since we don't evaluate the byte count returned by libssh2_sftp_realpath()!
int rc = 0;
runSftpCommand(login_, "libssh2_sftp_realpath", //throw SysError, SysErrorSftpProtocol
[&](const SshSession::Details& sd) { return rc = ::libssh2_sftp_realpath(sd.sftpChannel, sftpPath, buf.data(), bufSize); }); //noexcept!
const std::string_view sftpPathTrg = makeStringView(buf.data(), rc);
if (!startsWith(sftpPathTrg, '/'))
throw SysError(replaceCpy<std::wstring>(L"Invalid path %x.", L"%x", fmtPath(utfTo<std::wstring>(sftpPathTrg))));
return sanitizeDeviceRelativePath(utfTo<Zstring>(sftpPathTrg)); //code-reuse! but the sanitize part isn't really needed here...
}
AbstractPath getSymlinkResolvedPath(const AfsPath& linkPath) const override //throw FileError
{
try
{
const AfsPath linkPathTrg = getServerRealPath(getLibssh2Path(linkPath)); //throw SysError
return AbstractPath(makeSharedRef<SftpFileSystem>(login_), linkPathTrg);
}
catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot determine final path for %x."), L"%x", fmtPath(getDisplayPath(linkPath))), e.toString()); }
}
static std::string getSymlinkContentImpl(const SftpFileSystem& sftpFs, const AfsPath& linkPath) //throw SysError
{
std::string buf(10000, '\0');
int rc = 0;
runSftpCommand(sftpFs.login_, "libssh2_sftp_readlink", //throw SysError, SysErrorSftpProtocol
[&](const SshSession::Details& sd) { return rc = ::libssh2_sftp_readlink(sd.sftpChannel, getLibssh2Path(linkPath), buf.data(), buf.size()); }); //noexcept!
ASSERT_SYSERROR(makeUnsigned(rc) <= buf.size()); //better safe than sorry
buf.resize(rc);
return buf;
}
bool equalSymlinkContentForSameAfsType(const AfsPath& linkPathL, const AbstractPath& linkPathR) const override //throw FileError
{
auto getLinkContent = [](const SftpFileSystem& sftpFs, const AfsPath& linkPath)
{
try
{
return getSymlinkContentImpl(sftpFs, linkPath); //throw SysError
}
catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot resolve symbolic link %x."), L"%x", fmtPath(sftpFs.getDisplayPath(linkPath))), e.toString()); }
};
return getLinkContent(*this, linkPathL) == getLinkContent(static_cast<const SftpFileSystem&>(linkPathR.afsDevice.ref()), linkPathR.afsPath); //throw FileError
}
//----------------------------------------------------------------------------------------------------------------
//return value always bound:
std::unique_ptr<InputStream> getInputStream(const AfsPath& filePath) const override //throw FileError, (ErrorFileLocked)
{
return std::make_unique<InputStreamSftp>(login_, filePath); //throw FileError
}
//already existing: undefined behavior! (e.g. fail/overwrite/auto-rename)
//=> actual behavior: fail with obscure LIBSSH2_FX_FAILURE error
std::unique_ptr<OutputStreamImpl> getOutputStream(const AfsPath& filePath, //throw FileError
std::optional<uint64_t> streamSize,
std::optional<time_t> modTime) const override
{
return std::make_unique<OutputStreamSftp>(login_, filePath, modTime); //throw FileError
}
//----------------------------------------------------------------------------------------------------------------
void traverseFolderRecursive(const TraverserWorkload& workload /*throw X*/, size_t parallelOps) const override
{
traverseFolderRecursiveSftp(login_, workload /*throw X*/, parallelOps); //throw X
}
//----------------------------------------------------------------------------------------------------------------
//symlink handling: follow
//already existing: undefined behavior! (e.g. fail/overwrite/auto-rename)
FileCopyResult copyFileForSameAfsType(const AfsPath& sourcePath, const StreamAttributes& attrSource, //throw FileError, (ErrorFileLocked), X
const AbstractPath& targetPath, bool copyFilePermissions, const IoCallback& notifyUnbufferedIO /*throw X*/) const override
{
//no native SFTP file copy => use stream-based file copy:
if (copyFilePermissions)
throw FileError(replaceCpy(_("Cannot write permissions of %x."), L"%x", fmtPath(AFS::getDisplayPath(targetPath))), _("Operation not supported by device."));
//already existing: undefined behavior! (e.g. fail/overwrite/auto-rename)
return copyFileAsStream(sourcePath, attrSource, targetPath, notifyUnbufferedIO); //throw FileError, (ErrorFileLocked), X
}
//symlink handling: follow
//already existing: fail
void copyNewFolderForSameAfsType(const AfsPath& sourcePath, const AbstractPath& targetPath, bool copyFilePermissions) const override //throw FileError
{
//already existing: fail
AFS::createFolderPlain(targetPath); //throw FileError
if (copyFilePermissions)
throw FileError(replaceCpy(_("Cannot write permissions of %x."), L"%x", fmtPath(AFS::getDisplayPath(targetPath))), _("Operation not supported by device."));
}
//already existing: fail (SSH_FX_FAILURE)
void copySymlinkForSameAfsType(const AfsPath& sourcePath, const AbstractPath& targetPath, bool copyFilePermissions) const override //throw FileError
{
try
{
const std::string buf = getSymlinkContentImpl(*this, sourcePath); //throw SysError
runSftpCommand(static_cast<const SftpFileSystem&>(targetPath.afsDevice.ref()).login_, "libssh2_sftp_symlink", //throw SysError, SysErrorSftpProtocol
[&](const SshSession::Details& sd) //noexcept!
{
return ::libssh2_sftp_symlink(sd.sftpChannel, getLibssh2Path(targetPath.afsPath), buf);
});
}
catch (const SysError& e)
{
throw FileError(replaceCpy(replaceCpy(_("Cannot copy symbolic link %x to %y."),
L"%x", L'\n' + fmtPath(getDisplayPath(sourcePath))),
L"%y", L'\n' + fmtPath(AFS::getDisplayPath(targetPath))), e.toString());
}
}
//already existing: undefined behavior! (e.g. fail/overwrite)
//=> actual behavior: fail with obscure LIBSSH2_FX_FAILURE error
void moveAndRenameItemForSameAfsType(const AfsPath& pathFrom, const AbstractPath& pathTo) const override //throw FileError, ErrorMoveUnsupported
{
if (compareDeviceSameAfsType(pathTo.afsDevice.ref()) != std::weak_ordering::equivalent)
throw ErrorMoveUnsupported(generateMoveErrorMsg(pathFrom, pathTo), _("Operation not supported between different devices."));
try
{
runSftpCommand(login_, "libssh2_sftp_rename", //throw SysError, SysErrorSftpProtocol
[&](const SshSession::Details& sd) //noexcept!
{
/* LIBSSH2_SFTP_RENAME_NATIVE: "The server is free to do the rename operation in whatever way it chooses. Any other set flags are to be taken as hints to the server." No, thanks!
LIBSSH2_SFTP_RENAME_OVERWRITE: "No overwriting rename in [SFTP] v3/v4" https://www.greenend.org.uk/rjk/sftp/sftpversions.html
Test: LIBSSH2_SFTP_RENAME_OVERWRITE is not honored on freefilesync.org, no matter if LIBSSH2_SFTP_RENAME_NATIVE is set or not
=> makes sense since SFTP v3 does not honor the additional flags that libssh2 sends!
"... the most widespread SFTP server implementation, the OpenSSH, will fail the SSH_FXP_RENAME request if the target file already exists"
=> incidentally this is just the behavior we want! */
const std::string sftpPathOld = getLibssh2Path(pathFrom);
const std::string sftpPathNew = getLibssh2Path(pathTo.afsPath);
return ::libssh2_sftp_rename(sd.sftpChannel, sftpPathOld, sftpPathNew, LIBSSH2_SFTP_RENAME_ATOMIC);
});
}
catch (const SysError& e) //libssh2_sftp_rename_ex reports generic LIBSSH2_FX_FAILURE if target is already existing!
{
throw FileError(generateMoveErrorMsg(pathFrom, pathTo), e.toString());
}
}
bool supportsPermissions(const AfsPath& folderPath) const override { return false; } //throw FileError
//wait until there is real demand for copying from and to SFTP with permissions => use stream-based file copy:
//----------------------------------------------------------------------------------------------------------------
FileIconHolder getFileIcon (const AfsPath& filePath, int pixelSize) const override { return {}; } //throw FileError; optional return value
ImageHolder getThumbnailImage(const AfsPath& filePath, int pixelSize) const override { return {}; } //throw FileError; optional return value
void authenticateAccess(const RequestPasswordFun& requestPassword /*throw X*/) const override //throw FileError, X
{
try
{
const std::shared_ptr<SftpSessionManager> mgr = globalSftpSessionManager.get();
if (!mgr)
throw SysError(formatSystemError("getSessionPassword", L"", L"Function call not allowed during init/shutdown."));
mgr->setActiveConfig(login_);
if (login_.authType == SftpAuthType::password ||
login_.authType == SftpAuthType::keyFile)
if (!login_.password)
{
try //1. test for connection error *before* bothering user to enter a password
{
/*auto session =*/ mgr->getSharedSession(login_); //throw SysError, SysErrorPassword
return; //got new SshSession (connected in constructor) or already connected session from cache
}
catch (const SysErrorPassword& e)
{
if (!requestPassword)
throw SysError(e.toString() + L'\n' + _("Password prompt not permitted by current settings."));
}
std::wstring lastErrorMsg;
for (;;)
{
//2. request (new) password
std::wstring msg = replaceCpy(_("Please enter your password to connect to %x."), L"%x", fmtPath(getDisplayPath(AfsPath())));
if (lastErrorMsg.empty())
msg += L"\n" + _("The password will only be remembered until FreeFileSync is closed.");
const Zstring password = requestPassword(msg, lastErrorMsg); //throw X
mgr->setSessionPassword(login_, password, login_.authType);
try //3. test access:
{
/*auto session =*/ mgr->getSharedSession(login_); //throw SysError, SysErrorPassword
return;
}
catch (const SysErrorPassword& e) { lastErrorMsg = e.toString(); }
}
}
}
catch (const SysError& e) { throw FileError(replaceCpy(_("Unable to connect to %x."), L"%x", fmtPath(getDisplayPath(AfsPath()))), e.toString()); }
}
bool hasNativeTransactionalCopy() const override { return false; }
//----------------------------------------------------------------------------------------------------------------
int64_t getFreeDiskSpace(const AfsPath& folderPath) const override //throw FileError, returns < 0 if not available
{
//statvfs is an SFTP v3 extension and not supported by all server implementations
//Mikrotik SFTP server fails with LIBSSH2_FX_OP_UNSUPPORTED and corrupts session so that next SFTP call will hang
//(Server sends a duplicate SSH_FX_OP_UNSUPPORTED response with seemingly corrupt body and fails to respond from now on)
//https://freefilesync.org/forum/viewtopic.php?t=618
//Just discarding the current session is not enough in all cases, e.g. 1. Open SFTP file handle 2. statvfs fails 3. must close file handle
return -1;
#if 0
const std::string sftpPath = "/"; //::libssh2_sftp_statvfs will fail if path is not yet existing, OTOH root path should work, too?
//NO, for correctness we must check free space for the given folder!!
//"It is unspecified whether all members of the returned struct have meaningful values on all file systems."
LIBSSH2_SFTP_STATVFS fsStats = {};
try
{
runSftpCommand(login_, "libssh2_sftp_statvfs", //throw SysError, SysErrorSftpProtocol
[&](const SshSession::Details& sd) { return ::libssh2_sftp_statvfs(sd.sftpChannel, sftpPath.c_str(), sftpPath.size(), &fsStats); }); //noexcept!
}
catch (const SysError& e) { throw FileError(replaceCpy(_("Cannot determine free disk space for %x."), L"%x", fmtPath(getDisplayPath(L"/"))), e.toString()); }
static_assert(sizeof(fsStats.f_bsize) >= 8);
return fsStats.f_bsize * fsStats.f_bavail;
#endif
}
std::unique_ptr<RecycleSession> createRecyclerSession(const AfsPath& folderPath) const override //throw FileError, RecycleBinUnavailable
{
throw RecycleBinUnavailable(replaceCpy(_("The recycle bin is not available for %x."), L"%x", fmtPath(getDisplayPath(folderPath))));
}
void moveToRecycleBin(const AfsPath& itemPath) const override //throw FileError, RecycleBinUnavailable
{
throw RecycleBinUnavailable(replaceCpy(_("The recycle bin is not available for %x."), L"%x", fmtPath(getDisplayPath(itemPath))));
}
const SftpLogin login_;
};
//===========================================================================================================================
//expects "clean" login data
Zstring concatenateSftpFolderPathPhrase(const SftpLogin& login, const AfsPath& folderPath) //noexcept
{
Zstring username;
if (!login.username.empty())
username = encodeFtpUsername(login.username) + Zstr("@");
Zstring server = login.server;
if (parseIpv6Address(server) && login.portCfg > 0)
server = Zstr('[') + server + Zstr(']'); //e.g. [::1]:80
Zstring port;
if (login.portCfg > 0)
port = Zstr(':') + numberTo<Zstring>(login.portCfg);
Zstring relPath = getServerRelPath(folderPath);
if (relPath == Zstr("/"))
relPath.clear();
const SftpLogin loginDefault;
Zstring options;
if (login.timeoutSec != loginDefault.timeoutSec)
options += Zstr("|timeout=") + numberTo<Zstring>(login.timeoutSec);
if (login.traverserChannelsPerConnection != loginDefault.traverserChannelsPerConnection)
options += Zstr("|chan=") + numberTo<Zstring>(login.traverserChannelsPerConnection);
if (login.allowZlib)
options += Zstr("|zlib");
switch (login.authType)
{
case SftpAuthType::password:
break;
case SftpAuthType::keyFile:
options += Zstr("|keyfile=") + login.privateKeyFilePath;
break;
case SftpAuthType::agent:
options += Zstr("|agent");
break;
}
if (login.authType != SftpAuthType::agent)
{
if (login.password)
{
if (!login.password->empty()) //password always last => visually truncated by folder input field
options += Zstr("|pass64=") + encodePasswordBase64(*login.password);
}
else
options += Zstr("|pwprompt");
}
return Zstring(sftpPrefix) + Zstr("//") + username + server + port + relPath + options;
}
}
void fff::sftpInit()
{
assert(!globalSftpSessionManager.get());
globalSftpSessionManager.set(std::make_unique<SftpSessionManager>());
}
void fff::sftpTeardown()
{
assert(globalSftpSessionManager.get());
globalSftpSessionManager.set(nullptr);
}
AfsPath fff::getSftpHomePath(const SftpLogin& login) //throw FileError
{
return SftpFileSystem(login).getHomePath(); //throw FileError
}
AfsDevice fff::condenseToSftpDevice(const SftpLogin& login) //noexcept
{
//clean up input:
SftpLogin loginTmp = login;
trim(loginTmp.server);
trim(loginTmp.username);
trim(loginTmp.privateKeyFilePath);
loginTmp.timeoutSec = std::max(1, loginTmp.timeoutSec);
loginTmp.traverserChannelsPerConnection = std::max(1, loginTmp.traverserChannelsPerConnection);
if (startsWithAsciiNoCase(loginTmp.server, "http:" ) ||
startsWithAsciiNoCase(loginTmp.server, "https:") ||
startsWithAsciiNoCase(loginTmp.server, "ftp:" ) ||
startsWithAsciiNoCase(loginTmp.server, "ftps:" ) ||
startsWithAsciiNoCase(loginTmp.server, "sftp:" ))
loginTmp.server = afterFirst(loginTmp.server, Zstr(':'), IfNotFoundReturn::none);
trim(loginTmp.server, TrimSide::both, [](Zchar c) { return c == Zstr('/') || c == Zstr('\\'); });
if (std::optional<std::pair<Zstring, int>> ip6AndPort = parseIpv6Address(loginTmp.server))
loginTmp.server = ip6AndPort->first; //remove IPv6 leading/trailing brackets
return makeSharedRef<SftpFileSystem>(loginTmp);
}
SftpLogin fff::extractSftpLogin(const AfsDevice& afsDevice) //noexcept
{
if (const auto sftpDevice = dynamic_cast<const SftpFileSystem*>(&afsDevice.ref()))
return sftpDevice->getLogin();
assert(false);
return {};
}
int fff::getServerMaxChannelsPerConnection(const SftpLogin& login) //throw FileError
{
try
{
const auto timeoutTime = std::chrono::steady_clock::now() + SFTP_CHANNEL_LIMIT_DETECTION_TIME_OUT;
std::unique_ptr<SftpSessionManager::SshSessionExclusive> exSession = getExclusiveSftpSession(login); //throw SysError
ZEN_ON_SCOPE_EXIT(exSession->markAsCorrupted()); //after hitting the server limits, the session might have gone bananas (e.g. server fails on all requests)
for (;;)
{
try
{
SftpSessionManager::SshSessionExclusive::addSftpChannel({exSession.get()}); //throw SysError
}
catch (SysError&) { if (exSession->getSftpChannelCount() == 0) throw; return static_cast<int>(exSession->getSftpChannelCount()); }
if (std::chrono::steady_clock::now() > timeoutTime)
throw SysError(_P("Operation timed out after 1 second.", "Operation timed out after %x seconds.",
std::chrono::seconds(SFTP_CHANNEL_LIMIT_DETECTION_TIME_OUT).count()) + L' ' +
replaceCpy(_("Failed to open SFTP channel number %x."), L"%x", formatNumber(exSession->getSftpChannelCount() + 1)));
}
}
catch (const SysError& e)
{
throw FileError(replaceCpy(_("Unable to connect to %x."), L"%x", fmtPath(login.server)), e.toString());
}
}
bool fff::acceptsItemPathPhraseSftp(const Zstring& itemPathPhrase) //noexcept
{
Zstring path = expandMacros(itemPathPhrase); //expand before trimming!
trim(path);
return startsWithAsciiNoCase(path, sftpPrefix); //check for explicit SFTP path
}
/* syntax: sftp://[<user>[:<password>]@]<server>[:port]/<relative-path>[|option_name=value]
e.g. sftp://user001:secretpassword@private.example.com:222/mydirectory/
sftp://user001:secretpassword@[::1]:80/ipv6folder/
sftp://user001:secretpassword@::1/ipv6withoutPort/
sftp://user001@private.example.com/mydirectory|con=2|cpc=10|keyfile=%AppData%\id_rsa|pass64=c2VjcmV0cGFzc3dvcmQ */
AbstractPath fff::createItemPathSftp(const Zstring& itemPathPhrase) //noexcept
{
Zstring pathPhrase = expandMacros(itemPathPhrase); //expand before trimming!
trim(pathPhrase);
if (startsWithAsciiNoCase(pathPhrase, sftpPrefix))
pathPhrase = pathPhrase.c_str() + strLength(sftpPrefix);
trim(pathPhrase, TrimSide::left, [](Zchar c) { return c == Zstr('/') || c == Zstr('\\'); });
const ZstringView credentials = beforeFirst<ZstringView>(pathPhrase, Zstr('@'), IfNotFoundReturn::none);
const ZstringView fullPathOpt = afterFirst<ZstringView>(pathPhrase, Zstr('@'), IfNotFoundReturn::all);
SftpLogin login;
login.username = decodeFtpUsername(Zstring(beforeFirst(credentials, Zstr(':'), IfNotFoundReturn::all))); //support standard FTP syntax, even though
login.password = Zstring( afterFirst(credentials, Zstr(':'), IfNotFoundReturn::none)); //concatenateSftpFolderPathPhrase() uses "pass64" instead
const ZstringView fullPath = beforeFirst(fullPathOpt, Zstr('|'), IfNotFoundReturn::all);
const ZstringView options = afterFirst(fullPathOpt, Zstr('|'), IfNotFoundReturn::none);
auto it = std::find_if(fullPath.begin(), fullPath.end(), [](Zchar c) { return c == '/' || c == '\\'; });
const ZstringView serverPort = makeStringView(fullPath.begin(), it);
const AfsPath serverRelPath = sanitizeDeviceRelativePath({it, fullPath.end()});
if (std::optional<std::pair<Zstring, int /*optional: port*/>> ip6AndPort = parseIpv6Address(serverPort)) //e.g. 2001:db8::ff00:42:8329 or [::1]:80
{
login.server = ip6AndPort->first;
login.portCfg = ip6AndPort->second; //0 if empty
}
else
{
login.server = Zstring(beforeLast(serverPort, Zstr(':'), IfNotFoundReturn::all));
const ZstringView port = afterLast(serverPort, Zstr(':'), IfNotFoundReturn::none);
login.portCfg = stringTo<int>(port); //0 if empty
}
assert(login.allowZlib == false);
split(options, Zstr('|'), [&](ZstringView optPhrase)
{
optPhrase = trimCpy(optPhrase);
if (!optPhrase.empty())
{
if (startsWith(optPhrase, Zstr("timeout=")))
login.timeoutSec = stringTo<int>(afterFirst(optPhrase, Zstr('='), IfNotFoundReturn::none));
else if (startsWith(optPhrase, Zstr("chan=")))
login.traverserChannelsPerConnection = stringTo<int>(afterFirst(optPhrase, Zstr('='), IfNotFoundReturn::none));
else if (startsWith(optPhrase, Zstr("keyfile=")))
{
login.authType = SftpAuthType::keyFile;
login.privateKeyFilePath = getResolvedFilePath(Zstring(afterFirst(optPhrase, Zstr('='), IfNotFoundReturn::none)));
}
else if (optPhrase == Zstr("agent"))
login.authType = SftpAuthType::agent;
else if (startsWith(optPhrase, Zstr("pass64=")))
login.password = decodePasswordBase64(afterFirst(optPhrase, Zstr('='), IfNotFoundReturn::none));
else if (optPhrase == Zstr("pwprompt"))
login.password = std::nullopt;
else if (optPhrase == Zstr("zlib"))
login.allowZlib = true;
else
assert(false);
}
});
return AbstractPath(makeSharedRef<SftpFileSystem>(login), serverRelPath);
}