pingap_location/
location.rs

1// Copyright 2024-2025 Tree xie.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use super::regex::RegexCapture;
16use ahash::AHashMap;
17use arc_swap::ArcSwap;
18use once_cell::sync::Lazy;
19use pingap_config::LocationConf;
20use pingap_core::{convert_headers, HttpHeader};
21use pingora::http::RequestHeader;
22use regex::Regex;
23use snafu::{ResultExt, Snafu};
24use std::collections::HashMap;
25use std::sync::atomic::{AtomicI32, AtomicU64, Ordering};
26use std::sync::Arc;
27use substring::Substring;
28use tracing::{debug, error};
29
30const LOG_CATEGORY: &str = "location";
31
32// Error enum for various location-related errors
33#[derive(Debug, Snafu)]
34pub enum Error {
35    #[snafu(display("Invalid error {message}"))]
36    Invalid { message: String },
37    #[snafu(display("Regex value: {value}, {source}"))]
38    Regex { value: String, source: regex::Error },
39    #[snafu(display("Too Many Requests, max:{max}"))]
40    TooManyRequest { max: i32 },
41    #[snafu(display("Request Entity Too Large, max:{max}"))]
42    BodyTooLarge { max: usize },
43}
44type Result<T, E = Error> = std::result::Result<T, E>;
45
46#[derive(Debug)]
47struct RegexPath {
48    value: RegexCapture,
49}
50
51#[derive(Debug)]
52struct PrefixPath {
53    value: String,
54}
55
56#[derive(Debug)]
57struct EqualPath {
58    value: String,
59}
60
61// PathSelector enum represents different ways to match request paths:
62// - RegexPath: Uses regex pattern matching
63// - PrefixPath: Matches if path starts with prefix
64// - EqualPath: Matches exact path
65// - Empty: Matches all paths
66#[derive(Debug)]
67enum PathSelector {
68    RegexPath(RegexPath),
69    PrefixPath(PrefixPath),
70    EqualPath(EqualPath),
71    Empty,
72}
73/// Creates a new path selector based on the input path string.
74///
75/// # Arguments
76/// * `path` - The path pattern string to parse
77///
78/// # Returns
79/// * `Result<PathSelector>` - The parsed path selector or error
80///
81/// # Path Format
82/// - Empty string: Matches all paths
83/// - Starting with "~": Regex pattern matching
84/// - Starting with "=": Exact path matching  
85/// - Otherwise: Prefix path matching
86fn new_path_selector(path: &str) -> Result<PathSelector> {
87    let path = path.trim();
88    if path.is_empty() {
89        return Ok(PathSelector::Empty);
90    }
91    let first = path.chars().next().unwrap_or_default();
92    let last = path.substring(1, path.len()).trim();
93    let se = match first {
94        '~' => {
95            let re = RegexCapture::new(last).context(RegexSnafu {
96                value: last.to_string(),
97            })?;
98            PathSelector::RegexPath(RegexPath { value: re })
99        },
100        '=' => PathSelector::EqualPath(EqualPath {
101            value: last.to_string(),
102        }),
103        _ => {
104            // trim
105            PathSelector::PrefixPath(PrefixPath {
106                value: path.to_string(),
107            })
108        },
109    };
110
111    Ok(se)
112}
113
114#[derive(Debug)]
115struct RegexHost {
116    value: RegexCapture,
117}
118
119#[derive(Debug)]
120struct EqualHost {
121    value: String,
122}
123
124// HostSelector enum represents ways to match request hosts:
125// - RegexHost: Uses regex pattern matching with capture groups
126// - EqualHost: Matches exact hostname
127#[derive(Debug)]
128enum HostSelector {
129    RegexHost(RegexHost),
130    EqualHost(EqualHost),
131}
132
133/// Creates a new host selector based on the input host string.
134///
135/// # Arguments
136/// * `host` - The host pattern string to parse
137///
138/// # Returns
139/// * `Result<HostSelector>` - The parsed host selector or error
140///
141/// # Host Format
142/// - Empty string: Matches empty host
143/// - Starting with "~": Regex pattern matching with capture groups
144/// - Otherwise: Exact hostname matching
145fn new_host_selector(host: &str) -> Result<HostSelector> {
146    let host = host.trim();
147    if host.is_empty() {
148        return Ok(HostSelector::EqualHost(EqualHost {
149            value: host.to_string(),
150        }));
151    }
152    let first = host.chars().next().unwrap_or_default();
153    let last = host.substring(1, host.len()).trim();
154    let se = match first {
155        '~' => {
156            let re = RegexCapture::new(last).context(RegexSnafu {
157                value: last.to_string(),
158            })?;
159            HostSelector::RegexHost(RegexHost { value: re })
160        },
161        _ => {
162            // trim
163            HostSelector::EqualHost(EqualHost {
164                value: host.to_string(),
165            })
166        },
167    };
168
169    Ok(se)
170}
171
172/// Location represents a routing configuration for handling HTTP requests.
173/// It defines rules for matching requests based on paths and hosts, and specifies
174/// how these requests should be processed and proxied.
175#[derive(Debug)]
176pub struct Location {
177    /// Unique identifier for this location configuration
178    pub name: String,
179
180    /// Hash key used for configuration versioning and change detection
181    pub key: String,
182
183    /// Target upstream server where requests will be proxied to
184    pub upstream: String,
185
186    /// Original path pattern string used for matching requests
187    path: String,
188
189    /// Compiled path matching rules (regex, prefix, or exact match)
190    path_selector: PathSelector,
191
192    /// List of host patterns to match against request Host header
193    /// Empty list means match all hosts
194    hosts: Vec<HostSelector>,
195
196    /// Optional URL rewriting rule consisting of:
197    /// - regex pattern to match against request path
198    /// - replacement string with optional capture group references
199    reg_rewrite: Option<(Regex, String)>,
200
201    /// Additional headers to append to proxied requests
202    /// These are added without removing existing headers
203    pub proxy_add_headers: Option<Vec<HttpHeader>>,
204
205    /// Headers to set on proxied requests
206    /// These override any existing headers with the same name
207    pub proxy_set_headers: Option<Vec<HttpHeader>>,
208
209    /// Ordered list of plugin names to execute during request/response processing
210    pub plugins: Option<Vec<String>>,
211
212    /// Total number of requests accepted by this location
213    /// Used for metrics and monitoring
214    accepted: AtomicU64,
215
216    /// Number of requests currently being processed
217    /// Used for concurrency control
218    processing: AtomicI32,
219
220    /// Maximum number of concurrent requests allowed
221    /// Zero means unlimited
222    max_processing: i32,
223
224    /// Whether to enable gRPC-Web protocol support
225    /// When true, handles gRPC-Web requests and converts them to regular gRPC
226    grpc_web: bool,
227
228    /// Maximum allowed size of client request body in bytes
229    /// Zero means unlimited. Requests exceeding this limit receive 413 error
230    client_max_body_size: usize,
231
232    /// Whether to automatically add standard reverse proxy headers like:
233    /// X-Forwarded-For, X-Real-IP, X-Forwarded-Proto, etc.
234    pub enable_reverse_proxy_headers: bool,
235}
236
237/// Formats a vector of header strings into internal HttpHeader representation.
238///
239/// # Arguments
240/// * `values` - Optional vector of header strings in "Name: Value" format
241///
242/// # Returns
243/// * `Result<Option<Vec<HttpHeader>>>` - Parsed headers or None if input was None
244fn format_headers(
245    values: &Option<Vec<String>>,
246) -> Result<Option<Vec<HttpHeader>>> {
247    if let Some(header_values) = values {
248        let arr =
249            convert_headers(header_values).map_err(|err| Error::Invalid {
250                message: err.to_string(),
251            })?;
252        Ok(Some(arr))
253    } else {
254        Ok(None)
255    }
256}
257
258/// Get the content length from http request header.
259fn get_content_length(header: &RequestHeader) -> Option<usize> {
260    if let Some(content_length) =
261        header.headers.get(http::header::CONTENT_LENGTH)
262    {
263        if let Ok(size) =
264            content_length.to_str().unwrap_or_default().parse::<usize>()
265        {
266            return Some(size);
267        }
268    }
269    None
270}
271
272impl Location {
273    /// Creates a new Location from configuration
274    /// Validates and compiles path/host patterns and other settings
275    pub fn new(name: &str, conf: &LocationConf) -> Result<Location> {
276        if name.is_empty() {
277            return Err(Error::Invalid {
278                message: "Name is required".to_string(),
279            });
280        }
281        let key = conf.hash_key();
282        let upstream = conf.upstream.clone().unwrap_or_default();
283        let mut reg_rewrite = None;
284        // rewrite: "^/users/(.*)$ /api/users/$1"
285        if let Some(value) = &conf.rewrite {
286            let mut arr: Vec<&str> = value.split(' ').collect();
287            if arr.len() == 1 && arr[0].contains("$") {
288                arr.push(arr[0]);
289                arr[0] = ".*";
290            }
291
292            let value = if arr.len() == 2 { arr[1] } else { "" };
293            if let Ok(re) = Regex::new(arr[0]) {
294                reg_rewrite = Some((re, value.to_string()));
295            }
296        }
297        let mut hosts = vec![];
298        for item in conf.host.clone().unwrap_or_default().split(',') {
299            let host = item.trim().to_string();
300            if host.is_empty() {
301                continue;
302            }
303            hosts.push(new_host_selector(&host)?);
304        }
305
306        let path = conf.path.clone().unwrap_or_default();
307
308        let location = Location {
309            name: name.to_string(),
310            key,
311            path_selector: new_path_selector(&path)?,
312            path,
313            hosts,
314            upstream,
315            reg_rewrite,
316            plugins: conf.plugins.clone(),
317            accepted: AtomicU64::new(0),
318            processing: AtomicI32::new(0),
319            max_processing: conf.max_processing.unwrap_or_default(),
320            grpc_web: conf.grpc_web.unwrap_or_default(),
321            proxy_add_headers: format_headers(&conf.proxy_add_headers)?,
322            proxy_set_headers: format_headers(&conf.proxy_set_headers)?,
323            client_max_body_size: conf
324                .client_max_body_size
325                .unwrap_or_default()
326                .as_u64() as usize,
327            enable_reverse_proxy_headers: conf
328                .enable_reverse_proxy_headers
329                .unwrap_or_default(),
330        };
331        debug!(
332            category = LOG_CATEGORY,
333            location = format!("{location:?}"),
334            "create a new location"
335        );
336
337        Ok(location)
338    }
339
340    /// Returns whether gRPC-Web protocol support is enabled for this location
341    /// When enabled, the proxy will handle gRPC-Web requests and convert them to regular gRPC
342    #[inline]
343    pub fn support_grpc_web(&self) -> bool {
344        self.grpc_web
345    }
346
347    /// Validates that the request's Content-Length header does not exceed the configured maximum
348    ///
349    /// # Arguments
350    /// * `header` - The HTTP request header to validate
351    ///
352    /// # Returns
353    /// * `Result<()>` - Ok if validation passes, Error::BodyTooLarge if content length exceeds limit
354    ///
355    /// # Notes
356    /// - Returns Ok if client_max_body_size is 0 (unlimited)
357    /// - Uses get_content_length() helper to parse the Content-Length header
358    #[inline]
359    pub fn validate_content_length(
360        &self,
361        header: &RequestHeader,
362    ) -> Result<()> {
363        if self.client_max_body_size == 0 {
364            return Ok(());
365        }
366        if get_content_length(header).unwrap_or_default()
367            > self.client_max_body_size
368        {
369            return Err(Error::BodyTooLarge {
370                max: self.client_max_body_size,
371            });
372        }
373
374        Ok(())
375    }
376
377    /// Sets the maximum allowed size of the client request body.
378    /// If the size in a request exceeds the configured value, the 413 (Request Entity Too Large) error
379    /// is returned to the client.
380    #[inline]
381    pub fn client_body_size_limit(&self, payload_size: usize) -> Result<()> {
382        if self.client_max_body_size == 0 {
383            return Ok(());
384        }
385        if payload_size > self.client_max_body_size {
386            return Err(Error::BodyTooLarge {
387                max: self.client_max_body_size,
388            });
389        }
390        Ok(())
391    }
392
393    /// Increments the processing and accepted request counters for this location.
394    ///
395    /// This method is called when a new request starts being processed by this location.
396    /// It performs two atomic operations:
397    /// 1. Increments the total accepted requests counter
398    /// 2. Increments the currently processing requests counter
399    ///
400    /// # Returns
401    /// * `Result<(u64, i32)>` - A tuple containing:
402    ///   - The new total number of accepted requests (u64)
403    ///   - The new number of currently processing requests (i32)
404    ///
405    /// # Errors
406    /// Returns `Error::TooManyRequest` if the number of currently processing requests
407    /// would exceed the configured `max_processing` limit (when non-zero).
408    #[inline]
409    pub fn add_processing(&self) -> Result<(u64, i32)> {
410        let accepted = self.accepted.fetch_add(1, Ordering::Relaxed) + 1;
411        let processing = self.processing.fetch_add(1, Ordering::Relaxed) + 1;
412        if self.max_processing != 0 && processing > self.max_processing {
413            return Err(Error::TooManyRequest {
414                max: self.max_processing,
415            });
416        }
417        Ok((accepted, processing))
418    }
419
420    /// Decrements the processing request counter for this location.
421    ///
422    /// This method is called when a request finishes being processed.
423    /// It performs an atomic decrement of the currently processing requests counter.
424    #[inline]
425    pub fn sub_processing(&self) {
426        self.processing.fetch_sub(1, Ordering::Relaxed);
427    }
428
429    /// Checks if a request matches this location's path and host rules
430    /// Returns a tuple containing:
431    /// - bool: Whether the request matched both path and host rules
432    /// - Option<Vec<(String, String)>>: Any captured variables from regex host matching
433    #[inline]
434    pub fn match_host_path(
435        &self,
436        host: &str,
437        path: &str,
438    ) -> (bool, Option<Vec<(String, String)>>) {
439        // Check host matching against configured host patterns
440        let mut variables: Vec<(String, String)> = vec![];
441
442        // First check path matching if a path pattern is configured
443        if !self.path.is_empty() {
444            let matched = match &self.path_selector {
445                // For exact path matching, compare path strings directly
446                PathSelector::EqualPath(EqualPath { value }) => value == path,
447                // For regex path matching, use regex is_match
448                PathSelector::RegexPath(RegexPath { value }) => {
449                    let (matched, value) = value.captures(path);
450                    if let (true, Some(vars)) = (matched, value) {
451                        variables.extend(vars);
452                    }
453                    matched
454                },
455                // For prefix path matching, check if path starts with prefix
456                PathSelector::PrefixPath(PrefixPath { value }) => {
457                    path.starts_with(value)
458                },
459                // Empty path selector matches everything
460                PathSelector::Empty => true,
461            };
462            // If path doesn't match, return false early
463            if !matched {
464                return (false, None);
465            }
466        }
467
468        // If no host patterns configured, path match is sufficient
469        if self.hosts.is_empty() {
470            return (true, None);
471        }
472
473        let matched = self.hosts.iter().any(|item| match item {
474            // For regex host matching:
475            // - Attempt to capture variables from host string
476            // - Store captures in variables if match successful
477            HostSelector::RegexHost(RegexHost { value }) => {
478                let (matched, value) = value.captures(host);
479                if let (true, Some(vars)) = (matched, value) {
480                    variables.extend(vars);
481                }
482                matched
483            },
484            // For exact host matching:
485            // - Empty host pattern matches everything
486            // - Otherwise compare host strings directly
487            HostSelector::EqualHost(EqualHost { value }) => {
488                if value.is_empty() {
489                    return true;
490                }
491                value == host
492            },
493        });
494        if variables.is_empty() {
495            return (matched, None);
496        }
497
498        // Return whether both path and host matched, along with any captured variables
499        (matched, Some(variables))
500    }
501
502    /// Applies URL rewriting rules if configured for this location.
503    ///
504    /// This method performs path rewriting based on regex patterns and replacement rules.
505    /// It supports variable interpolation from captured values in the host matching.
506    ///
507    /// # Arguments
508    /// * `header` - Mutable reference to the request header containing the URI to rewrite
509    /// * `variables` - Optional map of variables captured from host matching that can be interpolated
510    ///   into the replacement value
511    ///
512    /// # Returns
513    /// * `bool` - Returns true if the path was rewritten, false if no rewriting was performed
514    ///
515    /// # Examples
516    /// ```
517    /// // Configuration example:
518    /// // rewrite: "^/users/(.*)$ /api/users/$1"
519    /// // This would rewrite "/users/123" to "/api/users/123"
520    /// ```
521    ///
522    /// # Notes
523    /// - Preserves query parameters when rewriting the path
524    /// - Logs debug information about path rewrites
525    /// - Logs errors if the new path cannot be parsed as a valid URI
526    #[inline]
527    pub fn rewrite(
528        &self,
529        header: &mut RequestHeader,
530        variables: Option<&AHashMap<String, String>>,
531    ) -> bool {
532        if let Some((re, value)) = &self.reg_rewrite {
533            let mut replace_value = value.to_string();
534            // replace variables for rewrite value
535            if let Some(variables) = variables {
536                for (k, v) in variables.iter() {
537                    replace_value = replace_value.replace(k, v);
538                }
539            }
540            let path = header.uri.path();
541            let mut new_path = if re.to_string() == ".*" {
542                replace_value
543            } else {
544                re.replace(path, replace_value).to_string()
545            };
546            if path == new_path {
547                return false;
548            }
549            // preserve query parameters
550            if let Some(query) = header.uri.query() {
551                new_path = format!("{new_path}?{query}");
552            }
553            debug!(category = LOG_CATEGORY, new_path, "rewrite path");
554            // set new uri
555            if let Err(e) =
556                new_path.parse::<http::Uri>().map(|uri| header.set_uri(uri))
557            {
558                error!(category = LOG_CATEGORY, error = %e, location = self.name, "new path parse fail");
559            }
560            return true;
561        }
562        false
563    }
564}
565
566type Locations = AHashMap<String, Arc<Location>>;
567static LOCATION_MAP: Lazy<ArcSwap<Locations>> =
568    Lazy::new(|| ArcSwap::from_pointee(AHashMap::new()));
569
570/// Gets a location configuration by name from the global location map.
571///
572/// # Arguments
573/// * `name` - Name of the location to retrieve
574///
575/// # Returns
576/// * `Option<Arc<Location>>` - The location if found, None otherwise
577pub fn get_location(name: &str) -> Option<Arc<Location>> {
578    if name.is_empty() {
579        return None;
580    }
581    LOCATION_MAP.load().get(name).cloned()
582}
583
584/// Gets a map of current request processing and accepted counts for all locations.
585///
586/// # Returns
587/// * `HashMap<String, (i32, u64)>` - Map of location names to their current processing and accepted counts
588pub fn get_locations_stats() -> HashMap<String, (i32, u64)> {
589    let mut stats = HashMap::new();
590    LOCATION_MAP.load().iter().for_each(|(k, v)| {
591        stats.insert(
592            k.to_string(),
593            (
594                v.processing.load(Ordering::Relaxed),
595                v.accepted.load(Ordering::Relaxed),
596            ),
597        );
598    });
599    stats
600}
601
602/// Initializes or updates the global location configurations
603/// Returns list of location names that were updated
604pub fn try_init_locations(
605    location_configs: &HashMap<String, LocationConf>,
606) -> Result<Vec<String>> {
607    let mut locations = AHashMap::new();
608    let mut updated_locations = vec![];
609    for (name, conf) in location_configs.iter() {
610        if let Some(found) = get_location(name) {
611            if found.key == conf.hash_key() {
612                locations.insert(name.to_string(), found);
613                continue;
614            }
615        }
616        updated_locations.push(name.clone());
617        let lo = Location::new(name, conf)?;
618        locations.insert(name.to_string(), Arc::new(lo));
619    }
620    LOCATION_MAP.store(Arc::new(locations));
621    Ok(updated_locations)
622}
623
624#[cfg(test)]
625mod tests {
626    use super::*;
627    use bytesize::ByteSize;
628    use pingap_config::LocationConf;
629    use pingora::http::RequestHeader;
630    use pingora::proxy::Session;
631    use pretty_assertions::assert_eq;
632    use tokio_test::io::Builder;
633
634    #[test]
635    fn test_format_headers() {
636        let headers = format_headers(&Some(vec![
637            "Content-Type: application/json".to_string(),
638        ]))
639        .unwrap();
640        assert_eq!(
641            r###"Some([("content-type", "application/json")])"###,
642            format!("{headers:?}")
643        );
644    }
645    #[test]
646    fn test_new_path_selector() {
647        let selector = new_path_selector("").unwrap();
648        assert_eq!(true, matches!(selector, PathSelector::Empty));
649
650        let selector = new_path_selector("~/api").unwrap();
651        assert_eq!(true, matches!(selector, PathSelector::RegexPath(_)));
652
653        let selector = new_path_selector("=/api").unwrap();
654        assert_eq!(true, matches!(selector, PathSelector::EqualPath(_)));
655
656        let selector = new_path_selector("/api").unwrap();
657        assert_eq!(true, matches!(selector, PathSelector::PrefixPath(_)));
658    }
659    #[test]
660    fn test_path_host_select_location() {
661        let upstream_name = "charts";
662
663        // no path, no host
664        let lo = Location::new(
665            "lo",
666            &LocationConf {
667                upstream: Some(upstream_name.to_string()),
668                ..Default::default()
669            },
670        )
671        .unwrap();
672        assert_eq!(true, lo.match_host_path("pingap", "/api").0);
673        assert_eq!(true, lo.match_host_path("", "").0);
674
675        // host
676        let lo = Location::new(
677            "lo",
678            &LocationConf {
679                upstream: Some(upstream_name.to_string()),
680                host: Some("test.com,pingap".to_string()),
681                ..Default::default()
682            },
683        )
684        .unwrap();
685        assert_eq!(true, lo.match_host_path("pingap", "/api").0);
686        assert_eq!(true, lo.match_host_path("pingap", "").0);
687        assert_eq!(false, lo.match_host_path("", "/api").0);
688
689        // regex
690        let lo = Location::new(
691            "lo",
692            &LocationConf {
693                upstream: Some(upstream_name.to_string()),
694                path: Some("~/users".to_string()),
695                ..Default::default()
696            },
697        )
698        .unwrap();
699        assert_eq!(true, lo.match_host_path("", "/api/users").0);
700        assert_eq!(true, lo.match_host_path("", "/users").0);
701        assert_eq!(false, lo.match_host_path("", "/api").0);
702
703        // regex ^/api
704        let lo = Location::new(
705            "lo",
706            &LocationConf {
707                upstream: Some(upstream_name.to_string()),
708                path: Some("~^/api".to_string()),
709                ..Default::default()
710            },
711        )
712        .unwrap();
713        assert_eq!(true, lo.match_host_path("", "/api/users").0);
714        assert_eq!(false, lo.match_host_path("", "/users").0);
715        assert_eq!(true, lo.match_host_path("", "/api").0);
716
717        // prefix
718        let lo = Location::new(
719            "lo",
720            &LocationConf {
721                upstream: Some(upstream_name.to_string()),
722                path: Some("/api".to_string()),
723                ..Default::default()
724            },
725        )
726        .unwrap();
727        assert_eq!(true, lo.match_host_path("", "/api/users").0);
728        assert_eq!(false, lo.match_host_path("", "/users").0);
729        assert_eq!(true, lo.match_host_path("", "/api").0);
730
731        // equal
732        let lo = Location::new(
733            "lo",
734            &LocationConf {
735                upstream: Some(upstream_name.to_string()),
736                path: Some("=/api".to_string()),
737                ..Default::default()
738            },
739        )
740        .unwrap();
741        assert_eq!(false, lo.match_host_path("", "/api/users").0);
742        assert_eq!(false, lo.match_host_path("", "/users").0);
743        assert_eq!(true, lo.match_host_path("", "/api").0);
744    }
745
746    #[test]
747    fn test_match_host_path_variables() {
748        let lo = Location::new(
749            "lo",
750            &LocationConf {
751                upstream: Some("charts".to_string()),
752                host: Some("~(?<name>.+).npmtrend.com".to_string()),
753                path: Some("~/(?<route>.+)/(.*)".to_string()),
754                ..Default::default()
755            },
756        )
757        .unwrap();
758        let (matched, variables) =
759            lo.match_host_path("charts.npmtrend.com", "/users/123");
760        assert_eq!(true, matched);
761        assert_eq!(
762            Some(vec![
763                ("route".to_string(), "users".to_string()),
764                ("name".to_string(), "charts".to_string()),
765            ]),
766            variables
767        );
768    }
769
770    #[test]
771    fn test_rewrite_path() {
772        let upstream_name = "charts";
773
774        let lo = Location::new(
775            "lo",
776            &LocationConf {
777                upstream: Some(upstream_name.to_string()),
778                rewrite: Some("^/users/(.*)$ /$1".to_string()),
779                ..Default::default()
780            },
781        )
782        .unwrap();
783        let mut req_header =
784            RequestHeader::build("GET", b"/users/me?abc=1", None).unwrap();
785        assert_eq!(true, lo.rewrite(&mut req_header, None));
786        assert_eq!("/me?abc=1", req_header.uri.to_string());
787
788        let mut req_header =
789            RequestHeader::build("GET", b"/api/me?abc=1", None).unwrap();
790        assert_eq!(false, lo.rewrite(&mut req_header, None));
791        assert_eq!("/api/me?abc=1", req_header.uri.to_string());
792    }
793
794    #[test]
795    fn test_client_body_size_limit() {
796        let upstream_name = "charts";
797
798        let lo = Location::new(
799            "lo",
800            &LocationConf {
801                upstream: Some(upstream_name.to_string()),
802                rewrite: Some("^/users/(.*)$ /$1".to_string()),
803                plugins: Some(vec!["test:mock".to_string()]),
804                client_max_body_size: Some(ByteSize(10)),
805                ..Default::default()
806            },
807        )
808        .unwrap();
809
810        let result = lo.client_body_size_limit(2);
811        assert_eq!(true, result.is_ok());
812
813        let result = lo.client_body_size_limit(20);
814        assert_eq!(
815            "Request Entity Too Large, max:10",
816            result.err().unwrap().to_string()
817        );
818    }
819
820    #[tokio::test]
821    async fn test_get_content_length() {
822        let headers = ["Content-Length: 123"].join("\r\n");
823        let input_header =
824            format!("GET /vicanso/pingap?size=1 HTTP/1.1\r\n{headers}\r\n\r\n");
825        let mock_io = Builder::new().read(input_header.as_bytes()).build();
826        let mut session = Session::new_h1(Box::new(mock_io));
827        session.read_request().await.unwrap();
828        assert_eq!(get_content_length(session.req_header()), Some(123));
829    }
830
831    #[test]
832    fn test_location_processing() {
833        let lo = Location::new(
834            "lo",
835            &LocationConf {
836                ..Default::default()
837            },
838        )
839        .unwrap();
840        let value = lo.add_processing().unwrap();
841        assert_eq!(1, value.0);
842        assert_eq!(1, value.1);
843
844        lo.sub_processing();
845        assert_eq!(1, lo.accepted.load(Ordering::Relaxed));
846        assert_eq!(0, lo.processing.load(Ordering::Relaxed));
847    }
848
849    #[test]
850    fn test_validate_content_length() {
851        let lo = Location::new(
852            "lo",
853            &LocationConf {
854                client_max_body_size: Some(ByteSize(10)),
855                ..Default::default()
856            },
857        )
858        .unwrap();
859        let mut req_header =
860            RequestHeader::build("GET", b"/users/me?abc=1", None).unwrap();
861        assert_eq!(true, lo.validate_content_length(&req_header).is_ok());
862
863        req_header
864            .append_header(
865                http::header::CONTENT_LENGTH,
866                http::HeaderValue::from_str("20").unwrap(),
867            )
868            .unwrap();
869        assert_eq!(
870            "Request Entity Too Large, max:10",
871            lo.validate_content_length(&req_header)
872                .err()
873                .unwrap()
874                .to_string()
875        );
876    }
877}