1use ferryman_edge_core::{Limits, SharedTable};
20use http::{HeaderMap, HeaderValue};
21use http_body_util::{BodyExt, Full, LengthLimitError, Limited};
22use hyper::body::Bytes;
23use hyper::body::Incoming;
24use hyper::{Request, Response};
25use hyper_util::client::legacy::connect::HttpConnector;
26use hyper_util::client::legacy::Client;
27use std::net::IpAddr;
28use std::sync::atomic::{AtomicBool, Ordering};
29use std::sync::Arc;
30use std::time::Duration;
31
32#[cfg(feature = "boxed_body")]
33pub type BoxErr = Box<dyn std::error::Error + Send + Sync>;
34
35#[cfg(not(feature = "boxed_body"))]
36pub type Body = Full<Bytes>;
37#[cfg(feature = "boxed_body")]
38pub type Body = http_body_util::combinators::BoxBody<Bytes, BoxErr>;
39
40const HOP_BY_HOP_HEADERS: &[&str] = &[
41 "connection",
42 "keep-alive",
43 "proxy-connection",
44 "te",
45 "trailer",
46 "transfer-encoding",
47 "upgrade",
48 "proxy-authenticate",
49 "proxy-authorization",
50];
51
52pub(crate) fn strip_hop_by_hop(headers: &mut HeaderMap) {
56 let mut extra: Vec<String> = Vec::new();
57 for v in headers.get_all(http::header::CONNECTION) {
58 if let Ok(s) = v.to_str() {
59 extra.extend(
60 s.split(',')
61 .map(|p| p.trim().to_ascii_lowercase())
62 .filter(|p| !p.is_empty()),
63 );
64 }
65 }
66 for name in HOP_BY_HOP_HEADERS {
67 headers.remove(*name);
68 }
69 for name in extra {
70 headers.remove(name.as_str());
71 }
72}
73
74fn set_forwarded(headers: &mut HeaderMap, ip: IpAddr) {
77 headers.remove("forwarded");
78 headers.remove("x-real-ip");
79 headers.insert(
80 "x-forwarded-for",
81 HeaderValue::from_str(&ip.to_string()).expect("an IP is a valid header value"),
82 );
83 headers.insert("x-forwarded-proto", HeaderValue::from_static("https"));
84}
85
86#[cfg(not(feature = "boxed_body"))]
87pub(crate) fn text_body(bytes: Bytes) -> Body {
88 Full::new(bytes)
89}
90
91#[cfg(feature = "boxed_body")]
92pub(crate) fn text_body(bytes: Bytes) -> Body {
93 Full::new(bytes)
94 .map_err(|never: std::convert::Infallible| -> BoxErr { match never {} })
95 .boxed()
96}
97
98fn plain(status: u16, msg: &'static [u8]) -> anyhow::Result<Response<Body>> {
99 metrics::counter!("ferryman_requests_total", "status" => status.to_string()).increment(1);
100 Ok(Response::builder()
101 .status(status)
102 .body(text_body(Bytes::from_static(msg)))?)
103}
104
105fn bad_path(path: &str) -> bool {
109 let b = path.as_bytes();
110 let (mut start, mut i) = (0, 0);
111 while i <= b.len() {
112 let sep = match b[i..] {
113 [] => Some(0),
114 [b'/' | b'\\', ..] => Some(1),
115 [b'%', b'2', b'f' | b'F', ..] | [b'%', b'5', b'c' | b'C', ..] => Some(3),
116 [b'%', b'0', b'0', ..] | [b'%', b'u' | b'U', ..] => return true,
117 [b'%', b'2', b'5', b'2', b'e' | b'E' | b'f' | b'F', ..]
118 | [b'%', b'2', b'5', b'5', b'c' | b'C', ..] => return true,
119 _ => None,
120 };
121 match sep {
122 Some(n) => {
123 if dot_piece(&b[start..i]) {
124 return true;
125 }
126 i += n.max(1);
127 start = i;
128 }
129 None => i += 1,
130 }
131 }
132 false
133}
134
135fn dot_piece(piece: &[u8]) -> bool {
138 let end = piece.iter().position(|&c| c == b';').unwrap_or(piece.len());
139 let b = &piece[..end];
140 let (mut i, mut dots) = (0, 0);
141 while i < b.len() {
142 match b[i..] {
143 [b'.', ..] => i += 1,
144 [b'%', b'2', b'e' | b'E', ..] => i += 3,
145 _ => return false,
146 }
147 dots += 1;
148 }
149 matches!(dots, 1 | 2)
150}
151
152fn is_client_body_error(e: &(dyn std::error::Error + 'static)) -> bool {
158 error_chain(e).any(|c| {
159 c.is::<LengthLimitError>()
160 || c.downcast_ref::<hyper::Error>()
161 .is_some_and(|h| h.is_user())
162 })
163}
164
165fn error_chain<'a>(
166 e: &'a (dyn std::error::Error + 'static),
167) -> impl Iterator<Item = &'a (dyn std::error::Error + 'static)> {
168 std::iter::successors(Some(e), |c| c.source())
169}
170
171pub(crate) fn wants_upgrade<B>(req: &Request<B>) -> bool {
182 req.method() == http::Method::CONNECT
183 || req
184 .headers()
185 .get(http::header::UPGRADE)
186 .is_some_and(|v| !v.as_bytes().eq_ignore_ascii_case(b"h2c"))
187}
188
189pub async fn handle(
193 table: SharedTable,
194 client: Client<HttpConnector, Body>,
195 req: Request<Incoming>,
196 peer_ip: IpAddr,
197) -> Result<Response<Body>, anyhow::Error> {
198 handle_with(table, client, req, peer_ip, &Limits::default()).await
199}
200
201pub async fn handle_with(
205 table: SharedTable,
206 client: Client<HttpConnector, Body>,
207 req: Request<Incoming>,
208 peer_ip: IpAddr,
209 limits: &Limits,
210) -> Result<Response<Body>, anyhow::Error> {
211 let upgrade = wants_upgrade(&req);
212 handle_checked(table, client, req, peer_ip, upgrade, limits).await
213}
214
215pub(crate) async fn handle_checked(
218 table: SharedTable,
219 client: Client<HttpConnector, Body>,
220 req: Request<Incoming>,
221 peer_ip: IpAddr,
222 upgrade: bool,
223 limits: &Limits,
224) -> Result<Response<Body>, anyhow::Error> {
225 let max_body = limits.max_request_body_bytes as usize;
226 let started = std::time::Instant::now();
227 let snapshot = table.load();
228 let path = req.uri().path().to_string();
229
230 if bad_path(&path) {
233 return plain(400, b"bad path");
234 }
235
236 if upgrade {
239 return plain(501, b"protocol upgrades are not supported");
240 }
241
242 if req
245 .headers()
246 .get(http::header::CONTENT_LENGTH)
247 .and_then(|v| v.to_str().ok())
248 .and_then(|s| s.parse::<u64>().ok())
249 .is_some_and(|len| len > limits.max_request_body_bytes)
250 {
251 return plain(413, b"payload too large");
252 }
253
254 let (mut parts, body) = req.into_parts();
260 let body_timeout = Duration::from_secs(limits.request_body_timeout_secs);
261 let (fwd_body, upload_done) =
262 match tokio::time::timeout(body_timeout, forward_body(body, max_body)).await {
263 Ok(Ok(b)) => b,
264 Ok(Err(e)) if e.downcast_ref::<LengthLimitError>().is_some() => {
265 return plain(413, b"payload too large");
266 }
267 Ok(Err(_)) => return plain(400, b"request body error"),
268 Err(_) => return plain(408, b"request body timeout"),
269 };
270
271 let upstream = match snapshot.lookup(&path) {
272 Some(u) => u.clone(),
273 None if snapshot.has_prefix(&path) => return plain(503, b"upstream unavailable"),
275 None => return plain(404, b"no route"),
276 };
277
278 let mut up_parts = upstream.uri.clone().into_parts();
282 up_parts.path_and_query = parts.uri.path_and_query().cloned();
283 parts.uri = http::Uri::from_parts(up_parts)?;
284 parts.version = http::Version::HTTP_11;
288 if let Some(authority) = upstream.uri.authority() {
289 parts.headers.insert(
290 http::header::HOST,
291 HeaderValue::from_str(authority.as_str())?,
292 );
293 }
294 set_forwarded(&mut parts.headers, peer_ip);
295
296 let now = tokio::time::Instant::now();
300 let deadline = now
301 .checked_add(Duration::from_secs(limits.upstream_timeout_secs))
302 .unwrap_or_else(|| now + Duration::from_secs(86_400 * 365 * 30));
303 let fwd = Request::from_parts(parts, fwd_body);
304
305 let host = upstream
307 .uri
308 .authority()
309 .map_or_else(String::new, |a| a.to_string());
310 let resp = match tokio::time::timeout_at(deadline, client.request(fwd)).await {
311 Ok(Ok(resp)) => resp,
312 Ok(Err(e)) if is_client_body_error(&e) => {
313 let too_large = error_chain(&e).any(|c| c.is::<LengthLimitError>());
314 return if too_large {
315 plain(413, b"payload too large")
316 } else {
317 plain(400, b"request body error")
318 };
319 }
320 Ok(Err(e)) => {
321 tracing::warn!(upstream = %host, error = %e, "upstream request failed");
322 upstream.mark_failed();
323 metrics::counter!("ferryman_requests_total", "status" => "502", "upstream" => host)
324 .increment(1);
325 return Ok(Response::builder()
326 .status(502)
327 .body(text_body(Bytes::from_static(b"bad gateway")))?);
328 }
329 Err(_) => {
330 if upload_done.load(Ordering::Acquire) {
333 upstream.mark_failed();
334 }
335 metrics::counter!("ferryman_requests_total", "status" => "504", "upstream" => host)
336 .increment(1);
337 return Ok(Response::builder()
338 .status(504)
339 .body(text_body(Bytes::from_static(b"upstream timeout")))?);
340 }
341 };
342
343 let status = resp.status();
344 let (mut resp_parts, resp_body) = resp.into_parts();
345 strip_hop_by_hop(&mut resp_parts.headers);
346 resp_parts.version = http::Version::default();
349
350 #[cfg(not(feature = "boxed_body"))]
351 let out_body: Body = match tokio::time::timeout_at(deadline, resp_body.collect()).await {
352 Ok(Ok(collected)) => Full::new(collected.to_bytes()),
353 Ok(Err(_)) | Err(_) => {
354 upstream.mark_failed();
355 metrics::counter!("ferryman_requests_total", "status" => "502", "upstream" => host)
356 .increment(1);
357 return Ok(Response::builder()
358 .status(502)
359 .body(text_body(Bytes::from_static(b"bad gateway")))?);
360 }
361 };
362 #[cfg(feature = "boxed_body")]
363 let out_body: Body = resp_body.map_err(Into::into).boxed();
364
365 if matches!(status.as_u16(), 502..=504) {
369 upstream.mark_failed();
370 } else {
371 upstream.mark_success();
372 }
373 metrics::histogram!("ferryman_request_duration_seconds", "upstream" => host.clone())
374 .record(started.elapsed().as_secs_f64());
375 metrics::counter!(
376 "ferryman_requests_total",
377 "status" => status.as_u16().to_string(),
378 "upstream" => host
379 )
380 .increment(1);
381
382 Ok(Response::from_parts(resp_parts, out_body))
383}
384
385type UploadDone = Arc<AtomicBool>;
396
397#[cfg(not(feature = "boxed_body"))]
398async fn forward_body(body: Incoming, max_body: usize) -> anyhow::Result<(Body, UploadDone)> {
399 match Limited::new(body, max_body).collect().await {
400 Ok(collected) => Ok((
401 Full::new(collected.to_bytes()),
402 Arc::new(AtomicBool::new(true)),
403 )),
404 Err(e) => match e.downcast::<LengthLimitError>() {
405 Ok(too_large) => Err(anyhow::Error::new(*too_large)),
406 Err(other) => Err(anyhow::anyhow!("{other}")),
407 },
408 }
409}
410
411#[cfg(feature = "boxed_body")]
415async fn forward_body(body: Incoming, max_body: usize) -> anyhow::Result<(Body, UploadDone)> {
416 let inner = Limited::new(body, max_body);
417 let done: UploadDone = Arc::new(AtomicBool::new(hyper::body::Body::is_end_stream(&inner)));
420 let body = TrackEnd {
421 inner,
422 done: done.clone(),
423 };
424 Ok((body.boxed(), done))
425}
426
427#[cfg(feature = "boxed_body")]
429struct TrackEnd<B> {
430 inner: B,
431 done: UploadDone,
432}
433
434#[cfg(feature = "boxed_body")]
435impl<B: hyper::body::Body + Unpin> hyper::body::Body for TrackEnd<B> {
436 type Data = B::Data;
437 type Error = B::Error;
438
439 fn poll_frame(
440 mut self: std::pin::Pin<&mut Self>,
441 cx: &mut std::task::Context<'_>,
442 ) -> std::task::Poll<Option<Result<hyper::body::Frame<Self::Data>, Self::Error>>> {
443 let polled = std::pin::Pin::new(&mut self.inner).poll_frame(cx);
444 if matches!(polled, std::task::Poll::Ready(None)) || self.inner.is_end_stream() {
445 self.done.store(true, Ordering::Release);
446 }
447 polled
448 }
449
450 fn is_end_stream(&self) -> bool {
451 self.inner.is_end_stream()
452 }
453
454 fn size_hint(&self) -> hyper::body::SizeHint {
455 self.inner.size_hint()
456 }
457}
458
459#[cfg(test)]
460mod tests {
461 use super::{bad_path, wants_upgrade};
462
463 #[test]
464 fn upgrade_detection() {
465 let get = |u: Option<&str>| {
466 let mut b = http::Request::get("/");
467 if let Some(u) = u {
468 b = b.header("upgrade", u);
469 }
470 b.body(()).unwrap()
471 };
472 assert!(wants_upgrade(
473 &http::Request::connect("example.com:443").body(()).unwrap()
474 ));
475 assert!(wants_upgrade(&get(Some("h2c, websocket"))));
476 assert!(wants_upgrade(&get(Some("websocket"))));
477 assert!(!wants_upgrade(&get(Some("h2c"))));
478 assert!(!wants_upgrade(&get(Some("H2C"))));
479 assert!(!wants_upgrade(&get(None)));
480 }
481
482 #[test]
483 fn dot_segments_are_rejected() {
484 for p in [
485 "/svc-a/../svc-b",
486 "/svc-a/./x",
487 "/svc-a/%2e%2e/svc-b",
488 "/svc-a/%2E/x",
489 "/api/../admin",
490 "/api/%2e%2e/admin",
491 "/api/..%2fadmin",
492 "/api/./../admin",
493 "/api/..",
494 "/api/%2E%2E/x",
495 "/api/%2e%2E/x",
496 "/api/.%2e/x",
497 "/api/..;/admin",
498 "/api/.;x/y",
499 "/api/%2e%2e;/x",
500 "/api/..%5cx",
501 "/api/..%5Cx",
502 "/api/a\\..\\b",
503 "/api/%2e/x",
504 "/..",
505 "/api/a%2f..",
506 "/api/%2e%2e%2f",
507 "/api/..%00",
508 "/api/.%00.",
509 "/api/%u002e%u002e",
510 "/api/%U002e",
511 "/api/%252e%252e",
512 "/api/%252E",
513 "/api/%252f",
514 "/api/%252F",
515 "/api/%255c",
516 "/api/a%00b",
517 "/api/a%5c..",
518 "/api/a%5C..%5Cb",
519 ] {
520 assert!(bad_path(p), "{p}");
521 }
522 }
523
524 #[test]
525 fn legitimate_paths_are_allowed() {
526 for p in [
527 "/svc-a/x",
528 "/svc-a/.hidden",
529 "/svc-a/a..b",
530 "/a..b/",
531 "/.well-known/acme",
532 "/file.tar.gz",
533 "/api/v1.2/x",
534 "/",
535 "/api/...",
536 "/api/a;..",
537 "/api/%2e%2e%2e/x",
538 "/api/%41/x",
539 "/api/.a/x",
540 "/api/a%2/",
541 "/api/v4/projects/group%2Fproject",
542 "/api/queues/%2F/q",
543 "/@scope%2fpkg",
544 "/%2F",
545 "/api/%25",
546 "/api/100%25",
547 "/api/%c0%ae",
548 ] {
549 assert!(!bad_path(p), "{p}");
550 }
551 }
552}