diff --git a/Cargo.toml b/Cargo.toml index 8039a7d..a80f507 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ chrono = "0.4" clap = { version = "4.4.14", features = ["derive"] } curve25519-dalek = { version = "4", default-features = false, features = ["serde", "rand_core", "digest"] } ed25519-dalek = { version = "2", features = ["serde", "rand_core"] } +faketime = { version = "0.2", optional = true } futures = "0.3.30" hkdf = "0.12" http = "1" @@ -41,6 +42,7 @@ x25519-dalek = { version = "2", features = ["serde", "static_secrets"] } [dev-dependencies] base64 = "0.21.7" +faketime = "0.2" [features] -simulation = ["lox_cli"] +simulation = ["faketime", "lox_cli"] diff --git a/src/lib.rs b/src/lib.rs index c46d1dc..f3ac791 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,6 +12,13 @@ use std::{ }; use x25519_dalek::{PublicKey, StaticSecret}; +#[cfg(any(feature = "simulation", test))] +use { + chrono::{DateTime, Utc}, + julianday::JulianDay, + std::{path::Path, time::UNIX_EPOCH}, +}; + pub mod analysis; pub mod bridge_verification_info; pub mod crypto; @@ -46,8 +53,20 @@ lazy_static! { /// We will accept reports up to this many days old. pub const MAX_BACKDATE: u32 = 3; -/// Get Julian date +#[cfg(any(feature = "simulation", test))] +const FAKETIME_FILE: &str = "/tmp/troll-patrol-faketime"; + +/// Get real or simulated Julian date pub fn get_date() -> u32 { + // If this is a simulation, get the simulated date + #[cfg(any(feature = "simulation", test))] + return get_simulated_date(); + + // If we're not running a simulation, return today's date + get_real_date() +} + +fn get_real_date() -> u32 { time::OffsetDateTime::now_utc() .date() .to_julian_day() @@ -55,6 +74,38 @@ pub fn get_date() -> u32 { .unwrap() } +#[cfg(any(feature = "simulation", test))] +fn get_simulated_date() -> u32 { + faketime::enable(Path::new(FAKETIME_FILE)); + JulianDay::from(DateTime::::from(UNIX_EPOCH + faketime::unix_time()).date_naive()) + .inner() + .try_into() + .unwrap() +} + +#[cfg(any(feature = "simulation", test))] +pub fn set_simulated_date(date: u32) { + faketime::enable(Path::new(FAKETIME_FILE)); + let unix_date_ms = DateTime::::from_naive_utc_and_offset( + JulianDay::new(date.try_into().unwrap()).to_date().into(), + Utc, + ) + .timestamp_millis(); + //str.push_str(format!("\nbridge-stats-end {} 23:59:59 (86400 s)", date).as_str()); + faketime::write_millis(Path::new(FAKETIME_FILE), unix_date_ms.try_into().unwrap()).unwrap(); +} + +#[cfg(any(feature = "simulation", test))] +pub fn increment_simulated_date() { + let date = get_date(); + set_simulated_date(date + 1); +} + +#[cfg(any(feature = "simulation", test))] +pub fn reset_simulated_date() { + set_simulated_date(get_real_date()); +} + #[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)] pub enum BridgeDistributor { Lox, diff --git a/src/tests.rs b/src/tests.rs index 1adcd07..61ba7ae 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -8,3 +8,4 @@ mod reports { mod negative_reports; mod positive_reports; } +mod simulated_time; diff --git a/src/tests/simulated_time.rs b/src/tests/simulated_time.rs new file mode 100644 index 0000000..e9d564e --- /dev/null +++ b/src/tests/simulated_time.rs @@ -0,0 +1,153 @@ +use crate::*; + +use hyper::{ + body, + header::HeaderValue, + service::{make_service_fn, service_fn}, + Body, Client, Method, Request, Response, Server, +}; +use std::{convert::Infallible, net::SocketAddr, time::Duration}; +use tokio::{spawn, time::sleep}; + +use lox_library::bridge_table::BridgeLine; +use rand::RngCore; + +// Prepare HTTP Response for successful Server Request +fn prepare_header(response: String) -> Response { + let mut resp = Response::new(Body::from(response)); + resp.headers_mut() + .insert("Access-Control-Allow-Origin", HeaderValue::from_static("*")); + resp +} + +// Lets the client get or set the simulated date +async fn date_server(req: Request) -> Result, Infallible> { + match req.method() { + &Method::OPTIONS => Ok(Response::builder() + .header("Access-Control-Allow-Origin", HeaderValue::from_static("*")) + .header("Access-Control-Allow-Headers", "accept, content-type") + .header("Access-Control-Allow-Methods", "POST") + .status(200) + .body(Body::from("Allow POST")) + .unwrap()), + _ => match req.uri().path() { + "/getdate" => Ok::<_, Infallible>(prepare_header(get_date().to_string())), + "/setdate" => Ok::<_, Infallible>({ + let bytes = body::to_bytes(req.into_body()).await.unwrap(); + let date: u32 = std::str::from_utf8(&bytes).unwrap().parse().unwrap(); + set_simulated_date(date); + prepare_header("OK".to_string()) + }), + _ => Ok::<_, Infallible>(prepare_header("Wrong path".to_string())), + }, + } +} + +// Serves the function to let the client get or set the simulated date +async fn server() { + let addr = SocketAddr::from(([127, 0, 0, 1], 9999)); + + let make_svc = make_service_fn(|_conn| async { Ok::<_, Infallible>(service_fn(date_server)) }); + + let server = Server::bind(&addr).serve(make_svc); + + if let Err(e) = server.await { + eprintln!("server error: {}", e); + } +} + +async fn get_date_from_server() -> u32 { + download("http://localhost:9999/getdate") + .await + .unwrap() + .parse() + .unwrap() +} + +async fn set_date_on_server(date: u32) { + let client = Client::new(); + let req = Request::builder() + .method(Method::POST) + .uri( + "http://localhost:9999/setdate" + .parse::() + .unwrap(), + ) + .body(Body::from(date.to_string())) + .unwrap(); + client.request(req).await.unwrap(); +} + +#[tokio::test] +async fn test_simulated_time() { + // Reset date in case we had a previous simulated date + reset_simulated_date(); + + // Get date + let date = get_date(); + + // Check that simulated date matches real date + let real_date: u32 = time::OffsetDateTime::now_utc() + .date() + .to_julian_day() + .try_into() + .unwrap(); + + assert_eq!(date, real_date); + + // Check that incrementing simulated date works + increment_simulated_date(); + assert_eq!(date + 1, get_date()); + + // Create dummy bridge + let mut rng = rand::thread_rng(); + let mut bl = BridgeLine::default(); + rng.fill_bytes(&mut bl.fingerprint); + let negative_report = + NegativeReport::from_bridgeline(bl, "ru".to_string(), BridgeDistributor::Lox); + + // No issue + let negative_report = negative_report + .to_serializable_report() + .to_report() + .unwrap(); + + // Advance time so the report is no longer valid + set_simulated_date(get_date() + MAX_BACKDATE + 1); + + // Report fails to deserialize + let negative_report_result = negative_report.to_serializable_report().to_report(); + assert!(negative_report_result.is_err()); + + // Ensure one thread CAN influence the time for other threads + spawn(async move { + server().await; + }); + + // Give server time to start + sleep(Duration::new(1, 0)).await; + + // Increment date + increment_simulated_date(); + let date = get_date(); + + // Get date from server + let remote_date = get_date_from_server().await; + assert_eq!(date, remote_date); + + // Increase date a lot + set_simulated_date(get_date() + 100); + let date = get_date(); + + // Check that date from server matches + let remote_date = get_date_from_server().await; + assert_eq!(date, remote_date); + + // Have server increase date + let old_date = get_date(); + let new_date = old_date + 500; + set_date_on_server(new_date).await; + + // Check that we have the date as changed in the other thread + assert_eq!(get_date(), new_date); +}