This commit is contained in:
Mateusz Gruszczyński committed 2026-10-04 11:15:58 +02:00
1 parent 87e571c170
commit fa7abc9e53
308 files changed
+49746 -50237

No files matched your search

+366
View File
@@ -0,0 +1,366 @@
use super::*;
use crate::{
config::{self, Config, ValidationPolicy},
db::Store,
};
use axum::body::Body;
use http::Request;
use tower::ServiceExt;
async fn fixture() -> (Router, Store, tempfile::TempDir, String, String) {
fixture_with_config(Config::default()).await
}
async fn fixture_with_config(
config: Config,
) -> (Router, Store, tempfile::TempDir, String, String) {
let dir = tempfile::tempdir().unwrap();
let policy = ValidationPolicy {
tls_addr: None,
cert_dir: None,
data_dir: None,
proxy_addr: "127.0.0.1:8080".parse().unwrap(),
admin_addr: "127.0.0.1:9090".parse().unwrap(),
};
let store = Store::open(&dir.path().join("db"), &config).await.unwrap();
store
.initialize_admin("owner", "unused-test-hash", "test")
.await
.unwrap();
let auth = auth::Auth::new(store.clone(), dir.path().to_path_buf())
.await
.unwrap();
let issued = store
.create_session(1, "browser", None)
.await
.unwrap()
.unwrap();
let snapshot = config::compile(config, 0, &policy).unwrap();
let state = App::new(
store.clone(),
snapshot,
policy,
auth,
10,
Duration::from_secs(30),
Duration::from_secs(30),
Duration::from_secs(300),
);
(
router(state),
store,
dir,
issued.secret,
issued.session.csrf,
)
}
async fn fixture_with_certificate_storage(
config: Config,
) -> (Router, Store, tempfile::TempDir, String, String) {
let dir = tempfile::tempdir().unwrap();
let certificate_directory = dir.path().join("certs");
crate::tls::initialize_default(&certificate_directory).unwrap();
let policy = ValidationPolicy {
tls_addr: None,
cert_dir: Some(certificate_directory),
data_dir: None,
proxy_addr: "127.0.0.1:8080".parse().unwrap(),
admin_addr: "127.0.0.1:9090".parse().unwrap(),
};
let store = Store::open(&dir.path().join("db"), &config).await.unwrap();
store
.initialize_admin("owner", "unused-test-hash", "test")
.await
.unwrap();
let auth = auth::Auth::new(store.clone(), dir.path().to_path_buf())
.await
.unwrap();
let issued = store
.create_session(1, "browser", None)
.await
.unwrap()
.unwrap();
let snapshot = config::compile(config, 0, &policy).unwrap();
let state = App::new(
store.clone(),
snapshot,
policy,
auth,
10,
Duration::from_secs(30),
Duration::from_secs(30),
Duration::from_secs(300),
);
(
router(state),
store,
dir,
issued.secret,
issued.session.csrf,
)
}
fn request(path: &str) -> http::request::Builder {
Request::builder()
.uri(path)
.header("host", "127.0.0.1:9090")
}
#[test]
fn administration_authority_uses_http2_pseudo_authority_without_legacy_host() {
let request = Request::builder()
.version(Version::HTTP_2)
.uri("https://192.0.2.10:9090/healthz")
.body(Body::empty())
.unwrap();
assert_eq!(
admin_request_authority(&request, true).unwrap(),
"192.0.2.10:9090"
);
let conflicting = Request::builder()
.version(Version::HTTP_2)
.uri("https://192.0.2.10:9090/healthz")
.header(HOST, "panel.example.test:9090")
.body(Body::empty())
.unwrap();
assert!(admin_request_authority(&conflicting, true).is_err());
let h1_absolute = Request::builder()
.uri("https://192.0.2.10:9090/healthz")
.header(HOST, "192.0.2.10:9090")
.body(Body::empty())
.unwrap();
assert!(admin_request_authority(&h1_absolute, true).is_err());
}
#[tokio::test]
async fn administration_https_health_accepts_http2_authority_without_host_header() {
let (router, _store, _dir, _secret, _csrf) = fixture().await;
let request = Request::builder()
.version(Version::HTTP_2)
.uri("https://127.0.0.1:9090/healthz")
.extension(Transport { tls: true })
.body(Body::empty())
.unwrap();
assert_eq!(
router.oneshot(request).await.unwrap().status(),
StatusCode::OK
);
}
#[test]
fn tls_admin_accepts_dns_without_allowlist_but_keeps_explicit_lists_strict() {
let wildcard: std::net::SocketAddr = "0.0.0.0:9090".parse().unwrap();
assert!(admin_host_allowed("127.0.0.1:9090", wildcard, true, &[]));
assert!(admin_host_allowed("192.0.2.10:9090", wildcard, true, &[]));
assert!(admin_host_allowed(
"example.test:9090",
wildcard,
true,
&["example.test".into()]
));
assert!(admin_host_allowed("example.test:9090", wildcard, true, &[]));
assert!(!admin_host_allowed(
"example.test:9090",
wildcard,
false,
&[]
));
assert!(!admin_host_allowed("0.0.0.0:9091", wildcard, true, &[]));
assert!(!admin_host_allowed("0.0.0.0:9090", wildcard, true, &[]));
assert!(admin_host_allowed("localhost:9090", wildcard, true, &[]));
let concrete: std::net::SocketAddr = "192.0.2.10:9090".parse().unwrap();
assert!(admin_host_allowed("localhost:9090", concrete, true, &[]));
assert!(admin_host_allowed(
"localhost:9090",
concrete,
true,
&["localhost".into()]
));
assert!(!admin_host_allowed(
"other.test:9090",
concrete,
true,
&["localhost".into()]
));
}
#[test]
fn reverse_proxy_dns_allowlist_uses_external_port_and_keeps_direct_port_available() {
let wildcard: std::net::SocketAddr = "0.0.0.0:9090".parse().unwrap();
let allowed = vec!["panel.example.test".to_owned()];
assert!(admin_host_allowed(
"panel.example.test",
wildcard,
false,
&allowed
));
assert!(admin_host_allowed(
"panel.example.test:443",
wildcard,
true,
&allowed
));
assert!(admin_host_allowed(
"panel.example.test:8443",
wildcard,
true,
&allowed
));
assert!(admin_host_allowed(
"192.0.2.10:9090",
wildcard,
false,
&allowed
));
assert!(admin_host_allowed(
"localhost:9090",
wildcard,
false,
&allowed
));
assert!(!admin_host_allowed(
"192.0.2.10:443",
wildcard,
true,
&allowed
));
assert!(!admin_host_allowed(
"other.example.test:443",
wildcard,
true,
&allowed
));
}
#[test]
fn origin_matching_normalizes_default_ports_without_weakening_host_checks() {
assert!(admin_origin_matches(
"https://panel.example.test",
"https",
"panel.example.test:443"
));
assert!(admin_origin_matches(
"https://PANEL.EXAMPLE.TEST:443/",
"https",
"panel.example.test"
));
assert!(!admin_origin_matches(
"https://panel.example.test:8443",
"https",
"panel.example.test"
));
assert!(!admin_origin_matches(
"http://panel.example.test",
"https",
"panel.example.test"
));
}
#[tokio::test]
async fn trusted_https_reverse_proxy_accepts_external_origin_and_refreshes_secure_cookie() {
let mut config = Config::default();
config.administration.allowed_hosts = vec!["panel.example.test".into()];
let (router, store, _dir, secret, _) = fixture_with_config(config).await;
let response = router
.oneshot(
Request::builder()
.uri("/api/status")
.header("host", "127.0.0.1:9090")
.header("x-forwarded-host", "panel.example.test")
.header("x-forwarded-proto", "https")
.header("origin", "https://panel.example.test")
.header("cookie", format!("{}={secret}", auth::COOKIE_NAME))
.extension(PeerAddr("127.0.0.1:45000".parse().unwrap()))
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let refreshed = response
.headers()
.get_all(SET_COOKIE)
.iter()
.filter_map(|value| value.to_str().ok())
.find(|value| value.starts_with(auth::COOKIE_NAME))
.expect("session cookie refreshed");
assert!(refreshed.contains("; Secure"));
store.close().await;
}
#[tokio::test]
async fn https_redirect_router_preserves_authority_path_and_query() {
let request = Request::builder()
.uri("/api/status?view=compact")
.header("host", "panel.example.test:9090")
.body(Body::empty())
.unwrap();
let response = https_redirect_router().oneshot(request).await.unwrap();
assert_eq!(response.status(), StatusCode::PERMANENT_REDIRECT);
assert_eq!(
response.headers().get(http::header::LOCATION).unwrap(),
"https://panel.example.test:9090/api/status?view=compact"
);
assert_eq!(
response.headers().get(http::header::CACHE_CONTROL).unwrap(),
"no-store"
);
}
#[tokio::test]
async fn status_exposes_runtime_memory_accounting_and_cold_limits() {
let (router, store, _dir, secret, _) = fixture().await;
let response = router
.oneshot(
request("/api/status")
.header("cookie", format!("{}={secret}", auth::COOKIE_NAME))
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = json_body(response).await;
assert_eq!(body["transform_resources"]["memory_budget_bytes"], 128 * 1024 * 1024);
assert_eq!(body["transform_resources"]["max_inflight"], 32);
assert_eq!(body["memory_accounting"]["transform_budget_bytes"], 128 * 1024 * 1024);
assert_eq!(body["memory_accounting"]["cache_hot_limit_bytes"], 256 * 1024 * 1024);
assert_eq!(body["cache"]["cold_limit_bytes"], 4_u64 * 1024 * 1024 * 1024);
assert_eq!(body["cache"]["cold_limit_entries"], 50_000);
assert_eq!(body["memory_accounting"]["non_cache_rss_includes_transform"], true);
store.close().await;
}
#[tokio::test]
async fn rejects_legacy_token_rebinding_and_cross_origin() {
let (router, store, _dir, secret, _) = fixture().await;
let old = request("/api/status")
.header("authorization", format!("Bearer {}", "a".repeat(64)))
.body(Body::empty())
.unwrap();
assert_eq!(
router.clone().oneshot(old).await.unwrap().status(),
StatusCode::UNAUTHORIZED
);
let evil = Request::builder()
.uri("/healthz")
.header("host", "evil.test:9090")
.body(Body::empty())
.unwrap();
assert_eq!(
router.clone().oneshot(evil).await.unwrap().status(),
StatusCode::FORBIDDEN
);
let req = request("/api/status")
.header("cookie", format!("{}={secret}", auth::COOKIE_NAME))
.header("origin", "http://evil.test")
.body(Body::empty())
.unwrap();
assert_eq!(
router.clone().oneshot(req).await.unwrap().status(),
StatusCode::FORBIDDEN
);
let req = request("/api/status")
.header("cookie", format!("{}={secret}", auth::COOKIE_NAME))
.body(Body::empty())
.unwrap();
assert_eq!(router.oneshot(req).await.unwrap().status(), StatusCode::OK);
store.close().await;
}