20260707 支持本地HTTP root证书

This commit is contained in:
yuyr 2026-07-07 23:04:44 +08:00
parent b0f76738e5
commit 63ac3ee254
3 changed files with 89 additions and 20 deletions

View File

@ -163,6 +163,7 @@ pub struct CliArgs {
pub rsync_command: Option<PathBuf>,
pub http_timeout_secs: u64,
pub http_root_cert_paths: Vec<PathBuf>,
pub rsync_timeout_secs: u64,
pub rsync_mirror_root: Option<PathBuf>,
pub rsync_scope_policy: RsyncScopePolicy,
@ -254,6 +255,7 @@ Options:
--disable-rrdp Disable RRDP and synchronize only via rsync
--rsync-command <path> Use this rsync command instead of the default rsync binary
--http-timeout-secs <n> HTTP fetch timeout seconds (default: 20)
--http-root-cert <path> Extra PEM root certificate trusted by HTTPS fetches (repeatable)
--rsync-timeout-secs <n> rsync I/O timeout seconds (default: 60)
--rsync-mirror-root <path> Persist rsync mirrors under this directory (default: disabled)
--rsync-scope <policy> rsync scope policy: host, publication-point, or module-root (default: module-root)
@ -313,6 +315,7 @@ pub fn parse_args(argv: &[String]) -> Result<CliArgs, String> {
let mut disable_rrdp: bool = false;
let mut rsync_command: Option<PathBuf> = None;
let mut http_timeout_secs: u64 = 30;
let mut http_root_cert_paths: Vec<PathBuf> = Vec::new();
let mut rsync_timeout_secs: u64 = 30;
let mut rsync_mirror_root: Option<PathBuf> = None;
let mut rsync_scope_policy = RsyncScopePolicy::default();
@ -636,6 +639,11 @@ pub fn parse_args(argv: &[String]) -> Result<CliArgs, String> {
.parse::<u64>()
.map_err(|_| format!("invalid --http-timeout-secs: {v}"))?;
}
"--http-root-cert" => {
i += 1;
let v = argv.get(i).ok_or("--http-root-cert requires a value")?;
http_root_cert_paths.push(PathBuf::from(v));
}
"--rsync-timeout-secs" => {
i += 1;
let v = argv.get(i).ok_or("--rsync-timeout-secs requires a value")?;
@ -977,6 +985,7 @@ pub fn parse_args(argv: &[String]) -> Result<CliArgs, String> {
disable_rrdp,
rsync_command,
http_timeout_secs,
http_root_cert_paths,
rsync_timeout_secs,
rsync_mirror_root,
rsync_scope_policy,
@ -2001,6 +2010,14 @@ pub fn run(argv: &[String]) -> Result<(), String> {
let validation_time = args
.validation_time
.unwrap_or_else(time::OffsetDateTime::now_utc);
let http_root_certificates_pem = args
.http_root_cert_paths
.iter()
.map(|path| {
std::fs::read(path)
.map_err(|e| format!("read HTTP root certificate failed: {}: {e}", path.display()))
})
.collect::<Result<Vec<_>, _>>()?;
let store = if args.raw_store_db.is_some() || args.repo_bytes_db.is_some() {
Arc::new(
@ -2221,6 +2238,7 @@ pub fn run(argv: &[String]) -> Result<(), String> {
} else if let Some(dir) = args.rsync_local_dir.as_ref() {
let http = BlockingHttpFetcher::new(HttpFetcherConfig {
timeout: std::time::Duration::from_secs(args.http_timeout_secs.max(1)),
extra_root_certificates_pem: http_root_certificates_pem.clone(),
..HttpFetcherConfig::default()
})
.map_err(|e| e.to_string())?;
@ -2239,6 +2257,7 @@ pub fn run(argv: &[String]) -> Result<(), String> {
} else {
let http = BlockingHttpFetcher::new(HttpFetcherConfig {
timeout: std::time::Duration::from_secs(args.http_timeout_secs.max(1)),
extra_root_certificates_pem: http_root_certificates_pem.clone(),
..HttpFetcherConfig::default()
})
.map_err(|e| e.to_string())?;

View File

@ -289,6 +289,29 @@ fn parse_accepts_host_rsync_scope_policy() {
assert_eq!(args.rsync_scope_policy, RsyncScopePolicy::Host);
}
#[test]
fn parse_accepts_repeatable_http_root_cert_paths() {
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-url".to_string(),
"https://example.test/x.tal".to_string(),
"--http-root-cert".to_string(),
"local-ca-1.pem".to_string(),
"--http-root-cert".to_string(),
"local-ca-2.pem".to_string(),
];
let args = parse_args(&argv).expect("parse args");
assert_eq!(
args.http_root_cert_paths,
vec![
PathBuf::from("local-ca-1.pem"),
PathBuf::from("local-ca-2.pem")
]
);
}
#[test]
fn parse_rejects_invalid_rsync_scope_policy() {
let argv = vec![

View File

@ -29,6 +29,8 @@ pub struct HttpFetcherConfig {
/// Larger timeout used for RRDP snapshot / delta bodies.
pub large_body_timeout: Duration,
pub user_agent: String,
/// Extra PEM trust anchors for HTTPS transport tests or private RRDP endpoints.
pub extra_root_certificates_pem: Vec<Vec<u8>>,
}
impl Default for HttpFetcherConfig {
@ -38,6 +40,7 @@ impl Default for HttpFetcherConfig {
timeout: Duration::from_secs(30),
large_body_timeout: Duration::from_secs(180),
user_agent: "rpki-dev/0.1 (stage2)".to_string(),
extra_root_certificates_pem: Vec::new(),
}
}
}
@ -61,22 +64,28 @@ impl BlockingHttpFetcher {
let short_timeout = config.timeout;
let large_body_timeout = std::cmp::max(config.large_body_timeout, config.timeout);
let connect_timeout = std::cmp::min(config.connect_timeout, config.timeout);
let short_client = Client::builder()
.connect_timeout(connect_timeout)
.timeout(config.timeout)
.user_agent(config.user_agent.clone())
let short_client = Self::client_builder(
&config,
connect_timeout,
config.timeout,
config.user_agent.clone(),
)?
.build()
.map_err(|e| e.to_string())?;
let large_body_client = Client::builder()
.connect_timeout(connect_timeout)
.timeout(large_body_timeout)
.user_agent(config.user_agent)
let large_body_client = Self::client_builder(
&config,
connect_timeout,
large_body_timeout,
config.user_agent.clone(),
)?
.build()
.map_err(|e| e.to_string())?;
let retry_short_client = Client::builder()
.connect_timeout(Duration::from_secs(1))
.timeout(Duration::from_secs(1))
.user_agent("rpki-dev/0.1 (stage2)")
let retry_short_client = Self::client_builder(
&config,
Duration::from_secs(1),
Duration::from_secs(1),
"rpki-dev/0.1 (stage2)".to_string(),
)?
.build()
.map_err(|e| e.to_string())?;
Ok(Self {
@ -88,11 +97,29 @@ impl BlockingHttpFetcher {
})
}
fn client_builder(
config: &HttpFetcherConfig,
connect_timeout: Duration,
timeout: Duration,
user_agent: String,
) -> Result<reqwest::blocking::ClientBuilder, String> {
let mut builder = Client::builder()
.connect_timeout(connect_timeout)
.timeout(timeout)
.user_agent(user_agent);
for (idx, pem) in config.extra_root_certificates_pem.iter().enumerate() {
let certificate = reqwest::Certificate::from_pem(pem)
.map_err(|e| format!("parse HTTP root certificate #{idx} failed: {e}"))?;
builder = builder.add_root_certificate(certificate);
}
Ok(builder)
}
pub fn fetch_bytes(&self, uri: &str) -> Result<Vec<u8>, String> {
let started = std::time::Instant::now();
let (client, timeout_profile, timeout_value) = self.client_for_uri(uri);
let resp = client.get(uri).send().map_err(|e| {
let msg = format!("http request failed: {e}");
let msg = format!("http request failed: {e:?}");
crate::progress_log::emit(
"http_fetch_failed",
serde_json::json!({
@ -222,7 +249,7 @@ impl Fetcher for BlockingHttpFetcher {
let started = std::time::Instant::now();
let (client, timeout_profile, timeout_value) = self.client_for_uri(uri);
let resp = client.get(uri).send().map_err(|e| {
let msg = format!("http request failed: {e}");
let msg = format!("http request failed: {e:?}");
crate::progress_log::emit(
"http_fetch_failed",
serde_json::json!({