ctclient_async/
google_log_list.rs1use crate::Error;
4use crate::internal::new_http_client;
5
6use serde::Deserialize;
7use std::collections::HashMap;
8
9#[derive(Debug, Deserialize, Clone)]
10struct ResponseJSON {
11 operators: Vec<OperatorJSON>,
12}
13
14#[derive(Debug, Deserialize, Clone)]
15struct OperatorJSON {
16 name: String,
17 email: Vec<String>,
18 logs: Vec<LogJson>,
19}
20
21#[derive(Debug, Deserialize, Clone)]
22struct LogJson {
23 key: String,
24 log_id: String,
25 mmd: u64,
26 url: String,
27 state: HashMap<String, serde_json::Value>,
28 description: String,
29}
30
31#[derive(Debug, Clone)]
33pub struct LogList {
34 pub map_id_to_log: HashMap<Vec<u8>, Log>,
35}
36
37#[derive(Debug, Clone)]
39pub struct Log {
40 pub pub_key: Vec<u8>,
41 pub base_url: String,
42 pub state: LogState,
43 pub description: String,
44}
45
46#[derive(Debug, PartialEq, Eq, Copy, Clone)]
47pub enum LogState {
48 Pending,
49 Qualified,
50 Usable,
51 Readonly,
52 Retired,
53 Rejected,
54}
55
56impl LogList {
57 pub async fn get() -> Result<LogList, Error> {
59 LogList::get_with_url("https://www.gstatic.com/ct/log_list/v3/log_list.json").await
60 }
61
62 pub async fn get_with_url(url: &str) -> Result<LogList, Error> {
64 let client = new_http_client()?;
65 let json: ResponseJSON = client
66 .get(url)
67 .send()
68 .await
69 .map_err(Error::NetIO)?
70 .json()
71 .await
72 .map_err(|e| Error::MalformedResponseBody(format!("{}", e)))?;
73 let mut hm: HashMap<Vec<u8>, Log> =
74 HashMap::with_capacity(json.operators.iter().map(|x| x.logs.len()).sum());
75 fn b64_dec_err(e: base64::DecodeError) -> Error {
76 Error::MalformedResponseBody(format!("Unable to decode base64: {}", e))
77 }
78 for op in json.operators.iter() {
79 for log in op.logs.iter() {
80 let log_id = base64::decode(&log.log_id).map_err(b64_dec_err)?;
81 let pub_key = base64::decode(&log.key).map_err(b64_dec_err)?;
82 let base_url = log.url.to_owned();
83 if hm.contains_key(&log_id) {
84 return Err(Error::MalformedResponseBody(
85 "Multiple logs returned with the same id.".to_owned(),
86 ));
87 }
88 let state_keys: Vec<&str> = log.state.keys().map(|x| &x[..]).collect();
89 use LogState::*;
90 let log_state = match &state_keys[..] {
91 ["pending"] => Pending,
92 ["qualified"] => Qualified,
93 ["usable"] => Usable,
94 ["readonly"] => Readonly,
95 ["retired"] => Retired,
96 ["rejected"] => Rejected,
97 _ => {
98 return Err(Error::MalformedResponseBody(format!(
99 "Invalid log state object: {:?}",
100 &log.state
101 )));
102 }
103 };
104 hm.insert(
105 log_id,
106 Log {
107 pub_key,
108 base_url,
109 state: log_state,
110 description: log.description.clone(),
111 },
112 );
113 }
114 }
115
116 Ok(LogList { map_id_to_log: hm })
117 }
118
119 pub fn find_by_id<'a>(&'a self, id: &[u8]) -> Option<&'a Log> {
121 self.map_id_to_log.get(id)
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128 #[tokio::test]
129 async fn test() {
130 let ll = LogList::get().await.unwrap();
131 let nb_logs = ll.map_id_to_log.len();
132 assert!(nb_logs > 0);
133 assert_eq!(
134 ll.find_by_id(&base64::decode("2AlVO5RPev/IFhlvlE+Fq7D4/F6HVSYPFdEucrtFSxQ=").unwrap())
135 .unwrap()
136 .base_url,
137 "https://ct.googleapis.com/logs/eu1/xenon2026h2/"
138 );
139 }
140}