feat(stockbit): print the ssh forward when the shell is remote

The login page listens on 127.0.0.1, so a browser on the Mac driving a VM
could never reach it. Over SSH, idx now picks port 7878, skips opening a
browser on the VM, and prints the exact ssh -L to run where the browser
is.
This commit is contained in:
hermes 2026-10-23 13:52:15 +00:00
commit cb532b670b
3 changed files with 54 additions and 8 deletions

View file

@ -57,7 +57,7 @@ pub fn handle(cmd: &StockbitCmd, format: &OutputFormat) -> Result<(), IdxError>
match &cmd.command {
StockbitSubcommand::Login { no_browser, port } => {
let status = if std::io::stdin().is_terminal() {
match login_in_browser(port.unwrap_or(0), !no_browser) {
match login_in_browser(*port, !no_browser) {
Ok(()) => stockbit::session_status()?,
Err(err) => {
runtime::warn(format!("browser login unavailable ({err}); paste instead"));
@ -105,11 +105,24 @@ pub fn handle(cmd: &StockbitCmd, format: &OutputFormat) -> Result<(), IdxError>
}
/// Serves the local login page and waits for a session it can verify.
fn login_in_browser(port: u16, open: bool) -> Result<(), IdxError> {
let page = stockbit::loopback::LoginPage::bind(port)?;
///
/// The page is only reachable from this machine. Over SSH the browser is
/// usually somewhere else, so idx picks a stable port, skips opening a
/// browser here, and prints the forward to run on the machine with the
/// browser.
fn login_in_browser(port: Option<u16>, open: bool) -> Result<(), IdxError> {
let ssh = std::env::var("SSH_CONNECTION").ok();
// A stable port makes the printed `ssh -L` match what the page serves.
let page = stockbit::loopback::LoginPage::bind(port.unwrap_or(if ssh.is_some() {
DEFAULT_SSH_PORT
} else {
0
}))?;
println!("Open this page to connect Stockbit: {}", page.url());
if open {
open_in_browser(&page.url());
match &ssh {
Some(connection) => print_ssh_hint(connection, page.port()),
None if open => open_in_browser(&page.url()),
None => {}
}
page.wait(LOGIN_TIMEOUT, |pasted| {
stockbit::login(pasted)
@ -118,6 +131,29 @@ fn login_in_browser(port: u16, open: bool) -> Result<(), IdxError> {
})
}
/// Port the page uses over SSH, so `ssh -L 7878:…:7878` matches it.
const DEFAULT_SSH_PORT: u16 = 7878;
/// The forward to run where the browser is, from `SSH_CONNECTION`
/// (`client_ip client_port server_ip server_port`).
fn print_ssh_hint(connection: &str, port: u16) {
let fields: Vec<&str> = connection.split_whitespace().collect();
let target = match (std::env::var("USER").ok(), fields.get(2)) {
(Some(user), Some(server)) => format!("{user}@{server}"),
(None, Some(server)) => (*server).to_string(),
_ => "<this host>".to_string(),
};
runtime::warn(
"this shell is over SSH: the page listens on the remote machine, so open it from the \
machine with your browser",
);
println!(
"\nFrom the machine you are sitting at, run (and keep it open):\n \
ssh -L {port}:127.0.0.1:{port} {target}\n\
then open the URL above there.\n"
);
}
/// Opens `url` with the desktop's default browser, best effort.
fn open_in_browser(url: &str) {
#[cfg(target_os = "macos")]

View file

@ -62,6 +62,11 @@ impl LoginPage {
format!("http://127.0.0.1:{}/login/{}", self.port, self.secret)
}
/// The port the page listens on.
pub fn port(&self) -> u16 {
self.port
}
/// Serves until `verify` accepts a paste or `timeout` passes. `verify`
/// returns the error text to show on the page when the paste is refused.
///