v0.25.1
This commit is contained in:
1 parent
87e571c170
commit
fa7abc9e53
308 files changed
+49746
-50237
No files matched your search
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user