Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ rate_limit:
| `CERTSTREAM_CT_LOG_REQUEST_TIMEOUT_SECS` | 30 | Request timeout |
| `CERTSTREAM_CT_LOG_BATCH_SIZE` | 1024 | Entries requested per get-entries call (servers clamp to their own max) |
| `CERTSTREAM_CT_LOG_FETCH_CONCURRENCY` | 4 | Concurrent range/tile fetches per watcher during catch-up (1-16) |
| `CERTSTREAM_USER_AGENT` | certstream-server-rust/{VERSION} | HTTP User-Agent for CT log fetches. Some operators (e.g. Geomys) apply a more generous rate limit tier to clients that include a contact email. |

**Hot Reload**

Expand Down
5 changes: 5 additions & 0 deletions config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ ct_log:
# any operator absent from operator_rate_limits.
default_operator_rate_limit_ms: 500
operator_rate_limits: {}
# HTTP User-Agent for CT log fetches. Some operators (e.g. Geomys) apply a
# more generous rate limit tier to clients that include a contact email.
# When unset, defaults to certstream-server-rust/{VERSION}.
# (env: CERTSTREAM_USER_AGENT)
user_agent: null
# Per-catalog-source runtime-authority overrides. Keys are
# google_v3_usable, google_v3_all, and apple. An override can only grant
# authority to a source that currently verifies; it cannot promote an
Expand Down
2 changes: 2 additions & 0 deletions docs/docs.html
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,7 @@ <h3>CT log settings</h3>
<tr><td><code>CERTSTREAM_CT_LOG_REQUEST_TIMEOUT_SECS</code></td><td>30</td><td>Request timeout</td></tr>
<tr><td><code>CERTSTREAM_CT_LOG_BATCH_SIZE</code></td><td>1024</td><td>Entries requested per get-entries call (servers clamp to their own max)</td></tr>
<tr><td><code>CERTSTREAM_CT_LOG_FETCH_CONCURRENCY</code></td><td>4</td><td>Concurrent range/tile fetches per watcher during catch-up (1-16)</td></tr>
<tr><td><code>CERTSTREAM_USER_AGENT</code></td><td>certstream-server-rust/{VERSION}</td><td>HTTP User-Agent for CT log fetches. Some operators (e.g. Geomys) apply a more generous rate limit tier to clients that include a contact email.</td></tr>
<tr><td><code>CERTSTREAM_STATIC_CT_CHECKPOINT_SIGNATURE</code></td><td>warn</td><td>Checkpoint signature policy: <code>warn</code> or <code>enforce</code></td></tr>
<tr><td><code>CERTSTREAM_DEDUP_CAPACITY</code></td><td>200000</td><td>Cross-log dedup capacity</td></tr>
<tr><td><code>CERTSTREAM_DEDUP_TTL_SECS</code></td><td>900</td><td>Dedup window (seconds)</td></tr>
Expand Down Expand Up @@ -549,6 +550,7 @@ <h2>YAML config file</h2>
<span class="str">poll_interval_ms</span>: <span class="var">500</span>
<span class="str">retry_max_attempts</span>: <span class="var">3</span>
<span class="str">request_timeout_secs</span>: <span class="var">30</span>
<span class="str">user_agent</span>: <span class="str">"certstream-server-rust/1.5.3 (contact@example.com)"</span>

<span class="str">dedup</span>:
<span class="str">capacity</span>: <span class="var">200000</span>
Expand Down
1 change: 1 addition & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ impl CliArgs {
println!(" CERTSTREAM_PORT Server port (default: 8080)");
println!(" CERTSTREAM_LOG_LEVEL Log level (default: info)");
println!(" CERTSTREAM_BUFFER_SIZE Broadcast buffer size (default: 1000)");
println!(" CERTSTREAM_USER_AGENT Override HTTP User-Agent for CT log requests");
println!();
println!("For more information, see: https://github.com/reloading01/certstream-server-rust");
}
Expand Down
44 changes: 44 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,12 @@ pub struct CtLogConfig {
/// whitespace, or punctuation. Empty map means every operator uses the default.
#[serde(default)]
pub operator_rate_limits: std::collections::HashMap<String, u64>,
/// HTTP User-Agent for CT log fetches. Some CT log operators (e.g.
/// Geomys) apply a more generous rate limit tier to clients that include
/// a contact email. When unset, defaults to
/// `certstream-server-rust/{VERSION}`.
#[serde(default)]
pub user_agent: Option<String>,
/// Per-catalog-source runtime-authority overrides. Keys are the catalog
/// registry source names (`google_v3_usable`, `google_v3_all`, `apple`).
/// An override can only grant authority to a source that currently verifies;
Expand Down Expand Up @@ -219,6 +225,7 @@ impl Default for CtLogConfig {
static_ct_enabled: true,
default_operator_rate_limit_ms: default_operator_rate_limit_ms(),
operator_rate_limits: std::collections::HashMap::new(),
user_agent: None,
catalog_authority_overrides: std::collections::HashMap::new(),
}
}
Expand Down Expand Up @@ -586,6 +593,7 @@ impl Config {
ct_log.checkpoint_signature_mode,
"CERTSTREAM_STATIC_CT_CHECKPOINT_SIGNATURE"
);
env_override!(ct_log.user_agent, "CERTSTREAM_USER_AGENT", some_str);

let mut connection_limit = yaml_config.connection_limit.unwrap_or_default();
env_override!(connection_limit.enabled, "CERTSTREAM_CONNECTION_LIMIT_ENABLED");
Expand Down Expand Up @@ -706,6 +714,14 @@ impl Config {
message: "Fetch concurrency must be between 1 and 16".to_string(),
});
}
if let Some(ua) = &self.ct_log.user_agent
&& reqwest::header::HeaderValue::try_from(ua.as_str()).is_err()
{
errors.push(ConfigValidationError {
field: "ct_log.user_agent".to_string(),
message: "User-Agent must be a valid HTTP header value".to_string(),
});
}

if errors.is_empty() {
Ok(())
Expand Down Expand Up @@ -801,6 +817,34 @@ mod tests {
assert_eq!(config.start_overlap_leaves, 256);
assert!(config.rfc6962_enabled);
assert!(config.static_ct_enabled);
assert!(config.user_agent.is_none());
}

#[test]
fn test_ct_log_config_deserialize_user_agent() {
let yaml = r#"
user_agent: "certstream-server-rust/1.5.3 (contact@example.com)"
"#;
let config: CtLogConfig = serde_yaml::from_str(yaml).unwrap();
assert_eq!(
config.user_agent.as_deref(),
Some("certstream-server-rust/1.5.3 (contact@example.com)")
);
}

#[test]
fn test_validate_user_agent_invalid_header() {
let config = Config {
ct_log: CtLogConfig {
user_agent: Some("bad\nuser-agent".to_string()),
..CtLogConfig::default()
},
..test_config()
};
let result = config.validate();
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.field == "ct_log.user_agent"));
}

#[test]
Expand Down
8 changes: 7 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,14 @@ async fn main() {
let tx: broadcast::Sender<Arc<PreSerializedMessage>> =
broadcast::channel(config.buffer_size).0;

let user_agent = config
.ct_log
.user_agent
.clone()
.unwrap_or_else(|| format!("certstream-server-rust/{}", VERSION));

let client = Client::builder()
.user_agent(format!("certstream-server-rust/{}", VERSION))
.user_agent(&user_agent)
// Pre-1.5.0 kept 20 idle connections per host × 55 hosts = 1100
// hot TCP sockets, ~40-55 MiB of kernel + TLS state per process.
// Watchers now pipeline up to `fetch_concurrency` range/tile fetches
Expand Down