extra_info.rs 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  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::collections::{BTreeMap, HashMap, HashSet};
  8. /// Fields we need from extra-info document
  9. #[derive(Eq, PartialEq, Hash, Serialize, Deserialize)]
  10. pub struct ExtraInfo {
  11. /// Bridge nickname, probably unused
  12. pub nickname: String,
  13. /// Bridge fingerprint, a SHA-1 hash of the bridge ID
  14. pub fingerprint: [u8; 20],
  15. /// Date (in UTC) that this document covered (bridge-stats-end if
  16. /// available) or that the document was published (published), stored
  17. /// as a Julian date because we don't need to know more precisely than
  18. /// the day.
  19. pub date: u32,
  20. /// Map of country codes and how many users (rounded up to a multiple of
  21. /// 8) have connected to that bridge during the day.
  22. /// Uses BTreeMap instead of HashMap so ExtraInfo can implement Hash.
  23. pub bridge_ips: BTreeMap<String, u32>,
  24. }
  25. impl ExtraInfo {
  26. /// Converts a map of keys and values into an ExtraInfo if all necessary fields
  27. /// are represented.
  28. fn from_map(entry: &HashMap<String, String>) -> Result<Self, String> {
  29. if !entry.contains_key("nickname") || !entry.contains_key("fingerprint") {
  30. // How did we get here??
  31. return Err("Cannot parse extra-info: Missing nickname or fingerprint".to_string());
  32. }
  33. if !(entry.contains_key("bridge-stats-end") || entry.contains_key("published"))
  34. || !entry.contains_key("bridge-ips")
  35. {
  36. // Some extra-infos are missing data on connecting IPs...
  37. // But we can't do anything in that case.
  38. return Err(format!(
  39. "Failed to parse extra-info for {} {}",
  40. entry.get("nickname").unwrap(),
  41. entry.get("fingerprint").unwrap()
  42. ));
  43. }
  44. let nickname = entry.get("nickname").unwrap().to_string();
  45. let fingerprint_str = entry.get("fingerprint").unwrap();
  46. if fingerprint_str.len() != 40 {
  47. return Err("Fingerprint must be 20 bytes".to_string());
  48. }
  49. let fingerprint = array_bytes::hex2array(fingerprint_str).unwrap();
  50. let date: u32 = {
  51. let date_str = if entry.contains_key("bridge-stats-end") {
  52. let line = entry.get("bridge-stats-end").unwrap();
  53. // Parse out (86400 s) from end of line
  54. &line[..line.find("(").unwrap() - 1]
  55. } else {
  56. entry.get("published").unwrap().as_str()
  57. };
  58. JulianDay::from(
  59. DateTime::parse_from_str(&(date_str.to_owned() + " +0000"), "%F %T %z")
  60. .unwrap()
  61. .date_naive(),
  62. )
  63. .inner()
  64. .try_into()
  65. .unwrap()
  66. };
  67. let bridge_ips_str = entry.get("bridge-ips").unwrap();
  68. let mut bridge_ips: BTreeMap<String, u32> = BTreeMap::new();
  69. let countries: Vec<&str> = bridge_ips_str.split(',').collect();
  70. for country in countries {
  71. if country != "" {
  72. // bridge-ips may be empty
  73. let (cc, count) = country.split_once('=').unwrap();
  74. bridge_ips.insert(cc.to_string(), count.parse::<u32>().unwrap());
  75. }
  76. }
  77. Ok(Self {
  78. nickname,
  79. fingerprint,
  80. date,
  81. bridge_ips,
  82. })
  83. }
  84. /// Accepts a downloaded extra-infos file as a big string, returns a set of
  85. /// the ExtraInfos represented by the file.
  86. pub fn parse_file<'a>(extra_info_str: &str) -> HashSet<Self> {
  87. let mut set = HashSet::<Self>::new();
  88. let mut entry = HashMap::<String, String>::new();
  89. for line in extra_info_str.lines() {
  90. let line = line;
  91. if line.starts_with("@type bridge-extra-info ") {
  92. if !entry.is_empty() {
  93. let extra_info = Self::from_map(&entry);
  94. if extra_info.is_ok() {
  95. set.insert(extra_info.unwrap());
  96. } else {
  97. // Just print the error and continue.
  98. println!("{}", extra_info.err().unwrap());
  99. }
  100. entry = HashMap::<String, String>::new();
  101. }
  102. } else {
  103. if line.starts_with("extra-info ") {
  104. // extra-info line has format:
  105. // extra-info <nickname> <fingerprint>
  106. let line_split: Vec<&str> = line.split(' ').collect();
  107. if line_split.len() != 3 {
  108. println!("Misformed extra-info line");
  109. } else {
  110. entry.insert("nickname".to_string(), line_split[1].to_string());
  111. entry.insert("fingerprint".to_string(), line_split[2].to_string());
  112. }
  113. } else {
  114. let (key, value) = match line.split_once(' ') {
  115. Some((k, v)) => (k, v),
  116. None => (line, ""),
  117. };
  118. entry.insert(key.to_string(), value.to_string());
  119. }
  120. }
  121. }
  122. // Do for the last one
  123. let extra_info = Self::from_map(&entry);
  124. if extra_info.is_ok() {
  125. set.insert(extra_info.unwrap());
  126. } else {
  127. println!("{}", extra_info.err().unwrap());
  128. }
  129. set
  130. }
  131. }