From ccb4d96da757c4213ca2120c28a48dc57a1bf285 Mon Sep 17 00:00:00 2001 From: Riley Apeldoorn Date: Thu, 21 Jul 2022 11:31:14 +0200 Subject: [PATCH] Implement proxying and redirection --- src/main.rs | 190 +++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 150 insertions(+), 40 deletions(-) diff --git a/src/main.rs b/src/main.rs index ad9cdbc..732c0ba 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,5 @@ -use std::{net::SocketAddr, convert::Infallible}; - -use hyper::{Client, Server, service::{make_service_fn, service_fn}, body::HttpBody, Body, Response, Request, Method, http, upgrade::{self, Upgraded}}; -use tokio::net::TcpStream; +use std::net::SocketAddr; +use hyper::{service::{make_service_fn, service_fn}, Client, Error, Server, Response, StatusCode, Body, Request, Uri}; // https://github.com/hyperium/hyper/blob/master/examples/gateway.rs // https://en.wikipedia.org/wiki/Gateway_(telecommunications) @@ -13,12 +11,12 @@ async fn main() { let config = { // Prefer ./config over /etc/proxima - let file = unsafe { + let file = { ["./config", "/etc/proxima"] .iter() .map(load) .reduce(Result::or) - .unwrap_unchecked() + .expect("Impossible") }; file.map(parse) @@ -27,8 +25,68 @@ async fn main() { }; let addr = SocketAddr::from(([127, 0, 0, 1], 8100)); + + let client = Client::new(); + + let make_service = make_service_fn(move |_| { + let client = client.clone(); + let config = config.clone(); + + async move { + // This is the `Service` that will handle the connection. + // `service_fn` is a helper to convert a function that + // returns a Response into a `Service`. + Ok::<_, Error>(service_fn(move |mut req| { + let config = config.clone(); + let client = client.clone(); + + async move { + for Rule (pattern, effect) in config.rules() { + println!("{} {}", req.method(), req.uri()); + if pattern.matches(&req) { + return match effect { + Effect::Proxy { port, .. } => { + let host = "0.0.0.0"; // Support for custom hosts added later + let path = req.uri().path_and_query().map(|x| x.as_str()).unwrap_or(""); + let target = format!("http://{host}:{port}{path}"); + + let uri = target.parse().unwrap(); + *req.uri_mut() = uri; + + println!("Proxying to {target}"); + + client.request(req).await + }, + Effect::Redirect (uri) => Ok ({ + println!("Redirecting to {uri}"); + Response::builder() + .status(StatusCode::PERMANENT_REDIRECT) + .header("Location", uri) + .body(Body::empty()) + .unwrap() + }), + } + } + } + + Ok (Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::empty()) + .unwrap()) + } + })) + } + }); + + let server = Server::bind(&addr).serve(make_service); + + if let Err(e) = server.await { + eprintln!("server error: {}", e); + } + } +#[derive(Clone, Debug)] pub struct Rule (Pattern, Effect); impl Rule { @@ -47,11 +105,55 @@ impl Rule { } } +#[derive(Clone, Debug)] pub struct Pattern { domain: String, ports: Ports, } +impl Pattern { + pub fn matches (&self, req: &Request) -> bool { + let uri = req.uri(); + let (host, port) = { + let host = req + .headers() + .get("host") + .and_then(|x| x.to_str().ok()) + .and_then(|x| x.parse::().ok()); + + let h = uri + .host() + .map(|x| x.to_string()) + .or_else(|| { + host.clone().and_then(|x| { + x.host().map(|x| x.to_string()) + }) + }); + + let p = uri + .port_u16() + .or_else(|| { + host.and_then(|x| x.port_u16()) + }); + + (h, p) + }; + + match host { + Some (h) if &h == &self.domain => match &self.ports { + Ports::Any => true, + spec => match port { + Some (p) => spec.includes(p), + None => false, + } + }, + _ => false, + } + + } +} + +#[derive(Clone, Debug)] pub enum Effect { Redirect (String), Proxy { @@ -61,11 +163,12 @@ pub enum Effect { } impl Effect { - pub async fn perform (&self) -> std::io::Result<()> { + pub async fn perform (&self) -> Response { todo!() } } +#[derive(Clone, Debug)] pub enum Ports { Single (u16), Either (Vec), @@ -83,6 +186,45 @@ impl Ports { } } +/// A config consists of a set if [`Rule`]. +#[derive(Clone, Debug)] +pub struct Config (Vec); + +impl Config { + pub fn rules (&self) -> impl Iterator { + self.0.iter() + } +} + +/// Load a config from a path. +pub fn load (p: impl AsRef) -> std::io::Result { + std::fs::read_to_string(p.as_ref()) +} + +/// Parse a config string. +/// +/// Example config string: +/// +/// ```text +/// hmt.riley.lgbt : (80 | 443) --> 6000 # --> is proxy_pass +/// riley.lgbt : (80 | 443) --> 3000 [ssl] # add [ssl] to automate ssl for this domain +/// rly.cx : any ==> riley.lgbt # ==> is HTTP redirect +/// ``` +pub fn parse (data: String) -> Config { + let rules = data + .lines() + .map(parse::rule) + .filter_map(|x| match x { + Ok ((_, rule)) => Some (rule), + Err (e) => { + eprintln!("Error parsing rule: {:?}", e); + None + }, + }) + .collect(); + + Config (rules) +} pub mod parse { use super::{ Ports, Effect, Pattern, Rule }; @@ -176,7 +318,7 @@ pub mod parse { /// # } /// ``` pub fn pattern (s: &str) -> PResult<'_, Pattern> { - let spaced = |x| seq::delimited(chr::char(' '), x, chr::char(' ')); + let spaced = |x| seq::delimited(chr::space1, x, chr::space1); seq::separated_pair(domain, spaced(chr::char(':')), portspec) .map(|(domain, ports)| Pattern { domain, ports }) .parse(s) @@ -241,35 +383,3 @@ pub mod parse { } -/// A config consists of a set if [`Rule`]. -pub struct Config (Vec); - -/// Load a config from a path. -pub fn load (p: impl AsRef) -> std::io::Result { - std::fs::read_to_string(p.as_ref()) -} - -/// Parse a config string. -/// -/// Example config string: -/// -/// ```text -/// hmt.riley.lgbt : (80 | 443) --> 6000 # --> is proxy_pass -/// riley.lgbt : (80 | 443) --> 3000 [ssl] # add [ssl] to automate ssl for this domain -/// rly.cx : any ==> riley.lgbt # ==> is HTTP redirect -/// ``` -pub fn parse (data: String) -> Config { - let rules = data - .lines() - .map(parse::rule) - .filter_map(|x| match x { - Ok ((_, rule)) => Some (rule), - Err (e) => { - eprintln!("Error parsing rule: {:?}", e); - None - }, - }) - .collect(); - - Config (rules) -}