Add docs and refactor for readability

This commit is contained in:
Riley Apeldoorn 2022-07-21 13:50:52 +02:00
parent 8ae90553ae
commit a6f9325e33
3 changed files with 310 additions and 276 deletions

View File

@ -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.

View File

@ -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<C = HttpConnector> = hyper::Client<C>;
#[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 <T> (&self, req: &Request<T>) -> 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::<Uri>().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<Body> {
todo!()
}
}
#[derive(Clone, Debug)]
pub enum Ports {
Single (u16),
Either (Vec<u16>),
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<Rule>);
impl Config {
/// Get the rules in the config.
pub fn rules (&self) -> impl Iterator <Item = &Rule> {
self.0.iter()
}
@ -203,9 +99,9 @@ pub fn load (p: impl AsRef<std::path::Path>) -> std::io::Result<String> {
/// 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 <I, O, P, E, A, B> (a: A, b: B) -> impl Parser<I, O, E>
where E: ParseError<I>,
A: Parser<I, P, E> + Clone,
B: Parser<I, O, E>,
{
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 <T> (&self, req: &Request<T>) -> 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::<Uri>().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<Body>) -> hyper::Result<Response<Body>> {
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<u16>),
/// 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,
}
}
}

149
src/parse.rs Normal file
View File

@ -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 <I, O, P, E, A, B> (a: A, b: B) -> impl Parser<I, O, E>
where E: ParseError<I>,
A: Parser<I, P, E> + Clone,
B: Parser<I, O, E>,
{
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,
});
}
}