@@ -9,17 +9,15 @@ use core::ptr::NonNull;
99use std:: io;
1010
1111use bytes:: Bytes ;
12- use http:: uri:: { PathAndQuery , Scheme } ;
13- use http:: { Method , Request , Response , StatusCode , Uri } ;
12+ use http:: uri:: Scheme ;
13+ use http:: { Request , Response } ;
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 ;
2321use thiserror:: Error ;
2422
2523use super :: peer_conn:: PeerConnection ;
@@ -42,9 +40,6 @@ const NGX_ACME_USER_AGENT: &str = constcat::concat!(
4240 NGINX_VER ,
4341) ;
4442
45- // Maximum number of redirects to follow for a single request.
46- const MAX_REDIRECTS : usize = 10 ;
47-
4843#[ allow( async_fn_in_trait) ]
4944pub trait HttpClient {
5045 type Error : StdError + Send + Sync + ' static ;
@@ -106,41 +101,26 @@ impl<'a> NgxHttpClient<'a> {
106101impl HttpClient for NgxHttpClient < ' _ > {
107102 type Error = HttpClientError ;
108103
109- async fn request < B > ( & self , req : Request < B > ) -> Result < Response < Bytes > , Self :: Error >
104+ async fn request < B > ( & self , mut req : Request < B > ) -> Result < Response < Bytes > , Self :: Error >
110105 where
111106 B : Body + Send + ' static ,
112107 <B as Body >:: Data : Send ,
113108 <B as Body >:: Error : StdError + Send + Sync ,
114109 {
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 ( ) ;
110+ let uri = req. uri ( ) . clone ( ) ;
124111
125- // Basic validation of the starting URI.
126- let mut authority = current_uri
112+ let authority = uri
127113 . authority ( )
128- . cloned ( )
129114 . ok_or ( HttpClientError :: Uri ( "missing authority" ) ) ?;
130115
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" ) ) ?;
116+ let path_and_query = uri
117+ . path_and_query ( )
118+ . ok_or ( HttpClientError :: Uri ( "missing path" ) ) ?;
139119
140- parts . uri = Uri :: from ( path_and_query. clone ( ) ) ;
120+ * req . uri_mut ( ) = path_and_query. clone ( ) . into ( ) ;
141121
142- // Build a fresh headers map for each attempt to avoid header leakage across hosts.
143- let mut headers = parts . headers . clone ( ) ;
122+ {
123+ let headers = req . headers_mut ( ) ;
144124 headers. insert (
145125 http:: header:: HOST ,
146126 http:: HeaderValue :: from_str ( authority. as_str ( ) )
@@ -154,207 +134,50 @@ impl HttpClient for NgxHttpClient<'_> {
154134 http:: header:: CONNECTION ,
155135 http:: HeaderValue :: from_static ( "close" ) ,
156136 ) ;
137+ }
157138
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 ( ) ;
188-
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- } ;
195-
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 ?;
200-
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- }
206- }
207-
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 ( ) ;
139+ let ssl = if uri. scheme ( ) == Some ( & Scheme :: HTTPS ) {
140+ Some ( self . ssl . as_ref ( ) )
141+ } else {
142+ None
143+ } ;
221144
222- let resp = sender . send_request ( req ) . await ? ;
145+ let mut peer = Box :: pin ( PeerConnection :: new ( self . log ) ? ) ;
223146
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) ) ;
233- }
147+ peer. as_mut ( )
148+ . connect_to ( authority. as_str ( ) , & self . resolver , ssl)
149+ . await ?;
234150
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) ) ;
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 ( ) ) ;
247155 }
156+ }
248157
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" ) ) ?;
259-
260- let new_uri = resolve_location ( & current_uri, location) ?;
158+ if let Some ( c) = peer. connection_mut ( ) {
159+ c. requests += 1 ;
160+ }
261161
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- }
162+ let ( mut sender, conn) = hyper:: client:: conn:: http1:: handshake ( peer) . await ?;
268163
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- _ => { }
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}" ) ;
291168 }
169+ } )
170+ . detach ( ) ;
292171
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" ) ) ?;
172+ let resp = sender. send_request ( req) . await ?;
173+ let ( parts, body) = resp. into_parts ( ) ;
318174
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- }
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 ( ) ;
325180
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" ) )
181+ Ok ( Response :: from_parts ( parts, body) )
359182 }
360183}
0 commit comments