From d1820c1516a31a149fc51a9e5126bf899e4c4e08 Mon Sep 17 00:00:00 2001 From: Chelsea Holland Komlo Date: Wed, 27 Sep 2017 19:48:07 +0000 Subject: rust implementation of protover --- src/rust/protover/Cargo.toml | 22 + src/rust/protover/ffi.rs | 205 +++++++++ src/rust/protover/include.am | 14 + src/rust/protover/lib.rs | 34 ++ src/rust/protover/protover.rs | 847 ++++++++++++++++++++++++++++++++++++ src/rust/protover/tests/protover.rs | 288 ++++++++++++ 6 files changed, 1410 insertions(+) create mode 100644 src/rust/protover/Cargo.toml create mode 100644 src/rust/protover/ffi.rs create mode 100644 src/rust/protover/include.am create mode 100644 src/rust/protover/lib.rs create mode 100644 src/rust/protover/protover.rs create mode 100644 src/rust/protover/tests/protover.rs (limited to 'src/rust/protover') diff --git a/src/rust/protover/Cargo.toml b/src/rust/protover/Cargo.toml new file mode 100644 index 0000000000..a8f794f838 --- /dev/null +++ b/src/rust/protover/Cargo.toml @@ -0,0 +1,22 @@ +[package] +authors = ["The Tor Project"] +version = "0.0.1" +name = "protover" + +[dependencies] +libc = "0.2.22" + +[dependencies.smartlist] +path = "../smartlist" + +[dependencies.external] +path = "../external" + +[dependencies.tor_util] +path = "../tor_util" + +[lib] +name = "protover" +path = "lib.rs" +crate_type = ["rlib", "staticlib"] + diff --git a/src/rust/protover/ffi.rs b/src/rust/protover/ffi.rs new file mode 100644 index 0000000000..7365d7cd8f --- /dev/null +++ b/src/rust/protover/ffi.rs @@ -0,0 +1,205 @@ +//! FFI functions, only to be called from C. +//! +//! Equivalent C versions of this api are in `src/or/protover.c` + +use libc::{c_char, c_int, uint32_t}; +use std::ffi::CStr; +use std::ffi::CString; + +use protover::*; +use smartlist::*; + +/// Translate C enums to Rust Proto enums, using the integer value of the C +/// enum to map to its associated Rust enum +/// This is dependant on the associated C enum preserving ordering. +/// Modify the C documentation to give warnings- you must also re-order the rust +fn translate_to_rust(c_proto: uint32_t) -> Result { + match c_proto { + 0 => Ok(Proto::Link), + 1 => Ok(Proto::LinkAuth), + 2 => Ok(Proto::Relay), + 3 => Ok(Proto::DirCache), + 4 => Ok(Proto::HSDir), + 5 => Ok(Proto::HSIntro), + 6 => Ok(Proto::HSRend), + 7 => Ok(Proto::Desc), + 8 => Ok(Proto::Microdesc), + 9 => Ok(Proto::Cons), + _ => Err("Invalid protocol type"), + } +} + +/// Provide an interface for C to translate arguments and return types for +/// protover::all_supported +#[no_mangle] +pub extern "C" fn rust_protover_all_supported( + c_relay_version: *const c_char, + missing_out: *mut *mut c_char, +) -> c_int { + + if c_relay_version.is_null() { + return 1; + } + + // Require an unsafe block to read the version from a C string. The pointer + // is checked above to ensure it is not null. + let c_str: &CStr; + unsafe { + c_str = CStr::from_ptr(c_relay_version); + } + + let relay_version = match c_str.to_str() { + Ok(n) => n, + Err(_) => return 1, + }; + + let (is_supported, unsupported) = all_supported(relay_version); + + if unsupported.len() > 0 { + let c_unsupported = match CString::new(unsupported) { + Ok(n) => n, + Err(_) => return 1, + }; + + let ptr = c_unsupported.into_raw(); + unsafe { *missing_out = ptr }; + } + + return if is_supported { 1 } else { 0 }; +} + +/// Provide an interface for C to translate arguments and return types for +/// protover::list_supports_protocol +#[no_mangle] +pub extern "C" fn rust_protocol_list_supports_protocol( + c_protocol_list: *const c_char, + c_protocol: uint32_t, + version: uint32_t, +) -> c_int { + if c_protocol_list.is_null() { + return 1; + } + + // Require an unsafe block to read the version from a C string. The pointer + // is checked above to ensure it is not null. + let c_str: &CStr; + unsafe { + c_str = CStr::from_ptr(c_protocol_list); + } + + let protocol_list = match c_str.to_str() { + Ok(n) => n, + Err(_) => return 1, + }; + + let protocol = match translate_to_rust(c_protocol) { + Ok(n) => n, + Err(_) => return 0, + }; + + let is_supported = + protover_string_supports_protocol(protocol_list, protocol, version); + + return if is_supported { 1 } else { 0 }; +} + +/// Provide an interface for C to translate arguments and return types for +/// protover::get_supported_protocols +#[no_mangle] +pub extern "C" fn rust_protover_get_supported_protocols() -> *mut c_char { + // Not handling errors when unwrapping as the content is controlled + // and is an empty string + let empty = CString::new("").unwrap(); + + let supported = get_supported_protocols(); + let c_supported = match CString::new(supported) { + Ok(n) => n, + Err(_) => return empty.into_raw(), + }; + + c_supported.into_raw() +} + +/// Provide an interface for C to translate arguments and return types for +/// protover::compute_vote +#[no_mangle] +pub extern "C" fn rust_protover_compute_vote( + list: *const Stringlist, + threshold: c_int, +) -> *mut c_char { + // Not handling errors when unwrapping as the content is controlled + // and is an empty string + let empty = CString::new("").unwrap(); + + if list.is_null() { + return empty.into_raw(); + } + + // Dereference of raw pointer requires an unsafe block. The pointer is + // checked above to ensure it is not null. + let data: Vec; + unsafe { + data = (*list).get_list(); + } + + let vote = compute_vote(data, threshold); + let c_vote = match CString::new(vote) { + Ok(n) => n, + Err(_) => return empty.into_raw(), + }; + + c_vote.into_raw() +} + +/// Provide an interface for C to translate arguments and return types for +/// protover::is_supported_here +#[no_mangle] +pub extern "C" fn rust_protover_is_supported_here( + c_protocol: uint32_t, + version: uint32_t, +) -> c_int { + let protocol = match translate_to_rust(c_protocol) { + Ok(n) => n, + Err(_) => return 0, + }; + + let is_supported = is_supported_here(protocol, version); + + return if is_supported { 1 } else { 0 }; +} + +/// Provide an interface for C to translate arguments and return types for +/// protover::compute_for_old_tor +#[no_mangle] +pub extern "C" fn rust_protover_compute_for_old_tor( + version: *const c_char, +) -> *mut c_char { + // Not handling errors when unwrapping as the content is controlled + // and is an empty string + let empty = CString::new("").unwrap(); + + if version.is_null() { + return empty.into_raw(); + } + + // Require an unsafe block to read the version from a C string. The pointer + // is checked above to ensure it is not null. + let c_str: &CStr; + unsafe { + c_str = CStr::from_ptr(version); + } + + let version = match c_str.to_str() { + Ok(n) => n, + Err(_) => return empty.into_raw(), + }; + + let supported = compute_for_old_tor(&version); + + let c_supported = match CString::new(supported) { + Ok(n) => n, + Err(_) => return empty.into_raw(), + }; + + c_supported.into_raw() +} diff --git a/src/rust/protover/include.am b/src/rust/protover/include.am new file mode 100644 index 0000000000..326dbbb7f2 --- /dev/null +++ b/src/rust/protover/include.am @@ -0,0 +1,14 @@ +EXTRA_DIST +=\ + src/rust/protover/Cargo.toml \ + src/rust/protover/lib.rs \ + src/rust/protover/ffi.rs \ + src/rust/protover/external.rs + +src/rust/target/release/@TOR_RUST_PROTOVER_STATIC_NAME@: FORCE + ( cd "$(abs_top_srcdir)/src/rust/protover" ; \ + CARGO_TARGET_DIR="$(abs_top_builddir)/src/rust/target" \ + CARGO_HOME="$(abs_top_builddir)/src/rust" \ + $(CARGO) build --release --quiet $(CARGO_ONLINE) ) + +FORCE: + diff --git a/src/rust/protover/lib.rs b/src/rust/protover/lib.rs new file mode 100644 index 0000000000..89378c7b7e --- /dev/null +++ b/src/rust/protover/lib.rs @@ -0,0 +1,34 @@ +#![feature(inclusive_range_syntax)] + +//! Copyright (c) 2016-2017, The Tor Project, Inc. */ +//! See LICENSE for licensing information */ + +//! Versioning information for different pieces of the Tor protocol. +//! +//! The below description is taken from src/rust/protover.c, which is currently +//! enabled by default. We are in the process of experimenting with Rust in +//! tor, and this protover module is implemented to help achieve this goal. +//! +//! Starting in version 0.2.9.3-alpha, Tor places separate version numbers on +//! each of the different components of its protocol. Relays use these numbers +//! to advertise what versions of the protocols they can support, and clients +//! use them to find what they can ask a given relay to do. Authorities vote +//! on the supported protocol versions for each relay, and also vote on the +//! which protocols you should have to support in order to be on the Tor +//! network. All Tor instances use these required/recommended protocol versions +//! to tell what level of support for recent protocols each relay has, and +//! to decide whether they should be running given their current protocols. +//! +//! The main advantage of these protocol versions numbers over using Tor +//! version numbers is that they allow different implementations of the Tor +//! protocols to develop independently, without having to claim compatibility +//! with specific versions of Tor. + +extern crate libc; +extern crate smartlist; +extern crate external; + +mod protover; +pub mod ffi; + +pub use protover::*; diff --git a/src/rust/protover/protover.rs b/src/rust/protover/protover.rs new file mode 100644 index 0000000000..0893362cec --- /dev/null +++ b/src/rust/protover/protover.rs @@ -0,0 +1,847 @@ +use external::c_tor_version_as_new_as; + +use std::str::FromStr; +use std::str::SplitN; +use std::fmt; +use std::collections::HashMap; +use std::collections::HashSet; +use std::string::String; + +/// The first version of Tor that included "proto" entries in its descriptors. +/// Authorities should use this to decide whether to guess proto lines. +const FIRST_TOR_VERSION_TO_ADVERTISE_PROTOCOLS: &'static str = "0.2.9.3-alpha"; + +/// The maximum number of subprotocol version numbers we will attempt to expand +/// before concluding that someone is trying to DoS us +const MAX_PROTOCOLS_TO_EXPAND: u32 = 500; + +/// Currently supported protocols and their versions +const SUPPORTED_PROTOCOLS: &'static [&'static str] = &[ + "Cons=1-2", + "Desc=1-2", + "DirCache=1-2", + "HSDir=1-2", + "HSIntro=3-4", + "HSRend=1-2", + "Link=1-4", + "LinkAuth=1,3", + "Microdesc=1-2", + "Relay=1-2", +]; + +/// Known subprotocols in Tor. Indicates which subprotocol a relay supports. +#[derive(Hash, Eq, PartialEq, Debug)] +pub enum Proto { + Cons, + Desc, + DirCache, + HSDir, + HSIntro, + HSRend, + Link, + LinkAuth, + Microdesc, + Relay, +} + +impl fmt::Display for Proto { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +/// Translates a string representation of a protocol into a Proto type. +/// Error if the string is an unrecognized protocol name. +impl FromStr for Proto { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + match s { + "Cons" => Ok(Proto::Cons), + "Desc" => Ok(Proto::Desc), + "DirCache" => Ok(Proto::DirCache), + "HSDir" => Ok(Proto::HSDir), + "HSIntro" => Ok(Proto::HSIntro), + "HSRend" => Ok(Proto::HSRend), + "Link" => Ok(Proto::Link), + "LinkAuth" => Ok(Proto::LinkAuth), + "Microdesc" => Ok(Proto::Microdesc), + "Relay" => Ok(Proto::Relay), + _ => Err("Not a valid protocol type"), + } + } +} + +/// Get the string representation of current supported protocols +/// +/// # Returns +/// +/// A `String` whose value is the existing protocols supported by tor. +/// Returned data is in the format as follows: +/// +/// "HSDir=1-1 LinkAuth=1" +/// +pub fn get_supported_protocols() -> String { + SUPPORTED_PROTOCOLS.join(" ") +} + +/// Translates a vector representation of a protocol list into a HashMap +fn parse_protocols( + protocols: &[&str], +) -> Result>, &'static str> { + let mut parsed = HashMap::new(); + + for subproto in protocols { + let (name, version) = get_proto_and_vers(subproto)?; + parsed.insert(name, version); + } + Ok(parsed) +} + +/// Translates a string representation of a protocol list to a HashMap +fn parse_protocols_from_string<'a>( + protocol_string: &'a str, +) -> Result>, &'static str> { + let protocols: &[&'a str] = + &protocol_string.split(" ").collect::>()[..]; + + parse_protocols(protocols) +} + +/// Translates supported tor versions from a string into a HashMap, which is +/// useful when looking up a specific subprotocol. +/// +/// # Returns +/// +/// A `Result` whose `Ok` value is a `HashMap>` holding all +/// subprotocols and versions currently supported by tor. +/// +/// The returned `Result`'s `Err` value is an `&'static str` with a description +/// of the error. +/// +fn tor_supported() -> Result>, &'static str> { + parse_protocols(&SUPPORTED_PROTOCOLS) +} + +/// Get the unique version numbers supported by a subprotocol. +/// +/// # Inputs +/// +/// * `version_string`, a string comprised of "[0-9,-]" +/// +/// # Returns +/// +/// A `Result` whose `Ok` value is a `HashSet` holding all of the unique +/// version numbers. If there were ranges in the `version_string`, then these +/// are expanded, i.e. `"1-3"` would expand to `HashSet::new([1, 2, 3])`. +/// The returned HashSet is *unordered*. +/// +/// The returned `Result`'s `Err` value is an `&'static str` with a description +/// of the error. +/// +/// # Errors +/// +/// This function will error if: +/// +/// * the `version_string` is empty or contains an equals (`"="`) sign, +/// * the expansion of a version range produces an error (see `expand_version_range`), +/// * any single version number is not parseable as an `u32` in radix 10, or +/// * there are greater than 2^16 version numbers to expand. +/// +fn get_versions(version_string: &str) -> Result, &'static str> { + if version_string.is_empty() { + return Err("version string is empty"); + } + + let mut versions = HashSet::::new(); + + for piece in version_string.split(",") { + if piece.contains("-") { + for p in expand_version_range(piece)? { + versions.insert(p); + } + } else { + versions.insert(u32::from_str(piece).or( + Err("invalid protocol entry"), + )?); + } + + if versions.len() > MAX_PROTOCOLS_TO_EXPAND as usize { + return Err("Too many versions to expand"); + } + } + Ok(versions) +} + + +/// Parse the subprotocol type and its version numbers. +/// +/// # Inputs +/// +/// * A `protocol_entry` string, comprised of a keyword, an "=" sign, and one +/// or more version numbers. +/// +/// # Returns +/// +/// A `Result` whose `Ok` value is a tuple of `(Proto, HashSet)`, where the +/// first element is the subprotocol type (see `protover::Proto`) and the last +/// element is a(n unordered) set of unique version numbers which are supported. +/// Otherwise, the `Err` value of this `Result` is a description of the error +/// +fn get_proto_and_vers<'a>( + protocol_entry: &'a str, +) -> Result<(Proto, HashSet), &'static str> { + let mut parts: SplitN<'a, &str> = protocol_entry.splitn(2, "="); + + let proto: &str = match parts.next() { + Some(n) => n, + None => return Err("invalid protover entry"), + }; + + let vers: &str = match parts.next() { + Some(n) => n, + None => return Err("invalid protover entry"), + }; + + let versions = get_versions(vers)?; + let proto_name = proto.parse()?; + + Ok((proto_name, versions)) +} + +/// Parses a single subprotocol entry string into subprotocol and version +/// parts, and then checks whether any of those versions are unsupported. +/// Helper for protover::all_supported +/// +/// # Inputs +/// +/// Accepted data is in the string format as follows: +/// +/// "HSDir=1-1" +/// +/// # Returns +/// +/// Returns `true` if the protocol entry is well-formatted and only contains +/// versions that are also supported in tor. Otherwise, returns false +/// +fn contains_only_supported_protocols(proto_entry: &str) -> bool { + let (name, mut vers) = match get_proto_and_vers(proto_entry) { + Ok(n) => n, + Err(_) => return false, + }; + + let currently_supported: HashMap> = + match tor_supported() { + Ok(n) => n, + Err(_) => return false, + }; + + let supported_versions = match currently_supported.get(&name) { + Some(n) => n, + None => return false, + }; + + vers.retain(|x| !supported_versions.contains(x)); + vers.is_empty() +} + +/// Determine if we support every protocol a client supports, and if not, +/// determine which protocols we do not have support for. +/// +/// # Inputs +/// +/// Accepted data is in the string format as follows: +/// +/// "HSDir=1-1 LinkAuth=1-2" +/// +/// # Returns +/// +/// Return `true` if every protocol version is one that we support. +/// Otherwise, return `false`. +/// Optionally, return parameters which the client supports but which we do not +/// +/// # Examples +/// ``` +/// use protover::all_supported; +/// +/// let (is_supported, unsupported) = all_supported("Link=1"); +/// assert_eq!(true, is_supported); +/// +/// let (is_supported, unsupported) = all_supported("Link=5-6"); +/// assert_eq!(false, is_supported); +/// assert_eq!("Link=5-6", unsupported); +/// +pub fn all_supported(protocols: &str) -> (bool, String) { + let unsupported: Vec<&str> = protocols + .split_whitespace() + .filter(|v| !contains_only_supported_protocols(v)) + .collect::>(); + + (unsupported.is_empty(), unsupported.join(" ")) +} + +/// Return true iff the provided protocol list includes support for the +/// indicated protocol and version. +/// Otherwise, return false +/// +/// # Inputs +/// +/// * `list`, a string representation of a list of protocol entries. +/// * `proto`, a `Proto` to test support for +/// * `vers`, a `u32` version which we will go on to determine whether the +/// specified protocol supports. +/// +/// # Examples +/// ``` +/// use protover::*; +/// +/// let is_supported = protover_string_supports_protocol("Link=3-4 Cons=1", Proto::Cons,1); +/// assert_eq!(true, is_supported); +/// +/// let is_not_supported = protover_string_supports_protocol("Link=3-4 Cons=1", Proto::Cons,5); +/// assert_eq!(false, is_not_supported) +/// ``` +pub fn protover_string_supports_protocol( + list: &str, + proto: Proto, + vers: u32, +) -> bool { + let supported: HashMap>; + + match parse_protocols_from_string(list) { + Ok(result) => supported = result, + Err(_) => return false, + } + + let supported_versions = match supported.get(&proto) { + Some(n) => n, + None => return false, + }; + + supported_versions.contains(&vers) +} + +/// Fully expand a version range. For example, 1-3 expands to 1,2,3 +/// Helper for get_versions +/// +/// # Inputs +/// +/// `range`, a string comprised of "[0-9,-]" +/// +/// # Returns +/// +/// A `Result` whose `Ok` value a vector of unsigned integers representing the +/// expanded range of supported versions by a single protocol. +/// Otherwise, the `Err` value of this `Result` is a description of the error +/// +/// # Errors +/// +/// This function will error if: +/// +/// * the specified range is empty +/// * the version range does not contain both a valid lower and upper bound. +/// +fn expand_version_range(range: &str) -> Result, &'static str> { + if range.is_empty() { + return Err("version string empty"); + } + + let mut parts = range.split("-"); + + let lower_string: &str = parts.next().ok_or( + "cannot parse protocol range lower bound", + )?; + + let lower: u32 = u32::from_str_radix(lower_string, 10).or(Err( + "cannot parse protocol range lower bound", + ))?; + + let higher_string: &str = parts.next().ok_or( + "cannot parse protocol range upper bound", + )?; + + let higher: u32 = u32::from_str_radix(higher_string, 10).or(Err( + "cannot parse protocol range upper bound", + ))?; + + Ok((lower...higher).collect()) +} + +/// Checks to see if there is a continuous range of integers, starting at the +/// first in the list. Returns the last integer in the range if a range exists. +/// Helper for compute_vote +/// +/// # Inputs +/// +/// `list`, an ordered vector of `u32` integers of "[0-9,-]" representing the +/// supported versions for a single protocol. +/// +/// # Returns +/// +/// A `bool` indicating whether the list contains a range, starting at the +/// first in the list, and an `u32` of the last integer in the range. +/// +/// For example, if given vec![1, 2, 3, 5], find_range will return true, +/// as there is a continuous range, and 3, which is the last number in the +/// continuous range. +/// +fn find_range(list: &Vec) -> (bool, u32) { + if list.len() == 0 { + return (false, 0); + } + + let mut iterable = list.iter().peekable(); + let mut range_end: u32 = match iterable.next() { + Some(n) => *n, + None => return (false, 0), + }; + + let mut has_range = false; + + while iterable.peek().is_some() { + let n = *iterable.next().unwrap(); + if n != range_end + 1 { + break; + } + + has_range = true; + range_end = n; + } + + (has_range, range_end) +} + +/// Contracts a HashSet representation of supported versions into a string. +/// Helper for compute_vote +/// +/// # Inputs +/// +/// `supported_set`, a set of integers of "[0-9,-]" representing the +/// supported versions for a single protocol. +/// +/// # Returns +/// +/// A `String` representation of this set in ascending order. +/// +fn contract_protocol_list<'a>(supported_set: &'a HashSet) -> String { + let mut supported_clone = supported_set.clone(); + let mut supported: Vec = supported_clone.drain().collect(); + supported.sort(); + + let mut final_output: Vec = Vec::new(); + + while supported.len() != 0 { + let (has_range, end) = find_range(&supported); + let current = supported.remove(0); + + if has_range { + final_output.push(format!( + "{}-{}", + current.to_string(), + &end.to_string(), + )); + supported.retain(|&x| x > end); + } else { + final_output.push(current.to_string()); + } + } + + final_output.join(",") +} + +/// Parses a protocol list without validating the protocol names +/// +/// # Inputs +/// +/// * `protocol_string`, a string comprised of keys and values, both which are +/// strings. The keys are the protocol names while values are a string +/// representation of the supported versions. +/// +/// The input is _not_ expected to be a subset of the Proto types +/// +/// # Returns +/// +/// A `Result` whose `Ok` value is a `HashSet` holding all of the unique +/// version numbers. +/// +/// The returned `Result`'s `Err` value is an `&'static str` with a description +/// of the error. +/// +/// # Errors +/// +/// This function will error if: +/// +/// * The protocol string does not follow the "protocol_name=version_list" +/// expected format +/// * If the version string is malformed. See `get_versions`. +/// +fn parse_protocols_from_string_with_no_validation<'a>( + protocol_string: &'a str, +) -> Result>, &'static str> { + let protocols: &[&'a str] = + &protocol_string.split(" ").collect::>()[..]; + + let mut parsed: HashMap> = HashMap::new(); + + for subproto in protocols { + let mut parts: SplitN<'a, &str> = subproto.splitn(2, "="); + + let name: &str = match parts.next() { + Some(n) => n, + None => return Err("invalid protover entry"), + }; + + let vers: &str = match parts.next() { + Some(n) => n, + None => return Err("invalid protover entry"), + }; + + let versions = get_versions(vers)?; + + parsed.insert(String::from(name), versions); + } + Ok(parsed) +} + +/// Protocol voting implementation. +/// +/// Given a list of strings describing protocol versions, return a new +/// string encoding all of the protocols that are listed by at +/// least threshold of the inputs. +/// +/// The string is sorted according to the following conventions: +/// - Protocols names are alphabetized +/// - Protocols are in order low to high +/// - Individual and ranges are listed together. For example, +/// "3, 5-10,13" +/// - All entries are unique +/// +/// # Examples +/// ``` +/// use protover::compute_vote; +/// +/// let protos = vec![String::from("Link=3-4"), String::from("Link=3")]; +/// let vote = compute_vote(protos, 2); +/// assert_eq!("Link=3", vote) +/// ``` +pub fn compute_vote( + list_of_proto_strings: Vec, + threshold: i32, +) -> String { + let empty = String::from(""); + + if list_of_proto_strings.is_empty() { + return empty; + } + + // all_count is a structure to represent the count of the number of + // supported versions for a specific protocol. For example, in JSON format: + // { + // "FirstSupportedProtocol": { + // "1": "3", + // "2": "1" + // } + // } + // means that FirstSupportedProtocol has three votes which support version + // 1, and one vote that supports version 2 + let mut all_count: HashMap> = HashMap::new(); + + // parse and collect all of the protos and their versions and collect them + for vote in list_of_proto_strings { + let this_vote: HashMap> = + match parse_protocols_from_string_with_no_validation(&vote) { + Ok(result) => result, + Err(_) => continue, + }; + + for (protocol, versions) in this_vote { + let supported_vers: &mut HashMap = + all_count.entry(protocol).or_insert(HashMap::new()); + + for version in versions { + let counter: &mut usize = + supported_vers.entry(version).or_insert(0); + *counter += 1; + } + } + } + + let mut final_output: HashMap = + HashMap::with_capacity(SUPPORTED_PROTOCOLS.len()); + + // Go through and remove verstions that are less than the threshold + for (protocol, versions) in all_count { + let mut meets_threshold = HashSet::new(); + for (version, count) in versions { + if count >= threshold as usize { + meets_threshold.insert(version); + } + } + + // For each protocol, compress its version list into the expected + // protocol version string format + let contracted = contract_protocol_list(&meets_threshold); + if !contracted.is_empty() { + final_output.insert(protocol, contracted); + } + } + + write_vote_to_string(&final_output) +} + +/// Return a String comprised of protocol entries in alphabetical order +/// +/// # Inputs +/// +/// * `vote`, a `HashMap` comprised of keys and values, both which are strings. +/// The keys are the protocol names while values are a string representation of +/// the supported versions. +/// +/// # Returns +/// +/// A `String` whose value is series of pairs, comprising of the protocol name +/// and versions that it supports. The string takes the following format: +/// +/// "first_protocol_name=1,2-5, second_protocol_name=4,5" +/// +/// Sorts the keys in alphabetical order and creates the expected subprotocol +/// entry format. +/// +fn write_vote_to_string(vote: &HashMap) -> String { + let mut keys: Vec<&String> = vote.keys().collect(); + keys.sort(); + + let mut output = Vec::new(); + for key in keys { + // TODO error in indexing here? + output.push(format!("{}={}", key, vote[key])); + } + output.join(" ") +} + +/// Returns a boolean indicating whether the given protocol and version is +/// supported in any of the existing Tor protocols +/// +/// # Examples +/// ``` +/// use protover::*; +/// +/// let is_supported = is_supported_here(Proto::Link, 5); +/// assert_eq!(false, is_supported); +/// +/// let is_supported = is_supported_here(Proto::Link, 1); +/// assert_eq!(true, is_supported); +/// ``` +pub fn is_supported_here(proto: Proto, vers: u32) -> bool { + let currently_supported: HashMap>; + + match tor_supported() { + Ok(result) => currently_supported = result, + Err(_) => return false, + } + + let supported_versions = match currently_supported.get(&proto) { + Some(n) => n, + None => return false, + }; + + supported_versions.contains(&vers) +} + +/// Older versions of Tor cannot infer their own subprotocols +/// Used to determine which subprotocols are supported by older Tor versions. +/// +/// # Inputs +/// +/// * `version`, a string comprised of "[0-9,-]" +/// +/// # Returns +/// +/// A `String` whose value is series of pairs, comprising of the protocol name +/// and versions that it supports. The string takes the following format: +/// +/// "HSDir=1-1 LinkAuth=1" +/// +/// This function returns the protocols that are supported by the version input, +/// only for tor versions older than FIRST_TOR_VERSION_TO_ADVERTISE_PROTOCOLS. +/// +pub fn compute_for_old_tor(version: &str) -> String { + if c_tor_version_as_new_as( + version, + FIRST_TOR_VERSION_TO_ADVERTISE_PROTOCOLS, + ) + { + return String::new(); + } + + if c_tor_version_as_new_as(version, "0.2.9.1-alpha") { + let ret = "Cons=1-2 Desc=1-2 DirCache=1 HSDir=1 HSIntro=3 HSRend=1-2 \ + Link=1-4 LinkAuth=1 Microdesc=1-2 Relay=1-2"; + return String::from(ret); + } + + if c_tor_version_as_new_as(version, "0.2.7.5") { + let ret = "Cons=1-2 Desc=1-2 DirCache=1 HSDir=1 HSIntro=3 HSRend=1 \ + Link=1-4 LinkAuth=1 Microdesc=1-2 Relay=1-2"; + return String::from(ret); + } + + if c_tor_version_as_new_as(version, "0.2.4.19") { + let ret = "Cons=1 Desc=1 DirCache=1 HSDir=1 HSIntro=3 HSRend=1 \ + Link=1-4 LinkAuth=1 Microdesc=1 Relay=1-2"; + return String::from(ret); + } + String::new() +} + +#[cfg(test)] +mod test { + #[test] + fn test_get_versions() { + use std::collections::HashSet; + + use super::get_versions; + + assert_eq!(Err("version string is empty"), get_versions("")); + assert_eq!(Err("invalid protocol entry"), get_versions("a,b")); + assert_eq!(Err("invalid protocol entry"), get_versions("1,!")); + + { + let mut versions: HashSet = HashSet::new(); + versions.insert(1); + assert_eq!(Ok(versions), get_versions("1")); + } + { + let mut versions: HashSet = HashSet::new(); + versions.insert(1); + versions.insert(2); + assert_eq!(Ok(versions), get_versions("1,2")); + } + { + let mut versions: HashSet = HashSet::new(); + versions.insert(1); + versions.insert(2); + versions.insert(3); + assert_eq!(Ok(versions), get_versions("1-3")); + } + { + let mut versions: HashSet = HashSet::new(); + versions.insert(1); + versions.insert(2); + versions.insert(5); + assert_eq!(Ok(versions), get_versions("1-2,5")); + } + { + let mut versions: HashSet = HashSet::new(); + versions.insert(1); + versions.insert(3); + versions.insert(4); + versions.insert(5); + assert_eq!(Ok(versions), get_versions("1,3-5")); + } + } + + #[test] + fn test_contains_only_supported_protocols() { + use super::contains_only_supported_protocols; + + assert_eq!(false, contains_only_supported_protocols("")); + assert_eq!(false, contains_only_supported_protocols("Cons=")); + assert_eq!(true, contains_only_supported_protocols("Cons=1")); + assert_eq!(false, contains_only_supported_protocols("Cons=0")); + assert_eq!(false, contains_only_supported_protocols("Cons=0-1")); + assert_eq!(false, contains_only_supported_protocols("Cons=5")); + assert_eq!(false, contains_only_supported_protocols("Cons=1-5")); + assert_eq!(false, contains_only_supported_protocols("Cons=1,5")); + assert_eq!(false, contains_only_supported_protocols("Cons=5,6")); + assert_eq!(false, contains_only_supported_protocols("Cons=1,5,6")); + assert_eq!(true, contains_only_supported_protocols("Cons=1,2")); + assert_eq!(true, contains_only_supported_protocols("Cons=1-2")); + } + + #[test] + fn test_find_range() { + use super::find_range; + + assert_eq!((false, 0), find_range(&vec![])); + assert_eq!((false, 1), find_range(&vec![1])); + assert_eq!((true, 2), find_range(&vec![1, 2])); + assert_eq!((true, 3), find_range(&vec![1, 2, 3])); + assert_eq!((true, 3), find_range(&vec![1, 2, 3, 5])); + } + + #[test] + fn test_expand_version_range() { + use super::expand_version_range; + + assert_eq!(Err("version string empty"), expand_version_range("")); + assert_eq!(Ok(vec![1, 2]), expand_version_range("1-2")); + assert_eq!(Ok(vec![1, 2, 3, 4]), expand_version_range("1-4")); + assert_eq!( + Err("cannot parse protocol range lower bound"), + expand_version_range("a") + ); + assert_eq!( + Err("cannot parse protocol range upper bound"), + expand_version_range("1-a") + ); + } + + #[test] + fn test_contract_protocol_list() { + use std::collections::HashSet; + use super::contract_protocol_list; + + { + let mut versions = HashSet::::new(); + assert_eq!(String::from(""), contract_protocol_list(&versions)); + + versions.insert(1); + assert_eq!(String::from("1"), contract_protocol_list(&versions)); + + versions.insert(2); + assert_eq!(String::from("1-2"), contract_protocol_list(&versions)); + } + + { + let mut versions = HashSet::::new(); + versions.insert(1); + versions.insert(3); + assert_eq!(String::from("1,3"), contract_protocol_list(&versions)); + } + + { + let mut versions = HashSet::::new(); + versions.insert(1); + versions.insert(2); + versions.insert(3); + versions.insert(4); + assert_eq!(String::from("1-4"), contract_protocol_list(&versions)); + } + + { + let mut versions = HashSet::::new(); + versions.insert(1); + versions.insert(3); + versions.insert(5); + versions.insert(6); + versions.insert(7); + assert_eq!( + String::from("1,3,5-7"), + contract_protocol_list(&versions) + ); + } + + { + let mut versions = HashSet::::new(); + versions.insert(1); + versions.insert(2); + versions.insert(3); + versions.insert(500); + assert_eq!( + String::from("1-3,500"), + contract_protocol_list(&versions) + ); + } + } +} diff --git a/src/rust/protover/tests/protover.rs b/src/rust/protover/tests/protover.rs new file mode 100644 index 0000000000..7d8484ecc2 --- /dev/null +++ b/src/rust/protover/tests/protover.rs @@ -0,0 +1,288 @@ +extern crate protover; + +#[test] +fn parse_protocol_list_with_single_protocol_and_single_version_returns_set_of_one(){ + let protocol = "Cons=1"; + let (is_supported, unsupported) = protover::all_supported(protocol); + assert_eq!(true, is_supported); + assert_eq!("", &unsupported); +} + +#[test] +fn parse_protocol_list_with_single_protocol_and_multiple_versions_returns_set_of_one(){ + let protocol = "Cons=1-2"; + let (is_supported, unsupported) = protover::all_supported(protocol); + assert_eq!(true, is_supported); + assert_eq!("", &unsupported); +} + +#[test] +fn parse_protocol_list_with_different_single_protocol_and_single_version_returns_set_of_one(){ + let protocol = "HSDir=1"; + let (is_supported, unsupported) = protover::all_supported(protocol); + assert_eq!(true, is_supported); + assert_eq!("", &unsupported); +} + +#[test] +fn parse_protocol_list_with_single_protocol_and_supported_version_returns_set_of_one(){ + let protocol = "Desc=2"; + let (is_supported, unsupported) = protover::all_supported(protocol); + assert_eq!(true, is_supported); + assert_eq!("", &unsupported); +} + +#[test] +fn parse_protocol_list_with_two_protocols_and_single_version_returns_set_of_one(){ + let protocols = "Cons=1 HSDir=1"; + let (is_supported, unsupported) = protover::all_supported(protocols); + assert_eq!(true, is_supported); + assert_eq!("", &unsupported); +} + + +#[test] +fn parse_protocol_list_with_single_protocol_and_two_nonsequential_versions_returns_set_of_two(){ + let protocol = "Desc=1,2"; + let (is_supported, unsupported) = protover::all_supported(protocol); + assert_eq!(true, is_supported); + assert_eq!("", &unsupported); +} + + +#[test] +fn parse_protocol_list_with_single_protocol_and_two_sequential_versions_returns_set_of_two(){ + let protocol = "Desc=1-2"; + let (is_supported, unsupported) = protover::all_supported(protocol); + assert_eq!(true, is_supported); + assert_eq!("", &unsupported); +} + +#[test] +fn parse_protocol_list_with_single_protocol_and_protocol_range_returns_set() { + let protocol = "Link=1-4"; + let (is_supported, unsupported) = protover::all_supported(protocol); + assert_eq!(true, is_supported); + assert_eq!("", &unsupported); +} + +#[test] +fn parse_protocol_list_with_single_protocol_and_protocol_set() { + let protocols = "Link=3-4 Desc=2"; + let (is_supported, unsupported) = protover::all_supported(protocols); + assert_eq!(true, is_supported); + assert_eq!("", &unsupported); +} + +#[test] +fn protover_all_supported_with_two_values() { + let protocols = "Microdesc=1-2 Relay=2"; + let (is_supported, unsupported) = protover::all_supported(protocols); + assert_eq!("", &unsupported); + assert_eq!(true, is_supported); +} + +#[test] +fn protover_all_supported_with_one_value() { + let protocols = "Microdesc=1-2"; + let (is_supported, unsupported) = protover::all_supported(protocols); + assert_eq!("", &unsupported); + assert_eq!(true, is_supported); +} + +#[test] +fn protover_all_supported_with_empty() { + let protocols = ""; + let (is_supported, unsupported) = protover::all_supported(protocols); + assert_eq!(true, is_supported); + assert_eq!("", &unsupported); +} + +#[test] +fn protover_all_supported_with_three_values() { + let protocols = "LinkAuth=1 Microdesc=1-2 Relay=2"; + let (is_supported, unsupported) = protover::all_supported(protocols); + assert_eq!("", &unsupported); + assert_eq!(true, is_supported); +} + +#[test] +fn protover_all_supported_with_unsupported_protocol() { + let protocols = "Wombat=9"; + let (is_supported, unsupported) = protover::all_supported(protocols); + assert_eq!(false, is_supported); + assert_eq!("Wombat=9", &unsupported); +} + +#[test] +fn protover_all_supported_with_unsupported_versions() { + let protocols = "Link=3-999"; + let (is_supported, unsupported) = protover::all_supported(protocols); + assert_eq!(false, is_supported); + assert_eq!("Link=3-999", &unsupported); +} + +#[test] +fn protover_all_supported_with_unsupported_low_version() { + let protocols = "Cons=0-1"; + let (is_supported, unsupported) = protover::all_supported(protocols); + assert_eq!(false, is_supported); + assert_eq!("Cons=0-1", &unsupported); +} + +#[test] +fn protover_all_supported_with_unsupported_high_version() { + let protocols = "Cons=1-3"; + let (is_supported, unsupported) = protover::all_supported(protocols); + assert_eq!(false, is_supported); + assert_eq!("Cons=1-3", &unsupported); +} + +#[test] +fn protover_all_supported_with_mix_of_supported_and_unsupproted() { + let protocols = "Link=3-4 Wombat=9"; + let (is_supported, unsupported) = protover::all_supported(protocols); + assert_eq!(false, is_supported); + assert_eq!("Wombat=9", &unsupported); +} + +#[test] +fn protover_string_supports_protocol_returns_true_for_single_supported() { + let protocols = "Link=3-4 Cons=1"; + let is_supported = protover::protover_string_supports_protocol( + protocols, + protover::Proto::Cons, + 1, + ); + assert_eq!(true, is_supported); +} + +#[test] +fn protover_string_supports_protocol_returns_false_for_single_unsupported() { + let protocols = "Link=3-4 Cons=1"; + let is_supported = protover::protover_string_supports_protocol( + protocols, + protover::Proto::Cons, + 2, + ); + assert_eq!(false, is_supported); +} + +#[test] +fn protover_string_supports_protocol_returns_false_when_protocol_name_is_not_in_map(){ + let protocols = "Link=3-4"; + let is_supported = protover::protover_string_supports_protocol( + protocols, + protover::Proto::Cons, + 2, + ); + assert_eq!(false, is_supported); +} + +#[test] +fn protover_all_supported_with_unexpected_characters() { + let protocols = "Cons=*-%"; + let (is_supported, unsupported) = protover::all_supported(protocols); + assert_eq!(false, is_supported); + assert_eq!("Cons=*-%", &unsupported); +} + +#[test] +fn protover_compute_vote_returns_empty_for_empty_string() { + let protocols = vec![String::from("")]; + let listed = protover::compute_vote(protocols, 1); + assert_eq!("", listed); +} + +#[test] +fn protover_compute_vote_returns_single_protocol_for_matching() { + let protocols = vec![String::from("Cons=1")]; + let listed = protover::compute_vote(protocols, 1); + assert_eq!("Cons=1", listed); +} + +#[test] +fn protover_compute_vote_returns_two_protocols_for_two_matching() { + let protocols = vec![String::from("Link=1 Cons=1")]; + let listed = protover::compute_vote(protocols, 1); + assert_eq!("Cons=1 Link=1", listed); +} + +#[test] +fn protover_compute_vote_returns_one_protocol_when_one_out_of_two_matches() { + let protocols = vec![String::from("Cons=1 Link=2"), String::from("Cons=1")]; + let listed = protover::compute_vote(protocols, 2); + assert_eq!("Cons=1", listed); +} + +#[test] +fn protover_compute_vote_returns_protocols_that_it_doesnt_currently_support() { + let protocols = vec![String::from("Foo=1 Cons=2"), String::from("Bar=1")]; + let listed = protover::compute_vote(protocols, 1); + assert_eq!("Bar=1 Cons=2 Foo=1", listed); +} + +#[test] +fn protover_compute_vote_returns_matching_for_mix() { + let protocols = vec![String::from("Link=1-10,500 Cons=1,3-7,8")]; + let listed = protover::compute_vote(protocols, 1); + assert_eq!("Cons=1,3-8 Link=1-10,500", listed); +} + +#[test] +fn protover_compute_vote_returns_matching_for_longer_mix() { + let protocols = vec![ + String::from("Desc=1-10,500 Cons=1,3-7,8"), + String::from("Link=123-456,78 Cons=2-6,8 Desc=9"), + ]; + + let listed = protover::compute_vote(protocols, 1); + assert_eq!("Cons=1-8 Desc=1-10,500 Link=78,123-456", listed); +} + +#[test] +fn protover_compute_vote_returns_matching_for_longer_mix_with_threshold_two() { + let protocols = vec![ + String::from("Desc=1-10,500 Cons=1,3-7,8"), + String::from("Link=123-456,78 Cons=2-6,8 Desc=9"), + ]; + + let listed = protover::compute_vote(protocols, 2); + assert_eq!("Cons=3-6,8 Desc=9", listed); +} + +#[test] +fn protover_compute_vote_handles_duplicated_versions() { + let protocols = vec![String::from("Cons=1"), String::from("Cons=1")]; + assert_eq!("Cons=1", protover::compute_vote(protocols, 2)); + + let protocols = vec![String::from("Cons=1-2"), String::from("Cons=1-2")]; + assert_eq!("Cons=1-2", protover::compute_vote(protocols, 2)); +} + +#[test] +fn protover_compute_vote_handles_invalid_proto_entries() { + let protocols = vec![ + String::from("Cons=1"), + String::from("Cons=1"), + String::from("Link=a"), + ]; + assert_eq!("Cons=1", protover::compute_vote(protocols, 2)); + + let protocols = vec![ + String::from("Cons=1"), + String::from("Cons=1"), + String::from("Link=1-%"), + ]; + assert_eq!("Cons=1", protover::compute_vote(protocols, 2)); +} + +#[test] +fn protover_is_supported_here_returns_true_for_supported_protocol() { + assert_eq!(true, protover::is_supported_here(protover::Proto::Cons, 1)); +} + +#[test] +fn protover_is_supported_here_returns_false_for_unsupported_protocol() { + assert_eq!(false, protover::is_supported_here(protover::Proto::Cons, 5)); +} -- cgit v1.2.3-54-g00ecf