OXYGENHUB
# 01320eefd505 spectra / crates / spectra-mcp / src / proxy.rs
3.6 KiB · Rust · last changed Radar from another machine: spectra open's loopback proxy to the pinned server (#24) 1 day ago source blame history raw
1
//! Radar from a workstation (spec §8.6): a browser cannot pin the
2
//! instance's certificate, so `spectra open` serves a loopback proxy that
3
//! forwards to the pinned server over TLS. It answers only loopback `Host`
4
//! and `Origin`, so no other site can reach the server through it, and
5
//! passes responses (the SSE stream included) through as they come.
6
7
use crate::client::{Client, Endpoint};
8
use axum::body::{Body, Bytes};
9
use axum::extract::{Request, State};
10
use axum::http::{header, HeaderMap, StatusCode};
11
use axum::response::{IntoResponse, Response};
12
use http_body_util::{BodyExt, Full};
13
use std::sync::Arc;
14
15
struct Proxy {
16
    upstream: Client,
17
    /// `127.0.0.1:PORT` and `localhost:PORT`.
18
    hosts: Vec<String>,
19
}
20
21
/// The proxy's routes, forwarding to `upstream` as the browser at
22
/// `127.0.0.1:port` asks.
23
pub fn router(upstream: Endpoint, port: u16) -> axum::Router {
24
    let p = Arc::new(Proxy { upstream: Client::at(upstream, None), hosts: vec![format!("127.0.0.1:{port}"), format!("localhost:{port}")] });
25
    axum::Router::new().fallback(forward).with_state(p)
26
}
27
28
/// The sign-in link the server made, pointed at the proxy instead.
29
pub fn local_link(url: &str, port: u16) -> String {
30
    let rest = url.split_once("://").map(|(_, r)| r).unwrap_or(url);
31
    let path = rest.find('/').map(|i| &rest[i..]).unwrap_or("/");
32
    format!("http://127.0.0.1:{port}{path}")
33
}
34
35
fn ours(p: &Proxy, headers: &HeaderMap) -> bool {
36
    let host = headers.get(header::HOST).and_then(|h| h.to_str().ok()).unwrap_or("");
37
    if !p.hosts.iter().any(|h| h == host) {
38
        return false;
39
    }
40
    match headers.get(header::ORIGIN).and_then(|o| o.to_str().ok()) {
41
        None => true,
42
        Some(o) => p.hosts.iter().any(|h| o == format!("http://{h}")),
43
    }
44
}
45
46
async fn forward(State(p): State<Arc<Proxy>>, req: Request) -> Response {
47
    if !ours(&p, req.headers()) {
48
        return (StatusCode::FORBIDDEN, "this proxy serves its own page only").into_response();
49
    }
50
    let (parts, body) = req.into_parts();
51
    let body: Bytes = match body.collect().await {
52
        Ok(b) => b.to_bytes(),
53
        Err(_) => return StatusCode::BAD_REQUEST.into_response(),
54
    };
55
    let path = parts.uri.path_and_query().map(|p| p.as_str()).unwrap_or("/");
56
    let mut up = hyper::Request::builder().method(parts.method.clone()).uri(path);
57
    for (k, v) in parts.headers.iter() {
58
        // The proxy has checked the origin itself; hop-by-hop headers stay here.
59
        if matches!(k.as_str(), "host" | "origin" | "connection" | "keep-alive" | "transfer-encoding" | "upgrade" | "referer") {
60
            continue;
61
        }
62
        up = up.header(k, v);
63
    }
64
    let up = match up.header(header::HOST, "spectra").header(header::CONNECTION, "close").body(Full::new(body)) {
65
        Ok(r) => r,
66
        Err(_) => return StatusCode::BAD_REQUEST.into_response(),
67
    };
68
    match p.upstream.forward(up).await {
69
        Ok(resp) => {
70
            let (parts, incoming) = resp.into_parts();
71
            let mut out = Response::new(Body::new(incoming));
72
            *out.status_mut() = parts.status;
73
            for (k, v) in parts.headers.iter() {
74
                if !matches!(k.as_str(), "connection" | "transfer-encoding" | "keep-alive") {
75
                    out.headers_mut().append(k, v.clone());
76
                }
77
            }
78
            out
79
        }
80
        Err(e) => (StatusCode::BAD_GATEWAY, format!("Spectra on the server did not answer: {e:#}")).into_response(),
81
    }
82
}
83
84
#[cfg(test)]
85
mod tests {
86
    #[test]
87
    fn the_link_points_at_the_proxy() {
88
        assert_eq!(super::local_link("http://127.0.0.1:7350/signin?code=abc", 7351), "http://127.0.0.1:7351/signin?code=abc");
89
    }
90
}