Skip to main content

postrust_proxy/vendored/
types.rs

1//! Vendored types from rpxy-lib: globals.rs, error.rs, name_exp.rs
2
3use thiserror::Error;
4
5/// Vendored error type (adapted from rpxy error.rs).
6#[derive(Error, Debug)]
7pub enum ProxyError {
8    #[error("Backend not found: {0}")]
9    BackendNotFound(String),
10
11    #[error("Upstream not found: {0}")]
12    UpstreamNotFound(String),
13
14    #[error("Connection error: {0}")]
15    Connection(String),
16
17    #[error("Request error: {0}")]
18    Request(String),
19
20    #[error("Response error: {0}")]
21    Response(String),
22
23    #[error("Timeout")]
24    Timeout,
25
26    #[error("Hyper error: {0}")]
27    Hyper(#[from] hyper::Error),
28
29    #[error("HTTP error: {0}")]
30    Http(#[from] hyper::http::Error),
31
32    #[error("IO error: {0}")]
33    Io(#[from] std::io::Error),
34}
35
36/// Server name matching (adapted from rpxy name_exp.rs).
37#[derive(Clone, Debug, PartialEq, Eq, Hash)]
38pub struct ServerName(pub String);
39
40impl ServerName {
41    pub fn new(name: impl Into<String>) -> Self {
42        Self(name.into().to_lowercase())
43    }
44
45    /// Check if the server name matches the given host.
46    pub fn matches(&self, host: &str) -> bool {
47        let host = host.to_lowercase();
48
49        // Exact match
50        if self.0 == host {
51            return true;
52        }
53
54        // Wildcard match: `*.example.com` matches exactly one label
55        // (e.g. `sub.example.com`) but not `example.com` or the multi-level
56        // `sub.sub.example.com`, per RFC 6125 wildcard semantics.
57        if self.0.starts_with("*.") {
58            let suffix = &self.0[2..];
59            if host.ends_with(suffix) {
60                let prefix_len = host.len() - suffix.len();
61                // The prefix must be a single non-empty label followed by a dot,
62                // with no interior dots of its own.
63                if prefix_len > 0
64                    && host.chars().nth(prefix_len - 1) == Some('.')
65                    && !host[..prefix_len - 1].contains('.')
66                {
67                    return true;
68                }
69            }
70        }
71
72        false
73    }
74}
75
76impl From<&str> for ServerName {
77    fn from(s: &str) -> Self {
78        Self::new(s)
79    }
80}
81
82impl From<String> for ServerName {
83    fn from(s: String) -> Self {
84        Self::new(s)
85    }
86}
87
88/// Path name matching with longest-prefix support (adapted from rpxy name_exp.rs).
89#[derive(Clone, Debug, PartialEq, Eq, Hash)]
90pub struct PathName(pub String);
91
92impl PathName {
93    pub fn new(path: impl Into<String>) -> Self {
94        let mut path = path.into();
95        // Ensure path starts with /
96        if !path.starts_with('/') {
97            path = format!("/{}", path);
98        }
99        Self(path)
100    }
101
102    /// Check if this path matches the given request path.
103    pub fn matches(&self, request_path: &str) -> bool {
104        request_path.starts_with(&self.0)
105    }
106
107    /// Get the length of this path (for longest-prefix matching).
108    pub fn len(&self) -> usize {
109        self.0.len()
110    }
111
112    /// Whether the underlying path string is empty.
113    pub fn is_empty(&self) -> bool {
114        self.0.is_empty()
115    }
116
117    /// Check if the path is empty (just "/").
118    pub fn is_root(&self) -> bool {
119        self.0 == "/"
120    }
121}
122
123impl From<&str> for PathName {
124    fn from(s: &str) -> Self {
125        Self::new(s)
126    }
127}
128
129impl From<String> for PathName {
130    fn from(s: String) -> Self {
131        Self::new(s)
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138
139    #[test]
140    fn test_server_name_exact_match() {
141        let name = ServerName::new("example.com");
142        assert!(name.matches("example.com"));
143        assert!(name.matches("EXAMPLE.COM"));
144        assert!(!name.matches("sub.example.com"));
145        assert!(!name.matches("example.org"));
146    }
147
148    #[test]
149    fn test_server_name_wildcard() {
150        let name = ServerName::new("*.example.com");
151        assert!(name.matches("sub.example.com"));
152        assert!(name.matches("api.example.com"));
153        assert!(!name.matches("example.com"));
154        assert!(!name.matches("sub.sub.example.com")); // Multi-level shouldn't match single wildcard
155    }
156
157    #[test]
158    fn test_path_name_matching() {
159        let path = PathName::new("/api");
160        assert!(path.matches("/api"));
161        assert!(path.matches("/api/users"));
162        assert!(path.matches("/api/users/123"));
163        assert!(!path.matches("/other"));
164        assert!(!path.matches("/"));
165    }
166
167    #[test]
168    fn test_path_name_root() {
169        let path = PathName::new("/");
170        assert!(path.matches("/"));
171        assert!(path.matches("/anything"));
172        assert!(path.is_root());
173    }
174}