#![forbid(unsafe_code)]
use crate::{browser_storage::BrowserStorage, config::load_config};
use chrono::DateTime;
use js_sys::{Array, Uint8Array};
use luct_client::deduplication::RequestDeduplicationClient;
use luct_core::{
CertificateChain as CertChain, Fingerprint, log_list::v3::LogList, v1::SignedTreeHead,
};
use luct_otlsp::{OtlspClient, OtlspClientConfig};
use luct_scanner::{Report, Scanner as CtScanner, ScannerConfig, ScannerImpl, Validated};
use luct_store::{LastValCacheStore, LruCacheStore};
use std::sync::Arc;
use tracing::{Level, info};
use tracing_wasm::WASMLayerConfigBuilder;
use url::Url;
use wasm_bindgen::{JsValue, prelude::wasm_bindgen};
use web_time::{SystemTime, UNIX_EPOCH};
mod browser_storage;
mod config;
mod extension_sys;
mod local_store;
const USER_AGENT: &str = concat!(
"luct-firefox/",
env!("CARGO_PKG_VERSION"),
" (https://github.com/Sawchord/luct/)"
);
struct ExtensionScannerImpl;
impl ScannerImpl for ExtensionScannerImpl {
type Client = RequestDeduplicationClient<OtlspClient>;
type ReportStore = LruCacheStore<BrowserStorage<Fingerprint, Report>>;
type SthStore = LastValCacheStore<BrowserStorage<u64, Validated<SignedTreeHead>>>;
}
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console)]
fn log(s: &str);
}
#[wasm_bindgen(start)]
pub fn start() -> Result<(), JsValue> {
console_error_panic_hook::set_once();
#[cfg(debug_assertions)]
let log_level = Level::DEBUG;
#[cfg(not(debug_assertions))]
let log_level = Level::DEBUG;
tracing_wasm::set_as_global_default_with_config(
WASMLayerConfigBuilder::default()
.set_max_level(log_level)
.build(),
);
Ok(())
}
#[wasm_bindgen]
pub struct CertificateChain {
cert_chain: CertChain,
}
#[wasm_bindgen]
impl CertificateChain {
#[wasm_bindgen(constructor)]
pub fn new(certs: Array) -> Result<Self, String> {
let cert_chain_bytes = certs
.to_vec()
.into_iter()
.map(|value| Uint8Array::from(value).to_vec())
.collect::<Vec<_>>();
let cert_chain =
CertChain::from_der_chain(&cert_chain_bytes).map_err(|err| err.to_string())?;
Ok(Self { cert_chain })
}
#[wasm_bindgen]
pub fn report(&self) -> Result<JsValue, String> {
let report = Report::from(&self.cert_chain);
let report = serde_wasm_bindgen::to_value(&report).map_err(|err| format!("{err}"))?;
Ok(report)
}
}
#[wasm_bindgen]
pub struct Scanner {
scanner: CtScanner<ExtensionScannerImpl>,
}
#[wasm_bindgen]
impl Scanner {
#[wasm_bindgen(constructor)]
pub fn new(log_list: String) -> Result<Self, String> {
let log_list: LogList = serde_json::from_str(&log_list).map_err(|err| format!("{err}"))?;
let logs = log_list.currently_active_logs();
let extension_config = load_config()?;
let scanner_config = ScannerConfig::try_from(&extension_config)?;
let otlsp_config = OtlspClientConfig::try_from(&extension_config)?;
let client = RequestDeduplicationClient::new(OtlspClient::new(otlsp_config));
let report_cache =
BrowserStorage::<Fingerprint, Report>::new_local_store("report".to_string())?;
let report_cache = LruCacheStore::new(report_cache, extension_config.report_lru_cache());
let time_source = || {
DateTime::from_timestamp_millis(
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis() as i64,
)
.unwrap()
};
let mut scanner = CtScanner::new(scanner_config, report_cache, client, time_source);
for log in logs {
let name = log.description();
scanner.add_log(
&log,
LastValCacheStore::new(BrowserStorage::new_local_store(format!("sth/{name}"))?),
);
}
info!("Initialized scanner");
Ok(Scanner { scanner })
}
#[wasm_bindgen]
pub async fn collect_report(
&self,
url: String,
certs: CertificateChain,
) -> Result<Option<JsValue>, String> {
if self.is_recursion(&url)? {
tracing::trace!("Skipping request to log itself to prevent recursion");
return Ok(None);
}
let report = self
.scanner
.collect_report(Arc::new(certs.cert_chain))
.await
.map_err(|err| err.to_string())?;
let report = serde_wasm_bindgen::to_value(&report).map_err(|err| format!("{err}"))?;
Ok(Some(report))
}
#[wasm_bindgen]
pub fn is_report_safe(report: JsValue) -> Result<bool, String> {
let report: Report =
serde_wasm_bindgen::from_value(report).map_err(|err| format!("{err}"))?;
match report.get_error() {
Some(_) => Ok(false),
None => Ok(true),
}
}
fn is_recursion(&self, url: &str) -> Result<bool, String> {
let url = Url::parse(url).map_err(|err| format!("{err}"))?;
let is_recusion = self.scanner.logs().any(|log| {
log.config().url().domain() == url.domain()
|| log
.config()
.tile_url()
.as_ref()
.map(|tile_url| tile_url.domain())
== Some(url.domain())
});
Ok(is_recusion)
}
#[wasm_bindgen]
pub async fn basic_statistics(&self) -> Result<JsValue, String> {
let stats = self.scanner.basic_statistics().await;
serde_wasm_bindgen::to_value(&stats).map_err(|err| format!("{err}"))
}
}
#[cfg(test)]
mod test {
use super::*;
use luct_test::utils::test_tracing;
use serde::{Deserialize, Serialize};
use wasm_bindgen_test::wasm_bindgen_test;
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
struct TestStruct {
a: u64,
b: String,
}
#[wasm_bindgen_test]
fn validated_js_value_roundtrip() {
test_tracing();
let test_data = Validated::new(TestStruct {
a: 5,
b: String::from("Test"),
});
let js = serde_wasm_bindgen::to_value(&test_data).unwrap();
let new_test_data = serde_wasm_bindgen::from_value(js).unwrap();
assert_eq!(test_data, new_test_data)
}
#[wasm_bindgen_test]
fn legacy_validated_js_value() {
test_tracing();
let test_data = Validated::new(TestStruct {
a: 5,
b: String::from("Test"),
});
let data_js = serde_wasm_bindgen::to_value(&test_data.inner()).unwrap();
let now_js = serde_wasm_bindgen::to_value(
&test_data
.validated_at()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis(),
)
.unwrap();
let legacy_validated = Array::new();
legacy_validated.push(&now_js);
legacy_validated.push(&data_js);
let new_test_data: Validated<TestStruct> =
serde_wasm_bindgen::from_value(legacy_validated.into()).unwrap();
assert_eq!(test_data, new_test_data)
}
}