| 1 | |
|
| 2 | |
|
| 3 | |
|
| 4 | |
|
| 5 | |
|
| 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 | | |
| 18 | | hosts: Vec<String>, |
| 19 | | } |
| 20 | |
|
| 21 | |
|
| 22 | |
|
| 23 | 0× | pub fn router(upstream: Endpoint, port: u16) -> axum::Router { |
| 24 | 0× | let p = Arc::new(Proxy { upstream: Client::at(upstream, None), hosts: vec![format!("127.0.0.1:{port}"), format!("localhost:{port}")] }); |
| 25 | 0× | axum::Router::new().fallback(forward).with_state(p) |
| 26 | 0× | } |
| 27 | |
|
| 28 | |
|
| 29 | 1× | pub fn local_link(url: &str, port: u16) -> String { |
| 30 | 1× | let rest = url.split_once("://").map(|(_, r)| r).unwrap_or(url); |
| 31 | 1× | let path = rest.find('/').map(|i| &rest[i..]).unwrap_or("/"); |
| 32 | 1× | format!("http://127.0.0.1:{port}{path}") |
| 33 | 1× | } |
| 34 | |
|
| 35 | 0× | fn ours(p: &Proxy, headers: &HeaderMap) -> bool { |
| 36 | 0× | let host = headers.get(header::HOST).and_then(|h| h.to_str().ok()).unwrap_or(""); |
| 37 | 0× | if !p.hosts.iter().any(|h| h == host) { |
| 38 | 0× | return false; |
| 39 | 0× | } |
| 40 | 0× | match headers.get(header::ORIGIN).and_then(|o| o.to_str().ok()) { |
| 41 | 0× | None => true, |
| 42 | 0× | Some(o) => p.hosts.iter().any(|h| o == format!("http://{h}")), |
| 43 | | } |
| 44 | 0× | } |
| 45 | |
|
| 46 | 0× | async fn forward(State(p): State<Arc<Proxy>>, req: Request) -> Response { |
| 47 | 0× | if !ours(&p, req.headers()) { |
| 48 | 0× | return (StatusCode::FORBIDDEN, "this proxy serves its own page only").into_response(); |
| 49 | 0× | } |
| 50 | 0× | let (parts, body) = req.into_parts(); |
| 51 | 0× | let body: Bytes = match body.collect().await { |
| 52 | 0× | Ok(b) => b.to_bytes(), |
| 53 | 0× | Err(_) => return StatusCode::BAD_REQUEST.into_response(), |
| 54 | | }; |
| 55 | 0× | let path = parts.uri.path_and_query().map(|p| p.as_str()).unwrap_or("/"); |
| 56 | 0× | let mut up = hyper::Request::builder().method(parts.method.clone()).uri(path); |
| 57 | 0× | for (k, v) in parts.headers.iter() { |
| 58 | | |
| 59 | 0× | if matches!(k.as_str(), "host" | "origin" | "connection" | "keep-alive" | "transfer-encoding" | "upgrade" | "referer") { |
| 60 | 0× | continue; |
| 61 | 0× | } |
| 62 | 0× | up = up.header(k, v); |
| 63 | | } |
| 64 | 0× | let up = match up.header(header::HOST, "spectra").header(header::CONNECTION, "close").body(Full::new(body)) { |
| 65 | 0× | Ok(r) => r, |
| 66 | 0× | Err(_) => return StatusCode::BAD_REQUEST.into_response(), |
| 67 | | }; |
| 68 | 0× | match p.upstream.forward(up).await { |
| 69 | 0× | Ok(resp) => { |
| 70 | 0× | let (parts, incoming) = resp.into_parts(); |
| 71 | 0× | let mut out = Response::new(Body::new(incoming)); |
| 72 | 0× | *out.status_mut() = parts.status; |
| 73 | 0× | for (k, v) in parts.headers.iter() { |
| 74 | 0× | if !matches!(k.as_str(), "connection" | "transfer-encoding" | "keep-alive") { |
| 75 | 0× | out.headers_mut().append(k, v.clone()); |
| 76 | 0× | } |
| 77 | | } |
| 78 | 0× | out |
| 79 | | } |
| 80 | 0× | Err(e) => (StatusCode::BAD_GATEWAY, format!("Spectra on the server did not answer: {e:#}")).into_response(), |
| 81 | | } |
| 82 | 0× | } |
| 83 | |
|
| 84 | | #[cfg(test)] |
| 85 | | mod tests { |
| 86 | | #[test] |
| 87 | 1× | fn the_link_points_at_the_proxy() { |
| 88 | 1× | 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 | 1× | } |
| 90 | | } |