#![warn(clippy::pedantic)]
use std::error::Error;
use std::fs::File;
use std::io::{Read, Take};
use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
use std::{fmt, io};
const VERSION: &str = env!("CARGO_PKG_VERSION");
const READ_LIMIT: u64 = 25_000_000; const CCADB_API_ROOT: &[u8] = include_bytes!("DigiCertGlobalRootCA.crt");
pub type Result<T> = core::result::Result<T, FetchError>;
#[derive(Debug)]
#[non_exhaustive]
pub enum FetchError {
#[non_exhaustive]
Api { source: Box<ureq::Error> },
#[non_exhaustive]
DataSource {
source: Box<ccadb_csv::DataSourceError>,
},
#[non_exhaustive]
File { source: io::Error },
UnknownReport { name: String },
}
impl fmt::Display for FetchError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FetchError::Api { source } => {
write!(f, "failed to fetch CCADB CSV: {source}")
}
FetchError::DataSource { source } => {
write!(f, "failed to decode fetched CCADB CSV: {source}")
}
FetchError::File { source } => {
write!(f, "failed to write fetched CCADB CSV: {source}")
}
FetchError::UnknownReport { name } => {
write!(f, "unknown report type: {name}")
}
}
}
}
impl Error for FetchError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
FetchError::Api { source } => Some(source),
FetchError::DataSource { source } => Some(source),
FetchError::File { source } => Some(source),
FetchError::UnknownReport { .. } => None,
}
}
}
impl From<ureq::Error> for FetchError {
fn from(source: ureq::Error) -> Self {
let source = Box::new(source);
FetchError::Api { source }
}
}
impl From<ccadb_csv::DataSourceError> for FetchError {
fn from(source: ccadb_csv::DataSourceError) -> Self {
let source = Box::new(source);
FetchError::DataSource { source }
}
}
impl From<io::Error> for FetchError {
fn from(source: io::Error) -> Self {
FetchError::File { source }
}
}
pub enum ReportType {
AllCertRecords,
MozillaIncludedRoots,
}
impl ReportType {
#[must_use]
pub fn url(&self) -> &str {
match self {
ReportType::AllCertRecords => ccadb_csv::all_cert_records::URL,
ReportType::MozillaIncludedRoots => ccadb_csv::mozilla_included_roots::URL,
}
}
}
impl fmt::Display for ReportType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ReportType::AllCertRecords => write!(f, "all-cert-records"),
ReportType::MozillaIncludedRoots => write!(f, "mozilla-included-roots"),
}
}
}
impl TryFrom<&str> for ReportType {
type Error = FetchError;
fn try_from(report_type: &str) -> std::result::Result<Self, Self::Error> {
match report_type {
"all-cert-records" => Ok(ReportType::AllCertRecords),
"mozilla-included-roots" => Ok(ReportType::MozillaIncludedRoots),
&_ => Err(FetchError::UnknownReport {
name: report_type.to_owned(),
}),
}
}
}
pub fn fetch_report(report_type: &ReportType, output: impl AsRef<Path>) -> Result<u64> {
let mut output_file = File::create(output)?;
let mut csv_reader = read_csv_url(report_type.url())?;
Ok(io::copy(&mut csv_reader, &mut output_file)?)
}
fn read_csv_url(url: &str) -> Result<Take<Box<dyn Read + Send + Sync>>> {
let agent = ureq::builder()
.tls_config(Arc::new(tls_config()))
.timeout_read(Duration::from_secs(60))
.user_agent(format!("ccadb-csv-fetch/{VERSION}").as_ref())
.build();
let resp = agent.get(url).call()?;
Ok(resp.into_reader().take(READ_LIMIT))
}
fn tls_config() -> rustls::ClientConfig {
let mut root_store = rustls::RootCertStore::empty();
let anchor = webpki::TrustAnchor::try_from_cert_der(CCADB_API_ROOT).unwrap();
let anchors = vec![
rustls::OwnedTrustAnchor::from_subject_spki_name_constraints(
anchor.subject,
anchor.spki,
anchor.name_constraints,
),
];
root_store.add_server_trust_anchors(anchors.into_iter());
rustls::ClientConfig::builder()
.with_safe_default_cipher_suites()
.with_safe_default_kx_groups()
.with_safe_default_protocol_versions()
.unwrap()
.with_root_certificates(root_store)
.with_no_client_auth()
}