OXYGENHUB

Coverage / crates/spectra-mcp/src/proxy.rs 15.5%

9 of 58 lines run, 3 of 23 functions · run #28 on main · run missed

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.
230×
pub fn router(upstream: Endpoint, port: u16) -> axum::Router {
240×
    let p = Arc::new(Proxy { upstream: Client::at(upstream, None), hosts: vec![format!("127.0.0.1:{port}"), format!("localhost:{port}")] });
250×
    axum::Router::new().fallback(forward).with_state(p)
260×
}
27
28
/// The sign-in link the server made, pointed at the proxy instead.
291×
pub fn local_link(url: &str, port: u16) -> String {
301×
    let rest = url.split_once("://").map(|(_, r)| r).unwrap_or(url);
311×
    let path = rest.find('/').map(|i| &rest[i..]).unwrap_or("/");
321×
    format!("http://127.0.0.1:{port}{path}")
331×
}
34
350×
fn ours(p: &Proxy, headers: &HeaderMap) -> bool {
360×
    let host = headers.get(header::HOST).and_then(|h| h.to_str().ok()).unwrap_or("");
370×
    if !p.hosts.iter().any(|h| h == host) {
380×
        return false;
390×
    }
400×
    match headers.get(header::ORIGIN).and_then(|o| o.to_str().ok()) {
410×
        None => true,
420×
        Some(o) => p.hosts.iter().any(|h| o == format!("http://{h}")),
43
    }
440×
}
45
460×
async fn forward(State(p): State<Arc<Proxy>>, req: Request) -> Response {
470×
    if !ours(&p, req.headers()) {
480×
        return (StatusCode::FORBIDDEN, "this proxy serves its own page only").into_response();
490×
    }
500×
    let (parts, body) = req.into_parts();
510×
    let body: Bytes = match body.collect().await {
520×
        Ok(b) => b.to_bytes(),
530×
        Err(_) => return StatusCode::BAD_REQUEST.into_response(),
54
    };
550×
    let path = parts.uri.path_and_query().map(|p| p.as_str()).unwrap_or("/");
560×
    let mut up = hyper::Request::builder().method(parts.method.clone()).uri(path);
570×
    for (k, v) in parts.headers.iter() {
58
        // The proxy has checked the origin itself; hop-by-hop headers stay here.
590×
        if matches!(k.as_str(), "host" | "origin" | "connection" | "keep-alive" | "transfer-encoding" | "upgrade" | "referer") {
600×
            continue;
610×
        }
620×
        up = up.header(k, v);
63
    }
640×
    let up = match up.header(header::HOST, "spectra").header(header::CONNECTION, "close").body(Full::new(body)) {
650×
        Ok(r) => r,
660×
        Err(_) => return StatusCode::BAD_REQUEST.into_response(),
67
    };
680×
    match p.upstream.forward(up).await {
690×
        Ok(resp) => {
700×
            let (parts, incoming) = resp.into_parts();
710×
            let mut out = Response::new(Body::new(incoming));
720×
            *out.status_mut() = parts.status;
730×
            for (k, v) in parts.headers.iter() {
740×
                if !matches!(k.as_str(), "connection" | "transfer-encoding" | "keep-alive") {
750×
                    out.headers_mut().append(k, v.clone());
760×
                }
77
            }
780×
            out
79
        }
800×
        Err(e) => (StatusCode::BAD_GATEWAY, format!("Spectra on the server did not answer: {e:#}")).into_response(),
81
    }
820×
}
83
84
#[cfg(test)]
85
mod tests {
86
    #[test]
871×
    fn the_link_points_at_the_proxy() {
881×
        assert_eq!(super::local_link("http://127.0.0.1:7350/signin?code=abc", 7351), "http://127.0.0.1:7351/signin?code=abc");
891×
    }
90
}