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