spacegate_kernel/service/http_route/
match_request.rs1use hyper::{
2 http::{HeaderName, HeaderValue},
3 Uri,
4};
5use regex::Regex;
6
7use crate::{utils::query_kv::QueryKvIter, BoxError, Request, SgBody};
8
9#[derive(Debug, Clone)]
11pub enum HttpPathMatchRewrite {
12 Exact(String, Option<String>),
14 Prefix(String, Option<String>),
17 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 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 Exact(HeaderValue, Option<HeaderValue>),
97 Regular(Regex, Option<String>),
99}
100
101#[derive(Debug, Clone)]
102pub struct SgHttpHeaderMatchRewrite {
103 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 Exact(String),
148 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#[derive(Default, Debug, Clone)]
165pub struct HttpRouteMatch {
166 pub path: Option<HttpPathMatchRewrite>,
169 pub header: Option<Vec<SgHttpHeaderMatchRewrite>>,
172 pub query: Option<Vec<HttpQueryMatch>>,
175 pub method: Option<Vec<HttpMethodMatch>>,
178}
179
180impl HttpRouteMatch {
181 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}