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    /// No backend is registered under this id.
9    #[error("Backend not found: {0}")]
10    BackendNotFound(String),
11
12    /// A route named an upstream that is not in the config.
13    #[error("Upstream not found: {0}")]
14    UpstreamNotFound(String),
15
16    /// The connection to the upstream could not be made or was lost.
17    #[error("Connection error: {0}")]
18    Connection(String),
19
20    /// The request could not be rebuilt for the upstream.
21    #[error("Request error: {0}")]
22    Request(String),
23
24    /// The upstream's response could not be read or relayed.
25    #[error("Response error: {0}")]
26    Response(String),
27
28    /// The upstream did not answer in time.
29    #[error("Timeout")]
30    Timeout,
31
32    /// From hyper, on either leg.
33    #[error("Hyper error: {0}")]
34    Hyper(#[from] hyper::Error),
35
36    /// A request or response that cannot be represented -- an invalid header
37    /// name, an unparseable URI.
38    #[error("HTTP error: {0}")]
39    Http(#[from] hyper::http::Error),
40
41    /// From the socket.
42    #[error("IO error: {0}")]
43    Io(#[from] std::io::Error),
44}
45
46/// Server name matching (adapted from rpxy name_exp.rs).
47#[derive(Clone, Debug, PartialEq, Eq, Hash)]
48pub struct ServerName(
49    /// The name, already lowercased by [`ServerName::new`].
50    pub String,
51);
52
53impl ServerName {
54    /// Wrap a host name, lowercasing it.
55    ///
56    /// Host matching is case-insensitive per RFC 9110 section 4.2.3, and
57    /// normalising here means every comparison afterwards can be an equality
58    /// check.
59    pub fn new(name: impl Into<String>) -> Self {
60        Self(name.into().to_lowercase())
61    }
62
63    /// Check if the server name matches the given host.
64    pub fn matches(&self, host: &str) -> bool {
65        let host = host.to_lowercase();
66
67        // Exact match
68        if self.0 == host {
69            return true;
70        }
71
72        // Wildcard match: `*.example.com` matches exactly one label
73        // (e.g. `sub.example.com`) but not `example.com` or the multi-level
74        // `sub.sub.example.com`, per RFC 6125 wildcard semantics.
75        if self.0.starts_with("*.") {
76            let suffix = &self.0[2..];
77            if host.ends_with(suffix) {
78                let prefix_len = host.len() - suffix.len();
79                // The prefix must be a single non-empty label followed by a dot,
80                // with no interior dots of its own.
81                if prefix_len > 0
82                    && host.chars().nth(prefix_len - 1) == Some('.')
83                    && !host[..prefix_len - 1].contains('.')
84                {
85                    return true;
86                }
87            }
88        }
89
90        false
91    }
92}
93
94impl From<&str> for ServerName {
95    fn from(s: &str) -> Self {
96        Self::new(s)
97    }
98}
99
100impl From<String> for ServerName {
101    fn from(s: String) -> Self {
102        Self::new(s)
103    }
104}
105
106/// Path name matching with longest-prefix support (adapted from rpxy name_exp.rs).
107#[derive(Clone, Debug, PartialEq, Eq, Hash)]
108pub struct PathName(
109    /// The path, guaranteed by [`PathName::new`] to start with `/`.
110    pub String,
111);
112
113impl PathName {
114    /// Wrap a path, adding a leading `/` if it is missing.
115    ///
116    /// Without that, a pattern written as `v1` would never match a request
117    /// path, which always starts with `/`.
118    pub fn new(path: impl Into<String>) -> Self {
119        let mut path = path.into();
120        // Ensure path starts with /
121        if !path.starts_with('/') {
122            path = format!("/{}", path);
123        }
124        Self(path)
125    }
126
127    /// Check if this path matches the given request path, on a segment
128    /// boundary -- see [`crate::config::path_has_prefix`].
129    pub fn matches(&self, request_path: &str) -> bool {
130        crate::config::path_has_prefix(request_path, &self.0)
131    }
132
133    /// Get the length of this path (for longest-prefix matching).
134    pub fn len(&self) -> usize {
135        self.0.len()
136    }
137
138    /// Whether the underlying path string is empty.
139    pub fn is_empty(&self) -> bool {
140        self.0.is_empty()
141    }
142
143    /// Check if the path is empty (just "/").
144    pub fn is_root(&self) -> bool {
145        self.0 == "/"
146    }
147}
148
149impl From<&str> for PathName {
150    fn from(s: &str) -> Self {
151        Self::new(s)
152    }
153}
154
155impl From<String> for PathName {
156    fn from(s: String) -> Self {
157        Self::new(s)
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164
165    #[test]
166    fn test_server_name_exact_match() {
167        let name = ServerName::new("example.com");
168        assert!(name.matches("example.com"));
169        assert!(name.matches("EXAMPLE.COM"));
170        assert!(!name.matches("sub.example.com"));
171        assert!(!name.matches("example.org"));
172    }
173
174    #[test]
175    fn test_server_name_wildcard() {
176        let name = ServerName::new("*.example.com");
177        assert!(name.matches("sub.example.com"));
178        assert!(name.matches("api.example.com"));
179        assert!(!name.matches("example.com"));
180        assert!(!name.matches("sub.sub.example.com")); // Multi-level shouldn't match single wildcard
181    }
182
183    #[test]
184    fn test_path_name_matching() {
185        let path = PathName::new("/api");
186        assert!(path.matches("/api"));
187        assert!(path.matches("/api/users"));
188        assert!(path.matches("/api/users/123"));
189        assert!(!path.matches("/other"));
190        assert!(!path.matches("/"));
191        assert!(!path.matches("/apis"));
192    }
193
194    #[test]
195    fn test_path_name_root() {
196        let path = PathName::new("/");
197        assert!(path.matches("/"));
198        assert!(path.matches("/anything"));
199        assert!(path.is_root());
200    }
201}