Compare commits

...

2 Commits

4 changed files with 111 additions and 72 deletions

View File

@ -24,11 +24,16 @@ pub const DIRECTORY: &str = "extra_infos";
pub struct ExtraInfo { pub struct ExtraInfo {
/// Bridge nickname, probably unused /// Bridge nickname, probably unused
pub nickname: String, pub nickname: String,
/// Bridge fingerprint, a SHA-1 hash of the bridge ID /// Bridge fingerprint, a SHA-1 hash of the bridge ID
pub fingerprint: [u8; 20], pub fingerprint: [u8; 20],
/// Date (in UTC) that this document was published, stored as a Julian
/// date because we don't need to know more precisely than the day. /// Date (in UTC) that this document covered (bridge-stats-end if
pub published: u32, /// available) or that the document was published (published), stored
/// as a Julian date because we don't need to know more precisely than
/// the day.
pub date: u32,
/// Map of country codes and how many users (rounded up to a multiple of /// Map of country codes and how many users (rounded up to a multiple of
/// 8) have connected to that bridge during the day. /// 8) have connected to that bridge during the day.
/// Uses BTreeMap instead of HashMap so ExtraInfo can implement Hash. /// Uses BTreeMap instead of HashMap so ExtraInfo can implement Hash.
@ -40,7 +45,7 @@ fn get_extra_info_or_error(entry: &HashMap<String, String>) -> Result<ExtraInfo,
// How did we get here?? // How did we get here??
return Err("Cannot parse extra-info: Missing nickname or fingerprint".to_string()); return Err("Cannot parse extra-info: Missing nickname or fingerprint".to_string());
} }
if !entry.contains_key("published") || !entry.contains_key("bridge-ips") { if !(entry.contains_key("bridge-stats-end") || entry.contains_key("published")) || !entry.contains_key("bridge-ips") {
// Some extra-infos are missing data on connecting IPs... // Some extra-infos are missing data on connecting IPs...
// But we can't do anything in that case. // But we can't do anything in that case.
return Err(format!( return Err(format!(
@ -55,9 +60,17 @@ fn get_extra_info_or_error(entry: &HashMap<String, String>) -> Result<ExtraInfo,
return Err("Fingerprint must be 20 bytes".to_string()); return Err("Fingerprint must be 20 bytes".to_string());
} }
let fingerprint = array_bytes::hex2array(fingerprint_str).unwrap(); let fingerprint = array_bytes::hex2array(fingerprint_str).unwrap();
let published: u32 = JulianDay::from( let date: u32 = {
let date_str = if entry.contains_key("bridge-stats-end") {
let line = entry.get("bridge-stats-end").unwrap();
// Parse out (86400 s) from end of line
&line[..line.find("(").unwrap()-1]
} else {
entry.get("published").unwrap().as_str()
};
JulianDay::from(
DateTime::parse_from_str( DateTime::parse_from_str(
&(entry.get("published").unwrap().to_owned() + " +0000"), &(date_str.to_owned() + " +0000"),
"%F %T %z", "%F %T %z",
) )
.unwrap() .unwrap()
@ -65,7 +78,8 @@ fn get_extra_info_or_error(entry: &HashMap<String, String>) -> Result<ExtraInfo,
) )
.inner() .inner()
.try_into() .try_into()
.unwrap(); .unwrap()
};
let bridge_ips_str = entry.get("bridge-ips").unwrap(); let bridge_ips_str = entry.get("bridge-ips").unwrap();
let mut bridge_ips: BTreeMap<String, u32> = BTreeMap::new(); let mut bridge_ips: BTreeMap<String, u32> = BTreeMap::new();
let countries: Vec<&str> = bridge_ips_str.split(',').collect(); let countries: Vec<&str> = bridge_ips_str.split(',').collect();
@ -80,7 +94,7 @@ fn get_extra_info_or_error(entry: &HashMap<String, String>) -> Result<ExtraInfo,
Ok(ExtraInfo { Ok(ExtraInfo {
nickname, nickname,
fingerprint, fingerprint,
published, date,
bridge_ips, bridge_ips,
}) })
} }

View File

@ -63,21 +63,25 @@ pub fn get_date() -> u32 {
pub struct BridgeInfo { pub struct BridgeInfo {
/// hashed fingerprint (SHA-1 hash of 20-byte bridge ID) /// hashed fingerprint (SHA-1 hash of 20-byte bridge ID)
pub fingerprint: [u8; 20], pub fingerprint: [u8; 20],
/// nickname of bridge (probably not necessary) /// nickname of bridge (probably not necessary)
pub nickname: String, pub nickname: String,
/// first Julian date we started collecting data on this bridge /// first Julian date we started collecting data on this bridge
pub first_seen: u32, pub first_seen: u32,
/// flag indicating whether the bridge is believed to be blocked /// flag indicating whether the bridge is believed to be blocked
pub is_blocked: bool, pub is_blocked: bool,
/// map of dates to data for that day /// map of dates to data for that day
pub info_by_day: HashMap<u32, DailyBridgeInfo>, pub info_by_day: HashMap<u32, DailyBridgeInfo>,
} }
impl BridgeInfo { impl BridgeInfo {
pub fn new(fingerprint: [u8; 20], nickname: String) -> Self { pub fn new(fingerprint: [u8; 20], nickname: &String) -> Self {
Self { Self {
fingerprint: fingerprint, fingerprint: fingerprint,
nickname: nickname, nickname: nickname.to_string(),
first_seen: get_date(), first_seen: get_date(),
is_blocked: false, is_blocked: false,
info_by_day: HashMap::<u32, DailyBridgeInfo>::new(), info_by_day: HashMap::<u32, DailyBridgeInfo>::new(),
@ -106,50 +110,80 @@ impl fmt::Display for BridgeInfo {
} }
} }
// TODO: Should this be an enum to make it easier to implement different #[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
// versions for plugins? pub enum BridgeInfoType {
BridgeIps,
NegativeReports,
PositiveReports,
}
/// Information about bridge reachability, gathered daily /// Information about bridge reachability, gathered daily
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct DailyBridgeInfo { pub struct DailyBridgeInfo {
/// Map of country codes and how many users (rounded up to a multiple of pub info_by_country: BTreeMap<String, BTreeMap<BridgeInfoType, u32>>,
/// 8) have connected to that bridge during the day.
pub bridge_ips: BTreeMap<String, u32>,
/// Map of negative reports to count of negative reports received
pub negative_reports: BTreeMap<SerializableNegativeReport, u32>,
/// Set of positive reports received during this day
pub positive_reports: Vec<SerializablePositiveReport>,
// We don't care about ordering of the reports, but I'm using vectors for
// reports because we don't want a set to deduplicate our reports, and
// I don't want to implement Hash or Ord. Another possibility might be a
// map of the report to the number of that exact report we received.
// Positive reports include a Lox proof and should be unique, but negative
// reports could be deduplicated.
} }
impl DailyBridgeInfo { impl DailyBridgeInfo {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
bridge_ips: BTreeMap::<String, u32>::new(), info_by_country: BTreeMap::<String, BTreeMap::<BridgeInfoType, u32>>::new(),
negative_reports: BTreeMap::<SerializableNegativeReport, u32>::new(), }
positive_reports: Vec::<SerializablePositiveReport>::new(), }
pub fn add_info(&mut self, info_type: BridgeInfoType, count_per_country: &BTreeMap::<String, u32>) {
for country in count_per_country.keys() {
if self.info_by_country.contains_key(country) {
let info = self.info_by_country.get_mut(country).unwrap();
if !info.contains_key(&info_type) {
info.insert(info_type, *count_per_country.get(&country.to_string()).unwrap());
} else if info_type == BridgeInfoType::BridgeIps {
// Use newest value we've seen today
if info.get(&info_type).unwrap() < count_per_country.get(country).unwrap() {
info.insert(BridgeInfoType::BridgeIps, *count_per_country.get(&country.to_string()).unwrap());
}
} else {
let new_count = info.get(&info_type).unwrap() + *count_per_country.get(&country.to_string()).unwrap();
info.insert(info_type, new_count);
}
} else {
let mut info = BTreeMap::<BridgeInfoType, u32>::new();
info.insert(info_type, *count_per_country.get(&country.to_string()).unwrap());
self.info_by_country.insert(country.to_string(), info);
}
} }
} }
} }
impl fmt::Display for DailyBridgeInfo { impl fmt::Display for DailyBridgeInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut str = String::from("bridge_ips:"); let mut str = String::from("info:");
for country in self.bridge_ips.keys() { for country in self.info_by_country.keys() {
let info = self.info_by_country.get(country).unwrap();
let ip_count = match info.get(&BridgeInfoType::BridgeIps) {
Some(v) => v,
None => &0,
};
let nr_count = match info.get(&BridgeInfoType::NegativeReports) {
Some(v) => v,
None => &0,
};
let pr_count = match info.get(&BridgeInfoType::PositiveReports) {
Some(v) => v,
None => &0,
};
if ip_count > &0 || nr_count > &0 || pr_count > &0 {
str.push_str( str.push_str(
format!( format!(
"\n cc: {}, connections: {}", "\n cc: {}\n connections: {}\n negative reports: {}\n positive reports: {}",
country, country,
self.bridge_ips.get(country).unwrap() ip_count,
nr_count,
pr_count,
) )
.as_str(), .as_str(),
); );
} }
}
write!(f, "{}", str) write!(f, "{}", str)
} }
} }
@ -162,45 +196,25 @@ pub fn add_extra_info_to_db(db: &Db, extra_info: ExtraInfo) {
let fingerprint = extra_info.fingerprint; let fingerprint = extra_info.fingerprint;
let mut bridge_info = match db.get(&fingerprint).unwrap() { let mut bridge_info = match db.get(&fingerprint).unwrap() {
Some(v) => bincode::deserialize(&v).unwrap(), Some(v) => bincode::deserialize(&v).unwrap(),
None => BridgeInfo::new(fingerprint, extra_info.nickname), None => BridgeInfo::new(fingerprint, &extra_info.nickname),
}; };
// If we already have an entry, compare it with the new one. For each // If we already have an entry, compare it with the new one. For each
// country:count mapping, use the greater of the two counts. // country:count mapping, use the greater of the two counts.
if bridge_info.info_by_day.contains_key(&extra_info.published) { if bridge_info.info_by_day.contains_key(&extra_info.date) {
let daily_bridge_info = bridge_info let daily_bridge_info = bridge_info
.info_by_day .info_by_day
.get_mut(&extra_info.published) .get_mut(&extra_info.date)
.unwrap(); .unwrap();
if extra_info.bridge_ips != daily_bridge_info.bridge_ips { daily_bridge_info.add_info(BridgeInfoType::BridgeIps, &extra_info.bridge_ips);
for country in extra_info.bridge_ips.keys() {
if daily_bridge_info.bridge_ips.contains_key(country) {
// Use greatest value we've seen today
if daily_bridge_info.bridge_ips.get(country).unwrap()
< extra_info.bridge_ips.get(country).unwrap()
{
daily_bridge_info.bridge_ips.insert(
country.to_string(),
*extra_info.bridge_ips.get(country).unwrap(),
);
}
} else {
daily_bridge_info.bridge_ips.insert(
country.to_string(),
*extra_info.bridge_ips.get(country).unwrap(),
);
}
}
}
} else { } else {
// No existing entry; make a new one. // No existing entry; make a new one.
let daily_bridge_info = DailyBridgeInfo { let mut daily_bridge_info = DailyBridgeInfo {
bridge_ips: extra_info.bridge_ips, info_by_country: BTreeMap::<String, BTreeMap::<BridgeInfoType, u32>>::new(),
negative_reports: BTreeMap::<SerializableNegativeReport, u32>::new(),
positive_reports: Vec::<SerializablePositiveReport>::new(),
}; };
daily_bridge_info.add_info(BridgeInfoType::BridgeIps, &extra_info.bridge_ips);
bridge_info bridge_info
.info_by_day .info_by_day
.insert(extra_info.published, daily_bridge_info); .insert(extra_info.date, daily_bridge_info);
} }
// Commit changes to database // Commit changes to database
db.insert(fingerprint, bincode::serialize(&bridge_info).unwrap()) db.insert(fingerprint, bincode::serialize(&bridge_info).unwrap())

View File

@ -19,10 +19,13 @@ pub enum NegativeReportError {
pub struct NegativeReport { pub struct NegativeReport {
/// hashed fingerprint (SHA-1 hash of 20-byte bridge ID) /// hashed fingerprint (SHA-1 hash of 20-byte bridge ID)
pub fingerprint: [u8; 20], pub fingerprint: [u8; 20],
/// some way to prove knowledge of bridge /// some way to prove knowledge of bridge
bridge_pok: ProofOfBridgeKnowledge, bridge_pok: ProofOfBridgeKnowledge,
/// user's country code /// user's country code
pub country: String, pub country: String,
/// today's Julian date /// today's Julian date
pub date: u32, pub date: u32,
} }
@ -140,6 +143,7 @@ impl SerializableNegativeReport {
pub enum ProofOfBridgeKnowledge { pub enum ProofOfBridgeKnowledge {
/// Hash of bridge line as proof of knowledge of bridge line /// Hash of bridge line as proof of knowledge of bridge line
HashOfBridgeLine(HashOfBridgeLine), HashOfBridgeLine(HashOfBridgeLine),
/// Hash of bucket ID for Lox user /// Hash of bucket ID for Lox user
HashOfBucket(HashOfBucket), HashOfBucket(HashOfBucket),
} }

View File

@ -24,12 +24,16 @@ pub enum PositiveReportError {
pub struct PositiveReport { pub struct PositiveReport {
/// hashed fingerprint (SHA-1 hash of 20-byte bridge ID) /// hashed fingerprint (SHA-1 hash of 20-byte bridge ID)
pub fingerprint: [u8; 20], pub fingerprint: [u8; 20],
/// token from the bridge indicating it was reached /// token from the bridge indicating it was reached
bridge_token: Option<BridgeToken>, bridge_token: Option<BridgeToken>,
// proof of Lox cred with level >= 3 and this bridge // proof of Lox cred with level >= 3 and this bridge
lox_proof: lox_pr::Request, lox_proof: lox_pr::Request,
/// user's country code, may be an empty string /// user's country code, may be an empty string
pub country: String, pub country: String,
/// today's Julian date /// today's Julian date
pub date: u32, pub date: u32,
} }
@ -189,8 +193,10 @@ impl SerializablePositiveReport {
pub struct UnsignedBridgeToken { pub struct UnsignedBridgeToken {
/// hashed fingerprint (SHA-1 hash of 20-byte bridge ID) /// hashed fingerprint (SHA-1 hash of 20-byte bridge ID)
pub fingerprint: [u8; 20], pub fingerprint: [u8; 20],
/// client's country code /// client's country code
pub country: String, pub country: String,
/// today's Julian date /// today's Julian date
pub date: u32, pub date: u32,
} }
@ -259,6 +265,7 @@ impl SerializableUnsignedBridgeToken {
pub struct BridgeToken { pub struct BridgeToken {
/// the unsigned version of this token /// the unsigned version of this token
pub unsigned_bridge_token: UnsignedBridgeToken, pub unsigned_bridge_token: UnsignedBridgeToken,
/// signature from bridge's ed25519 key /// signature from bridge's ed25519 key
sig: Signature, sig: Signature,
} }