io_http/rfc8615/
well_known.rs1use 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#[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#[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#[derive(Debug)]
40pub struct WellKnown(Http11Send);
41
42impl WellKnown {
43 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 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 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}