Public Access
SSH client: a terminal on another machine (#2)
The SSH App opens one session to a shell, over libssh (LibSSH-ESP32 5.10.0) on mbedTLS. A server is trusted the first time on its fingerprint, and a changed key is a warning with Cancel selected. The password is typed each time and kept nowhere; or the device makes itself an Ed25519 key, whose public half is shown, written to /ssh/id_ed25519.pub and printed by `ssh status`. lib/term is the terminal: what a shell, less, top, nano and vim send, with sixteen colours, scroll regions, the alternate screen and 100 lines of scrollback. Five text sizes with Ctrl and + or -, from 60x20 to 26x8, told to the far end. The session goes on when the App is left; SSH shows in the Status Bar. `ssh user@host` in the Shell opens the App. Also: - Keys that aren't characters carry Shift, Ctrl and Alt. The terminal needs it, and it makes Ctrl+Fn+up/down in a note and Shift+Tab in Gemini work from the real keyboard. - IRC doesn't try to connect under 60 KB free: started with a session open, its TLS handshake took the heap down to 236 bytes. - libssh's own curve25519 is left out of the build (scripts/libssh_filter.py): libsodium's has the same names. Costs 292 KB of flash and about 50 KB of heap while a session is open; not started under 75 KB free. Docs: guide page, how-to, FAQ, home page, Status Bar, SD card folders, the memory how-to, README, glossary, N1 notes with Q254 to Q266 and the checks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
@@ -12,7 +12,9 @@
|
||||
#include "apps/demo_app.h"
|
||||
#include "apps/note_editor.h"
|
||||
#include "apps/shell_app.h"
|
||||
#include "apps/ssh_app.h"
|
||||
#include "services/net_tools.h"
|
||||
#include "services/ssh_service.h"
|
||||
#include "services/vpn_service.h"
|
||||
#include "services/web_share.h"
|
||||
#include "apps/gemini_app.h"
|
||||
@@ -91,6 +93,8 @@ static UpdateService* update;
|
||||
static DebugConsole* debugConsole;
|
||||
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 Notifier* notifier;
|
||||
static LauncherApp launcher;
|
||||
static AppManager* apps;
|
||||
@@ -139,6 +143,7 @@ static StatusInfo currentStatus() {
|
||||
s.capturing = loraCapture && loraCapture->capturing();
|
||||
if (vpnService && vpnService->wanted())
|
||||
s.vpn = vpnService->state() == VpnService::State::Up ? StatusInfo::Vpn::Up : StatusInfo::Vpn::Trying;
|
||||
s.ssh = sshService && sshService->active();
|
||||
s.debug = !debugConsole->on() ? StatusInfo::Debug::Off : debugConsole->clientConnected() ? StatusInfo::Debug::Client : StatusInfo::Debug::On;
|
||||
using WifiState = WifiController::State;
|
||||
switch (wifi->state()) {
|
||||
@@ -228,6 +233,9 @@ void setup() {
|
||||
apps->registerApp({"lora", "LoRa Scanner", false, new LoraScannerApp(*radioService, *loraCapture, settings, *clockService)});
|
||||
apps->registerApp({"storage", "Storage", false, new StorageApp(*fileOps, *storageService, *clockService, *update, *power, bus, *new WebShare(*storageService, *fileOps, *wifi))});
|
||||
apps->registerApp({"notes", "Notes", false, new NotesApp(*fileOps, *storageService, *clockService, *power)});
|
||||
sshService = new SshService(settings);
|
||||
sshApp = new SshApp(*sshService, settings, *storageService);
|
||||
apps->registerApp({"ssh", "SSH", false, sshApp});
|
||||
apps->registerApp({"shell", "Shell", false, new ShellApp(shellRun, shellProbe, shellList, shellCount, helpText(), *apps)});
|
||||
// Leaving the foreground App makes it save: a note being typed, when the device is powered off.
|
||||
power->beforePowerOff = []() { apps->home(); };
|
||||
@@ -680,6 +688,7 @@ 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"
|
||||
"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"
|
||||
"debug on | debug token <16 to 64 characters> | debug token new (USB serial only) switch it on, set its token\n"
|
||||
@@ -740,6 +749,28 @@ static void saveScreenshot() {
|
||||
});
|
||||
}
|
||||
|
||||
// `ssh ...` (issue #2, Q265): the session is the SSH App's; this opens it there.
|
||||
static void sshCommand(const String& arg) {
|
||||
if (!sshService) return (void)console.println("ssh: not available in Safe Mode");
|
||||
SshService& s = *sshService;
|
||||
if (arg == "status") {
|
||||
bool live = s.state() != SshService::State::Idle && s.state() != SshService::State::Ended;
|
||||
if (s.state() == SshService::State::Idle) console.println("ssh: no session");
|
||||
else console.printf("ssh: %s%s%s\n", s.target().text().c_str(), live ? ", " : ": ", s.state() == SshService::State::Open ? "open" : s.status().c_str());
|
||||
console.printf("ssh: this device's key: %s\n", s.hasKey() ? s.publicKey().c_str() : "none (the SSH App makes one)");
|
||||
} else if (arg == "stop") {
|
||||
s.disconnect();
|
||||
console.println("ssh: stopped");
|
||||
} else if (arg.indexOf('@') > 0) {
|
||||
std::string why = sshApp->connectTo(arg.c_str());
|
||||
if (!why.empty()) return (void)console.printf("ssh: error %s\n", why.c_str());
|
||||
if (!apps->open("ssh")) return (void)console.println("ssh: connecting, but Setup is running");
|
||||
console.printf("ssh: connecting to %s, in the SSH App\n", s.target().text().c_str());
|
||||
} else {
|
||||
console.println("ssh: user@host[:port] | status | stop");
|
||||
}
|
||||
}
|
||||
|
||||
// `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");
|
||||
@@ -874,6 +905,7 @@ 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 == "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);
|
||||
if (line == "screenshot" || line.startsWith("screenshot ")) {
|
||||
|
||||
Reference in New Issue
Block a user