use super::Result;
use crate::error::TlsError;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use tracing::{debug, info, warn};
const CT_LOG_LIST_URL: &str = "https://www.gstatic.com/ct/log_list/v3/log_list.json";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LogSource {
pub id: String,
pub description: String,
pub operator: String,
pub url: String,
pub key: Option<String>,
pub mmd: Option<u64>,
pub tree_size: u64,
pub usable: bool,
pub last_error: Option<String>,
pub failure_count: u32,
}
impl LogSource {
pub fn is_healthy(&self) -> bool {
self.usable && self.failure_count < 3
}
pub fn mark_failed(&mut self, error: String) {
self.failure_count += 1;
self.last_error = Some(error);
if self.failure_count >= 3 {
self.usable = false;
}
}
pub fn mark_success(&mut self) {
self.failure_count = 0;
self.last_error = None;
self.usable = true;
}
}
pub struct SourceManager {
sources: HashMap<String, LogSource>,
client: reqwest::Client,
}
impl SourceManager {
pub fn new() -> Self {
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()
.unwrap_or_else(|_| reqwest::Client::new());
Self {
sources: HashMap::new(),
client,
}
}
pub async fn fetch_sources(&mut self) -> Result<()> {
info!("Fetching CT log sources from {}", CT_LOG_LIST_URL);
let response = self
.client
.get(CT_LOG_LIST_URL)
.send()
.await
.map_err(|e| TlsError::Other(format!("Failed to fetch CT log list: {}", e)))?;
if !response.status().is_success() {
return Err(TlsError::HttpError {
status: response.status().as_u16(),
details: format!(
"CT log list request failed with status: {}",
response.status()
),
});
}
let log_list: GoogleCtLogList =
response.json().await.map_err(|e| TlsError::ParseError {
message: format!("Failed to parse CT log list: {}", e),
})?;
debug!(
"Fetched {} operators from CT log list",
log_list.operators.len()
);
for operator in &log_list.operators {
for log in &operator.logs {
if let Some(state) = &log.state
&& state.usable.is_some()
{
let log_id = log.log_id.clone();
let source = LogSource {
id: log_id.clone(),
description: log
.description
.clone()
.unwrap_or_else(|| format!("{} CT Log", operator.name)),
operator: operator.name.clone(),
url: log.url.clone(),
key: log.key.clone(),
mmd: log.mmd,
tree_size: 0,
usable: true,
last_error: None,
failure_count: 0,
};
self.sources.insert(log_id, source);
}
}
}
info!("Loaded {} usable CT log sources", self.sources.len());
Ok(())
}
pub fn get_healthy_sources(&self) -> Vec<&LogSource> {
self.sources.values().filter(|s| s.is_healthy()).collect()
}
pub fn get_source(&self, id: &str) -> Option<&LogSource> {
self.sources.get(id)
}
pub fn get_source_mut(&mut self, id: &str) -> Option<&mut LogSource> {
self.sources.get_mut(id)
}
pub fn update_tree_size(&mut self, id: &str, tree_size: u64) {
if let Some(source) = self.sources.get_mut(id) {
source.tree_size = tree_size;
}
}
pub fn mark_source_failed(&mut self, id: &str, error: String) {
if let Some(source) = self.sources.get_mut(id) {
source.mark_failed(error);
warn!(
"Source {} marked as failed (failure count: {})",
id, source.failure_count
);
}
}
pub fn mark_source_success(&mut self, id: &str) {
if let Some(source) = self.sources.get_mut(id) {
source.mark_success();
}
}
pub fn total_sources(&self) -> usize {
self.sources.len()
}
pub fn healthy_sources_count(&self) -> usize {
self.sources.values().filter(|s| s.is_healthy()).count()
}
}
impl Default for SourceManager {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Deserialize)]
struct GoogleCtLogList {
operators: Vec<Operator>,
}
#[derive(Debug, Deserialize)]
struct Operator {
name: String,
#[serde(default)]
logs: Vec<Log>,
}
#[derive(Debug, Deserialize)]
struct Log {
log_id: String,
description: Option<String>,
url: String,
key: Option<String>,
mmd: Option<u64>,
state: Option<LogState>,
}
#[derive(Debug, Deserialize)]
struct LogState {
usable: Option<UsableState>,
}
#[derive(Debug, Deserialize)]
struct UsableState {
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_log_source_health() {
let mut source = LogSource {
id: "test".to_string(),
description: "Test Log".to_string(),
operator: "Test Operator".to_string(),
url: "https://example.com".to_string(),
key: None,
mmd: None,
tree_size: 0,
usable: true,
last_error: None,
failure_count: 0,
};
assert!(source.is_healthy());
source.mark_failed("Test error".to_string());
assert!(source.is_healthy());
assert_eq!(source.failure_count, 1);
source.mark_failed("Test error 2".to_string());
source.mark_failed("Test error 3".to_string());
assert!(!source.is_healthy());
assert!(!source.usable);
source.mark_success();
assert!(source.is_healthy());
assert_eq!(source.failure_count, 0);
}
#[test]
fn test_source_manager_creation() {
let manager = SourceManager::new();
assert_eq!(manager.total_sources(), 0);
assert_eq!(manager.healthy_sources_count(), 0);
}
}