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 Http11WellKnownError {
21 #[error("HTTP well-known failed: invalid base URL `{1}`")]
23 InvalidBaseUrl(#[source] ParseError, String),
24 #[error("HTTP well-known failed: {0}")]
26 Send(#[from] Http11SendError),
27}
28
29#[derive(Debug)]
33pub struct Http11WellKnownOutput {
34 pub response: HttpResponse,
36 pub keep_alive: bool,
38 pub same_origin: bool,
41 pub redirect_url: Option<Url>,
43}
44
45#[derive(Debug)]
47pub struct Http11WellKnown(Http11Send);
48
49impl Http11WellKnown {
50 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 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}