Merge upstream changes

This commit is contained in:
Vecna 2024-04-26 23:05:00 -04:00
commit 5c0376cd56
20 changed files with 1612 additions and 448 deletions

1236
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -16,11 +16,12 @@ readme = "README.md"
[dependencies] [dependencies]
julianday = "1.2.0" julianday = "1.2.0"
base64 = "0.21.7" base64 = "0.22.0"
hyper = { version = "0.14.28", features = ["deprecated", "backports","server"] } hyper = { version = "0.14.28", features = ["deprecated", "backports","server"] }
hex = "0.4.3"
hex_fmt = "0.3" hex_fmt = "0.3"
futures = "0.3.30" futures = "0.3.30"
time = "0.3.34" time = "0.3.36"
tokio = { version = "1", features = ["full", "macros", "signal"] } tokio = { version = "1", features = ["full", "macros", "signal"] }
rand = "0.8.5" rand = "0.8.5"
reqwest = { version = "0.11", features = ["json", "stream"]} reqwest = { version = "0.11", features = ["json", "stream"]}
@ -30,7 +31,7 @@ lox-zkp = { git = "https://gitlab.torproject.org/onyinyang/lox-zkp", version = "
lox-library = { path = "../lox-library", version = "0.1.0"} lox-library = { path = "../lox-library", version = "0.1.0"}
lox_utils = { path = "../lox-utils", version = "0.1.0"} lox_utils = { path = "../lox-utils", version = "0.1.0"}
rdsys_backend = { path = "../rdsys-backend-api", version = "0.2"} rdsys_backend = { path = "../rdsys-backend-api", version = "0.2"}
clap = { version = "4.5.2", features = ["derive"] } clap = { version = "4.5.4", features = ["derive"] }
serde_json = "1.0.113" serde_json = "1.0.113"
prometheus = "0.13.3" prometheus = "0.13.3"
sled = "0.34.7" sled = "0.34.7"
@ -42,5 +43,5 @@ array-bytes = "6.2.0"
sha1 = "0.10" sha1 = "0.10"
[dependencies.chrono] [dependencies.chrono]
version = "0.4.34" version = "0.4.38"
features = ["serde"] features = ["serde"]

View File

@ -0,0 +1,6 @@
{
"watched_blockages": [
"RU"
],
"percent_spares": 50
}

View File

@ -81,7 +81,7 @@ impl DB {
extra_bridges: Arc::new(Mutex::new(Vec::new())), extra_bridges: Arc::new(Mutex::new(Vec::new())),
to_be_replaced_bridges: Arc::new(Mutex::new(Vec::new())), to_be_replaced_bridges: Arc::new(Mutex::new(Vec::new())),
tp_bridge_infos: Arc::new(Mutex::new(HashMap::< tp_bridge_infos: Arc::new(Mutex::new(HashMap::<
[u8; 20], String,
BridgeVerificationInfo, BridgeVerificationInfo,
>::new())), >::new())),
metrics, metrics,

View File

@ -0,0 +1,151 @@
use crate::resource_parser::ACCEPTED_HOURS_OF_FAILURE;
use chrono::{Duration, Utc};
use rand::{Rng, RngCore};
use rdsys_backend::proto::{Resource, ResourceState, TestResults};
use std::collections::HashMap;
#[derive(Default)]
pub struct TestResourceState {
pub rstate: ResourceState,
}
impl TestResourceState {
// A previously working resources become not_working but within accepted failure time
pub fn working_with_accepted_failures(&mut self) {
match &mut self.rstate.working {
Some(resources) => {
if let Some(resource) = resources.pop() {
self.add_not_working_to_rstate(resource)
}
}
None => {
panic!("rstate.working Empty")
}
}
assert_ne!(self.rstate.working, None);
assert_eq!(self.rstate.not_working, None);
}
// Block resources that are working. Targeted blocked regions are specified in bridge_config.json
pub fn block_working(&mut self) {
match &mut self.rstate.working {
Some(resources) => {
for resource in resources {
resource.blocked_in = HashMap::from([
("AS".to_owned(), true),
("IR".to_owned(), false),
("RU".to_owned(), false),
("CN".to_owned(), false),
("SA".to_owned(), false),
]);
}
}
None => {
panic!("rstate.working Empty")
}
}
assert_ne!(self.rstate.working, None);
assert_eq!(self.rstate.not_working, None);
}
// Add a resource that is working
pub fn add_working_resource(&mut self) {
let working_resource = make_resource(
HashMap::from([
("AS".to_owned(), false),
("IR".to_owned(), false),
("RU".to_owned(), false),
("CN".to_owned(), false),
("SA".to_owned(), false),
]),
ACCEPTED_HOURS_OF_FAILURE - 12,
);
self.add_working_to_rstate(working_resource);
}
// Add a not-working resource that has been failing for 1 hour longer than the accepted threshold
pub fn add_not_working_resource(&mut self) {
let not_working_resource = make_resource(
HashMap::from([
("AS".to_owned(), false),
("IR".to_owned(), false),
("RU".to_owned(), false),
("CN".to_owned(), false),
("SA".to_owned(), false),
]),
ACCEPTED_HOURS_OF_FAILURE + 1,
);
self.add_not_working_to_rstate(not_working_resource);
}
// Add resource to rstate's working field
pub fn add_working_to_rstate(&mut self, working_resource: Resource) {
match &mut self.rstate.working {
Some(resources) => {
resources.push(working_resource);
}
None => {
self.rstate.working = Some(vec![working_resource]);
}
}
}
// Add resource to rstate's not_working field
pub fn add_not_working_to_rstate(&mut self, not_working_resource: Resource) {
match &mut self.rstate.not_working {
Some(resources) => {
resources.push(not_working_resource);
}
None => {
self.rstate.not_working = Some(vec![not_working_resource]);
}
}
}
}
pub fn make_resource(blocked_in: HashMap<String, bool>, last_passed: i64) -> Resource {
let mut flags = HashMap::new();
flags.insert(String::from("fast"), true);
flags.insert(String::from("stable"), true);
let mut params = HashMap::new();
params.insert(
String::from("password"),
String::from("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"),
);
Resource {
r#type: String::from("obfs4"),
blocked_in,
test_result: TestResults {
last_passed: Utc::now() - Duration::hours(last_passed),
},
protocol: String::from("tcp"),
address: gen_ip(),
port: gen_port(),
fingerprint: gen_fingerprint(),
or_addresses: None,
distribution: String::from("https"),
flags: Some(flags),
params: Some(params),
}
}
pub fn gen_fingerprint() -> String {
let mut rng = rand::thread_rng();
let mut fingerprint_array: [u8; 16] = [0; 16];
rng.fill_bytes(&mut fingerprint_array);
hex::encode_upper(fingerprint_array)
}
pub fn gen_port() -> u16 {
rand::thread_rng().gen_range(0..u16::MAX)
}
pub fn gen_ip() -> String {
let i = rand::thread_rng().gen_range(1..u8::MAX);
let ii = rand::thread_rng().gen_range(1..u8::MAX);
let iii = rand::thread_rng().gen_range(1..u8::MAX);
let iv = rand::thread_rng().gen_range(1..u8::MAX);
format!("{}.{}.{}.{}", i, ii, iii, iv)
}

View File

@ -38,7 +38,7 @@ pub struct LoxServerContext {
pub extra_bridges: Arc<Mutex<Vec<BridgeLine>>>, pub extra_bridges: Arc<Mutex<Vec<BridgeLine>>>,
pub to_be_replaced_bridges: Arc<Mutex<Vec<BridgeLine>>>, pub to_be_replaced_bridges: Arc<Mutex<Vec<BridgeLine>>>,
// Map of bridge fingerprint to values needed to verify TP reports // Map of bridge fingerprint to values needed to verify TP reports
pub tp_bridge_infos: Arc<Mutex<HashMap<[u8; 20], BridgeVerificationInfo>>>, pub tp_bridge_infos: Arc<Mutex<HashMap<String, BridgeVerificationInfo>>>,
#[serde(skip)] #[serde(skip)]
pub metrics: Metrics, pub metrics: Metrics,
} }
@ -82,7 +82,10 @@ impl LoxServerContext {
for bridge in blocked_bridgelines { for bridge in blocked_bridgelines {
let res = self.mark_blocked(bridge); let res = self.mark_blocked(bridge);
if res { if res {
println!("BridgeLine {:?} successfully marked unreachable", bridge); println!(
"BridgeLine {:?} successfully marked unreachable",
bridge.uid_fingerprint
);
self.metrics.blocked_bridges.inc(); self.metrics.blocked_bridges.inc();
} else { } else {
println!( println!(
@ -107,6 +110,7 @@ impl LoxServerContext {
self.metrics.new_bridges.inc(); self.metrics.new_bridges.inc();
} }
} }
accounted_for_bridges accounted_for_bridges
} }
@ -127,7 +131,7 @@ impl LoxServerContext {
if res { if res {
println!( println!(
"Blocked BridgeLine {:?} successfully marked unreachable", "Blocked BridgeLine {:?} successfully marked unreachable",
bridge bridge.uid_fingerprint
); );
self.metrics.blocked_bridges.inc(); self.metrics.blocked_bridges.inc();
} else { } else {
@ -154,33 +158,37 @@ impl LoxServerContext {
// Next, handle the failing bridges. If resource last passed tests >= ACCEPTED_HOURS_OF_FAILURE ago, // Next, handle the failing bridges. If resource last passed tests >= ACCEPTED_HOURS_OF_FAILURE ago,
// it should be replaced with a working resource and be removed from the bridgetable. // it should be replaced with a working resource and be removed from the bridgetable.
for bridge in failing { for bridge in failing {
let res = self.replace_with_new(bridge); match self.replace_with_new(bridge) {
if res == lox_library::ReplaceSuccess::Replaced { lox_library::ReplaceSuccess::Replaced => {
println!( println!(
"Failing BridgeLine {:?} successfully replaced.", "Failing BridgeLine {:?} successfully replaced.",
bridge.uid_fingerprint bridge.uid_fingerprint
); );
accounted_for_bridges.push(bridge.uid_fingerprint); accounted_for_bridges.push(bridge.uid_fingerprint);
self.metrics.removed_bridges.inc(); self.metrics.removed_bridges.inc();
} else if res == lox_library::ReplaceSuccess::NotReplaced { }
// Add the bridge to the list of to_be_replaced bridges in the Lox context and try lox_library::ReplaceSuccess::NotReplaced => {
// again to replace at the next update (nothing changes in the Lox Authority) // Add the bridge to the list of to_be_replaced bridges in the Lox context and try
println!( // again to replace at the next update (nothing changes in the Lox Authority)
"Failing BridgeLine {:?} NOT replaced, saved for next update!", println!(
bridge.uid_fingerprint "Failing BridgeLine {:?} NOT replaced, saved for next update!",
); bridge.uid_fingerprint
self.metrics.existing_or_updated_bridges.inc(); );
accounted_for_bridges.push(bridge.uid_fingerprint); self.metrics.existing_or_updated_bridges.inc();
} else { accounted_for_bridges.push(bridge.uid_fingerprint);
// NotFound }
assert!( lox_library::ReplaceSuccess::Removed => {
res == lox_library::ReplaceSuccess::NotFound, println!(
"ReplaceSuccess incorrectly set" "Failing BridgeLine {:?} successfully removed.",
); bridge.uid_fingerprint
println!( );
accounted_for_bridges.push(bridge.uid_fingerprint);
self.metrics.removed_bridges.inc();
}
lox_library::ReplaceSuccess::NotFound => println!(
"Failing BridgeLine {:?} not found in bridge table.", "Failing BridgeLine {:?} not found in bridge table.",
bridge.uid_fingerprint bridge.uid_fingerprint
); ),
} }
} }
accounted_for_bridges accounted_for_bridges
@ -204,33 +212,36 @@ impl LoxServerContext {
accounted_for_bridges, accounted_for_bridges,
); );
} }
let mut ba_clone = self.ba.lock().unwrap();
let total_reachable = ba_clone.bridge_table.reachable.len();
match total_reachable.cmp(&accounted_for_bridges.len()) {
Ordering::Greater => {
let unaccounted_for = ba_clone.find_and_remove_unaccounted_for_bridges(accounted_for_bridges);
for bridgeline in unaccounted_for {
match self.replace_with_new(bridgeline) {
lox_library::ReplaceSuccess::Replaced => {
println!("BridgeLine {:?} not found in rdsys update was successfully replaced.", bridgeline.uid_fingerprint);
self.metrics.removed_bridges.inc();
}
lox_library::ReplaceSuccess::NotReplaced => {
// Try again to replace at the next update (nothing changes in the Lox Authority)
println!("BridgeLine {:?} not found in rdsys update NOT replaced, saved for next update!",
bridgeline.uid_fingerprint);
self.metrics.existing_or_updated_bridges.inc();
}
lox_library::ReplaceSuccess::NotFound => println!(
"BridgeLine {:?} no longer in reachable bridges.",
bridgeline.uid_fingerprint
),
}
}
}
Ordering::Less => println!("Something unexpected occurred: The number of reachable bridges should not be less than those updated from rdsys"),
_ => (),
let unaccounted_for = self
.ba
.lock()
.unwrap()
.find_and_remove_unaccounted_for_bridges(accounted_for_bridges);
for bridgeline in unaccounted_for {
match self.replace_with_new(bridgeline) {
lox_library::ReplaceSuccess::Replaced => {
println!(
"BridgeLine {:?} not found in rdsys update was successfully replaced.",
bridgeline.uid_fingerprint
);
self.metrics.removed_bridges.inc();
}
lox_library::ReplaceSuccess::Removed => {
println!("BridgeLine {:?} not found in rdsys update was not distributed to a bucket so was removed", bridgeline.uid_fingerprint);
self.metrics.removed_bridges.inc();
}
lox_library::ReplaceSuccess::NotReplaced => {
// Try again to replace at the next update (nothing changes in the Lox Authority)
println!("BridgeLine {:?} not found in rdsys update NOT replaced, saved for next update!",
bridgeline.uid_fingerprint);
self.metrics.existing_or_updated_bridges.inc();
}
lox_library::ReplaceSuccess::NotFound => println!(
"BridgeLine {:?} no longer in reachable bridges.",
bridgeline.uid_fingerprint
),
}
} }
// Finally, assign any extra_bridges to new buckets if there are enough // Finally, assign any extra_bridges to new buckets if there are enough
while self.extra_bridges.lock().unwrap().len() >= MAX_BRIDGES_PER_BUCKET { while self.extra_bridges.lock().unwrap().len() >= MAX_BRIDGES_PER_BUCKET {
@ -238,7 +249,12 @@ impl LoxServerContext {
// TODO: Decide the circumstances under which a bridge is allocated to an open_inv or spare bucket, // TODO: Decide the circumstances under which a bridge is allocated to an open_inv or spare bucket,
// eventually also do some more fancy grouping of new resources, i.e., by type or region // eventually also do some more fancy grouping of new resources, i.e., by type or region
let mut db_obj = self.db.lock().unwrap(); let mut db_obj = self.db.lock().unwrap();
match ba_clone.add_spare_bucket(bucket, &mut db_obj) { match self
.ba
.lock()
.unwrap()
.add_spare_bucket(bucket, &mut db_obj)
{
Ok(_) => (), Ok(_) => (),
Err(e) => { Err(e) => {
println!("Error: {:?}", e); println!("Error: {:?}", e);
@ -251,6 +267,10 @@ impl LoxServerContext {
// Regenerate tables for verifying TP reports // Regenerate tables for verifying TP reports
self.generate_tp_bridge_infos(); self.generate_tp_bridge_infos();
// Any remaining extra bridges should be cleared from the Lox Context after each sync
// Currently bridgetable updating behaviour does not occur without receiving a resource list
// from rdsys so if the extra bridge is still working, it can be added to the table later
self.extra_bridges.lock().unwrap().clear();
} }
pub fn append_extra_bridges(&self, bridge: BridgeLine) { pub fn append_extra_bridges(&self, bridge: BridgeLine) {
@ -362,11 +382,12 @@ impl LoxServerContext {
let mut hasher = Sha1::new(); let mut hasher = Sha1::new();
hasher.update(&bridge.fingerprint); hasher.update(&bridge.fingerprint);
let fingerprint: [u8; 20] = hasher.finalize().into(); let fingerprint: [u8; 20] = hasher.finalize().into();
let fingerprint_str = array_bytes::bytes2hex("", fingerprint);
// Add bucket to existing entry or add new entry // Add bucket to existing entry or add new entry
if tp_bridge_infos.contains_key(&fingerprint) { if tp_bridge_infos.contains_key(&fingerprint_str) {
tp_bridge_infos tp_bridge_infos
.get_mut(&fingerprint) .get_mut(&fingerprint_str)
.unwrap() .unwrap()
.buckets .buckets
.insert(bucket); .insert(bucket);
@ -374,7 +395,7 @@ impl LoxServerContext {
let mut buckets = HashSet::<Scalar>::new(); let mut buckets = HashSet::<Scalar>::new();
buckets.insert(bucket); buckets.insert(bucket);
tp_bridge_infos.insert( tp_bridge_infos.insert(
fingerprint, fingerprint_str,
BridgeVerificationInfo { BridgeVerificationInfo {
bridge_line: *bridge, bridge_line: *bridge,
buckets: buckets, buckets: buckets,
@ -807,13 +828,12 @@ impl LoxServerContext {
}; };
// TODO: Forward this information to rdsys // TODO: Forward this information to rdsys
for bridge_str in blocked_bridges.keys() { for bridge_str in blocked_bridges.keys() {
let bridge = array_bytes::hex2array(bridge_str).unwrap(); if self.tp_bridge_infos.lock().unwrap().contains_key(bridge_str) {
if self.tp_bridge_infos.lock().unwrap().contains_key(&bridge) {
let bl = self let bl = self
.tp_bridge_infos .tp_bridge_infos
.lock() .lock()
.unwrap() .unwrap()
.get(&bridge) .get(bridge_str)
.unwrap() .unwrap()
.bridge_line; .bridge_line;
self.mark_blocked(bl); self.mark_blocked(bl);
@ -828,7 +848,7 @@ impl LoxServerContext {
.tp_bridge_infos .tp_bridge_infos
.lock() .lock()
.unwrap() .unwrap()
.get(&report.fingerprint) .get(&array_bytes::bytes2hex("", report.fingerprint))
{ {
Some(bridge_info) => report.verify(&bridge_info), Some(bridge_info) => report.verify(&bridge_info),
None => false, None => false,
@ -871,7 +891,7 @@ impl LoxServerContext {
.tp_bridge_infos .tp_bridge_infos
.lock() .lock()
.unwrap() .unwrap()
.get(&report.fingerprint) .get(&array_bytes::bytes2hex("", report.fingerprint))
{ {
Some(bridge_info) => report.verify(la, &bridge_info, &Htable), Some(bridge_info) => report.verify(la, &bridge_info, &Htable),
None => false, None => false,
@ -920,3 +940,212 @@ fn prepare_header(response: String) -> Response<Body> {
.insert("Access-Control-Allow-Origin", HeaderValue::from_static("*")); .insert("Access-Control-Allow-Origin", HeaderValue::from_static("*"));
resp resp
} }
#[cfg(test)]
mod tests {
use crate::{fake_resource_state::TestResourceState, metrics::Metrics, BridgeConfig};
use lox_library::{bridge_table::MAX_BRIDGES_PER_BUCKET, BridgeAuth, BridgeDb};
use std::{
collections::HashMap,
env, fs,
sync::{Arc, Mutex},
};
use troll_patrol::bridge_verification_info::BridgeVerificationInfo;
use super::LoxServerContext;
struct TestHarness {
context: LoxServerContext,
}
impl TestHarness {
fn new() -> Self {
let bridgedb = BridgeDb::new();
let mut lox_auth = BridgeAuth::new(bridgedb.pubkey);
lox_auth.enc_bridge_table();
let context = LoxServerContext {
db: Arc::new(Mutex::new(bridgedb)),
ba: Arc::new(Mutex::new(lox_auth)),
extra_bridges: Arc::new(Mutex::new(Vec::new())),
to_be_replaced_bridges: Arc::new(Mutex::new(Vec::new())),
tp_bridge_infos: Arc::new(Mutex::new(
HashMap::<String, BridgeVerificationInfo>::new(),
)),
metrics: Metrics::default(),
};
Self { context }
}
fn new_with_bridges() -> Self {
let mut bridgedb = BridgeDb::new();
let mut lox_auth = BridgeAuth::new(bridgedb.pubkey);
// Make 3 x num_buckets open invitation bridges, in sets of 3
for _ in 0..5 {
let bucket = [
lox_utils::random(),
lox_utils::random(),
lox_utils::random(),
];
let _ = lox_auth.add_openinv_bridges(bucket, &mut bridgedb);
}
// Add hot_spare more hot spare buckets
for _ in 0..5 {
let bucket = [
lox_utils::random(),
lox_utils::random(),
lox_utils::random(),
];
let _ = lox_auth.add_spare_bucket(bucket, &mut bridgedb);
}
// Create the encrypted bridge table
lox_auth.enc_bridge_table();
let context = LoxServerContext {
db: Arc::new(Mutex::new(bridgedb)),
ba: Arc::new(Mutex::new(lox_auth)),
extra_bridges: Arc::new(Mutex::new(Vec::new())),
to_be_replaced_bridges: Arc::new(Mutex::new(Vec::new())),
tp_bridge_infos: Arc::new(Mutex::new(
HashMap::<String, BridgeVerificationInfo>::new(),
)),
metrics: Metrics::default(),
};
Self { context }
}
}
fn get_config() -> BridgeConfig {
env::set_var("BRIDGE_CONFIG_PATH", "bridge_config.json");
let path = env::var("BRIDGE_CONFIG_PATH").unwrap();
let config_file = fs::File::open(&path).unwrap();
serde_json::from_reader(config_file).unwrap()
}
#[test]
fn test_sync_with_bridgetable_only_working_resources() {
let bridge_config = get_config();
// Add bridges to empty bridge table and update with changed bridge state
let th = TestHarness::new();
let mut rs = TestResourceState::default();
for _ in 0..5 {
rs.add_working_resource();
}
assert_ne!(rs.rstate.working, None);
assert_eq!(rs.rstate.not_working, None);
th.context
.sync_with_bridgetable(bridge_config.watched_blockages.clone(), rs.rstate.clone());
let mut reachable_expected_length = rs.rstate.clone().working.unwrap().len();
let expected_extra_bridges = reachable_expected_length % MAX_BRIDGES_PER_BUCKET;
if expected_extra_bridges != 0 {
reachable_expected_length = reachable_expected_length - expected_extra_bridges;
}
assert_eq!(
th.context.ba.lock().unwrap().bridge_table.reachable.len(),
reachable_expected_length,
"Unexpected number of reachable bridges"
);
// Extra bridges should be cleared from the Lox Context after each sync
assert!(
th.context.extra_bridges.lock().unwrap().is_empty(),
"Extra bridges should be empty after sync"
);
}
#[test]
fn test_sync_with_bridgetable_working_and_not_working_resources() {
let bridge_config = get_config();
// Add bridges to empty bridge table and update with changed bridge state
let th = TestHarness::new();
let mut rs = TestResourceState::default();
for _ in 0..5 {
rs.add_working_resource();
}
for _ in 0..5 {
rs.add_not_working_resource()
}
assert_ne!(rs.rstate.working, None);
assert_ne!(rs.rstate.not_working, None);
th.context
.sync_with_bridgetable(bridge_config.watched_blockages.clone(), rs.rstate.clone());
let mut reachable_expected_length = rs.rstate.clone().working.unwrap().len();
let expected_extra_bridges = reachable_expected_length % MAX_BRIDGES_PER_BUCKET;
if expected_extra_bridges != 0 {
reachable_expected_length = reachable_expected_length - expected_extra_bridges;
}
assert_eq!(
th.context.ba.lock().unwrap().bridge_table.reachable.len(),
reachable_expected_length,
"Unexpected number of reachable bridges"
);
// Extra bridges should be cleared from the Lox Context after each sync
assert!(
th.context.extra_bridges.lock().unwrap().is_empty(),
"Extra bridges should be empty after sync"
);
}
#[test]
fn test_sync_with_preloaded_obsolete_bridgetable() {
// Tests the case where all bridges in the bridgetable are no longer in rdsys.
// In this case, all bridges should be replaced. If it's a bridge in a spare bucket, just remove the other bridges
// from the spare bucket and delete the bridge
let bridge_config = get_config();
// Sync bridges to non-empty bridge table with disparate sets of bridges
let th_with_bridges = TestHarness::new_with_bridges(); //Creates 5 open invitation and 5 hot spare buckets, so 30 total buckets to be replaced
let mut rs = TestResourceState::default();
for _ in 0..5 {
rs.add_working_resource();
}
assert_ne!(rs.rstate.working, None);
assert_eq!(rs.rstate.not_working, None);
assert_eq!(th_with_bridges.context.ba.lock().unwrap().bridge_table.reachable.len(), 15+15, "Unexpected number of reachable bridges should equal the number of open invitation bridges plus the number of spares added: 2x5x3");
assert_eq!(
th_with_bridges
.context
.ba
.lock()
.unwrap()
.bridge_table
.spares
.len(),
5,
"Unexpected number of spare bridges, should be 5"
);
// All potentially distributed resources (i.e., those assigned to open invitation/trusted buckets)
// not found in the rdsys update will first be replaced with any new resources coming in from rdsys then
// by bridges from the hot spare buckets. In this case, the hot spare buckets are also not in the bridge table
// so will also be replaced.
// Since there are fewer working resources than resources that have populated the bridge table, this update will
// exhaust the spare buckets and leave some obsolete bridges. The set of open invitation/trusted buckets should be
// preserved (5 open invitation buckets * 3)
th_with_bridges
.context
.sync_with_bridgetable(bridge_config.watched_blockages, rs.rstate.clone());
assert_eq!(th_with_bridges.context.ba.lock().unwrap().bridge_table.reachable.len(), 15, "Unexpected number of reachable bridges should equal the number of open invitation bridges added: 5x3");
assert_eq!(
th_with_bridges
.context
.ba
.lock()
.unwrap()
.bridge_table
.spares
.len(),
0,
"Unexpected number of spare bridges, should be exhausted"
);
assert_eq!(th_with_bridges.context.ba.lock().unwrap().bridge_table.unallocated_bridges.len(), 0, "Unexpected number of unallocated bridges, should be 0 (All spare buckets and new resources for replacement exhausted)"
);
assert_eq!(
th_with_bridges.context.extra_bridges.lock().unwrap().len(),
0,
"Unexpected number of extra bridges"
);
}
}

View File

@ -26,6 +26,7 @@ use std::{
mod db_handler; mod db_handler;
use db_handler::DB; use db_handler::DB;
mod fake_resource_state;
mod lox_context; mod lox_context;
mod metrics; mod metrics;
use metrics::Metrics; use metrics::Metrics;

View File

@ -74,14 +74,10 @@ mod tests {
use super::*; use super::*;
use base64::{engine::general_purpose, Engine as _}; use base64::{engine::general_purpose, Engine};
use chrono::{Duration, Utc}; use chrono::{Duration, Utc};
use julianday::JulianDay; use julianday::JulianDay;
use lox_library::{ use lox_library::{bridge_table, bridge_table::BridgeLine, cred::BucketReachability, proto, BridgeAuth, BridgeDb};
bridge_table::{self, BridgeLine},
cred::BucketReachability,
proto, BridgeAuth, BridgeDb,
};
use rand::RngCore; use rand::RngCore;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
@ -226,13 +222,21 @@ mod tests {
// Make 3 x num_buckets open invitation bridges, in sets of 3 // Make 3 x num_buckets open invitation bridges, in sets of 3
for _ in 0..5 { for _ in 0..5 {
let bucket = [random(), random(), random()]; let bucket = [
lox_utils::random(),
lox_utils::random(),
lox_utils::random(),
];
let _ = lox_auth.add_openinv_bridges(bucket, &mut bridgedb); let _ = lox_auth.add_openinv_bridges(bucket, &mut bridgedb);
} }
// Add hot_spare more hot spare buckets // Add hot_spare more hot spare buckets
for _ in 0..5 { for _ in 0..5 {
let bucket = [random(), random(), random()]; let bucket = [
lox_utils::random(),
lox_utils::random(),
lox_utils::random(),
];
let _ = lox_auth.add_spare_bucket(bucket, &mut bridgedb); let _ = lox_auth.add_spare_bucket(bucket, &mut bridgedb);
} }
// Create the encrypted bridge table // Create the encrypted bridge table
@ -243,7 +247,7 @@ mod tests {
extra_bridges: Arc::new(Mutex::new(Vec::new())), extra_bridges: Arc::new(Mutex::new(Vec::new())),
to_be_replaced_bridges: Arc::new(Mutex::new(Vec::new())), to_be_replaced_bridges: Arc::new(Mutex::new(Vec::new())),
tp_bridge_infos: Arc::new(Mutex::new(std::collections::HashMap::< tp_bridge_infos: Arc::new(Mutex::new(std::collections::HashMap::<
[u8; 20], String,
troll_patrol::bridge_verification_info::BridgeVerificationInfo, troll_patrol::bridge_verification_info::BridgeVerificationInfo,
>::new())), >::new())),
metrics: Metrics::default(), metrics: Metrics::default(),

View File

@ -189,7 +189,7 @@ mod tests {
"123.456.789.100".to_owned(), "123.456.789.100".to_owned(),
3002, 3002,
"BE84A97D02130470A1C77839954392BA979F7EE1".to_owned(), "BE84A97D02130470A1C77839954392BA979F7EE1".to_owned(),
ACCEPTED_HOURS_OF_FAILURE-1, ACCEPTED_HOURS_OF_FAILURE - 1,
); );
let resource_two = make_resource( let resource_two = make_resource(
"https".to_owned(), "https".to_owned(),
@ -203,7 +203,7 @@ mod tests {
"123.222.333.444".to_owned(), "123.222.333.444".to_owned(),
6002, 6002,
"C56B9EF202130470A1C77839954392BA979F7FF9".to_owned(), "C56B9EF202130470A1C77839954392BA979F7FF9".to_owned(),
ACCEPTED_HOURS_OF_FAILURE+2, ACCEPTED_HOURS_OF_FAILURE + 2,
); );
let resource_three = make_resource( let resource_three = make_resource(
"scramblesuit".to_owned(), "scramblesuit".to_owned(),
@ -217,7 +217,7 @@ mod tests {
"443.288.222.100".to_owned(), "443.288.222.100".to_owned(),
3042, 3042,
"5E3A8BD902130470A1C77839954392BA979F7B46".to_owned(), "5E3A8BD902130470A1C77839954392BA979F7B46".to_owned(),
ACCEPTED_HOURS_OF_FAILURE+1, ACCEPTED_HOURS_OF_FAILURE + 1,
); );
let resource_four = make_resource( let resource_four = make_resource(
"https".to_owned(), "https".to_owned(),

View File

@ -142,7 +142,7 @@ mod tests {
extra_bridges: Arc::new(Mutex::new(Vec::new())), extra_bridges: Arc::new(Mutex::new(Vec::new())),
to_be_replaced_bridges: Arc::new(Mutex::new(Vec::new())), to_be_replaced_bridges: Arc::new(Mutex::new(Vec::new())),
tp_bridge_infos: Arc::new(Mutex::new( tp_bridge_infos: Arc::new(Mutex::new(
HashMap::<[u8; 20], BridgeVerificationInfo>::new(), HashMap::<String, BridgeVerificationInfo>::new(),
)), )),
metrics: Metrics::default(), metrics: Metrics::default(),
}; };

View File

@ -16,7 +16,7 @@ readme = "README.md"
[dependencies] [dependencies]
curve25519-dalek = { version = "4", default-features = false, features = ["serde", "rand_core", "digest"] } curve25519-dalek = { version = "4", default-features = false, features = ["serde", "rand_core", "digest"] }
ed25519-dalek = { version = "2", features = ["serde", "rand_core"] } ed25519-dalek = { version = "2", features = ["serde", "rand_core"] }
bincode = "1" bincode = "1.3.3"
chrono = "0.4" chrono = "0.4"
rand = { version = "0.8", features = ["std_rng"]} rand = { version = "0.8", features = ["std_rng"]}
serde = "1.0.197" serde = "1.0.197"
@ -26,11 +26,11 @@ statistical = "1.0.0"
lazy_static = "1" lazy_static = "1"
hex_fmt = "0.3" hex_fmt = "0.3"
aes-gcm = { version = "0.10", features =["aes"]} aes-gcm = { version = "0.10", features =["aes"]}
base64 = "0.21" base64 = "0.22.0"
time = "0.3.34" time = "0.3.36"
prometheus = "0.13.3" prometheus = "0.13.3"
subtle = "2.5" subtle = "2.5"
thiserror = "1.0.58" thiserror = "1.0.59"
lox-zkp = { git = "https://gitlab.torproject.org/onyinyang/lox-zkp", version = "0.8.0" } lox-zkp = { git = "https://gitlab.torproject.org/onyinyang/lox-zkp", version = "0.8.0" }
[features] [features]

View File

@ -73,6 +73,7 @@ pub enum ReplaceSuccess {
NotFound = 0, NotFound = 0,
NotReplaced = 1, NotReplaced = 1,
Replaced = 2, Replaced = 2,
Removed = 3,
} }
/// This error is thrown if the number of buckets/keys in the bridge table /// This error is thrown if the number of buckets/keys in the bridge table
@ -511,6 +512,17 @@ impl BridgeAuth {
return res; return res;
} }
} }
// Also check the unallocated bridges just in case there is a bridge that should be updated there
let unallocated_bridges = self.bridge_table.unallocated_bridges.clone();
for (i, unallocated_bridge) in unallocated_bridges.iter().enumerate() {
if unallocated_bridge.uid_fingerprint == bridge.uid_fingerprint {
// Now we must remove the old bridge from the unallocated bridges and insert the new bridge
// in its place
self.bridge_table.unallocated_bridges.remove(i);
self.bridge_table.unallocated_bridges.push(*bridge);
res = true;
}
}
// If this is returned, we assume that the bridge wasn't found in the bridge table // If this is returned, we assume that the bridge wasn't found in the bridge table
// and therefore should be treated as a "new bridge" // and therefore should be treated as a "new bridge"
res res
@ -535,6 +547,25 @@ impl BridgeAuth {
Ok(()) Ok(())
} }
// Remove an unallocated resource
pub fn remove_unallocated(&mut self, bridge: &BridgeLine) -> ReplaceSuccess {
// Check if the bridge is in the unallocated bridges and remove the bridge if so
// Bridges in spare buckets should have been moved to the unallocated bridges
if self.bridge_table.unallocated_bridges.contains(bridge) {
let index = self
.bridge_table
.unallocated_bridges
.iter()
.position(|x| x == bridge)
.unwrap();
self.bridge_table.unallocated_bridges.swap_remove(index);
// A bridge that is in the unallocated_bridges is not exactly replaced
// but is successfully handled and no further action is needed
return ReplaceSuccess::Removed;
}
ReplaceSuccess::NotFound
}
/// Attempt to remove a bridge that is failing tests and replace it with a bridge from /// Attempt to remove a bridge that is failing tests and replace it with a bridge from
/// available_bridge or from a spare bucket /// available_bridge or from a spare bucket
pub fn bridge_replace( pub fn bridge_replace(
@ -544,8 +575,23 @@ impl BridgeAuth {
) -> ReplaceSuccess { ) -> ReplaceSuccess {
let reachable_bridges = &self.bridge_table.reachable.clone(); let reachable_bridges = &self.bridge_table.reachable.clone();
let Some(positions) = reachable_bridges.get(bridge) else { let Some(positions) = reachable_bridges.get(bridge) else {
return ReplaceSuccess::NotFound; return self.remove_unallocated(bridge);
}; };
// Check if the bridge is in a spare bucket first, if it is, dissolve the bucket
if let Some(spare) = self
.bridge_table
.spares
.iter()
.find(|x| positions.iter().any(|(bucketnum, _)| &bucketnum == x))
.cloned()
{
let Ok(_) = self.dissolve_spare_bucket(spare) else {
return ReplaceSuccess::NotReplaced;
};
// Next Check if the bridge is in the unallocated bridges and remove the bridge if so
// Bridges in spare buckets should have been moved to the unallocated bridges
return self.remove_unallocated(bridge);
}
// select replacement: // select replacement:
// - first try the given bridge // - first try the given bridge
// - second try to pick one from the set of available bridges // - second try to pick one from the set of available bridges

View File

@ -1060,22 +1060,39 @@ fn test_update_bridge() {
#[test] #[test]
fn test_bridge_replace() { fn test_bridge_replace() {
// Create 3 open invitation buckets and 3 spare buckets // Create 3 open invitation buckets and 3 spare buckets
let cases = vec!["not found", "available", "unallocated", "spare", "failed"]; let cases = vec![
"not found",
"available",
"unallocated",
"use_spare",
"remove_spare",
"failed",
];
let num_buckets = 5; let num_buckets = 5;
let hot_spare = 0; let hot_spare = 0;
for case in cases { for case in cases {
let table_size: usize;
let mut th: TestHarness; let mut th: TestHarness;
if String::from(case) != "failed" { match case {
th = TestHarness::new(); "failed" => {
} else { th = TestHarness::new_buckets(num_buckets, hot_spare);
th = TestHarness::new_buckets(num_buckets, hot_spare); table_size = th.ba.bridge_table.buckets.len();
}
"remove_spare" => {
th = TestHarness::new_buckets(0, 5);
table_size = th.ba.bridge_table.buckets.len();
}
_ => {
th = TestHarness::new();
// Ensure that a randomly selected bucket isn't taken from the set of spare bridges
table_size = th.ba.bridge_table.buckets.len() - 5;
}
} }
// Randomly select a bridge to replace // Randomly select a bridge to replace
let table_size = th.ba.bridge_table.buckets.len();
let mut num = 100000; let mut num = 100000;
while !th.ba.bridge_table.buckets.contains_key(&num) { while !th.ba.bridge_table.buckets.contains_key(&num) {
num = rand::thread_rng().gen_range(0..th.ba.bridge_table.counter); num = rand::thread_rng().gen_range(0..table_size as u32);
} }
println!("chosen num is: {:?}", num); println!("chosen num is: {:?}", num);
let replaceable_bucket = *th.ba.bridge_table.buckets.get(&num).unwrap(); let replaceable_bucket = *th.ba.bridge_table.buckets.get(&num).unwrap();
@ -1093,7 +1110,10 @@ fn test_bridge_replace() {
// Case zero: bridge to be replaced is not in the bridgetable // Case zero: bridge to be replaced is not in the bridgetable
let random_bridgeline = BridgeLine::random(); let random_bridgeline = BridgeLine::random();
assert!( assert!(
!th.ba.bridge_table.reachable.contains_key(&random_bridgeline), !th.ba
.bridge_table
.reachable
.contains_key(&random_bridgeline),
"Random bridgeline happens to be in the bridge_table (and should not be)" "Random bridgeline happens to be in the bridge_table (and should not be)"
); );
assert!( assert!(
@ -1133,8 +1153,10 @@ fn test_bridge_replace() {
.is_some(), .is_some(),
"Replacement bridge not added to reachable bridges" "Replacement bridge not added to reachable bridges"
); );
println!("Table Size {:?}", table_size);
println!("Bucket length {:?}", th.ba.bridge_table.buckets.len() - 5);
assert!( assert!(
table_size == th.ba.bridge_table.buckets.len(), table_size == th.ba.bridge_table.buckets.len() - 5,
"Number of buckets changed size" "Number of buckets changed size"
); );
assert!( assert!(
@ -1175,7 +1197,7 @@ fn test_bridge_replace() {
"Replacement bridge not added to reachable bridges" "Replacement bridge not added to reachable bridges"
); );
assert!( assert!(
table_size == th.ba.bridge_table.buckets.len(), table_size == th.ba.bridge_table.buckets.len() - 5,
"Number of buckets changed size" "Number of buckets changed size"
); );
assert!( assert!(
@ -1185,7 +1207,7 @@ fn test_bridge_replace() {
println!("Successfully added unallocated bridgeline"); println!("Successfully added unallocated bridgeline");
} }
"spare" => { "use_spare" => {
// Case three: available_bridge == null and unallocated_bridges == null // Case three: available_bridge == null and unallocated_bridges == null
assert!( assert!(
th.ba.bridge_table.unallocated_bridges.is_empty(), th.ba.bridge_table.unallocated_bridges.is_empty(),
@ -1205,7 +1227,7 @@ fn test_bridge_replace() {
); );
// Remove a spare bucket to replace bridge, buckets decrease by 1 // Remove a spare bucket to replace bridge, buckets decrease by 1
assert!( assert!(
(table_size - 1) == th.ba.bridge_table.buckets.len(), (table_size - 1) == th.ba.bridge_table.buckets.len() - 5,
"Number of buckets changed size" "Number of buckets changed size"
); );
assert!( assert!(
@ -1215,6 +1237,40 @@ fn test_bridge_replace() {
println!("Successfully added bridgeline from spare"); println!("Successfully added bridgeline from spare");
} }
"remove_spare" => {
// Case three: available_bridge == null and unallocated_bridges == null
assert!(
th.ba.bridge_table.unallocated_bridges.is_empty(),
"Unallocated bridges should have a length of 0"
);
assert!(
th.ba.bridge_replace(replacement_bridge, None) == ReplaceSuccess::Removed,
"Bridge was replaced with available spare, instead of being removed"
);
assert!(
th.ba.bridge_table.unallocated_bridges.len() == 2,
"Unallocated bridges should have a length of 2"
);
assert!(
th.ba
.bridge_table
.reachable
.get(replacement_bridge)
.is_none(),
"Replacement bridge still marked as reachable"
);
// Remove a spare bucket to replace bridge, buckets decrease by 1
assert!(
(table_size - 1) == th.ba.bridge_table.buckets.len(),
"Number of buckets changed size"
);
assert!(
th.ba.bridge_table.unallocated_bridges.len() == 2,
"Extra spare bridges not added to unallocated bridges"
);
println!("Successfully removed a spare bridgeline marked to be replaced");
}
"failed" => { "failed" => {
// Case four: available_bridge == None and unallocated_bridges == None and spare buckets == None // Case four: available_bridge == None and unallocated_bridges == None and spare buckets == None
assert!( assert!(

View File

@ -12,8 +12,10 @@ categories = ["rust-patterns"]
repository = "https://gitlab.torproject.org/tpo/anti-censorship/lox.git/" repository = "https://gitlab.torproject.org/tpo/anti-censorship/lox.git/"
[dependencies] [dependencies]
chrono = { version = "0.4.34", features = ["serde", "clock"] } base64 = "0.22.0"
chrono = { version = "0.4.38", features = ["serde", "clock"] }
lox-library = {path = "../lox-library", version = "0.1.0"} lox-library = {path = "../lox-library", version = "0.1.0"}
rand = "0.8.5"
serde = "1" serde = "1"
serde_json = "1.0.113" serde_json = "1.0.113"
serde_with = "3.7.0" serde_with = "3.7.0"

View File

@ -1,3 +1,4 @@
use base64::{engine::general_purpose, Engine as _};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use lox_library::bridge_table::{ use lox_library::bridge_table::{
from_scalar, BridgeLine, BridgeTable, EncryptedBucket, MAX_BRIDGES_PER_BUCKET, from_scalar, BridgeLine, BridgeTable, EncryptedBucket, MAX_BRIDGES_PER_BUCKET,
@ -5,18 +6,53 @@ use lox_library::bridge_table::{
use lox_library::cred::{BucketReachability, Invitation, Lox}; use lox_library::cred::{BucketReachability, Invitation, Lox};
use lox_library::proto::{self, check_blockage, level_up, trust_promotion}; use lox_library::proto::{self, check_blockage, level_up, trust_promotion};
use lox_library::{IssuerPubKey, OPENINV_LENGTH}; use lox_library::{IssuerPubKey, OPENINV_LENGTH};
use rand::RngCore;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_with::serde_as; use serde_with::serde_as;
use std::array::TryFromSliceError; use std::array::TryFromSliceError;
use std::collections::HashMap; use std::collections::HashMap;
#[serde_as] const LOX_INVITE_TOKEN: &str = "loxinvite_";
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct Invite { pub struct Invite {
#[serde_as(as = "[_; OPENINV_LENGTH]")] #[serde(with = "base64serde")]
pub invite: [u8; OPENINV_LENGTH], pub invite: [u8; OPENINV_LENGTH],
} }
mod base64serde {
use base64::{engine::general_purpose::STANDARD_NO_PAD, Engine as _};
use lox_library::OPENINV_LENGTH;
use serde::{Deserialize, Serialize};
use serde::{Deserializer, Serializer};
use crate::LOX_INVITE_TOKEN;
pub fn serialize<S: Serializer>(v: &[u8; OPENINV_LENGTH], s: S) -> Result<S::Ok, S::Error> {
let mut base64 = STANDARD_NO_PAD.encode(v);
base64.insert_str(0, LOX_INVITE_TOKEN);
String::serialize(&base64, s)
}
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<[u8; OPENINV_LENGTH], D::Error> {
let mut base64 = String::deserialize(d)?;
let encoded_str = base64.split_off(LOX_INVITE_TOKEN.len());
if base64 != LOX_INVITE_TOKEN {
return Err(serde::de::Error::custom("Token identifier does not match"))
}
match STANDARD_NO_PAD.decode(encoded_str) {
Ok(output) => {
let out: Result<[u8; OPENINV_LENGTH], D::Error> = match output.try_into() {
Ok(out) => Ok(out),
Err(e) => Err(serde::de::Error::custom(String::from_utf8(e).unwrap())),
};
out
}
Err(e) => Err(serde::de::Error::custom(e)),
}
}
}
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
pub struct OpenReqState { pub struct OpenReqState {
pub request: proto::open_invite::Request, pub request: proto::open_invite::Request,
@ -154,3 +190,34 @@ pub fn calc_test_days(trust_level: i64) -> i64 {
// } // }
total total
} }
pub fn random() -> BridgeLine {
let mut rng = rand::thread_rng();
let mut res: BridgeLine = BridgeLine::default();
// Pick a random 4-byte address
let mut addr: [u8; 4] = [0; 4];
rng.fill_bytes(&mut addr);
// If the leading byte is 224 or more, that's not a valid IPv4
// address. Choose an IPv6 address instead (but don't worry too
// much about it being well formed).
if addr[0] >= 224 {
rng.fill_bytes(&mut res.addr);
} else {
// Store an IPv4 address as a v4-mapped IPv6 address
res.addr[10] = 255;
res.addr[11] = 255;
res.addr[12..16].copy_from_slice(&addr);
};
let ports: [u16; 4] = [443, 4433, 8080, 43079];
let portidx = (rng.next_u32() % 4) as usize;
res.port = ports[portidx];
res.uid_fingerprint = rng.next_u64();
let mut cert: [u8; 52] = [0; 52];
rng.fill_bytes(&mut cert);
let infostr: String = format!(
"obfs4 cert={}, iat-mode=0",
general_purpose::STANDARD_NO_PAD.encode(cert)
);
res.info[..infostr.len()].copy_from_slice(infostr.as_bytes());
res
}

View File

@ -21,7 +21,7 @@ lazy_static = "1.4.0"
lox-library = { path = "../lox-library", version = "0.1.0" } lox-library = { path = "../lox-library", version = "0.1.0" }
lox_utils = { path = "../lox-utils", version = "0.1.0" } lox_utils = { path = "../lox-utils", version = "0.1.0" }
wasm-bindgen = "0.2" wasm-bindgen = "0.2"
time = "0.3.34" time = "0.3.36"
serde_json = "1.0.113" serde_json = "1.0.113"
console_error_panic_hook = "0.1.7" console_error_panic_hook = "0.1.7"
@ -30,5 +30,5 @@ rand = { version = "0.7", features = ["wasm-bindgen"] }
lox-zkp = { git = "https://gitlab.torproject.org/onyinyang/lox-zkp", version = "0.8.0" } lox-zkp = { git = "https://gitlab.torproject.org/onyinyang/lox-zkp", version = "0.8.0" }
[dependencies.chrono] [dependencies.chrono]
version = "0.4.34" version = "0.4.38"
features = ["serde", "wasmbind"] features = ["serde", "wasmbind"]

View File

@ -199,7 +199,7 @@ function request_open_invite() {
return new Promise((fulfill, reject) => { return new Promise((fulfill, reject) => {
loxServerPostRequest("/invite", null).then((response) => { loxServerPostRequest("/invite", null).then((response) => {
console.log("Got invitation token: " + response.invite); console.log("Got invitation token: " + response.invite);
fulfill(response.invite); fulfill(JSON.stringify(response));
return; return;
}).catch(() => { }).catch(() => {
console.log("Error requesting open invite from Lox server"); console.log("Error requesting open invite from Lox server");

View File

@ -29,9 +29,13 @@ pub fn set_panic_hook() {
// Receives an invite and prepares an open_invite request, returning the // Receives an invite and prepares an open_invite request, returning the
// Request and State // Request and State
#[wasm_bindgen] #[wasm_bindgen]
pub fn open_invite(invite: &[u8]) -> Result<String, JsValue> { pub fn open_invite(base64_invite: String) -> Result<String, JsValue> {
log(&format!("Using invite: {:?}", invite)); log(&format!("Using invite: {:?}", base64_invite));
let token = match lox_utils::validate(invite) { let invite: lox_utils::Invite = match serde_json::from_str(&base64_invite) {
Ok(invite) => invite,
Err(e) => return Err(JsValue::from(e.to_string())),
};
let token = match lox_utils::validate(&invite.invite) {
Ok(token) => token, Ok(token) => token,
Err(e) => return Err(JsValue::from(e.to_string())), Err(e) => return Err(JsValue::from(e.to_string())),
}; };
@ -831,29 +835,32 @@ pub fn get_next_unlock(constants_str: String, lox_cred_str: String) -> Result<St
days_to_next_level + constants.level_interval[trust_level as usize + 1]; days_to_next_level + constants.level_interval[trust_level as usize + 1];
invitations_at_next_level = constants.level_invitations[trust_level as usize + 1]; invitations_at_next_level = constants.level_invitations[trust_level as usize + 1];
} }
let days_to_blockage_migration_unlock = let days_to_blockage_migration_unlock = match trust_level
match trust_level < constants.min_blockage_migration_trust_level { < constants.min_blockage_migration_trust_level
// If the credential is greater than the minimum level that enables {
// migrating after a blockage, the time to unlock is 0, otherwise we // If the credential is greater than the minimum level that enables
// add the time to upgrade until that level // migrating after a blockage, the time to unlock is 0, otherwise we
true => { // add the time to upgrade until that level
let mut blockage_days = days_to_next_level; true => {
let mut count = 1; let mut blockage_days = days_to_next_level;
while trust_level + count < constants.min_blockage_migration_trust_level { let mut count = 1;
blockage_days += constants.level_interval[trust_level as usize + count as usize]; while trust_level + count < constants.min_blockage_migration_trust_level {
count += 1; blockage_days += constants.level_interval[trust_level as usize + count as usize];
} count += 1;
blockage_days
} }
false => 0, blockage_days
}; }
false => 0,
};
let day_of_level_unlock = let day_of_level_unlock =
(scalar_u32(&lox_cred.lox_credential.level_since).unwrap() + days_to_next_level) as i32; (scalar_u32(&lox_cred.lox_credential.level_since).unwrap() + days_to_next_level) as i32;
let level_unlock_date = JulianDay::new(day_of_level_unlock).to_date(); let level_unlock_date = JulianDay::new(day_of_level_unlock).to_date();
let day_of_invite_unlock = let day_of_invite_unlock =
(scalar_u32(&lox_cred.lox_credential.level_since).unwrap() + days_to_invite_inc) as i32; (scalar_u32(&lox_cred.lox_credential.level_since).unwrap() + days_to_invite_inc) as i32;
let invite_unlock_date = JulianDay::new(day_of_invite_unlock).to_date(); let invite_unlock_date = JulianDay::new(day_of_invite_unlock).to_date();
let day_of_blockage_migration_unlock = (scalar_u32(&lox_cred.lox_credential.level_since).unwrap() + days_to_blockage_migration_unlock) as i32; let day_of_blockage_migration_unlock = (scalar_u32(&lox_cred.lox_credential.level_since)
.unwrap()
+ days_to_blockage_migration_unlock) as i32;
let blockage_migration_unlock_date = let blockage_migration_unlock_date =
JulianDay::new(day_of_blockage_migration_unlock as i32).to_date(); JulianDay::new(day_of_blockage_migration_unlock as i32).to_date();
let next_unlock: lox_utils::LoxNextUnlock = lox_utils::LoxNextUnlock { let next_unlock: lox_utils::LoxNextUnlock = lox_utils::LoxNextUnlock {

View File

@ -25,4 +25,4 @@ reqwest = { version = "0.11", features = ["json", "stream"]}
tokio-stream = "0.1.15" tokio-stream = "0.1.15"
futures = "0.3.30" futures = "0.3.30"
tokio-util = "0.7.10" tokio-util = "0.7.10"
chrono = { version = "0.4.34", features = ["serde", "clock"] } chrono = { version = "0.4.38", features = ["serde", "clock"] }

View File

@ -10,13 +10,13 @@ pub struct ResourceRequest {
pub resource_types: Vec<String>, pub resource_types: Vec<String>,
} }
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct TestResults { pub struct TestResults {
pub last_passed: DateTime<Utc>, pub last_passed: DateTime<Utc>,
} }
/// Representation of a bridge resource /// Representation of a bridge resource
#[derive(Deserialize, PartialEq, Eq, Debug)] #[derive(Clone, Deserialize, PartialEq, Eq, Debug)]
pub struct Resource { pub struct Resource {
pub r#type: String, pub r#type: String,
pub blocked_in: HashMap<String, bool>, pub blocked_in: HashMap<String, bool>,
@ -57,7 +57,7 @@ impl Resource {
} }
/// A ResourceState holds information about new, changed, or pruned resources /// A ResourceState holds information about new, changed, or pruned resources
#[derive(Deserialize, PartialEq, Eq, Debug)] #[derive(Clone, Deserialize, Default, PartialEq, Eq, Debug)]
pub struct ResourceState { pub struct ResourceState {
pub working: Option<Vec<Resource>>, pub working: Option<Vec<Resource>>,
pub not_working: Option<Vec<Resource>>, pub not_working: Option<Vec<Resource>>,