spacegate_kernel/service/http_route/
match_request.rs

1use hyper::{
2    http::{HeaderName, HeaderValue},
3    Uri,
4};
5use regex::Regex;
6
7use crate::{utils::query_kv::QueryKvIter, BoxError, Request, SgBody};
8
9/// PathMatchType specifies the semantics of how HTTP paths should be compared.
10#[derive(Debug, Clone)]
11pub enum HttpPathMatchRewrite {
12    /// Matches the URL path exactly and with case sensitivity.
13    Exact(String, Option<String>),
14    /// Matches based on a URL path prefix split by /. Matching is case sensitive and done on a path element by element basis.
15    /// A path element refers to the list of labels in the path split by the / separator. When specified, a trailing / is ignored.
16    Prefix(String, Option<String>),
17    /// Matches if the URL path matches the given regular expression with case sensitivity.
18    RegExp(Regex, Option<String>),
19}
20
21impl HttpPathMatchRewrite {
22    pub fn prefix<S: Into<String>>(s: S) -> Self {
23        Self::Prefix(s.into(), None)
24    }
25    pub fn exact<S: Into<String>>(s: S) -> Self {
26        Self::Exact(s.into(), None)
27    }
28    pub fn regex(re: Regex) -> Self {
29        Self::RegExp(re, None)
30    }
31    pub fn replace_with(self, replace: impl Into<String>) -> Self {
32        match self {
33            HttpPathMatchRewrite::Exact(path, _) => HttpPathMatchRewrite::Exact(path, Some(replace.into())),
34            HttpPathMatchRewrite::Prefix(path, _) => HttpPathMatchRewrite::Prefix(path, Some(replace.into())),
35            HttpPathMatchRewrite::RegExp(re, _) => HttpPathMatchRewrite::RegExp(re, Some(replace.into())),
36        }
37    }
38    pub fn rewrite(&self, path: &str) -> Option<String> {
39        match self {
40            HttpPathMatchRewrite::Exact(_, Some(replace)) => {
41                if replace.eq_ignore_ascii_case(path) {
42                    Some(replace.clone())
43                } else {
44                    None
45                }
46            }
47            HttpPathMatchRewrite::Prefix(prefix, Some(replace)) => {
48                fn not_empty(s: &&str) -> bool {
49                    !s.is_empty()
50                }
51                let mut path_segments = path.split('/').filter(not_empty);
52                let mut prefix_segments = prefix.split('/').filter(not_empty);
53                loop {
54                    match (path_segments.next(), prefix_segments.next()) {
55                        (Some(path_seg), Some(prefix_seg)) => {
56                            if !path_seg.eq_ignore_ascii_case(prefix_seg) {
57                                return None;
58                            }
59                        }
60                        (None, None) => {
61                            // handle with duplicated stash and no stash
62                            let mut new_path = String::from("/");
63                            new_path.push_str(replace.trim_start_matches('/'));
64                            return Some(new_path);
65                        }
66                        (Some(rest_path), None) => {
67                            let mut new_path = String::from("/");
68                            let replace_value = replace.trim_matches('/');
69                            new_path.push_str(replace_value);
70                            if !replace_value.is_empty() {
71                                new_path.push('/');
72                            }
73                            new_path.push_str(rest_path);
74                            for seg in path_segments {
75                                new_path.push('/');
76                                new_path.push_str(seg);
77                            }
78                            if path.ends_with('/') {
79                                new_path.push('/')
80                            }
81                            return Some(new_path);
82                        }
83                        (None, Some(_)) => return None,
84                    }
85                }
86            }
87            HttpPathMatchRewrite::RegExp(re, Some(replace)) => Some(re.replace(path, replace).to_string()),
88            _ => None,
89        }
90    }
91}
92
93#[derive(Debug, Clone)]
94pub enum SgHttpHeaderMatchRewritePolicy {
95    /// Matches the HTTP header exactly and with case sensitivity.
96    Exact(HeaderValue, Option<HeaderValue>),
97    /// Matches if the Http header matches the given regular expression with case sensitivity.
98    Regular(Regex, Option<String>),
99}
100
101#[derive(Debug, Clone)]
102pub struct SgHttpHeaderMatchRewrite {
103    /// Name is the name of the HTTP Header to be matched. Name matching MUST be case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2).
104    pub header_name: HeaderName,
105    pub policy: SgHttpHeaderMatchRewritePolicy,
106}
107
108impl SgHttpHeaderMatchRewrite {
109    pub fn regex(name: impl Into<HeaderName>, re: Regex) -> Self {
110        Self {
111            header_name: name.into(),
112            policy: SgHttpHeaderMatchRewritePolicy::Regular(re, None),
113        }
114    }
115    pub fn exact(name: impl Into<HeaderName>, value: impl Into<HeaderValue>) -> Self {
116        Self {
117            header_name: name.into(),
118            policy: SgHttpHeaderMatchRewritePolicy::Exact(value.into(), None),
119        }
120    }
121    pub fn rewrite(&self, req: &Request<SgBody>) -> Option<HeaderValue> {
122        let header_value = req.headers().get(&self.header_name)?;
123        let s = header_value.to_str().ok()?;
124        match &self.policy {
125            SgHttpHeaderMatchRewritePolicy::Exact(_, Some(replace)) => {
126                if s == replace {
127                    Some(replace.clone())
128                } else {
129                    None
130                }
131            }
132            SgHttpHeaderMatchRewritePolicy::Regular(re, Some(replace)) => {
133                if re.is_match(s) {
134                    Some(HeaderValue::from_str(replace).ok()?)
135                } else {
136                    None
137                }
138            }
139            _ => None,
140        }
141    }
142}
143
144#[derive(Debug, Clone)]
145pub enum SgHttpQueryMatchPolicy {
146    /// Matches the HTTP query parameter exactly and with case sensitivity.
147    Exact(String),
148    /// Matches if the Http query parameter matches the given regular expression with case sensitivity.
149    Regular(Regex),
150}
151
152#[derive(Debug, Clone)]
153pub struct HttpQueryMatch {
154    pub name: String,
155    pub policy: SgHttpQueryMatchPolicy,
156}
157
158#[derive(Default, Debug, Clone)]
159
160pub struct HttpMethodMatch(pub String);
161
162/// HTTPRouteMatch defines the predicate used to match requests to a given action.
163/// Multiple match types are ANDed together, i.e. the match will evaluate to true only if all conditions are satisfied.
164#[derive(Default, Debug, Clone)]
165pub struct HttpRouteMatch {
166    /// Path specifies a HTTP request path matcher.
167    /// If this field is not specified, a default prefix match on the “/” path is provided.
168    pub path: Option<HttpPathMatchRewrite>,
169    /// Headers specifies HTTP request header matchers.
170    /// Multiple match values are ANDed together, meaning, a request must match all the specified headers to select the route.
171    pub header: Option<Vec<SgHttpHeaderMatchRewrite>>,
172    /// Query specifies HTTP query parameter matchers.
173    /// Multiple match values are ANDed together, meaning, a request must match all the specified query parameters to select the route.
174    pub query: Option<Vec<HttpQueryMatch>>,
175    /// Method specifies HTTP method matcher.
176    /// When specified, this route will be matched only if the request has the specified method.
177    pub method: Option<Vec<HttpMethodMatch>>,
178}
179
180impl HttpRouteMatch {
181    /// rewrite request path and headers
182    /// # Errors
183    /// Rewritten path is invalid.
184    pub fn rewrite(&self, req: &mut Request<SgBody>) -> Result<(), BoxError> {
185        if let Some(headers_match) = self.header.as_ref() {
186            for header_match in headers_match {
187                if let (Some(replace), Some(v)) = (header_match.rewrite(req), req.headers_mut().get_mut(&header_match.header_name)) {
188                    *v = replace;
189                }
190            }
191        }
192        let path_match = self.path.as_ref();
193        if let (Some(pq), Some(path_match)) = (req.uri().path_and_query(), path_match) {
194            let old_path = pq.path();
195            if let Some(new_path) = path_match.rewrite(old_path) {
196                let mut uri_part = req.uri().clone().into_parts();
197                tracing::debug!("[Sg.Rewrite] rewrite path from {} to {}", old_path, new_path);
198                let mut new_pq = new_path;
199                if let Some(query) = pq.query() {
200                    new_pq.push('?');
201                    new_pq.push_str(query)
202                }
203                let new_pq = hyper::http::uri::PathAndQuery::from_maybe_shared(new_pq)?;
204                uri_part.path_and_query = Some(new_pq);
205                *req.uri_mut() = Uri::from_parts(uri_part)?;
206            }
207        }
208        Ok(())
209    }
210}
211
212pub trait MatchRequest {
213    fn match_request(&self, req: &Request<SgBody>) -> bool;
214}
215
216impl MatchRequest for HttpQueryMatch {
217    fn match_request(&self, req: &Request<SgBody>) -> bool {
218        let query = req.uri().query();
219        if let Some(query) = query {
220            let mut iter = QueryKvIter::new(query);
221            match &self.policy {
222                SgHttpQueryMatchPolicy::Exact(query) => iter.any(|(k, v)| k == self.name && v == Some(query)),
223                SgHttpQueryMatchPolicy::Regular(query) => iter.any(|(k, v)| k == self.name && v.map_or(false, |v| query.is_match(v))),
224            }
225        } else {
226            false
227        }
228    }
229}
230
231impl From<HttpPathMatchRewrite> for HttpRouteMatch {
232    fn from(val: HttpPathMatchRewrite) -> Self {
233        HttpRouteMatch {
234            path: Some(val),
235            header: None,
236            query: None,
237            method: None,
238        }
239    }
240}
241
242impl From<SgHttpHeaderMatchRewrite> for HttpRouteMatch {
243    fn from(value: SgHttpHeaderMatchRewrite) -> Self {
244        HttpRouteMatch {
245            path: None,
246            header: Some(vec![value]),
247            query: None,
248            method: None,
249        }
250    }
251}
252
253impl From<HttpQueryMatch> for HttpRouteMatch {
254    fn from(value: HttpQueryMatch) -> Self {
255        HttpRouteMatch {
256            path: None,
257            header: None,
258            query: Some(vec![value]),
259            method: None,
260        }
261    }
262}
263
264impl From<HttpMethodMatch> for HttpRouteMatch {
265    fn from(value: HttpMethodMatch) -> Self {
266        HttpRouteMatch {
267            path: None,
268            header: None,
269            query: None,
270            method: Some(vec![value]),
271        }
272    }
273}
274
275impl MatchRequest for HttpPathMatchRewrite {
276    fn match_request(&self, req: &Request<SgBody>) -> bool {
277        match self {
278            HttpPathMatchRewrite::Exact(path, _) => req.uri().path() == path,
279            HttpPathMatchRewrite::Prefix(path, _) => {
280                let mut path_segments = req.uri().path().split('/').filter(|s| !s.is_empty());
281                let mut prefix_segments = path.split('/').filter(|s| !s.is_empty());
282                loop {
283                    match (path_segments.next(), prefix_segments.next()) {
284                        (Some(path_seg), Some(prefix_seg)) => {
285                            if !path_seg.eq_ignore_ascii_case(prefix_seg) {
286                                return false;
287                            }
288                        }
289                        (_, None) => return true,
290                        (None, Some(_)) => return false,
291                    }
292                }
293            }
294            HttpPathMatchRewrite::RegExp(path, _) => path.is_match(req.uri().path()),
295        }
296    }
297}
298
299impl MatchRequest for SgHttpHeaderMatchRewrite {
300    fn match_request(&self, req: &Request<SgBody>) -> bool {
301        match &self.policy {
302            SgHttpHeaderMatchRewritePolicy::Exact(header, _) => req.headers().get(&self.header_name).is_some_and(|v| v == header),
303            SgHttpHeaderMatchRewritePolicy::Regular(header, _) => {
304                req.headers().iter().any(|(k, v)| k.as_str() == self.header_name && v.to_str().map_or(false, |v| header.is_match(v)))
305            }
306        }
307    }
308}
309
310impl MatchRequest for HttpMethodMatch {
311    fn match_request(&self, req: &Request<SgBody>) -> bool {
312        req.method().as_str().eq_ignore_ascii_case(&self.0)
313    }
314}
315
316impl MatchRequest for HttpRouteMatch {
317    fn match_request(&self, req: &Request<SgBody>) -> bool {
318        self.path.match_request(req) && self.header.match_request(req) && self.query.match_request(req) && self.method.match_request(req)
319    }
320}
321
322impl<T> MatchRequest for Option<T>
323where
324    T: MatchRequest,
325{
326    fn match_request(&self, req: &Request<SgBody>) -> bool {
327        self.as_ref().map(|r| MatchRequest::match_request(r, req)).unwrap_or(true)
328    }
329}
330
331impl<T> MatchRequest for Vec<T>
332where
333    T: MatchRequest,
334{
335    fn match_request(&self, req: &Request<SgBody>) -> bool {
336        self.iter().any(|query| query.match_request(req))
337    }
338}
339
340#[test]
341fn test_match_path() {
342    let req = Request::builder().uri("https://localhost:8080/child/subApp").body(SgBody::empty()).expect("invalid request");
343    assert!(HttpPathMatchRewrite::Prefix("/child/subApp".into(), None).match_request(&req));
344}