From cb532b670b6eb74b48f8eef1de836cc01bbf326a Mon Sep 17 00:00:00 2001 From: hermes Date: Fri, 23 Oct 2026 13:52:15 +0000 Subject: [PATCH] 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. --- crates/idx-cli/src/cli/stockbit.rs | 46 +++++++++++++++++--- crates/idx-core/src/api/stockbit/loopback.rs | 5 +++ web/src/content/docs/cli/stockbit.md | 11 +++-- 3 files changed, 54 insertions(+), 8 deletions(-) diff --git a/crates/idx-cli/src/cli/stockbit.rs b/crates/idx-cli/src/cli/stockbit.rs index 3772afb..0fa2cde 100644 --- a/crates/idx-cli/src/cli/stockbit.rs +++ b/crates/idx-cli/src/cli/stockbit.rs @@ -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, 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(), + _ => "".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")] diff --git a/crates/idx-core/src/api/stockbit/loopback.rs b/crates/idx-core/src/api/stockbit/loopback.rs index e6181be..bb7bbac 100644 --- a/crates/idx-core/src/api/stockbit/loopback.rs +++ b/crates/idx-core/src/api/stockbit/loopback.rs @@ -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. /// diff --git a/web/src/content/docs/cli/stockbit.md b/web/src/content/docs/cli/stockbit.md index e8bc6c6..1c59f3f 100644 --- a/web/src/content/docs/cli/stockbit.md +++ b/web/src/content/docs/cli/stockbit.md @@ -99,12 +99,17 @@ minutes). A session Stockbit refuses leaves a previously stored one in place. The page listens on this machine's loopback interface only, so the browser must run on the same machine: from another host over SSH, or from a browser outside a container or WSL distro, `127.0.0.1` is a different machine and the -page will not load. Forward it instead, with a pinned port: +page will not load. idx notices an SSH session (`SSH_CONNECTION`) and prints +the exact command to run on the machine with the browser, on port 7878: + +```bash +ssh -L 7878:127.0.0.1:7878 # run where the browser is, keep it open +``` + +Elsewhere, pin the port yourself and forward it the same way: ```bash idx stockbit login --no-browser --port 7878 -# from the machine with the browser: -ssh -L 7878:127.0.0.1:7878 # then open the printed URL ``` **One refresher per session.** Stockbit replaces the refresh token every time