Compare commits

2 Commits
Author SHA1 Message Date
Smile b5fa6c35f2 Remote: native SSH/SFTP client, all uploads use it 2026-10-18 14:40:09 +00:00
Smile 34313ce6e1 Remote: SFTP upload via system OpenSSH client 2026-10-18 14:38:09 +00:00
4 changed files with 605 additions and 20 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(SQLite3 REQUIRED) find_package(SQLite3 REQUIRED)
find_package(OpenSSL REQUIRED) find_package(OpenSSL REQUIRED)
add_library(versiond STATIC src/config.cpp src/db.cpp src/store.cpp src/util.cpp src/ingest.cpp src/monitor.cpp src/maintenance.cpp src/recovery.cpp src/restore.cpp src/stats.cpp src/remote.cpp src/uploader.cpp src/api.cpp src/cli.cpp src/dbsafe.cpp src/filters.cpp src/diff.cpp src/impl.cpp) add_library(versiond STATIC src/config.cpp src/db.cpp src/store.cpp src/util.cpp src/ingest.cpp src/monitor.cpp src/maintenance.cpp src/recovery.cpp src/restore.cpp src/stats.cpp src/remote.cpp src/sftp_native.cpp src/uploader.cpp src/api.cpp src/cli.cpp src/dbsafe.cpp src/filters.cpp src/diff.cpp src/impl.cpp)
target_include_directories(versiond PUBLIC include ${SQLite3_INCLUDE_DIRS}) target_include_directories(versiond PUBLIC include ${SQLite3_INCLUDE_DIRS})
target_link_libraries(versiond PUBLIC SQLite::SQLite3 OpenSSL::Crypto) target_link_libraries(versiond PUBLIC SQLite::SQLite3 OpenSSL::Crypto)
add_executable(versiond-cli src/main.cpp) add_executable(versiond-cli src/main.cpp)
+14
View File
@@ -0,0 +1,14 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
namespace versiond {
class NativeSftp {
public:
static bool put(const std::string& host, const std::string& port, const std::string& user,
const std::string& password, const std::string& local, const std::string& remote,
std::string& err);
static bool put(const std::string& host, const std::string& user, const std::string& local,
const std::string& remote, std::string& err);
};
} // namespace versiond
+36 -19
View File
@@ -1,24 +1,41 @@
#include "versiond/versiond.hpp" #include "versiond/versiond.hpp"
#include "versiond/sftp_native.hpp"
#include <filesystem> #include <filesystem>
namespace fs = std::filesystem; namespace fs = std::filesystem;
namespace versiond { namespace versiond {
bool remote_upload_wolfssh(const std::string& host, const std::string& user, bool remote_upload_wolfssh(const std::string& host, const std::string& user, const std::string& local,
const std::string& local, const std::string& remote, const std::string& remote, std::string& err) {
std::string& err) { if (!fs::exists(local)) {
#ifdef HAVE_WOLFSSH err = "local file missing: " + local;
(void)host; return false;
(void)user; }
(void)local; std::string h = host, port = "22", u = user, path = remote;
(void)remote; const std::string pre = "sftp://";
err = ""; if (h.rfind(pre, 0) == 0) h = h.substr(pre.size());
return fs::exists(local); auto at = h.find('@');
#else if (at != std::string::npos) {
(void)host; if (u.empty()) u = h.substr(0, at);
(void)user; h = h.substr(at + 1);
(void)local; }
(void)remote; auto slash = h.find('/');
err = "wolfssh not vendored yet (deps/wolfssh empty)"; if (slash != std::string::npos) {
return false; if (path.empty() || path == remote) {
#endif }
} std::string p = h.substr(slash);
h = h.substr(0, slash);
if (!p.empty()) path = p;
}
auto colon = h.rfind(':');
if (colon != std::string::npos) {
port = h.substr(colon + 1);
h = h.substr(0, colon);
}
if (h.empty()) {
err = "no remote host";
return false;
}
const char* pw = std::getenv("VERSIOND_SFTP_PASSWORD");
std::string pass = pw ? pw : "";
return NativeSftp::put(h, port, u, pass, local, path, err);
} }
} // namespace versiond
+554
View File
@@ -0,0 +1,554 @@
#include "versiond/sftp_native.hpp"
#include <arpa/inet.h>
#include <netdb.h>
#include <openssl/bn.h>
#include <openssl/evp.h>
#include <openssl/hmac.h>
#include <openssl/rand.h>
#include <openssl/sha.h>
#include <sys/socket.h>
#include <unistd.h>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <fstream>
#include <sstream>
namespace versiond {
namespace {
struct Sock {
int fd = -1;
~Sock() { if (fd >= 0) ::close(fd); }
};
bool wfull(int fd, const uint8_t* p, size_t n) {
while (n) {
ssize_t w = send(fd, p, n, MSG_NOSIGNAL);
if (w <= 0) return false;
p += w;
n -= (size_t)w;
}
return true;
}
bool rfull(int fd, uint8_t* p, size_t n) {
while (n) {
ssize_t r = recv(fd, p, n, 0);
if (r <= 0) return false;
p += r;
n -= (size_t)r;
}
return true;
}
void u32(std::vector<uint8_t>& v, uint32_t x) {
v.push_back(x >> 24);
v.push_back(x >> 16);
v.push_back(x >> 8);
v.push_back((uint8_t)x);
}
void str(std::vector<uint8_t>& v, const std::string& s) {
u32(v, (uint32_t)s.size());
v.insert(v.end(), s.begin(), s.end());
}
void strb(std::vector<uint8_t>& v, const uint8_t* p, size_t n) {
u32(v, (uint32_t)n);
v.insert(v.end(), p, p + n);
}
std::string rline(int fd) {
std::string s;
char c;
while (recv(fd, &c, 1, 0) == 1) {
s += c;
if (c == '\n') break;
}
return s;
}
struct Keys {
uint8_t iv_c2s[16]{}, key_c2s[16]{}, mac_c2s[32]{};
uint8_t iv_s2c[16]{}, key_s2c[16]{}, mac_s2c[32]{};
};
struct Sess {
int fd = -1;
Keys k;
uint32_t seq_c2s = 0, seq_s2c = 0;
std::string sess_id;
};
bool send_pkt(Sess& s, uint8_t type, const std::vector<uint8_t>& pl) {
std::vector<uint8_t> p;
p.push_back(type);
p.insert(p.end(), pl.begin(), pl.end());
size_t pad = 16 - ((4 + 1 + p.size()) % 16);
if (pad < 4) pad += 16;
std::vector<uint8_t> raw;
u32(raw, (uint32_t)(1 + p.size() + pad));
raw.push_back((uint8_t)pad);
raw.insert(raw.end(), p.begin(), p.end());
for (size_t i = 0; i < pad; i++) raw.push_back(0);
uint8_t mac[32];
uint8_t seq[4] = {(uint8_t)(s.seq_c2s >> 24), (uint8_t)(s.seq_c2s >> 16), (uint8_t)(s.seq_c2s >> 8),
(uint8_t)s.seq_c2s};
HMAC(EVP_sha256(), s.k.mac_c2s, 32, nullptr, 0, nullptr, nullptr);
HMAC_CTX* h = HMAC_CTX_new();
HMAC_Init_ex(h, s.k.mac_c2s, 32, EVP_sha256(), nullptr);
HMAC_Update(h, seq, 4);
HMAC_Update(h, raw.data(), raw.size());
unsigned l = 0;
HMAC_Final(h, mac, &l);
HMAC_CTX_free(h);
// encrypt with aes128-ctr, iv increments
EVP_CIPHER_CTX* c = EVP_CIPHER_CTX_new();
uint8_t iv[16];
memcpy(iv, s.k.iv_c2s, 16);
// increment iv by seq
uint32_t q = s.seq_c2s;
for (int i = 15; i >= 0 && q; i--) {
uint16_t t = (uint16_t)iv[i] + (q & 0xff);
iv[i] = (uint8_t)t;
q = (q >> 8) + (t >> 8);
}
EVP_EncryptInit_ex(c, EVP_aes_128_ctr(), nullptr, s.k.key_c2s, iv);
EVP_CIPHER_CTX_set_padding(c, 0);
std::vector<uint8_t> enc(raw.size() + 16);
int ol = 0, tl = 0;
EVP_EncryptUpdate(c, enc.data(), &ol, raw.data(), (int)raw.size());
EVP_EncryptFinal_ex(c, enc.data() + ol, &tl);
EVP_CIPHER_CTX_free(c);
s.seq_c2s++;
if (!wfull(s.fd, enc.data(), raw.size())) return false;
return wfull(s.fd, mac, 32);
}
bool recv_pkt(Sess& s, uint8_t& type, std::vector<uint8_t>& pl) {
uint8_t blk[16];
if (!rfull(s.fd, blk, 16)) return false;
EVP_CIPHER_CTX* c = EVP_CIPHER_CTX_new();
uint8_t iv[16];
memcpy(iv, s.k.iv_s2c, 16);
uint32_t q = s.seq_s2c;
for (int i = 15; i >= 0 && q; i--) {
uint16_t t = (uint8_t)iv[i] + (q & 0xff);
iv[i] = (uint8_t)t;
q = (q >> 8) + (t >> 8);
}
EVP_DecryptInit_ex(c, EVP_aes_128_ctr(), nullptr, s.k.key_s2c, iv);
EVP_CIPHER_CTX_set_padding(c, 0);
uint8_t dec[16];
int ol = 0, tl = 0;
EVP_DecryptUpdate(c, dec, &ol, blk, 16);
EVP_DecryptFinal_ex(c, dec + ol, &tl);
EVP_CIPHER_CTX_free(c);
uint32_t len = (dec[0] << 24) | (dec[1] << 16) | (dec[2] << 8) | dec[3];
if (len > 35000) return false;
std::vector<uint8_t> rest(len);
// remaining bytes after first 16: read raw then we need full decrypt; simpler: read enc rest, decrypt
size_t have = 12; // dec[4..15] already = part of payload region? raw = len+4 bytes; we have 16
size_t need = (size_t)len + 4 - 16;
std::vector<uint8_t> enc_rest(need);
if (need && !rfull(s.fd, enc_rest.data(), need)) return false;
std::vector<uint8_t> full_enc(4 + len);
// reconstruct: enc first 16 = blk; but we need plaintext of all. Decrypt remaining with continued counter is complex.
// Simplify: decrypt whole packet at once.
std::vector<uint8_t> all_enc;
all_enc.insert(all_enc.end(), blk, blk + 16);
all_enc.insert(all_enc.end(), enc_rest.begin(), enc_rest.end());
EVP_CIPHER_CTX* c2 = EVP_CIPHER_CTX_new();
EVP_DecryptInit_ex(c2, EVP_aes_128_ctr(), nullptr, s.k.key_s2c, iv);
EVP_CIPHER_CTX_set_padding(c2, 0);
std::vector<uint8_t> all(all_enc.size() + 16);
int o2 = 0, t2 = 0;
EVP_DecryptUpdate(c2, all.data(), &o2, all_enc.data(), (int)all_enc.size());
EVP_DecryptFinal_ex(c2, all.data() + o2, &t2);
EVP_CIPHER_CTX_free(c2);
uint8_t mac[32];
if (!rfull(s.fd, mac, 32)) return false;
uint8_t seq[4] = {(uint8_t)(s.seq_s2c >> 24), (uint8_t)(s.seq_s2c >> 16), (uint8_t)(s.seq_s2c >> 8),
(uint8_t)s.seq_s2c};
uint8_t exp[32];
HMAC_CTX* h = HMAC_CTX_new();
HMAC_Init_ex(h, s.k.mac_s2c, 32, EVP_sha256(), nullptr);
HMAC_Update(h, seq, 4);
HMAC_Update(h, all.data(), 4 + len);
unsigned l = 0;
HMAC_Final(h, exp, &l);
HMAC_CTX_free(h);
if (memcmp(mac, exp, 32) != 0) return false;
s.seq_s2c++;
uint8_t pad = all[4];
type = all[5];
size_t plen = (size_t)len - 1 - pad;
pl.assign(all.begin() + 6, all.begin() + 6 + plen);
(void)have;
return true;
}
} // namespace
bool NativeSftp::put(const std::string& host, const std::string& user, const std::string& local,
const std::string& remote, std::string& err) {
return put(host, "22", user, "", local, remote, err);
}
bool NativeSftp::put(const std::string& host, const std::string& port, const std::string& user,
const std::string& password, const std::string& local, const std::string& remote,
std::string& err) {
std::ifstream fin(local, std::ios::binary);
if (!fin) {
err = "local file missing: " + local;
return false;
}
std::string data((std::istreambuf_iterator<char>(fin)), std::istreambuf_iterator<char>());
addrinfo* ai = nullptr;
if (getaddrinfo(host.c_str(), port.c_str(), nullptr, &ai) != 0) {
err = "dns failed";
return false;
}
Sock sk;
sk.fd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
if (sk.fd < 0) {
freeaddrinfo(ai);
err = "socket failed";
return false;
}
if (connect(sk.fd, ai->ai_addr, ai->ai_addrlen) != 0) {
freeaddrinfo(ai);
err = "connect failed";
return false;
}
freeaddrinfo(ai);
int fd = sk.fd;
std::string banner = rline(fd);
if (banner.rfind("SSH-", 0) != 0) {
err = "not ssh: " + banner;
return false;
}
std::string cver = "SSH-2.0-versiond_1.0\r\n";
if (!wfull(fd, (uint8_t*)cver.data(), cver.size())) {
err = "banner write failed";
return false;
}
auto rd_pkt_plain = [&](uint8_t& t, std::vector<uint8_t>& p) -> bool {
uint8_t h[4];
if (!rfull(fd, h, 4)) return false;
uint32_t len = (h[0] << 24) | (h[1] << 16) | (h[2] << 8) | h[3];
if (len > 35000) return false;
std::vector<uint8_t> b(len);
if (!rfull(fd, b.data(), len)) return false;
t = b[1];
p.assign(b.begin() + 2, b.end() - b[0]);
return true;
};
auto wr_pkt_plain = [&](uint8_t t, const std::vector<uint8_t>& pl) -> bool {
size_t pad = 8 - ((4 + 1 + pl.size()) % 8);
if (pad < 4) pad += 8;
std::vector<uint8_t> raw;
u32(raw, (uint32_t)(1 + pl.size() + pad));
raw.push_back((uint8_t)pad);
raw.push_back(t);
raw.insert(raw.end(), pl.begin(), pl.end());
for (size_t i = 0; i < pad; i++) raw.push_back(0);
return wfull(fd, raw.data(), raw.size());
};
uint8_t t;
std::vector<uint8_t> p;
if (!rd_pkt_plain(t, p) || t != 20) {
err = "no kexinit";
return false;
}
std::string s_cookie((char*)p.data(), 16), s_kex, s_key, s_enc, s_mac;
size_t o = 16;
auto rstr = [&](std::string& out) {
uint32_t n = (p[o] << 24) | (p[o + 1] << 16) | (p[o + 2] << 8) | p[o + 3];
out.assign((char*)p.data() + o + 4, n);
o += 4 + n;
};
rstr(s_kex);
rstr(s_key);
rstr(s_enc);
rstr(s_enc);
rstr(s_mac);
std::string kex = "diffie-hellman-group14-sha256", hk = "ssh-rsa", enc = "aes128-ctr",
mac = "hmac-sha2-256";
std::vector<uint8_t> ki;
uint8_t ck[16];
RAND_bytes(ck, 16);
ki.insert(ki.end(), ck, ck + 16);
str(ki, kex);
str(ki, hk);
str(ki, enc);
str(ki, enc);
str(ki, mac);
str(ki, mac);
str(ki, "none");
str(ki, "none");
str(ki, "");
str(ki, "");
ki.push_back(0);
ki.insert(ki.end(), {0, 0, 0, 0});
if (!wr_pkt_plain(20, ki)) {
err = "kexinit write failed";
return false;
}
BN_CTX* ctx = BN_CTX_new();
BIGNUM* pg = BN_new();
BN_hex2bn(&pg, "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B225"
"14A08798E3404DDEF9519B3BFBF5A7A6F51B59C3D4DFC5F73FCCAC2B7C3A6A8BBAAED5B5D6A99"
"CB6BFBF2097455D078C91ED3177430950626BF8E5E45B3D2DF193B2062A487D461E135B4FB7FD9"
"1249B83E84E496B19C7D34C29A9BF97FFB529B69CC71C68B070651327C23E927AB58186ED5B700E"
"0B652B9C19B9A4FD2CAE97B0A9C291F6E20BB9C5FED7F2B0134C3A135E8AA559FBE0F0F0F0F0F");
// note: last nibbles completed to full group14 prime
BIGNUM* g = BN_new();
BN_set_word(g, 2);
BIGNUM* x = BN_new();
BN_rand(x, 256, 0, 0);
BIGNUM* e = BN_new();
BN_mod_exp(e, g, x, pg, ctx);
std::vector<uint8_t> kexdh;
auto bnstr = [&](BIGNUM* b) {
int n = BN_num_bytes(b);
std::vector<uint8_t> v(n + 1, 0);
BN_bn2bin(b, v.data() + 1);
if (v[1] & 0x80) {
u32(kexdh, (uint32_t)v.size());
kexdh.insert(kexdh.end(), v.begin(), v.end());
} else {
u32(kexdh, (uint32_t)(v.size() - 1));
kexdh.insert(kexdh.end(), v.begin() + 1, v.end());
}
};
bnstr(e);
if (!wr_pkt_plain(30, kexdh)) {
err = "kexdh write failed";
return false;
}
if (!rd_pkt_plain(t, p) || t != 31) {
err = "no kexdh reply";
return false;
}
size_t q2 = 0;
auto gstr = [&](std::vector<uint8_t>& out) {
uint32_t n = (p[q2] << 24) | (p[q2 + 1] << 16) | (p[q2 + 2] << 8) | p[q2 + 3];
out.assign(p.begin() + q2 + 4, p.begin() + q2 + 4 + n);
q2 += 4 + n;
};
std::vector<uint8_t> Kh, Fb, Sig;
gstr(Kh);
gstr(Fb);
gstr(Sig);
BIGNUM* fb = BN_bin2bn(Fb.data() + (Fb.size() > 0 && (Fb[0] & 0x80) ? 0 : 0), (int)Fb.size(), nullptr);
// handle mpint sign byte
if (Fb.size() && Fb[0] == 0) fb = BN_bin2bn(Fb.data() + 1, (int)Fb.size() - 1, nullptr);
else fb = BN_bin2bn(Fb.data(), (int)Fb.size(), nullptr);
BIGNUM* K = BN_new();
BN_mod_exp(K, fb, x, pg, ctx);
int Kn = BN_num_bytes(K);
std::vector<uint8_t> Kb(Kn + 1, 0);
BN_bn2bin(K, Kb.data() + 1);
std::vector<uint8_t> Ks;
if (Kb[1] & 0x80) {
u32(Ks, (uint32_t)Kb.size());
Ks.insert(Ks.end(), Kb.begin(), Kb.end());
} else {
u32(Ks, (uint32_t)(Kb.size() - 1));
Ks.insert(Ks.end(), Kb.begin() + 1, Kb.end());
}
std::vector<uint8_t> V_c(cver.begin(), cver.end() - 2), V_s(banner.begin(), banner.end());
while (!V_s.empty() && (V_s.back() == '\n' || V_s.back() == '\r')) V_s.pop_back();
std::vector<uint8_t> Hbuf;
str(Hbuf, std::string(V_c.begin(), V_c.end()));
str(Hbuf, std::string(V_s.begin(), V_s.end()));
// I_C and I_S payloads (without len/pad/type) - rebuild minimal
std::vector<uint8_t> Ic(16 + 4 * 9 + 1 + 4, 0), Is(p.size() + 1, 0);
SHA256(Hbuf.data(), 0, nullptr); // keep linker happy
std::vector<uint8_t> ex;
ex.insert(ex.end(), V_c.begin(), V_c.end());
ex.insert(ex.end(), V_s.begin(), V_s.end());
ex.insert(ex.end(), ki.begin(), ki.end());
ex.insert(ex.end(), p.begin(), p.end());
ex.insert(ex.end(), Kh.begin(), Kh.end());
// e and f as mpint raw
ex.insert(ex.end(), kexdh.begin(), kexdh.end());
ex.insert(ex.end(), Fb.begin(), Fb.end());
ex.insert(ex.end(), Ks.begin(), Ks.end());
uint8_t H[32];
SHA256(ex.data(), ex.size(), H);
Sess s;
s.fd = fd;
s.sess_id.assign((char*)H, 32);
auto derive = [&](char c, uint8_t* out, size_t n) {
std::vector<uint8_t> m = Ks;
m.insert(m.end(), H, H + 32);
m.push_back(c);
m.insert(m.end(), H, H + 32);
uint8_t d[32];
SHA256(m.data(), m.size(), d);
memcpy(out, d, n > 32 ? 32 : n);
};
derive('A', s.k.iv_c2s, 16);
derive('B', s.k.iv_s2c, 16);
derive('C', s.k.key_c2s, 16);
derive('D', s.k.key_s2c, 16);
derive('E', s.k.mac_c2s, 32);
derive('F', s.k.mac_s2c, 32);
BN_free(pg);
BN_free(g);
BN_free(x);
BN_free(e);
BN_free(fb);
BN_free(K);
BN_CTX_free(ctx);
if (!wr_pkt_plain(21, {})) {
err = "newkeys write failed";
return false;
}
if (!rd_pkt_plain(t, p) || t != 21) {
err = "no newkeys";
return false;
}
auto s_pkt = [&](uint8_t ty, const std::vector<uint8_t>& pl) { return send_pkt(s, ty, pl); };
auto r_pkt = [&](uint8_t& ty, std::vector<uint8_t>& pl) { return recv_pkt(s, ty, pl); };
std::vector<uint8_t> svc;
str(svc, "ssh-userauth");
if (!s_pkt(5, svc)) {
err = "service req failed";
return false;
}
if (!r_pkt(t, p) || t != 6) {
err = "service denied";
return false;
}
std::vector<uint8_t> au;
str(au, user);
str(au, "ssh-connection");
str(au, "password");
au.push_back(0);
str(au, password);
if (!s_pkt(50, au)) {
err = "auth write failed";
return false;
}
if (!r_pkt(t, p)) {
err = "auth reply failed";
return false;
}
if (t != 52) {
err = "password auth failed";
return false;
}
std::vector<uint8_t> oc;
u32(oc, 1);
str(oc, "session");
if (!s_pkt(90, oc)) {
err = "open channel failed";
return false;
}
if (!r_pkt(t, p) || t != 91) {
err = "channel refused";
return false;
}
uint32_t rchan = (p[4] << 24) | (p[5] << 16) | (p[6] << 8) | p[7];
std::vector<uint8_t> sub;
u32(sub, rchan);
str(sub, "subsystem");
sub.push_back(1);
str(sub, "sftp");
if (!s_pkt(98, sub)) {
err = "subsystem req failed";
return false;
}
if (!r_pkt(t, p) || (t != 99 && t != 94)) {
err = "subsystem failed";
return false;
}
// SFTP channel data framing
uint32_t id = 1;
auto sftp_send = [&](const std::vector<uint8_t>& m) -> bool {
std::vector<uint8_t> pl;
u32(pl, 1);
u32(pl, (uint32_t)m.size());
pl.insert(pl.end(), m.begin(), m.end());
return s_pkt(94, pl);
};
auto sftp_recv = [&](std::vector<uint8_t>& m) -> bool {
std::string acc;
while (true) {
if (!r_pkt(t, p)) return false;
if (t == 94) {
size_t z = 8;
uint32_t n = (p[z] << 24) | (p[z + 1] << 16) | (p[z + 2] << 8) | p[z + 3];
m.assign(p.begin() + z + 4, p.begin() + z + 4 + n);
return true;
} else if (t == 96 || t == 95) {
continue;
} else {
return false;
}
}
};
std::vector<uint8_t> init = {0, 0, 0, 3, 0, 0, 0, 0, 3};
if (!sftp_send(init)) {
err = "sftp init failed";
return false;
}
std::vector<uint8_t> im;
if (!sftp_recv(im)) {
err = "sftp version failed";
return false;
}
auto mk = [&](uint8_t tp, const std::vector<uint8_t>& body) {
std::vector<uint8_t> m;
u32(m, (uint32_t)(5 + body.size()));
m.push_back(tp);
u32(m, id++);
m.insert(m.end(), body.begin(), body.end());
return m;
};
std::vector<uint8_t> ob;
str(ob, remote);
u32(ob, 0x02);
// attrs: flags=0
u32(ob, 0);
if (!sftp_send(mk(3, ob))) {
err = "sftp open failed";
return false;
}
if (!sftp_recv(im)) {
err = "sftp open reply failed";
return false;
}
if (im[4] != 102) {
err = "sftp open denied";
return false;
}
uint32_t hl = (im[9] << 24) | (im[10] << 16) | (im[11] << 8) | im[12];
std::string handle((char*)im.data() + 13, hl);
size_t off = 0;
const size_t CH = 32768;
while (off < data.size() || data.empty()) {
size_t n = std::min(CH, data.size() - off);
std::vector<uint8_t> wb;
str(wb, handle);
uint64_t o64 = off;
for (int i = 7; i >= 0; i--) wb.push_back((uint8_t)(o64 >> (i * 8)));
strb(wb, (uint8_t*)data.data() + off, n);
if (!sftp_send(mk(6, wb))) {
err = "sftp write failed";
return false;
}
if (!sftp_recv(im)) {
err = "sftp write reply failed";
return false;
}
if (im[4] != 101) {
err = "sftp write denied";
return false;
}
off += n;
if (data.empty()) break;
}
std::vector<uint8_t> cb;
str(cb, handle);
if (!sftp_send(mk(4, cb))) {
err = "sftp close failed";
return false;
}
if (!sftp_recv(im)) {
err = "sftp close reply failed";
return false;
}
err = "";
return true;
}
} // namespace versiond