1use std::fmt;
21
22pub const MAX_ROUTES: usize = 64;
25const MAX_SEGMENTS: usize = 64;
26
27pub const METHOD_OVERRIDE_HEADERS: &[&str] = &[
31 "x-http-method-override",
32 "x-http-method",
33 "x-method-override",
34];
35
36const ECHOING: &[&str] = &["TRACE", "TRACK", "CONNECT"];
38
39const METHOD_OVERRIDE_PARAMETER: &str = "_method";
41
42#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct Route {
45 method: String,
46 segments: Vec<Segment>,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq)]
50enum Segment {
51 Literal(String),
52 One,
54 Any,
56}
57
58impl Route {
59 pub fn parse(text: &str) -> Result<Self, String> {
61 let (method, path) = text
62 .split_once(' ')
63 .ok_or_else(|| format!("rule `{text}` must be `METHOD /path`"))?;
64 if method.is_empty() || !method.bytes().all(|b| b.is_ascii_uppercase()) {
65 return Err(format!(
66 "rule `{text}`: the method must be capital letters (`GET`, `POST`)"
67 ));
68 }
69 if ECHOING.contains(&method) {
72 return Err(format!(
73 "rule `{text}`: {method} is never given a secret: the server would send it back \
74 or the proxy could not read what follows"
75 ));
76 }
77 let Some(rest) = path.strip_prefix('/') else {
78 return Err(format!("rule `{text}`: the path must start with `/`"));
79 };
80 let mut segments = Vec::new();
81 let parts: Vec<&str> = if rest.is_empty() {
82 Vec::new()
83 } else {
84 rest.split('/').collect()
85 };
86 for (n, part) in parts.iter().enumerate() {
87 let last = n + 1 == parts.len();
88 segments.push(match *part {
89 "*" => Segment::One,
90 "**" => Segment::Any,
91 "" if last => Segment::Literal(String::new()),
93 "" => return Err(format!("rule `{text}`: the path has an empty segment")),
94 "." | ".." => return Err(format!("rule `{text}`: `.` and `..` are not segments")),
95 literal if literal.bytes().all(is_literal_byte) => {
96 Segment::Literal(literal.to_owned())
97 }
98 _ => {
99 return Err(format!(
100 "rule `{text}`: a segment is a literal of letters, digits and \
101 `-._~!$&'()+,=:@`, `*` or `**`"
102 ));
103 }
104 });
105 }
106 if segments.len() > MAX_SEGMENTS {
107 return Err(format!(
108 "rule `{text}` has more than {MAX_SEGMENTS} segments"
109 ));
110 }
111 Ok(Self {
112 method: method.to_owned(),
113 segments,
114 })
115 }
116
117 #[must_use]
119 pub fn matches(&self, method: &str, path: &RequestPath) -> bool {
120 self.method == method && matches(&self.segments, &path.segments)
121 }
122}
123
124fn matches(rule: &[Segment], path: &[String]) -> bool {
127 let mut matched = vec![false; path.len() + 1];
128 matched[0] = true;
129 for segment in rule {
130 let mut next = vec![false; path.len() + 1];
131 for j in 0..=path.len() {
132 next[j] = match segment {
133 Segment::Any => matched[j] || (j > 0 && next[j - 1]),
134 Segment::One => j > 0 && matched[j - 1] && !path[j - 1].is_empty(),
135 Segment::Literal(literal) => j > 0 && matched[j - 1] && path[j - 1] == *literal,
136 };
137 }
138 matched = next;
139 }
140 matched[path.len()]
141}
142
143fn is_literal_byte(b: u8) -> bool {
145 b.is_ascii_alphanumeric() || b"-._~!$&'()+,=:@".contains(&b)
146}
147
148#[derive(Debug, Clone, PartialEq, Eq)]
151pub struct RequestPath {
152 segments: Vec<String>,
153 overrides_method: bool,
154}
155
156#[derive(Debug, Clone, PartialEq, Eq)]
158#[non_exhaustive]
159pub enum PathRefusal {
160 NotOriginForm,
162 Malformed,
164 DotSegment,
166 AmbiguousSegment,
169 EmptySegment,
171}
172
173impl fmt::Display for PathRefusal {
174 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
175 f.write_str(match self {
176 Self::NotOriginForm => "the request target is not a path starting with `/`",
177 Self::Malformed => {
178 "the path holds a byte outside visible ASCII, a `\\`, a `#` or a malformed `%` \
179 escape"
180 }
181 Self::DotSegment => "the path holds a `.` or `..` segment, in some spelling",
182 Self::AmbiguousSegment => {
183 "a path segment holds `;` or decodes to `/`, `\\`, `%`, `;` or a control byte"
184 }
185 Self::EmptySegment => "the path holds an empty segment (`//`)",
186 })
187 }
188}
189
190impl RequestPath {
191 pub fn parse(target: &str) -> Result<Self, PathRefusal> {
194 let Some(rest) = target.strip_prefix('/') else {
195 return Err(PathRefusal::NotOriginForm);
196 };
197 if !target.bytes().all(|b| b.is_ascii_graphic()) || target.contains(['\\', '#']) {
198 return Err(PathRefusal::Malformed);
199 }
200 let (path, query) = rest.split_once('?').unwrap_or((rest, ""));
201 let parts: Vec<&str> = if path.is_empty() {
202 Vec::new()
203 } else {
204 path.split('/').collect()
205 };
206 let mut segments = Vec::with_capacity(parts.len());
207 for (n, part) in parts.iter().enumerate() {
208 if part.is_empty() && n + 1 < parts.len() {
209 return Err(PathRefusal::EmptySegment);
210 }
211 if part.contains(';') {
212 return Err(PathRefusal::AmbiguousSegment);
213 }
214 let decoded = decode(part).ok_or(PathRefusal::Malformed)?;
215 let decoded = String::from_utf8(decoded).map_err(|_| PathRefusal::AmbiguousSegment)?;
216 if decoded == "." || decoded == ".." {
217 return Err(PathRefusal::DotSegment);
218 }
219 if decoded
220 .chars()
221 .any(|c| c.is_control() || matches!(c, '/' | '\\' | '%' | ';'))
222 {
223 return Err(PathRefusal::AmbiguousSegment);
224 }
225 segments.push(decoded);
226 }
227 let overrides_method = query.split('&').any(|pair| {
228 let name = pair.split_once('=').map_or(pair, |(name, _)| name);
229 decode(name)
230 .is_none_or(|n| n.eq_ignore_ascii_case(METHOD_OVERRIDE_PARAMETER.as_bytes()))
231 });
232 Ok(Self {
233 segments,
234 overrides_method,
235 })
236 }
237
238 #[must_use]
241 pub fn overrides_method(&self) -> bool {
242 self.overrides_method
243 }
244}
245
246fn decode(text: &str) -> Option<Vec<u8>> {
248 let bytes = text.as_bytes();
249 let mut out = Vec::with_capacity(bytes.len());
250 let mut i = 0;
251 while i < bytes.len() {
252 if bytes[i] == b'%' {
253 let hex = bytes
255 .get(i + 1..i + 3)
256 .filter(|h| h.iter().all(u8::is_ascii_hexdigit))?;
257 out.push(u8::from_str_radix(std::str::from_utf8(hex).ok()?, 16).ok()?);
258 i += 3;
259 } else {
260 out.push(bytes[i]);
261 i += 1;
262 }
263 }
264 Some(out)
265}
266
267#[cfg(test)]
268mod tests;