use lazy_static::lazy_static; use serde::{Deserialize, Serialize}; use sled::Db; use std::{ collections::{BTreeMap, HashMap, HashSet}, fmt, fs::File, io::BufReader, }; pub mod extra_info; pub mod negative_report; pub mod positive_report; use extra_info::*; use negative_report::*; use positive_report::*; #[derive(Debug, Deserialize)] pub struct Config { pub db: DbConfig, require_bridge_token: bool, } #[derive(Debug, Deserialize)] pub struct DbConfig { // The path for the server database, default is "server_db" pub db_path: String, } impl Default for DbConfig { fn default() -> DbConfig { DbConfig { db_path: "server_db".to_owned(), } } } lazy_static! { // known country codes based on Tor geoIP database // Produced with `cat /usr/share/tor/geoip{,6} | grep -v ^# | grep -o ..$ | sort | uniq | tr '[:upper:]' '[:lower:]' | tr '\n' ',' | sed 's/,/","/g'` pub static ref COUNTRY_CODES: HashSet<&'static str> = HashSet::from(["??","ad","ae","af","ag","ai","al","am","ao","ap","aq","ar","as","at","au","aw","ax","az","ba","bb","bd","be","bf","bg","bh","bi","bj","bl","bm","bn","bo","bq","br","bs","bt","bv","bw","by","bz","ca","cc","cd","cf","cg","ch","ci","ck","cl","cm","cn","co","cr","cs","cu","cv","cw","cx","cy","cz","de","dj","dk","dm","do","dz","ec","ee","eg","eh","er","es","et","eu","fi","fj","fk","fm","fo","fr","ga","gb","gd","ge","gf","gg","gh","gi","gl","gm","gn","gp","gq","gr","gs","gt","gu","gw","gy","hk","hm","hn","hr","ht","hu","id","ie","il","im","in","io","iq","ir","is","it","je","jm","jo","jp","ke","kg","kh","ki","km","kn","kp","kr","kw","ky","kz","la","lb","lc","li","lk","lr","ls","lt","lu","lv","ly","ma","mc","md","me","mf","mg","mh","mk","ml","mm","mn","mo","mp","mq","mr","ms","mt","mu","mv","mw","mx","my","mz","na","nc","ne","nf","ng","ni","nl","no","np","nr","nu","nz","om","pa","pe","pf","pg","ph","pk","pl","pm","pn","pr","ps","pt","pw","py","qa","re","ro","rs","ru","rw","sa","sb","sc","sd","se","sg","sh","si","sj","sk","sl","sm","sn","so","sr","ss","st","sv","sx","sy","sz","tc","td","tf","tg","th","tj","tk","tl","tm","tn","to","tr","tt","tv","tw","tz","ua","ug","um","us","uy","uz","va","vc","ve","vg","vi","vn","vu","wf","ws","ye","yt","za","zm","zw"]); // read config data at run time pub static ref CONFIG: Config = serde_json::from_reader( BufReader::new( File::open("config.json").expect("Could not read config file") // TODO: Make config filename configurable ) ).expect("Reading config file from JSON failed"); } /// Get Julian date pub fn get_date() -> u32 { time::OffsetDateTime::now_utc() .date() .to_julian_day() .try_into() .unwrap() } /// All the info for a bridge, to be stored in the database #[derive(Serialize, Deserialize)] pub struct BridgeInfo { /// hashed fingerprint (SHA-1 hash of 20-byte bridge ID) pub fingerprint: [u8; 20], /// nickname of bridge (probably not necessary) pub nickname: String, /// first Julian date we started collecting data on this bridge pub first_seen: u32, /// flag indicating whether the bridge is believed to be blocked pub is_blocked: bool, /// map of dates to data for that day pub info_by_day: HashMap, } impl BridgeInfo { pub fn new(fingerprint: [u8; 20], nickname: &String) -> Self { Self { fingerprint: fingerprint, nickname: nickname.to_string(), first_seen: get_date(), is_blocked: false, info_by_day: HashMap::::new(), } } } impl fmt::Display for BridgeInfo { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut str = format!( "fingerprint:{}\n", array_bytes::bytes2hex("", self.fingerprint).as_str() ); str.push_str(format!("nickname: {}\n", self.nickname).as_str()); str.push_str(format!("first_seen: {}\n", self.first_seen).as_str()); str.push_str(format!("is_blocked: {}\n", self.is_blocked).as_str()); str.push_str("info_by_day:"); for day in self.info_by_day.keys() { str.push_str(format!("\n day: {}", day).as_str()); let daily_info = self.info_by_day.get(day).unwrap(); for line in daily_info.to_string().lines() { str.push_str(format!("\n {}", line).as_str()); } } write!(f, "{}", str) } } #[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)] pub enum BridgeInfoType { BridgeIps, NegativeReports, PositiveReports, } /// Information about bridge reachability, gathered daily #[derive(Serialize, Deserialize)] pub struct DailyBridgeInfo { pub info_by_country: BTreeMap>, } impl DailyBridgeInfo { pub fn new() -> Self { Self { info_by_country: BTreeMap::>::new(), } } pub fn add_info( &mut self, info_type: BridgeInfoType, count_per_country: &BTreeMap, ) { 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::::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 { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut str = String::from("info:"); 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( format!( "\n cc: {}\n connections: {}\n negative reports: {}\n positive reports: {}", country, ip_count, nr_count, pr_count, ) .as_str(), ); } } write!(f, "{}", str) } } /// Adds the extra-info data for a single bridge to the database. If the /// database already contains an extra-info for this bridge for thid date, /// but this extra-info contains different data for some reason, use the /// greater count of connections from each country. pub fn add_extra_info_to_db(db: &Db, extra_info: ExtraInfo) { let fingerprint = extra_info.fingerprint; let mut bridge_info = match db.get(&fingerprint).unwrap() { Some(v) => bincode::deserialize(&v).unwrap(), None => BridgeInfo::new(fingerprint, &extra_info.nickname), }; // If we already have an entry, compare it with the new one. For each // country:count mapping, use the greater of the two counts. if bridge_info.info_by_day.contains_key(&extra_info.date) { let daily_bridge_info = bridge_info.info_by_day.get_mut(&extra_info.date).unwrap(); daily_bridge_info.add_info(BridgeInfoType::BridgeIps, &extra_info.bridge_ips); } else { // No existing entry; make a new one. let mut daily_bridge_info = DailyBridgeInfo { info_by_country: BTreeMap::>::new(), }; daily_bridge_info.add_info(BridgeInfoType::BridgeIps, &extra_info.bridge_ips); bridge_info .info_by_day .insert(extra_info.date, daily_bridge_info); } // Commit changes to database db.insert(fingerprint, bincode::serialize(&bridge_info).unwrap()) .unwrap(); }