Skip to content

Commit 1acf361

Browse files
committed
Refactor HttpClient request handling to support redirect logic.
1 parent 013f8f6 commit 1acf361

File tree

1 file changed

+221
-44
lines changed

1 file changed

+221
-44
lines changed

src/net/http.rs

Lines changed: 221 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,17 @@ use core::ptr::NonNull;
99
use std::io;
1010

1111
use 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};
1414
use http_body::Body;
1515
use http_body_util::BodyExt;
1616
use nginx_sys::{ngx_log_t, ngx_resolver_t, NGX_LOG_WARN};
1717
use ngx::allocator::Box;
1818
use ngx::async_::resolver::Resolver;
1919
use ngx::async_::spawn;
2020
use ngx::ngx_log_error;
21+
use std::format;
22+
use std::string::ToString;
2123
use thiserror::Error;
2224

2325
use 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)]
4449
pub trait HttpClient {
4550
type Error: StdError + Send + Sync + 'static;
@@ -101,26 +106,41 @@ impl<'a> NgxHttpClient<'a> {
101106
impl 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

Comments
 (0)