use std::fmt;
pub const MAX_ROUTES: usize = 64;
const MAX_SEGMENTS: usize = 64;
pub const METHOD_OVERRIDE_HEADERS: &[&str] = &[
"x-http-method-override",
"x-http-method",
"x-method-override",
];
const ECHOING: &[&str] = &["TRACE", "TRACK", "CONNECT"];
const METHOD_OVERRIDE_PARAMETER: &str = "_method";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Route {
method: String,
segments: Vec<Segment>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum Segment {
Literal(String),
One,
Any,
}
impl Route {
pub fn parse(text: &str) -> Result<Self, String> {
let (method, path) = text
.split_once(' ')
.ok_or_else(|| format!("rule `{text}` must be `METHOD /path`"))?;
if method.is_empty() || !method.bytes().all(|b| b.is_ascii_uppercase()) {
return Err(format!(
"rule `{text}`: the method must be capital letters (`GET`, `POST`)"
));
}
if ECHOING.contains(&method) {
return Err(format!(
"rule `{text}`: {method} is never given a secret: the server would send it back \
or the proxy could not read what follows"
));
}
let Some(rest) = path.strip_prefix('/') else {
return Err(format!("rule `{text}`: the path must start with `/`"));
};
let mut segments = Vec::new();
let parts: Vec<&str> = if rest.is_empty() {
Vec::new()
} else {
rest.split('/').collect()
};
for (n, part) in parts.iter().enumerate() {
let last = n + 1 == parts.len();
segments.push(match *part {
"*" => Segment::One,
"**" => Segment::Any,
"" if last => Segment::Literal(String::new()),
"" => return Err(format!("rule `{text}`: the path has an empty segment")),
"." | ".." => return Err(format!("rule `{text}`: `.` and `..` are not segments")),
literal if literal.bytes().all(is_literal_byte) => {
Segment::Literal(literal.to_owned())
}
_ => {
return Err(format!(
"rule `{text}`: a segment is a literal of letters, digits and \
`-._~!$&'()+,=:@`, `*` or `**`"
));
}
});
}
if segments.len() > MAX_SEGMENTS {
return Err(format!(
"rule `{text}` has more than {MAX_SEGMENTS} segments"
));
}
Ok(Self {
method: method.to_owned(),
segments,
})
}
#[must_use]
pub fn matches(&self, method: &str, path: &RequestPath) -> bool {
self.method == method && matches(&self.segments, &path.segments)
}
}
fn matches(rule: &[Segment], path: &[String]) -> bool {
let mut matched = vec![false; path.len() + 1];
matched[0] = true;
for segment in rule {
let mut next = vec![false; path.len() + 1];
for j in 0..=path.len() {
next[j] = match segment {
Segment::Any => matched[j] || (j > 0 && next[j - 1]),
Segment::One => j > 0 && matched[j - 1] && !path[j - 1].is_empty(),
Segment::Literal(literal) => j > 0 && matched[j - 1] && path[j - 1] == *literal,
};
}
matched = next;
}
matched[path.len()]
}
fn is_literal_byte(b: u8) -> bool {
b.is_ascii_alphanumeric() || b"-._~!$&'()+,=:@".contains(&b)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RequestPath {
segments: Vec<String>,
overrides_method: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum PathRefusal {
NotOriginForm,
Malformed,
DotSegment,
AmbiguousSegment,
EmptySegment,
}
impl fmt::Display for PathRefusal {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::NotOriginForm => "the request target is not a path starting with `/`",
Self::Malformed => {
"the path holds a byte outside visible ASCII, a `\\`, a `#` or a malformed `%` \
escape"
}
Self::DotSegment => "the path holds a `.` or `..` segment, in some spelling",
Self::AmbiguousSegment => {
"a path segment holds `;` or decodes to `/`, `\\`, `%`, `;` or a control byte"
}
Self::EmptySegment => "the path holds an empty segment (`//`)",
})
}
}
impl RequestPath {
pub fn parse(target: &str) -> Result<Self, PathRefusal> {
let Some(rest) = target.strip_prefix('/') else {
return Err(PathRefusal::NotOriginForm);
};
if !target.bytes().all(|b| b.is_ascii_graphic()) || target.contains(['\\', '#']) {
return Err(PathRefusal::Malformed);
}
let (path, query) = rest.split_once('?').unwrap_or((rest, ""));
let parts: Vec<&str> = if path.is_empty() {
Vec::new()
} else {
path.split('/').collect()
};
let mut segments = Vec::with_capacity(parts.len());
for (n, part) in parts.iter().enumerate() {
if part.is_empty() && n + 1 < parts.len() {
return Err(PathRefusal::EmptySegment);
}
if part.contains(';') {
return Err(PathRefusal::AmbiguousSegment);
}
let decoded = decode(part).ok_or(PathRefusal::Malformed)?;
let decoded = String::from_utf8(decoded).map_err(|_| PathRefusal::AmbiguousSegment)?;
if decoded == "." || decoded == ".." {
return Err(PathRefusal::DotSegment);
}
if decoded
.chars()
.any(|c| c.is_control() || matches!(c, '/' | '\\' | '%' | ';'))
{
return Err(PathRefusal::AmbiguousSegment);
}
segments.push(decoded);
}
let overrides_method = query.split('&').any(|pair| {
let name = pair.split_once('=').map_or(pair, |(name, _)| name);
decode(name)
.is_none_or(|n| n.eq_ignore_ascii_case(METHOD_OVERRIDE_PARAMETER.as_bytes()))
});
Ok(Self {
segments,
overrides_method,
})
}
#[must_use]
pub fn overrides_method(&self) -> bool {
self.overrides_method
}
}
fn decode(text: &str) -> Option<Vec<u8>> {
let bytes = text.as_bytes();
let mut out = Vec::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'%' {
let hex = bytes
.get(i + 1..i + 3)
.filter(|h| h.iter().all(u8::is_ascii_hexdigit))?;
out.push(u8::from_str_radix(std::str::from_utf8(hex).ok()?, 16).ok()?);
i += 3;
} else {
out.push(bytes[i]);
i += 1;
}
}
Some(out)
}
#[cfg(test)]
mod tests;