Skip to main content

io_http/rfc8615/
well_known.rs

1//! I/O-free coroutine for `.well-known` URI discovery ([RFC 8615]).
2//! Wraps [`Http11Send`] and surfaces the resolved redirect URL as part
3//! of the terminal output.
4//!
5//! [RFC 8615]: https://www.rfc-editor.org/rfc/rfc8615
6
7use alloc::{format, string::String};
8
9use thiserror::Error;
10use url::{ParseError, Url};
11
12use crate::{
13    coroutine::*,
14    rfc9110::{request::HttpRequest, response::HttpResponse, send::HttpSendYield},
15    rfc9112::send::{Http11Send, Http11SendError},
16};
17
18/// Failure causes during the HTTP well-known discovery flow.
19#[derive(Debug, Error)]
20pub enum WellKnownError {
21    #[error("HTTP well-known failed: invalid base URL `{1}`")]
22    InvalidBaseUrl(#[source] ParseError, String),
23    #[error("HTTP well-known failed: {0}")]
24    Send(#[from] Http11SendError),
25}
26
27/// Terminal output of [`WellKnown`]; `redirect_url` is `Some` only on
28/// 3xx with a parseable `Location`. `same_origin` is `false` when a
29/// redirect crosses scheme/host/port (do not forward credentials).
30#[derive(Debug)]
31pub struct WellKnownOutput {
32    pub response: HttpResponse,
33    pub keep_alive: bool,
34    pub same_origin: bool,
35    pub redirect_url: Option<Url>,
36}
37
38/// I/O-free coroutine to perform a `.well-known` URI discovery request.
39#[derive(Debug)]
40pub struct WellKnown(Http11Send);
41
42impl WellKnown {
43    /// Builds a GET on `/.well-known/{service}` against `base_url`; the
44    /// base scheme, host, and port are preserved.
45    pub fn prepare_request(
46        base_url: impl AsRef<str>,
47        service: impl AsRef<str>,
48    ) -> Result<HttpRequest, WellKnownError> {
49        let base = base_url.as_ref();
50        let mut url =
51            Url::parse(base).map_err(|e| WellKnownError::InvalidBaseUrl(e, base.into()))?;
52        url.set_path(&format!("/.well-known/{}", service.as_ref()));
53        Ok(HttpRequest::get(url))
54    }
55
56    /// Creates a new coroutine from a prepared request.
57    pub fn new(request: HttpRequest) -> Self {
58        Self(Http11Send::new(request))
59    }
60}
61
62impl HttpCoroutine for WellKnown {
63    type Yield = HttpYield;
64    type Return = Result<WellKnownOutput, WellKnownError>;
65
66    fn resume(&mut self, arg: Option<&[u8]>) -> HttpCoroutineState<Self::Yield, Self::Return> {
67        match self.0.resume(arg) {
68            HttpCoroutineState::Complete(Ok(out)) => {
69                HttpCoroutineState::Complete(Ok(WellKnownOutput {
70                    response: out.response,
71                    keep_alive: out.keep_alive,
72                    same_origin: true,
73                    redirect_url: None,
74                }))
75            }
76            HttpCoroutineState::Yielded(HttpSendYield::WantsRead) => {
77                HttpCoroutineState::Yielded(HttpYield::WantsRead)
78            }
79            HttpCoroutineState::Yielded(HttpSendYield::WantsWrite(bytes)) => {
80                HttpCoroutineState::Yielded(HttpYield::WantsWrite(bytes))
81            }
82            HttpCoroutineState::Yielded(HttpSendYield::WantsRedirect {
83                url,
84                response,
85                keep_alive,
86                same_origin,
87            }) => HttpCoroutineState::Complete(Ok(WellKnownOutput {
88                response,
89                keep_alive,
90                same_origin,
91                redirect_url: Some(url),
92            })),
93            HttpCoroutineState::Complete(Err(err)) => HttpCoroutineState::Complete(Err(err.into())),
94        }
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use alloc::vec::Vec;
101
102    use super::*;
103
104    #[test]
105    fn prepare_request_sets_well_known_path() {
106        let req = WellKnown::prepare_request("http://example.com", "caldav").unwrap();
107        assert_eq!(req.url.path(), "/.well-known/caldav");
108    }
109
110    #[test]
111    fn prepare_request_preserves_scheme_and_host() {
112        let req = WellKnown::prepare_request("https://example.com", "carddav").unwrap();
113        assert_eq!(req.url.scheme(), "https");
114        assert_eq!(req.url.host_str(), Some("example.com"));
115    }
116
117    #[test]
118    fn prepare_request_preserves_port() {
119        let req = WellKnown::prepare_request("http://example.com:8080", "oauth").unwrap();
120        assert_eq!(req.url.port(), Some(8080));
121    }
122
123    #[test]
124    fn prepare_request_rejects_invalid_url() {
125        let err = WellKnown::prepare_request("not a url", "caldav").unwrap_err();
126        let WellKnownError::InvalidBaseUrl(_, base) = err else {
127            panic!("expected InvalidBaseUrl, got {err:?}");
128        };
129        assert_eq!(base, "not a url");
130    }
131
132    #[test]
133    fn redirect_surfaces_redirect_url() {
134        let req = WellKnown::prepare_request("http://example.com", "caldav").unwrap();
135        let mut coroutine = WellKnown::new(req);
136
137        let _bytes = expect_wants_write(&mut coroutine, None);
138        expect_wants_read(&mut coroutine, None);
139
140        let reply =
141            b"HTTP/1.1 301 Moved Permanently\r\nLocation: /caldav\r\nContent-Length: 0\r\n\r\n";
142        let out = expect_complete_ok(&mut coroutine, Some(reply));
143        let url = out.redirect_url.expect("redirect URL should be set");
144        assert_eq!(url.path(), "/caldav");
145        assert!(out.same_origin);
146    }
147
148    #[test]
149    fn non_redirect_completes_without_redirect_url() {
150        let req = WellKnown::prepare_request("http://example.com", "caldav").unwrap();
151        let mut coroutine = WellKnown::new(req);
152
153        expect_wants_write(&mut coroutine, None);
154        expect_wants_read(&mut coroutine, None);
155
156        let reply = b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n";
157        let out = expect_complete_ok(&mut coroutine, Some(reply));
158        assert!(out.redirect_url.is_none());
159        assert_eq!(*out.response.status, 200);
160    }
161
162    #[test]
163    fn parse_error_propagates_as_send_failure() {
164        let req = WellKnown::prepare_request("http://example.com", "caldav").unwrap();
165        let mut coroutine = WellKnown::new(req);
166
167        expect_wants_write(&mut coroutine, None);
168        expect_wants_read(&mut coroutine, None);
169
170        let reply = b"HTTP/1.1 200 OK\r\nContent-Length: notanumber\r\n\r\n";
171        let err = expect_complete_err(&mut coroutine, Some(reply));
172        assert!(
173            matches!(
174                err,
175                WellKnownError::Send(Http11SendError::InvalidContentLength(_))
176            ),
177            "expected Send(InvalidContentLength), got {err:?}",
178        );
179    }
180
181    // --- utils
182
183    fn expect_wants_write(cor: &mut WellKnown, arg: Option<&[u8]>) -> Vec<u8> {
184        match cor.resume(arg) {
185            HttpCoroutineState::Yielded(HttpYield::WantsWrite(bytes)) => bytes,
186            state => panic!("expected WantsWrite, got {state:?}"),
187        }
188    }
189
190    fn expect_wants_read(cor: &mut WellKnown, arg: Option<&[u8]>) {
191        match cor.resume(arg) {
192            HttpCoroutineState::Yielded(HttpYield::WantsRead) => {}
193            state => panic!("expected WantsRead, got {state:?}"),
194        }
195    }
196
197    fn expect_complete_ok(cor: &mut WellKnown, arg: Option<&[u8]>) -> WellKnownOutput {
198        match cor.resume(arg) {
199            HttpCoroutineState::Complete(Ok(out)) => out,
200            state => panic!("expected Complete(Ok), got {state:?}"),
201        }
202    }
203
204    fn expect_complete_err(cor: &mut WellKnown, arg: Option<&[u8]>) -> WellKnownError {
205        match cor.resume(arg) {
206            HttpCoroutineState::Complete(Err(err)) => err,
207            state => panic!("expected Complete(Err), got {state:?}"),
208        }
209    }
210}