diff --git a/Cargo.lock b/Cargo.lock index 2464963..6fd758c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -270,6 +270,16 @@ dependencies = [ "want", ] +[[package]] +name = "iri-string" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "itertools" version = "0.13.0" @@ -341,6 +351,7 @@ dependencies = [ "http-body-util", "http-serde", "hyper", + "iri-string", "libc", "nginx-sys", "ngx", diff --git a/Cargo.toml b/Cargo.toml index 925ef24..12672b3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ http-body = "1.0.1" http-body-util = "0.1.3" http-serde = "2.1.1" hyper = { version = "1.6.0", features = ["client", "http1"] } +iri-string = "0.7.9" libc = "0.2.174" nginx-sys = "0.5.0" ngx = { version = "0.5.0", features = ["async", "serde", "std"] } diff --git a/src/acme.rs b/src/acme.rs index 6876326..415a88b 100644 --- a/src/acme.rs +++ b/src/acme.rs @@ -10,8 +10,9 @@ use std::collections::VecDeque; use std::string::{String, ToString}; use bytes::Bytes; -use error::{NewAccountError, NewCertificateError, RequestError}; +use error::{NewAccountError, NewCertificateError, RedirectError, RequestError}; use http::Uri; +use iri_string::types::{UriAbsoluteString, UriReferenceStr}; use ngx::allocator::{Allocator, Box}; use ngx::async_::sleep; use ngx::collections::Vec; @@ -43,6 +44,9 @@ const MAX_SERVER_RETRY_INTERVAL: Duration = Duration::from_secs(60); static REPLAY_NONCE: http::HeaderName = http::HeaderName::from_static("replay-nonce"); +// Maximum number of redirects to follow for a single request. +const MAX_REDIRECTS: usize = 10; + pub enum NewAccountOutput<'a> { Created(&'a str), Found(&'a str), @@ -170,12 +174,34 @@ where } pub async fn get(&self, url: &Uri) -> Result, RequestError> { - let req = http::Request::builder() - .uri(url) - .method(http::Method::GET) - .header(http::header::CONTENT_LENGTH, 0) - .body(String::new())?; - Ok(self.http.request(req).await?) + let mut u = url.clone(); + + for _ in 0..MAX_REDIRECTS { + let req = http::Request::builder() + .uri(&u) + .method(http::Method::GET) + .header(http::header::CONTENT_LENGTH, 0) + .body(String::new())?; + let res = self.http.request(req).await?; + + if res.status().is_redirection() { + if let Some(location) = try_get_header(res.headers(), http::header::LOCATION) { + let base = UriAbsoluteString::try_from(u.to_string()) + .map_err(RedirectError::InvalidBase)?; + let location_ref = + UriReferenceStr::new(location).map_err(RedirectError::InvalidLocation)?; + let resolved = location_ref.resolve_against(&base).to_string(); + u = Uri::try_from(resolved).map_err(RedirectError::InvalidUri)?; + continue; + } else { + return Err(RedirectError::MissingLocation.into()); + } + } + + return Ok(res); + } + + Err(RedirectError::TooManyRedirects.into()) } pub async fn post>( diff --git a/src/acme/error.rs b/src/acme/error.rs index 6e0e626..b45f62c 100644 --- a/src/acme/error.rs +++ b/src/acme/error.rs @@ -114,6 +114,24 @@ impl NewCertificateError { } } +#[derive(Debug, Error)] +pub enum RedirectError { + #[error("missing Location header")] + MissingLocation, + + #[error("too many redirects")] + TooManyRedirects, + + #[error("invalid base URI (IRI creation)")] + InvalidBase(#[source] iri_string::types::CreationError), + + #[error("invalid redirect location (IRI validation)")] + InvalidLocation(#[source] iri_string::validate::Error), + + #[error("invalid resolved redirect URI")] + InvalidUri(#[source] http::uri::InvalidUri), +} + #[derive(Debug, Error)] pub enum RequestError { #[error(transparent)] @@ -145,6 +163,9 @@ pub enum RequestError { #[error("cannot sign request body ({0})")] Sign(#[from] crate::jws::Error), + + #[error("redirect failed: {0}")] + Redirect(#[from] RedirectError), } impl From for RequestError {