Skip to content

Conversation

@mdegel
Copy link
Contributor

@mdegel mdegel commented Nov 2, 2025

Partial fix for #75

Technically this PR fixes the general redirect aspects for the GET call in acme.rs.
As of now the POST from acme.rs would still not be covered:
https://github.com/nginx/nginx-acme/blob/main/src/acme.rs#L181

Based on my testing this should only affect use-cases where URLs (relative, or absolute) are returned, that cause another redirect, which should be rarely the case (at least not in the environments I tested with). Reason being, that the URLs provided by https://acme.example.com/directory should normally be OK not requiring redirects.

Also I'm not completely sure how to best implement the requirements for RFC 8555 §6.2.
I can add those as well if needed, though it might be best to extract part of the redirect functionality to it's own (reusable) unit I assume.
Any opinions on this matter?

IMO this basic PR should already cover a few additional standard use cases, such as:

@bavshin-f5
Copy link
Member

Did not have time to take a good look at this; some general comments:

  • I would definitely avoid implementing redirects for anything other than GET. The behavior for redirects in POST/POST-as-GET requests is not defined in RFC8555, and too many things can be interpreted differently. For example, I would assume that 301-303 must fail, and 307-308 must obtain new nonce and update URL in the JWS header. Server implementers may have different opinion.
  • Implementation in the NgxHttpClient looks a bit too low-level.
  • I am planning to swap the HttpClient implementation with another one using subrequests on a fake request, as soon as we make it possible with the open-source nginx code. Maintaining redirect support in both implementations is undesirable.

Something I would want to see here is a simple loop in AcmeClient::get():

pub async fn get(&self, url: &Uri) -> Result<http::Response<Bytes>, RequestError> {
    let mut u = url.clone();

    for _ in 0..MAX_REDIRECTS {
        let req = ...;
        let res = self.http.request(req).await?;

        if res.status().is_redirection() {
            u = ...;
            continue;
        }

        return Ok(res);
    }

    Err(...)
}

@mdegel
Copy link
Contributor Author

mdegel commented Nov 7, 2025

Thanks for your input.

Your suggestion makes a lot of sense, compared to my previously overengineered solution.
I have updated the PR to reflect the required changes.

I have added some new error options, some input if that works for you, or if I should rather compress them into existing ones would be appreciated.

src/acme.rs Outdated

if res.status().is_redirection() {
if let Some(location) = try_get_header(res.headers(), http::header::LOCATION) {
u = Uri::try_from(location).map_err(RequestError::UrlParse)?;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's still necessary to handle relative URIs in the Location header.

I don't like the idea of reimplementing RFC3986 § 5 reference resolution algorithm (resolve_location in the previous revision didn't look quite right), so I suggest to use the same dependency as reqwest and tower-http do, iri-string. Here's how it may look: https://docs.rs/tower-http/0.6.6/src/tower_http/follow_redirect/mod.rs.html#394.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have adjusted the code to match your suggestion.

I also split out the error logic to a custom RedirectError, similar to NewAccountError, as the potential issues kind of started to pollute RequestError.

In addition I could also extend it into the Problem and extend some logic there:

impl From<RequestError> for NewAccountError {
    fn from(value: RequestError) -> Self {
        match value {
            RequestError::Protocol(problem) => Self::Protocol(problem),
            _ => Self::Request(value),
        }
    }
}

impl Problem {

Not sure what you'd prefer here though?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mind extracting URI resolution code into a function? I suspect we may need it elsewhere in the future, because RFC8555 does not require any of the directory or resource location URIs to be absolute.

I also don't believe we need that many errors:

  • UriAbsoluteString::try_from() should not fail, because self.http.request() already fails on invalid URI. Still need to check the result, but don't expect anything useful.
  • UriReferenceStr::new() returns a rather useless error "Invalid IRI".
  • Uri::try_from() should succeed, because it always receives a valid absolute URI.

With that in mind, fn resolve_uri(base, relative) -> Option<Uri> is a reasonable interface, and the None can be later converted to a single InvalidUri.

"invalid redirect URI"/"missing redirect URI"/"too many redirects" is a sufficient set of errors to handle this, and it will look acceptable both as a separate RedirectError or as members of RequestError. Just ensure that you maintain the sorting order of the enum members.

In addition I could also extend it into the Problem and extend some logic there:

Problem is reserved for ACME protocol errors received from the server. Redirect errors don't belong there.
The conversion you quoted exists to simplify handling Problems that point to a permanent configuration error, i.e. cases when we should stop attempting to process the current item. Also, to make logs prettier.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants