1#![forbid(unsafe_code)]
2use crate::{browser_storage::BrowserStorage, config::load_config};
5use chrono::DateTime;
6use js_sys::{Array, Uint8Array};
7use luct_client::deduplication::RequestDeduplicationClient;
8use luct_core::{
9 CertificateChain as CertChain, Fingerprint, log_list::v3::LogList, v1::SignedTreeHead,
10};
11use luct_otlsp::{OtlspClient, OtlspClientConfig};
12use luct_scanner::{Report, Scanner as CtScanner, ScannerConfig, ScannerImpl, Validated};
13use luct_store::{LastValCacheStore, LruCacheStore};
14use std::sync::Arc;
15use tracing::{Level, info};
16use tracing_wasm::WASMLayerConfigBuilder;
17use url::Url;
18use wasm_bindgen::{JsValue, prelude::wasm_bindgen};
19use web_time::{SystemTime, UNIX_EPOCH};
20
21mod browser_storage;
22mod config;
23mod extension_sys;
24mod local_store;
25
26const USER_AGENT: &str = concat!(
27 "luct-firefox/",
28 env!("CARGO_PKG_VERSION"),
29 " (https://github.com/Sawchord/luct/)"
30);
31
32struct ExtensionScannerImpl;
33
34impl ScannerImpl for ExtensionScannerImpl {
35 type Client = RequestDeduplicationClient<OtlspClient>;
36 type ReportStore = LruCacheStore<BrowserStorage<Fingerprint, Report>>;
37 type SthStore = LastValCacheStore<BrowserStorage<u64, Validated<SignedTreeHead>>>;
38}
39
40#[wasm_bindgen]
41extern "C" {
42 #[wasm_bindgen(js_namespace = console)]
43 fn log(s: &str);
44}
45
46#[wasm_bindgen(start)]
47pub fn start() -> Result<(), JsValue> {
48 console_error_panic_hook::set_once();
49
50 #[cfg(debug_assertions)]
51 let log_level = Level::DEBUG;
52
53 #[cfg(not(debug_assertions))]
54 let log_level = Level::DEBUG;
55
56 tracing_wasm::set_as_global_default_with_config(
57 WASMLayerConfigBuilder::default()
58 .set_max_level(log_level)
59 .build(),
60 );
61
62 Ok(())
63}
64
65#[wasm_bindgen]
66pub struct CertificateChain {
67 cert_chain: CertChain,
68}
69
70#[wasm_bindgen]
71impl CertificateChain {
72 #[wasm_bindgen(constructor)]
73 pub fn new(certs: Array) -> Result<Self, String> {
74 let cert_chain_bytes = certs
75 .to_vec()
76 .into_iter()
77 .map(|value| Uint8Array::from(value).to_vec())
78 .collect::<Vec<_>>();
79
80 let cert_chain =
81 CertChain::from_der_chain(&cert_chain_bytes).map_err(|err| err.to_string())?;
82
83 Ok(Self { cert_chain })
84 }
85
86 #[wasm_bindgen]
87 pub fn report(&self) -> Result<JsValue, String> {
88 let report = Report::from(&self.cert_chain);
89 let report = serde_wasm_bindgen::to_value(&report).map_err(|err| format!("{err}"))?;
90
91 Ok(report)
92 }
93}
94#[wasm_bindgen]
95pub struct Scanner {
96 scanner: CtScanner<ExtensionScannerImpl>,
97}
98
99#[wasm_bindgen]
100impl Scanner {
101 #[wasm_bindgen(constructor)]
102 pub fn new(log_list: String) -> Result<Self, String> {
103 let log_list: LogList = serde_json::from_str(&log_list).map_err(|err| format!("{err}"))?;
104 let logs = log_list.currently_active_logs();
105
106 let extension_config = load_config()?;
107 let scanner_config = ScannerConfig::try_from(&extension_config)?;
108 let otlsp_config = OtlspClientConfig::try_from(&extension_config)?;
109
110 let client = RequestDeduplicationClient::new(OtlspClient::new(otlsp_config));
111
112 let report_cache =
113 BrowserStorage::<Fingerprint, Report>::new_local_store("report".to_string())?;
114 let report_cache = LruCacheStore::new(report_cache, extension_config.report_lru_cache());
115
116 let time_source = || {
117 DateTime::from_timestamp_millis(
118 SystemTime::now()
119 .duration_since(UNIX_EPOCH)
120 .unwrap()
121 .as_millis() as i64,
122 )
123 .unwrap()
124 };
125
126 let mut scanner = CtScanner::new(scanner_config, report_cache, client, time_source);
127
128 for log in logs {
129 let name = log.description();
130 scanner.add_log(
131 &log,
132 LastValCacheStore::new(BrowserStorage::new_local_store(format!("sth/{name}"))?),
133 );
134 }
135
136 info!("Initialized scanner");
137
138 Ok(Scanner { scanner })
139 }
140
141 #[wasm_bindgen]
142 pub async fn collect_report(
143 &self,
144 url: String,
145 certs: CertificateChain,
146 ) -> Result<Option<JsValue>, String> {
147 if self.is_recursion(&url)? {
149 tracing::trace!("Skipping request to log itself to prevent recursion");
150 return Ok(None);
151 }
152
153 let report = self
155 .scanner
156 .collect_report(Arc::new(certs.cert_chain))
157 .await
158 .map_err(|err| err.to_string())?;
159
160 let report = serde_wasm_bindgen::to_value(&report).map_err(|err| format!("{err}"))?;
161
162 Ok(Some(report))
163 }
164
165 #[wasm_bindgen]
166 pub fn is_report_safe(report: JsValue) -> Result<bool, String> {
167 let report: Report =
168 serde_wasm_bindgen::from_value(report).map_err(|err| format!("{err}"))?;
169
170 match report.get_error() {
171 Some(_) => Ok(false),
172 None => Ok(true),
173 }
174 }
175
176 fn is_recursion(&self, url: &str) -> Result<bool, String> {
181 let url = Url::parse(url).map_err(|err| format!("{err}"))?;
182 let is_recusion = self.scanner.logs().any(|log| {
183 log.config().url().domain() == url.domain()
184 || log
185 .config()
186 .tile_url()
187 .as_ref()
188 .map(|tile_url| tile_url.domain())
189 == Some(url.domain())
190 });
191
192 Ok(is_recusion)
193 }
194
195 #[wasm_bindgen]
196 pub async fn basic_statistics(&self) -> Result<JsValue, String> {
197 let stats = self.scanner.basic_statistics().await;
198 serde_wasm_bindgen::to_value(&stats).map_err(|err| format!("{err}"))
199 }
200}
201
202#[cfg(test)]
204mod test {
205 use super::*;
206 use luct_test::utils::test_tracing;
207 use serde::{Deserialize, Serialize};
208 use wasm_bindgen_test::wasm_bindgen_test;
209
210 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
211
212 #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
215 struct TestStruct {
216 a: u64,
217 b: String,
218 }
219
220 #[wasm_bindgen_test]
221 fn validated_js_value_roundtrip() {
222 test_tracing();
223
224 let test_data = Validated::new(TestStruct {
225 a: 5,
226 b: String::from("Test"),
227 });
228 let js = serde_wasm_bindgen::to_value(&test_data).unwrap();
229 let new_test_data = serde_wasm_bindgen::from_value(js).unwrap();
230 assert_eq!(test_data, new_test_data)
231 }
232
233 #[wasm_bindgen_test]
234 fn legacy_validated_js_value() {
235 test_tracing();
236
237 let test_data = Validated::new(TestStruct {
238 a: 5,
239 b: String::from("Test"),
240 });
241 let data_js = serde_wasm_bindgen::to_value(&test_data.inner()).unwrap();
242 let now_js = serde_wasm_bindgen::to_value(
243 &test_data
244 .validated_at()
245 .duration_since(UNIX_EPOCH)
246 .unwrap()
247 .as_millis(),
248 )
249 .unwrap();
250
251 let legacy_validated = Array::new();
252 legacy_validated.push(&now_js);
253 legacy_validated.push(&data_js);
254
255 let new_test_data: Validated<TestStruct> =
256 serde_wasm_bindgen::from_value(legacy_validated.into()).unwrap();
257 assert_eq!(test_data, new_test_data)
258 }
259}