1use std::sync::Arc;
30
31use async_trait::async_trait;
32use bytes::Bytes;
33use http_body_util::{BodyExt, Empty, Limited};
34use hyper::Request;
35use tracing::{debug, info, warn};
36use url::Url;
37
38use super::{ChallengeError, ChallengeValidator, HTTP_01, ValidationContext};
39use crate::config::Http01Config;
40
41pub(crate) const WELL_KNOWN_PREFIX: &str = "/.well-known/acme-challenge/";
51
52#[derive(Debug)]
54pub struct HttpResponse {
55 pub status: u16,
56 pub location: Option<String>,
59 pub body: Vec<u8>,
60 pub truncated: bool,
64}
65
66#[derive(Debug)]
68pub enum FetchError {
69 Connect(String),
71 Protocol(String),
73}
74
75#[async_trait]
81pub trait HttpFetcher: Send + Sync {
82 async fn get(&self, url: &Url, max_bytes: usize) -> Result<HttpResponse, FetchError>;
83}
84
85pub struct Http01Validator {
87 fetcher: Arc<dyn HttpFetcher>,
88 port: u16,
89 https_port: u16,
90 follow_redirects: bool,
91 max_redirects: u8,
92 max_response_bytes: usize,
93}
94
95impl std::fmt::Debug for Http01Validator {
96 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98 formatter
99 .debug_struct("Http01Validator")
100 .field("port", &self.port)
101 .field("https_port", &self.https_port)
102 .field("follow_redirects", &self.follow_redirects)
103 .field("max_redirects", &self.max_redirects)
104 .field("max_response_bytes", &self.max_response_bytes)
105 .finish_non_exhaustive()
106 }
107}
108
109impl Http01Validator {
110 pub fn from_config(
116 cfg: &Http01Config,
117 outbound: crate::http_client::Outbound,
118 ) -> anyhow::Result<Self> {
119 let fetcher = Arc::new(
120 HyperFetcher::new(outbound)
121 .map_err(|error| anyhow::anyhow!("challenge.http_01: {error}"))?,
122 );
123 info!(
124 event = "challenge_http_01_loaded",
125 outcome = "success",
126 port = cfg.port,
127 follow_redirects = cfg.follow_redirects,
128 );
129 Ok(Self::with_fetcher(cfg, fetcher))
130 }
131
132 pub fn with_fetcher(cfg: &Http01Config, fetcher: Arc<dyn HttpFetcher>) -> Self {
134 Self {
135 fetcher,
136 port: cfg.port,
137 https_port: cfg.https_port,
138 follow_redirects: cfg.follow_redirects,
139 max_redirects: cfg.max_redirects,
140 max_response_bytes: cfg.max_response_bytes,
141 }
142 }
143
144 fn challenge_url(&self, identifier: &str, token: &str) -> Result<Url, ChallengeError> {
146 let authority = if self.port == 80 {
147 identifier.to_string()
148 } else {
149 format!("{identifier}:{}", self.port)
150 };
151 Url::parse(&format!("http://{authority}{WELL_KNOWN_PREFIX}{token}")).map_err(|error| {
152 ChallengeError::Internal(format!(
153 "building the challenge URL for {identifier}: {error}"
154 ))
155 })
156 }
157
158 fn redirect_allowed(&self, target: &Url) -> Result<(), ChallengeError> {
164 let (scheme, expected_port) = match target.scheme() {
165 "http" => ("http", self.port),
166 "https" => ("https", self.https_port),
167 other => {
168 return Err(ChallengeError::Unauthorized(format!(
169 "redirect to unsupported scheme {other}"
170 )));
171 }
172 };
173
174 let default_port = if scheme == "http" { 80 } else { 443 };
175 let port = target.port().unwrap_or(default_port);
176 if port != expected_port {
177 return Err(ChallengeError::Unauthorized(format!(
178 "redirect to {scheme} port {port}, which is not the configured {expected_port}"
179 )));
180 }
181 Ok(())
182 }
183}
184
185#[async_trait]
186impl ChallengeValidator for Http01Validator {
187 fn typ(&self) -> &'static str {
188 HTTP_01
189 }
190
191 async fn validate(&self, ctx: &ValidationContext<'_>) -> Result<(), ChallengeError> {
192 let mut url = self.challenge_url(ctx.identifier, ctx.token)?;
193
194 for hop in 0..=self.max_redirects {
195 let response = self
196 .fetcher
197 .get(&url, self.max_response_bytes)
198 .await
199 .map_err(|error| match error {
200 FetchError::Connect(detail) => ChallengeError::Connection(detail),
201 FetchError::Protocol(detail) => ChallengeError::Connection(detail),
204 })?;
205
206 if let (true, Some(location)) = (is_redirect(response.status), &response.location) {
207 if !self.follow_redirects {
208 return Err(ChallengeError::Unauthorized(format!(
209 "{url} redirected but following redirects is disabled"
210 )));
211 }
212 let target = url.join(location).map_err(|error| {
213 ChallengeError::Unauthorized(format!("redirect Location is not a URL: {error}"))
214 })?;
215 self.redirect_allowed(&target)?;
216 debug!(
217 event = "challenge_http_01_redirect",
218 outcome = "progress",
219 from = %url,
220 to = %target,
221 hop,
222 challenge_id = ctx.challenge_id,
223 );
224 url = target;
225 continue;
226 }
227
228 if response.status != 200 {
229 return Err(ChallengeError::Unauthorized(format!(
230 "{url} responded with HTTP {}",
231 response.status
232 )));
233 }
234
235 if response.truncated {
236 return Err(ChallengeError::Unauthorized(format!(
237 "{url} responded with more than {} bytes",
238 self.max_response_bytes
239 )));
240 }
241
242 let body = String::from_utf8_lossy(&response.body);
245 if body.trim() == ctx.key_authorization {
246 debug!(
247 event = "challenge_http_01_matched",
248 outcome = "success",
249 probe_url = %url,
250 challenge_id = ctx.challenge_id,
251 );
252 return Ok(());
253 }
254
255 warn!(
259 event = "challenge_http_01_mismatch",
260 outcome = "failure",
261 probe_url = %url,
262 challenge_id = ctx.challenge_id,
263 body_bytes = response.body.len(),
264 );
265 debug!(
271 event = "challenge_http_01_mismatch_body",
272 outcome = "failure",
273 probe_url = %url,
274 challenge_id = ctx.challenge_id,
275 preview = %body.chars().take(64).collect::<String>(),
276 );
277 return Err(ChallengeError::Unauthorized(format!(
278 "{url} served {} bytes that are not the key authorization",
279 response.body.len()
280 )));
281 }
282
283 Err(ChallengeError::Connection(format!(
284 "more than {} redirects while validating {}",
285 self.max_redirects, ctx.identifier
286 )))
287 }
288}
289
290fn is_redirect(status: u16) -> bool {
292 matches!(status, 301 | 302 | 303 | 307 | 308)
293}
294
295pub struct HyperFetcher {
297 tls: Arc<rustls::ClientConfig>,
298 outbound: crate::http_client::Outbound,
301}
302
303impl HyperFetcher {
304 pub fn new(outbound: crate::http_client::Outbound) -> anyhow::Result<Self> {
305 Ok(Self {
309 tls: super::tls_alpn_01::accept_any_client_config(&[])?,
310 outbound,
311 })
312 }
313
314 async fn exchange(
316 mut connection: crate::http_client::Connection<Empty<Bytes>>,
317 endpoint: &crate::http_client::Endpoint,
318 url: &Url,
319 max_bytes: usize,
320 ) -> Result<HttpResponse, FetchError> {
321 let authority = endpoint.authority();
325
326 let target = connection.request_target(url);
329
330 let request = Request::builder()
333 .uri(target)
334 .header(hyper::header::HOST, &authority)
335 .header(hyper::header::USER_AGENT, "acme-proxy")
336 .header(hyper::header::CONNECTION, "close")
337 .body(Empty::<Bytes>::new())
338 .map_err(|error| FetchError::Protocol(format!("building the request: {error}")))?;
339
340 let response = connection
341 .send_request(request)
342 .await
343 .map_err(|error| FetchError::Protocol(format!("request to {url} failed: {error}")))?;
344
345 let status = response.status().as_u16();
346 let location = response
347 .headers()
348 .get(hyper::header::LOCATION)
349 .and_then(|value| value.to_str().ok())
350 .map(str::to_string);
351
352 let (body, truncated) = match Limited::new(response.into_body(), max_bytes)
355 .collect()
356 .await
357 {
358 Ok(collected) => (collected.to_bytes().to_vec(), false),
359 Err(_) => (Vec::new(), true),
360 };
361
362 Ok(HttpResponse {
363 status,
364 location,
365 body,
366 truncated,
367 })
368 }
369}
370
371#[async_trait]
372impl HttpFetcher for HyperFetcher {
373 async fn get(&self, url: &Url, max_bytes: usize) -> Result<HttpResponse, FetchError> {
374 let endpoint = crate::http_client::Endpoint::from_url(url).map_err(FetchError::Protocol)?;
387
388 let connection = self
389 .outbound
390 .connect(&endpoint, &self.tls)
391 .await
392 .map_err(FetchError::Connect)?;
393
394 Self::exchange(connection, &endpoint, url, max_bytes).await
395 }
396}
397
398#[cfg(test)]
399mod tests {
400 use super::*;
401 use crate::dns::Resolver;
402 use std::collections::HashMap;
403 use std::sync::Mutex;
404
405 const KEY_AUTH: &str = "token-value.thumbprint-value";
406 const TOKEN: &str = "token-value";
407
408 #[derive(Default)]
411 struct StubFetcher {
412 responses: HashMap<String, (u16, Option<String>, Vec<u8>, bool)>,
413 error: Option<&'static str>,
414 requested: Mutex<Vec<String>>,
415 }
416
417 impl StubFetcher {
418 fn serving(url: &str, body: &str) -> Self {
419 Self::default().with(url, 200, None, body.as_bytes().to_vec(), false)
420 }
421
422 fn with(
423 mut self,
424 url: &str,
425 status: u16,
426 location: Option<&str>,
427 body: Vec<u8>,
428 truncated: bool,
429 ) -> Self {
430 self.responses.insert(
431 url.to_string(),
432 (status, location.map(str::to_string), body, truncated),
433 );
434 self
435 }
436
437 fn redirecting(url: &str, to: &str) -> Self {
438 Self::default().with(url, 301, Some(to), Vec::new(), false)
439 }
440
441 fn failing(error: &'static str) -> Self {
442 Self {
443 error: Some(error),
444 ..Self::default()
445 }
446 }
447
448 fn requested(&self) -> Vec<String> {
449 self.requested.lock().unwrap().clone()
450 }
451 }
452
453 #[async_trait]
454 impl HttpFetcher for StubFetcher {
455 async fn get(&self, url: &Url, _max_bytes: usize) -> Result<HttpResponse, FetchError> {
456 self.requested.lock().unwrap().push(url.to_string());
457 if let Some(error) = self.error {
458 return Err(FetchError::Connect(error.to_string()));
459 }
460 match self.responses.get(url.as_str()) {
461 Some((status, location, body, truncated)) => Ok(HttpResponse {
462 status: *status,
463 location: location.clone(),
464 body: body.clone(),
465 truncated: *truncated,
466 }),
467 None => Ok(HttpResponse {
469 status: 404,
470 location: None,
471 body: Vec::new(),
472 truncated: false,
473 }),
474 }
475 }
476 }
477
478 fn validate_with(cfg: Http01Config, fetcher: Arc<StubFetcher>) -> Http01Validator {
479 Http01Validator::with_fetcher(&cfg, fetcher)
480 }
481
482 fn context(identifier: &str) -> ValidationContext<'_> {
483 ValidationContext {
484 identifier,
485 wildcard: false,
486 token: TOKEN,
487 key_authorization: KEY_AUTH,
488 challenge_id: "chall-1",
489 }
490 }
491
492 const CHALLENGE_URL: &str = "http://example.com/.well-known/acme-challenge/token-value";
493
494 #[tokio::test]
495 async fn the_key_authorization_is_fetched_from_the_well_known_url() {
496 let fetcher = Arc::new(StubFetcher::serving(CHALLENGE_URL, KEY_AUTH));
497 let validator = validate_with(Http01Config::default(), fetcher.clone());
498
499 assert!(validator.validate(&context("example.com")).await.is_ok());
500 assert_eq!(fetcher.requested(), vec![CHALLENGE_URL.to_string()]);
501 }
502
503 #[tokio::test]
506 async fn surrounding_whitespace_is_ignored() {
507 let fetcher = Arc::new(StubFetcher::serving(
508 CHALLENGE_URL,
509 &format!(" {KEY_AUTH}\n"),
510 ));
511 assert!(
512 validate_with(Http01Config::default(), fetcher)
513 .validate(&context("example.com"))
514 .await
515 .is_ok()
516 );
517 }
518
519 #[tokio::test]
522 async fn a_wrong_body_is_refused_without_echoing_it() {
523 let secret = "internal-api-token-abc123";
524 let fetcher = Arc::new(StubFetcher::serving(CHALLENGE_URL, secret));
525 let error = validate_with(Http01Config::default(), fetcher)
526 .validate(&context("example.com"))
527 .await
528 .unwrap_err();
529
530 match &error {
531 ChallengeError::Unauthorized(detail) => {
532 assert!(!detail.contains(secret), "the body leaked: {detail}");
533 assert!(detail.contains("25 bytes"), "{detail}");
534 }
535 other => panic!("expected Unauthorized, got {other:?}"),
536 }
537 }
538
539 #[tokio::test]
540 async fn a_non_200_response_is_refused() {
541 let error = validate_with(Http01Config::default(), Arc::new(StubFetcher::default()))
543 .validate(&context("example.com"))
544 .await
545 .unwrap_err();
546 assert!(
547 matches!(&error, ChallengeError::Unauthorized(detail) if detail.contains("HTTP 404")),
548 "{error:?}"
549 );
550 }
551
552 #[tokio::test]
553 async fn an_unreachable_target_is_a_connection_error() {
554 let error = validate_with(
555 Http01Config::default(),
556 Arc::new(StubFetcher::failing("connection refused")),
557 )
558 .validate(&context("example.com"))
559 .await
560 .unwrap_err();
561 assert!(
562 matches!(&error, ChallengeError::Connection(detail) if detail.contains("refused")),
563 "{error:?}"
564 );
565 }
566
567 #[tokio::test]
569 async fn a_redirect_to_https_is_followed() {
570 const HTTPS_URL: &str = "https://example.com/.well-known/acme-challenge/token-value";
571 let fetcher = Arc::new(StubFetcher::redirecting(CHALLENGE_URL, HTTPS_URL).with(
572 HTTPS_URL,
573 200,
574 None,
575 KEY_AUTH.as_bytes().to_vec(),
576 false,
577 ));
578 let validator = validate_with(Http01Config::default(), fetcher.clone());
579
580 assert!(validator.validate(&context("example.com")).await.is_ok());
581 assert_eq!(
582 fetcher.requested(),
583 vec![CHALLENGE_URL.to_string(), HTTPS_URL.to_string()]
584 );
585 }
586
587 #[tokio::test]
588 async fn a_relative_location_is_resolved_against_the_current_url() {
589 const MOVED: &str = "http://example.com/elsewhere";
590 let fetcher = Arc::new(StubFetcher::redirecting(CHALLENGE_URL, "/elsewhere").with(
591 MOVED,
592 200,
593 None,
594 KEY_AUTH.as_bytes().to_vec(),
595 false,
596 ));
597 let validator = validate_with(Http01Config::default(), fetcher.clone());
598
599 assert!(validator.validate(&context("example.com")).await.is_ok());
600 assert_eq!(fetcher.requested()[1], MOVED);
601 }
602
603 #[tokio::test]
606 async fn a_redirect_loop_stops_at_the_cap() {
607 let fetcher = Arc::new(StubFetcher::redirecting(CHALLENGE_URL, CHALLENGE_URL));
608 let cfg = Http01Config {
609 max_redirects: 3,
610 ..Http01Config::default()
611 };
612 let validator = validate_with(cfg, fetcher.clone());
613
614 let error = validator
615 .validate(&context("example.com"))
616 .await
617 .unwrap_err();
618 assert!(
619 matches!(&error, ChallengeError::Connection(detail) if detail.contains("more than 3 redirects")),
620 "{error:?}"
621 );
622 assert_eq!(fetcher.requested().len(), 4);
624 }
625
626 #[tokio::test]
628 async fn a_redirect_to_another_scheme_is_refused() {
629 let fetcher = Arc::new(StubFetcher::redirecting(
630 CHALLENGE_URL,
631 "file:///etc/passwd",
632 ));
633 let error = validate_with(Http01Config::default(), fetcher)
634 .validate(&context("example.com"))
635 .await
636 .unwrap_err();
637 assert!(
638 matches!(&error, ChallengeError::Unauthorized(detail) if detail.contains("unsupported scheme")),
639 "{error:?}"
640 );
641 }
642
643 #[tokio::test]
646 async fn a_redirect_to_another_port_is_refused() {
647 let fetcher = Arc::new(StubFetcher::redirecting(
648 CHALLENGE_URL,
649 "http://10.0.0.5:9200/_cluster/health",
650 ));
651 let error = validate_with(Http01Config::default(), fetcher)
652 .validate(&context("example.com"))
653 .await
654 .unwrap_err();
655 assert!(
656 matches!(&error, ChallengeError::Unauthorized(detail) if detail.contains("port 9200")),
657 "{error:?}"
658 );
659 }
660
661 #[tokio::test]
662 async fn redirects_can_be_turned_off_entirely() {
663 let fetcher = Arc::new(StubFetcher::redirecting(
664 CHALLENGE_URL,
665 "https://example.com/.well-known/acme-challenge/token-value",
666 ));
667 let cfg = Http01Config {
668 follow_redirects: false,
669 ..Http01Config::default()
670 };
671 let error = validate_with(cfg, fetcher.clone())
672 .validate(&context("example.com"))
673 .await
674 .unwrap_err();
675
676 assert!(
677 matches!(&error, ChallengeError::Unauthorized(detail) if detail.contains("disabled")),
678 "{error:?}"
679 );
680 assert_eq!(fetcher.requested().len(), 1);
681 }
682
683 #[tokio::test]
686 async fn an_oversized_body_is_refused_without_comparison() {
687 let fetcher = Arc::new(StubFetcher::default().with(
688 CHALLENGE_URL,
689 200,
690 None,
691 KEY_AUTH.as_bytes().to_vec(),
692 true,
693 ));
694 let error = validate_with(Http01Config::default(), fetcher)
695 .validate(&context("example.com"))
696 .await
697 .unwrap_err();
698 assert!(
699 matches!(&error, ChallengeError::Unauthorized(detail) if detail.contains("more than 4096 bytes")),
700 "{error:?}"
701 );
702 }
703
704 #[tokio::test]
705 async fn a_non_default_port_appears_in_the_url() {
706 const URL: &str = "http://example.com:8080/.well-known/acme-challenge/token-value";
707 let fetcher = Arc::new(StubFetcher::serving(URL, KEY_AUTH));
708 let cfg = Http01Config {
709 port: 8080,
710 ..Http01Config::default()
711 };
712 let validator = validate_with(cfg, fetcher.clone());
713
714 assert!(validator.validate(&context("example.com")).await.is_ok());
715 assert_eq!(fetcher.requested(), vec![URL.to_string()]);
716 }
717
718 #[test]
719 fn reports_its_challenge_type() {
720 assert_eq!(
721 validate_with(Http01Config::default(), Arc::new(StubFetcher::default())).typ(),
722 "http-01"
723 );
724 }
725
726 #[test]
727 fn only_3xx_statuses_carrying_a_location_are_redirects() {
728 for status in [301, 302, 303, 307, 308] {
729 assert!(is_redirect(status), "{status}");
730 }
731 for status in [200, 204, 304, 400, 404, 500] {
732 assert!(!is_redirect(status), "{status}");
733 }
734 }
735
736 mod loopback {
740 use super::*;
741 use std::net::IpAddr;
742 use tokio::io::{AsyncReadExt, AsyncWriteExt};
743 use tokio::net::TcpListener;
744
745 struct UnreachableResolver;
748
749 #[async_trait]
750 impl Resolver for UnreachableResolver {
751 async fn reverse(&self, _ip: IpAddr) -> Result<Vec<String>, String> {
752 unreachable!()
753 }
754 async fn forward(&self, _name: &str) -> Result<Vec<IpAddr>, String> {
755 unreachable!("a literal 127.0.0.1 must short-circuit before this is called")
756 }
757 async fn txt(&self, _name: &str) -> Result<Vec<String>, String> {
758 unreachable!()
759 }
760 }
761
762 fn fetcher() -> HyperFetcher {
763 HyperFetcher::new(crate::testutil::outbound_with(Arc::new(
764 UnreachableResolver,
765 )))
766 .unwrap()
767 }
768
769 async fn serve_once(response: &'static str) -> (u16, tokio::task::JoinHandle<String>) {
771 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
772 let port = listener.local_addr().unwrap().port();
773
774 let handle = tokio::spawn(async move {
775 let (mut stream, _) = listener.accept().await.unwrap();
776 let mut buffer = vec![0u8; 2048];
777 let read = stream.read(&mut buffer).await.unwrap();
778 stream.write_all(response.as_bytes()).await.unwrap();
779 stream.shutdown().await.unwrap();
780 String::from_utf8_lossy(&buffer[..read]).into_owned()
781 });
782
783 (port, handle)
784 }
785
786 #[tokio::test]
787 async fn fetches_a_body_and_sends_a_host_header() {
788 let (port, server) = serve_once(
789 "HTTP/1.1 200 OK\r\nContent-Length: 5\r\nConnection: close\r\n\r\nhello",
790 )
791 .await;
792 let url = Url::parse(&format!("http://127.0.0.1:{port}/.well-known/x")).unwrap();
793
794 let response = fetcher().get(&url, 4096).await.unwrap();
795 assert_eq!(response.status, 200);
796 assert_eq!(response.body, b"hello");
797 assert!(!response.truncated);
798
799 let request = server.await.unwrap();
800 assert!(
801 request.starts_with("GET /.well-known/x HTTP/1.1"),
802 "{request}"
803 );
804 assert!(
805 request
806 .to_lowercase()
807 .contains(&format!("host: 127.0.0.1:{port}")),
808 "{request}"
809 );
810 }
811
812 #[tokio::test]
813 async fn reads_a_redirect_location() {
814 let (port, _server) = serve_once(
815 "HTTP/1.1 301 Moved Permanently\r\nLocation: https://example.com/x\r\n\
816 Content-Length: 0\r\nConnection: close\r\n\r\n",
817 )
818 .await;
819 let url = Url::parse(&format!("http://127.0.0.1:{port}/x")).unwrap();
820
821 let response = fetcher().get(&url, 4096).await.unwrap();
822 assert_eq!(response.status, 301);
823 assert_eq!(response.location.as_deref(), Some("https://example.com/x"));
824 }
825
826 #[tokio::test]
827 async fn a_body_over_the_cap_is_reported_as_truncated() {
828 let (port, _server) = serve_once(
829 "HTTP/1.1 200 OK\r\nContent-Length: 20\r\nConnection: close\r\n\r\n\
830 aaaaaaaaaaaaaaaaaaaa",
831 )
832 .await;
833 let url = Url::parse(&format!("http://127.0.0.1:{port}/x")).unwrap();
834
835 let response = fetcher().get(&url, 8).await.unwrap();
836 assert!(response.truncated);
837 }
838
839 #[tokio::test]
840 async fn a_closed_port_is_a_connect_error() {
841 let port = {
843 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
844 listener.local_addr().unwrap().port()
845 };
846 let url = Url::parse(&format!("http://127.0.0.1:{port}/x")).unwrap();
847
848 let error = fetcher().get(&url, 4096).await.unwrap_err();
849 assert!(matches!(&error, FetchError::Connect(_)), "{error:?}");
850 }
851
852 #[tokio::test]
856 async fn the_request_target_keeps_the_query_string() {
857 let (port, server) =
858 serve_once("HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\nok")
859 .await;
860 let url =
861 Url::parse(&format!("http://127.0.0.1:{port}/.well-known/x?a=1&b=2")).unwrap();
862
863 fetcher().get(&url, 4096).await.unwrap();
864 let request = server.await.unwrap();
865 assert!(
866 request.starts_with("GET /.well-known/x?a=1&b=2 "),
867 "{request}"
868 );
869 }
870
871 #[tokio::test]
875 async fn an_https_url_against_a_cleartext_server_is_a_connect_error() {
876 let (port, _server) =
877 serve_once("HTTP/1.1 200 OK\r\nContent-Length: 0\r\nConnection: close\r\n\r\n")
878 .await;
879 let url = Url::parse(&format!("https://127.0.0.1:{port}/x")).unwrap();
880
881 let error = fetcher().get(&url, 4096).await.unwrap_err();
882 assert!(matches!(&error, FetchError::Connect(_)), "{error:?}");
883 }
884 }
885
886 #[test]
889 fn the_validator_debug_shows_its_policy() {
890 let validator = validate_with(
891 Http01Config {
892 port: 8080,
893 https_port: 8443,
894 follow_redirects: false,
895 max_redirects: 2,
896 max_response_bytes: 1024,
897 },
898 Arc::new(StubFetcher::default()),
899 );
900 let rendered = format!("{validator:?}");
901 for expected in ["Http01Validator", "8080", "8443", "false", "1024"] {
902 assert!(
903 rendered.contains(expected),
904 "{expected} missing: {rendered}"
905 );
906 }
907 }
908
909 #[tokio::test]
913 async fn a_location_that_is_not_a_url_is_refused() {
914 let fetcher = Arc::new(StubFetcher::redirecting(CHALLENGE_URL, "http://"));
915 let error = validate_with(Http01Config::default(), fetcher)
916 .validate(&context("example.com"))
917 .await
918 .unwrap_err();
919 match error {
920 ChallengeError::Unauthorized(detail) => {
921 assert!(detail.contains("Location is not a URL"), "{detail}")
922 }
923 other => panic!("expected Unauthorized, got {other:?}"),
924 }
925 }
926}