//! Radar from a workstation (spec ยง8.6): a browser cannot pin the //! instance's certificate, so `spectra open` serves a loopback proxy that //! forwards to the pinned server over TLS. It answers only loopback `Host` //! and `Origin`, so no other site can reach the server through it, and //! passes responses (the SSE stream included) through as they come. use crate::client::{Client, Endpoint}; use axum::body::{Body, Bytes}; use axum::extract::{Request, State}; use axum::http::{header, HeaderMap, StatusCode}; use axum::response::{IntoResponse, Response}; use http_body_util::{BodyExt, Full}; use std::sync::Arc; struct Proxy { upstream: Client, /// `127.0.0.1:PORT` and `localhost:PORT`. hosts: Vec, } /// The proxy's routes, forwarding to `upstream` as the browser at /// `127.0.0.1:port` asks. pub fn router(upstream: Endpoint, port: u16) -> axum::Router { let p = Arc::new(Proxy { upstream: Client::at(upstream, None), hosts: vec![format!("127.0.0.1:{port}"), format!("localhost:{port}")] }); axum::Router::new().fallback(forward).with_state(p) } /// The sign-in link the server made, pointed at the proxy instead. pub fn local_link(url: &str, port: u16) -> String { let rest = url.split_once("://").map(|(_, r)| r).unwrap_or(url); let path = rest.find('/').map(|i| &rest[i..]).unwrap_or("/"); format!("http://127.0.0.1:{port}{path}") } fn ours(p: &Proxy, headers: &HeaderMap) -> bool { let host = headers.get(header::HOST).and_then(|h| h.to_str().ok()).unwrap_or(""); if !p.hosts.iter().any(|h| h == host) { return false; } match headers.get(header::ORIGIN).and_then(|o| o.to_str().ok()) { None => true, Some(o) => p.hosts.iter().any(|h| o == format!("http://{h}")), } } async fn forward(State(p): State>, req: Request) -> Response { if !ours(&p, req.headers()) { return (StatusCode::FORBIDDEN, "this proxy serves its own page only").into_response(); } let (parts, body) = req.into_parts(); let body: Bytes = match body.collect().await { Ok(b) => b.to_bytes(), Err(_) => return StatusCode::BAD_REQUEST.into_response(), }; let path = parts.uri.path_and_query().map(|p| p.as_str()).unwrap_or("/"); let mut up = hyper::Request::builder().method(parts.method.clone()).uri(path); for (k, v) in parts.headers.iter() { // The proxy has checked the origin itself; hop-by-hop headers stay here. if matches!(k.as_str(), "host" | "origin" | "connection" | "keep-alive" | "transfer-encoding" | "upgrade" | "referer") { continue; } up = up.header(k, v); } let up = match up.header(header::HOST, "spectra").header(header::CONNECTION, "close").body(Full::new(body)) { Ok(r) => r, Err(_) => return StatusCode::BAD_REQUEST.into_response(), }; match p.upstream.forward(up).await { Ok(resp) => { let (parts, incoming) = resp.into_parts(); let mut out = Response::new(Body::new(incoming)); *out.status_mut() = parts.status; for (k, v) in parts.headers.iter() { if !matches!(k.as_str(), "connection" | "transfer-encoding" | "keep-alive") { out.headers_mut().append(k, v.clone()); } } out } Err(e) => (StatusCode::BAD_GATEWAY, format!("Spectra on the server did not answer: {e:#}")).into_response(), } } #[cfg(test)] mod tests { #[test] fn the_link_points_at_the_proxy() { assert_eq!(super::local_link("http://127.0.0.1:7350/signin?code=abc", 7351), "http://127.0.0.1:7351/signin?code=abc"); } }