Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 30 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,14 +208,22 @@ Enabled hosts are connected at startup, all at once, and one that does not answe
reported rather than fatal: a laptop on the wrong network has half of them unreachable,
and refusing to start then would be refusing exactly when it is wanted.

**Enabled means open, not "opens on demand".** Connecting when a request for an unopened
host arrives would be nicer to describe and much worse to have: every request to an alias
origin arrives through the proxy, so any web page could start ssh sessions by naming a host
in an `<img src>`, and time the answer to learn which hosts you have. The header that would
separate a navigation from a subresource is not available — **Chromium sends no
`Sec-Fetch-*` at all on a proxied request**, measured against a real browser. So a session
is opened by the daemon at startup or by a control call carrying the token, and by nothing
else.
**A host in `ssh_config` is never opened by a request.** Every request to an alias origin
arrives through the proxy, so if any name could be dialled by asking for it, a web page
could start ssh sessions by naming hosts in an `<img src>` and time the answers to learn
which ones you have. The header that would separate a navigation from a subresource is not
available — **Chromium sends no `Sec-Fetch-*` at all on a proxied request**, measured
against a real browser. So an `ssh_config` host is opened at startup if it is enabled, or by
a control call carrying the token, and by nothing else.

**A declared alias is different, and a request does open it.** The set of them is fixed when
the daemon starts, from the config file and the command line — it is not `ssh_config` — so
the furthest a page can reach is a host you already asked to have served, and one whose
up-or-down it can already read off the status code. What it must not get is the *rate*, so a
failed dial is remembered for three seconds and requests inside that window are answered
from the memory. Long enough that a loop cannot choose how often this machine opens an ssh;
short enough that reloading is still a retry, which is the point of the retry being a
reload.

`--port` and `--suffix` change the listener and the hostname suffix. `ssh-browser
pac` prints the script without starting a server.
Expand Down Expand Up @@ -244,8 +252,20 @@ host = "login-node"

`base` may be an absolute path, or `~` and a path under the remote's home directory, or
omitted for the home directory itself. The tilde is resolved by asking the remote, once,
at startup: it is shell syntax and this transport never runs a shell, so expanding it
locally would produce *your* home directory rather than the account's.
when the alias connects: it is shell syntax and this transport never runs a shell, so
expanding it locally would produce *your* home directory rather than the account's.

**An alias that will not connect does not stop the daemon.** All of them are dialled at
startup, at once; the ones that come up are served, and the ones that do not are named with
what ssh said, listed as not connected, and opened by the next request that asks for them.
A cluster in maintenance used to take the rest of your sites down with it. The exception is
an alias typed on the command line — `ssh-browser serve docs=myhost` — which is a thing you
are standing there waiting on, so failing to open it is still an error.

An alias that is down answers `502` and says what ssh said; one stopped from the dashboard
answers `503` and says a restart brings it back; a name that was never declared is still a
`404`. Three situations, three answers — a daemon that called all of them "not found" sent
you looking in the config file for a name that was sitting in it.

Pointing an alias at a home directory is reasonable because **no name beginning with a dot
is ever served**, at any depth, and they are left out of listings. An alias base is one
Expand Down
14 changes: 14 additions & 0 deletions crates/ssh-browser/src/fs/sftp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,20 @@ impl SftpFs {
Ok(Self::drive(sftp, Some(child)))
}

/// Whether this connection is still worth sending a request down.
///
/// The driver task owns the ssh child's pipes and returns when they close, which drops the
/// receiving end of this channel. So a shut channel is not a proxy for "the ssh died" --
/// it is the same event, observed from the only side that can see it without a syscall.
///
/// Racy by nature: a connection alive when this is asked can be gone by the time the
/// request lands. That is fine, because the caller retries either way. What this prevents
/// is the other thing -- holding a corpse forever and answering every request with
/// `sftp session is gone` until somebody restarts the daemon.
pub fn is_alive(&self) -> bool {
!self.jobs.is_closed()
}

/// Drive a session over arbitrary streams. Exists so the round-trip invariant
/// can be asserted against an in-memory server, with no ssh anywhere.
pub async fn over<W, R>(w: W, r: R) -> Result<Self>
Expand Down
4 changes: 3 additions & 1 deletion crates/ssh-browser/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -289,5 +289,7 @@ fn parse_alias(spec: &str) -> Result<Alias> {
Some((host, base)) => (host, Some(base)),
None => (rest, None),
};
Alias::new(name, host, base).with_context(|| format!("in {spec:?}"))
Alias::new(name, host, base)
.map(Alias::for_this_run)
.with_context(|| format!("in {spec:?}"))
}
Loading