Merge upstream changes
This commit is contained in:
commit
5c0376cd56
File diff suppressed because it is too large
Load Diff
|
@ -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"]
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"watched_blockages": [
|
||||||
|
"RU"
|
||||||
|
],
|
||||||
|
"percent_spares": 50
|
||||||
|
}
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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(),
|
||||||
};
|
};
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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!(
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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"] }
|
||||||
|
|
|
@ -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>>,
|
||||||
|
|
Loading…
Reference in New Issue