This commit is contained in:
Mateusz Gruszczyński committed 2026-10-10 20:56:36 +02:00
1 parent f213d853e2
commit 76b8bb7ee2
78 files changed
+7750 -5204

No files matched your search

+25
View File
@@ -16,6 +16,10 @@
assert_eq!(response.status(), StatusCode::OK);
let initial = json_body(response).await;
assert_eq!(initial["live_log_columns"].as_array().unwrap().len(), LIVE_LOG_COLUMNS.len());
assert_eq!(initial["refresh"], json!({
"live_enabled":true, "overview_seconds":2, "traffic_seconds":5,
"cache_seconds":5, "system_info_seconds":60
}));
assert_eq!(initial["theme"], "dark");
assert_eq!(initial["language"], "auto");
assert_eq!(initial["ui_scale"], 100);
@@ -50,6 +54,7 @@
assert_eq!(response.status(), StatusCode::OK);
let legacy = json_body(response).await;
assert_eq!(legacy["live_log_columns"], json!(["time", "status"]));
assert_eq!(legacy["refresh"], initial["refresh"]);
assert_eq!(legacy["theme"], "dark");
assert_eq!(legacy["language"], "auto");
assert_eq!(legacy["ui_scale"], 100);
@@ -65,6 +70,7 @@
assert_eq!(legacy["backend_table_descending"], false);
let selected = json!({
"refresh":{"live_enabled":false,"overview_seconds":10,"traffic_seconds":0,"cache_seconds":3,"system_info_seconds":300},
"live_log_columns":["time","request","status","upstream_error"],
"theme":"light",
"language":"pl_pl",
@@ -103,6 +109,20 @@
serde_json::from_str(&store.admin_preferences("owner").await.unwrap().unwrap())
.unwrap();
assert_eq!(saved, selected);
// Preferences must also survive a subsequent authenticated read.
let persisted = router.clone().oneshot(request("/api/preferences")
.header("cookie", &cookie).body(Body::empty()).unwrap()).await.unwrap();
assert_eq!(json_body(persisted).await, selected);
// Wrong allowlist value is rejected even if the rest is valid.
let mut invalid = selected.clone();
invalid["refresh"]["traffic_seconds"] = json!(4);
let rejected = router.clone().oneshot(request("/api/preferences")
.method("PUT").header("cookie", &cookie)
.header("origin", "http://127.0.0.1:9090")
.header("x-rustproxy-request", "1").header("x-csrf-token", &csrf)
.header("content-type", "application/json")
.body(Body::from(invalid.to_string())).unwrap()).await.unwrap();
assert_eq!(rejected.status(), StatusCode::UNPROCESSABLE_ENTITY);
let response = router
.clone()
@@ -327,6 +347,7 @@
let (router, store, _dir, _, _) = fixture().await;
for path in [
"/api/status",
"/api/system-info",
"/api/config",
"/api/config/history",
"/api/backends",
@@ -446,3 +467,7 @@
}
store.close().await;
}
+69
View File
@@ -0,0 +1,69 @@
#[tokio::test]
async fn system_info_and_compact_live_status_reuse_existing_admin_session() {
let (router, store, _dir, secret, _) = fixture().await;
let cookie = format!("{}={secret}", auth::COOKIE_NAME);
let info = router.clone().oneshot(request("/api/system-info")
.header("cookie", &cookie).body(Body::empty()).unwrap()).await.unwrap();
assert_eq!(info.status(), StatusCode::OK);
let info = json_body(info).await;
assert!(info.get("system").is_some());
assert!(info.get("runtime").is_some());
assert!(info.get("memory_accounting").is_some());
assert!(info.get("config").is_none());
let live = router.clone().oneshot(request("/api/status?view=live")
.header("cookie", &cookie).body(Body::empty()).unwrap()).await.unwrap();
assert_eq!(live.status(), StatusCode::OK);
let live = json_body(live).await;
assert!(live.get("metrics").is_some());
assert!(live.get("backend_pools").is_some());
assert!(live["listeners"].get("active_endpoints").is_some());
assert!(live.get("system").is_none());
assert!(live.get("memory_accounting").is_none());
assert!(live.get("administration").is_none());
let complete = router.clone().oneshot(request("/api/status")
.header("cookie", &cookie).body(Body::empty()).unwrap()).await.unwrap();
assert_eq!(complete.status(), StatusCode::OK);
assert!(json_body(complete).await.get("system").is_some());
let invalid = router.clone().oneshot(request("/api/status?view=unknown")
.header("cookie", &cookie).body(Body::empty()).unwrap()).await.unwrap();
assert_eq!(invalid.status(), StatusCode::BAD_REQUEST);
let bad_sse = router.oneshot(request("/api/events?interval=4")
.header("cookie", &cookie).body(Body::empty()).unwrap()).await.unwrap();
assert_eq!(bad_sse.status(), StatusCode::UNPROCESSABLE_ENTITY);
store.close().await;
}
#[test]
fn defaults_and_partial_legacy_refresh_documents() {
let previous = serde_json::to_value(AdminPreferences::default()).unwrap();
let mut legacy = previous.as_object().unwrap().clone();
legacy.remove("refresh");
let loaded: AdminPreferences = serde_json::from_value(Value::Object(legacy)).unwrap();
assert_eq!(loaded.refresh, RefreshPreferences::default());
let partial: RefreshPreferences = serde_json::from_str(r#"{"traffic_seconds":0}"#).unwrap();
assert_eq!(partial.traffic_seconds, 0);
assert_eq!(partial.system_info_seconds, 60);
assert!(validate_admin_preferences(&loaded).is_ok());
}
#[test]
fn refresh_values_are_allowlisted() {
let mut value = RefreshPreferences::default();
for seconds in [0, 4, 11, 65000] {
value.overview_seconds = seconds;
assert!(validate_refresh_preferences(&value).is_err());
}
value.overview_seconds = 1;
for seconds in [1, 2, 4, 31] {
value.traffic_seconds = seconds;
assert!(validate_refresh_preferences(&value).is_err());
}
value.traffic_seconds = 0;
value.cache_seconds = 0;
value.system_info_seconds = 0;
assert!(validate_refresh_preferences(&value).is_ok());
value.system_info_seconds = 16;
assert!(validate_refresh_preferences(&value).is_err());
assert!(serde_json::from_str::<RefreshPreferences>(r#"{"unexpected":1}"#).is_err());
}
+71
View File
@@ -0,0 +1,71 @@
#[tokio::test]
async fn cache_system_events_and_static_private_assets_reject_anonymous_requests() {
let (router, store, _dir, secret, csrf) = fixture().await;
for path in [
"/api/status", "/api/status?view=live", "/api/system-info", "/api/config",
"/api/preferences", "/api/cache", "/api/cache/bans", "/api/audit",
"/api/logs", "/api/traffic", "/api/events", "/api/live-logs",
"/api/file-logs", "/api/certificate-api", "/docs/openapi.json",
"/assets/app.js", "/assets/cache-panel.js", "/assets/langs.json",
] {
let response = router.clone().oneshot(request(path).body(Body::empty()).unwrap())
.await.unwrap();
assert_eq!(response.status(), StatusCode::UNAUTHORIZED, "anonymous GET {path}");
assert_eq!(response.headers().get("x-frame-options").unwrap(), "DENY");
assert!(response.headers().get("content-security-policy").is_some());
}
let cookie = format!("{}={secret}", auth::COOKIE_NAME);
for (method, path) in [
("POST", "/api/cache/purge"),
("POST", "/api/cache/invalidate"),
("DELETE", "/api/cache/bans/1"),
("PUT", "/api/config/cache"),
("PUT", "/api/preferences"),
("POST", "/api/auth/logout-all"),
] {
let no_session = request(path).method(method).body(Body::empty()).unwrap();
let response = router.clone().oneshot(no_session).await.unwrap();
assert_eq!(response.status(), StatusCode::UNAUTHORIZED, "anonymous {method} {path}");
let without_csrf = request(path).method(method)
.header("cookie", &cookie)
.header("origin", "http://127.0.0.1:9090")
.header("x-rustproxy-request", "1")
.body(Body::empty()).unwrap();
let response = router.clone().oneshot(without_csrf).await.unwrap();
assert_eq!(response.status(), StatusCode::FORBIDDEN, "CSRF {method} {path}");
}
// The valid session and CSRF continue to work for a benign mutation,
// proving the failed requests above did not revoke the cookie.
let response = router.oneshot(request("/api/auth/logout")
.method("POST")
.header("cookie", &cookie)
.header("origin", "http://127.0.0.1:9090")
.header("x-rustproxy-request", "1")
.header("x-csrf-token", &csrf)
.body(Body::empty()).unwrap()).await.unwrap();
assert_eq!(response.status(), StatusCode::OK);
store.close().await;
}
#[tokio::test]
async fn admin_origin_host_and_fetch_metadata_are_checked_even_with_valid_cookie() {
let (router, store, _dir, secret, _csrf) = fixture().await;
let cookie = format!("{}={secret}", auth::COOKIE_NAME);
let evil_origin = request("/api/status").header("cookie", &cookie)
.header("origin", "https://another.example")
.body(Body::empty()).unwrap();
let response = router.clone().oneshot(evil_origin).await.unwrap();
assert_eq!(response.status(), StatusCode::FORBIDDEN);
let mut repeated_host = request("/api/status").header("cookie", &cookie)
.body(Body::empty()).unwrap();
repeated_host.headers_mut().append("host", "other.example".parse().unwrap());
let response = router.clone().oneshot(repeated_host).await.unwrap();
assert_eq!(response.status(), StatusCode::FORBIDDEN);
let cross_site = request("/api/status").header("cookie", &cookie)
.header("sec-fetch-site", "cross-site").body(Body::empty()).unwrap();
let response = router.clone().oneshot(cross_site).await.unwrap();
assert_eq!(response.status(), StatusCode::FORBIDDEN);
store.close().await;
}