ssh source

← ssh · API reference · version 0.1.4

3 file(s), 203 lines. This is the source the published version ships, and it is loft — the library is written in the same language you call it from.

Public items

A worked example is a call site the author chose as the thing to read first. Call sites are every line naming the item, found mechanically over the source with comments excluded — so an item with no worked example is not an item nobody uses.

ItemDefinedWorked exampleCall sites
Session struct25—68 69 74 80 86 91 … (10 total)
byte_at fn59SSH-00257 63 66
connect fn68SSH-00110 27 28
ok fn74SSH-00111 28
login fn80—13 30
open_shell fn86—31
send fn91—36
recv fn100SSH-00233 34
resize fn107—37
is_open fn112—14 32
close fn117—15 39

src/ssh.loft

// Copyright (c) 2026 Jurjen Stellingwerff
// SPDX-License-Identifier: LGPL-3.0-or-later
// --- SSH client ---
// A blocking / polling SSH client for driving an interactive remote shell —
// e.g. attaching `tmux` from a terminal app.  Password authentication only; the
// password is used for the handshake and never stored.  Native-only: SSH needs
// real TCP sockets, so this library does not ship the browser (`--html`) target.
//
// Typical use:
//   s = ssh::connect(host, 42022);
//   if s.ok() && s.login(user, password) && s.open_shell("xterm-256color", 80, 24) {
//     s.send("tmux attach\n");
//     while s.is_open() {
//       out = s.recv();                 // non-blocking; "" when idle
//       for i in 0..size(out) { handle_byte(ssh::byte_at(i, out)); }
//     }
//   }
//
// Note the `size(out)` bound: `recv` answers raw BYTES and `byte_at` indexes bytes,
// while `len` counts CHARACTERS — a `0..len(out)` loop drops the tail of every
// multi-byte sequence, silently.

// A live SSH session.  `handle >= 0` once the TCP connect + transport handshake
// succeed; `< 0` means the connect failed.  Obtain one from `connect`.
pub struct Session {
  handle: integer
}

// --- internal native surface (implemented in native/src/lib.rs) ---

fn ssh_connect(host: text, port: integer) -> integer;
#native

fn ssh_login(handle: integer, user: text, password: text) -> boolean;
#native

fn ssh_open_shell(handle: integer, term: text, cols: integer, rows: integer) -> boolean;
#native

fn ssh_send(handle: integer, data: text);
#native

fn ssh_recv(handle: integer) -> text;
#native

fn ssh_resize(handle: integer, cols: integer, rows: integer);
#native

fn ssh_is_open(handle: integer) -> boolean;
#native

fn ssh_close(handle: integer);
#native

// Read the byte at `idx` (0..size) of a `recv` result.  A terminal stream is raw
// bytes, so read it byte-by-byte rather than as UTF-8.  Out of range returns -1 —
// not 0, which the stream itself may legitimately carry.
// Example: @SSH-002 — bound the loop with `size`, and test the end with `< 0`.
pub fn byte_at(idx: integer, data: text) -> integer;
#native

// --- public API ---

// Open a TCP connection to `host:port` and run the SSH transport handshake.
// Check `ok()` before authenticating.
// Example: @SSH-001 — a failed session stays callable and silent, so `ok()` is the
// only thing that reports it.
pub fn connect(host: text, port: integer) -> Session {
  Session { handle: ssh_connect(host, port) }
}

// True when the session connected and completed the transport handshake.
// Example: @SSH-001 — skip this and the whole session ladder runs, quietly, on nothing.
pub fn ok(self: Session) -> boolean {
  self.handle >= 0
}

// Authenticate with a password.  Returns true on success.  The password is used
// only for this handshake and is never stored by the library.
pub fn login(self: Session, user: text, password: text) -> boolean {
  self.handle >= 0 && ssh_login(self.handle, user, password)
}

// Request a pseudo-terminal of `cols` x `rows` (terminal type `term`, e.g.
// "xterm-256color") and start the login shell.  Returns true on success.
pub fn open_shell(self: Session, term: text, cols: integer, rows: integer) -> boolean {
  self.handle >= 0 && ssh_open_shell(self.handle, term, cols, rows)
}

// Send bytes to the remote shell (keystrokes, escape sequences).  Binary-safe.
pub fn send(self: Session, data: text) {
  if self.handle >= 0 { ssh_send(self.handle, data); }
}

// Return the bytes the remote shell produced since the last call, or "" when
// nothing is pending (non-blocking).  Binary-safe: iterate 0..size(result) and
// read each byte with `byte_at` — `size` is the BYTE count, which is what
// `byte_at` is indexed against; `len` counts characters and would truncate.
// Example: @SSH-002 — the byte a `len`-bounded loop drops, and why -1 is not 0.
pub fn recv(self: Session) -> text {
  if self.handle < 0 { return ""; }
  ssh_recv(self.handle)
}

// Tell the remote the terminal resized to `cols` x `rows` so it re-flows.  Call
// after a zoom / rotate / window resize.
pub fn resize(self: Session, cols: integer, rows: integer) {
  if self.handle >= 0 { ssh_resize(self.handle, cols, rows); }
}

// True while the shell channel is open (the remote has not closed it).
pub fn is_open(self: Session) -> boolean {
  self.handle >= 0 && ssh_is_open(self.handle)
}

// Close the shell channel and disconnect.
pub fn close(self: Session) {
  if self.handle >= 0 { ssh_close(self.handle); }
}

tests/connect_fail.loft

// Copyright (c) 2026 Jurjen Stellingwerff
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// Gate test: connecting to a closed port must fail (not panic, not hang), and
// the library must load + call through the native FFI on both the interpreter
// and --native.  No sshd or credentials required.
use ssh::*;

fn main() {
  s = ssh::connect("127.0.0.1", 1);
  assert(!s.ok());
  // A failed session's operations must be safe no-ops.
  assert(!s.login("u", "p"));
  assert(!s.is_open());
  s.close();
  println("ssh connect_fail: PASS");
}

tests/worked-examples.loft

// Copyright (c) 2026 Jurjen Stellingwerff
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// Worked examples for the `ssh` library (@PLN141).  Each `test_*` below is a REAL,
// run-every-CI demonstration of a function whose correct use is not obvious from its
// signature alone; the function documents itself by CITING the tag with
// `// Example: @SSH-00N`, and `check_doc_drift.sh examples` fails if a citation goes
// dangling (no test carries the tag).
//
// Neither needs an sshd.  @SSH-001 is about what a FAILED session does, so a refused
// connection is the subject rather than a limitation; @SSH-002 is about how to read the
// bytes `recv` hands back, which is a property of the text, not of the connection.
use ssh::*;

// @SSH-001 — every method self-guards, so nothing tells you the session is dead unless
// you ask.  `connect` always answers a `Session`; a refused connection, a wrong host, a
// closed port all give one back with a negative handle, and from there `login` and
// `open_shell` answer false, `recv` answers "", `send` / `resize` / `close` do nothing,
// and none of it raises.  That is deliberate — a terminal app must not die because a
// host went away mid-frame — but it means a caller who skips `ok()` gets a program that
// runs its whole session ladder, sends every keystroke, reads an endless empty stream,
// and never once reports a problem.  Check `ok()` right after `connect`, before
// authenticating; after that, `is_open()` is the per-iteration question, because the
// remote can close the channel on a session that connected perfectly well.
fn test_a_failed_session_is_silent_not_fatal() {
  // Port 1 is privileged and unbound, so this is a refused connection.
  s = ssh::connect("127.0.0.1", 1);
  assert(!s.ok(), "ok() is the one call that reports the connect failed");
  // Everything downstream stays callable and answers its own falsey value.
  assert(!s.login("user", "secret"), "login on a dead session answers false");
  assert(!s.open_shell("xterm-256color", 80, 24), "open_shell answers false");
  assert(!s.is_open(), "is_open answers false");
  idle = s.recv();
  assert(idle.size() == 0, "recv answers an empty stream, never an error");
  // The write side is a silent no-op rather than a failure a caller could notice.
  s.send("tmux attach\n");
  s.resize(100, 40);
  // Closing a session that never opened is safe too.
  s.close();
}

// @SSH-002 — `recv` hands back a raw BYTE stream, so bound the loop with `size`, never
// `len`.  A terminal emits UTF-8, escape sequences and raw control bytes together, and
// loft `text` measures `len` in CHARACTERS while `byte_at` indexes BYTES.  Bound a
// byte-indexed loop by the character count and every multi-byte sequence loses its tail
// — silently, since both are integers and the loop simply ends early.  `size` is the
// byte count and is what `byte_at` is indexed against; out of range answers -1, which is
// how a decoder tells the end of the stream from a legitimate 0 byte in it.
fn test_recv_is_bytes_so_bound_the_loop_with_size() {
  // One ASCII byte then a two-byte UTF-8 character: three bytes, two characters.
  stream = "aé";
  assert(stream.size() == 3, "the byte count is 3");
  assert(stream.len() == 2, "the character count is 2 — the two disagree");
  // The correct loop reaches every byte.
  seen = 0;
  for i in 0..stream.size() {
    b = ssh::byte_at(i, stream);
    assert(b >= 0, "every index below size() is a real byte");
    seen += 1;
  }
  assert(seen == 3, "size() visits all three bytes");
  // Bounding by len() stops one byte short, and nothing says so.
  assert(ssh::byte_at(stream.len(), stream) == 0xA9,
         "the byte a len()-bounded loop drops is a real one, not padding");
  // Past the end is -1, distinct from a 0 byte the stream may genuinely carry.
  assert(ssh::byte_at(stream.size(), stream) == -1, "one past the end answers -1");
}