extra_info.rs 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. /*! Fields we need from the extra-info documents for bridges...
  2. Note, this is NOT a complete implementation of the document format.
  3. (https://spec.torproject.org/dir-spec/extra-info-document-format.html) */
  4. use chrono::DateTime;
  5. use julianday::JulianDay;
  6. use serde::{Deserialize, Serialize};
  7. use std::{
  8. collections::{BTreeMap, HashMap, HashSet},
  9. fmt,
  10. };
  11. /// Fields we need from extra-info document
  12. #[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
  13. pub struct ExtraInfo {
  14. /// Bridge nickname, probably unused
  15. pub nickname: String,
  16. /// Bridge fingerprint, a SHA-1 hash of the bridge ID
  17. pub fingerprint: [u8; 20],
  18. /// Date (in UTC) that this document covered (bridge-stats-end if
  19. /// available) or that the document was published (published), stored
  20. /// as a Julian date because we don't need to know more precisely than
  21. /// the day.
  22. pub date: u32,
  23. /// Map of country codes and how many users (rounded up to a multiple of
  24. /// 8) have connected to that bridge during the day.
  25. /// Uses BTreeMap instead of HashMap so ExtraInfo can implement Hash.
  26. pub bridge_ips: BTreeMap<String, u32>,
  27. }
  28. impl ExtraInfo {
  29. /// Converts a map of keys and values into an ExtraInfo if all necessary fields
  30. /// are represented.
  31. fn from_map(entry: &HashMap<String, String>) -> Result<Self, String> {
  32. if !entry.contains_key("nickname") || !entry.contains_key("fingerprint") {
  33. // How did we get here??
  34. return Err("Cannot parse extra-info: Missing nickname or fingerprint".to_string());
  35. }
  36. if !(entry.contains_key("bridge-stats-end") || entry.contains_key("published"))
  37. || !entry.contains_key("bridge-ips")
  38. {
  39. // Some extra-infos are missing data on connecting IPs...
  40. // But we can't do anything in that case.
  41. return Err(format!(
  42. "Failed to parse extra-info for {} {}",
  43. entry.get("nickname").unwrap(),
  44. entry.get("fingerprint").unwrap()
  45. ));
  46. }
  47. let nickname = entry.get("nickname").unwrap().to_string();
  48. let fingerprint_str = entry.get("fingerprint").unwrap();
  49. if fingerprint_str.len() != 40 {
  50. return Err("Fingerprint must be 20 bytes".to_string());
  51. }
  52. let fingerprint = array_bytes::hex2array(fingerprint_str).unwrap();
  53. let date: u32 = {
  54. let date_str = if entry.contains_key("bridge-stats-end") {
  55. let line = entry.get("bridge-stats-end").unwrap();
  56. // Parse out (86400 s) from end of line
  57. &line[..line.find("(").unwrap() - 1]
  58. } else {
  59. entry.get("published").unwrap().as_str()
  60. };
  61. JulianDay::from(
  62. DateTime::parse_from_str(&(date_str.to_owned() + " +0000"), "%F %T %z")
  63. .unwrap()
  64. .date_naive(),
  65. )
  66. .inner()
  67. .try_into()
  68. .unwrap()
  69. };
  70. let bridge_ips_str = entry.get("bridge-ips").unwrap();
  71. let mut bridge_ips: BTreeMap<String, u32> = BTreeMap::new();
  72. let countries: Vec<&str> = bridge_ips_str.split(',').collect();
  73. for country in countries {
  74. if country != "" {
  75. // bridge-ips may be empty
  76. let (cc, count) = country.split_once('=').unwrap();
  77. bridge_ips.insert(cc.to_string(), count.parse::<u32>().unwrap());
  78. }
  79. }
  80. Ok(Self {
  81. nickname,
  82. fingerprint,
  83. date,
  84. bridge_ips,
  85. })
  86. }
  87. /// Accepts a downloaded extra-infos file as a big string, returns a set of
  88. /// the ExtraInfos represented by the file.
  89. pub fn parse_file<'a>(extra_info_str: &str) -> HashSet<Self> {
  90. let mut set = HashSet::<Self>::new();
  91. let mut entry = HashMap::<String, String>::new();
  92. for line in extra_info_str.lines() {
  93. let line = line;
  94. if line.starts_with("@type bridge-extra-info ") {
  95. if !entry.is_empty() {
  96. let extra_info = Self::from_map(&entry);
  97. if extra_info.is_ok() {
  98. set.insert(extra_info.unwrap());
  99. } else {
  100. // Just print the error and continue.
  101. println!("{}", extra_info.err().unwrap());
  102. }
  103. entry = HashMap::<String, String>::new();
  104. }
  105. } else {
  106. if line.starts_with("extra-info ") {
  107. // extra-info line has format:
  108. // extra-info <nickname> <fingerprint>
  109. let line_split: Vec<&str> = line.split(' ').collect();
  110. if line_split.len() != 3 {
  111. println!("Misformed extra-info line");
  112. } else {
  113. entry.insert("nickname".to_string(), line_split[1].to_string());
  114. entry.insert("fingerprint".to_string(), line_split[2].to_string());
  115. }
  116. } else {
  117. let (key, value) = match line.split_once(' ') {
  118. Some((k, v)) => (k, v),
  119. None => (line, ""),
  120. };
  121. entry.insert(key.to_string(), value.to_string());
  122. }
  123. }
  124. }
  125. // Do for the last one
  126. let extra_info = Self::from_map(&entry);
  127. if extra_info.is_ok() {
  128. set.insert(extra_info.unwrap());
  129. } else {
  130. println!("{}", extra_info.err().unwrap());
  131. }
  132. set
  133. }
  134. }
  135. /// Convert the ExtraInfo object to a string record, as in a downloaded file
  136. impl fmt::Display for ExtraInfo {
  137. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  138. let mut str = String::from("@type bridge-extra-info 1.3");
  139. str.push_str(
  140. format!(
  141. "\nextra-info {} {}",
  142. self.nickname,
  143. array_bytes::bytes2hex("", self.fingerprint).to_uppercase()
  144. )
  145. .as_str(),
  146. );
  147. let date = JulianDay::new(self.date.try_into().unwrap()).to_date();
  148. str.push_str(format!("\nbridge-stats-end {} 23:59:59 (86400 s)", date).as_str());
  149. str.push_str(format!("\npublished {} 23:59:59", date).as_str());
  150. // These should be sorted in descending order by count, but that's not
  151. // necessary for our purposes.
  152. str.push_str("\nbridge-ips ");
  153. let mut first_cc = true;
  154. for (cc, count) in &self.bridge_ips {
  155. if !first_cc {
  156. str.push(',');
  157. }
  158. str.push_str(format!("{}={}", cc, count,).as_str());
  159. first_cc = false;
  160. }
  161. str.push_str("\n");
  162. write!(f, "{}", str)
  163. }
  164. }