| 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 |
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 |
|
| 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 |
|
| 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 |
} |