diff --git a/deploy/docker-installer/.env.example b/deploy/docker-installer/.env.example index c0ba16b..8b3493d 100644 --- a/deploy/docker-installer/.env.example +++ b/deploy/docker-installer/.env.example @@ -47,6 +47,9 @@ LIVE_TA_REFRESH_MAX_TIME_SECS=120 # Sync and runtime behavior. RSYNC_SCOPE=module-root +# HTTP User-Agent for all outgoing HTTP requests (RRDP / TAL / TA / dead-repo probes). +# Unset, empty, or header-illegal values fall back to panda-rpki/0.2. +#RPKI_HTTP_USER_AGENT=panda-rpki/0.2 DISABLE_COMPETING_RPS=0 RUN_ROOT=/var/lib/ours-rp DB_DIR=/var/lib/ours-rp/state/db diff --git a/scripts/soak/portable-soak.env.example b/scripts/soak/portable-soak.env.example index 766e3d6..b9c9edd 100644 --- a/scripts/soak/portable-soak.env.example +++ b/scripts/soak/portable-soak.env.example @@ -102,6 +102,10 @@ ENABLE_CHILD_CERTIFICATE_VALIDATION_CACHE=0 # 实验性 transport 预热:RPKI_EXTRA_ARGS="--enable-transport-request-prefetch --enable-roa-validation-cache" RPKI_EXTRA_ARGS="" +# 所有出站 HTTP 请求(RRDP / TAL / TA / 死 repo 探针)的 User-Agent。 +# 不设置、为空或含非法 header 字符时回退默认 panda-rpki/0.2。 +#RPKI_HTTP_USER_AGENT=panda-rpki/0.2 + # 是否为每轮输出 timing profile 到 runs/run_xxxx/analyze/timing.json。 # 性能 profile 或打点验证时设置为 1;普通 soak 建议保持 0,避免额外开销。 RPKI_ANALYZE=0 diff --git a/src/fetch/http.rs b/src/fetch/http.rs index 91859b3..7286277 100644 --- a/src/fetch/http.rs +++ b/src/fetch/http.rs @@ -20,6 +20,28 @@ pub fn with_scoped_http_timeout_override(timeout: Duration, f: impl FnOnce() }) } +/// Default User-Agent sent with every outgoing HTTP request (RRDP +/// notification/snapshot/delta, TAL/TA downloads, daemon probes). +pub const DEFAULT_HTTP_USER_AGENT: &str = "panda-rpki/0.2"; + +/// Environment variable that overrides the HTTP User-Agent. Unset, empty, or +/// values containing characters that are illegal in a header value fall back +/// to [`DEFAULT_HTTP_USER_AGENT`]. +pub const HTTP_USER_AGENT_ENV: &str = "RPKI_HTTP_USER_AGENT"; + +fn resolve_http_user_agent(env_value: Option) -> String { + let Some(value) = env_value else { + return DEFAULT_HTTP_USER_AGENT.to_string(); + }; + let trimmed = value.trim(); + let valid = !trimmed.is_empty() && trimmed.bytes().all(|b| (0x20..=0x7e).contains(&b)); + if valid { + trimmed.to_string() + } else { + DEFAULT_HTTP_USER_AGENT.to_string() + } +} + #[derive(Clone, Debug)] pub struct HttpFetcherConfig { /// Connection-establishment timeout for HTTP requests. @@ -39,7 +61,7 @@ impl Default for HttpFetcherConfig { connect_timeout: Duration::from_secs(15), timeout: Duration::from_secs(30), large_body_timeout: Duration::from_secs(180), - user_agent: "rpki-dev/0.1 (stage2)".to_string(), + user_agent: resolve_http_user_agent(std::env::var(HTTP_USER_AGENT_ENV).ok()), extra_root_certificates_pem: Vec::new(), } } @@ -84,7 +106,7 @@ impl BlockingHttpFetcher { &config, Duration::from_secs(1), Duration::from_secs(1), - "rpki-dev/0.1 (stage2)".to_string(), + config.user_agent.clone(), )? .build() .map_err(|e| e.to_string())?; @@ -532,4 +554,99 @@ mod tests { assert_eq!(profile_large, "large_body"); assert_eq!(timeout_large, Duration::from_secs(9)); } + + #[test] + fn resolve_http_user_agent_falls_back_to_default() { + assert_eq!(resolve_http_user_agent(None), DEFAULT_HTTP_USER_AGENT); + assert_eq!( + resolve_http_user_agent(Some(String::new())), + DEFAULT_HTTP_USER_AGENT + ); + assert_eq!( + resolve_http_user_agent(Some(" ".to_string())), + DEFAULT_HTTP_USER_AGENT + ); + // Control characters are illegal in header values; fall back instead + // of letting reqwest fail to build the client. + assert_eq!( + resolve_http_user_agent(Some("bad\u{1}ua".to_string())), + DEFAULT_HTTP_USER_AGENT + ); + assert_eq!( + resolve_http_user_agent(Some("non-ascii-ua-中".to_string())), + DEFAULT_HTTP_USER_AGENT + ); + } + + #[test] + fn resolve_http_user_agent_accepts_custom_value() { + assert_eq!( + resolve_http_user_agent(Some("panda-rpki/9.9 (test)".to_string())), + "panda-rpki/9.9 (test)" + ); + // Surrounding whitespace is trimmed. + assert_eq!( + resolve_http_user_agent(Some(" custom/1.0 ".to_string())), + "custom/1.0" + ); + } + + fn spawn_user_agent_capture_server() -> (String, std::sync::mpsc::Receiver) { + let listener = TcpListener::bind(("127.0.0.1", 0)).expect("bind"); + let addr = listener.local_addr().expect("addr"); + let (tx, rx) = std::sync::mpsc::channel(); + thread::spawn(move || { + let (mut stream, _) = listener.accept().expect("accept"); + let mut buf = [0u8; 4096]; + let read = stream.read(&mut buf).expect("read request"); + let request = String::from_utf8_lossy(&buf[..read]).to_string(); + let body = b"ok"; + let hdr = format!( + "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", + body.len() + ); + stream.write_all(hdr.as_bytes()).expect("write hdr"); + stream.write_all(body).expect("write body"); + let _ = tx.send(request); + }); + (format!("http://{}/", addr), rx) + } + + #[test] + fn fetch_bytes_sends_configured_user_agent_header() { + let (url, rx) = spawn_user_agent_capture_server(); + let http = BlockingHttpFetcher::new(HttpFetcherConfig { + timeout: Duration::from_secs(2), + user_agent: "panda-rpki/0.2".to_string(), + ..HttpFetcherConfig::default() + }) + .expect("http"); + http.fetch_bytes(&url).expect("fetch"); + let request = rx.recv().expect("request captured"); + assert!( + request + .to_ascii_lowercase() + .contains("user-agent: panda-rpki/0.2"), + "{request}" + ); + } + + #[test] + fn fetch_bytes_sends_custom_user_agent_header() { + let (url, rx) = spawn_user_agent_capture_server(); + let http = BlockingHttpFetcher::new(HttpFetcherConfig { + timeout: Duration::from_secs(2), + user_agent: "custom-marker/7.7".to_string(), + ..HttpFetcherConfig::default() + }) + .expect("http"); + http.fetch_bytes(&url).expect("fetch"); + let request = rx.recv().expect("request captured"); + assert!( + request + .to_ascii_lowercase() + .contains("user-agent: custom-marker/7.7"), + "{request}" + ); + } } diff --git a/tests/test_apnic_rrdp_delta_live_20260226.rs b/tests/test_apnic_rrdp_delta_live_20260226.rs index 2be02fd..48ba4a3 100644 --- a/tests/test_apnic_rrdp_delta_live_20260226.rs +++ b/tests/test_apnic_rrdp_delta_live_20260226.rs @@ -29,7 +29,7 @@ fn live_http_fetcher() -> BlockingHttpFetcher { BlockingHttpFetcher::new(HttpFetcherConfig { timeout: Duration::from_secs(timeout_secs), large_body_timeout: Duration::from_secs(timeout_secs), - user_agent: "rpki-dev/0.1 (stage2 live rrdp delta test)".to_string(), + user_agent: "panda-rpki/0.2 (live rrdp delta test)".to_string(), ..HttpFetcherConfig::default() }) .expect("http fetcher")