From 1b7c72f870485d612bfb01d7b7c21f6756282b5b Mon Sep 17 00:00:00 2001 From: Marc Brinkmann Date: Thu, 18 Jan 2024 18:12:50 +0100 Subject: [PATCH 1/6] Make `quick-test.sh` curl the `testing/hello` page --- quick-test.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/quick-test.sh b/quick-test.sh index 2c57940..9ef87a9 100755 --- a/quick-test.sh +++ b/quick-test.sh @@ -12,3 +12,7 @@ podman login --tls-verify=false --username devuser --password devpw http://${REG podman pull crccheck/hello-world podman tag crccheck/hello-world ${REGISTRY_ADDR}/testing/hello:prod podman push --tls-verify=false ${REGISTRY_ADDR}/testing/hello:prod + +sleep 2 + +curl -v http://${REGISTRY_ADDR}/testing/hello From ecb474d00eefbd2ccc48e98ec6c43f3e15304f82 Mon Sep 17 00:00:00 2001 From: Marc Brinkmann Date: Thu, 18 Jan 2024 19:54:58 +0100 Subject: [PATCH 2/6] Inject `X-Script-Name` --- src/reverse_proxy.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/reverse_proxy.rs b/src/reverse_proxy.rs index 8b7e960..c8d25f4 100644 --- a/src/reverse_proxy.rs +++ b/src/reverse_proxy.rs @@ -110,6 +110,7 @@ impl PartialEq for Domain { enum Destination { ReverseProxied { uri: Uri, + script_name: Option, config: Arc, }, Internal(Uri), @@ -167,6 +168,7 @@ impl RoutingTable { ); return Destination::ReverseProxied { uri: Uri::from_parts(parts).expect("should not have invalidated Uri"), + script_name: None, config: pc.config().clone(), }; } @@ -200,6 +202,7 @@ impl RoutingTable { return Destination::ReverseProxied { uri: Uri::from_parts(parts).unwrap(), + script_name: Some(image_location.to_string()), config: pc.config().clone(), }; } @@ -330,7 +333,11 @@ async fn route_request( }; match dest_uri { - Destination::ReverseProxied { uri: dest, config } => { + Destination::ReverseProxied { + uri: dest, + script_name, + config, + } => { trace!(%dest, "reverse proxying"); // First, check if http authentication is enabled. @@ -371,6 +378,11 @@ async fn route_request( bld = bld.header(key_string, value_str); } + + if let Some(script_name) = script_name { + bld = bld.header("X-Script-Name", script_name); + } + Ok(bld.body(Body::from(response.bytes().await?)).map_err(|_| { AppError::AssertionFailed("should not fail to construct response") })?) From a8cc0a3496a1817a01e0665ef8f51514e1b72319 Mon Sep 17 00:00:00 2001 From: Marc Brinkmann Date: Fri, 19 Jan 2024 02:02:53 +0100 Subject: [PATCH 3/6] Actually attach script name header to the correct request, add leading slash --- src/reverse_proxy.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/reverse_proxy.rs b/src/reverse_proxy.rs index c8d25f4..3672ecf 100644 --- a/src/reverse_proxy.rs +++ b/src/reverse_proxy.rs @@ -202,7 +202,7 @@ impl RoutingTable { return Destination::ReverseProxied { uri: Uri::from_parts(parts).unwrap(), - script_name: Some(image_location.to_string()), + script_name: Some(format!("/{}", image_location)), config: pc.config().clone(), }; } @@ -363,7 +363,15 @@ async fn route_request( let method = request.method().to_string().parse().map_err(|_| { AppError::AssertionFailed("method http version mismatch workaround failed") })?; - let response = rp.client.request(method, dest.to_string()).send().await; + + let mut req = rp.client.request(method, dest.to_string()); + + // Attach script name. + if let Some(script_name) = script_name { + req = req.header("X-Script-Name", script_name); + }; + + let response = req.send().await; match response { Ok(response) => { @@ -379,10 +387,6 @@ async fn route_request( bld = bld.header(key_string, value_str); } - if let Some(script_name) = script_name { - bld = bld.header("X-Script-Name", script_name); - } - Ok(bld.body(Body::from(response.bytes().await?)).map_err(|_| { AppError::AssertionFailed("should not fail to construct response") })?) From 4fcfbe5bce7c2d0906156afb5710f2cf5f8b6bf6 Mon Sep 17 00:00:00 2001 From: Marc Brinkmann Date: Fri, 19 Jan 2024 02:10:06 +0100 Subject: [PATCH 4/6] Include leading slash when constructing remainder --- Cargo.lock | 16 ---------------- Cargo.toml | 1 - src/reverse_proxy.rs | 8 ++++++-- 3 files changed, 6 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8ac71a3..2d5c66d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -224,12 +224,6 @@ dependencies = [ "crypto-common", ] -[[package]] -name = "either" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" - [[package]] name = "encoding_rs" version = "0.8.33" @@ -621,15 +615,6 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" -[[package]] -name = "itertools" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25db6b064527c5d482d0423354fcd07a89a2dfe07b67892e62411946db7f07b0" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "1.0.9" @@ -975,7 +960,6 @@ dependencies = [ "gethostname", "hex", "http-body-util", - "itertools", "nom", "reqwest", "sec", diff --git a/Cargo.toml b/Cargo.toml index a01dc26..85cdf53 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,6 @@ constant_time_eq = "0.3.0" futures = "0.3.29" gethostname = "0.4.3" hex = "0.4.3" -itertools = "0.12.0" nom = "7.1.3" reqwest = { version = "0.11.23", default-features = false } sec = { version = "1.0.0", features = [ "deserialize", "serialize" ] } diff --git a/src/reverse_proxy.rs b/src/reverse_proxy.rs index 3672ecf..a133ec0 100644 --- a/src/reverse_proxy.rs +++ b/src/reverse_proxy.rs @@ -17,7 +17,6 @@ use axum::{ response::{IntoResponse, Response}, RequestExt, Router, }; -use itertools::Itertools; use tokio::sync::RwLock; use tracing::{info, trace, warn}; @@ -318,7 +317,12 @@ fn split_path_base_url(uri: &Uri) -> Option<(ImageLocation, String)> { // Now create the path, format is: '' / repository / image / ... // Need to skip the first three. - let remainder = segments.join("/"); + let mut remainder = String::new(); + + segments.for_each(|segment| { + remainder.push('/'); + remainder.push_str(segment); + }); Some((image_location, remainder)) } From 59d0e5fe9dc7c4657ad4ddddc1f9472868c10035 Mon Sep 17 00:00:00 2001 From: Marc Brinkmann Date: Fri, 19 Jan 2024 02:21:16 +0100 Subject: [PATCH 5/6] Properly pass through headers in reverse proxy --- src/reverse_proxy.rs | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/reverse_proxy.rs b/src/reverse_proxy.rs index a133ec0..7dc97b4 100644 --- a/src/reverse_proxy.rs +++ b/src/reverse_proxy.rs @@ -370,6 +370,22 @@ async fn route_request( let mut req = rp.client.request(method, dest.to_string()); + for (name, value) in request.headers() { + let name: reqwest::header::HeaderName = if let Ok(name) = name.as_str().parse() { + name + } else { + continue; + }; + + if !BLACKLISTED.contains(&name) && !HOP_BY_HOP.contains(&name) { + if let Ok(value) = value.to_str() { + req = req.header(name, value); + } else { + continue; + } + } + } + // Attach script name. if let Some(script_name) = script_name { req = req.header("X-Script-Name", script_name); @@ -487,7 +503,7 @@ async fn route_request( } /// HTTP/1.1 hop-by-hop headers -mod hop_by_hop { +mod known_headers { use reqwest::header::HeaderName; pub(super) static HOP_BY_HOP: [HeaderName; 8] = [ HeaderName::from_static("keep-alive"), @@ -499,5 +515,7 @@ mod hop_by_hop { HeaderName::from_static("proxy-authorization"), HeaderName::from_static("proxy-authenticate"), ]; + pub(super) static BLACKLISTED: [HeaderName; 1] = [HeaderName::from_static("x-script-name")]; } -use hop_by_hop::HOP_BY_HOP; +use known_headers::BLACKLISTED; +use known_headers::HOP_BY_HOP; From 1981ec2fd1989dad505b2de16d42872818ae5fb0 Mon Sep 17 00:00:00 2001 From: Marc Brinkmann Date: Fri, 19 Jan 2024 02:37:57 +0100 Subject: [PATCH 6/6] Added support for passing through body in reverse proxy --- Cargo.lock | 22 ++++++++++------------ Cargo.toml | 9 +++++++-- src/main.rs | 3 ++- src/reverse_proxy.rs | 16 +++++++++++++++- 4 files changed, 34 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2d5c66d..ccaa27d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -51,9 +51,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "axum" -version = "0.7.1" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "810a80b128d70e6ed2bdf3fe8ed72c0ae56f5f5948d01c2753282dd92a84fce8" +checksum = "1236b4b292f6c4d6dc34604bb5120d85c3fe1d1aa596bd5cc52ca054d13e7b9e" dependencies = [ "async-trait", "axum-core", @@ -62,7 +62,7 @@ dependencies = [ "http 1.0.0", "http-body 1.0.0", "http-body-util", - "hyper 1.0.1", + "hyper 1.1.0", "hyper-util", "itoa", "matchit", @@ -85,9 +85,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.4.0" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de0ddc355eab88f4955090a823715df47acf0b7660aab7a69ad5ce6301ee3b73" +checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" dependencies = [ "async-trait", "bytes", @@ -552,9 +552,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "403f9214f3e703236b221f1a9cd88ec8b4adfa5296de01ab96216361f4692f56" +checksum = "fb5aa53871fc917b1a9ed87b683a5d86db645e23acb32c2e0785a353e522fb75" dependencies = [ "bytes", "futures-channel", @@ -571,21 +571,19 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ca339002caeb0d159cc6e023dff48e199f081e42fa039895c7c6f38b37f2e9d" +checksum = "bdea9aac0dbe5a9240d68cfd9501e2db94222c6dc06843e06640b9e07f0fdc67" dependencies = [ "bytes", "futures-channel", "futures-util", "http 1.0.0", "http-body 1.0.0", - "hyper 1.0.1", + "hyper 1.1.0", "pin-project-lite", "socket2", "tokio", - "tower", - "tower-service", "tracing", ] diff --git a/Cargo.toml b/Cargo.toml index 85cdf53..3f2bf4b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ license = "MIT" [dependencies] anyhow = "1.0.75" -axum = { version = "0.7.1", features = [ "tracing" ] } +axum = { version = "0.7.4", features = [ "tracing" ] } base64 = "0.21.5" constant_time_eq = "0.3.0" futures = "0.3.29" @@ -22,7 +22,12 @@ serde_json = "1.0.108" sha2 = "0.10.8" tempfile = "3.9.0" thiserror = "1.0.50" -tokio = { version = "1.34.0", features = [ "rt-multi-thread", "macros", "fs", "process" ] } +tokio = { version = "1.34.0", features = [ + "rt-multi-thread", + "macros", + "fs", + "process", +] } tokio-util = { version = "0.7.10", features = [ "io" ] } toml = "0.8.8" tower-http = { version = "0.5.0", features = [ "trace" ] } diff --git a/src/main.rs b/src/main.rs index eeac2bb..a548934 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,7 +10,7 @@ use std::{ }; use anyhow::Context; -use axum::Router; +use axum::{extract::DefaultBodyLimit, Router}; use gethostname::gethostname; use registry::ContainerRegistry; @@ -86,6 +86,7 @@ async fn main() -> anyhow::Result<()> { let app = Router::new() .merge(registry.make_router()) .merge(reverse_proxy.make_router()) + .layer(DefaultBodyLimit::max(1024 * 1024)) // See #43. .layer(TraceLayer::new_for_http()); let listener = tokio::net::TcpListener::bind(cfg.reverse_proxy.http_bind) diff --git a/src/reverse_proxy.rs b/src/reverse_proxy.rs index 7dc97b4..1170200 100644 --- a/src/reverse_proxy.rs +++ b/src/reverse_proxy.rs @@ -222,6 +222,7 @@ enum AppError { status: StatusCode, }, InvalidPayload, + BodyReadError(axum::Error), Internal(anyhow::Error), } @@ -235,6 +236,7 @@ impl Display for AppError { AppError::NonUtf8Header => f.write_str("a header contained non-utf8 data"), AppError::AuthFailure { .. } => f.write_str("authentication missing or not present"), AppError::InvalidPayload => f.write_str("invalid payload"), + AppError::BodyReadError(err) => write!(f, "could not read body: {}", err), AppError::Internal(err) => Display::fmt(err, f), } } @@ -264,6 +266,8 @@ impl IntoResponse for AppError { .body(Body::empty()) .expect("should never fail to build auth failure response"), AppError::InvalidPayload => StatusCode::BAD_REQUEST.into_response(), + // TODO: Could probably be more specific here instead of just `BAD_REQUEST`: + AppError::BodyReadError(_) => StatusCode::BAD_REQUEST.into_response(), AppError::Internal(err) => { (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response() } @@ -391,6 +395,15 @@ async fn route_request( req = req.header("X-Script-Name", script_name); }; + // Retrieve body. + let request_body = axum::body::to_bytes( + request.into_limited_body(), + 1024 * 1024, // See #43. + ) + .await + .map_err(AppError::BodyReadError)?; + req = req.body(request_body); + let response = req.send().await; match response { @@ -407,7 +420,8 @@ async fn route_request( bld = bld.header(key_string, value_str); } - Ok(bld.body(Body::from(response.bytes().await?)).map_err(|_| { + let body = response.bytes().await?; + Ok(bld.body(Body::from(body)).map_err(|_| { AppError::AssertionFailed("should not fail to construct response") })?) }