diff options
Diffstat (limited to 'src/rust')
40 files changed, 4570 insertions, 0 deletions
diff --git a/src/rust/.cargo/config.in b/src/rust/.cargo/config.in new file mode 100644 index 0000000000..6eddc75459 --- /dev/null +++ b/src/rust/.cargo/config.in @@ -0,0 +1,12 @@ +[source] + +@RUST_DL@ [source.crates-io] +@RUST_DL@ registry = 'https://github.com/rust-lang/crates.io-index' +@RUST_DL@ replace-with = 'vendored-sources' + +@RUST_DL@ [source.vendored-sources] +@RUST_DL@ directory = '@TOR_RUST_DEPENDENCIES@' + +[build] +@RUST_WARN@ rustflags = [ "-D", "warnings" ] +@RUST_TARGET_PROP@ diff --git a/src/rust/.rustfmt.toml b/src/rust/.rustfmt.toml new file mode 100644 index 0000000000..f25bd51883 --- /dev/null +++ b/src/rust/.rustfmt.toml @@ -0,0 +1,2 @@ +max_width = 80 +comment_width = 80 diff --git a/src/rust/Cargo.lock b/src/rust/Cargo.lock new file mode 100644 index 0000000000..3a067a39dc --- /dev/null +++ b/src/rust/Cargo.lock @@ -0,0 +1,121 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "crypto" +version = "0.0.1" +dependencies = [ + "digest 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", + "external 0.0.1", + "libc 0.2.39 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.5.0-pre.2 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.2.0-pre.0 (registry+https://github.com/rust-lang/crates.io-index)", + "smartlist 0.0.1", + "tor_allocate 0.0.1", + "tor_log 0.1.0", +] + +[[package]] +name = "digest" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "generic-array 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "external" +version = "0.0.1" +dependencies = [ + "libc 0.2.39 (registry+https://github.com/rust-lang/crates.io-index)", + "smartlist 0.0.1", +] + +[[package]] +name = "generic-array" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "typenum 1.9.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "libc" +version = "0.2.39" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "protover" +version = "0.0.1" +dependencies = [ + "external 0.0.1", + "libc 0.2.39 (registry+https://github.com/rust-lang/crates.io-index)", + "smartlist 0.0.1", + "tor_allocate 0.0.1", + "tor_log 0.1.0", + "tor_util 0.0.1", +] + +[[package]] +name = "rand" +version = "0.5.0-pre.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand_core 0.2.0-pre.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand_core" +version = "0.2.0-pre.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "smartlist" +version = "0.0.1" +dependencies = [ + "libc 0.2.39 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tor_allocate" +version = "0.0.1" +dependencies = [ + "libc 0.2.39 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tor_log" +version = "0.1.0" +dependencies = [ + "libc 0.2.39 (registry+https://github.com/rust-lang/crates.io-index)", + "tor_allocate 0.0.1", +] + +[[package]] +name = "tor_rust" +version = "0.1.0" +dependencies = [ + "protover 0.0.1", + "tor_util 0.0.1", +] + +[[package]] +name = "tor_util" +version = "0.0.1" +dependencies = [ + "libc 0.2.39 (registry+https://github.com/rust-lang/crates.io-index)", + "tor_allocate 0.0.1", + "tor_log 0.1.0", +] + +[[package]] +name = "typenum" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[metadata] +"checksum digest 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "00a49051fef47a72c9623101b19bd71924a45cca838826caae3eaa4d00772603" +"checksum generic-array 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ef25c5683767570c2bbd7deba372926a55eaae9982d7726ee2a1050239d45b9d" +"checksum libc 0.2.39 (registry+https://github.com/rust-lang/crates.io-index)" = "f54263ad99207254cf58b5f701ecb432c717445ea2ee8af387334bdd1a03fdff" +"checksum rand 0.5.0-pre.2 (registry+https://github.com/rust-lang/crates.io-index)" = "3795e4701d9628a63a84d0289e66279883b40df165fca7caed7b87122447032a" +"checksum rand_core 0.2.0-pre.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c7255ffbdb188d5be1a69b6f9f3cf187de4207430b9e79ed5b76458a6b20de9a" +"checksum typenum 1.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "13a99dc6780ef33c78780b826cf9d2a78840b72cae9474de4bcaf9051e60ebbd" diff --git a/src/rust/Cargo.toml b/src/rust/Cargo.toml new file mode 100644 index 0000000000..de8693ea33 --- /dev/null +++ b/src/rust/Cargo.toml @@ -0,0 +1,26 @@ +[workspace] +members = [ + "crypto", + "external", + "protover", + "smartlist", + "tor_allocate", + "tor_log", + "tor_rust", + "tor_util", +] + +# Can remove panic="abort" when this issue is fixed: +# https://github.com/rust-lang/rust/issues/52652 +[profile.dev] +panic = "abort" + +[profile.release] +debug = true +panic = "abort" + +[profile.test] +panic = "abort" + +[profile.bench] +panic = "abort" diff --git a/src/rust/build.rs b/src/rust/build.rs new file mode 100644 index 0000000000..b943aa5535 --- /dev/null +++ b/src/rust/build.rs @@ -0,0 +1,179 @@ +//! Build script for Rust modules in Tor. +//! +//! We need to use this because some of our Rust tests need to use some +//! of our C modules, which need to link some external libraries. +//! +//! This script works by looking at a "config.rust" file generated by our +//! configure script, and then building a set of options for cargo to pass to +//! the compiler. + +use std::collections::HashMap; +use std::env; +use std::fs::File; +use std::io::prelude::*; +use std::io; +use std::path::PathBuf; + +/// Wrapper around a key-value map. +struct Config( + HashMap<String,String> +); + +/// Locate a config.rust file generated by autoconf, starting in the OUT_DIR +/// location provided by cargo and recursing up the directory tree. Note that +/// we need to look in the OUT_DIR, since autoconf will place generated files +/// in the build directory. +fn find_cfg() -> io::Result<String> { + let mut path = PathBuf::from(env::var("OUT_DIR").unwrap()); + loop { + path.push("config.rust"); + if path.exists() { + return Ok(path.to_str().unwrap().to_owned()); + } + path.pop(); // remove config.rust + if ! path.pop() { // can't remove last part of directory + return Err(io::Error::new(io::ErrorKind::NotFound, + "No config.rust")); + } + } +} + +impl Config { + /// Find the config.rust file and try to parse it. + /// + /// The file format is a series of lines of the form KEY=VAL, with + /// any blank lines and lines starting with # ignored. + fn load() -> io::Result<Config> { + let path = find_cfg()?; + let f = File::open(&path)?; + let reader = io::BufReader::new(f); + let mut map = HashMap::new(); + for line in reader.lines() { + let s = line?; + if s.trim().starts_with("#") || s.trim() == "" { + continue; + } + let idx = match s.find("=") { + None => { + return Err(io::Error::new(io::ErrorKind::InvalidData, + "missing =")); + }, + Some(x) => x + }; + let (var,eq_val) = s.split_at(idx); + let val = &eq_val[1..]; + map.insert(var.to_owned(), val.to_owned()); + } + Ok(Config(map)) + } + + /// Return a reference to the value whose key is 'key'. + /// + /// Panics if 'key' is not found in the configuration. + fn get(&self, key : &str) -> &str { + self.0.get(key).unwrap() + } + + /// Add a dependency on a static C library that is part of Tor, by name. + fn component(&self, s : &str) { + println!("cargo:rustc-link-lib=static={}", s); + } + + /// Add a dependency on a native library that is not part of Tor, by name. + fn dependency(&self, s : &str) { + println!("cargo:rustc-link-lib={}", s); + } + + /// Add a link path, relative to Tor's build directory. + fn link_relpath(&self, s : &str) { + let builddir = self.get("BUILDDIR"); + println!("cargo:rustc-link-search=native={}/{}", builddir, s); + } + + /// Add an absolute link path. + fn link_path(&self, s : &str) { + println!("cargo:rustc-link-search=native={}", s); + } + + /// Parse the CFLAGS in s, looking for -l and -L items, and adding + /// rust configuration as appropriate. + fn from_cflags(&self, s : &str) { + let mut next_is_lib = false; + let mut next_is_path = false; + for ent in self.get(s).split_whitespace() { + if next_is_lib { + self.dependency(ent); + next_is_lib = false; + } else if next_is_path { + self.link_path(ent); + next_is_path = false; + } else if ent == "-l" { + next_is_lib = true; + } else if ent == "-L" { + next_is_path = true; + } else if ent.starts_with("-L") { + self.link_path(&ent[2..]); + } else if ent.starts_with("-l") { + self.dependency(&ent[2..]); + } + } + } +} + +pub fn main() { + let cfg = Config::load().unwrap(); + let package = env::var("CARGO_PKG_NAME").unwrap(); + + match package.as_ref() { + "crypto" => { + // Right now, I'm having a separate configuration for each Rust + // package, since I'm hoping we can trim them down. Once we have a + // second Rust package that needs to use this build script, let's + // extract some of this stuff into a module. + // + // This is a ridiculous amount of code to be pulling in just + // to test our crypto library: modularity would be our + // friend here. + cfg.from_cflags("TOR_LDFLAGS_zlib"); + cfg.from_cflags("TOR_LDFLAGS_openssl"); + cfg.from_cflags("TOR_LDFLAGS_libevent"); + + cfg.link_relpath("src/common"); + cfg.link_relpath("src/ext/keccak-tiny"); + cfg.link_relpath("src/ext/keccak-tiny"); + cfg.link_relpath("src/ext/ed25519/ref10"); + cfg.link_relpath("src/ext/ed25519/donna"); + cfg.link_relpath("src/trunnel"); + + // Note that we can't pull in "libtor-testing", or else we + // will have dependencies on all the other rust packages that + // tor uses. We must be careful with factoring and dependencies + // moving forward! + cfg.component("or-crypto-testing"); + cfg.component("or-ctime-testing"); + cfg.component("or-testing"); + cfg.component("or-event-testing"); + cfg.component("or-ctime-testing"); + cfg.component("curve25519_donna"); + cfg.component("keccak-tiny"); + cfg.component("ed25519_ref10"); + cfg.component("ed25519_donna"); + cfg.component("or-trunnel-testing"); + + cfg.from_cflags("TOR_ZLIB_LIBS"); + cfg.from_cflags("TOR_LIB_MATH"); + cfg.from_cflags("TOR_OPENSSL_LIBS"); + cfg.from_cflags("TOR_LIBEVENT_LIBS"); + cfg.from_cflags("TOR_LIB_WS32"); + cfg.from_cflags("TOR_LIB_GDI"); + cfg.from_cflags("TOR_LIB_USERENV"); + cfg.from_cflags("CURVE25519_LIBS"); + cfg.from_cflags("TOR_LZMA_LIBS"); + cfg.from_cflags("TOR_ZSTD_LIBS"); + cfg.from_cflags("LIBS"); + }, + _ => { + panic!("No configuration in build.rs for package {}", package); + } + } +} diff --git a/src/rust/crypto/Cargo.toml b/src/rust/crypto/Cargo.toml new file mode 100644 index 0000000000..869e0d6256 --- /dev/null +++ b/src/rust/crypto/Cargo.toml @@ -0,0 +1,28 @@ +[package] +authors = ["The Tor Project", + "Isis Lovecruft <isis@torproject.org>"] +name = "crypto" +version = "0.0.1" +publish = false +build = "../build.rs" + +[lib] +name = "crypto" +path = "lib.rs" +crate_type = ["rlib", "staticlib"] + +[dependencies] +libc = "=0.2.39" +digest = "=0.7.2" +rand_core = { version = "=0.2.0-pre.0", default-features = false } + +external = { path = "../external" } +smartlist = { path = "../smartlist" } +tor_allocate = { path = "../tor_allocate" } +tor_log = { path = "../tor_log" } + +[dev-dependencies] +rand = { version = "=0.5.0-pre.2", default-features = false } +rand_core = { version = "=0.2.0-pre.0", default-features = false } + +[features] diff --git a/src/rust/crypto/digests/mod.rs b/src/rust/crypto/digests/mod.rs new file mode 100644 index 0000000000..a2463b89eb --- /dev/null +++ b/src/rust/crypto/digests/mod.rs @@ -0,0 +1,7 @@ +// Copyright (c) 2018, The Tor Project, Inc. +// Copyright (c) 2018, isis agora lovecruft +// See LICENSE for licensing information + +//! Hash Digests and eXtendible Output Functions (XOFs) + +pub mod sha2; diff --git a/src/rust/crypto/digests/sha2.rs b/src/rust/crypto/digests/sha2.rs new file mode 100644 index 0000000000..03e0843dc0 --- /dev/null +++ b/src/rust/crypto/digests/sha2.rs @@ -0,0 +1,222 @@ +// Copyright (c) 2018, The Tor Project, Inc. +// Copyright (c) 2018, isis agora lovecruft +// See LICENSE for licensing information + +//! Hash Digests and eXtendible Output Functions (XOFs) + +pub use digest::Digest; + +use digest::BlockInput; +use digest::FixedOutput; +use digest::Input; +use digest::generic_array::GenericArray; +use digest::generic_array::typenum::U32; +use digest::generic_array::typenum::U64; + +use external::crypto_digest::CryptoDigest; +use external::crypto_digest::DigestAlgorithm; +use external::crypto_digest::get_256_bit_digest; +use external::crypto_digest::get_512_bit_digest; + +pub use external::crypto_digest::DIGEST256_LEN; +pub use external::crypto_digest::DIGEST512_LEN; + +/// The block size for both SHA-256 and SHA-512 digests is 512 bits/64 bytes. +/// +/// Unfortunately, we have to use the generic_array crate currently to express +/// this at compile time. Later, in the future, when Rust implements const +/// generics, we'll be able to remove this dependency (actually, it will get +/// removed from the digest crate, which is currently `pub use`ing it). +type BlockSize = U64; + +/// A SHA2-256 digest. +/// +/// # C_RUST_COUPLED +/// +/// * `crypto_digest_dup` +#[derive(Clone)] +pub struct Sha256 { + engine: CryptoDigest, +} + +/// Construct a new, default instance of a `Sha256` hash digest function. +/// +/// # Examples +/// +/// ```rust,no_run +/// use crypto::digests::sha2::{Sha256, Digest}; +/// +/// let mut hasher: Sha256 = Sha256::default(); +/// ``` +/// +/// # Returns +/// +/// A new `Sha256` digest. +impl Default for Sha256 { + fn default() -> Sha256 { + Sha256{ engine: CryptoDigest::new(Some(DigestAlgorithm::SHA2_256)) } + } +} + +impl BlockInput for Sha256 { + type BlockSize = BlockSize; +} + +/// Input `msg` into the digest. +/// +/// # Examples +/// +/// ```rust,no_run +/// use crypto::digests::sha2::{Sha256, Digest}; +/// +/// let mut hasher: Sha256 = Sha256::default(); +/// +/// hasher.input(b"foo"); +/// hasher.input(b"bar"); +/// ``` +impl Input for Sha256 { + fn process(&mut self, msg: &[u8]) { + self.engine.add_bytes(&msg); + } +} + +/// Retrieve the output hash from everything which has been fed into this +/// `Sha256` digest thus far. +/// +// +// FIXME: Once const generics land in Rust, we should genericise calling +// crypto_digest_get_digest in external::crypto_digest. +impl FixedOutput for Sha256 { + type OutputSize = U32; + + fn fixed_result(self) -> GenericArray<u8, Self::OutputSize> { + let buffer: [u8; DIGEST256_LEN] = get_256_bit_digest(self.engine); + + GenericArray::from(buffer) + } +} + +/// A SHA2-512 digest. +/// +/// # C_RUST_COUPLED +/// +/// * `crypto_digest_dup` +#[derive(Clone)] +pub struct Sha512 { + engine: CryptoDigest, +} + +/// Construct a new, default instance of a `Sha512` hash digest function. +/// +/// # Examples +/// +/// ```rust,no_run +/// use crypto::digests::sha2::{Sha512, Digest}; +/// +/// let mut hasher: Sha512 = Sha512::default(); +/// ``` +/// +/// # Returns +/// +/// A new `Sha512` digest. +impl Default for Sha512 { + fn default() -> Sha512 { + Sha512{ engine: CryptoDigest::new(Some(DigestAlgorithm::SHA2_512)) } + } +} + +impl BlockInput for Sha512 { + type BlockSize = BlockSize; +} + +/// Input `msg` into the digest. +/// +/// # Examples +/// +/// ```rust,no_run +/// use crypto::digests::sha2::{Sha512, Digest}; +/// +/// let mut hasher: Sha512 = Sha512::default(); +/// +/// hasher.input(b"foo"); +/// hasher.input(b"bar"); +/// ``` +impl Input for Sha512 { + fn process(&mut self, msg: &[u8]) { + self.engine.add_bytes(&msg); + } +} + +/// Retrieve the output hash from everything which has been fed into this +/// `Sha512` digest thus far. +/// +// +// FIXME: Once const generics land in Rust, we should genericise calling +// crypto_digest_get_digest in external::crypto_digest. +impl FixedOutput for Sha512 { + type OutputSize = U64; + + fn fixed_result(self) -> GenericArray<u8, Self::OutputSize> { + let buffer: [u8; DIGEST512_LEN] = get_512_bit_digest(self.engine); + + GenericArray::clone_from_slice(&buffer) + } +} + +#[cfg(test)] +mod test { + use digest::Digest; + + use super::*; + + #[test] + fn sha256_default() { + let _: Sha256 = Sha256::default(); + } + + #[test] + fn sha256_digest() { + let mut h: Sha256 = Sha256::new(); + let mut result: [u8; DIGEST256_LEN] = [0u8; DIGEST256_LEN]; + let expected = [151, 223, 53, 136, 181, 163, 242, 75, 171, 195, + 133, 27, 55, 47, 11, 167, 26, 157, 205, 222, 212, + 59, 20, 185, 208, 105, 97, 191, 193, 112, 125, 157]; + + h.input(b"foo"); + h.input(b"bar"); + h.input(b"baz"); + + result.copy_from_slice(h.fixed_result().as_slice()); + + println!("{:?}", &result[..]); + + assert_eq!(result, expected); + } + + #[test] + fn sha512_default() { + let _: Sha512 = Sha512::default(); + } + + #[test] + fn sha512_digest() { + let mut h: Sha512 = Sha512::new(); + let mut result: [u8; DIGEST512_LEN] = [0u8; DIGEST512_LEN]; + + let expected = [203, 55, 124, 16, 176, 245, 166, 44, 128, 54, 37, 167, + 153, 217, 233, 8, 190, 69, 231, 103, 245, 209, 71, 212, 116, + 73, 7, 203, 5, 89, 122, 164, 237, 211, 41, 160, 175, 20, 122, + 221, 12, 244, 24, 30, 211, 40, 250, 30, 121, 148, 38, 88, 38, + 179, 237, 61, 126, 246, 240, 103, 202, 153, 24, 90]; + + h.input(b"foo"); + h.input(b"bar"); + h.input(b"baz"); + + result.copy_from_slice(h.fixed_result().as_slice()); + + println!("{:?}", &result[..]); + + assert_eq!(&result[..], &expected[..]); + } +} diff --git a/src/rust/crypto/lib.rs b/src/rust/crypto/lib.rs new file mode 100644 index 0000000000..f72a859dd7 --- /dev/null +++ b/src/rust/crypto/lib.rs @@ -0,0 +1,45 @@ +// Copyright (c) 2018, The Tor Project, Inc. +// Copyright (c) 2018, isis agora lovecruft +// See LICENSE for licensing information + +//! Common cryptographic functions and utilities. +//! +//! # Hash Digests and eXtendable Output Functions (XOFs) +//! +//! The `digests` module contains submodules for specific hash digests +//! and extendable output functions. +//! +//! ```rust,no_run +//! use crypto::digests::sha2::*; +//! +//! let mut hasher: Sha256 = Sha256::default(); +//! let mut result: [u8; 32] = [0u8; 32]; +//! +//! hasher.input(b"foo"); +//! hasher.input(b"bar"); +//! hasher.input(b"baz"); +//! +//! result.copy_from_slice(hasher.result().as_slice()); +//! +//! assert!(result == [b'X'; DIGEST256_LEN]); +//! ``` + +#[deny(missing_docs)] + +// External crates from cargo or TOR_RUST_DEPENDENCIES. +extern crate digest; +extern crate libc; +extern crate rand_core; + +// External dependencies for tests. +#[cfg(test)] +extern crate rand as rand_crate; + +// Our local crates. +extern crate external; +#[cfg(not(test))] +#[macro_use] +extern crate tor_log; + +pub mod digests; // Unfortunately named "digests" plural to avoid name conflict with the digest crate +pub mod rand; diff --git a/src/rust/crypto/rand/mod.rs b/src/rust/crypto/rand/mod.rs new file mode 100644 index 0000000000..82d02a70bb --- /dev/null +++ b/src/rust/crypto/rand/mod.rs @@ -0,0 +1,6 @@ +// Copyright (c) 2018, The Tor Project, Inc. +// Copyright (c) 2018, isis agora lovecruft +// See LICENSE for licensing information + +// Internal dependencies +pub mod rng; diff --git a/src/rust/crypto/rand/rng.rs b/src/rust/crypto/rand/rng.rs new file mode 100644 index 0000000000..07a0a7bdc7 --- /dev/null +++ b/src/rust/crypto/rand/rng.rs @@ -0,0 +1,140 @@ +// Copyright (c) 2018, The Tor Project, Inc. +// Copyright (c) 2018, isis agora lovecruft +// See LICENSE for licensing information + +//! Wrappers for Tor's random number generators to provide implementations of +//! `rand_core` traits. + +// This is the real implementation, in use in production, which calls into our C +// wrappers in /src/common/crypto_rand.c, which call into OpenSSL, system +// libraries, and make syscalls. +#[cfg(not(test))] +mod internal { + use std::u64; + + use rand_core::CryptoRng; + use rand_core::Error; + use rand_core::RngCore; + use rand_core::impls::next_u32_via_fill; + use rand_core::impls::next_u64_via_fill; + + use external::c_tor_crypto_rand; + use external::c_tor_crypto_strongest_rand; + use external::c_tor_crypto_seed_rng; + + use tor_log::LogDomain; + use tor_log::LogSeverity; + + /// Largest strong entropy request permitted. + // + // C_RUST_COUPLED: `MAX_STRONGEST_RAND_SIZE` /src/common/crypto_rand.c + const MAX_STRONGEST_RAND_SIZE: usize = 256; + + /// A wrapper around OpenSSL's RNG. + pub struct TorRng { + // This private, zero-length field forces the struct to be treated the + // same as its opaque C couterpart. + _unused: [u8; 0], + } + + /// Mark `TorRng` as being suitable for cryptographic purposes. + impl CryptoRng for TorRng {} + + impl TorRng { + // C_RUST_COUPLED: `crypto_seed_rng()` /src/common/crypto_rand.c + #[allow(dead_code)] + pub fn new() -> Self { + if !c_tor_crypto_seed_rng() { + tor_log_msg!(LogSeverity::Warn, LogDomain::General, + "TorRng::from_seed()", + "The RNG could not be seeded!"); + } + // XXX also log success at info level —isis + TorRng{ _unused: [0u8; 0] } + } + } + + impl RngCore for TorRng { + // C_RUST_COUPLED: `crypto_strongest_rand()` /src/common/crypto_rand.c + fn next_u32(&mut self) -> u32 { + next_u32_via_fill(self) + } + + // C_RUST_COUPLED: `crypto_strongest_rand()` /src/common/crypto_rand.c + fn next_u64(&mut self) -> u64 { + next_u64_via_fill(self) + } + + // C_RUST_COUPLED: `crypto_strongest_rand()` /src/common/crypto_rand.c + fn fill_bytes(&mut self, dest: &mut [u8]) { + c_tor_crypto_rand(dest); + } + + // C_RUST_COUPLED: `crypto_strongest_rand()` /src/common/crypto_rand.c + fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Error> { + Ok(self.fill_bytes(dest)) + } + } + + /// A CSPRNG which hashes together randomness from OpenSSL's RNG and entropy + /// obtained from the operating system. + pub struct TorStrongestRng { + // This private, zero-length field forces the struct to be treated the + // same as its opaque C couterpart. + _unused: [u8; 0], + } + + /// Mark `TorRng` as being suitable for cryptographic purposes. + impl CryptoRng for TorStrongestRng {} + + impl TorStrongestRng { + // C_RUST_COUPLED: `crypto_seed_rng()` /src/common/crypto_rand.c + #[allow(dead_code)] + pub fn new() -> Self { + if !c_tor_crypto_seed_rng() { + tor_log_msg!(LogSeverity::Warn, LogDomain::General, + "TorStrongestRng::from_seed()", + "The RNG could not be seeded!"); + } + // XXX also log success at info level —isis + TorStrongestRng{ _unused: [0u8; 0] } + } + } + + impl RngCore for TorStrongestRng { + // C_RUST_COUPLED: `crypto_strongest_rand()` /src/common/crypto_rand.c + fn next_u32(&mut self) -> u32 { + next_u32_via_fill(self) + } + + // C_RUST_COUPLED: `crypto_strongest_rand()` /src/common/crypto_rand.c + fn next_u64(&mut self) -> u64 { + next_u64_via_fill(self) + } + + // C_RUST_COUPLED: `crypto_strongest_rand()` /src/common/crypto_rand.c + fn fill_bytes(&mut self, dest: &mut [u8]) { + debug_assert!(dest.len() <= MAX_STRONGEST_RAND_SIZE); + + c_tor_crypto_strongest_rand(dest); + } + + // C_RUST_COUPLED: `crypto_strongest_rand()` /src/common/crypto_rand.c + fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Error> { + Ok(self.fill_bytes(dest)) + } + } +} + +// For testing, we expose a pure-Rust implementation. +#[cfg(test)] +mod internal { + // It doesn't matter if we pretend ChaCha is a CSPRNG in tests. + pub use rand_crate::ChaChaRng as TorRng; + pub use rand_crate::ChaChaRng as TorStrongestRng; +} + +// Finally, expose the public functionality of whichever appropriate internal +// module. +pub use self::internal::*; + diff --git a/src/rust/external/Cargo.toml b/src/rust/external/Cargo.toml new file mode 100644 index 0000000000..60ec03be40 --- /dev/null +++ b/src/rust/external/Cargo.toml @@ -0,0 +1,16 @@ +[package] +authors = ["The Tor Project"] +version = "0.0.1" +name = "external" + +[dependencies] +libc = "=0.2.39" + +[dependencies.smartlist] +path = "../smartlist" + +[lib] +name = "external" +path = "lib.rs" +crate_type = ["rlib", "staticlib"] + diff --git a/src/rust/external/crypto_digest.rs b/src/rust/external/crypto_digest.rs new file mode 100644 index 0000000000..3e8801f203 --- /dev/null +++ b/src/rust/external/crypto_digest.rs @@ -0,0 +1,406 @@ +// Copyright (c) 2018, The Tor Project, Inc. +// Copyright (c) 2018, isis agora lovecruft +// See LICENSE for licensing information + +//! Bindings to external digest and XOF functions which live within +//! src/common/crypto_digest.[ch]. +//! +//! We wrap our C implementations in src/common/crypto_digest.[ch] with more +//! Rusty types and interfaces in src/rust/crypto/digest/. + +use std::process::abort; + +use libc::c_char; +use libc::c_int; +use libc::size_t; +use libc::uint8_t; + +use smartlist::Stringlist; + +/// Length of the output of our message digest. +pub const DIGEST_LEN: usize = 20; + +/// Length of the output of our second (improved) message digests. (For now +/// this is just sha256, but it could be any other 256-bit digest.) +pub const DIGEST256_LEN: usize = 32; + +/// Length of the output of our 64-bit optimized message digests (SHA512). +pub const DIGEST512_LEN: usize = 64; + +/// Length of a sha1 message digest when encoded in base32 with trailing = signs +/// removed. +pub const BASE32_DIGEST_LEN: usize = 32; + +/// Length of a sha1 message digest when encoded in base64 with trailing = signs +/// removed. +pub const BASE64_DIGEST_LEN: usize = 27; + +/// Length of a sha256 message digest when encoded in base64 with trailing = +/// signs removed. +pub const BASE64_DIGEST256_LEN: usize = 43; + +/// Length of a sha512 message digest when encoded in base64 with trailing = +/// signs removed. +pub const BASE64_DIGEST512_LEN: usize = 86; + +/// Length of hex encoding of SHA1 digest, not including final NUL. +pub const HEX_DIGEST_LEN: usize = 40; + +/// Length of hex encoding of SHA256 digest, not including final NUL. +pub const HEX_DIGEST256_LEN: usize = 64; + +/// Length of hex encoding of SHA512 digest, not including final NUL. +pub const HEX_DIGEST512_LEN: usize = 128; + +/// Our C code uses an enum to declare the digest algorithm types which we know +/// about. However, because enums are implementation-defined in C, we can +/// neither work with them directly nor translate them into Rust enums. +/// Instead, we represent them as a u8 (under the assumption that we'll never +/// support more than 256 hash functions). +#[allow(non_camel_case_types)] +type digest_algorithm_t = u8; + +const DIGEST_SHA1: digest_algorithm_t = 0; +const DIGEST_SHA256: digest_algorithm_t = 1; +const DIGEST_SHA512: digest_algorithm_t = 2; +const DIGEST_SHA3_256: digest_algorithm_t = 3; +const DIGEST_SHA3_512: digest_algorithm_t = 4; + +/// The number of hash digests we produce for a `common_digests_t`. +/// +/// We can't access these from Rust, because their definitions in C require +/// introspecting the `digest_algorithm_t` typedef, which is an enum, so we have +/// to redefine them here. +const N_COMMON_DIGEST_ALGORITHMS: usize = DIGEST_SHA256 as usize + 1; + +/// A digest function. +#[repr(C)] +#[derive(Debug, Copy, Clone)] +#[allow(non_camel_case_types)] +struct crypto_digest_t { + // This private, zero-length field forces the struct to be treated the same + // as its opaque C couterpart. + _unused: [u8; 0], +} + +/// An eXtendible Output Function (XOF). +#[repr(C)] +#[derive(Debug, Copy, Clone)] +#[allow(non_camel_case_types)] +struct crypto_xof_t { + // This private, zero-length field forces the struct to be treated the same + // as its opaque C couterpart. + _unused: [u8; 0], +} + +/// A set of all the digests we commonly compute, taken on a single +/// string. Any digests that are shorter than 512 bits are right-padded +/// with 0 bits. +/// +/// Note that this representation wastes 44 bytes for the SHA1 case, so +/// don't use it for anything where we need to allocate a whole bunch at +/// once. +#[repr(C)] +#[derive(Debug, Copy, Clone)] +#[allow(non_camel_case_types)] +struct common_digests_t { + pub d: [[c_char; N_COMMON_DIGEST_ALGORITHMS]; DIGEST256_LEN], +} + +/// A `smartlist_t` is just an alias for the `#[repr(C)]` type `Stringlist`, to +/// make it more clear that we're working with a smartlist which is owned by C. +#[allow(non_camel_case_types)] +// BINDGEN_GENERATED: This type isn't actually bindgen generated, but the code +// below it which uses it is. As such, this comes up as "dead code" as well. +#[allow(dead_code)] +type smartlist_t = Stringlist; + +/// All of the external functions from `src/common/crypto_digest.h`. +/// +/// These are kept private because they should be wrapped with Rust to make their usage safer. +// +// BINDGEN_GENERATED: These definitions were generated with bindgen and cleaned +// up manually. As such, there are more bindings than are likely necessary or +// which are in use. +#[allow(dead_code)] +extern "C" { + fn crypto_digest(digest: *mut c_char, m: *const c_char, len: size_t) -> c_int; + fn crypto_digest256(digest: *mut c_char, m: *const c_char, len: size_t, + algorithm: digest_algorithm_t) -> c_int; + fn crypto_digest512(digest: *mut c_char, m: *const c_char, len: size_t, + algorithm: digest_algorithm_t) -> c_int; + fn crypto_common_digests(ds_out: *mut common_digests_t, m: *const c_char, len: size_t) -> c_int; + fn crypto_digest_smartlist_prefix(digest_out: *mut c_char, len_out: size_t, prepend: *const c_char, + lst: *const smartlist_t, append: *const c_char, alg: digest_algorithm_t); + fn crypto_digest_smartlist(digest_out: *mut c_char, len_out: size_t, + lst: *const smartlist_t, append: *const c_char, alg: digest_algorithm_t); + fn crypto_digest_algorithm_get_name(alg: digest_algorithm_t) -> *const c_char; + fn crypto_digest_algorithm_get_length(alg: digest_algorithm_t) -> size_t; + fn crypto_digest_algorithm_parse_name(name: *const c_char) -> c_int; + fn crypto_digest_new() -> *mut crypto_digest_t; + fn crypto_digest256_new(algorithm: digest_algorithm_t) -> *mut crypto_digest_t; + fn crypto_digest512_new(algorithm: digest_algorithm_t) -> *mut crypto_digest_t; + fn crypto_digest_free_(digest: *mut crypto_digest_t); + fn crypto_digest_add_bytes(digest: *mut crypto_digest_t, data: *const c_char, len: size_t); + fn crypto_digest_get_digest(digest: *mut crypto_digest_t, out: *mut c_char, out_len: size_t); + fn crypto_digest_dup(digest: *const crypto_digest_t) -> *mut crypto_digest_t; + fn crypto_digest_assign(into: *mut crypto_digest_t, from: *const crypto_digest_t); + fn crypto_hmac_sha256(hmac_out: *mut c_char, key: *const c_char, key_len: size_t, + msg: *const c_char, msg_len: size_t); + fn crypto_mac_sha3_256(mac_out: *mut uint8_t, len_out: size_t, + key: *const uint8_t, key_len: size_t, + msg: *const uint8_t, msg_len: size_t); + fn crypto_xof_new() -> *mut crypto_xof_t; + fn crypto_xof_add_bytes(xof: *mut crypto_xof_t, data: *const uint8_t, len: size_t); + fn crypto_xof_squeeze_bytes(xof: *mut crypto_xof_t, out: *mut uint8_t, len: size_t); + fn crypto_xof_free(xof: *mut crypto_xof_t); +} + +/// A wrapper around a `digest_algorithm_t`. +pub enum DigestAlgorithm { + SHA2_256, + SHA2_512, + SHA3_256, + SHA3_512, +} + +impl From<DigestAlgorithm> for digest_algorithm_t { + fn from(digest: DigestAlgorithm) -> digest_algorithm_t { + match digest { + DigestAlgorithm::SHA2_256 => DIGEST_SHA256, + DigestAlgorithm::SHA2_512 => DIGEST_SHA512, + DigestAlgorithm::SHA3_256 => DIGEST_SHA3_256, + DigestAlgorithm::SHA3_512 => DIGEST_SHA3_512, + } + } +} + +/// A wrapper around a mutable pointer to a `crypto_digest_t`. +pub struct CryptoDigest(*mut crypto_digest_t); + +/// Explicitly copy the state of a `CryptoDigest` hash digest context. +/// +/// # C_RUST_COUPLED +/// +/// * `crypto_digest_dup` +impl Clone for CryptoDigest { + fn clone(&self) -> CryptoDigest { + let digest: *mut crypto_digest_t; + + unsafe { + digest = crypto_digest_dup(self.0 as *const crypto_digest_t); + } + + // See the note in the implementation of CryptoDigest for the + // reasoning for `abort()` here. + if digest.is_null() { + abort(); + } + + CryptoDigest(digest) + } +} + +impl CryptoDigest { + /// A wrapper to call one of the C functions `crypto_digest_new`, + /// `crypto_digest256_new`, or `crypto_digest512_new`. + /// + /// # Warnings + /// + /// This function will `abort()` the entire process in an "abnormal" fashion, + /// i.e. not unwinding this or any other thread's stack, running any + /// destructors, or calling any panic/exit hooks) if `tor_malloc()` (called in + /// `crypto_digest256_new()`) is unable to allocate memory. + /// + /// # Returns + /// + /// A new `CryptoDigest`, which is a wrapper around a opaque representation + /// of a `crypto_digest_t`. The underlying `crypto_digest_t` _MUST_ only + /// ever be handled via a raw pointer, and never introspected. + /// + /// # C_RUST_COUPLED + /// + /// * `crypto_digest_new` + /// * `crypto_digest256_new` + /// * `crypto_digest512_new` + /// * `tor_malloc` (called by `crypto_digest256_new`, but we make + /// assumptions about its behvaiour and return values here) + pub fn new(algorithm: Option<DigestAlgorithm>) -> CryptoDigest { + let digest: *mut crypto_digest_t; + + if algorithm.is_none() { + unsafe { + digest = crypto_digest_new(); + } + } else { + let algo: digest_algorithm_t = algorithm.unwrap().into(); // can't fail because it's Some + + unsafe { + // XXX This is a pretty awkward API to use from Rust... + digest = match algo { + DIGEST_SHA1 => crypto_digest_new(), + DIGEST_SHA256 => crypto_digest256_new(DIGEST_SHA256), + DIGEST_SHA3_256 => crypto_digest256_new(DIGEST_SHA3_256), + DIGEST_SHA512 => crypto_digest512_new(DIGEST_SHA512), + DIGEST_SHA3_512 => crypto_digest512_new(DIGEST_SHA3_512), + _ => abort(), + } + } + } + + // In our C code, `crypto_digest*_new()` allocates memory with + // `tor_malloc()`. In `tor_malloc()`, if the underlying malloc + // implementation fails to allocate the requested memory and returns a + // NULL pointer, we call `exit(1)`. In the case that this `exit(1)` is + // called within a worker, be that a process or a thread, the inline + // comments within `tor_malloc()` mention "that's ok, since the parent + // will run out of memory soon anyway". However, if it takes long + // enough for the worker to die, and it manages to return a NULL pointer + // to our Rust code, our Rust is now in an irreparably broken state and + // may exhibit undefined behaviour. An even worse scenario, if/when we + // have parent/child processes/threads controlled by Rust, would be that + // the UB contagion in Rust manages to spread to other children before + // the entire process (hopefully terminates). + // + // However, following the assumptions made in `tor_malloc()` that + // calling `exit(1)` in a child is okay because the parent will + // eventually run into the same errors, and also to stymie any UB + // contagion in the meantime, we call abort!() here to terminate the + // entire program immediately. + if digest.is_null() { + abort(); + } + + CryptoDigest(digest) + } + + /// A wrapper to call the C function `crypto_digest_add_bytes`. + /// + /// # Inputs + /// + /// * `bytes`: a byte slice of bytes to be added into this digest. + /// + /// # C_RUST_COUPLED + /// + /// * `crypto_digest_add_bytes` + pub fn add_bytes(&self, bytes: &[u8]) { + unsafe { + crypto_digest_add_bytes(self.0 as *mut crypto_digest_t, + bytes.as_ptr() as *const c_char, + bytes.len() as size_t) + } + } +} + +impl Drop for CryptoDigest { + fn drop(&mut self) { + unsafe { + crypto_digest_free_(self.0 as *mut crypto_digest_t); + } + } +} + +/// Get the 256-bit digest output of a `crypto_digest_t`. +/// +/// # Inputs +/// +/// * `digest`: A `CryptoDigest` which wraps either a `DIGEST_SHA256` or a +/// `DIGEST_SHA3_256`. +/// +/// # Warning +/// +/// Calling this function with a `CryptoDigest` which is neither SHA2-256 or +/// SHA3-256 is a programming error. Since we cannot introspect the opaque +/// struct from Rust, however, there is no way for us to check that the correct +/// one is being passed in. That is up to you, dear programmer. If you mess +/// up, you will get a incorrectly-sized hash digest in return, and it will be +/// your fault. Don't do that. +/// +/// # Returns +/// +/// A 256-bit hash digest, as a `[u8; 32]`. +/// +/// # C_RUST_COUPLED +/// +/// * `crypto_digest_get_digest` +/// * `DIGEST256_LEN` +// +// FIXME: Once const generics land in Rust, we should genericise calling +// crypto_digest_get_digest w.r.t. output array size. +pub fn get_256_bit_digest(digest: CryptoDigest) -> [u8; DIGEST256_LEN] { + let mut buffer: [u8; DIGEST256_LEN] = [0u8; DIGEST256_LEN]; + + unsafe { + crypto_digest_get_digest(digest.0, + buffer.as_mut_ptr() as *mut c_char, + DIGEST256_LEN as size_t); + + if buffer.as_ptr().is_null() { + abort(); + } + } + buffer +} + +/// Get the 512-bit digest output of a `crypto_digest_t`. +/// +/// # Inputs +/// +/// * `digest`: A `CryptoDigest` which wraps either a `DIGEST_SHA512` or a +/// `DIGEST_SHA3_512`. +/// +/// # Warning +/// +/// Calling this function with a `CryptoDigest` which is neither SHA2-512 or +/// SHA3-512 is a programming error. Since we cannot introspect the opaque +/// struct from Rust, however, there is no way for us to check that the correct +/// one is being passed in. That is up to you, dear programmer. If you mess +/// up, you will get a incorrectly-sized hash digest in return, and it will be +/// your fault. Don't do that. +/// +/// # Returns +/// +/// A 512-bit hash digest, as a `[u8; 64]`. +/// +/// # C_RUST_COUPLED +/// +/// * `crypto_digest_get_digest` +/// * `DIGEST512_LEN` +// +// FIXME: Once const generics land in Rust, we should genericise calling +// crypto_digest_get_digest w.r.t. output array size. +pub fn get_512_bit_digest(digest: CryptoDigest) -> [u8; DIGEST512_LEN] { + let mut buffer: [u8; DIGEST512_LEN] = [0u8; DIGEST512_LEN]; + + unsafe { + crypto_digest_get_digest(digest.0, + buffer.as_mut_ptr() as *mut c_char, + DIGEST512_LEN as size_t); + + if buffer.as_ptr().is_null() { + abort(); + } + } + buffer +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_layout_common_digests_t() { + assert_eq!(::std::mem::size_of::<common_digests_t>(), 64usize, + concat!("Size of: ", stringify!(common_digests_t))); + assert_eq!(::std::mem::align_of::<common_digests_t>(), 1usize, + concat!("Alignment of ", stringify!(common_digests_t))); + } + + #[test] + fn test_layout_crypto_digest_t() { + assert_eq!(::std::mem::size_of::<crypto_digest_t>(), 0usize, + concat!("Size of: ", stringify!(crypto_digest_t))); + assert_eq!(::std::mem::align_of::<crypto_digest_t>(), 1usize, + concat!("Alignment of ", stringify!(crypto_digest_t))); + } +} diff --git a/src/rust/external/crypto_rand.rs b/src/rust/external/crypto_rand.rs new file mode 100644 index 0000000000..af1ade0161 --- /dev/null +++ b/src/rust/external/crypto_rand.rs @@ -0,0 +1,87 @@ +// Copyright (c) 2018, The Tor Project, Inc. +// Copyright (c) 2018, isis agora lovecruft +// See LICENSE for licensing information + +//! Bindings to external (P)RNG interfaces and utilities in +//! src/common/crypto_rand.[ch]. +//! +//! We wrap our C implementations in src/common/crypto_rand.[ch] here in order +//! to provide wrappers with native Rust types, and then provide more Rusty +//! types and and trait implementations in src/rust/crypto/rand/. + +use std::time::Duration; + +use libc::c_double; +use libc::c_int; +use libc::size_t; +use libc::time_t; +use libc::uint8_t; + +extern "C" { + fn crypto_seed_rng() -> c_int; + fn crypto_rand(out: *mut uint8_t, out_len: size_t); + fn crypto_strongest_rand(out: *mut uint8_t, out_len: size_t); + fn crypto_rand_time_range(min: time_t, max: time_t) -> time_t; + fn crypto_rand_double() -> c_double; +} + +/// Seed OpenSSL's random number generator with bytes from the operating +/// system. +/// +/// # Returns +/// +/// `true` on success; `false` on failure. +pub fn c_tor_crypto_seed_rng() -> bool { + let ret: c_int; + + unsafe { + ret = crypto_seed_rng(); + } + match ret { + 0 => return true, + _ => return false, + } +} + +/// Fill the bytes of `dest` with random data. +pub fn c_tor_crypto_rand(dest: &mut [u8]) { + unsafe { + crypto_rand(dest.as_mut_ptr(), dest.len() as size_t); + } +} + +/// Fill the bytes of `dest` with "strong" random data by hashing +/// together randomness obtained from OpenSSL's RNG and the operating +/// system. +pub fn c_tor_crypto_strongest_rand(dest: &mut [u8]) { + // We'll let the C side panic if the len is larger than + // MAX_STRONGEST_RAND_SIZE, rather than potentially panicking here. A + // paranoid caller should assert on the length of dest *before* calling this + // function. + unsafe { + crypto_strongest_rand(dest.as_mut_ptr(), dest.len() as size_t); + } +} + +/// Get a random time, in seconds since the Unix Epoch. +/// +/// # Returns +/// +/// A `std::time::Duration` of seconds since the Unix Epoch. +pub fn c_tor_crypto_rand_time_range(min: &Duration, max: &Duration) -> Duration { + let ret: time_t; + + unsafe { + ret = crypto_rand_time_range(min.as_secs() as time_t, max.as_secs() as time_t); + } + + Duration::from_secs(ret as u64) +} + +/// Return a pseudorandom 64-bit float, chosen uniformly from the range [0.0, 1.0). +pub fn c_tor_crypto_rand_double() -> f64 { + unsafe { + crypto_rand_double() + } +} + diff --git a/src/rust/external/external.rs b/src/rust/external/external.rs new file mode 100644 index 0000000000..b9e17f021d --- /dev/null +++ b/src/rust/external/external.rs @@ -0,0 +1,33 @@ +// Copyright (c) 2016-2017, The Tor Project, Inc. */ +// See LICENSE for licensing information */ + +use libc::{c_char, c_int}; +use std::ffi::CString; + +extern "C" { + fn tor_version_as_new_as( + platform: *const c_char, + cutoff: *const c_char, + ) -> c_int; +} + +/// Wrap calls to tor_version_as_new_as, defined in src/or/routerparse.c +pub fn c_tor_version_as_new_as(platform: &str, cutoff: &str) -> bool { + // CHK: These functions should log a warning if an error occurs. This + // can be added when integration with tor's logger is added to rust + let c_platform = match CString::new(platform) { + Ok(n) => n, + Err(_) => return false, + }; + + let c_cutoff = match CString::new(cutoff) { + Ok(n) => n, + Err(_) => return false, + }; + + let result: c_int = unsafe { + tor_version_as_new_as(c_platform.as_ptr(), c_cutoff.as_ptr()) + }; + + result == 1 +} diff --git a/src/rust/external/lib.rs b/src/rust/external/lib.rs new file mode 100644 index 0000000000..b72a4f6e4c --- /dev/null +++ b/src/rust/external/lib.rs @@ -0,0 +1,19 @@ +//! Copyright (c) 2016-2018, The Tor Project, Inc. */ +//! See LICENSE for licensing information */ + +//! Interface for external calls to tor C ABI +//! +//! The purpose of this module is to provide a clean interface for when Rust +//! modules need to interact with functionality in tor C code rather than each +//! module implementing this functionality repeatedly. + +extern crate libc; + +extern crate smartlist; + +pub mod crypto_digest; +mod crypto_rand; +mod external; + +pub use crypto_rand::*; +pub use external::*; diff --git a/src/rust/include.am b/src/rust/include.am new file mode 100644 index 0000000000..5e5b0b3faf --- /dev/null +++ b/src/rust/include.am @@ -0,0 +1,41 @@ +include src/rust/tor_rust/include.am + +EXTRA_DIST +=\ + src/rust/build.rs \ + src/rust/Cargo.toml \ + src/rust/Cargo.lock \ + src/rust/.cargo/config.in \ + src/rust/crypto/Cargo.toml \ + src/rust/crypto/lib.rs \ + src/rust/crypto/digests/mod.rs \ + src/rust/crypto/digests/sha2.rs \ + src/rust/crypto/rand/mod.rs \ + src/rust/crypto/rand/rng.rs \ + src/rust/external/Cargo.toml \ + src/rust/external/crypto_digest.rs \ + src/rust/external/crypto_rand.rs \ + src/rust/external/external.rs \ + src/rust/external/lib.rs \ + src/rust/protover/Cargo.toml \ + src/rust/protover/errors.rs \ + src/rust/protover/protoset.rs \ + src/rust/protover/ffi.rs \ + src/rust/protover/lib.rs \ + src/rust/protover/protover.rs \ + src/rust/protover/tests/protover.rs \ + src/rust/smartlist/Cargo.toml \ + src/rust/smartlist/lib.rs \ + src/rust/smartlist/smartlist.rs \ + src/rust/tor_allocate/Cargo.toml \ + src/rust/tor_allocate/lib.rs \ + src/rust/tor_allocate/tor_allocate.rs \ + src/rust/tor_log/Cargo.toml \ + src/rust/tor_log/lib.rs \ + src/rust/tor_log/tor_log.rs \ + src/rust/tor_rust/Cargo.toml \ + src/rust/tor_rust/include.am \ + src/rust/tor_rust/lib.rs \ + src/rust/tor_util/Cargo.toml \ + src/rust/tor_util/ffi.rs \ + src/rust/tor_util/lib.rs \ + src/rust/tor_util/strings.rs diff --git a/src/rust/protover/Cargo.toml b/src/rust/protover/Cargo.toml new file mode 100644 index 0000000000..a8480e142a --- /dev/null +++ b/src/rust/protover/Cargo.toml @@ -0,0 +1,30 @@ +[package] +authors = ["The Tor Project"] +version = "0.0.1" +name = "protover" + +[features] + +[dependencies] +libc = "=0.2.39" + +[dependencies.smartlist] +path = "../smartlist" + +[dependencies.external] +path = "../external" + +[dependencies.tor_util] +path = "../tor_util" + +[dependencies.tor_allocate] +path = "../tor_allocate" + +[dependencies.tor_log] +path = "../tor_log" + +[lib] +name = "protover" +path = "lib.rs" +crate_type = ["rlib", "staticlib"] + diff --git a/src/rust/protover/errors.rs b/src/rust/protover/errors.rs new file mode 100644 index 0000000000..d9dc73381f --- /dev/null +++ b/src/rust/protover/errors.rs @@ -0,0 +1,46 @@ +// Copyright (c) 2018, The Tor Project, Inc. +// Copyright (c) 2018, isis agora lovecruft +// See LICENSE for licensing information + +//! Various errors which may occur during protocol version parsing. + +use std::fmt; +use std::fmt::Display; + +/// All errors which may occur during protover parsing routines. +#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] +#[allow(missing_docs)] // See Display impl for error descriptions +pub enum ProtoverError { + Overlap, + LowGreaterThanHigh, + Unparseable, + ExceedsMax, + ExceedsExpansionLimit, + UnknownProtocol, + ExceedsNameLimit, + InvalidProtocol, +} + +/// Descriptive error messages for `ProtoverError` variants. +impl Display for ProtoverError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + ProtoverError::Overlap + => write!(f, "Two or more (low, high) protover ranges would overlap once expanded."), + ProtoverError::LowGreaterThanHigh + => write!(f, "The low in a (low, high) protover range was greater than high."), + ProtoverError::Unparseable + => write!(f, "The protover string was unparseable."), + ProtoverError::ExceedsMax + => write!(f, "The high in a (low, high) protover range exceeds u32::MAX."), + ProtoverError::ExceedsExpansionLimit + => write!(f, "The protover string would exceed the maximum expansion limit."), + ProtoverError::UnknownProtocol + => write!(f, "A protocol in the protover string we attempted to parse is unknown."), + ProtoverError::ExceedsNameLimit + => write!(f, "An unrecognised protocol name was too long."), + ProtoverError::InvalidProtocol + => write!(f, "A protocol name includes invalid characters."), + } + } +} diff --git a/src/rust/protover/ffi.rs b/src/rust/protover/ffi.rs new file mode 100644 index 0000000000..7386e988c5 --- /dev/null +++ b/src/rust/protover/ffi.rs @@ -0,0 +1,260 @@ +// Copyright (c) 2016-2017, The Tor Project, Inc. */ +// See LICENSE for licensing information */ + +//! 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 smartlist::*; +use tor_allocate::allocate_and_copy_string; + +use errors::ProtoverError; +use protover::*; + +/// Translate C enums to Rust Proto enums, using the integer value of the C +/// enum to map to its associated Rust enum. +/// +/// C_RUST_COUPLED: src/or/protover.h `protocol_type_t` +fn translate_to_rust(c_proto: uint32_t) -> Result<Protocol, ProtoverError> { + match c_proto { + 0 => Ok(Protocol::Link), + 1 => Ok(Protocol::LinkAuth), + 2 => Ok(Protocol::Relay), + 3 => Ok(Protocol::DirCache), + 4 => Ok(Protocol::HSDir), + 5 => Ok(Protocol::HSIntro), + 6 => Ok(Protocol::HSRend), + 7 => Ok(Protocol::Desc), + 8 => Ok(Protocol::Microdesc), + 9 => Ok(Protocol::Cons), + _ => Err(ProtoverError::UnknownProtocol), + } +} + +/// Provide an interface for C to translate arguments and return types for +/// protover::all_supported +#[no_mangle] +pub extern "C" fn 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 { CStr::from_ptr(c_relay_version) }; + + let relay_version = match c_str.to_str() { + Ok(n) => n, + Err(_) => return 1, + }; + + let relay_proto_entry: UnvalidatedProtoEntry = + match UnvalidatedProtoEntry::from_str_any_len(relay_version) { + Ok(n) => n, + Err(_) => return 1, + }; + let maybe_unsupported: Option<UnvalidatedProtoEntry> = relay_proto_entry.all_supported(); + + if maybe_unsupported.is_some() { + let unsupported: UnvalidatedProtoEntry = maybe_unsupported.unwrap(); + if missing_out.is_null() { + return 0; + } + let ptr = allocate_and_copy_string(&unsupported.to_string()); + unsafe { *missing_out = ptr }; + + return 0; + } + + 1 +} + +/// Provide an interface for C to translate arguments and return types for +/// protover::list_supports_protocol +#[no_mangle] +pub extern "C" fn 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 { CStr::from_ptr(c_protocol_list) }; + + let protocol_list = match c_str.to_str() { + Ok(n) => n, + Err(_) => return 1, + }; + let proto_entry: UnvalidatedProtoEntry = match protocol_list.parse() { + Ok(n) => n, + Err(_) => return 0, + }; + let protocol: UnknownProtocol = match translate_to_rust(c_protocol) { + Ok(n) => n.into(), + Err(_) => return 0, + }; + match proto_entry.supports_protocol(&protocol, &version) { + false => return 0, + true => return 1, + } +} + +#[no_mangle] +pub extern "C" fn protover_contains_long_protocol_names_( + c_protocol_list: *const c_char +) -> 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 { CStr::from_ptr(c_protocol_list) }; + + let protocol_list = match c_str.to_str() { + Ok(n) => n, + Err(_) => return 1 + }; + + let protocol_entry : Result<UnvalidatedProtoEntry,_> = + protocol_list.parse(); + + match protocol_entry { + Ok(_) => 0, + Err(_) => 1, + } +} + +/// Provide an interface for C to translate arguments and return types for +/// protover::list_supports_protocol_or_later +#[no_mangle] +pub extern "C" fn protocol_list_supports_protocol_or_later( + 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 { 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 proto_entry: UnvalidatedProtoEntry = match protocol_list.parse() { + Ok(n) => n, + Err(_) => return 1, + }; + + if proto_entry.supports_protocol_or_later(&protocol.into(), &version) { + return 1; + } + 0 +} + +/// Provide an interface for C to translate arguments and return types for +/// protover::get_supported_protocols +#[no_mangle] +pub extern "C" fn protover_get_supported_protocols() -> *const c_char { + let supported: &'static CStr; + + supported = get_supported_protocols_cstr(); + supported.as_ptr() +} + +/// Provide an interface for C to translate arguments and return types for +/// protover::compute_vote +// +// Why is the threshold a signed integer? —isis +#[no_mangle] +pub extern "C" fn protover_compute_vote( + list: *const Stringlist, + threshold: c_int +) -> *mut c_char { + + if list.is_null() { + let empty = String::new(); + return allocate_and_copy_string(&empty); + } + + // Dereference of raw pointer requires an unsafe block. The pointer is + // checked above to ensure it is not null. + let data: Vec<String> = unsafe { (*list).get_list() }; + let hold: usize = threshold as usize; + let mut proto_entries: Vec<UnvalidatedProtoEntry> = Vec::new(); + + for datum in data { + let entry: UnvalidatedProtoEntry = match datum.parse() { + Ok(n) => n, + Err(_) => continue + }; + proto_entries.push(entry); + } + let vote: UnvalidatedProtoEntry = ProtoverVote::compute(&proto_entries, &hold); + + allocate_and_copy_string(&vote.to_string()) +} + +/// Provide an interface for C to translate arguments and return types for +/// protover::is_supported_here +#[no_mangle] +pub extern "C" fn 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 protover_compute_for_old_tor(version: *const c_char) -> *const c_char { + let supported: &'static CStr; + let empty: &'static CStr; + + empty = cstr!(""); + + if version.is_null() { + return empty.as_ptr(); + } + + // 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 { CStr::from_ptr(version) }; + + let version = match c_str.to_str() { + Ok(n) => n, + Err(_) => return empty.as_ptr(), + }; + + supported = compute_for_old_tor_cstr(&version); + supported.as_ptr() +} diff --git a/src/rust/protover/lib.rs b/src/rust/protover/lib.rs new file mode 100644 index 0000000000..ce964196fd --- /dev/null +++ b/src/rust/protover/lib.rs @@ -0,0 +1,39 @@ +//! 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. + +#[deny(missing_docs)] + +extern crate libc; +extern crate smartlist; +extern crate external; +extern crate tor_allocate; +#[macro_use] +extern crate tor_util; + +pub mod errors; +pub mod protoset; +mod protover; +pub mod ffi; + +pub use protover::*; diff --git a/src/rust/protover/protoset.rs b/src/rust/protover/protoset.rs new file mode 100644 index 0000000000..465b8f2850 --- /dev/null +++ b/src/rust/protover/protoset.rs @@ -0,0 +1,673 @@ +// Copyright (c) 2018, The Tor Project, Inc. +// Copyright (c) 2018, isis agora lovecruft +// See LICENSE for licensing information + +//! Sets for lazily storing ordered, non-overlapping ranges of integers. + +use std::cmp; +use std::iter; +use std::slice; +use std::str::FromStr; +use std::u32; + +use errors::ProtoverError; + +/// A single version number. +pub type Version = u32; + +/// A `ProtoSet` stores an ordered `Vec<T>` of `(low, high)` pairs of ranges of +/// non-overlapping protocol versions. +/// +/// # Examples +/// +/// ``` +/// use std::str::FromStr; +/// +/// use protover::errors::ProtoverError; +/// use protover::protoset::ProtoSet; +/// use protover::protoset::Version; +/// +/// # fn do_test() -> Result<ProtoSet, ProtoverError> { +/// let protoset: ProtoSet = ProtoSet::from_str("3-5,8")?; +/// +/// // We could also equivalently call: +/// let protoset: ProtoSet = "3-5,8".parse()?; +/// +/// assert!(protoset.contains(&4)); +/// assert!(!protoset.contains(&7)); +/// +/// let expanded: Vec<Version> = protoset.clone().into(); +/// +/// assert_eq!(&expanded[..], &[3, 4, 5, 8]); +/// +/// let contracted: String = protoset.clone().to_string(); +/// +/// assert_eq!(contracted, "3-5,8".to_string()); +/// # Ok(protoset) +/// # } +/// # fn main() { do_test(); } // wrap the test so we can use the ? operator +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +pub struct ProtoSet { + pub(crate) pairs: Vec<(Version, Version)>, +} + +impl Default for ProtoSet { + fn default() -> Self { + let pairs: Vec<(Version, Version)> = Vec::new(); + + ProtoSet{ pairs } + } +} + +impl<'a> ProtoSet { + /// Create a new `ProtoSet` from a slice of `(low, high)` pairs. + /// + /// # Inputs + /// + /// We do not assume the input pairs are deduplicated or ordered. + pub fn from_slice(low_high_pairs: &'a [(Version, Version)]) -> Result<Self, ProtoverError> { + let mut pairs: Vec<(Version, Version)> = Vec::with_capacity(low_high_pairs.len()); + + for &(low, high) in low_high_pairs { + pairs.push((low, high)); + } + // Sort the pairs without reallocation and remove all duplicate pairs. + pairs.sort_unstable(); + pairs.dedup(); + + ProtoSet{ pairs }.is_ok() + } +} + +/// Expand this `ProtoSet` to a `Vec` of all its `Version`s. +/// +/// # Examples +/// +/// ``` +/// use std::str::FromStr; +/// use protover::protoset::ProtoSet; +/// use protover::protoset::Version; +/// # use protover::errors::ProtoverError; +/// +/// # fn do_test() -> Result<Vec<Version>, ProtoverError> { +/// let protoset: ProtoSet = ProtoSet::from_str("3-5,21")?; +/// let versions: Vec<Version> = protoset.into(); +/// +/// assert_eq!(&versions[..], &[3, 4, 5, 21]); +/// # +/// # Ok(versions) +/// # } +/// # fn main() { do_test(); } // wrap the test so we can use the ? operator +/// ``` +impl Into<Vec<Version>> for ProtoSet { + fn into(self) -> Vec<Version> { + let mut versions: Vec<Version> = Vec::new(); + + for &(low, high) in self.iter() { + versions.extend(low..high + 1); + } + versions + } +} + +impl ProtoSet { + /// Get an iterator over the `(low, high)` `pairs` in this `ProtoSet`. + pub fn iter(&self) -> slice::Iter<(Version, Version)> { + self.pairs.iter() + } + + /// Expand this `ProtoSet` into a `Vec` of all its `Version`s. + /// + /// # Examples + /// + /// ``` + /// # use protover::errors::ProtoverError; + /// use protover::protoset::ProtoSet; + /// + /// # fn do_test() -> Result<bool, ProtoverError> { + /// let protoset: ProtoSet = "3-5,9".parse()?; + /// + /// assert_eq!(protoset.expand(), vec![3, 4, 5, 9]); + /// + /// let protoset: ProtoSet = "1,3,5-7".parse()?; + /// + /// assert_eq!(protoset.expand(), vec![1, 3, 5, 6, 7]); + /// # + /// # Ok(true) + /// # } + /// # fn main() { do_test(); } // wrap the test so we can use the ? operator + /// ``` + pub fn expand(self) -> Vec<Version> { + self.into() + } + + pub fn len(&self) -> usize { + let mut length: usize = 0; + + for &(low, high) in self.iter() { + length += (high as usize - low as usize) + 1; + } + + length + } + + /// Check that this `ProtoSet` is well-formed. + /// + /// This is automatically called in `ProtoSet::from_str()`. + /// + /// # Errors + /// + /// * `ProtoverError::LowGreaterThanHigh`: if its `pairs` were not + /// well-formed, i.e. a `low` in a `(low, high)` was higher than the + /// previous `high`, + /// * `ProtoverError::Overlap`: if one or more of the `pairs` are + /// overlapping, + /// * `ProtoverError::ExceedsMax`: if the number of versions when expanded + /// would exceed `MAX_PROTOCOLS_TO_EXPAND`, and + /// + /// # Returns + /// + /// A `Result` whose `Ok` is this `Protoset`, and whose `Err` is one of the + /// errors enumerated in the Errors section above. + fn is_ok(self) -> Result<ProtoSet, ProtoverError> { + let mut last_high: Version = 0; + + for &(low, high) in self.iter() { + if low == u32::MAX || high == u32::MAX { + return Err(ProtoverError::ExceedsMax); + } + if low <= last_high { + return Err(ProtoverError::Overlap); + } else if low > high { + return Err(ProtoverError::LowGreaterThanHigh); + } + last_high = high; + } + + Ok(self) + } + + /// Determine if this `ProtoSet` contains no `Version`s. + /// + /// # Returns + /// + /// * `true` if this `ProtoSet`'s length is zero, and + /// * `false` otherwise. + /// + /// # Examples + /// + /// ``` + /// use protover::protoset::ProtoSet; + /// + /// let protoset: ProtoSet = ProtoSet::default(); + /// + /// assert!(protoset.is_empty()); + /// ``` + pub fn is_empty(&self) -> bool { + self.pairs.len() == 0 + } + + /// Determine if `version` is included within this `ProtoSet`. + /// + /// # Inputs + /// + /// * `version`: a `Version`. + /// + /// # Returns + /// + /// `true` if the `version` is contained within this set; `false` otherwise. + /// + /// # Examples + /// + /// ``` + /// # use protover::errors::ProtoverError; + /// use protover::protoset::ProtoSet; + /// + /// # fn do_test() -> Result<ProtoSet, ProtoverError> { + /// let protoset: ProtoSet = ProtoSet::from_slice(&[(0, 5), (7, 9), (13, 14)])?; + /// + /// assert!(protoset.contains(&5)); + /// assert!(!protoset.contains(&10)); + /// # + /// # Ok(protoset) + /// # } + /// # fn main() { do_test(); } // wrap the test so we can use the ? operator + /// ``` + pub fn contains(&self, version: &Version) -> bool { + for &(low, high) in self.iter() { + if low <= *version && *version <= high { + return true; + } + } + false + } + + /// Returns all the `Version`s in `self` which are not also in the `other` + /// `ProtoSet`. + /// + /// # Examples + /// + /// ``` + /// # use protover::errors::ProtoverError; + /// use protover::protoset::ProtoSet; + /// + /// # fn do_test() -> Result<bool, ProtoverError> { + /// let protoset: ProtoSet = "1,3-6,10-12,15-16".parse()?; + /// let other: ProtoSet = "2,5-7,9-11,14-20".parse()?; + /// + /// let subset: ProtoSet = protoset.and_not_in(&other); + /// + /// assert_eq!(subset.expand(), vec![1, 3, 4, 12]); + /// # + /// # Ok(true) + /// # } + /// # fn main() { do_test(); } // wrap the test so we can use the ? operator + /// ``` + pub fn and_not_in(&self, other: &Self) -> Self { + if self.is_empty() || other.is_empty() { + return self.clone(); + } + + let pairs = self.iter().flat_map(|&(lo, hi)| { + let the_end = (hi + 1, hi + 1); // special case to mark the end of the range. + let excluded_ranges = other + .iter() + .cloned() // have to be owned tuples, to match iter::once(the_end). + .skip_while(move|&(_, hi2)| hi2 < lo) // skip the non-overlapping ranges. + .take_while(move|&(lo2, _)| lo2 <= hi) // take all the overlapping ones. + .chain(iter::once(the_end)); + + let mut nextlo = lo; + excluded_ranges.filter_map(move |(excluded_lo, excluded_hi)| { + let pair = if nextlo < excluded_lo { + Some((nextlo, excluded_lo - 1)) + } else { + None + }; + nextlo = cmp::min(excluded_hi, u32::MAX - 1) + 1; + pair + }) + }); + + let pairs = pairs.collect(); + ProtoSet::is_ok(ProtoSet{ pairs }).expect("should be already sorted") + } +} + +impl FromStr for ProtoSet { + type Err = ProtoverError; + + /// Parse the unique version numbers supported by a subprotocol from a string. + /// + /// # Inputs + /// + /// * `version_string`, a string comprised of "[0-9,-]" + /// + /// # Returns + /// + /// A `Result` whose `Ok` value is a `ProtoSet` holding all of the unique + /// version numbers. + /// + /// The returned `Result`'s `Err` value is an `ProtoverError` appropriate to + /// the error. + /// + /// # Errors + /// + /// This function will error if: + /// + /// * the `version_string` is 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. + /// + /// # Examples + /// + /// ``` + /// use std::str::FromStr; + /// + /// use protover::errors::ProtoverError; + /// use protover::protoset::ProtoSet; + /// + /// # fn do_test() -> Result<ProtoSet, ProtoverError> { + /// let protoset: ProtoSet = ProtoSet::from_str("2-5,8")?; + /// + /// assert!(protoset.contains(&5)); + /// assert!(!protoset.contains(&10)); + /// + /// // We can also equivalently call `ProtoSet::from_str` by doing (all + /// // implementations of `FromStr` can be called this way, this one isn't + /// // special): + /// let protoset: ProtoSet = "4-6,12".parse()?; + /// + /// // Calling it (either way) can take really large ranges (up to `u32::MAX`): + /// let protoset: ProtoSet = "1-70000".parse()?; + /// let protoset: ProtoSet = "1-4294967296".parse()?; + /// + /// // There are lots of ways to get an `Err` from this function. Here are + /// // a few: + /// assert_eq!(Err(ProtoverError::Unparseable), ProtoSet::from_str("=")); + /// assert_eq!(Err(ProtoverError::Unparseable), ProtoSet::from_str("-")); + /// assert_eq!(Err(ProtoverError::Unparseable), ProtoSet::from_str("not_an_int")); + /// assert_eq!(Err(ProtoverError::Unparseable), ProtoSet::from_str("3-")); + /// assert_eq!(Err(ProtoverError::Unparseable), ProtoSet::from_str("1-,4")); + /// + /// // Things which would get parsed into an _empty_ `ProtoSet` are, + /// // however, legal, and result in an empty `ProtoSet`: + /// assert_eq!(Ok(ProtoSet::default()), ProtoSet::from_str("")); + /// assert_eq!(Ok(ProtoSet::default()), ProtoSet::from_str(",,,")); + /// # + /// # Ok(protoset) + /// # } + /// # fn main() { do_test(); } // wrap the test so we can use the ? operator + /// ``` + fn from_str(version_string: &str) -> Result<Self, Self::Err> { + let mut pairs: Vec<(Version, Version)> = Vec::new(); + let pieces: ::std::str::Split<char> = version_string.split(','); + + for p in pieces { + if p.is_empty() { + continue; + } else if p.contains('-') { + let mut pair = p.splitn(2, '-'); + + let low = pair.next().ok_or(ProtoverError::Unparseable)?; + let high = pair.next().ok_or(ProtoverError::Unparseable)?; + + let lo: Version = low.parse().or(Err(ProtoverError::Unparseable))?; + let hi: Version = high.parse().or(Err(ProtoverError::Unparseable))?; + + if lo == u32::MAX || hi == u32::MAX { + return Err(ProtoverError::ExceedsMax); + } + pairs.push((lo, hi)); + } else { + let v: u32 = p.parse().or(Err(ProtoverError::Unparseable))?; + + if v == u32::MAX { + return Err(ProtoverError::ExceedsMax); + } + pairs.push((v, v)); + } + } + // If we were passed in an empty string, or + // simply a comma, or a pile of commas, then return an empty ProtoSet. + if pairs.len() == 0 { + return Ok(ProtoSet::default()); + } + ProtoSet::from_slice(&pairs[..]) + } +} + +impl ToString for ProtoSet { + /// Contracts a `ProtoSet` of versions into a string. + /// + /// # Returns + /// + /// A `String` representation of this `ProtoSet` in ascending order. + fn to_string(&self) -> String { + let mut final_output: Vec<String> = Vec::new(); + + for &(lo, hi) in self.iter() { + if lo != hi { + debug_assert!(lo < hi); + final_output.push(format!("{}-{}", lo, hi)); + } else { + final_output.push(format!("{}", lo)); + } + } + final_output.join(",") + } +} + +/// 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. +/// +/// # 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, a`Version` of the last integer in the range, and a `usize` of +/// the index of that version. +/// +/// 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, and 2 which is the index of 3. +fn find_range(list: &Vec<Version>) -> (bool, Version, usize) { + if list.len() == 0 { + return (false, 0, 0); + } + + let mut index: usize = 0; + let mut iterable = list.iter().peekable(); + let mut range_end = match iterable.next() { + Some(n) => *n, + None => return (false, 0, 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; + index += 1; + } + + (has_range, range_end, index) +} + +impl From<Vec<Version>> for ProtoSet { + fn from(mut v: Vec<Version>) -> ProtoSet { + let mut version_pairs: Vec<(Version, Version)> = Vec::new(); + + v.sort_unstable(); + v.dedup(); + + 'vector: while !v.is_empty() { + let (has_range, end, index): (bool, Version, usize) = find_range(&v); + + if has_range { + let first: Version = match v.first() { + Some(x) => *x, + None => continue, + }; + let last: Version = match v.get(index) { + Some(x) => *x, + None => continue, + }; + debug_assert!(last == end, format!("last = {}, end = {}", last, end)); + + version_pairs.push((first, last)); + v = v.split_off(index + 1); + + if v.len() == 0 { + break 'vector; + } + } else { + let last: Version = match v.get(index) { + Some(x) => *x, + None => continue, + }; + version_pairs.push((last, last)); + v.remove(index); + } + } + ProtoSet::from_slice(&version_pairs[..]).unwrap_or(ProtoSet::default()) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_find_range() { + assert_eq!((false, 0, 0), find_range(&vec![])); + assert_eq!((false, 1, 0), find_range(&vec![1])); + assert_eq!((true, 2, 1), find_range(&vec![1, 2])); + assert_eq!((true, 3, 2), find_range(&vec![1, 2, 3])); + assert_eq!((true, 3, 2), find_range(&vec![1, 2, 3, 5])); + } + + macro_rules! assert_contains_each { + ($protoset:expr, $versions:expr) => ( + for version in $versions { + assert!($protoset.contains(version)); + } + ) + } + + macro_rules! test_protoset_contains_versions { + ($list:expr, $str:expr) => ( + let versions: &[Version] = $list; + let protoset: Result<ProtoSet, ProtoverError> = ProtoSet::from_str($str); + + assert!(protoset.is_ok()); + let p = protoset.unwrap(); + assert_contains_each!(p, versions); + ) + } + + #[test] + fn test_versions_from_str() { + test_protoset_contains_versions!(&[], ""); + test_protoset_contains_versions!(&[1], "1"); + test_protoset_contains_versions!(&[1, 2], "1,2"); + test_protoset_contains_versions!(&[1, 2, 3], "1-3"); + test_protoset_contains_versions!(&[1, 2, 5], "1-2,5"); + test_protoset_contains_versions!(&[1, 3, 4, 5], "1,3-5"); + test_protoset_contains_versions!(&[42, 55, 56, 57, 58], "42,55-58"); + } + + #[test] + fn test_versions_from_str_ab() { + assert_eq!(Err(ProtoverError::Unparseable), ProtoSet::from_str("a,b")); + } + + #[test] + fn test_versions_from_str_negative_1() { + assert_eq!(Err(ProtoverError::Unparseable), ProtoSet::from_str("-1")); + } + + #[test] + fn test_versions_from_str_hyphens() { + assert_eq!(Err(ProtoverError::Unparseable), ProtoSet::from_str("--1")); + assert_eq!(Err(ProtoverError::Unparseable), ProtoSet::from_str("-1-2")); + assert_eq!(Err(ProtoverError::Unparseable), ProtoSet::from_str("1--2")); + } + + #[test] + fn test_versions_from_str_triple() { + assert_eq!(Err(ProtoverError::Unparseable), ProtoSet::from_str("1-2-3")); + } + + #[test] + fn test_versions_from_str_1exclam() { + assert_eq!(Err(ProtoverError::Unparseable), ProtoSet::from_str("1,!")); + } + + #[test] + fn test_versions_from_str_percent_equal() { + assert_eq!(Err(ProtoverError::Unparseable), ProtoSet::from_str("%=")); + } + + #[test] + fn test_versions_from_str_whitespace() { + assert_eq!(Err(ProtoverError::Unparseable), ProtoSet::from_str("1,2\n")); + assert_eq!(Err(ProtoverError::Unparseable), ProtoSet::from_str("1\r,2")); + assert_eq!(Err(ProtoverError::Unparseable), ProtoSet::from_str("1,\t2")); + } + + #[test] + fn test_versions_from_str_overlap() { + assert_eq!(Err(ProtoverError::Overlap), ProtoSet::from_str("1-3,2-4")); + } + + #[test] + fn test_versions_from_slice_overlap() { + assert_eq!(Err(ProtoverError::Overlap), ProtoSet::from_slice(&[(1, 3), (2, 4)])); + } + + #[test] + fn test_versions_from_str_max() { + assert_eq!(Err(ProtoverError::ExceedsMax), ProtoSet::from_str("4294967295")); + } + + #[test] + fn test_versions_from_slice_max() { + assert_eq!(Err(ProtoverError::ExceedsMax), ProtoSet::from_slice(&[(4294967295, 4294967295)])); + } + + #[test] + fn test_protoset_contains() { + let protoset: ProtoSet = ProtoSet::from_slice(&[(1, 5), (7, 9), (13, 14)]).unwrap(); + + for x in 1..6 { assert!(protoset.contains(&x), format!("should contain {}", x)); } + for x in 7..10 { assert!(protoset.contains(&x), format!("should contain {}", x)); } + for x in 13..15 { assert!(protoset.contains(&x), format!("should contain {}", x)); } + + for x in [6, 10, 11, 12, 15, 42, 43, 44, 45, 1234584].iter() { + assert!(!protoset.contains(&x), format!("should not contain {}", x)); + } + } + + #[test] + fn test_protoset_contains_1_3() { + let protoset: ProtoSet = ProtoSet::from_slice(&[(1, 3)]).unwrap(); + + for x in 1..4 { assert!(protoset.contains(&x), format!("should contain {}", x)); } + } + + macro_rules! assert_protoset_from_vec_contains_all { + ($($x:expr),*) => ( + let vec: Vec<Version> = vec!($($x),*); + let protoset: ProtoSet = vec.clone().into(); + + for x in vec.iter() { + assert!(protoset.contains(&x)); + } + ) + } + + #[test] + fn test_protoset_from_vec_123() { + assert_protoset_from_vec_contains_all!(1, 2, 3); + } + + #[test] + fn test_protoset_from_vec_1_315() { + assert_protoset_from_vec_contains_all!(1, 2, 3, 15); + } + + #[test] + fn test_protoset_from_vec_unordered() { + let v: Vec<Version> = vec!(2, 3, 8, 4, 3, 9, 7, 2); + let ps: ProtoSet = v.into(); + + assert_eq!(ps.to_string(), "2-4,7-9"); + } + + #[test] + fn test_protoset_into_vec() { + let ps: ProtoSet = "1-13,42,9001,4294967294".parse().unwrap(); + let v: Vec<Version> = ps.into(); + + assert!(v.contains(&7)); + assert!(v.contains(&9001)); + assert!(v.contains(&4294967294)); + } +} + +#[cfg(all(test, feature = "bench"))] +mod bench { + use super::*; +} diff --git a/src/rust/protover/protover.rs b/src/rust/protover/protover.rs new file mode 100644 index 0000000000..68027056c4 --- /dev/null +++ b/src/rust/protover/protover.rs @@ -0,0 +1,914 @@ +// Copyright (c) 2016-2017, The Tor Project, Inc. */ +// See LICENSE for licensing information */ + +use std::collections::HashMap; +use std::collections::hash_map; +use std::ffi::CStr; +use std::fmt; +use std::str; +use std::str::FromStr; +use std::string::String; + +use external::c_tor_version_as_new_as; + +use errors::ProtoverError; +use protoset::Version; +use protoset::ProtoSet; + +/// The first version of Tor that included "proto" entries in its descriptors. +/// Authorities should use this to decide whether to guess proto lines. +/// +/// C_RUST_COUPLED: +/// src/or/protover.h `FIRST_TOR_VERSION_TO_ADVERTISE_PROTOCOLS` +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 +/// +/// C_RUST_COUPLED: src/or/protover.c `MAX_PROTOCOLS_TO_EXPAND` +const MAX_PROTOCOLS_TO_EXPAND: usize = (1<<16); + +/// The maximum size an `UnknownProtocol`'s name may be. +pub(crate) const MAX_PROTOCOL_NAME_LENGTH: usize = 100; + +/// Known subprotocols in Tor. Indicates which subprotocol a relay supports. +/// +/// C_RUST_COUPLED: src/or/protover.h `protocol_type_t` +#[derive(Clone, Hash, Eq, PartialEq, Debug)] +pub enum Protocol { + Cons, + Desc, + DirCache, + HSDir, + HSIntro, + HSRend, + Link, + LinkAuth, + Microdesc, + Relay, +} + +impl fmt::Display for Protocol { + 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. +/// +/// C_RUST_COUPLED: src/or/protover.c `PROTOCOL_NAMES` +impl FromStr for Protocol { + type Err = ProtoverError; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + match s { + "Cons" => Ok(Protocol::Cons), + "Desc" => Ok(Protocol::Desc), + "DirCache" => Ok(Protocol::DirCache), + "HSDir" => Ok(Protocol::HSDir), + "HSIntro" => Ok(Protocol::HSIntro), + "HSRend" => Ok(Protocol::HSRend), + "Link" => Ok(Protocol::Link), + "LinkAuth" => Ok(Protocol::LinkAuth), + "Microdesc" => Ok(Protocol::Microdesc), + "Relay" => Ok(Protocol::Relay), + _ => Err(ProtoverError::UnknownProtocol), + } + } +} + +/// A protocol string which is not one of the `Protocols` we currently know +/// about. +#[derive(Clone, Debug, Hash, Eq, PartialEq)] +pub struct UnknownProtocol(String); + +impl fmt::Display for UnknownProtocol { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +fn is_valid_proto(s: &str) -> bool { + s.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') +} + +impl FromStr for UnknownProtocol { + type Err = ProtoverError; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + if !is_valid_proto(s) { + Err(ProtoverError::InvalidProtocol) + } else if s.len() <= MAX_PROTOCOL_NAME_LENGTH { + Ok(UnknownProtocol(s.to_string())) + } else { + Err(ProtoverError::ExceedsNameLimit) + } + } +} + +impl UnknownProtocol { + /// Create an `UnknownProtocol`, ignoring whether or not it + /// exceeds MAX_PROTOCOL_NAME_LENGTH. + fn from_str_any_len(s: &str) -> Result<Self, ProtoverError> { + if !is_valid_proto(s) { + return Err(ProtoverError::InvalidProtocol); + } + Ok(UnknownProtocol(s.to_string())) + } +} + +impl From<Protocol> for UnknownProtocol { + fn from(p: Protocol) -> UnknownProtocol { + UnknownProtocol(p.to_string()) + } +} + +/// Get a CStr representation of current supported protocols, for +/// passing to C, or for converting to a `&str` for Rust. +/// +/// # Returns +/// +/// An `&'static CStr` whose value is the existing protocols supported by tor. +/// Returned data is in the format as follows: +/// +/// "HSDir=1-1 LinkAuth=1" +/// +/// # Note +/// +/// Rust code can use the `&'static CStr` as a normal `&'a str` by +/// calling `protover::get_supported_protocols`. +/// +// C_RUST_COUPLED: src/or/protover.c `protover_get_supported_protocols` +pub(crate) fn get_supported_protocols_cstr() -> &'static CStr { + cstr!("Cons=1-2 \ + Desc=1-2 \ + DirCache=1-2 \ + HSDir=1-2 \ + HSIntro=3-4 \ + HSRend=1-2 \ + Link=1-5 \ + LinkAuth=1,3 \ + Microdesc=1-2 \ + Relay=1-2") +} + +/// A map of protocol names to the versions of them which are supported. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ProtoEntry(HashMap<Protocol, ProtoSet>); + +impl Default for ProtoEntry { + fn default() -> ProtoEntry { + ProtoEntry( HashMap::new() ) + } +} + +impl ProtoEntry { + /// Get an iterator over the `Protocol`s and their `ProtoSet`s in this `ProtoEntry`. + pub fn iter(&self) -> hash_map::Iter<Protocol, ProtoSet> { + self.0.iter() + } + + /// Translate the supported tor versions from a string into a + /// ProtoEntry, which is useful when looking up a specific + /// subprotocol. + pub fn supported() -> Result<Self, ProtoverError> { + let supported_cstr: &'static CStr = get_supported_protocols_cstr(); + let supported: &str = supported_cstr.to_str().unwrap_or(""); + + supported.parse() + } + + pub fn len(&self) -> usize { + self.0.len() + } + + pub fn get(&self, protocol: &Protocol) -> Option<&ProtoSet> { + self.0.get(protocol) + } + + pub fn insert(&mut self, key: Protocol, value: ProtoSet) { + self.0.insert(key, value); + } + + pub fn remove(&mut self, key: &Protocol) -> Option<ProtoSet> { + self.0.remove(key) + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +impl FromStr for ProtoEntry { + type Err = ProtoverError; + + /// Parse a string of subprotocol types and their version numbers. + /// + /// # Inputs + /// + /// * A `protocol_entry` string, comprised of a keywords, an "=" sign, and + /// one or more version numbers, each separated by a space. For example, + /// `"Cons=3-4 HSDir=1"`. + /// + /// # Returns + /// + /// A `Result` whose `Ok` value is a `ProtoEntry`, where the + /// first element is the subprotocol type (see `protover::Protocol`) and the last + /// element is an ordered set of `(low, high)` unique version numbers which are supported. + /// Otherwise, the `Err` value of this `Result` is a `ProtoverError`. + fn from_str(protocol_entry: &str) -> Result<ProtoEntry, ProtoverError> { + let mut proto_entry: ProtoEntry = ProtoEntry::default(); + let entries = protocol_entry.split(' '); + + for entry in entries { + let mut parts = entry.splitn(2, '='); + + let proto = match parts.next() { + Some(n) => n, + None => return Err(ProtoverError::Unparseable), + }; + + let vers = match parts.next() { + Some(n) => n, + None => return Err(ProtoverError::Unparseable), + }; + let versions: ProtoSet = vers.parse()?; + let proto_name: Protocol = proto.parse()?; + + proto_entry.insert(proto_name, versions); + + if proto_entry.len() > MAX_PROTOCOLS_TO_EXPAND { + return Err(ProtoverError::ExceedsMax); + } + } + Ok(proto_entry) + } +} + +/// Generate an implementation of `ToString` for either a `ProtoEntry` or an +/// `UnvalidatedProtoEntry`. +macro_rules! impl_to_string_for_proto_entry { + ($t:ty) => ( + impl ToString for $t { + fn to_string(&self) -> String { + let mut parts: Vec<String> = Vec::new(); + + for (protocol, versions) in self.iter() { + parts.push(format!("{}={}", protocol.to_string(), versions.to_string())); + } + parts.sort_unstable(); + parts.join(" ") + } + } + ) +} + +impl_to_string_for_proto_entry!(ProtoEntry); +impl_to_string_for_proto_entry!(UnvalidatedProtoEntry); + +/// A `ProtoEntry`, but whose `Protocols` can be any `UnknownProtocol`, not just +/// the supported ones enumerated in `Protocols`. The protocol versions are +/// validated, however. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct UnvalidatedProtoEntry(HashMap<UnknownProtocol, ProtoSet>); + +impl Default for UnvalidatedProtoEntry { + fn default() -> UnvalidatedProtoEntry { + UnvalidatedProtoEntry( HashMap::new() ) + } +} + +impl UnvalidatedProtoEntry { + /// Get an iterator over the `Protocol`s and their `ProtoSet`s in this `ProtoEntry`. + pub fn iter(&self) -> hash_map::Iter<UnknownProtocol, ProtoSet> { + self.0.iter() + } + + pub fn get(&self, protocol: &UnknownProtocol) -> Option<&ProtoSet> { + self.0.get(protocol) + } + + pub fn insert(&mut self, key: UnknownProtocol, value: ProtoSet) { + self.0.insert(key, value); + } + + pub fn remove(&mut self, key: &UnknownProtocol) -> Option<ProtoSet> { + self.0.remove(key) + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + pub fn len(&self) -> usize { + let mut total: usize = 0; + + for (_, versions) in self.iter() { + total += versions.len(); + } + total + } + + /// Determine if we support every protocol a client supports, and if not, + /// determine which protocols we do not have support for. + /// + /// # Returns + /// + /// Optionally, return parameters which the client supports but which we do not. + /// + /// # Examples + /// ``` + /// use protover::UnvalidatedProtoEntry; + /// + /// let protocols: UnvalidatedProtoEntry = "LinkAuth=1 Microdesc=1-2 Relay=2".parse().unwrap(); + /// let unsupported: Option<UnvalidatedProtoEntry> = protocols.all_supported(); + /// assert_eq!(true, unsupported.is_none()); + /// + /// let protocols: UnvalidatedProtoEntry = "Link=1-2 Wombat=9".parse().unwrap(); + /// let unsupported: Option<UnvalidatedProtoEntry> = protocols.all_supported(); + /// assert_eq!(true, unsupported.is_some()); + /// assert_eq!("Wombat=9", &unsupported.unwrap().to_string()); + /// ``` + pub fn all_supported(&self) -> Option<UnvalidatedProtoEntry> { + let mut unsupported: UnvalidatedProtoEntry = UnvalidatedProtoEntry::default(); + let supported: ProtoEntry = match ProtoEntry::supported() { + Ok(x) => x, + Err(_) => return None, + }; + + for (protocol, versions) in self.iter() { + let is_supported: Result<Protocol, ProtoverError> = protocol.0.parse(); + let supported_protocol: Protocol; + + // If the protocol wasn't even in the enum, then we definitely don't + // know about it and don't support any of its versions. + if is_supported.is_err() { + if !versions.is_empty() { + unsupported.insert(protocol.clone(), versions.clone()); + } + continue; + } else { + supported_protocol = is_supported.unwrap(); + } + + let maybe_supported_versions: Option<&ProtoSet> = supported.get(&supported_protocol); + let supported_versions: &ProtoSet; + + // If the protocol wasn't in the map, then we don't know about it + // and don't support any of its versions. Add its versions to the + // map (if it has versions). + if maybe_supported_versions.is_none() { + if !versions.is_empty() { + unsupported.insert(protocol.clone(), versions.clone()); + } + continue; + } else { + supported_versions = maybe_supported_versions.unwrap(); + } + let unsupported_versions = versions.and_not_in(supported_versions); + + if !unsupported_versions.is_empty() { + unsupported.insert(protocol.clone(), unsupported_versions); + } + } + + if unsupported.is_empty() { + return None; + } + Some(unsupported) + } + + /// Determine if we have support for some protocol and version. + /// + /// # Inputs + /// + /// * `proto`, an `UnknownProtocol` to test support for + /// * `vers`, a `Version` which we will go on to determine whether the + /// specified protocol supports. + /// + /// # Return + /// + /// Returns `true` iff this `UnvalidatedProtoEntry` includes support for the + /// indicated protocol and version, and `false` otherwise. + /// + /// # Examples + /// + /// ``` + /// # use std::str::FromStr; + /// use protover::*; + /// # use protover::errors::ProtoverError; + /// + /// # fn do_test () -> Result<UnvalidatedProtoEntry, ProtoverError> { + /// let proto: UnvalidatedProtoEntry = "Link=3-4 Cons=1 Doggo=3-5".parse()?; + /// assert_eq!(true, proto.supports_protocol(&Protocol::Cons.into(), &1)); + /// assert_eq!(false, proto.supports_protocol(&Protocol::Cons.into(), &5)); + /// assert_eq!(true, proto.supports_protocol(&UnknownProtocol::from_str("Doggo")?, &4)); + /// # Ok(proto) + /// # } fn main () { do_test(); } + /// ``` + pub fn supports_protocol(&self, proto: &UnknownProtocol, vers: &Version) -> bool { + let supported_versions: &ProtoSet = match self.get(proto) { + Some(n) => n, + None => return false, + }; + supported_versions.contains(&vers) + } + + /// As `UnvalidatedProtoEntry::supports_protocol()`, but also returns `true` + /// if any later version of the protocol is supported. + /// + /// # Examples + /// ``` + /// use protover::*; + /// # use protover::errors::ProtoverError; + /// + /// # fn do_test () -> Result<UnvalidatedProtoEntry, ProtoverError> { + /// let proto: UnvalidatedProtoEntry = "Link=3-4 Cons=5".parse()?; + /// + /// assert_eq!(true, proto.supports_protocol_or_later(&Protocol::Cons.into(), &5)); + /// assert_eq!(true, proto.supports_protocol_or_later(&Protocol::Cons.into(), &4)); + /// assert_eq!(false, proto.supports_protocol_or_later(&Protocol::Cons.into(), &6)); + /// # Ok(proto) + /// # } fn main () { do_test(); } + /// ``` + pub fn supports_protocol_or_later(&self, proto: &UnknownProtocol, vers: &Version) -> bool { + let supported_versions: &ProtoSet = match self.get(&proto) { + Some(n) => n, + None => return false, + }; + supported_versions.iter().any(|v| v.1 >= *vers) + } + + /// Split a string containing (potentially) several protocols and their + /// versions into a `Vec` of tuples of string in `(protocol, versions)` + /// form. + /// + /// # Inputs + /// + /// A &str in the form `"Link=3-4 Cons=5"`. + /// + /// # Returns + /// + /// A `Result` whose `Ok` variant is a `Vec<(&str, &str)>` of `(protocol, + /// versions)`, or whose `Err` variant is a `ProtoverError`. + /// + /// # Errors + /// + /// This will error with a `ProtoverError::Unparseable` if any of the + /// following are true: + /// + /// * If a protocol name is an empty string, e.g. `"Cons=1,3 =3-5"`. + /// * If a protocol name cannot be parsed as utf-8. + /// * If the version numbers are an empty string, e.g. `"Cons="`. + fn parse_protocol_and_version_str<'a>(protocol_string: &'a str) + -> Result<Vec<(&'a str, &'a str)>, ProtoverError> + { + let mut protovers: Vec<(&str, &str)> = Vec::new(); + + for subproto in protocol_string.split(' ') { + let mut parts = subproto.splitn(2, '='); + + let name = match parts.next() { + Some("") => return Err(ProtoverError::Unparseable), + Some(n) => n, + None => return Err(ProtoverError::Unparseable), + }; + let vers = match parts.next() { + Some(n) => n, + None => return Err(ProtoverError::Unparseable), + }; + protovers.push((name, vers)); + } + Ok(protovers) + } +} + +impl FromStr for UnvalidatedProtoEntry { + type Err = ProtoverError; + + /// 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 Protocol types + /// + /// # Returns + /// + /// A `Result` whose `Ok` value is a `ProtoSet` holding all of the + /// unique version numbers. + /// + /// The returned `Result`'s `Err` value is an `ProtoverError` whose `Display` + /// impl has a description of the error. + /// + /// # Errors + /// + /// This function will error if: + /// + /// * The protocol string does not follow the "protocol_name=version_list" + /// expected format, or + /// * If the version string is malformed. See `impl FromStr for ProtoSet`. + fn from_str(protocol_string: &str) -> Result<UnvalidatedProtoEntry, ProtoverError> { + let mut parsed: UnvalidatedProtoEntry = UnvalidatedProtoEntry::default(); + let parts: Vec<(&str, &str)> = + UnvalidatedProtoEntry::parse_protocol_and_version_str(protocol_string)?; + + for &(name, vers) in parts.iter() { + let versions = ProtoSet::from_str(vers)?; + let protocol = UnknownProtocol::from_str(name)?; + + parsed.insert(protocol, versions); + } + Ok(parsed) + } +} + +impl UnvalidatedProtoEntry { + /// Create an `UnknownProtocol`, ignoring whether or not it + /// exceeds MAX_PROTOCOL_NAME_LENGTH. + pub(crate) fn from_str_any_len(protocol_string: &str) + -> Result<UnvalidatedProtoEntry, ProtoverError> + { + let mut parsed: UnvalidatedProtoEntry = UnvalidatedProtoEntry::default(); + let parts: Vec<(&str, &str)> = + UnvalidatedProtoEntry::parse_protocol_and_version_str(protocol_string)?; + + for &(name, vers) in parts.iter() { + let versions = ProtoSet::from_str(vers)?; + let protocol = UnknownProtocol::from_str_any_len(name)?; + + parsed.insert(protocol, versions); + } + Ok(parsed) + } +} + +/// Pretend a `ProtoEntry` is actually an `UnvalidatedProtoEntry`. +impl From<ProtoEntry> for UnvalidatedProtoEntry { + fn from(proto_entry: ProtoEntry) -> UnvalidatedProtoEntry { + let mut unvalidated: UnvalidatedProtoEntry = UnvalidatedProtoEntry::default(); + + for (protocol, versions) in proto_entry.iter() { + unvalidated.insert(UnknownProtocol::from(protocol.clone()), versions.clone()); + } + unvalidated + } +} + +/// A mapping of protocols to a count of how many times each of their `Version`s +/// were voted for or supported. +/// +/// # Warning +/// +/// The "protocols" are *not* guaranteed to be known/supported `Protocol`s, in +/// order to allow new subprotocols to be introduced even if Directory +/// Authorities don't yet know of them. +pub struct ProtoverVote( HashMap<UnknownProtocol, HashMap<Version, usize>> ); + +impl Default for ProtoverVote { + fn default() -> ProtoverVote { + ProtoverVote( HashMap::new() ) + } +} + +impl IntoIterator for ProtoverVote { + type Item = (UnknownProtocol, HashMap<Version, usize>); + type IntoIter = hash_map::IntoIter<UnknownProtocol, HashMap<Version, usize>>; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +impl ProtoverVote { + pub fn entry(&mut self, key: UnknownProtocol) + -> hash_map::Entry<UnknownProtocol, HashMap<Version, usize>> + { + self.0.entry(key) + } + + /// Protocol voting implementation. + /// + /// Given a slice of `UnvalidatedProtoEntry`s and a vote `threshold`, return + /// a new `UnvalidatedProtoEntry` encoding all of the protocols that are + /// listed by at least `threshold` of the inputs. + /// + /// # Examples + /// + /// ``` + /// use protover::ProtoverVote; + /// use protover::UnvalidatedProtoEntry; + /// + /// let protos: &[UnvalidatedProtoEntry] = &["Link=3-4".parse().unwrap(), + /// "Link=3".parse().unwrap()]; + /// let vote = ProtoverVote::compute(protos, &2); + /// assert_eq!("Link=3", vote.to_string()); + /// ``` + // C_RUST_COUPLED: /src/or/protover.c protover_compute_vote + pub fn compute(proto_entries: &[UnvalidatedProtoEntry], threshold: &usize) -> UnvalidatedProtoEntry { + let mut all_count: ProtoverVote = ProtoverVote::default(); + let mut final_output: UnvalidatedProtoEntry = UnvalidatedProtoEntry::default(); + + if proto_entries.is_empty() { + return final_output; + } + + // parse and collect all of the protos and their versions and collect them + for vote in proto_entries { + // C_RUST_DIFFERS: This doesn't actually differ, bu this check on + // the total is here to make it match. Because the C version calls + // expand_protocol_list() which checks if there would be too many + // subprotocols *or* individual version numbers, i.e. more than + // MAX_PROTOCOLS_TO_EXPAND, and does this *per vote*, we need to + // match it's behaviour and ensure we're not allowing more than it + // would. + if vote.len() > MAX_PROTOCOLS_TO_EXPAND { + continue; + } + + for (protocol, versions) in vote.iter() { + let supported_vers: &mut HashMap<Version, usize> = + all_count.entry(protocol.clone()).or_insert(HashMap::new()); + + for version in versions.clone().expand() { + let counter: &mut usize = + supported_vers.entry(version).or_insert(0); + *counter += 1; + } + } + } + + for (protocol, mut versions) in all_count { + // Go through and remove versions that are less than the threshold + versions.retain(|_, count| *count as usize >= *threshold); + + if versions.len() > 0 { + let voted_versions: Vec<Version> = versions.keys().cloned().collect(); + let voted_protoset: ProtoSet = ProtoSet::from(voted_versions); + + final_output.insert(protocol, voted_protoset); + } + } + final_output + } +} + +/// Returns a boolean indicating whether the given protocol and version is +/// supported in any of the existing Tor protocols +/// +/// # Examples +/// ``` +/// use protover::is_supported_here; +/// use protover::Protocol; +/// +/// let is_supported = is_supported_here(&Protocol::Link, &10); +/// assert_eq!(false, is_supported); +/// +/// let is_supported = is_supported_here(&Protocol::Link, &1); +/// assert_eq!(true, is_supported); +/// ``` +pub fn is_supported_here(proto: &Protocol, vers: &Version) -> bool { + let currently_supported: ProtoEntry = match ProtoEntry::supported() { + Ok(result) => result, + Err(_) => return false, + }; + let supported_versions = match currently_supported.get(proto) { + Some(n) => n, + None => return false, + }; + supported_versions.contains(vers) +} + +/// Since older versions of Tor cannot infer their own subprotocols, +/// determine which subprotocols are supported by older Tor versions. +/// +/// # Inputs +/// +/// * `version`, a string comprised of "[0-9a-z.-]" +/// +/// # Returns +/// +/// A `&'static CStr` encoding a list of protocol names and supported +/// versions. 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` +/// (but not older than 0.2.4.19). For newer tors (or older than 0.2.4.19), it +/// returns an empty string. +/// +/// # Note +/// +/// This function is meant to be called for/within FFI code. If you'd +/// like to use this code in Rust, please see `compute_for_old_tor()`. +// +// C_RUST_COUPLED: src/rust/protover.c `compute_for_old_tor` +pub(crate) fn compute_for_old_tor_cstr(version: &str) -> &'static CStr { + let empty: &'static CStr = cstr!(""); + + if c_tor_version_as_new_as(version, FIRST_TOR_VERSION_TO_ADVERTISE_PROTOCOLS) { + return empty; + } + if c_tor_version_as_new_as(version, "0.2.9.1-alpha") { + return cstr!("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"); + } + if c_tor_version_as_new_as(version, "0.2.7.5") { + return cstr!("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"); + } + if c_tor_version_as_new_as(version, "0.2.4.19") { + return cstr!("Cons=1 Desc=1 DirCache=1 HSDir=1 HSIntro=3 HSRend=1 \ + Link=1-4 LinkAuth=1 Microdesc=1 Relay=1-2"); + } + empty +} + +/// Since older versions of Tor cannot infer their own subprotocols, +/// determine which subprotocols are supported by older Tor versions. +/// +/// # Inputs +/// +/// * `version`, a string comprised of "[0-9a-z.-]" +/// +/// # Returns +/// +/// A `Result` whose `Ok` value is an `&'static str` encoding a list of protocol +/// names and supported versions. 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`. +/// (but not older than 0.2.4.19). For newer tors (or older than 0.2.4.19), its +/// `Ok` `Result` contains an empty string. +/// +/// Otherwise, its `Err` contains a `ProtoverError::Unparseable` if the +/// `version` string was invalid utf-8. +/// +/// # Note +/// +/// This function is meant to be called for/within non-FFI Rust code. +// +// C_RUST_COUPLED: src/rust/protover.c `compute_for_old_tor` +pub fn compute_for_old_tor(version: &str) -> Result<&'static str, ProtoverError> { + // .to_str() fails with a Utf8Error if it couldn't validate the + // utf-8, so convert that here into an Unparseable ProtoverError. + compute_for_old_tor_cstr(version).to_str().or(Err(ProtoverError::Unparseable)) +} + +#[cfg(test)] +mod test { + use std::str::FromStr; + use std::string::ToString; + + use super::*; + + macro_rules! parse_proto { + ($e:expr) => {{ + let proto: Result<UnknownProtocol, _> = $e.parse(); + let proto2 = UnknownProtocol::from_str_any_len($e); + assert_eq!(proto, proto2); + proto + }}; + } + + #[test] + fn test_protocol_from_str() { + assert!(parse_proto!("Cons").is_ok()); + assert!(parse_proto!("123").is_ok()); + assert!(parse_proto!("1-2-3").is_ok()); + + let err = Err(ProtoverError::InvalidProtocol); + assert_eq!(err, parse_proto!("a_b_c")); + assert_eq!(err, parse_proto!("a b")); + assert_eq!(err, parse_proto!("a,")); + assert_eq!(err, parse_proto!("b.")); + assert_eq!(err, parse_proto!("é")); + } + + macro_rules! assert_protoentry_is_parseable { + ($e:expr) => ( + let protoentry: Result<ProtoEntry, ProtoverError> = $e.parse(); + + assert!(protoentry.is_ok(), format!("{:?}", protoentry.err())); + ) + } + + macro_rules! assert_protoentry_is_unparseable { + ($e:expr) => ( + let protoentry: Result<ProtoEntry, ProtoverError> = $e.parse(); + + assert!(protoentry.is_err()); + ) + } + + #[test] + fn test_protoentry_from_str_multiple_protocols_multiple_versions() { + assert_protoentry_is_parseable!("Cons=3-4 Link=1,3-5"); + } + + #[test] + fn test_protoentry_from_str_empty() { + assert_protoentry_is_unparseable!(""); + } + + #[test] + fn test_protoentry_from_str_single_protocol_single_version() { + assert_protoentry_is_parseable!("HSDir=1"); + } + + #[test] + fn test_protoentry_from_str_unknown_protocol() { + assert_protoentry_is_unparseable!("Ducks=5-7,8"); + } + + #[test] + fn test_protoentry_from_str_allowed_number_of_versions() { + assert_protoentry_is_parseable!("Desc=1-4294967294"); + } + + #[test] + fn test_protoentry_from_str_too_many_versions() { + assert_protoentry_is_unparseable!("Desc=1-4294967295"); + } + + #[test] + fn test_protoentry_from_str_() { + assert_protoentry_is_unparseable!(""); + } + + #[test] + fn test_protoentry_all_supported_single_protocol_single_version() { + let protocol: UnvalidatedProtoEntry = "Cons=1".parse().unwrap(); + let unsupported: Option<UnvalidatedProtoEntry> = protocol.all_supported(); + assert_eq!(true, unsupported.is_none()); + } + + #[test] + fn test_protoentry_all_supported_multiple_protocol_multiple_versions() { + let protocols: UnvalidatedProtoEntry = "Link=3-4 Desc=2".parse().unwrap(); + let unsupported: Option<UnvalidatedProtoEntry> = protocols.all_supported(); + assert_eq!(true, unsupported.is_none()); + } + + #[test] + fn test_protoentry_all_supported_three_values() { + let protocols: UnvalidatedProtoEntry = "LinkAuth=1 Microdesc=1-2 Relay=2".parse().unwrap(); + let unsupported: Option<UnvalidatedProtoEntry> = protocols.all_supported(); + assert_eq!(true, unsupported.is_none()); + } + + #[test] + fn test_protoentry_all_supported_unknown_protocol() { + let protocols: UnvalidatedProtoEntry = "Wombat=9".parse().unwrap(); + let unsupported: Option<UnvalidatedProtoEntry> = protocols.all_supported(); + assert_eq!(true, unsupported.is_some()); + assert_eq!("Wombat=9", &unsupported.unwrap().to_string()); + } + + #[test] + fn test_protoentry_all_supported_unsupported_high_version() { + let protocols: UnvalidatedProtoEntry = "HSDir=12-100".parse().unwrap(); + let unsupported: Option<UnvalidatedProtoEntry> = protocols.all_supported(); + assert_eq!(true, unsupported.is_some()); + assert_eq!("HSDir=12-100", &unsupported.unwrap().to_string()); + } + + #[test] + fn test_protoentry_all_supported_unsupported_low_version() { + let protocols: UnvalidatedProtoEntry = "HSIntro=2-3".parse().unwrap(); + let unsupported: Option<UnvalidatedProtoEntry> = protocols.all_supported(); + assert_eq!(true, unsupported.is_some()); + assert_eq!("HSIntro=2", &unsupported.unwrap().to_string()); + } + + #[test] + fn test_contract_protocol_list() { + let mut versions = ""; + assert_eq!(String::from(versions), ProtoSet::from_str(&versions).unwrap().to_string()); + + versions = "1"; + assert_eq!(String::from(versions), ProtoSet::from_str(&versions).unwrap().to_string()); + + versions = "1-2"; + assert_eq!(String::from(versions), ProtoSet::from_str(&versions).unwrap().to_string()); + + versions = "1,3"; + assert_eq!(String::from(versions), ProtoSet::from_str(&versions).unwrap().to_string()); + + versions = "1-4"; + assert_eq!(String::from(versions), ProtoSet::from_str(&versions).unwrap().to_string()); + + versions = "1,3,5-7"; + assert_eq!(String::from(versions), ProtoSet::from_str(&versions).unwrap().to_string()); + + versions = "1-3,500"; + assert_eq!(String::from(versions), ProtoSet::from_str(&versions).unwrap().to_string()); + } +} diff --git a/src/rust/protover/tests/protover.rs b/src/rust/protover/tests/protover.rs new file mode 100644 index 0000000000..9258d869d7 --- /dev/null +++ b/src/rust/protover/tests/protover.rs @@ -0,0 +1,394 @@ +// Copyright (c) 2016-2017, The Tor Project, Inc. */ +// See LICENSE for licensing information */ + +extern crate protover; + +use protover::ProtoEntry; +use protover::ProtoverVote; +use protover::UnvalidatedProtoEntry; +use protover::errors::ProtoverError; + +#[test] +fn parse_protocol_with_single_proto_and_single_version() { + let _: ProtoEntry = "Cons=1".parse().unwrap(); +} + +#[test] +fn parse_protocol_with_single_protocol_and_multiple_versions() { + let _: ProtoEntry = "Cons=1-2".parse().unwrap(); +} + +#[test] +fn parse_protocol_with_different_single_protocol_and_single_version() { + let _: ProtoEntry = "HSDir=1".parse().unwrap(); +} + +#[test] +fn parse_protocol_with_single_protocol_and_supported_version() { + let _: ProtoEntry = "Desc=2".parse().unwrap(); +} + +#[test] +fn parse_protocol_with_two_protocols_and_single_version() { + let _: ProtoEntry = "Cons=1 HSDir=1".parse().unwrap(); +} + +#[test] +fn parse_protocol_with_single_protocol_and_two_sequential_versions() { + let _: ProtoEntry = "Desc=1-2".parse().unwrap(); +} + +#[test] +fn parse_protocol_with_single_protocol_and_protocol_range() { + let _: ProtoEntry = "Link=1-4".parse().unwrap(); +} + +#[test] +fn parse_protocol_with_single_protocol_and_protocol_set() { + let _: ProtoEntry = "Link=3-4 Desc=2".parse().unwrap(); +} + +#[test] +fn protocol_all_supported_with_single_protocol_and_protocol_set() { + let protocols: UnvalidatedProtoEntry = "Link=3-4 Desc=2".parse().unwrap(); + let unsupported: Option<UnvalidatedProtoEntry> = protocols.all_supported(); + assert_eq!(true, unsupported.is_none()); +} + +#[test] +fn protocol_all_supported_with_two_values() { + let protocols: UnvalidatedProtoEntry = "Microdesc=1-2 Relay=2".parse().unwrap(); + let unsupported: Option<UnvalidatedProtoEntry> = protocols.all_supported(); + assert_eq!(true, unsupported.is_none()); +} + +#[test] +fn protocol_all_supported_with_one_value() { + let protocols: UnvalidatedProtoEntry = "Microdesc=1-2".parse().unwrap(); + let unsupported: Option<UnvalidatedProtoEntry> = protocols.all_supported(); + assert_eq!(true, unsupported.is_none()); +} + +#[test] +#[should_panic] +fn parse_protocol_unvalidated_with_empty() { + let _: UnvalidatedProtoEntry = "".parse().unwrap(); +} + +#[test] +#[should_panic] +fn parse_protocol_validated_with_empty() { + let _: UnvalidatedProtoEntry = "".parse().unwrap(); +} + +#[test] +fn protocol_all_supported_with_three_values() { + let protocols: UnvalidatedProtoEntry = "LinkAuth=1 Microdesc=1-2 Relay=2".parse().unwrap(); + let unsupported: Option<UnvalidatedProtoEntry> = protocols.all_supported(); + assert_eq!(true, unsupported.is_none()); +} + +#[test] +fn protocol_all_supported_with_unsupported_protocol() { + let protocols: UnvalidatedProtoEntry = "Wombat=9".parse().unwrap(); + let unsupported: Option<UnvalidatedProtoEntry> = protocols.all_supported(); + assert_eq!(true, unsupported.is_some()); + assert_eq!("Wombat=9", &unsupported.unwrap().to_string()); +} + +#[test] +fn protocol_all_supported_with_unsupported_versions() { + let protocols: UnvalidatedProtoEntry = "Link=3-999".parse().unwrap(); + let unsupported: Option<UnvalidatedProtoEntry> = protocols.all_supported(); + assert_eq!(true, unsupported.is_some()); + assert_eq!("Link=6-999", &unsupported.unwrap().to_string()); +} + +#[test] +fn protocol_all_supported_with_unsupported_low_version() { + let protocols: UnvalidatedProtoEntry = "HSIntro=2-3".parse().unwrap(); + let unsupported: Option<UnvalidatedProtoEntry> = protocols.all_supported(); + assert_eq!(true, unsupported.is_some()); + assert_eq!("HSIntro=2", &unsupported.unwrap().to_string()); +} + +#[test] +fn protocol_all_supported_with_unsupported_high_version() { + let protocols: UnvalidatedProtoEntry = "Cons=1-2,999".parse().unwrap(); + let unsupported: Option<UnvalidatedProtoEntry> = protocols.all_supported(); + assert_eq!(true, unsupported.is_some()); + assert_eq!("Cons=999", &unsupported.unwrap().to_string()); +} + +#[test] +fn protocol_all_supported_with_mix_of_supported_and_unsupproted() { + let protocols: UnvalidatedProtoEntry = "Link=3-4 Wombat=9".parse().unwrap(); + let unsupported: Option<UnvalidatedProtoEntry> = protocols.all_supported(); + assert_eq!(true, unsupported.is_some()); + assert_eq!("Wombat=9", &unsupported.unwrap().to_string()); +} + +#[test] +fn protover_string_supports_protocol_returns_true_for_single_supported() { + let protocols: UnvalidatedProtoEntry = "Link=3-4 Cons=1".parse().unwrap(); + let is_supported = protocols.supports_protocol(&protover::Protocol::Cons.into(), &1); + assert_eq!(true, is_supported); +} + +#[test] +fn protover_string_supports_protocol_returns_false_for_single_unsupported() { + let protocols: UnvalidatedProtoEntry = "Link=3-4 Cons=1".parse().unwrap(); + let is_supported = protocols.supports_protocol(&protover::Protocol::Cons.into(), &2); + assert_eq!(false, is_supported); +} + +#[test] +fn protover_string_supports_protocol_returns_false_for_unsupported() { + let protocols: UnvalidatedProtoEntry = "Link=3-4".parse().unwrap(); + let is_supported = protocols.supports_protocol(&protover::Protocol::Cons.into(), &2); + assert_eq!(false, is_supported); +} + +#[test] +#[should_panic] +fn parse_protocol_with_unexpected_characters() { + let _: UnvalidatedProtoEntry = "Cons=*-%".parse().unwrap(); +} + +#[test] +#[should_panic] +fn protover_compute_vote_returns_empty_for_empty_string() { + let protocols: &[UnvalidatedProtoEntry] = &["".parse().unwrap()]; + let listed = ProtoverVote::compute(protocols, &1); + assert_eq!("", listed.to_string()); +} + +#[test] +fn protover_compute_vote_returns_single_protocol_for_matching() { + let protocols: &[UnvalidatedProtoEntry] = &["Cons=1".parse().unwrap()]; + let listed = ProtoverVote::compute(protocols, &1); + assert_eq!("Cons=1", listed.to_string()); +} + +#[test] +fn protover_compute_vote_returns_two_protocols_for_two_matching() { + let protocols: &[UnvalidatedProtoEntry] = &["Link=1 Cons=1".parse().unwrap()]; + let listed = ProtoverVote::compute(protocols, &1); + assert_eq!("Cons=1 Link=1", listed.to_string()); +} + +#[test] +fn protover_compute_vote_returns_one_protocol_when_one_out_of_two_matches() { + let protocols: &[UnvalidatedProtoEntry] = &["Cons=1 Link=2".parse().unwrap(), "Cons=1".parse().unwrap()]; + let listed = ProtoverVote::compute(protocols, &2); + assert_eq!("Cons=1", listed.to_string()); +} + +#[test] +fn protover_compute_vote_returns_protocols_that_it_doesnt_currently_support() { + let protocols: &[UnvalidatedProtoEntry] = &["Foo=1 Cons=2".parse().unwrap(), "Bar=1".parse().unwrap()]; + let listed = ProtoverVote::compute(protocols, &1); + assert_eq!("Bar=1 Cons=2 Foo=1", listed.to_string()); +} + +#[test] +fn protover_compute_vote_returns_matching_for_mix() { + let protocols: &[UnvalidatedProtoEntry] = &["Link=1-10,500 Cons=1,3-7,8".parse().unwrap()]; + let listed = ProtoverVote::compute(protocols, &1); + assert_eq!("Cons=1,3-8 Link=1-10,500", listed.to_string()); +} + +#[test] +fn protover_compute_vote_returns_matching_for_longer_mix() { + let protocols: &[UnvalidatedProtoEntry] = &[ + "Desc=1-10,500 Cons=1,3-7,8".parse().unwrap(), + "Link=123-456,78 Cons=2-6,8 Desc=9".parse().unwrap(), + ]; + + let listed = ProtoverVote::compute(protocols, &1); + assert_eq!("Cons=1-8 Desc=1-10,500 Link=78,123-456", listed.to_string()); +} + +#[test] +fn protover_compute_vote_returns_matching_for_longer_mix_with_threshold_two() { + let protocols: &[UnvalidatedProtoEntry] = &[ + "Desc=1-10,500 Cons=1,3-7,8".parse().unwrap(), + "Link=123-456,78 Cons=2-6,8 Desc=9".parse().unwrap(), + ]; + + let listed = ProtoverVote::compute(protocols, &2); + assert_eq!("Cons=3-6,8 Desc=9", listed.to_string()); +} + +#[test] +fn protover_compute_vote_handles_duplicated_versions() { + let protocols: &[UnvalidatedProtoEntry] = &["Cons=1".parse().unwrap(), "Cons=1".parse().unwrap()]; + assert_eq!("Cons=1", ProtoverVote::compute(protocols, &2).to_string()); + + let protocols: &[UnvalidatedProtoEntry] = &["Cons=1-2".parse().unwrap(), "Cons=1-2".parse().unwrap()]; + assert_eq!("Cons=1-2", ProtoverVote::compute(protocols, &2).to_string()); +} + +#[test] +fn protover_compute_vote_handles_invalid_proto_entries() { + let protocols: &[UnvalidatedProtoEntry] = &[ + "Cons=1".parse().unwrap(), + "Cons=1".parse().unwrap(), + "Dinosaur=1".parse().unwrap(), + ]; + assert_eq!("Cons=1", ProtoverVote::compute(protocols, &2).to_string()); +} + +#[test] +fn parse_protocol_with_single_protocol_and_two_nonsequential_versions() { + let _: ProtoEntry = "Desc=1,2".parse().unwrap(); +} + +#[test] +fn protover_is_supported_here_returns_true_for_supported_protocol() { + assert_eq!(true, protover::is_supported_here(&protover::Protocol::Cons, &1)); +} + +#[test] +fn protover_is_supported_here_returns_false_for_unsupported_protocol() { + assert_eq!(false, protover::is_supported_here(&protover::Protocol::Cons, &5)); +} + +#[test] +fn protocol_all_supported_with_single_proto_and_single_version() { + let protocol: UnvalidatedProtoEntry = "Cons=1".parse().unwrap(); + let unsupported: Option<UnvalidatedProtoEntry> = protocol.all_supported(); + assert_eq!(true, unsupported.is_none()); +} + +#[test] +fn protocol_all_supported_with_single_protocol_and_multiple_versions() { + let protocol: UnvalidatedProtoEntry = "Cons=1-2".parse().unwrap(); + let unsupported: Option<UnvalidatedProtoEntry> = protocol.all_supported(); + assert_eq!(true, unsupported.is_none()); +} + +#[test] +fn protocol_all_supported_with_different_single_protocol_and_single_version() { + let protocol: UnvalidatedProtoEntry = "HSDir=1".parse().unwrap(); + let unsupported: Option<UnvalidatedProtoEntry> = protocol.all_supported(); + assert_eq!(true, unsupported.is_none()); +} + +#[test] +fn protocol_all_supported_with_single_protocol_and_supported_version() { + let protocol: UnvalidatedProtoEntry = "Desc=2".parse().unwrap(); + let unsupported: Option<UnvalidatedProtoEntry> = protocol.all_supported(); + assert_eq!(true, unsupported.is_none()); +} + +#[test] +fn protocol_all_supported_with_two_protocols_and_single_version() { + let protocols: UnvalidatedProtoEntry = "Cons=1 HSDir=1".parse().unwrap(); + let unsupported: Option<UnvalidatedProtoEntry> = protocols.all_supported(); + assert_eq!(true, unsupported.is_none()); +} + +#[test] +fn protocol_all_supported_with_single_protocol_and_two_nonsequential_versions() { + let protocol: UnvalidatedProtoEntry = "Desc=1,2".parse().unwrap(); + let unsupported: Option<UnvalidatedProtoEntry> = protocol.all_supported(); + assert_eq!(true, unsupported.is_none()); +} + +#[test] +fn protocol_all_supported_with_single_protocol_and_two_sequential_versions() { + let protocol: UnvalidatedProtoEntry = "Desc=1-2".parse().unwrap(); + let unsupported: Option<UnvalidatedProtoEntry> = protocol.all_supported(); + assert_eq!(true, unsupported.is_none()); +} + +#[test] +fn protocol_all_supported_with_single_protocol_and_protocol_range() { + let protocol: UnvalidatedProtoEntry = "Link=1-4".parse().unwrap(); + let unsupported: Option<UnvalidatedProtoEntry> = protocol.all_supported(); + assert_eq!(true, unsupported.is_none()); +} + +// By allowing us to add to votes, the C implementation allows us to +// exceed the limit. +#[test] +fn protover_compute_vote_may_exceed_limit() { + let proto1: UnvalidatedProtoEntry = "Sleen=1-65535".parse().unwrap(); + let proto2: UnvalidatedProtoEntry = "Sleen=100000".parse().unwrap(); + + let _result: UnvalidatedProtoEntry = ProtoverVote::compute(&[proto1, proto2], &1); +} + +#[test] +fn protover_all_supported_should_exclude_versions_we_actually_do_support() { + let proto: UnvalidatedProtoEntry = "Link=3-999".parse().unwrap(); + let result: String = proto.all_supported().unwrap().to_string(); + + assert_eq!(result, "Link=6-999".to_string()); +} + +#[test] +fn protover_all_supported_should_exclude_versions_we_actually_do_support_complex1() { + let proto: UnvalidatedProtoEntry = "Link=1-3,345-666".parse().unwrap(); + let result: String = proto.all_supported().unwrap().to_string(); + + assert_eq!(result, "Link=345-666".to_string()); +} + +#[test] +fn protover_all_supported_should_exclude_versions_we_actually_do_support_complex2() { + let proto: UnvalidatedProtoEntry = "Link=1-3,5-12".parse().unwrap(); + let result: String = proto.all_supported().unwrap().to_string(); + + assert_eq!(result, "Link=6-12".to_string()); +} + +#[test] +fn protover_all_supported_should_exclude_some_versions_and_entire_protocols() { + let proto: UnvalidatedProtoEntry = "Link=1-3,5-12 Quokka=9000-9001".parse().unwrap(); + let result: String = proto.all_supported().unwrap().to_string(); + + assert_eq!(result, "Link=6-12 Quokka=9000-9001".to_string()); +} + +#[test] +fn protover_all_supported_should_not_dos_anyones_computer() { + let proto: UnvalidatedProtoEntry = "Link=1-2147483648".parse().unwrap(); + let result: String = proto.all_supported().unwrap().to_string(); + + assert_eq!(result, "Link=6-2147483648".to_string()); +} + +#[test] +fn protover_all_supported_should_not_dos_anyones_computer_max_versions() { + let proto: UnvalidatedProtoEntry = "Link=1-4294967294".parse().unwrap(); + let result: String = proto.all_supported().unwrap().to_string(); + + assert_eq!(result, "Link=6-4294967294".to_string()); +} + +#[test] +// C_RUST_DIFFERS: The C will return true (e.g. saying "yes, that's supported") +// but set the msg to NULL (??? seems maybe potentially bad). The Rust will +// simply return a None. +fn protover_all_supported_should_return_empty_string_for_weird_thing() { + let proto: UnvalidatedProtoEntry = "Fribble=".parse().unwrap(); + let result: Option<UnvalidatedProtoEntry> = proto.all_supported(); + + assert!(result.is_none()); +} + +#[test] +fn protover_unvalidatedprotoentry_should_err_entirely_unparseable_things() { + let proto: Result<UnvalidatedProtoEntry, ProtoverError> = "Fribble".parse(); + + assert_eq!(Err(ProtoverError::Unparseable), proto); +} + +#[test] +fn protover_all_supported_over_maximum_limit() { + let proto: Result<UnvalidatedProtoEntry, ProtoverError> = "Sleen=1-4294967295".parse(); + + assert_eq!(Err(ProtoverError::ExceedsMax), proto); +} diff --git a/src/rust/smartlist/Cargo.toml b/src/rust/smartlist/Cargo.toml new file mode 100644 index 0000000000..6ddcbee8e9 --- /dev/null +++ b/src/rust/smartlist/Cargo.toml @@ -0,0 +1,13 @@ +[package] +authors = ["The Tor Project"] +version = "0.0.1" +name = "smartlist" + +[dependencies] +libc = "0.2.39" + +[lib] +name = "smartlist" +path = "lib.rs" +crate_type = ["rlib", "staticlib"] + diff --git a/src/rust/smartlist/lib.rs b/src/rust/smartlist/lib.rs new file mode 100644 index 0000000000..14a8148315 --- /dev/null +++ b/src/rust/smartlist/lib.rs @@ -0,0 +1,8 @@ +// Copyright (c) 2016-2017, The Tor Project, Inc. */ +// See LICENSE for licensing information */ + +extern crate libc; + +mod smartlist; + +pub use smartlist::*; diff --git a/src/rust/smartlist/smartlist.rs b/src/rust/smartlist/smartlist.rs new file mode 100644 index 0000000000..2a822d89f4 --- /dev/null +++ b/src/rust/smartlist/smartlist.rs @@ -0,0 +1,115 @@ +// Copyright (c) 2016-2017, The Tor Project, Inc. */ +// See LICENSE for licensing information */ + +use std::slice; +use libc::{c_char, c_int}; +use std::ffi::CStr; + +/// Smartlists are a type used in C code in tor to define a collection of a +/// generic type, which has a capacity and a number used. Each Smartlist +/// defines how to extract the list of values from the underlying C structure +/// +/// Implementations are required to have a C representation, as this module +/// serves purely to translate smartlists as defined in tor to vectors in Rust. +pub trait Smartlist<T> { + fn get_list(&self) -> Vec<T>; +} + +#[repr(C)] +pub struct Stringlist { + pub list: *const *const c_char, + pub num_used: c_int, + pub capacity: c_int, +} + +impl Smartlist<String> for Stringlist { + fn get_list(&self) -> Vec<String> { + let empty: Vec<String> = Vec::new(); + let mut rust_list: Vec<String> = Vec::new(); + + if self.list.is_null() || self.num_used == 0 { + return empty; + } + + // unsafe, as we need to extract the smartlist list into a vector of + // pointers, and then transform each element into a Rust string. + let elems: &[*const c_char] = + unsafe { slice::from_raw_parts(self.list, self.num_used as usize) }; + + for elem in elems.iter() { + if elem.is_null() { + continue; + } + + // unsafe, as we need to create a cstring from the referenced + // element + let c_string = unsafe { CStr::from_ptr(*elem) }; + + let r_string = match c_string.to_str() { + Ok(n) => n, + Err(_) => return empty, + }; + + rust_list.push(String::from(r_string)); + } + + rust_list + } +} + +// TODO: CHK: this module maybe should be tested from a test in C with a +// smartlist as defined in tor. +#[cfg(test)] +mod test { + #[test] + fn test_get_list_of_strings() { + extern crate libc; + + use std::ffi::CString; + use libc::c_char; + + use super::Smartlist; + use super::Stringlist; + + { + // test to verify that null pointers are gracefully handled + use std::ptr; + + let sl = Stringlist { + list: ptr::null(), + num_used: 0, + capacity: 0, + }; + + let data = sl.get_list(); + assert_eq!(0, data.len()); + } + + { + let args = vec![String::from("a"), String::from("b")]; + + // for each string, transform it into a CString + let c_strings: Vec<_> = args.iter() + .map(|arg| CString::new(arg.as_str()).unwrap()) + .collect(); + + // then, collect a pointer for each CString + let p_args: Vec<_> = + c_strings.iter().map(|arg| arg.as_ptr()).collect(); + + let p: *const *const c_char = p_args.as_ptr(); + + // This is the representation that we expect when receiving a + // smartlist at the Rust/C FFI layer. + let sl = Stringlist { + list: p, + num_used: 2, + capacity: 2, + }; + + let data = sl.get_list(); + assert_eq!("a", &data[0]); + assert_eq!("b", &data[1]); + } + } +} diff --git a/src/rust/tor_allocate/Cargo.toml b/src/rust/tor_allocate/Cargo.toml new file mode 100644 index 0000000000..468425f115 --- /dev/null +++ b/src/rust/tor_allocate/Cargo.toml @@ -0,0 +1,13 @@ +[package] +authors = ["The Tor Project"] +version = "0.0.1" +name = "tor_allocate" + +[dependencies] +libc = "=0.2.39" + +[lib] +name = "tor_allocate" +path = "lib.rs" +crate_type = ["rlib", "staticlib"] + diff --git a/src/rust/tor_allocate/lib.rs b/src/rust/tor_allocate/lib.rs new file mode 100644 index 0000000000..937a5dcf63 --- /dev/null +++ b/src/rust/tor_allocate/lib.rs @@ -0,0 +1,15 @@ +// Copyright (c) 2016-2017, The Tor Project, Inc. */ +// See LICENSE for licensing information */ + +//! Allocation helper functions that allow data to be allocated in Rust +//! using tor's specified allocator. In doing so, this can be later freed +//! from C. +//! +//! This is currently a temporary solution, we will later use tor's allocator +//! by default for any allocation that occurs in Rust. However, as this will +//! stabalize in 2018, we can use this as a temporary measure. + +extern crate libc; + +mod tor_allocate; +pub use tor_allocate::*; diff --git a/src/rust/tor_allocate/tor_allocate.rs b/src/rust/tor_allocate/tor_allocate.rs new file mode 100644 index 0000000000..3c0037f139 --- /dev/null +++ b/src/rust/tor_allocate/tor_allocate.rs @@ -0,0 +1,108 @@ +// Copyright (c) 2016-2017, The Tor Project, Inc. */ +// See LICENSE for licensing information */ +// No-op defined purely for testing at the module level +use libc::c_char; + +#[cfg(not(feature = "testing"))] +use std::{ptr, slice, mem}; +use libc::c_void; + +// Define a no-op implementation for testing Rust modules without linking to C +#[cfg(feature = "testing")] +pub fn allocate_and_copy_string(s: &String) -> *mut c_char { + use std::ffi::CString; + CString::new(s.as_str()).unwrap().into_raw() +} + +// Defined only for tests, used for testing purposes, so that we don't need +// to link to tor C files. Uses the system allocator +#[cfg(test)] +unsafe extern "C" fn tor_malloc_(size: usize) -> *mut c_void { + use libc::malloc; + malloc(size) +} + +#[cfg(all(not(test), not(feature = "testing")))] +extern "C" { + fn tor_malloc_(size: usize) -> *mut c_void; +} + +/// Allocate memory using tor_malloc_ and copy an existing string into the +/// allocated buffer, returning a pointer that can later be called in C. +/// +/// # Inputs +/// +/// * `src`, a reference to a String. +/// +/// # Returns +/// +/// A `*mut c_char` that should be freed by tor_free in C +/// +#[cfg(not(feature = "testing"))] +pub fn allocate_and_copy_string(src: &String) -> *mut c_char { + let bytes: &[u8] = src.as_bytes(); + + let size = mem::size_of_val::<[u8]>(bytes); + let size_one_byte = mem::size_of::<u8>(); + + // handle integer overflow when adding one to the calculated length + let size_with_null_byte = match size.checked_add(size_one_byte) { + Some(n) => n, + None => return ptr::null_mut(), + }; + + let dest = unsafe { tor_malloc_(size_with_null_byte) as *mut u8 }; + + if dest.is_null() { + return ptr::null_mut(); + } + + unsafe { ptr::copy_nonoverlapping(bytes.as_ptr(), dest, size) }; + + // set the last byte as null, using the ability to index into a slice + // rather than doing pointer arithmatic + let slice = unsafe { slice::from_raw_parts_mut(dest, size_with_null_byte) }; + slice[size] = 0; // add a null terminator + + dest as *mut c_char +} + +#[cfg(test)] +mod test { + + #[test] + fn test_allocate_and_copy_string_with_empty() { + use std::ffi::CStr; + use libc::{free, c_void}; + + use tor_allocate::allocate_and_copy_string; + + let empty = String::new(); + let allocated_empty = allocate_and_copy_string(&empty); + + let allocated_empty_rust = + unsafe { CStr::from_ptr(allocated_empty).to_str().unwrap() }; + + assert_eq!("", allocated_empty_rust); + + unsafe { free(allocated_empty as *mut c_void) }; + } + + #[test] + fn test_allocate_and_copy_string_with_not_empty_string() { + use std::ffi::CStr; + use libc::{free, c_void}; + + use tor_allocate::allocate_and_copy_string; + + let empty = String::from("foo bar biz"); + let allocated_empty = allocate_and_copy_string(&empty); + + let allocated_empty_rust = + unsafe { CStr::from_ptr(allocated_empty).to_str().unwrap() }; + + assert_eq!("foo bar biz", allocated_empty_rust); + + unsafe { free(allocated_empty as *mut c_void) }; + } +} diff --git a/src/rust/tor_log/Cargo.toml b/src/rust/tor_log/Cargo.toml new file mode 100644 index 0000000000..9d06299c05 --- /dev/null +++ b/src/rust/tor_log/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "tor_log" +version = "0.1.0" +authors = ["The Tor Project"] + +[lib] +name = "tor_log" +path = "lib.rs" +crate_type = ["rlib", "staticlib"] + +[features] + +[dependencies] +libc = "0.2.39" + +[dependencies.tor_allocate] +path = "../tor_allocate" diff --git a/src/rust/tor_log/lib.rs b/src/rust/tor_log/lib.rs new file mode 100644 index 0000000000..72f9e38339 --- /dev/null +++ b/src/rust/tor_log/lib.rs @@ -0,0 +1,16 @@ +//! Copyright (c) 2016-2017, The Tor Project, Inc. */ +//! See LICENSE for licensing information */ + +//! Logging wrapper for Rust to utilize Tor's logger, found at +//! src/common/log.c and src/common/torlog.h +//! +//! Exposes different interfaces depending on whether we are running in test +//! or non-test mode. When testing, we use a no-op implementation, +//! otherwise we link directly to C. + +extern crate libc; +extern crate tor_allocate; + +mod tor_log; + +pub use tor_log::*; diff --git a/src/rust/tor_log/tor_log.rs b/src/rust/tor_log/tor_log.rs new file mode 100644 index 0000000000..ad6725f0f2 --- /dev/null +++ b/src/rust/tor_log/tor_log.rs @@ -0,0 +1,270 @@ +// Copyright (c) 2016-2017, The Tor Project, Inc. */ +// See LICENSE for licensing information */ + +// Note that these functions are untested due to the fact that there are no +// return variables to test and they are calling into a C API. + +/// The related domain which the logging message is relevant. For example, +/// log messages relevant to networking would use LogDomain::LdNet, whereas +/// general messages can use LdGeneral. +#[derive(Eq, PartialEq)] +pub enum LogDomain { + Net, + General, +} + +/// The severity level at which to log messages. +#[derive(Eq, PartialEq)] +pub enum LogSeverity { + Notice, + Warn, +} + +/// Main entry point for Rust modules to log messages. +/// +/// # Inputs +/// +/// * A `severity` of type LogSeverity, which defines the level of severity the +/// message will be logged. +/// * A `domain` of type LogDomain, which defines the domain the log message +/// will be associated with. +/// * A `function` of type &str, which defines the name of the function where +/// the message is being logged. There is a current RFC for a macro that +/// defines function names. When it is, we should use it. See +/// https://github.com/rust-lang/rfcs/pull/1719 +/// * A `message` of type &str, which is the log message itself. +#[macro_export] +macro_rules! tor_log_msg { + ($severity: path, + $domain: path, + $function: expr, + $($message:tt)*) => + { + { + let msg = format!($($message)*); + $crate::tor_log_msg_impl($severity, $domain, $function, msg) + } + }; +} + +#[inline] +pub fn tor_log_msg_impl( + severity: LogSeverity, + domain: LogDomain, + function: &str, + message: String, +) { + use std::ffi::CString; + + /// Default function name to log in case of errors when converting + /// a function name to a CString + const ERR_LOG_FUNCTION: &str = "tor_log_msg"; + + /// Default message to log in case of errors when converting a log + /// message to a CString + const ERR_LOG_MSG: &str = "Unable to log message from Rust \ + module due to error when converting to CString"; + + let func = match CString::new(function) { + Ok(n) => n, + Err(_) => CString::new(ERR_LOG_FUNCTION).unwrap(), + }; + + let msg = match CString::new(message) { + Ok(n) => n, + Err(_) => CString::new(ERR_LOG_MSG).unwrap(), + }; + + // Bind to a local variable to preserve ownership. This is essential so + // that ownership is guaranteed until these local variables go out of scope + let func_ptr = func.as_ptr(); + let msg_ptr = msg.as_ptr(); + + let c_severity = unsafe { log::translate_severity(severity) }; + let c_domain = unsafe { log::translate_domain(domain) }; + + unsafe { log::tor_log_string(c_severity, c_domain, func_ptr, msg_ptr) } +} + +/// This implementation is used when compiling for actual use, as opposed to +/// testing. +#[cfg(not(test))] +pub mod log { + use libc::{c_char, c_int}; + use super::LogDomain; + use super::LogSeverity; + + /// Severity log types. These mirror definitions in /src/common/torlog.h + /// C_RUST_COUPLED: src/common/log.c, log domain types + extern "C" { + static LOG_WARN_: c_int; + static LOG_NOTICE_: c_int; + } + + /// Domain log types. These mirror definitions in /src/common/torlog.h + /// C_RUST_COUPLED: src/common/log.c, log severity types + extern "C" { + static LD_NET_: u32; + static LD_GENERAL_: u32; + } + + /// Translate Rust defintions of log domain levels to C. This exposes a 1:1 + /// mapping between types. + #[inline] + pub unsafe fn translate_domain(domain: LogDomain) -> u32 { + match domain { + LogDomain::Net => LD_NET_, + LogDomain::General => LD_GENERAL_, + } + } + + /// Translate Rust defintions of log severity levels to C. This exposes a + /// 1:1 mapping between types. + #[inline] + pub unsafe fn translate_severity(severity: LogSeverity) -> c_int { + match severity { + LogSeverity::Warn => LOG_WARN_, + LogSeverity::Notice => LOG_NOTICE_, + } + } + + /// The main entry point into Tor's logger. When in non-test mode, this + /// will link directly with `tor_log_string` in /src/or/log.c + extern "C" { + pub fn tor_log_string( + severity: c_int, + domain: u32, + function: *const c_char, + string: *const c_char, + ); + } +} + +/// This module exposes no-op functionality for testing other Rust modules +/// without linking to C. +#[cfg(test)] +pub mod log { + use libc::{c_char, c_int}; + use super::LogDomain; + use super::LogSeverity; + + pub static mut LAST_LOGGED_FUNCTION: *mut String = 0 as *mut String; + pub static mut LAST_LOGGED_MESSAGE: *mut String = 0 as *mut String; + + pub unsafe fn tor_log_string( + _severity: c_int, + _domain: u32, + function: *const c_char, + message: *const c_char, + ) { + use std::ffi::CStr; + + let f = CStr::from_ptr(function); + let fct = match f.to_str() { + Ok(n) => n, + Err(_) => "", + }; + LAST_LOGGED_FUNCTION = Box::into_raw(Box::new(String::from(fct))); + + let m = CStr::from_ptr(message); + let msg = match m.to_str() { + Ok(n) => n, + Err(_) => "", + }; + LAST_LOGGED_MESSAGE = Box::into_raw(Box::new(String::from(msg))); + } + + pub unsafe fn translate_domain(_domain: LogDomain) -> u32 { + 1 + } + + pub unsafe fn translate_severity(_severity: LogSeverity) -> c_int { + 1 + } +} + +#[cfg(test)] +mod test { + use tor_log::*; + use tor_log::log::{LAST_LOGGED_FUNCTION, LAST_LOGGED_MESSAGE}; + + #[test] + fn test_get_log_message() { + { + fn test_macro() { + tor_log_msg!( + LogSeverity::Warn, + LogDomain::Net, + "test_macro", + "test log message {}", + "a", + ); + } + + test_macro(); + + let function = unsafe { Box::from_raw(LAST_LOGGED_FUNCTION) }; + assert_eq!("test_macro", *function); + + let message = unsafe { Box::from_raw(LAST_LOGGED_MESSAGE) }; + assert_eq!("test log message a", *message); + } + + // test multiple inputs into the log message + { + fn test_macro() { + tor_log_msg!( + LogSeverity::Warn, + LogDomain::Net, + "next_test_macro", + "test log message {} {} {} {} {}", + 1, + 2, + 3, + 4, + 5 + ); + } + + test_macro(); + + let function = unsafe { Box::from_raw(LAST_LOGGED_FUNCTION) }; + assert_eq!("next_test_macro", *function); + + let message = unsafe { Box::from_raw(LAST_LOGGED_MESSAGE) }; + assert_eq!("test log message 1 2 3 4 5", *message); + } + + // test how a long log message will be formatted + { + fn test_macro() { + tor_log_msg!( + LogSeverity::Warn, + LogDomain::Net, + "test_macro", + "{}", + "All the world's a stage, and all the men and women \ + merely players: they have their exits and their \ + entrances; and one man in his time plays many parts, his \ + acts being seven ages." + ); + } + + test_macro(); + + let expected_string = "All the world's a \ + stage, and all the men \ + and women merely players: \ + they have their exits and \ + their entrances; and one man \ + in his time plays many parts, \ + his acts being seven ages."; + + let function = unsafe { Box::from_raw(LAST_LOGGED_FUNCTION) }; + assert_eq!("test_macro", *function); + + let message = unsafe { Box::from_raw(LAST_LOGGED_MESSAGE) }; + assert_eq!(expected_string, *message); + } + } +} diff --git a/src/rust/tor_rust/Cargo.toml b/src/rust/tor_rust/Cargo.toml new file mode 100644 index 0000000000..86fad3ee76 --- /dev/null +++ b/src/rust/tor_rust/Cargo.toml @@ -0,0 +1,16 @@ +[package] +authors = ["The Tor Project"] +name = "tor_rust" +version = "0.1.0" + +[lib] +name = "tor_rust" +path = "lib.rs" +crate_type = ["rlib", "staticlib"] + +[dependencies.tor_util] +path = "../tor_util" + +[dependencies.protover] +path = "../protover" + diff --git a/src/rust/tor_rust/include.am b/src/rust/tor_rust/include.am new file mode 100644 index 0000000000..99f3ede653 --- /dev/null +++ b/src/rust/tor_rust/include.am @@ -0,0 +1,28 @@ +EXTRA_DIST +=\ + src/rust/tor_rust/Cargo.toml \ + src/rust/tor_rust/lib.rs + +EXTRA_CARGO_OPTIONS= + +@TOR_RUST_LIB_PATH@: FORCE + ( cd "$(abs_top_builddir)/src/rust" ; \ + CARGO_TARGET_DIR="$(abs_top_builddir)/src/rust/target" \ + $(CARGO) build --release $(EXTRA_CARGO_OPTIONS) \ + $(CARGO_ONLINE) \ + --manifest-path "$(abs_top_srcdir)/src/rust/tor_rust/Cargo.toml" ) + +distclean-rust: + ( cd "$(abs_top_builddir)/src/rust" ; \ + CARGO_TARGET_DIR="$(abs_top_builddir)/src/rust/target" \ + $(CARGO) clean $(EXTRA_CARGO_OPTIONS) \ + $(CARGO_ONLINE) \ + --manifest-path "$(abs_top_srcdir)/src/rust/tor_rust/Cargo.toml" ) + rm -rf "$(abs_top_builddir)/src/rust/registry" + +if USE_RUST +build-rust: @TOR_RUST_LIB_PATH@ +else +build-rust: +endif + +FORCE: diff --git a/src/rust/tor_rust/lib.rs b/src/rust/tor_rust/lib.rs new file mode 100644 index 0000000000..c1585c0480 --- /dev/null +++ b/src/rust/tor_rust/lib.rs @@ -0,0 +1,5 @@ +extern crate tor_util; +extern crate protover; + +pub use tor_util::*; +pub use protover::*; diff --git a/src/rust/tor_util/Cargo.toml b/src/rust/tor_util/Cargo.toml new file mode 100644 index 0000000000..a606a280b2 --- /dev/null +++ b/src/rust/tor_util/Cargo.toml @@ -0,0 +1,19 @@ +[package] +authors = ["The Tor Project"] +name = "tor_util" +version = "0.0.1" + +[lib] +name = "tor_util" +path = "lib.rs" +crate_type = ["rlib", "staticlib"] + +[dependencies.tor_allocate] +path = "../tor_allocate" + +[dependencies.tor_log] +path = "../tor_log" + +[dependencies] +libc = "=0.2.39" + diff --git a/src/rust/tor_util/ffi.rs b/src/rust/tor_util/ffi.rs new file mode 100644 index 0000000000..32779ed476 --- /dev/null +++ b/src/rust/tor_util/ffi.rs @@ -0,0 +1,27 @@ +// Copyright (c) 2016-2017, The Tor Project, Inc. */ +// See LICENSE for licensing information */ + +//! FFI functions to announce Rust support during tor startup, only to be +//! called from C. +//! + +use tor_log::{LogSeverity, LogDomain}; + +/// Returns a short string to announce Rust support during startup. +/// +/// # Examples +/// ```c +/// char *rust_str = rust_welcome_string(); +/// printf("%s", rust_str); +/// tor_free(rust_str); +/// ``` +#[no_mangle] +pub extern "C" fn rust_log_welcome_string() { + tor_log_msg!( + LogSeverity::Notice, + LogDomain::General, + "rust_log_welcome_string", + "Tor is running with Rust integration. Please report \ + any bugs you encounter." + ); +} diff --git a/src/rust/tor_util/lib.rs b/src/rust/tor_util/lib.rs new file mode 100644 index 0000000000..94697b6069 --- /dev/null +++ b/src/rust/tor_util/lib.rs @@ -0,0 +1,14 @@ +// Copyright (c) 2016-2017, The Tor Project, Inc. */ +// See LICENSE for licensing information */ + +//! Small module to announce Rust support during startup for demonstration +//! purposes. + +extern crate libc; +extern crate tor_allocate; + +#[macro_use] +extern crate tor_log; + +pub mod ffi; +pub mod strings; diff --git a/src/rust/tor_util/strings.rs b/src/rust/tor_util/strings.rs new file mode 100644 index 0000000000..505191d913 --- /dev/null +++ b/src/rust/tor_util/strings.rs @@ -0,0 +1,140 @@ +// Copyright (c) 2016-2017, The Tor Project, Inc. */ +// See LICENSE for licensing information */ + +//! Utilities for working with static strings. + +/// Create a `CStr` from a literal byte slice, appending a NUL byte to it first. +/// +/// # Warning +/// +/// The literal byte slice which is taken as an argument *MUST NOT* have any NUL +/// bytes (`b"\0"`) in it, anywhere, or else an empty string will be returned +/// (`CStr::from_bytes_with_nul_unchecked(b"\0")`) so as to avoid `panic!()`ing. +/// +/// # Examples +/// +/// ``` +/// #[macro_use] +/// extern crate tor_util; +/// +/// use std::ffi::CStr; +/// +/// # fn do_test() -> Result<&'static CStr, &'static str> { +/// let message: &'static str = "This is a test of the tsunami warning system."; +/// let tuesday: &'static CStr; +/// let original: &str; +/// +/// tuesday = cstr!("This is a test of the tsunami warning system."); +/// original = tuesday.to_str().or(Err("Couldn't unwrap CStr!"))?; +/// +/// assert!(original == message); +/// # +/// # Ok(tuesday) +/// # } +/// # fn main() { +/// # do_test(); // so that we can use the ? operator in the test +/// # } +/// ``` +/// It is also possible to pass several string literals to this macro. They +/// will be concatenated together in the order of the arguments, unmodified, +/// before finally being suffixed with a NUL byte: +/// +/// ``` +/// #[macro_use] +/// extern crate tor_util; +/// # +/// # use std::ffi::CStr; +/// # +/// # fn do_test() -> Result<&'static CStr, &'static str> { +/// +/// let quux: &'static CStr = cstr!("foo", "bar", "baz"); +/// let orig: &'static str = quux.to_str().or(Err("Couldn't unwrap CStr!"))?; +/// +/// assert!(orig == "foobarbaz"); +/// # Ok(quux) +/// # } +/// # fn main() { +/// # do_test(); // so that we can use the ? operator in the test +/// # } +/// ``` +/// This is useful for passing static strings to C from Rust FFI code. To do so +/// so, use the `.as_ptr()` method on the resulting `&'static CStr` to convert +/// it to the Rust equivalent of a C `const char*`: +/// +/// ``` +/// #[macro_use] +/// extern crate tor_util; +/// +/// use std::ffi::CStr; +/// use std::os::raw::c_char; +/// +/// pub extern "C" fn give_static_borrowed_string_to_c() -> *const c_char { +/// let hello: &'static CStr = cstr!("Hello, language my parents wrote."); +/// +/// hello.as_ptr() +/// } +/// # fn main() { +/// # let greetings = give_static_borrowed_string_to_c(); +/// # } +/// ``` +/// Note that the C code this static borrowed string is passed to *MUST NOT* +/// attempt to free the memory for the string. +/// +/// # Note +/// +/// An unfortunate limitation of the rustc compiler (as of 1.25.0-nightly), is +/// that the first example above compiles, but if we were to change the +/// assignment of `tuesday` as follows, it will fail to compile, because Rust +/// macros are expanded at parse time, and at parse time there is no symbol +/// table available. +/// +/// ```ignore +/// tuesday = cstr!(message); +/// ``` +/// with the error message `error: expected a literal`. +/// +/// # Returns +/// +/// If the string literals passed as arguments contain no NUL bytes anywhere, +/// then an `&'static CStr` containing the (concatenated) bytes of the string +/// literal(s) passed as arguments, with a NUL byte appended, is returned. +/// Otherwise, an `&'static CStr` containing a single NUL byte is returned (an +/// "empty" string in C). +#[macro_export] +macro_rules! cstr { + ($($bytes:expr),*) => ( + ::std::ffi::CStr::from_bytes_with_nul( + concat!($($bytes),*, "\0").as_bytes() + ).unwrap_or( + unsafe{ + ::std::ffi::CStr::from_bytes_with_nul_unchecked(b"\0") + } + ) + ) +} + +#[cfg(test)] +mod test { + use std::ffi::CStr; + + #[test] + fn cstr_macro() { + let _: &'static CStr = cstr!("boo"); + } + + #[test] + fn cstr_macro_multi_input() { + let quux: &'static CStr = cstr!("foo", "bar", "baz"); + + assert!(quux.to_str().unwrap() == "foobarbaz"); + } + + #[test] + fn cstr_macro_bad_input() { + let waving: &'static CStr = cstr!("waving not drowning o/"); + let drowning: &'static CStr = cstr!("\0 drowning not waving"); + + assert!(waving.to_str().unwrap() == "waving not drowning o/"); + assert!(drowning.to_str().unwrap() == "") + } +} |