1use std::{
18 pin::Pin,
19 task::{Context, Poll},
20 time::Duration,
21};
22
23use bytes::Bytes;
24use http_body::Frame;
25use sciparse::address::ip_addr::ScionIpAddr;
26use url::Url;
27
28use crate::error::BuildRequestError;
29
30pub trait IntoUrl: sealed::Sealed {
36 fn into_url(self) -> Result<Url, BuildRequestError>;
38}
39
40impl IntoUrl for Url {
41 fn into_url(self) -> Result<Url, BuildRequestError> {
42 Ok(self)
43 }
44}
45
46impl IntoUrl for &str {
47 fn into_url(self) -> Result<Url, BuildRequestError> {
48 Url::parse(self).map_err(|e| {
49 BuildRequestError::InvalidUrl {
50 source: Box::new(e),
51 }
52 })
53 }
54}
55
56impl IntoUrl for String {
57 fn into_url(self) -> Result<Url, BuildRequestError> {
58 self.as_str().into_url()
59 }
60}
61
62impl IntoUrl for &String {
63 fn into_url(self) -> Result<Url, BuildRequestError> {
64 self.as_str().into_url()
65 }
66}
67
68mod sealed {
69 pub trait Sealed {}
70 impl Sealed for url::Url {}
71 impl Sealed for &str {}
72 impl Sealed for String {}
73 impl Sealed for &String {}
74}
75
76#[derive(Debug, Clone)]
83pub struct Request {
84 method: http::Method,
85 url: Url,
86 headers: http::HeaderMap,
87 body: Bytes,
88 targets: Option<Vec<ScionIpAddr>>,
89 timeout: Option<Duration>,
90}
91
92impl Request {
93 #[must_use]
96 pub fn builder() -> RequestBuilder {
97 RequestBuilder::new()
98 }
99
100 #[must_use]
102 pub fn get(url: impl IntoUrl) -> RequestBuilder {
103 RequestBuilder::new().method(http::Method::GET).url(url)
104 }
105
106 #[must_use]
108 pub fn post(url: impl IntoUrl) -> RequestBuilder {
109 RequestBuilder::new().method(http::Method::POST).url(url)
110 }
111
112 #[must_use]
114 pub fn method(&self) -> &http::Method {
115 &self.method
116 }
117
118 #[must_use]
120 pub fn url(&self) -> &Url {
121 &self.url
122 }
123
124 #[must_use]
126 pub fn headers(&self) -> &http::HeaderMap {
127 &self.headers
128 }
129
130 #[must_use]
132 pub fn body(&self) -> &Bytes {
133 &self.body
134 }
135
136 #[must_use]
139 pub fn targets(&self) -> Option<&[ScionIpAddr]> {
140 self.targets.as_deref()
141 }
142
143 #[must_use]
145 pub fn request_timeout(&self) -> Option<Duration> {
146 self.timeout
147 }
148
149 pub(crate) fn to_http(&self) -> Result<http::Request<BufferedBody>, crate::Error> {
152 let mut builder = http::Request::builder()
153 .method(self.method.clone())
154 .uri(self.url.as_str());
155 if let Some(headers) = builder.headers_mut() {
156 headers.extend(self.headers.clone());
157 }
158 builder
159 .body(BufferedBody::new(self.body.clone()))
160 .map_err(|e| {
161 crate::Error::InvalidRequest {
162 reason: e.to_string().into(),
163 }
164 })
165 }
166}
167
168#[derive(Debug)]
174pub struct RequestBuilder {
175 method: http::Method,
176 url: Option<Url>,
177 headers: http::HeaderMap,
178 body: Bytes,
179 targets: Option<Vec<ScionIpAddr>>,
180 timeout: Option<Duration>,
181 error: Option<BuildRequestError>,
182}
183
184impl RequestBuilder {
185 fn new() -> Self {
186 RequestBuilder {
187 method: http::Method::GET,
188 url: None,
189 headers: http::HeaderMap::new(),
190 body: Bytes::new(),
191 targets: None,
192 timeout: None,
193 error: None,
194 }
195 }
196
197 #[must_use]
199 pub fn method(mut self, method: http::Method) -> Self {
200 self.method = method;
201 self
202 }
203
204 #[must_use]
206 pub fn url(mut self, url: impl IntoUrl) -> Self {
207 match url.into_url() {
208 Ok(url) => self.url = Some(url),
209 Err(e) => {
210 self.error.get_or_insert(e);
211 }
212 }
213 self
214 }
215
216 #[must_use]
218 pub fn header<K, V>(mut self, key: K, value: V) -> Self
219 where
220 K: TryInto<http::header::HeaderName>,
221 K::Error: Into<http::Error>,
222 V: TryInto<http::header::HeaderValue>,
223 V::Error: Into<http::Error>,
224 {
225 match (key.try_into(), value.try_into()) {
226 (Ok(key), Ok(value)) => {
227 self.headers.append(key, value);
228 }
229 (Err(e), _) => {
230 self.error.get_or_insert(BuildRequestError::InvalidHeader {
231 source: Box::new(e.into()),
232 });
233 }
234 (_, Err(e)) => {
235 self.error.get_or_insert(BuildRequestError::InvalidHeader {
236 source: Box::new(e.into()),
237 });
238 }
239 }
240 self
241 }
242
243 #[must_use]
246 pub fn body(mut self, body: impl Into<Bytes>) -> Self {
247 self.body = body.into();
248 self
249 }
250
251 #[must_use]
258 pub fn target(self, addr: ScionIpAddr) -> Self {
259 self.targets(vec![addr])
260 }
261
262 #[must_use]
271 pub fn targets(mut self, mut addrs: Vec<ScionIpAddr>) -> Self {
272 if addrs.is_empty() {
273 self.error.get_or_insert(BuildRequestError::EmptyTargets);
274 return self;
275 }
276 addrs.sort_unstable();
277 addrs.dedup();
278 self.targets = Some(addrs);
279 self
280 }
281
282 #[must_use]
288 pub fn request_timeout(mut self, timeout: Duration) -> Self {
289 self.timeout = Some(timeout);
290 self
291 }
292
293 pub fn build(self) -> Result<Request, BuildRequestError> {
295 if let Some(error) = self.error {
296 return Err(error);
297 }
298 let url = self.url.ok_or(BuildRequestError::MissingUrl)?;
299 if url.scheme() != "https" {
300 return Err(BuildRequestError::UnsupportedScheme {
301 scheme: url.scheme().to_string(),
302 });
303 }
304 if url.host_str().is_none() {
305 return Err(BuildRequestError::MissingHost);
306 }
307 if !url.username().is_empty() || url.password().is_some() {
311 return Err(BuildRequestError::UserinfoNotAllowed);
312 }
313 Ok(Request {
314 method: self.method,
315 url,
316 headers: self.headers,
317 body: self.body,
318 targets: self.targets,
319 timeout: self.timeout,
320 })
321 }
322}
323
324pub(crate) struct BufferedBody {
326 data: Option<Bytes>,
327}
328
329impl BufferedBody {
330 fn new(data: Bytes) -> Self {
331 let data = if data.is_empty() { None } else { Some(data) };
332 BufferedBody { data }
333 }
334}
335
336impl http_body::Body for BufferedBody {
337 type Data = Bytes;
338 type Error = std::convert::Infallible;
339
340 fn poll_frame(
341 self: Pin<&mut Self>,
342 _cx: &mut Context<'_>,
343 ) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
344 Poll::Ready(self.get_mut().data.take().map(|data| Ok(Frame::data(data))))
345 }
346
347 fn is_end_stream(&self) -> bool {
348 self.data.is_none()
349 }
350
351 fn size_hint(&self) -> http_body::SizeHint {
352 let len = self.data.as_ref().map_or(0, Bytes::len) as u64;
353 http_body::SizeHint::with_exact(len)
354 }
355}
356
357#[cfg(test)]
358mod tests {
359 use std::net::Ipv4Addr;
360
361 use super::*;
362
363 fn addr(last_octet: u8) -> ScionIpAddr {
364 ScionIpAddr::new(
365 "1-ff00:0:110".parse().unwrap(),
366 Ipv4Addr::new(10, 0, 0, last_octet).into(),
367 )
368 }
369
370 #[test]
371 fn get_builds_with_defaults() {
372 let req = Request::get("https://chat.example.org/rooms")
373 .build()
374 .unwrap();
375 assert_eq!(req.method(), http::Method::GET);
376 assert_eq!(req.url().as_str(), "https://chat.example.org/rooms");
377 assert!(req.targets().is_none());
378 assert!(req.request_timeout().is_none());
379 assert!(req.body().is_empty());
380 }
381
382 #[test]
383 fn non_https_scheme_is_rejected() {
384 let err = Request::get("http://chat.example.org/")
385 .build()
386 .unwrap_err();
387 assert!(matches!(
388 err,
389 BuildRequestError::UnsupportedScheme { scheme } if scheme == "http"
390 ));
391 }
392
393 #[test]
394 fn userinfo_is_rejected() {
395 for url in [
396 "https://user:secret@api.example.org/x",
397 "https://user@api.example.org/x",
398 ] {
399 let err = Request::get(url).build().unwrap_err();
400 assert!(
401 matches!(err, BuildRequestError::UserinfoNotAllowed),
402 "{url}"
403 );
404 }
405 }
406
407 #[test]
408 fn unparsable_url_is_reported_at_build() {
409 let err = Request::get("not a url").build().unwrap_err();
410 assert!(matches!(err, BuildRequestError::InvalidUrl { .. }));
411 }
412
413 #[test]
414 fn missing_url_is_reported_at_build() {
415 let err = Request::builder().build().unwrap_err();
416 assert!(matches!(err, BuildRequestError::MissingUrl));
417 }
418
419 #[test]
420 fn invalid_header_is_deferred_to_build() {
421 let err = Request::get("https://example.org/")
422 .header("bad header name", "value")
423 .build()
424 .unwrap_err();
425 assert!(matches!(err, BuildRequestError::InvalidHeader { .. }));
426 }
427
428 #[test]
429 fn targets_are_sorted_and_deduplicated() {
430 let req = Request::get("https://example.org/")
431 .targets(vec![addr(2), addr(1), addr(2)])
432 .build()
433 .unwrap();
434 assert_eq!(req.targets(), Some(&[addr(1), addr(2)][..]));
435 }
436
437 #[test]
438 fn empty_targets_are_rejected() {
439 let err = Request::get("https://example.org/")
440 .targets(vec![])
441 .build()
442 .unwrap_err();
443 assert!(matches!(err, BuildRequestError::EmptyTargets));
444 }
445
446 #[test]
447 fn to_http_uses_the_full_url_as_uri() {
448 let req = Request::post("https://example.org:8443/echo")
449 .header("content-type", "text/plain")
450 .body("hello")
451 .build()
452 .unwrap();
453 let http_req = req.to_http().unwrap();
454 assert_eq!(http_req.uri(), "https://example.org:8443/echo");
455 assert_eq!(
456 http_req.uri().authority().map(http::uri::Authority::as_str),
457 Some("example.org:8443")
458 );
459 assert_eq!(http_req.headers()["content-type"], "text/plain");
460 }
461}