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 { match &cmd.command {
StockbitSubcommand::Login { no_browser, port } => { StockbitSubcommand::Login { no_browser, port } => {
let status = if std::io::stdin().is_terminal() { 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()?, Ok(()) => stockbit::session_status()?,
Err(err) => { Err(err) => {
runtime::warn(format!("browser login unavailable ({err}); paste instead")); 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. /// 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()); println!("Open this page to connect Stockbit: {}", page.url());
if open { match &ssh {
open_in_browser(&page.url()); Some(connection) => print_ssh_hint(connection, page.port()),
None if open => open_in_browser(&page.url()),
None => {}
} }
page.wait(LOGIN_TIMEOUT, |pasted| { page.wait(LOGIN_TIMEOUT, |pasted| {
stockbit::login(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. /// Opens `url` with the desktop's default browser, best effort.
fn open_in_browser(url: &str) { fn open_in_browser(url: &str) {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]

View file

@ -62,6 +62,11 @@ impl LoginPage {
format!("http://127.0.0.1:{}/login/{}", self.port, self.secret) 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` /// Serves until `verify` accepts a paste or `timeout` passes. `verify`
/// returns the error text to show on the page when the paste is refused. /// returns the error text to show on the page when the paste is refused.
/// ///

View file

@ -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 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 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 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 <this-host> # run where the browser is, keep it open
```
Elsewhere, pin the port yourself and forward it the same way:
```bash ```bash
idx stockbit login --no-browser --port 7878 idx stockbit login --no-browser --port 7878
# from the machine with the browser:
ssh -L 7878:127.0.0.1:7878 <this-host> # then open the printed URL
``` ```
**One refresher per session.** Stockbit replaces the refresh token every time **One refresher per session.** Stockbit replaces the refresh token every time