From a6f9325e334f8d5cc1a609db3fae8f499cc39a0d Mon Sep 17 00:00:00 2001 From: Riley Apeldoorn Date: Thu, 21 Jul 2022 13:50:52 +0200 Subject: [PATCH] Add docs and refactor for readability --- README.md | 13 ++ src/main.rs | 424 ++++++++++++++++++--------------------------------- src/parse.rs | 149 ++++++++++++++++++ 3 files changed, 310 insertions(+), 276 deletions(-) create mode 100644 src/parse.rs diff --git a/README.md b/README.md index a80610d..33b4887 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,16 @@ # Proxima Proxima is a simple gateway server. + +## Configuring + +Proxima reads the first config file it sees in the following order of preference: + +- `./config` +- `/etc/proxima` + +In these files, each line is a rule consisting of a pattern (consisting of a +hostname and a portspec) and an effect: either a number indicating a port on +`0.0.0.0` to proxy the request to or a string that is used as the `location` +header's value. If the latter effect is specified, Proxima redirects clients +to that location. diff --git a/src/main.rs b/src/main.rs index f3e937c..a5b5029 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,10 @@ use std::net::SocketAddr; -use hyper::{service::{make_service_fn, service_fn}, Client, Error, Server, Response, StatusCode, Body, Request, Uri}; +use hyper::{service::{make_service_fn, service_fn}, Error, Server, Response, StatusCode, Body, Request, Uri, client::HttpConnector}; + +pub mod parse; + +/// An alias for a [`hyper::Client`] with an [`HttpConnector`]. +pub type Client = hyper::Client; #[tokio::main(flavor = "current_thread")] async fn main() { @@ -21,8 +26,7 @@ async fn main() { }; - let addr = SocketAddr::from(([127, 0, 0, 1], 8100)); - + let addr = SocketAddr::from(([127, 0, 0, 1], 8100)); let client = Client::new(); let make_service = make_service_fn(move |_| { @@ -33,43 +37,34 @@ async fn main() { // 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| { + Ok::<_, Error>(service_fn(move |req| { let config = config.clone(); let client = client.clone(); + println!("{} {}", req.method(), req.uri()); + async move { + + // Perform the first matching rule 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() - }), - } + let res = effect.perform(client, req).await; + return res } } - Ok (Response::builder() - .status(StatusCode::BAD_REQUEST) + println!("No matching rule found, returning error"); + + // Return an empty response with a Bad Gateway status code + // if no rules matched (and thus caused the loop to short- + // circuit). + let res = Response::builder() + .status(StatusCode::BAD_GATEWAY) .body(Body::empty()) - .unwrap()) + .expect("Failed to construct response"); + + Ok (res) + } })) } @@ -77,117 +72,18 @@ async fn main() { let server = Server::bind(&addr).serve(make_service); - if let Err(e) = server.await { + if let Err (e) = server.await { eprintln!("server error: {}", e); } } -#[derive(Clone, Debug)] -pub struct Rule (Pattern, Effect); - -impl Rule { - /// Get the domain of the pattern. - pub fn domain (&self) -> &str { - &self.0.domain - } - - /// Get the portspec - pub fn ports (&self) -> &Ports { - &self.0.ports - } - - pub fn effect (&self) -> &Effect { - &self.1 - } -} - -#[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 { - port: u16, - ssl: bool, - }, -} - -impl Effect { - pub async fn perform (&self) -> Response { - todo!() - } -} - -#[derive(Clone, Debug)] -pub enum Ports { - Single (u16), - Either (Vec), - Any -} - -impl Ports { - /// Whether this set of ports includes the given port. - pub fn includes (&self, p: u16) -> bool { - match self { - Ports::Single (x) => *x == p, - Ports::Either (l) => l.contains(&p), - Ports::Any => true, - } - } -} - -/// A config consists of a set if [`Rule`]. +/// A config consists of a set of [`Rule`]s. #[derive(Clone, Debug)] pub struct Config (Vec); impl Config { + /// Get the rules in the config. pub fn rules (&self) -> impl Iterator { self.0.iter() } @@ -203,9 +99,9 @@ pub fn load (p: impl AsRef) -> std::io::Result { /// Example config string: /// /// ```text -/// hmt.riley.lgbt : (80 | 443) --> 6000 # --> is proxy_pass +/// api.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 +/// rly.cx : * ==> riley.lgbt # ==> is HTTP redirect /// ``` pub fn parse (data: String) -> Config { let rules = data @@ -222,161 +118,137 @@ pub fn parse (data: String) -> Config { Config (rules) } -pub mod parse { +/// A rule consists of a [`Pattern`] and an [`Effect`]. If the pattern matches, +/// the effect is performed. +#[derive(Clone, Debug)] +pub struct Rule (Pattern, Effect); - use super::{ Ports, Effect, Pattern, Rule }; - use nom::{ - sequence as seq, - multi as mul, - character::complete as chr, - bytes::complete::{self as byt, tag, take_till}, - Parser, error::ParseError, combinator::opt, - }; - - pub type PResult<'i, T> = nom::IResult<&'i str, T>; - - fn around (a: A, b: B) -> impl Parser - where E: ParseError, - A: Parser + Clone, - B: Parser, - { - seq::delimited(a.clone(), b, a) +impl Rule { + /// Get the domain of the pattern. + pub fn host (&self) -> &str { + &self.0.host } - /// Parse a [`Portspec`]. - pub (super) fn portspec (s: &str) -> PResult<'_, Ports> { - let single = chr::u16; - let either = { - let delim = around(chr::space1, chr::char('|')); - seq::delimited( - chr::char('('), - mul::separated_list1(delim, single), - chr::char(')'), - ) - }; - let any = byt::tag("any"); - - let single = single.map(Ports::Single); - let either = either.map(Ports::Either); - let any = any.map(|_| Ports::Any); - - single.or(either) - .or(any) - .parse(s) + /// Get the portspec. + pub fn ports (&self) -> &Ports { + &self.0.ports } - /// Parse an [`Effect`]. - pub fn effect (s: &str) -> PResult<'_, Effect> { - - let redirect = { - let spaced = |x| seq::delimited(chr::space1, x, chr::space1); - let internal = domain; - - seq::preceded( - spaced(tag("==>")), - internal, - ).map(Effect::Redirect) - }; - - let proxy = { - let spaced = |x| seq::delimited(chr::space1, x, chr::space1); - let ssl = opt(seq::delimited( - seq::preceded(chr::space1, chr::char('[')), - tag("ssl"), - chr::char(']'), - )).map(|o| o.is_some()); - let internal = seq::terminated(chr::u16, chr::space0); - - seq::preceded( - spaced(tag("-->")), - internal.and(ssl), - ).map(|(port, ssl)| Effect::Proxy { ssl, port }) - }; - - redirect.or(proxy) - .parse(s) - + /// Get the associated effect. + pub fn effect (&self) -> &Effect { + &self.1 } +} - fn domain (s: &str) -> PResult<'_, String> { - take_till(|c: char| !(c == '.' || c.is_alphanumeric())) - .map(str::to_string) - .parse(s) - } +/// A pattern consists of a host and a [portspec][Ports]. +#[derive(Clone, Debug)] +pub struct Pattern { + host: String, + ports: Ports, +} - /// Parse a [`Pattern`]. +impl Pattern { + + /// Determine whether the given [`Request`] matches this pattern. /// - /// ``` - /// use proxima::parse; - /// - /// # fn main () -> parse::PResult<'static, ()> { - /// let (_, pattern) = parse::pattern("example.com : any")?; - /// # Ok ("", ()) - /// # } - /// ``` - pub fn pattern (s: &str) -> PResult<'_, Pattern> { - 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) - } - - /// Parse a [`Rule`]. - pub fn rule (s: &str) -> PResult<'_, Rule> { - pattern.and(effect) - .map(|(p, e)| Rule (p, e)) - .parse(s) - } - - #[cfg(test)] - mod tests { - - use super::*; - - /// Test whether a pattern containing an Any portspec gets parsed - /// correctly. - #[test] - fn simple_pattern () { - let input = "example.com : any"; - let (_, Pattern { domain, ports }) = pattern(input).unwrap(); - assert!(domain == "example.com"); - assert!(match ports { - Ports::Any => true, - _ => false, - }) - } - - /// Test whether an Either portspec is parsed correctly. - #[test] - fn either_portspec () { - let input = "(69 | 420)"; - assert!(match portspec(input).unwrap() { - ("", Ports::Either(p)) => p == [69, 420], - _ => false, - }) - } - - /// Test whether domain names are parsed correctly. - #[test] - fn domains () { - let inputs = ["example.com", "im.badat.dev", "riley.lgbt", "toot.site", "a.b.c.d.e.f.g.h"]; - // Each of these should be considered valid - for input in inputs { - domain(input).unwrap(); + /// For a request to match, it needs a `host` header. + pub fn matches (&self, req: &Request) -> bool { + let (host, port) = { + // We need to parse the `host` header. + if let Some (uri) = req + .headers() + .get("host") + .and_then(|x| x.to_str().ok()) + .and_then(|x| x.parse::().ok()) + { + let host = uri.host().map(str::to_string); + let port = uri.port_u16(); + (host, port.unwrap_or(80)) + } else { + (None, 80) } + }; + + match host { + // The domain needs to match in all cases + Some (h) if &h == &self.host => match &self.ports { + // We don't care about the port from the header if the + // portspec is a wildcard + Ports::Any => true, + // Check if the port is included in the spec. If the + // port couldn't be parsed, it defaults to `80`. + spec => spec.includes(port), + }, + // If no host header or the host header does not equal + // the specified domain, the request does not match + _ => false, } - /// Test whether a simple rule gets parsed correctly. - #[test] - fn simple_rule () { - let input = "example.gay : any --> 3000"; - let (_, Rule (_, effect)) = rule(input).unwrap(); - assert!(match effect { - Effect::Proxy { port: 3000, ssl } if !ssl => true, - _ => false, - }); - } } } +/// What to do with a matched request. +#[derive(Clone, Debug)] +pub enum Effect { + /// Redirect to the given URI. + Redirect (String), + /// Proxy the request, optionally with managed SSL. + Proxy { + /// The port on `0.0.0.0` to proxy to. + port: u16, + /// Whether to manage SSL for this rule. + ssl: bool, + }, +} + +impl Effect { + /// Perform the effect. + pub async fn perform (&self, client: Client, mut req: Request) -> hyper::Result> { + match self { + 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() + }), + } + } +} + +/// A specification of ports. +#[derive(Clone, Debug)] +pub enum Ports { + /// Just this one port. + Single (u16), + /// Any of the specified ports. + Either (Vec), + /// Wildcard, skip port check. + Any +} + +impl Ports { + /// Whether this set of ports includes the given port. + pub fn includes (&self, p: u16) -> bool { + match self { + Ports::Single (x) => *x == p, + Ports::Either (l) => l.contains(&p), + Ports::Any => true, + } + } +} + diff --git a/src/parse.rs b/src/parse.rs new file mode 100644 index 0000000..370d64f --- /dev/null +++ b/src/parse.rs @@ -0,0 +1,149 @@ +//! Parsers for parts of the config file. + +// TODO: comments + +use super::{ Ports, Effect, Pattern, Rule }; +use nom::{ + sequence as seq, + multi as mul, + character::complete as chr, + bytes::complete::{self as byt, tag, take_till}, + Parser, error::ParseError, combinator::opt, +}; + +/// The result of running a parser. +pub type PResult<'i, T> = nom::IResult<&'i str, T>; + +fn around (a: A, b: B) -> impl Parser +where E: ParseError, + A: Parser + Clone, + B: Parser, +{ + seq::delimited(a.clone(), b, a) +} + +/// Parse a set of [`Ports`]. +pub fn ports (s: &str) -> PResult<'_, Ports> { + let single = chr::u16; + let either = { + let delim = around(chr::space1, chr::char('|')); + seq::delimited( + chr::char('('), + mul::separated_list1(delim, single), + chr::char(')'), + ) + }; + let any = byt::tag("*"); + + let single = single.map(Ports::Single); + let either = either.map(Ports::Either); + let any = any.map(|_| Ports::Any); + + single.or(either) + .or(any) + .parse(s) +} + +/// Parse an [`Effect`]. +pub fn effect (s: &str) -> PResult<'_, Effect> { + + let redirect = { + let spaced = |x| seq::delimited(chr::space1, x, chr::space1); + + seq::preceded( + spaced(tag("==>")), + target, + ).map(Effect::Redirect) + }; + + let proxy = { + let spaced = |x| seq::delimited(chr::space1, x, chr::space1); + let ssl = opt(seq::delimited( + seq::preceded(chr::space1, chr::char('[')), + tag("ssl"), + chr::char(']'), + )).map(|o| o.is_some()); + let internal = seq::terminated(chr::u16, chr::space0); + + seq::preceded( + spaced(tag("-->")), + internal.and(ssl), + ).map(|(port, ssl)| Effect::Proxy { ssl, port }) + }; + + redirect.or(proxy) + .parse(s) + +} + +fn target (s: &str) -> PResult<'_, String> { + take_till(|c: char| !(c == '.' || c.is_alphanumeric())) + .map(str::to_string) + .parse(s) +} + +/// Parse a [`Pattern`]. +pub fn pattern (s: &str) -> PResult<'_, Pattern> { + let spaced = |x| seq::delimited(chr::space1, x, chr::space1); + seq::separated_pair(target, spaced(chr::char(':')), ports) + .map(|(domain, ports)| Pattern { host: domain, ports }) + .parse(s) +} + +/// Parse a [`Rule`]. +pub fn rule (s: &str) -> PResult<'_, Rule> { + pattern.and(effect) + .map(|(p, e)| Rule (p, e)) + .parse(s) +} + +#[cfg(test)] +mod tests { + + use super::*; + + /// Test whether a pattern containing an Any portspec gets parsed + /// correctly. + #[test] + fn simple_pattern () { + let input = "example.com : *"; + let (_, Pattern { host: domain, ports }) = pattern(input).unwrap(); + assert!(domain == "example.com"); + assert!(match ports { + Ports::Any => true, + _ => false, + }) + } + + /// Test whether an Either portspec is parsed correctly. + #[test] + fn either_ports () { + let input = "(69 | 420)"; + assert!(match ports(input).unwrap() { + ("", Ports::Either(p)) => p == [69, 420], + _ => false, + }) + } + + /// Test whether target names are parsed correctly. + #[test] + fn targets () { + let inputs = ["example.com", "im.badat.dev", "riley.lgbt", "toot.site", "a.b.c.d.e.f.g.h"]; + // Each of these should be considered valid + for input in inputs { + target(input).unwrap(); + } + } + + /// Test whether a simple rule gets parsed correctly. + #[test] + fn simple_rule () { + let input = "example.gay : * --> 3000"; + let (_, Rule (_, effect)) = rule(input).unwrap(); + assert!(match effect { + Effect::Proxy { port: 3000, ssl } if !ssl => true, + _ => false, + }); + } +} +