use crate::helpers::client::E2eClient;
use crate::helpers::config::ConfigManager;
use crate::helpers::constants::{category_enabled, proxy_url};
use crate::helpers::mock_upstream::MockUpstream;
use crate::helpers::proxy::ProxyProcess;
use crate::helpers::report::TestReport;
use crate::run_test;
use std::path::PathBuf;
use std::time::Duration;
pub fn run(report: &mut TestReport) {
if !category_enabled("security") {
return;
}
eprintln!();
eprintln!("\x1b[1mSecurity Tests\x1b[0m");
eprintln!("--------------------------------------------");
let mock = MockUpstream::start();
let project_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let mut config = ConfigManager::new(&project_root);
write_security_config(&mut config, &mock.url());
eprintln!("\x1b[32m[INFO]\x1b[0m Starting proxy with security config...");
let mut proxy = ProxyProcess::start("127.0.0.1:8080");
if let Err(e) = proxy.wait_healthy(Duration::from_secs(5)) {
eprintln!("\x1b[31m[ERROR]\x1b[0m Security proxy failed to start: {e}");
proxy.stop();
config.restore();
return;
}
let client_no_auth = E2eClient::new(proxy_url());
let client_auth = E2eClient::new(proxy_url()).with_api_key("security-test-key");
run_auth_bypass(&client_no_auth, &client_auth, report);
run_rate_limit_enforcement(&client_auth, report);
std::thread::sleep(Duration::from_secs(3));
run_payload_abuse(&client_auth, report);
std::thread::sleep(Duration::from_secs(2));
run_admin_access_control(&client_no_auth, &client_auth, report);
run_header_injection(&client_no_auth, report);
proxy.stop();
config.restore();
eprintln!("--------------------------------------------");
}
fn write_security_config(config: &mut ConfigManager, mock_url: &str) {
let project_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let config_path = project_root.join(".conproxy/conproxy.toml");
if let Some(parent) = config_path.parent() {
let _ = std::fs::create_dir_all(parent);
}
config.write_mock_config(mock_url, "elasticsearch");
let security_config = format!(
r#"[server]
listen = "127.0.0.1:8080"
[upstreams.mock]
url = "{mock_url}"
type = "elasticsearch"
index = "mock_index"
search_fields = ["content", "title"]
timeout_secs = 5
[contexts.default]
default = true
[[contexts.default.upstreams]]
ref = "mock"
priority = 0
weight = 1
[contexts.default.cache]
fresh_secs = 60
stale_secs = 120
max_entries = 1000
[proxy]
api_key = "security-test-key"
[proxy.retry]
enabled = true
max_retries = 2
base_delay_ms = 50
max_delay_ms = 2000
[proxy.rate_limit]
enabled = true
requests_per_second = 10
burst_size = 5
"#
);
std::fs::write(&config_path, security_config).expect("Failed to write security config");
eprintln!(" Wrote security test config (auth + rate_limit)");
}
fn run_auth_bypass(client_no_auth: &E2eClient, client_auth: &E2eClient, report: &mut TestReport) {
eprintln!();
eprintln!("\x1b[32m[INFO]\x1b[0m Auth bypass tests...");
run_test!(report, "security", "Auth bypass: no API key", {
let (status, _) = client_no_auth.query_no_auth("bypass test");
assert_eq!(status, 401, "Expected 401 without key, got {status}");
});
run_test!(report, "security", "Auth bypass: malformed key", {
let (status, _) = client_no_auth.query_with_key("bypass test", "");
assert_eq!(status, 401, "Expected 401 with empty key, got {status}");
});
run_test!(report, "security", "Auth bypass: wrong key", {
let (status, _) = client_no_auth.query_with_key("bypass test", "wrong-key-value");
assert_eq!(status, 401, "Expected 401 with wrong key, got {status}");
});
run_test!(report, "security", "Auth bypass: null bytes in key", {
let (status, _) = client_no_auth.query_with_key("bypass test", "security-test-key\0extra");
assert!(
status == 401 || status == 0,
"Expected 401 (or builder-rejected) with null-byte key, got {status}"
);
});
run_test!(report, "security", "Auth bypass: valid key accepted", {
let (status, body) = client_auth.query("auth accepted test");
assert_eq!(status, 200, "Expected 200 with valid key, got {status}");
assert!(body["results"].is_array());
});
run_test!(report, "security", "Auth bypass: health is public", {
let (status, _) = client_no_auth.health();
assert_eq!(status, 200, "Health should be public, got {status}");
});
run_test!(report, "security", "Auth bypass: ready is public", {
let (status, _) = client_no_auth.ready();
assert_eq!(status, 200, "Ready should be public, got {status}");
});
}
fn run_rate_limit_enforcement(client_auth: &E2eClient, report: &mut TestReport) {
eprintln!();
eprintln!("\x1b[32m[INFO]\x1b[0m Rate limit enforcement tests...");
run_test!(report, "security", "Rate limit: burst triggers 429", {
let mut got_429 = false;
for i in 0..20 {
let (status, _) = client_auth.query(&format!("rate-limit-burst-{i}"));
if status == 429 {
got_429 = true;
break;
}
}
assert!(
got_429,
"Expected at least one 429 after burst, but never got one"
);
});
std::thread::sleep(Duration::from_secs(2));
run_test!(report, "security", "Rate limit: recovery after refill", {
let (status, _) = client_auth.query("rate-limit-recovery");
assert_eq!(
status, 200,
"Expected 200 after rate limit recovery, got {status}"
);
});
}
fn run_payload_abuse(client_auth: &E2eClient, report: &mut TestReport) {
eprintln!();
eprintln!("\x1b[32m[INFO]\x1b[0m Payload abuse tests...");
run_test!(report, "security", "Payload: deeply nested JSON", {
let mut nested = serde_json::json!("deep");
for _ in 0..100 {
nested = serde_json::json!({"nested": nested});
}
let body = serde_json::json!({"query": "deep-nest-test", "metadata": nested});
let (status, _) = client_auth.post_json("/query", &body);
assert!(
status > 0,
"Proxy should respond (not crash) on deeply nested JSON"
);
assert_ne!(status, 0, "Connection should not be dropped");
});
run_test!(report, "security", "Payload: empty query string", {
let (status, _) = client_auth.query("");
assert!(
status == 200 || status == 400,
"Expected 200 or 400 for empty query, got {status}"
);
});
run_test!(report, "security", "Payload: null bytes in query", {
let (status, _) = client_auth.query("test\0injection");
assert!(status > 0, "Proxy should respond on null-byte query");
assert_ne!(status, 500, "Should not cause internal server error");
});
run_test!(report, "security", "Payload: very long query string", {
let long_query = "a".repeat(10_000);
let (status, _) = client_auth.query(&long_query);
assert!(status > 0, "Proxy should respond on very long query");
});
run_test!(report, "security", "Payload: malformed JSON body", {
let url = format!("{}/query", proxy_url());
let client = reqwest::blocking::Client::new();
let resp = client
.post(&url)
.header("Content-Type", "application/json")
.header("X-API-Key", "security-test-key")
.body("{invalid json")
.send();
match resp {
Ok(r) => {
let status = r.status().as_u16();
assert!(
status == 400 || status == 422,
"Expected 400 or 422 for malformed JSON, got {status}"
);
}
Err(_) => {
}
}
});
}
fn run_admin_access_control(
client_no_auth: &E2eClient,
client_auth: &E2eClient,
report: &mut TestReport,
) {
eprintln!();
eprintln!("\x1b[32m[INFO]\x1b[0m Admin access control tests...");
run_test!(report, "security", "Admin ACL: pause without auth", {
let (status, _) = client_no_auth.admin_pause();
assert_eq!(status, 401, "Admin pause should require auth, got {status}");
});
run_test!(report, "security", "Admin ACL: resume without auth", {
let (status, _) = client_no_auth.admin_resume();
assert_eq!(
status, 401,
"Admin resume should require auth, got {status}"
);
});
run_test!(report, "security", "Admin ACL: reload without auth", {
let (status, _) = client_no_auth.admin_reload();
assert_eq!(
status, 401,
"Admin reload should require auth, got {status}"
);
});
run_test!(
report,
"security",
"Admin ACL: metrics reset without auth",
{
let status = client_no_auth.admin_metrics_reset();
assert_eq!(
status, 401,
"Admin metrics reset should require auth, got {status}"
);
}
);
run_test!(report, "security", "Admin ACL: pause with auth", {
let (status, _) = client_auth.admin_pause();
assert_eq!(
status, 200,
"Admin pause should succeed with auth, got {status}"
);
let _ = client_auth.admin_resume();
});
}
fn run_header_injection(client_no_auth: &E2eClient, report: &mut TestReport) {
eprintln!();
eprintln!("\x1b[32m[INFO]\x1b[0m Header injection tests...");
run_test!(report, "security", "Header injection: CRLF in API key", {
let (status, _) =
client_no_auth.query_with_key("injection test", "key\r\nX-Injected: true");
assert!(
status == 401 || status == 400 || status == 0,
"Expected rejection for CRLF injection, got {status}"
);
});
run_test!(report, "security", "Header injection: oversized API key", {
let long_key = "x".repeat(8192);
let (status, _) = client_no_auth.query_with_key("injection test", &long_key);
assert!(
status == 401 || status == 431 || status == 400 || status == 0,
"Expected rejection for oversized key, got {status}"
);
});
run_test!(
report,
"security",
"Header injection: special chars in request ID",
{
let url = format!("{}/health", proxy_url());
let client = reqwest::blocking::Client::new();
let resp = client
.get(&url)
.header("X-Request-Id", "<script>alert(1)</script>")
.send();
match resp {
Ok(r) => {
let status = r.status().as_u16();
assert_eq!(status, 200, "Health should still work, got {status}");
let body = r.text().unwrap_or_default();
assert!(
!body.contains("<script>"),
"Response should not reflect injected script"
);
}
Err(_) => {
}
}
}
);
}