ohttp_relay/
gateway_uri.rs1use 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#[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 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}