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:
@@ -0,0 +1,108 @@
|
||||
#include "ssh_hosts.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace roro::term {
|
||||
|
||||
namespace {
|
||||
std::vector<std::string> linesOf(const std::string& text) {
|
||||
std::vector<std::string> out;
|
||||
for (size_t at = 0; at < text.size();) {
|
||||
size_t end = text.find('\n', at);
|
||||
if (end == std::string::npos) end = text.size();
|
||||
if (end > at) out.push_back(text.substr(at, end - at));
|
||||
at = end + 1;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
bool hostChars(const std::string& s) {
|
||||
if (s.empty() || s.size() > 253) return false;
|
||||
for (char c : s)
|
||||
if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '.' || c == '-')) return false;
|
||||
return s.front() != '.' && s.front() != '-';
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::string SshTarget::text() const { return user + "@" + host + (port == 22 ? "" : ":" + std::to_string(port)); }
|
||||
std::string SshTarget::hostPort() const { return host + ":" + std::to_string(port); }
|
||||
|
||||
std::string parseSshTarget(const std::string& text, SshTarget& out) {
|
||||
size_t at = text.find('@');
|
||||
if (at == std::string::npos || at == 0) return "user@host, please";
|
||||
SshTarget t;
|
||||
t.user = text.substr(0, at);
|
||||
std::string rest = text.substr(at + 1);
|
||||
if (t.user.size() > 32 || t.user.find_first_of(" @:/") != std::string::npos) return "that isn't a user name";
|
||||
size_t colon = rest.rfind(':');
|
||||
if (colon != std::string::npos) {
|
||||
std::string port = rest.substr(colon + 1);
|
||||
long n = 0;
|
||||
if (port.empty() || port.size() > 5) return "a port from 1 to 65535";
|
||||
for (char c : port) {
|
||||
if (c < '0' || c > '9') return "a port from 1 to 65535";
|
||||
n = n * 10 + (c - '0');
|
||||
}
|
||||
if (n < 1 || n > 65535) return "a port from 1 to 65535";
|
||||
t.port = static_cast<uint16_t>(n);
|
||||
rest.resize(colon);
|
||||
}
|
||||
if (!hostChars(rest)) return "that isn't a host";
|
||||
t.host = rest;
|
||||
out = t;
|
||||
return "";
|
||||
}
|
||||
|
||||
SshHosts::SshHosts(const std::string& stored) {
|
||||
for (auto& line : linesOf(stored)) {
|
||||
SshTarget t;
|
||||
if (hosts_.size() < kMax && parseSshTarget(line, t).empty()) hosts_.push_back(t.text());
|
||||
}
|
||||
}
|
||||
|
||||
void SshHosts::used(const SshTarget& target) {
|
||||
std::string text = target.text();
|
||||
hosts_.erase(std::remove(hosts_.begin(), hosts_.end(), text), hosts_.end());
|
||||
hosts_.insert(hosts_.begin(), text);
|
||||
if (hosts_.size() > kMax) hosts_.resize(kMax);
|
||||
}
|
||||
|
||||
void SshHosts::remove(size_t index) {
|
||||
if (index < hosts_.size()) hosts_.erase(hosts_.begin() + static_cast<long>(index));
|
||||
}
|
||||
|
||||
std::string SshHosts::stored() const {
|
||||
std::string s;
|
||||
for (auto& h : hosts_) s += h + "\n";
|
||||
return s;
|
||||
}
|
||||
|
||||
SshKnownHosts::SshKnownHosts(const std::string& stored) {
|
||||
for (auto& line : linesOf(stored)) {
|
||||
size_t space = line.find(' ');
|
||||
if (space != std::string::npos && space > 0 && space + 1 < line.size() && known_.size() < kMax) known_.emplace_back(line.substr(0, space), line.substr(space + 1));
|
||||
}
|
||||
}
|
||||
|
||||
std::string SshKnownHosts::fingerprintOf(const std::string& hostPort) const {
|
||||
for (auto& k : known_)
|
||||
if (k.first == hostPort) return k.second;
|
||||
return "";
|
||||
}
|
||||
|
||||
void SshKnownHosts::remember(const std::string& hostPort, const std::string& fingerprint) {
|
||||
known_.erase(std::remove_if(known_.begin(), known_.end(), [&](const std::pair<std::string, std::string>& k) { return k.first == hostPort; }), known_.end());
|
||||
known_.emplace_back(hostPort, fingerprint);
|
||||
if (known_.size() > kMax) known_.erase(known_.begin());
|
||||
}
|
||||
|
||||
void SshKnownHosts::forget(const std::string& hostPort) {
|
||||
known_.erase(std::remove_if(known_.begin(), known_.end(), [&](const std::pair<std::string, std::string>& k) { return k.first == hostPort; }), known_.end());
|
||||
}
|
||||
|
||||
std::string SshKnownHosts::stored() const {
|
||||
std::string s;
|
||||
for (auto& k : known_) s += k.first + " " + k.second + "\n";
|
||||
return s;
|
||||
}
|
||||
|
||||
} // namespace roro::term
|
||||
Reference in New Issue
Block a user