ohttp_relay/
gateway_uri.rs

1use std::str::FromStr;
2
3use http::uri::{Authority, Scheme};
4use http::Uri;
5
6use crate::error::BoxError;
7
8pub(crate) const RFC_9540_GATEWAY_PATH: &str = "/.well-known/ohttp-gateway";
9const ALLOWED_PURPOSES_PATH_AND_QUERY: &str = "/.well-known/ohttp-gateway?allowed_purposes";
10
11/// A normalized gateway origin URI with a default port if none is specified.
12#[derive(Debug, Clone, PartialEq, Eq, Hash)]
13pub struct GatewayUri {
14    scheme: Scheme,
15    authority: Authority,
16}
17
18impl GatewayUri {
19    pub fn new(scheme: Scheme, authority: Authority) -> Result<Self, BoxError> {
20        let default_port = if scheme == Scheme::HTTP {
21            80
22        } else if scheme == Scheme::HTTPS {
23            443
24        } else {
25            return Err("Unsupported URI scheme".into());
26        };
27
28        // If no explicit port is provided, make the default one explicit
29        let mut authority = authority;
30        if authority.port().is_none() {
31            authority = Authority::from_str(&format!("{}:{}", authority.host(), default_port))
32                .expect("setting default port must succeed");
33        }
34
35        Ok(Self { scheme, authority })
36    }
37
38    pub fn from_static(string: &'static str) -> Self {
39        Uri::from_static(string)
40            .try_into()
41            .expect("gateway URI must consist of a scheme and authority only")
42    }
43
44    fn to_uri_builder(&self) -> http::uri::Builder {
45        Uri::builder().scheme(self.scheme.clone()).authority(self.authority.clone())
46    }
47
48    pub fn to_uri(&self) -> Uri {
49        self.to_uri_builder()
50            .path_and_query("/")
51            .build()
52            .expect("Building Uri from scheme and authority must succeed")
53    }
54
55    pub fn rfc_9540_url(&self) -> Uri {
56        self.to_uri_builder()
57            .path_and_query(RFC_9540_GATEWAY_PATH)
58            .build()
59            .expect("building RFC 9540 uri from scheme and authority must succeed")
60    }
61
62    pub fn probe_url(&self) -> Uri {
63        self.to_uri_builder()
64            .path_and_query(ALLOWED_PURPOSES_PATH_AND_QUERY)
65            .build()
66            .expect("building RFC 9540 uri from scheme and authority must succeed")
67    }
68
69    pub async fn to_socket_addr(&self) -> std::io::Result<Option<std::net::SocketAddr>> {
70        Ok(self.to_socket_addrs().await?.next())
71    }
72
73    pub async fn to_socket_addrs(
74        &self,
75    ) -> std::io::Result<impl Iterator<Item = std::net::SocketAddr>> {
76        tokio::net::lookup_host(self.authority.to_string()).await
77    }
78}
79
80impl From<GatewayUri> for Uri {
81    fn from(val: GatewayUri) -> Uri { val.to_uri() }
82}
83
84impl TryFrom<Uri> for GatewayUri {
85    type Error = BoxError;
86
87    fn try_from(uri: Uri) -> Result<Self, Self::Error> {
88        let parts = uri.into_parts();
89
90        if let Some(pq) = parts.path_and_query {
91            if pq.as_str() != "/" {
92                return Err("URI must not contain path or query".into());
93            }
94        }
95
96        let scheme = parts.scheme.ok_or::<BoxError>("URI must have a scheme".into())?;
97        let authority = parts.authority.ok_or::<BoxError>("URI must have an authority".into())?;
98
99        Self::new(scheme, authority)
100    }
101}
102
103impl From<Authority> for GatewayUri {
104    fn from(authority: Authority) -> Self {
105        Self::new(Scheme::HTTPS, authority)
106            .expect("constructing GatewayUri with valid authority must succeed")
107    }
108}
109
110impl FromStr for GatewayUri {
111    type Err = BoxError;
112    fn from_str(string: &str) -> Result<Self, Self::Err> { Uri::from_str(string)?.try_into() }
113}
114
115#[cfg(test)]
116mod test {
117    use super::*;
118
119    #[test]
120    fn conversion() {
121        let uri_with_port = Uri::from_static("http://payjo.in:80");
122        let gateway_uri = GatewayUri::try_from(uri_with_port.clone())
123            .expect("should be a valid gateway base URI");
124        assert_eq!(gateway_uri.to_uri(), uri_with_port, "uri should be the same as input");
125
126        let uri_without_port = Uri::from_static("http://payjo.in");
127        let gateway_uri =
128            GatewayUri::try_from(uri_without_port).expect("should be a valid gateway base URI");
129
130        let uri: Uri = gateway_uri.clone().into();
131        assert_eq!(uri, uri_with_port, "uri should be canonicalized to contain port");
132
133        assert_eq!(
134            gateway_uri.rfc_9540_url(),
135            Uri::from_static("http://payjo.in:80/.well-known/ohttp-gateway"),
136            "uri should be canonicalized to contain port"
137        );
138    }
139
140    #[test]
141    fn default_port() {
142        let uri = GatewayUri::from_static("http://payjo.in");
143        assert_eq!(
144            uri.authority.port_u16(),
145            Some(80),
146            "default port should be made explicit for http scheme"
147        );
148
149        let uri = GatewayUri::from_static("https://payjo.in");
150        assert_eq!(
151            uri.authority.port_u16(),
152            Some(443),
153            "default port should be made explicit for https scheme"
154        );
155
156        let uri = GatewayUri::from_static("https://payjo.in:80");
157        assert_eq!(uri.authority.port_u16(), Some(80), "explicit port should override default");
158
159        let uri = GatewayUri::from_static("http://payjo.in:1234");
160        assert_eq!(uri.authority.port_u16(), Some(1234), "explicit port should override default");
161    }
162
163    #[test]
164    fn invalid_uris() {
165        assert!(GatewayUri::from_str("payjo.in").is_err(), "scheme is mandatory");
166
167        assert!(GatewayUri::from_str("/index.html").is_err(), "url must be absolute");
168
169        assert!(
170            GatewayUri::from_str("ftp://payjo.in").is_err(),
171            "only http and https scheme should be allowed"
172        );
173
174        assert!(GatewayUri::from_str("http://payjo.in/blah").is_err(), "url must not contain path");
175    }
176}