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