Add docs and refactor for readability
This commit is contained in:
parent
8ae90553ae
commit
a6f9325e33
3 changed files with 310 additions and 276 deletions
13
README.md
13
README.md
|
@ -1,3 +1,16 @@
|
||||||
# Proxima
|
# Proxima
|
||||||
|
|
||||||
Proxima is a simple gateway server.
|
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.
|
||||||
|
|
476
src/main.rs
476
src/main.rs
|
@ -1,5 +1,10 @@
|
||||||
use std::net::SocketAddr;
|
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")]
|
#[tokio::main(flavor = "current_thread")]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
|
@ -22,7 +27,6 @@ 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 client = Client::new();
|
||||||
|
|
||||||
let make_service = make_service_fn(move |_| {
|
let make_service = make_service_fn(move |_| {
|
||||||
|
@ -33,15 +37,175 @@ async fn main() {
|
||||||
// This is the `Service` that will handle the connection.
|
// This is the `Service` that will handle the connection.
|
||||||
// `service_fn` is a helper to convert a function that
|
// `service_fn` is a helper to convert a function that
|
||||||
// returns a Response into a `Service`.
|
// returns a Response into a `Service`.
|
||||||
Ok::<_, Error>(service_fn(move |mut req| {
|
Ok::<_, Error>(service_fn(move |req| {
|
||||||
let config = config.clone();
|
let config = config.clone();
|
||||||
let client = client.clone();
|
let client = client.clone();
|
||||||
|
|
||||||
async move {
|
|
||||||
for Rule (pattern, effect) in config.rules() {
|
|
||||||
println!("{} {}", req.method(), req.uri());
|
println!("{} {}", req.method(), req.uri());
|
||||||
|
|
||||||
|
async move {
|
||||||
|
|
||||||
|
// Perform the first matching rule
|
||||||
|
for Rule (pattern, effect) in config.rules() {
|
||||||
if pattern.matches(&req) {
|
if pattern.matches(&req) {
|
||||||
return match effect {
|
let res = effect.perform(client, req).await;
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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())
|
||||||
|
.expect("Failed to construct response");
|
||||||
|
|
||||||
|
Ok (res)
|
||||||
|
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let server = Server::bind(&addr).serve(make_service);
|
||||||
|
|
||||||
|
if let Err (e) = server.await {
|
||||||
|
eprintln!("server error: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load a config from a path.
|
||||||
|
pub fn load (p: impl AsRef<std::path::Path>) -> std::io::Result<String> {
|
||||||
|
std::fs::read_to_string(p.as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a config string.
|
||||||
|
///
|
||||||
|
/// Example config string:
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// 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 : * ==> 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)
|
||||||
|
}
|
||||||
|
/// 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);
|
||||||
|
|
||||||
|
impl Rule {
|
||||||
|
/// Get the domain of the pattern.
|
||||||
|
pub fn host (&self) -> &str {
|
||||||
|
&self.0.host
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the portspec.
|
||||||
|
pub fn ports (&self) -> &Ports {
|
||||||
|
&self.0.ports
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the associated effect.
|
||||||
|
pub fn effect (&self) -> &Effect {
|
||||||
|
&self.1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A pattern consists of a host and a [portspec][Ports].
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Pattern {
|
||||||
|
host: String,
|
||||||
|
ports: Ports,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Pattern {
|
||||||
|
|
||||||
|
/// Determine whether the given [`Request`] matches this pattern.
|
||||||
|
///
|
||||||
|
/// 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,
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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, .. } => {
|
Effect::Proxy { port, .. } => {
|
||||||
let host = "0.0.0.0"; // Support for custom hosts added later
|
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 path = req.uri().path_and_query().map(|x| x.as_str()).unwrap_or("");
|
||||||
|
@ -64,111 +228,16 @@ async fn main() {
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
/// 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!()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A specification of ports.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum Ports {
|
pub enum Ports {
|
||||||
|
/// Just this one port.
|
||||||
Single (u16),
|
Single (u16),
|
||||||
|
/// Any of the specified ports.
|
||||||
Either (Vec<u16>),
|
Either (Vec<u16>),
|
||||||
|
/// Wildcard, skip port check.
|
||||||
Any
|
Any
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -183,200 +252,3 @@ impl Ports {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A config consists of a set if [`Rule`].
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct Config (Vec<Rule>);
|
|
||||||
|
|
||||||
impl Config {
|
|
||||||
pub fn rules (&self) -> impl Iterator <Item = &Rule> {
|
|
||||||
self.0.iter()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Load a config from a path.
|
|
||||||
pub fn load (p: impl AsRef<std::path::Path>) -> std::io::Result<String> {
|
|
||||||
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 };
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
fn domain (s: &str) -> PResult<'_, String> {
|
|
||||||
take_till(|c: char| !(c == '.' || c.is_alphanumeric()))
|
|
||||||
.map(str::to_string)
|
|
||||||
.parse(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse a [`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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
149
src/parse.rs
Normal file
149
src/parse.rs
Normal 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue