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.
128    pub fn matches(&self, request_path: &str) -> bool {
129        request_path.starts_with(&self.0)
130    }
131
132    /// Get the length of this path (for longest-prefix matching).
133    pub fn len(&self) -> usize {
134        self.0.len()
135    }
136
137    /// Whether the underlying path string is empty.
138    pub fn is_empty(&self) -> bool {
139        self.0.is_empty()
140    }
141
142    /// Check if the path is empty (just "/").
143    pub fn is_root(&self) -> bool {
144        self.0 == "/"
145    }
146}
147
148impl From<&str> for PathName {
149    fn from(s: &str) -> Self {
150        Self::new(s)
151    }
152}
153
154impl From<String> for PathName {
155    fn from(s: String) -> Self {
156        Self::new(s)
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163
164    #[test]
165    fn test_server_name_exact_match() {
166        let name = ServerName::new("example.com");
167        assert!(name.matches("example.com"));
168        assert!(name.matches("EXAMPLE.COM"));
169        assert!(!name.matches("sub.example.com"));
170        assert!(!name.matches("example.org"));
171    }
172
173    #[test]
174    fn test_server_name_wildcard() {
175        let name = ServerName::new("*.example.com");
176        assert!(name.matches("sub.example.com"));
177        assert!(name.matches("api.example.com"));
178        assert!(!name.matches("example.com"));
179        assert!(!name.matches("sub.sub.example.com")); // Multi-level shouldn't match single wildcard
180    }
181
182    #[test]
183    fn test_path_name_matching() {
184        let path = PathName::new("/api");
185        assert!(path.matches("/api"));
186        assert!(path.matches("/api/users"));
187        assert!(path.matches("/api/users/123"));
188        assert!(!path.matches("/other"));
189        assert!(!path.matches("/"));
190    }
191
192    #[test]
193    fn test_path_name_root() {
194        let path = PathName::new("/");
195        assert!(path.matches("/"));
196        assert!(path.matches("/anything"));
197        assert!(path.is_root());
198    }
199}