@@ -9,15 +9,17 @@ use core::ptr::NonNull;
99use std:: io;
1010
1111use bytes:: Bytes ;
12- use http:: uri:: Scheme ;
13- use http:: { Request , Response } ;
12+ use http:: uri:: { PathAndQuery , Scheme } ;
13+ use http:: { Method , Request , Response , StatusCode , Uri } ;
1414use http_body:: Body ;
1515use http_body_util:: BodyExt ;
1616use nginx_sys:: { ngx_log_t, ngx_resolver_t, NGX_LOG_WARN } ;
1717use ngx:: allocator:: Box ;
1818use ngx:: async_:: resolver:: Resolver ;
1919use ngx:: async_:: spawn;
2020use ngx:: ngx_log_error;
21+ use std:: format;
22+ use std:: string:: ToString ;
2123use thiserror:: Error ;
2224
2325use super :: peer_conn:: PeerConnection ;
@@ -40,6 +42,9 @@ const NGX_ACME_USER_AGENT: &str = constcat::concat!(
4042 NGINX_VER ,
4143) ;
4244
45+ // Maximum number of redirects to follow for a single request.
46+ const MAX_REDIRECTS : usize = 10 ;
47+
4348#[ allow( async_fn_in_trait) ]
4449pub trait HttpClient {
4550 type Error : StdError + Send + Sync + ' static ;
@@ -101,26 +106,41 @@ impl<'a> NgxHttpClient<'a> {
101106impl HttpClient for NgxHttpClient < ' _ > {
102107 type Error = HttpClientError ;
103108
104- async fn request < B > ( & self , mut req : Request < B > ) -> Result < Response < Bytes > , Self :: Error >
109+ async fn request < B > ( & self , req : Request < B > ) -> Result < Response < Bytes > , Self :: Error >
105110 where
106111 B : Body + Send + ' static ,
107112 <B as Body >:: Data : Send ,
108113 <B as Body >:: Error : StdError + Send + Sync ,
109114 {
110- let uri = req. uri ( ) . clone ( ) ;
115+ // Buffer the request body so it can be retransmitted across redirects safely.
116+ let ( mut parts, body) = req. into_parts ( ) ;
117+ let orig_method = parts. method . clone ( ) ;
118+ let mut body_bytes = BodyExt :: collect ( body)
119+ . await
120+ . map_err ( |e| HttpClientError :: Body ( std:: boxed:: Box :: new ( e) ) ) ?
121+ . to_bytes ( ) ;
122+
123+ let mut current_uri = parts. uri . clone ( ) ;
111124
112- let authority = uri
125+ // Basic validation of the starting URI.
126+ let mut authority = current_uri
113127 . authority ( )
128+ . cloned ( )
114129 . ok_or ( HttpClientError :: Uri ( "missing authority" ) ) ?;
115130
116- let path_and_query = uri
117- . path_and_query ( )
118- . ok_or ( HttpClientError :: Uri ( "missing path" ) ) ?;
131+ let mut redirects_followed = 0usize ;
132+
133+ loop {
134+ // Prepare request for the current target by setting origin-form URI and required headers.
135+ let path_and_query: PathAndQuery = current_uri
136+ . path_and_query ( )
137+ . cloned ( )
138+ . ok_or ( HttpClientError :: Uri ( "missing path" ) ) ?;
119139
120- * req . uri_mut ( ) = path_and_query. clone ( ) . into ( ) ;
140+ parts . uri = Uri :: from ( path_and_query. clone ( ) ) ;
121141
122- {
123- let headers = req . headers_mut ( ) ;
142+ // Build a fresh headers map for each attempt to avoid header leakage across hosts.
143+ let mut headers = parts . headers . clone ( ) ;
124144 headers. insert (
125145 http:: header:: HOST ,
126146 http:: HeaderValue :: from_str ( authority. as_str ( ) )
@@ -134,50 +154,207 @@ impl HttpClient for NgxHttpClient<'_> {
134154 http:: header:: CONNECTION ,
135155 http:: HeaderValue :: from_static ( "close" ) ,
136156 ) ;
137- }
138157
139- let ssl = if uri. scheme ( ) == Some ( & Scheme :: HTTPS ) {
140- Some ( self . ssl . as_ref ( ) )
141- } else {
142- None
143- } ;
158+ // Ensure Content-Length matches the body we will send; adjust for GET/HEAD.
159+ let sending_method = parts. method . clone ( ) ;
160+ let mut send_body = http_body_util:: Full :: new ( body_bytes. clone ( ) ) ;
161+ if sending_method == Method :: GET || sending_method == Method :: HEAD {
162+ headers. insert (
163+ http:: header:: CONTENT_LENGTH ,
164+ http:: HeaderValue :: from_static ( "0" ) ,
165+ ) ;
166+ // Empty the body for GET/HEAD attempts.
167+ send_body = http_body_util:: Full :: new ( Bytes :: new ( ) ) ;
168+ } else {
169+ headers. insert (
170+ http:: header:: CONTENT_LENGTH ,
171+ http:: HeaderValue :: from_str ( & body_bytes. len ( ) . to_string ( ) )
172+ . map_err ( |_| HttpClientError :: Uri ( "bad content length" ) ) ?,
173+ ) ;
174+ }
175+
176+ let mut req = Request :: new ( send_body) ;
177+ * req. method_mut ( ) = parts. method . clone ( ) ;
178+ * req. uri_mut ( ) = parts. uri . clone ( ) ;
179+ * req. version_mut ( ) = parts. version ;
180+ {
181+ let hm = req. headers_mut ( ) ;
182+ hm. clear ( ) ;
183+ for ( k, v) in headers. iter ( ) {
184+ hm. insert ( k, v. clone ( ) ) ;
185+ }
186+ }
187+ * req. extensions_mut ( ) = parts. extensions . clone ( ) ;
144188
145- let mut peer = Box :: pin ( PeerConnection :: new ( self . log ) ?) ;
189+ // Establish connection for current target.
190+ let ssl = if current_uri. scheme ( ) == Some ( & Scheme :: HTTPS ) {
191+ Some ( self . ssl . as_ref ( ) )
192+ } else {
193+ None
194+ } ;
146195
147- peer. as_mut ( )
148- . connect_to ( authority. as_str ( ) , & self . resolver , ssl)
149- . await ?;
196+ let mut peer = Box :: pin ( PeerConnection :: new ( self . log ) ?) ;
197+ peer. as_mut ( )
198+ . connect_to ( authority. as_str ( ) , & self . resolver , ssl)
199+ . await ?;
150200
151- if ssl. is_some ( ) && self . ssl_verify {
152- if let Err ( err) = peer. verify_peer ( ) {
153- let _ = future:: poll_fn ( |cx| peer. as_mut ( ) . poll_shutdown ( cx) ) . await ;
154- return Err ( err. into ( ) ) ;
201+ if ssl. is_some ( ) && self . ssl_verify {
202+ if let Err ( err) = peer. verify_peer ( ) {
203+ let _ = future:: poll_fn ( |cx| peer. as_mut ( ) . poll_shutdown ( cx) ) . await ;
204+ return Err ( err. into ( ) ) ;
205+ }
155206 }
156- }
157207
158- if let Some ( c) = peer. connection_mut ( ) {
159- c. requests += 1 ;
160- }
208+ if let Some ( c) = peer. connection_mut ( ) {
209+ c. requests += 1 ;
210+ }
211+
212+ let ( mut sender, conn) = hyper:: client:: conn:: http1:: handshake ( peer) . await ?;
213+
214+ let log = self . log ;
215+ spawn ( async move {
216+ if let Err ( err) = conn. await {
217+ ngx_log_error ! ( NGX_LOG_WARN , log. as_ptr( ) , "connection error: {err}" ) ;
218+ }
219+ } )
220+ . detach ( ) ;
161221
162- let ( mut sender , conn ) = hyper :: client :: conn :: http1 :: handshake ( peer ) . await ?;
222+ let resp = sender . send_request ( req ) . await ?;
163223
164- let log = self . log ;
165- spawn ( async move {
166- if let Err ( err) = conn. await {
167- ngx_log_error ! ( NGX_LOG_WARN , log. as_ptr( ) , "connection error: {err}" ) ;
224+ // If not a redirect, return the response with its body collected.
225+ if !is_redirect ( resp. status ( ) ) {
226+ let ( parts, body) = resp. into_parts ( ) ;
227+ let body = http_body_util:: Limited :: new ( body, NGX_ACME_MAX_BODY_SIZE )
228+ . collect ( )
229+ . await
230+ . map_err ( HttpClientError :: Body ) ?
231+ . to_bytes ( ) ;
232+ return Ok ( Response :: from_parts ( parts, body) ) ;
168233 }
169- } )
170- . detach ( ) ;
171234
172- let resp = sender. send_request ( req) . await ?;
173- let ( parts, body) = resp. into_parts ( ) ;
235+ // For non-GET/HEAD, do not auto-follow redirects.
236+ // ACME JWS 'url' must match the request URL; on redirect the client must re-sign and
237+ // POST to the new Location itself (RFC 8555 §6.2). Return the 3xx so the ACME layer
238+ // can handle it.
239+ if !( parts. method == Method :: GET || parts. method == Method :: HEAD ) {
240+ let ( parts_r, body) = resp. into_parts ( ) ;
241+ let body = http_body_util:: Limited :: new ( body, NGX_ACME_MAX_BODY_SIZE )
242+ . collect ( )
243+ . await
244+ . map_err ( HttpClientError :: Body ) ?
245+ . to_bytes ( ) ;
246+ return Ok ( Response :: from_parts ( parts_r, body) ) ;
247+ }
174248
175- let body = http_body_util:: Limited :: new ( body, NGX_ACME_MAX_BODY_SIZE )
176- . collect ( )
177- . await
178- . map_err ( HttpClientError :: Body ) ?
179- . to_bytes ( ) ;
249+ // Handle redirect logic.
250+ redirects_followed += 1 ;
251+ if redirects_followed > MAX_REDIRECTS {
252+ return Err ( HttpClientError :: Uri ( "too many redirects" ) ) ;
253+ }
254+
255+ let location = resp
256+ . headers ( )
257+ . get ( http:: header:: LOCATION )
258+ . ok_or ( HttpClientError :: Uri ( "redirect without location" ) ) ?;
180259
181- Ok ( Response :: from_parts ( parts, body) )
260+ let new_uri = resolve_location ( & current_uri, location) ?;
261+
262+ // Prevent HTTPS -> HTTP downgrade unless the original request was HTTP.
263+ if current_uri. scheme ( ) == Some ( & Scheme :: HTTPS )
264+ && new_uri. scheme ( ) == Some ( & Scheme :: HTTP )
265+ {
266+ return Err ( HttpClientError :: Uri ( "insecure redirect from https to http" ) ) ;
267+ }
268+
269+ // Adjust method and body according to status code per RFC 9110.
270+ match resp. status ( ) {
271+ StatusCode :: SEE_OTHER => {
272+ // 303: switch to GET, except HEAD remains HEAD
273+ parts. method = if orig_method == Method :: HEAD {
274+ Method :: HEAD
275+ } else {
276+ Method :: GET
277+ } ;
278+ body_bytes = Bytes :: new ( ) ;
279+ // Drop content-type if any
280+ parts. headers . remove ( http:: header:: CONTENT_TYPE ) ;
281+ }
282+ StatusCode :: MOVED_PERMANENTLY | StatusCode :: FOUND => {
283+ // 301/302: preserve method and body
284+ parts. method = orig_method. clone ( ) ;
285+ }
286+ StatusCode :: PERMANENT_REDIRECT | StatusCode :: TEMPORARY_REDIRECT => {
287+ // 308/307: MUST NOT change method
288+ // no-op; parts.method unchanged
289+ }
290+ _ => { }
291+ }
292+
293+ // Update target for next loop iteration.
294+ authority = new_uri
295+ . authority ( )
296+ . cloned ( )
297+ . ok_or ( HttpClientError :: Uri ( "bad redirect authority" ) ) ?;
298+ current_uri = new_uri;
299+ }
300+ }
301+ }
302+
303+ fn is_redirect ( status : StatusCode ) -> bool {
304+ matches ! (
305+ status,
306+ StatusCode :: MOVED_PERMANENTLY
307+ | StatusCode :: FOUND
308+ | StatusCode :: SEE_OTHER
309+ | StatusCode :: TEMPORARY_REDIRECT
310+ | StatusCode :: PERMANENT_REDIRECT
311+ )
312+ }
313+
314+ fn resolve_location ( base : & Uri , location : & http:: HeaderValue ) -> Result < Uri , HttpClientError > {
315+ let loc_str = location
316+ . to_str ( )
317+ . map_err ( |_| HttpClientError :: Uri ( "bad location header" ) ) ?;
318+
319+ // Absolute URI
320+ if let Ok ( uri) = loc_str. parse :: < Uri > ( ) {
321+ if uri. scheme ( ) . is_some ( ) && uri. authority ( ) . is_some ( ) {
322+ return Ok ( uri) ;
323+ }
324+ }
325+
326+ // Relative forms
327+ if loc_str. starts_with ( '/' ) {
328+ // absolute-path reference
329+ let mut builder = Uri :: builder ( ) ;
330+ if let Some ( scheme) = base. scheme ( ) {
331+ builder = builder. scheme ( scheme. clone ( ) ) ;
332+ }
333+ if let Some ( auth) = base. authority ( ) {
334+ builder = builder. authority ( auth. clone ( ) ) ;
335+ }
336+ builder
337+ . path_and_query ( loc_str)
338+ . build ( )
339+ . map_err ( |_| HttpClientError :: Uri ( "bad redirect path" ) )
340+ } else {
341+ // path-relative; join with base path directory
342+ let base_path = base. path_and_query ( ) . map ( |pq| pq. as_str ( ) ) . unwrap_or ( "/" ) ;
343+ let joined = if let Some ( idx) = base_path. rfind ( '/' ) {
344+ format ! ( "{}{}" , & base_path[ ..=idx] , loc_str)
345+ } else {
346+ format ! ( "/{}" , loc_str)
347+ } ;
348+ let mut builder = Uri :: builder ( ) ;
349+ if let Some ( scheme) = base. scheme ( ) {
350+ builder = builder. scheme ( scheme. clone ( ) ) ;
351+ }
352+ if let Some ( auth) = base. authority ( ) {
353+ builder = builder. authority ( auth. clone ( ) ) ;
354+ }
355+ builder
356+ . path_and_query ( joined. as_str ( ) )
357+ . build ( )
358+ . map_err ( |_| HttpClientError :: Uri ( "bad relative redirect path" ) )
182359 }
183360}
0 commit comments