Files
roro9stack/src/services/ssh_service.h
T
twislaandClaude Opus 5.5 6b71aace4f
CI / build (pull_request) Successful in 2m22s
Site / build (pull_request) Successful in 10s
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
2026-10-08 04:38:36 +02:00

80 lines
3.2 KiB
C++

#pragma once
#include <atomic>
#include <functional>
#include <memory>
#include <string>
#include "event_bus.h"
#include "settings.h"
#include "ssh_hosts.h"
#include "terminal.h"
namespace roro {
// One SSH session to a shell (issue #2, docs/milestones/N1.md): the protocol is libssh's, on a
// task of its own; what it prints goes into a term::Terminal, which the SSH App draws. The
// session lasts until the far end closes it or disconnect() is called, whether the App is in
// front or not.
//
// The task and the main loop share the terminal, the bytes waiting to be sent and the state,
// under one lock. Questions for the user (is this the right server? what is the password?) are
// states the task waits in until the App answers.
class SshService {
public:
enum class State { Idle, Connecting, AskTrust, AskPassword, Open, Ended };
static constexpr size_t kNeedFree = 75 * 1024; // a session peaks at about 63 KB (measured)
static constexpr int kHistory = 100;
explicit SshService(Settings& settings);
// "" when the connection is on its way, or why not.
std::string connect(const term::SshTarget& target, int cols, int rows);
void disconnect(); // from any state; the terminal stays to be read until the next connect
void clear(); // Ended, and read: the terminal and its history are given back
State state() const { return state_; }
bool active() const { return state_ == State::Open; } // for the Status Bar
bool opened() const { return opened_; } // this session got as far as a shell
std::string status() const; // what it is doing, or why it ended
const term::SshTarget& target() const { return target_; }
// AskTrust: the server's fingerprint, and the one remembered if this is a different one.
std::string fingerprint() const;
std::string remembered() const;
void answerTrust(bool yes);
// AskPassword.
void answerPassword(const std::string& password);
// Open (and Ended, to read what is left).
bool hasTerminal() const { return static_cast<bool>(term_); }
void withTerminal(const std::function<void(term::Terminal&)>& use);
uint32_t revision() const { return revision_; }
void send(const std::string& bytes);
void resize(int cols, int rows);
// This device's own key: made once, its public half is what goes on servers.
bool hasKey() const { return !settings_.getString(Setting::SshKey).empty(); }
std::string publicKey() const { return settings_.getString(Setting::SshPublic); }
std::string makeKey(); // "" or why not
private:
struct Run; // what one connection's task works with
static void task(void* arg);
void session(Run& run);
void end(const std::string& why);
void say(const std::string& what);
Settings& settings_;
void* lock_; // SemaphoreHandle_t
std::atomic<State> state_{State::Idle};
std::atomic<bool> stop_{false}, answered_{false}, trusted_{false}, running_{false}, opened_{false};
std::atomic<uint32_t> revision_{0};
term::SshTarget target_;
std::unique_ptr<term::Terminal> term_;
std::string status_, fingerprint_, remembered_, password_, outgoing_;
int wantCols_ = 0, wantRows_ = 0;
bool resized_ = false;
};
} // namespace roro