Skip to main content

luct_extension/
lib.rs

1#![forbid(unsafe_code)]
2//! Wrapper around [`Scanner`](CtScanner) to be used in a javascript environment.
3
4use 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        // Check that this is not a recursion
148        if self.is_recursion(&url)? {
149            tracing::trace!("Skipping request to log itself to prevent recursion");
150            return Ok(None);
151        }
152
153        // Generate the report
154        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    /// Check that we are not requesting from a URL that is the log itself
177    ///
178    /// This is necessary as in the browser, the calls to the logs go through the same
179    /// security context and will be intercepted by the browser
180    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// TODO: Full scenario test
203#[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    // These tests check that the custom deserialization format for
213    // `Validated<T>` also work with `serde_wasm_bindgen`
214    #[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}