Public Access
Add uname, /uname and scp (key or masked password) in the Shell
- uname in the console and /uname in IRC: the firmware, its version and the chip - scp: one file between the card and a trusted server, over SSH, with the device's key or a password asked (masked) in the Shell - shared libssh helpers in src/platform/libssh_util - README, user guide, command reference and a how-to updated Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0162FokPdvY2KsS4NBfwyWPk
This commit is contained in:
+32
-1
@@ -33,6 +33,9 @@ void ShellApp::onEnter() {
|
||||
|
||||
// Nothing is kept once it's left: the ring, the lines and the list of commands all go (Q207).
|
||||
void ShellApp::onExit() {
|
||||
if (password_ && abort) abort();
|
||||
password_ = false;
|
||||
input_.setText("");
|
||||
console.closeShellRing();
|
||||
console.shellShowsAll(false);
|
||||
open_ = false;
|
||||
@@ -49,6 +52,11 @@ void ShellApp::update(uint32_t) {
|
||||
if (skipped) log_.add("[... " + std::to_string(skipped) + " bytes lost: more was printed than fits]");
|
||||
log_.feed(reinterpret_cast<const char*>(buf), n);
|
||||
}
|
||||
if (!password_ && asking && asking()) { // after the question has been printed above
|
||||
password_ = true;
|
||||
input_.setText("");
|
||||
requestRedraw();
|
||||
}
|
||||
if (log_.revision() != seenRevision_) {
|
||||
seenRevision_ = log_.revision();
|
||||
requestRedraw();
|
||||
@@ -114,6 +122,23 @@ bool ShellApp::onKey(const KeyEvent& e) {
|
||||
log_.add(console.shellShowsAll() ? "Showing everything the console prints." : "Showing only the replies to your commands.");
|
||||
return true;
|
||||
}
|
||||
if (password_) { // masked, kept nowhere: not in the history, not in the log
|
||||
switch (e.key) {
|
||||
case Key::Char: input_.insert(e.ch); break;
|
||||
case Key::Delete: input_.backspace(); break;
|
||||
case Key::Select: {
|
||||
std::string typed = input_.text();
|
||||
input_.setText("");
|
||||
password_ = false;
|
||||
log_.add("> " + std::string(typed.size(), '*'));
|
||||
if (answer) answer(typed);
|
||||
typed.assign(typed.size(), '\0');
|
||||
break;
|
||||
}
|
||||
default: return false; // Back leaves the Shell, which cancels the copy
|
||||
}
|
||||
return true;
|
||||
}
|
||||
std::string recalled;
|
||||
switch (e.key) {
|
||||
case Key::Char: input_.insert(e.ch); break;
|
||||
@@ -197,7 +222,13 @@ void ShellApp::draw(Canvas& c) {
|
||||
}
|
||||
c.setTextDatum(top_left);
|
||||
|
||||
widgets::lineEditor(c, input_, {2, area.y + area.h - inputH, area.w - 4, 0});
|
||||
if (password_) {
|
||||
LineEditor stars(240);
|
||||
stars.setText(std::string(input_.text().size(), '*'));
|
||||
widgets::lineEditor(c, stars, {2, area.y + area.h - inputH, area.w - 4, 0});
|
||||
} else {
|
||||
widgets::lineEditor(c, input_, {2, area.y + area.h - inputH, area.w - 4, 0});
|
||||
}
|
||||
if (confirm_) widgets::dialog(c, "Delete?", question_, *confirm_);
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,12 @@ class ShellApp : public App {
|
||||
ShellApp(Run run, Probe probe, List list, Count count, const char* helpText, AppManager& apps)
|
||||
: run_(std::move(run)), probe_(std::move(probe)), list_(std::move(list)), count_(std::move(count)), helpText_(helpText), apps_(apps) {}
|
||||
|
||||
// `scp` asking for a password: while asking() is true, the next line typed is masked and goes to
|
||||
// answer() instead of the console; leaving the Shell calls abort().
|
||||
std::function<bool()> asking;
|
||||
std::function<void(const std::string&)> answer;
|
||||
std::function<void()> abort;
|
||||
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
bool onKey(const KeyEvent& e) override;
|
||||
@@ -63,6 +69,7 @@ class ShellApp : public App {
|
||||
uint32_t ringPos_ = 0, seenRevision_ = 0;
|
||||
int scroll_ = 0; // wrapped lines scrolled back from the bottom
|
||||
bool open_ = false;
|
||||
bool password_ = false; // the line being typed is a password
|
||||
};
|
||||
|
||||
} // namespace roro
|
||||
|
||||
+26
-2
@@ -14,6 +14,7 @@
|
||||
#include "apps/shell_app.h"
|
||||
#include "apps/ssh_app.h"
|
||||
#include "services/net_tools.h"
|
||||
#include "services/scp_service.h"
|
||||
#include "services/ssh_service.h"
|
||||
#include "services/vpn_service.h"
|
||||
#include "services/web_share.h"
|
||||
@@ -63,6 +64,7 @@
|
||||
#include "storage_paths.h"
|
||||
#include "ui/notifier.h"
|
||||
#include "ui/screen.h"
|
||||
#include "scp_args.h"
|
||||
#include "version.h"
|
||||
|
||||
using namespace roro;
|
||||
@@ -95,6 +97,8 @@ static VpnService* vpnService; // not in Safe Mode
|
||||
static NetTools netTools; // ping, nslookup and the rest (issue #90)
|
||||
static SshService* sshService; // not in Safe Mode (issue #2)
|
||||
static SshApp* sshApp;
|
||||
static ShellApp* shellApp;
|
||||
static ScpService* scpService; // not in Safe Mode
|
||||
static Notifier* notifier;
|
||||
static LauncherApp launcher;
|
||||
static AppManager* apps;
|
||||
@@ -235,8 +239,13 @@ void setup() {
|
||||
apps->registerApp({"notes", "Notes", false, new NotesApp(*fileOps, *storageService, *clockService, *power)});
|
||||
sshService = new SshService(settings);
|
||||
sshApp = new SshApp(*sshService, settings, *storageService);
|
||||
scpService = new ScpService(settings, *storageService, *fileOps);
|
||||
apps->registerApp({"ssh", "SSH", false, sshApp});
|
||||
apps->registerApp({"shell", "Shell", false, new ShellApp(shellRun, shellProbe, shellList, shellCount, helpText(), *apps)});
|
||||
shellApp = new ShellApp(shellRun, shellProbe, shellList, shellCount, helpText(), *apps);
|
||||
apps->registerApp({"shell", "Shell", false, shellApp});
|
||||
shellApp->asking = [] { return scpService->asking(); };
|
||||
shellApp->answer = [](const std::string& password) { scpService->answer(password); };
|
||||
shellApp->abort = [] { scpService->cancel(); };
|
||||
// Leaving the foreground App makes it save: a note being typed, when the device is powered off.
|
||||
power->beforePowerOff = []() { apps->home(); };
|
||||
netTools.ntpServer = []() { return settings.getString(Setting::Ntp1); };
|
||||
@@ -688,6 +697,8 @@ static const char* const kHelp =
|
||||
"ping <host> [count] [size] | nslookup <name> [server] | port <host> <port> | traceroute <host> | cancel is it there, does its name resolve, is its port open, which way; one at a time\n"
|
||||
"tls <host> [port] | ntp [server] a TLS handshake: who the certificate is for, by whom, until when, and whether this device trusts it; a time server's clock against this one\n"
|
||||
"ifconfig | arp | netstat the interfaces (Wi-Fi and the VPN), their addresses, the default route and the DNS servers; the neighbours heard; what listens and what is connected\n"
|
||||
"uname the firmware, its version and the chip it is built for\n"
|
||||
"scp [-f] [-P port] <file on the card> user@host:path | scp [-f] [-P port] user@host:path <file on the card> one file over SSH, with this device's key or, in the Shell, a password, to a server the SSH App already trusts; -f replaces a file on the card; `cancel` stops it\n"
|
||||
"ssh user@host[:port] | ssh status | ssh stop a terminal on another machine, in the SSH App; the password is asked there, never here\n"
|
||||
"vpn status | vpn up [seconds] | vpn down | vpn import [path] | vpn forget | vpn auto on|off the WireGuard tunnel (Settings > VPN); import reads /vpn/wg0.conf; with seconds, it goes down by itself\n"
|
||||
"debug status | debug off [seconds] the Debug Console over Wi-Fi (Settings > Debug Console); with seconds, it comes back\n"
|
||||
@@ -771,6 +782,16 @@ static void sshCommand(const String& arg) {
|
||||
}
|
||||
}
|
||||
|
||||
// `scp ...`: see ScpService. It needs the key and the trusted server the SSH App sets up.
|
||||
static void scpCommand(const String& arg) {
|
||||
if (!scpService) return (void)console.println("scp: not available in Safe Mode");
|
||||
term::ScpArgs args;
|
||||
std::string why = term::parseScp(arg.c_str(), args);
|
||||
if (!why.empty()) return (void)console.printf("scp: %s\n", why.c_str());
|
||||
why = scpService->start(args, sshService && sshService->state() != SshService::State::Idle && sshService->state() != SshService::State::Ended);
|
||||
if (!why.empty()) console.printf("scp: error %s\n", why.c_str());
|
||||
}
|
||||
|
||||
// `vpn ...` (issue #8). Nothing here prints a key.
|
||||
static void vpnCommand(const String& arg) {
|
||||
if (!vpnService) return (void)console.println("vpn: not available in Safe Mode");
|
||||
@@ -840,7 +861,7 @@ static void screenshotStep() {
|
||||
|
||||
// Commands that only touch what Safe Mode starts.
|
||||
static bool safeModeCommand(const String& line) {
|
||||
return line == "help" || line == "info" || line == "tasks" || line == "net" || line == "reboot" || line == "boot other" ||
|
||||
return line == "help" || line == "uname" || line == "info" || line == "tasks" || line == "net" || line == "reboot" || line == "boot other" ||
|
||||
line.startsWith("log level ") || line.startsWith("crash") || line.startsWith("coredump") ||
|
||||
line == "wifi status" || line.startsWith("wifi add ") || line.startsWith("debug ");
|
||||
}
|
||||
@@ -905,6 +926,9 @@ static void runCommand(String line, bool fromSerial = false) {
|
||||
}
|
||||
if (netTools.command(line.c_str())) return;
|
||||
if (line == "cancel") netTools.cancel(); // and a copy or a delete, below
|
||||
if (line == "uname" || line == "uname -a") return (void)console.println(unameString().c_str());
|
||||
if (line.startsWith("scp ")) return scpCommand(line.substring(4));
|
||||
if (line == "cancel" && scpService) scpService->cancel();
|
||||
if (line == "ssh" || line.startsWith("ssh ")) return sshCommand(line.length() > 4 ? line.substring(4) : String("status"));
|
||||
if (line == "vpn" || line.startsWith("vpn ")) return vpnCommand(line.length() > 4 ? line.substring(4) : String("status"));
|
||||
if (line.startsWith("debug ")) return debugCommand(line.substring(6), fromSerial);
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
#include "platform/libssh_util.h"
|
||||
|
||||
#include <libssh_esp32.h>
|
||||
|
||||
namespace roro::sshutil {
|
||||
|
||||
std::string fingerprintOf(ssh_session s) {
|
||||
ssh_key key = nullptr;
|
||||
unsigned char* hash = nullptr;
|
||||
size_t len = 0;
|
||||
std::string out;
|
||||
if (ssh_get_server_publickey(s, &key) == SSH_OK && ssh_get_publickey_hash(key, SSH_PUBLICKEY_HASH_SHA256, &hash, &len) == SSH_OK) {
|
||||
if (char* text = ssh_get_fingerprint_hash(SSH_PUBLICKEY_HASH_SHA256, hash, len)) {
|
||||
out = text; // "SHA256:..."
|
||||
ssh_string_free_char(text);
|
||||
}
|
||||
ssh_clean_pubkey_hash(&hash);
|
||||
}
|
||||
if (key) ssh_key_free(key);
|
||||
return out;
|
||||
}
|
||||
|
||||
void startLibrary() {
|
||||
static bool started = false;
|
||||
if (!started) libssh_begin();
|
||||
started = true;
|
||||
}
|
||||
|
||||
} // namespace roro::sshutil
|
||||
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <libssh/libssh.h>
|
||||
|
||||
// What the SSH Service and the Scp Service both do with libssh.
|
||||
namespace roro::sshutil {
|
||||
|
||||
// libssh_begin(), once.
|
||||
void startLibrary();
|
||||
// "SHA256:..." of the key the server showed, or "".
|
||||
std::string fingerprintOf(ssh_session s);
|
||||
|
||||
} // namespace roro::sshutil
|
||||
@@ -0,0 +1,269 @@
|
||||
#include "services/scp_service.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <SD.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <libssh/libssh.h>
|
||||
|
||||
#include "platform/libssh_util.h"
|
||||
|
||||
namespace roro {
|
||||
|
||||
namespace {
|
||||
constexpr uint32_t kStack = 20480; // as the SSH Service's
|
||||
constexpr size_t kPiece = 4096;
|
||||
|
||||
std::string baseName(const std::string& path) {
|
||||
size_t slash = path.rfind('/');
|
||||
return slash == std::string::npos ? path : path.substr(slash + 1);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
struct ScpService::Job {
|
||||
ScpService* owner;
|
||||
Console::Origin from;
|
||||
term::ScpArgs args;
|
||||
std::string privateKey, known;
|
||||
|
||||
bool stopped() const { return owner->stop_; }
|
||||
bool onCard(const std::function<void()>& work) { return owner->storage_.runAndWait(work); }
|
||||
void run();
|
||||
bool login(ssh_session s);
|
||||
void upload(ssh_session s);
|
||||
void download(ssh_session s);
|
||||
};
|
||||
|
||||
std::string ScpService::start(const term::ScpArgs& args, bool sshOpen) {
|
||||
if (busy_) return "A copy is running: `cancel` stops it";
|
||||
if (sshOpen) return "An SSH session is open: close it first, there isn't memory for both";
|
||||
if (ESP.getFreeHeap() < kNeedFree) return "Not enough memory: close IRC or a Gemini page";
|
||||
std::string key = settings_.getString(Setting::SshKey);
|
||||
if (!args.upload) {
|
||||
if (std::string why = files_.whyReadOnly(args.local, false); !why.empty()) return why;
|
||||
bool exists = false;
|
||||
if (!storage_.runAndWait([&]() { exists = SD.exists(args.local.c_str()); })) return "The card isn't answering";
|
||||
if (exists && !args.replace) return args.local + " exists already: scp -f replaces it";
|
||||
}
|
||||
auto* job = new Job{this, console.origin(), args, key, term::SshKnownHosts(settings_.getString(Setting::SshKnown)).fingerprintOf(args.target.hostPort())};
|
||||
stop_ = asking_ = answered_ = false;
|
||||
busy_ = true;
|
||||
if (xTaskCreate(task, "scp", kStack, job, 1, nullptr) != pdPASS) {
|
||||
busy_ = false;
|
||||
job->privateKey.assign(job->privateKey.size(), '\0');
|
||||
delete job;
|
||||
return "Not enough memory for it";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
void ScpService::task(void* arg) {
|
||||
Job* job = static_cast<Job*>(arg);
|
||||
{
|
||||
Console::As as(job->from);
|
||||
job->run();
|
||||
}
|
||||
job->privateKey.assign(job->privateKey.size(), '\0');
|
||||
ScpService* owner = job->owner;
|
||||
delete job;
|
||||
owner->busy_ = false;
|
||||
vTaskDelete(nullptr);
|
||||
}
|
||||
|
||||
void ScpService::answer(const std::string& password) {
|
||||
if (!asking_) return;
|
||||
password_ = password;
|
||||
asking_ = false;
|
||||
answered_ = true;
|
||||
}
|
||||
|
||||
// This device's key first, then the password, three tries: asked in the Shell that typed the command.
|
||||
bool ScpService::Job::login(ssh_session s) {
|
||||
int rc = ssh_userauth_none(s, nullptr);
|
||||
if (rc == SSH_AUTH_SUCCESS) return true;
|
||||
int methods = ssh_userauth_list(s, nullptr);
|
||||
if (!privateKey.empty() && (methods & SSH_AUTH_METHOD_PUBLICKEY)) {
|
||||
ssh_key key = nullptr;
|
||||
if (ssh_pki_import_privkey_base64(privateKey.c_str(), nullptr, nullptr, nullptr, &key) == SSH_OK) {
|
||||
rc = ssh_userauth_publickey(s, nullptr, key);
|
||||
ssh_key_free(key);
|
||||
}
|
||||
}
|
||||
privateKey.assign(privateKey.size(), '\0');
|
||||
if (rc == SSH_AUTH_SUCCESS) return true;
|
||||
if (!(methods & (SSH_AUTH_METHOD_PASSWORD | SSH_AUTH_METHOD_INTERACTIVE))) {
|
||||
console.println("scp: error the server takes neither this device's key nor a password");
|
||||
return false;
|
||||
}
|
||||
if (from != Console::Origin::Shell) {
|
||||
console.println("scp: error the server doesn't know this device's key, and a password is only asked in the Shell");
|
||||
return false;
|
||||
}
|
||||
for (int tries = 0; tries < 3 && !stopped(); tries++) {
|
||||
console.printf("scp: %s's password for %s (Back cancels):\n", args.target.user.c_str(), args.target.host.c_str());
|
||||
owner->answered_ = false;
|
||||
owner->asking_ = true;
|
||||
while (!owner->answered_ && !stopped()) vTaskDelay(pdMS_TO_TICKS(50));
|
||||
owner->asking_ = false;
|
||||
if (stopped()) break;
|
||||
std::string password;
|
||||
password.swap(owner->password_);
|
||||
if (methods & SSH_AUTH_METHOD_PASSWORD) rc = ssh_userauth_password(s, nullptr, password.c_str());
|
||||
else { // asked as questions: every one gets the password
|
||||
rc = ssh_userauth_kbdint(s, nullptr, nullptr);
|
||||
for (int round = 0; rc == SSH_AUTH_INFO && round < 4; round++) {
|
||||
int n = ssh_userauth_kbdint_getnprompts(s);
|
||||
for (int i = 0; i < n; i++) ssh_userauth_kbdint_setanswer(s, static_cast<unsigned>(i), password.c_str());
|
||||
rc = ssh_userauth_kbdint(s, nullptr, nullptr);
|
||||
}
|
||||
}
|
||||
password.assign(password.size(), '\0');
|
||||
if (rc == SSH_AUTH_SUCCESS) return true;
|
||||
if (rc == SSH_AUTH_ERROR) break;
|
||||
console.println("scp: wrong password");
|
||||
}
|
||||
console.println(stopped() ? "scp: stopped" : "scp: error not logged in");
|
||||
return false;
|
||||
}
|
||||
|
||||
void ScpService::Job::run() {
|
||||
sshutil::startLibrary();
|
||||
ssh_session s = ssh_new();
|
||||
if (!s) return (void)console.println("scp: error not enough memory");
|
||||
int verbosity = SSH_LOG_NOLOG, port = args.target.port;
|
||||
long timeout = 10;
|
||||
ssh_options_set(s, SSH_OPTIONS_HOST, args.target.host.c_str());
|
||||
ssh_options_set(s, SSH_OPTIONS_PORT, &port);
|
||||
ssh_options_set(s, SSH_OPTIONS_USER, args.target.user.c_str());
|
||||
ssh_options_set(s, SSH_OPTIONS_TIMEOUT, &timeout);
|
||||
ssh_options_set(s, SSH_OPTIONS_LOG_VERBOSITY, &verbosity);
|
||||
if (ssh_connect(s) != SSH_OK) {
|
||||
console.printf("scp: error no connection to %s: %s\n", args.target.hostPort().c_str(), ssh_get_error(s));
|
||||
} else if (known.empty()) {
|
||||
console.printf("scp: error %s is not a server this device trusts yet: connect with the SSH App once\n", args.target.hostPort().c_str());
|
||||
} else if (sshutil::fingerprintOf(s) != known) {
|
||||
console.printf("scp: error %s showed a different key than the one remembered: not connected\n", args.target.hostPort().c_str());
|
||||
} else if (login(s)) {
|
||||
if (args.upload) upload(s);
|
||||
else download(s);
|
||||
}
|
||||
ssh_disconnect(s);
|
||||
ssh_free(s);
|
||||
}
|
||||
|
||||
void ScpService::Job::upload(ssh_session s) {
|
||||
// The remote path is where the file goes; the name pushed is only used if that is a folder.
|
||||
ssh_scp scp = ssh_scp_new(s, SSH_SCP_WRITE, args.remote.c_str());
|
||||
if (!scp || ssh_scp_init(scp) != SSH_OK) {
|
||||
console.printf("scp: error %s\n", ssh_get_error(s));
|
||||
if (scp) ssh_scp_free(scp);
|
||||
return;
|
||||
}
|
||||
File f;
|
||||
size_t size = 0;
|
||||
bool opened = onCard([&]() {
|
||||
f = SD.open(args.local.c_str());
|
||||
if (f && !f.isDirectory()) size = f.size();
|
||||
else if (f) f.close();
|
||||
});
|
||||
if (!opened || !f) {
|
||||
console.printf("scp: error cannot read %s\n", args.local.c_str());
|
||||
ssh_scp_close(scp);
|
||||
ssh_scp_free(scp);
|
||||
return;
|
||||
}
|
||||
console.printf("scp: %s (%u bytes) to %s:%s\n", args.local.c_str(), (unsigned)size, args.target.text().c_str(), args.remote.c_str());
|
||||
bool ok = ssh_scp_push_file(scp, baseName(args.local).c_str(), size, 0644) == SSH_OK;
|
||||
uint8_t buf[kPiece];
|
||||
uint32_t started = millis();
|
||||
size_t sent = 0;
|
||||
while (ok && sent < size && !stopped()) {
|
||||
int n = 0;
|
||||
onCard([&]() { n = f.read(buf, std::min(sizeof buf, size - sent)); });
|
||||
if (n <= 0) {
|
||||
console.printf("scp: error the card stopped reading at %u of %u\n", (unsigned)sent, (unsigned)size);
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
if (ssh_scp_write(scp, buf, static_cast<size_t>(n)) != SSH_OK) {
|
||||
console.printf("scp: error %s\n", ssh_get_error(s));
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
sent += static_cast<size_t>(n);
|
||||
}
|
||||
onCard([&]() { f.close(); });
|
||||
if (ok && stopped()) console.printf("scp: stopped at %u of %u bytes\n", (unsigned)sent, (unsigned)size);
|
||||
else if (ok) console.printf("scp: sent %u bytes in %lu ms\n", (unsigned)sent, (unsigned long)(millis() - started));
|
||||
else if (!ok && sent == 0) console.printf("scp: error the server refused it: %s\n", ssh_get_error(s));
|
||||
ssh_scp_close(scp);
|
||||
ssh_scp_free(scp);
|
||||
}
|
||||
|
||||
void ScpService::Job::download(ssh_session s) {
|
||||
ssh_scp scp = ssh_scp_new(s, SSH_SCP_READ, args.remote.c_str());
|
||||
if (!scp || ssh_scp_init(scp) != SSH_OK) {
|
||||
console.printf("scp: error %s\n", ssh_get_error(s));
|
||||
if (scp) ssh_scp_free(scp);
|
||||
return;
|
||||
}
|
||||
// Into a part file, renamed once it is all there: a failure never leaves half a file under the real name.
|
||||
std::string part = args.local + ".part";
|
||||
File f;
|
||||
bool ok = false;
|
||||
size_t got = 0, size = 0;
|
||||
uint32_t started = millis();
|
||||
if (ssh_scp_pull_request(scp) != SSH_SCP_REQUEST_NEWFILE) {
|
||||
const char* why = ssh_get_error(s);
|
||||
console.printf("scp: error %s\n", why && *why ? why : "that isn't a plain file: scp copies one file");
|
||||
} else {
|
||||
size = static_cast<size_t>(ssh_scp_request_get_size64(scp));
|
||||
ssh_scp_accept_request(scp);
|
||||
console.printf("scp: %s:%s (%u bytes) to %s\n", args.target.text().c_str(), args.remote.c_str(), (unsigned)size, args.local.c_str());
|
||||
bool opened = false;
|
||||
onCard([&]() {
|
||||
size_t slash = args.local.rfind('/');
|
||||
if (slash > 0 && !SD.exists(args.local.substr(0, slash).c_str())) SD.mkdir(args.local.substr(0, slash).c_str());
|
||||
if (SD.exists(part.c_str())) SD.remove(part.c_str());
|
||||
f = SD.open(part.c_str(), FILE_WRITE);
|
||||
opened = static_cast<bool>(f);
|
||||
});
|
||||
if (!opened) console.printf("scp: error cannot write %s\n", args.local.c_str());
|
||||
ok = opened;
|
||||
uint8_t buf[kPiece];
|
||||
while (ok && got < size && !stopped()) {
|
||||
int n = ssh_scp_read(scp, buf, std::min(sizeof buf, size - got));
|
||||
if (n <= 0) {
|
||||
console.printf("scp: error the connection ended at %u of %u bytes\n", (unsigned)got, (unsigned)size);
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
size_t written = 0;
|
||||
onCard([&]() { written = f.write(buf, static_cast<size_t>(n)); });
|
||||
if (written != static_cast<size_t>(n)) {
|
||||
console.println("scp: error the card refused the write: is it full?");
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
got += static_cast<size_t>(n);
|
||||
}
|
||||
if (ok && got < size) ok = false, console.printf("scp: stopped at %u of %u bytes\n", (unsigned)got, (unsigned)size);
|
||||
}
|
||||
bool renamed = false;
|
||||
onCard([&]() {
|
||||
if (f) f.close();
|
||||
if (ok) {
|
||||
if (SD.exists(args.local.c_str())) SD.remove(args.local.c_str());
|
||||
renamed = SD.rename(part.c_str(), args.local.c_str());
|
||||
} else if (SD.exists(part.c_str())) {
|
||||
SD.remove(part.c_str());
|
||||
}
|
||||
});
|
||||
if (ok && !renamed) console.println("scp: error the card refused the new name");
|
||||
else if (ok) console.printf("scp: received %u bytes in %lu ms\n", (unsigned)got, (unsigned long)(millis() - started));
|
||||
ssh_scp_close(scp);
|
||||
ssh_scp_free(scp);
|
||||
}
|
||||
|
||||
} // namespace roro
|
||||
@@ -0,0 +1,50 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "platform/console.h"
|
||||
#include "scp_args.h"
|
||||
#include "services/file_ops.h"
|
||||
#include "services/storage_service.h"
|
||||
#include "settings.h"
|
||||
|
||||
namespace roro {
|
||||
|
||||
// `scp`: one file between the card and a server, over SSH (libssh's SCP), on a task of its own that
|
||||
// prints to the console that asked. It logs in with this device's key and, if the server doesn't
|
||||
// know it, asks for the password: but only when the command was typed in the Shell, which draws
|
||||
// the question masked (asking(), answer()). It goes only to servers the SSH App has already been
|
||||
// told to trust. One at a time, and not while an SSH session is open (they don't fit in memory
|
||||
// together).
|
||||
class ScpService {
|
||||
public:
|
||||
static constexpr size_t kNeedFree = 75 * 1024;
|
||||
|
||||
ScpService(Settings& settings, StorageService& storage, FileOps& files) : settings_(settings), storage_(storage), files_(files) {}
|
||||
|
||||
// "" when it started, or why not.
|
||||
std::string start(const term::ScpArgs& args, bool sshOpen);
|
||||
void cancel() {
|
||||
stop_ = true;
|
||||
asking_ = false;
|
||||
answered_ = true; // whatever it was waiting for
|
||||
}
|
||||
bool busy() const { return busy_; }
|
||||
// The password is wanted (for user@host, in `who`); the Shell calls answer() with what was typed.
|
||||
bool asking() const { return asking_; }
|
||||
void answer(const std::string& password);
|
||||
|
||||
private:
|
||||
struct Job;
|
||||
static void task(void* arg);
|
||||
|
||||
Settings& settings_;
|
||||
StorageService& storage_;
|
||||
FileOps& files_;
|
||||
std::atomic<bool> busy_{false}, stop_{false}, asking_{false}, answered_{false};
|
||||
std::string password_; // written by answer() before answered_ is set, read by the task after
|
||||
};
|
||||
|
||||
} // namespace roro
|
||||
@@ -5,6 +5,8 @@
|
||||
#include <libssh_esp32.h>
|
||||
#include <libssh/libssh.h>
|
||||
|
||||
#include "platform/libssh_util.h"
|
||||
|
||||
namespace roro {
|
||||
|
||||
namespace {
|
||||
@@ -17,27 +19,6 @@ struct Locked {
|
||||
SemaphoreHandle_t lock_;
|
||||
};
|
||||
|
||||
std::string fingerprintOf(ssh_session s) {
|
||||
ssh_key key = nullptr;
|
||||
unsigned char* hash = nullptr;
|
||||
size_t len = 0;
|
||||
std::string out;
|
||||
if (ssh_get_server_publickey(s, &key) == SSH_OK && ssh_get_publickey_hash(key, SSH_PUBLICKEY_HASH_SHA256, &hash, &len) == SSH_OK) {
|
||||
if (char* text = ssh_get_fingerprint_hash(SSH_PUBLICKEY_HASH_SHA256, hash, len)) {
|
||||
out = text; // "SHA256:..."
|
||||
ssh_string_free_char(text);
|
||||
}
|
||||
ssh_clean_pubkey_hash(&hash);
|
||||
}
|
||||
if (key) ssh_key_free(key);
|
||||
return out;
|
||||
}
|
||||
|
||||
void startLibrary() {
|
||||
static bool started = false;
|
||||
if (!started) libssh_begin();
|
||||
started = true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
struct SshService::Run {
|
||||
@@ -170,7 +151,7 @@ void SshService::task(void* arg) {
|
||||
}
|
||||
|
||||
void SshService::session(Run& run) {
|
||||
startLibrary();
|
||||
sshutil::startLibrary();
|
||||
ssh_session s = ssh_new();
|
||||
if (!s) return end("Not enough memory for the session");
|
||||
int verbosity = SSH_LOG_NOLOG;
|
||||
@@ -204,7 +185,7 @@ void SshService::session(Run& run) {
|
||||
if (stop_) return fail("Stopped");
|
||||
|
||||
// Is it the server it was last time? The first time, and when it has changed, the user decides.
|
||||
std::string seen = fingerprintOf(s);
|
||||
std::string seen = sshutil::fingerprintOf(s);
|
||||
if (seen.empty()) return fail("The server showed no key");
|
||||
if (seen != run.known) {
|
||||
{
|
||||
@@ -333,7 +314,7 @@ std::string SshService::makeKey() {
|
||||
auto work = [](void* p) {
|
||||
std::shared_ptr<Made> m = *static_cast<std::shared_ptr<Made>*>(p);
|
||||
delete static_cast<std::shared_ptr<Made>*>(p);
|
||||
startLibrary();
|
||||
sshutil::startLibrary();
|
||||
ssh_key key = nullptr, pub = nullptr;
|
||||
char *b64 = nullptr, *pub64 = nullptr;
|
||||
if (ssh_pki_generate(SSH_KEYTYPE_ED25519, 0, &key) != SSH_OK) m->why = "The key couldn't be made";
|
||||
|
||||
Reference in New Issue
Block a user