#[tokio::test] async fn docs_require_the_same_session_and_do_not_redirect_schema_fetches() { let (router, store, _dir, secret, _) = fixture().await; for path in ["/docs", "/docs/"] { let response = router .clone() .oneshot(request(path).body(Body::empty()).unwrap()) .await .unwrap(); assert_eq!(response.status(), StatusCode::SEE_OTHER); assert_eq!(response.headers()["location"], "/?next=docs"); } let cookie = format!("{}={secret}", auth::COOKIE_NAME); for path in [ "/", "/docs", "/docs/", "/docs/licenses.txt", "/docs/content.json", &format!("/assets/app_{}.js", super::asset_build_id()), ] { let response = router .clone() .oneshot( request(path) .header("cookie", &cookie) .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK, "{path}"); assert_eq!(response.headers()["x-frame-options"], "DENY"); assert_eq!(response.headers()["cache-control"], "no-store"); } let response = router .clone() .oneshot( request("/docs/openapi.json") .header("cookie", &cookie) .body(Body::empty()) .unwrap(), ) .await .unwrap(); let spec = json_body(response).await; assert_eq!(spec["openapi"], "3.0.3"); assert_eq!(spec["info"]["version"], env!("CARGO_PKG_VERSION")); assert_eq!( spec["components"]["securitySchemes"]["AdminSession"]["in"], "cookie" ); assert!(spec["paths"].get("/api/config").is_some()); assert!(spec["paths"].get("/api/config/history/restore").is_some()); let response = router .clone() .oneshot( request("/docs/content.json") .header("cookie", &cookie) .body(Body::empty()) .unwrap(), ) .await .unwrap(); let docs = json_body(response).await; let documents = docs["documents"].as_array().expect("documentation bundle"); assert!(documents.iter().any(|document| document["path"] == "docs/README.md")); assert!(documents.iter().any(|document| document["path"] == "docs/WEB_SERVER.md")); assert!(documents.iter().any(|document| document["path"] == "docs/PHP_FPM.md")); assert!(documents.iter().any(|document| document["path"] == "docs/SECURITY_AUDIT_0.31.0.md")); assert!(documents.iter().any(|document| document["path"] == "docs/TEST_REPORT.md")); store.close().await; } #[tokio::test] async fn credentials_in_query_or_duplicate_cookies_cannot_authorize() { let (router, store, _dir, secret, _) = fixture().await; for cookie in [ String::new(), format!("{}=invalid", auth::COOKIE_NAME), format!( "{}={secret}; {}={secret}", auth::COOKIE_NAME, auth::COOKIE_NAME ), ] { let path = format!("/api/config?token={secret}&session={secret}"); let response = router .clone() .oneshot( request(&path) .header("cookie", cookie) .header("authorization", format!("Bearer {secret}")) .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } store.close().await; } #[tokio::test] async fn duplicate_csrf_or_marker_is_rejected_and_session_survives() { let (router, store, _dir, secret, csrf) = fixture().await; let cookie = format!("{}={secret}", auth::COOKIE_NAME); for duplicate in ["x-csrf-token", "x-rustproxy-request"] { let response = router .clone() .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) .header( duplicate, if duplicate == "x-csrf-token" { csrf.as_str() } else { "1" }, ) .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::FORBIDDEN); } let response = router .oneshot( request("/api/auth/session") .header("cookie", cookie) .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); store.close().await; } #[tokio::test] async fn malformed_anonymous_json_never_reflects_submitted_values() { let (router, store, _dir, _, _) = fixture().await; for path in ["/api/auth/login", "/api/auth/setup"] { let response = router .clone() .oneshot( request(path) .method("POST") .header("origin", "http://127.0.0.1:9090") .header("x-rustproxy-request", "1") .header("content-type", "application/json") .body(Body::from( r#"{"username":{"untrusted":"DO_NOT_REFLECT_ME"}}"#, )) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::BAD_REQUEST); assert_eq!( json_body(response).await, json!({"error":"auth.invalid_request"}) ); } store.close().await; } #[tokio::test] async fn retained_configuration_revision_can_be_restored_as_a_new_revision() { let (router, store, _dir, secret, csrf) = fixture().await; let cookie = format!("{}={secret}", auth::COOKIE_NAME); let mut first = Config::default(); first.server_header.value = "first".into(); let response = router .clone() .oneshot( request("/api/config") .method("PUT") .header("content-type", "application/json") .header("cookie", &cookie) .header("origin", "http://127.0.0.1:9090") .header("x-rustproxy-request", "1") .header("x-csrf-token", &csrf) .body(Body::from( json!({"expected_revision":0,"config":first}).to_string(), )) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); assert_eq!(json_body(response).await["revision"], 1); let mut second = Config::default(); second.server_header.value = "second".into(); let response = router .clone() .oneshot( request("/api/config") .method("PUT") .header("content-type", "application/json") .header("cookie", &cookie) .header("origin", "http://127.0.0.1:9090") .header("x-rustproxy-request", "1") .header("x-csrf-token", &csrf) .body(Body::from( json!({"expected_revision":1,"config":second}).to_string(), )) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); assert_eq!(json_body(response).await["revision"], 2); let response = router .clone() .oneshot( request("/api/config/history/restore") .method("POST") .header("content-type", "application/json") .header("cookie", &cookie) .header("origin", "http://127.0.0.1:9090") .header("x-rustproxy-request", "1") .header("x-csrf-token", &csrf) .body(Body::from( json!({"revision":1,"expected_revision":2}).to_string(), )) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); let restored = json_body(response).await; assert_eq!(restored["revision"], 3); assert_eq!(restored["config"]["server_header"]["value"], "first"); let audit = store.recent_audit().await.unwrap(); assert_eq!(audit[0].actor, "owner"); assert_eq!(audit[0].action, "config.restore"); assert_eq!(audit[0].detail, "source_revision=1 revision=3"); let detail = store.configuration_revision(3).await.unwrap().unwrap(); assert_eq!(detail.config.server_header.value, "first"); assert_eq!(detail.previous_config.unwrap().server_header.value, "second"); store.close().await; }