Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,5 @@ tokio-rustls = "0.26"
tower-service = "0.3"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
uuid = { version = "1.18", features = ["v7"] }
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
webpki = { package = "rustls-webpki", version = "0.103.11", default-features = false, features = ["std", "aws-lc-rs"] }
12 changes: 8 additions & 4 deletions crates/rginx-app/tests/phase1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -267,11 +267,15 @@ fn parse_http_response(bytes: &[u8]) -> Result<ParsedResponse, String> {

fn assert_generated_request_id(value: Option<&str>) {
let value = value.expect("response should include x-request-id");
assert_eq!(value.len(), "rginx-0000000000000000".len());
assert!(value.starts_with("rginx-"), "generated request id should use the rginx- prefix");
assert_eq!(value.len(), 36, "generated request id should be a UUIDv7");
assert_eq!(value.as_bytes()[8], b'-');
assert_eq!(value.as_bytes()[13], b'-');
assert_eq!(value.as_bytes()[14], b'7', "generated request id should be UUIDv7");
assert_eq!(value.as_bytes()[18], b'-');
assert_eq!(value.as_bytes()[23], b'-');
assert!(
value["rginx-".len()..].chars().all(|ch| ch.is_ascii_hexdigit()),
"generated request id should end with lowercase hex digits, got {value:?}"
value.chars().filter(|ch| *ch != '-').all(|ch| ch.is_ascii_hexdigit()),
"generated request id should use UUID hex digits, got {value:?}"
);
}

Expand Down
42 changes: 41 additions & 1 deletion crates/rginx-app/tests/upstream_http2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const TEST_SERVER_KEY_PEM: &str = "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkq
struct ObservedRequest {
version: Version,
path: String,
authority: Option<String>,
alpn_protocol: Option<String>,
}

Expand Down Expand Up @@ -63,6 +64,37 @@ async fn proxies_to_https_upstreams_over_http2_when_alpn_negotiates_h2() {
fs::remove_dir_all(upstream_temp_dir).expect("upstream temp dir should be removed");
}

#[tokio::test(flavor = "multi_thread")]
async fn preserves_hostname_authority_when_h2_upstream_dials_resolved_ip() {
let (upstream_addr, observed_rx, upstream_task, upstream_temp_dir) = spawn_h2_upstream().await;

let listen_addr = reserve_loopback_addr();
let upstream_url = format!("https://localhost:{}", upstream_addr.port());
let mut server = ServerHarness::spawn("rginx-upstream-h2-authority", |_| {
proxy_config_for_upstream_url(listen_addr, &upstream_url)
});
server.wait_for_http_ready(listen_addr, Duration::from_secs(5));

let (status, body) = fetch_text_response(listen_addr, "/")
.expect("rginx should return a successful upstream response");
assert_eq!(status, 200);
assert_eq!(body, "upstream h2 ok\n");

let observed = tokio::time::timeout(Duration::from_secs(5), observed_rx)
.await
.expect("upstream request should be observed before timeout")
.expect("upstream observation channel should complete");
assert_eq!(observed.version, Version::HTTP_2);
assert_eq!(observed.path, "/");
assert_eq!(observed.alpn_protocol.as_deref(), Some("h2"));
let expected_authority = upstream_url.strip_prefix("https://").unwrap();
assert_eq!(observed.authority.as_deref(), Some(expected_authority));

server.shutdown_and_wait(Duration::from_secs(5));
upstream_task.await.expect("upstream h2 server task should finish");
fs::remove_dir_all(upstream_temp_dir).expect("upstream temp dir should be removed");
}

async fn spawn_h2_upstream()
-> (SocketAddr, oneshot::Receiver<ObservedRequest>, JoinHandle<()>, PathBuf) {
let temp_dir = temp_dir("rginx-upstream-h2");
Expand Down Expand Up @@ -109,6 +141,7 @@ async fn spawn_h2_upstream()
let _ = sender.send(ObservedRequest {
version: request.version(),
path: request.uri().path().to_string(),
authority: request.uri().authority().map(|value| value.to_string()),
alpn_protocol,
});
}
Expand All @@ -133,10 +166,17 @@ async fn spawn_h2_upstream()
}

fn proxy_config(listen_addr: SocketAddr, upstream_addr: SocketAddr) -> String {
proxy_config_for_upstream_url(
listen_addr,
&format!("https://127.0.0.1:{}", upstream_addr.port()),
)
}

fn proxy_config_for_upstream_url(listen_addr: SocketAddr, upstream_url: &str) -> String {
format!(
"Config(\n runtime: RuntimeConfig(\n shutdown_timeout_secs: 2,\n ),\n server: ServerConfig(\n listen: {:?},\n ),\n upstreams: [\n UpstreamConfig(\n name: \"backend\",\n peers: [\n UpstreamPeerConfig(\n url: {:?},\n ),\n ],\n tls: Some(Insecure),\n protocol: Auto,\n server_name_override: Some(\"localhost\"),\n ),\n ],\n locations: [\n{ready_route} LocationConfig(\n matcher: Exact(\"/\"),\n handler: Proxy(\n upstream: \"backend\",\n ),\n ),\n ],\n)\n",
listen_addr.to_string(),
format!("https://127.0.0.1:{}", upstream_addr.port()),
upstream_url,
ready_route = READY_ROUTE_CONFIG,
)
}
Expand Down
3 changes: 2 additions & 1 deletion crates/rginx-config/src/compile/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,9 @@ pub fn compile_with_base(raw: Config, base_dir: impl AsRef<Path>) -> Result<Conf
let compiled_server = server::compile_legacy_server(server, base_dir, any_vhost_tls)?;
(vec![compiled_server.listener.clone()], compiled_server.server_names)
} else {
let default_server_header = server.server_header;
let default_server_names = server.server_names;
let listeners = server::compile_listeners(raw_listeners, base_dir)?;
let listeners = server::compile_listeners(raw_listeners, default_server_header, base_dir)?;
(listeners, default_server_names)
};
let upstreams = upstream::compile_upstreams(raw_upstreams, base_dir)?;
Expand Down
25 changes: 21 additions & 4 deletions crates/rginx-config/src/compile/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ use std::net::IpAddr;
use std::path::Path;
use std::time::Duration;

use http::HeaderValue;
use ipnet::IpNet;
use rginx_core::{
AccessLogFormat, Error, Listener, ListenerHttp3, OcspConfig, OcspNonceMode,
OcspResponderPolicy, Result, Server, ServerCertificateBundle, ServerClientAuthMode,
ServerClientAuthPolicy, ServerTls, TlsCipherSuite, TlsKeyExchangeGroup, TlsVersion,
VirtualHostTls,
AccessLogFormat, DEFAULT_SERVER_HEADER, Error, Listener, ListenerHttp3, OcspConfig,
OcspNonceMode, OcspResponderPolicy, Result, Server, ServerCertificateBundle,
ServerClientAuthMode, ServerClientAuthPolicy, ServerTls, TlsCipherSuite, TlsKeyExchangeGroup,
TlsVersion, VirtualHostTls,
};

use crate::model::{
Expand Down Expand Up @@ -36,6 +37,7 @@ pub(super) fn compile_legacy_server(
) -> Result<CompiledServer> {
let ServerConfig {
listen,
server_header,
proxy_protocol,
default_certificate,
server_names,
Expand All @@ -56,6 +58,7 @@ pub(super) fn compile_legacy_server(
let compiled = compile_server_fields(
ServerFieldConfig {
listen,
server_header,
default_certificate,
trusted_proxies,
keep_alive,
Expand Down Expand Up @@ -93,6 +96,7 @@ pub(super) fn compile_legacy_server(
/// Compiles explicit listener blocks into runtime listener definitions.
pub(super) fn compile_listeners(
listeners: Vec<ListenerConfig>,
default_server_header: Option<String>,
base_dir: &Path,
) -> Result<Vec<Listener>> {
listeners
Expand All @@ -101,6 +105,7 @@ pub(super) fn compile_listeners(
let ListenerConfig {
name,
listen,
server_header,
proxy_protocol,
default_certificate,
trusted_proxies,
Expand All @@ -119,6 +124,7 @@ pub(super) fn compile_listeners(
let compiled = compile_server_fields(
ServerFieldConfig {
listen,
server_header: server_header.or_else(|| default_server_header.clone()),
default_certificate,
trusted_proxies,
keep_alive,
Expand Down Expand Up @@ -289,6 +295,7 @@ struct CompiledServerFields {

struct ServerFieldConfig {
listen: String,
server_header: Option<String>,
default_certificate: Option<String>,
trusted_proxies: Vec<String>,
keep_alive: Option<bool>,
Expand All @@ -309,6 +316,7 @@ fn compile_server_fields(
) -> Result<CompiledServerFields> {
let ServerFieldConfig {
listen,
server_header,
default_certificate,
trusted_proxies,
keep_alive,
Expand All @@ -326,6 +334,7 @@ fn compile_server_fields(
Ok(CompiledServerFields {
server: Server {
listen_addr: listen.parse()?,
server_header: compile_server_header(server_header)?,
default_certificate: compile_default_certificate(default_certificate),
trusted_proxies: compile_trusted_proxies(trusted_proxies)?,
keep_alive: keep_alive.unwrap_or(true),
Expand All @@ -342,6 +351,14 @@ fn compile_server_fields(
})
}

/// Parses the configured `Server` response header value, defaulting to `rginx`.
fn compile_server_header(server_header: Option<String>) -> Result<HeaderValue> {
let value = server_header.unwrap_or_else(|| DEFAULT_SERVER_HEADER.to_string());
let value = value.trim();
HeaderValue::from_str(value)
.map_err(|error| Error::Config(format!("server_header `{value}` is invalid: {error}")))
}

/// Normalizes the optional default certificate server name.
fn compile_default_certificate(default_certificate: Option<String>) -> Option<String> {
default_certificate.map(|name| name.trim().to_lowercase())
Expand Down
Loading
Loading