From 05f0127ae6a9162cb6b9e0633a1df94ead8e2e6b Mon Sep 17 00:00:00 2001 From: Casey Marshall Date: Sun, 30 Apr 2023 19:56:06 -0500 Subject: [PATCH 1/2] Squashed 'vendor/torut/' content from commit 2fec6d1 git-subtree-dir: vendor/torut git-subtree-split: 2fec6d1426b6bc615e7e30b89221b77ab4634620 --- .gitignore | 8 + .travis.yml | 13 + Cargo.toml | 47 + LICENSE.MD | 5 + README.MD | 26 + TODO | 9 + examples/cookie_authenticate.rs | 31 + examples/get_shared_random.rs | 39 + examples/make_onion_v3.rs | 47 + examples/run_tor.rs | 35 + src/control/conn/authenticated_conn.rs | 1108 ++++++++++++++++++++++ src/control/conn/conn.rs | 399 ++++++++ src/control/conn/mod.rs | 7 + src/control/conn/unauthenticated_conn.rs | 555 +++++++++++ src/control/mod.rs | 8 + src/control/primitives/auth.rs | 129 +++ src/control/primitives/error.rs | 65 ++ src/control/primitives/event.rs | 268 ++++++ src/control/primitives/mod.rs | 10 + src/control/primitives/signal.rs | 77 ++ src/fuzz.rs | 67 ++ src/lib.rs | 27 + src/onion/builder.rs | 84 ++ src/onion/common.rs | 50 + src/onion/mod.rs | 13 + src/onion/v3/key.rs | 156 +++ src/onion/v3/mod.rs | 11 + src/onion/v3/onion.rs | 199 ++++ src/onion/v3/serde_key.rs | 94 ++ src/onion/v3/serde_onion.rs | 24 + src/utils/connect.rs | 1 + src/utils/key_value.rs | 80 ++ src/utils/mod.rs | 161 ++++ src/utils/quoted.rs | 343 +++++++ src/utils/run.rs | 134 +++ src/utils/testing.rs | 34 + test_with_tor.sh | 2 + 37 files changed, 4366 insertions(+) create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 Cargo.toml create mode 100644 LICENSE.MD create mode 100644 README.MD create mode 100644 TODO create mode 100644 examples/cookie_authenticate.rs create mode 100644 examples/get_shared_random.rs create mode 100644 examples/make_onion_v3.rs create mode 100644 examples/run_tor.rs create mode 100644 src/control/conn/authenticated_conn.rs create mode 100644 src/control/conn/conn.rs create mode 100644 src/control/conn/mod.rs create mode 100644 src/control/conn/unauthenticated_conn.rs create mode 100644 src/control/mod.rs create mode 100644 src/control/primitives/auth.rs create mode 100644 src/control/primitives/error.rs create mode 100644 src/control/primitives/event.rs create mode 100644 src/control/primitives/mod.rs create mode 100644 src/control/primitives/signal.rs create mode 100644 src/fuzz.rs create mode 100644 src/lib.rs create mode 100644 src/onion/builder.rs create mode 100644 src/onion/common.rs create mode 100644 src/onion/mod.rs create mode 100644 src/onion/v3/key.rs create mode 100644 src/onion/v3/mod.rs create mode 100644 src/onion/v3/onion.rs create mode 100644 src/onion/v3/serde_key.rs create mode 100644 src/onion/v3/serde_onion.rs create mode 100644 src/utils/connect.rs create mode 100644 src/utils/key_value.rs create mode 100644 src/utils/mod.rs create mode 100644 src/utils/quoted.rs create mode 100644 src/utils/run.rs create mode 100644 src/utils/testing.rs create mode 100755 test_with_tor.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0ef5edf --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +/target +**/*.rs.bk +Cargo.lock +/fuzz +/.idea +/.devcontainer +/.vscode + diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..dae2be8 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,13 @@ +language: rust +rust: + - stable + - beta + - nightly +script: + - cargo build --verbose --all + - cargo test --verbose --all +jobs: + allow_failures: + - nightly + fast_finish: true +cache: cargo \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..e3b21e6 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,47 @@ +[package] +name = "torut" +version = "0.2.1" +authors = ["teawithsand "] +edition = "2018" +license = "MIT" +description = "torut is reasonable tor controller written in rust utilizing tokio for async communication" +repository = "https://github.com/teawithsand/torut" +homepage = "https://github.com/teawithsand/torut" +readme = "README.MD" +keywords = ["tor", "controller", "tokio", "onion"] + +[features] +default = ["serialize", "v3", "control"] +serialize = ["serde", "serde_derive", "base32", "base64"] +control = ["tokio", "rand", "hex", "sha2", "hmac"] +v3 = ["rand", "ed25519-dalek", "base32", "base64", "sha3"] + +[badges] +travis-ci = { repository = "teawithsand/torut", branch = "master" } +maintenance = { status = "passively-maintained" } + +[dependencies] +serde = { version = "1.0", optional = true } +serde_derive = { version = "1.0", optional = true } + +derive_more = "0.99" + +sha3 = { version = "0.9", optional = true } # for onion service v3 signature +sha2 = { version = "0.9", optional = true } # for ed25519-dalek key +hmac = { version = "0.11", optional = true } # for authentication with tor controller + +ed25519-dalek = { version = "1", optional = true } +rand = { version = "0.7", optional = true } +base32 = { version = "0.4", optional = true } +base64 = { version = "0.13", optional = true } +hex = { version = "0.4", optional = true } + +tokio = { version = "1", features = ["io-util"], optional = true } + +# for fuzzing right now +# TODO(reawithsand): fix it somehow +# tokio = { version = "0.3", optional = true, features = ["full"] } + +[dev-dependencies] +serde_json = "1.0" +tokio = { version = "1", features = ["full"] } diff --git a/LICENSE.MD b/LICENSE.MD new file mode 100644 index 0000000..1f95d26 --- /dev/null +++ b/LICENSE.MD @@ -0,0 +1,5 @@ +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.MD b/README.MD new file mode 100644 index 0000000..12b81ce --- /dev/null +++ b/README.MD @@ -0,0 +1,26 @@ +# torut +[![Build Status](https://travis-ci.org/teawithsand/torut.svg?branch=master)](https://travis-ci.org/teawithsand/torut) + +Torut is tor controller written in rust similar to +[stem](https://stem.torproject.org/) or [bine](https://github.com/cretz/bine). +It tries to reasonably implement [specification of tor control port proto](https://gitweb.torproject.org/torspec.git/tree/control-spec.txt) +It works asynchronously with tokio and async/await. + +It implements onion service key and address generation and serialization on its own without tor process. + +Right now logic is quite tightly coupled with tokio so there is no way to +remove tokio from dependencies and make all functions synchronous. + +## Status of onion service V2 +Code handling V2 services has been removed in 0.2 release, since tor project removed(should have?) v2 handling code +from their codebase as well. +See [This page](https://blog.torproject.org/v2-deprecation-timeline) + +# Testing +Tests in torut are split into two parts: +these which do use tor and these which do not use tor. +In order to enable tests which use tor use `RUSTFLAGS="--cfg=testtor"` +and provide `TORUT_TESTING_TOR_BINARY` environment variable containing path to tor binary. +Testing tor binary MUST be run with `--test-threads=1` for instance like: + +`$ RUSTFLAGS="--cfg testtor" cargo test -- --test-threads=1` \ No newline at end of file diff --git a/TODO b/TODO new file mode 100644 index 0000000..d15e51d --- /dev/null +++ b/TODO @@ -0,0 +1,9 @@ +Note: there are TODOs in code as well + +Figure out how to make tests utilizing tor process run with CI(is this possible?) +At least create a docker container so testing with tor is reasonably predictable + +Cleanup pub(crate) for fuzzing functions. Create modules exporting fuzzing stuff and then reexport them in src/fuzz.rs + +Add tests for conditional compilation features on travis and some bash file + So one can test if code compiles on all configurations of features enabled not only default or all features \ No newline at end of file diff --git a/examples/cookie_authenticate.rs b/examples/cookie_authenticate.rs new file mode 100644 index 0000000..a22b094 --- /dev/null +++ b/examples/cookie_authenticate.rs @@ -0,0 +1,31 @@ +use torut::utils::{run_tor, AutoKillChild}; +use torut::control::{UnauthenticatedConn}; +use tokio::net::TcpStream; + +#[tokio::main] +async fn main() { + // testing port is 47835 + // it must be free + + let child = run_tor(std::env::var("TORUT_TOR_BINARY").unwrap(), &mut [ + "--DisableNetwork", "1", + "--ControlPort", "47835", + "--CookieAuthentication", "1", + ].iter()).expect("Starting tor filed"); + let _child = AutoKillChild::new(child); + + let s = TcpStream::connect(&format!("127.0.0.1:{}", 47835)).await.unwrap(); + let mut utc = UnauthenticatedConn::new(s); + let proto_info = utc.load_protocol_info().await.unwrap(); + let ad = proto_info.make_auth_data().unwrap().unwrap(); + + utc.authenticate(&ad).await.unwrap(); + let mut ac = utc.into_authenticated().await; + ac.set_async_event_handler(Some(|_| { + async move { Ok(()) } + })); + + ac.take_ownership().await.unwrap(); + + println!("Now we can use tor conn. We are now forced to use cookie auth due to different tor config."); +} \ No newline at end of file diff --git a/examples/get_shared_random.rs b/examples/get_shared_random.rs new file mode 100644 index 0000000..a968400 --- /dev/null +++ b/examples/get_shared_random.rs @@ -0,0 +1,39 @@ +use torut::utils::{run_tor, AutoKillChild}; +use torut::control::{UnauthenticatedConn, TorAuthMethod, TorAuthData}; +use tokio::net::TcpStream; + +use std::thread::sleep; +use std::time::Duration; + +#[tokio::main] +async fn main() { + // testing port is 47835 + // it must be free + + let child = run_tor( std::env::var("TORUT_TOR_BINARY").unwrap(), &mut [ + // "--DisableNetwork", "1", + "--ControlPort", "47835", + // "--CookieAuthentication", "1", + ].iter()).expect("Starting tor filed"); + let _child = AutoKillChild::new(child); + + let s = TcpStream::connect(&format!("127.0.0.1:{}", 47835)).await.unwrap(); + let mut utc = UnauthenticatedConn::new(s); + let proto_info = utc.load_protocol_info().await.unwrap(); + + assert!(proto_info.auth_methods.contains(&TorAuthMethod::Null), "Null authentication is not allowed"); + utc.authenticate(&TorAuthData::Null).await.unwrap(); + let mut ac = utc.into_authenticated().await; + ac.set_async_event_handler(Some(|_| { + async move { Ok(()) } + })); + + ac.take_ownership().await.unwrap(); + + loop { + println!("getting shared random value..."); + let shared_random = ac.get_info("sr/previous").await.unwrap(); + println!("sr: {}", shared_random); + sleep(Duration::new(1, 0)); + } +} \ No newline at end of file diff --git a/examples/make_onion_v3.rs b/examples/make_onion_v3.rs new file mode 100644 index 0000000..6ad1c93 --- /dev/null +++ b/examples/make_onion_v3.rs @@ -0,0 +1,47 @@ +use torut::utils::{run_tor, AutoKillChild}; +use torut::control::{UnauthenticatedConn, TorAuthMethod, TorAuthData}; +use tokio::net::TcpStream; + +use std::net::{SocketAddr, IpAddr, Ipv4Addr}; + +#[tokio::main] +async fn main() { + // testing port is 47835 + // it must be free + + let child = run_tor(std::env::var("TORUT_TOR_BINARY").unwrap(), &mut [ + "--DisableNetwork", "1", + "--ControlPort", "47835", + // "--CookieAuthentication", "1", + ].iter()).expect("Starting tor filed"); + let _child = AutoKillChild::new(child); + + let s = TcpStream::connect(&format!("127.0.0.1:{}", 47835)).await.unwrap(); + let mut utc = UnauthenticatedConn::new(s); + let proto_info = utc.load_protocol_info().await.unwrap(); + + assert!(proto_info.auth_methods.contains(&TorAuthMethod::Null), "Null authentication is not allowed"); + utc.authenticate(&TorAuthData::Null).await.unwrap(); + let mut ac = utc.into_authenticated().await; + ac.set_async_event_handler(Some(|_| { + async move { Ok(()) } + })); + + ac.take_ownership().await.unwrap(); + + let key = torut::onion::TorSecretKeyV3::generate(); + println!("Generated new onion service v3 key for address: {}", key.public().get_onion_address()); + + println!("Adding onion service v3..."); + ac.add_onion_v3(&key, false, false, false, None, &mut [ + (15787, SocketAddr::new(IpAddr::from(Ipv4Addr::new(127,0,0,1)), 15787)), + ].iter()).await.unwrap(); + println!("Added onion service v3!"); + + println!("Now after enabling network clients should be able to connect to this port"); + + println!("Deleting created onion service..."); + // delete onion service so it works no more + ac.del_onion(&key.public().get_onion_address().get_address_without_dot_onion()).await.unwrap(); + println!("Deleted created onion service! It runs no more!"); +} \ No newline at end of file diff --git a/examples/run_tor.rs b/examples/run_tor.rs new file mode 100644 index 0000000..031c7b8 --- /dev/null +++ b/examples/run_tor.rs @@ -0,0 +1,35 @@ +use torut::utils::{run_tor, AutoKillChild}; +use torut::control::{UnauthenticatedConn, TorAuthMethod, TorAuthData}; +use tokio::net::TcpStream; + +#[tokio::main] +async fn main() { + // testing port is 47835 + // it must be free + + let child = run_tor(std::env::var("TORUT_TOR_BINARY").unwrap(), &mut [ + "--DisableNetwork", "1", + "--ControlPort", "47835", + // "--CookieAuthentication", "1", + ].iter()).expect("Starting tor filed"); + let _child = AutoKillChild::new(child); + + let s = TcpStream::connect(&format!("127.0.0.1:{}", 47835)).await.unwrap(); + let mut utc = UnauthenticatedConn::new(s); + let proto_info = utc.load_protocol_info().await.unwrap(); + + assert!(proto_info.auth_methods.contains(&TorAuthMethod::Null), "Null authentication is not allowed"); + utc.authenticate(&TorAuthData::Null).await.unwrap(); + let mut ac = utc.into_authenticated().await; + ac.set_async_event_handler(Some(|_| { + async move { Ok(()) } + })); + + ac.take_ownership().await.unwrap(); + + let socksport = ac.get_info_unquote("net/listeners/socks").await.unwrap(); + println!("Tor is running now. It's socks port is listening(or not) on: {:?} but it's not connected to the network because DisableNetwork is set", socksport); + + let controlport = ac.get_info_unquote("net/listeners/control").await.unwrap(); + println!("Tor is running now. It's control port listening on: {:?}", controlport); +} \ No newline at end of file diff --git a/src/control/conn/authenticated_conn.rs b/src/control/conn/authenticated_conn.rs new file mode 100644 index 0000000..47048b3 --- /dev/null +++ b/src/control/conn/authenticated_conn.rs @@ -0,0 +1,1108 @@ +use std::borrow::Cow; +use std::collections::{HashMap, HashSet}; +use std::future::Future; +use std::net::{Ipv4Addr, SocketAddr}; +use tokio::io::{AsyncRead, AsyncWrite}; + +use crate::control::conn::{AuthenticatedConnError, Conn, ConnError}; +use crate::control::primitives::AsyncEvent; +use crate::utils::{is_valid_event, is_valid_hostname, is_valid_keyword, is_valid_option, parse_single_key_value, quote_string, unquote_string}; + +/// AuthenticatedConn represents connection to TorCP after it has been authenticated so one may +/// perform various operations on it. +/// +/// This connection is aware of asynchronous events which may occur sometimes. +/// +/// It wraps `Conn`. +/// +/// # Async event handling +/// AuthenticatedConn automatically recognises and treats differently response for given request and asynchronous +/// event response. +/// If it receives an asynchronous event it will invoke async event handler(if some). +/// Asynchronous handler will be awaited in current thread(no calls to `tokio::spawn` or stuff like that). +/// Make sure that async handlers won't take long time to execute as this may cause latencies in handling other functions. +/// +/// Please also note that this connection won't do anything in background to handle events. +/// In order to trigger event handling(if any) use `noop` function. +/// +/// # Performance considerations +/// Come on it's tor controller. +/// Performance does not really matters. +/// I believe that simplicity and readability are more important(no zero-copy magic here). +pub struct AuthenticatedConn { + async_event_handler: Option, + conn: Conn, +} + +impl From> for AuthenticatedConn { + fn from(conn: Conn) -> Self { + Self { + async_event_handler: None, + conn, + } + } +} + +impl AuthenticatedConn { + /// set_async_event_handler sets handler used to process asynchronous events + pub fn set_async_event_handler(&mut self, handler: Option) { + self.async_event_handler = handler; + } +} + +// parsing stuff here(read only for test + fuzzing purposes) +impl AuthenticatedConn + where + S: AsyncRead + Unpin, + // there fns make use of event handler so it's needed + H: Fn(AsyncEvent<'static>) -> F, + F: Future>, +{ + async fn handle_async_event(&self, event: AsyncEvent<'static>) -> Result<(), ConnError> { + if let Some(handler) = &self.async_event_handler { + (handler)(event).await?; + } + Ok(()) + } + + // recv response + handle async event until there are some + async fn recv_response(&mut self) -> Result<(u16, Vec), ConnError> { + loop { + let (code, lines) = self.conn.receive_data().await?; + if code == 650 { // it's async response + self.handle_async_event(AsyncEvent { + code, + lines: lines.into_iter().map(|v| Cow::Owned(v)).collect(), + }).await?; + } else { + return Ok((code, lines)); + } + } + } + + // according to docs: + // ``` + // On success, + // one ReplyLine is sent for each requested value, followed by a final 250 OK + // ReplyLine. + // ``` + async fn read_get_info_response(&mut self) -> Result>, ConnError> { + let (code, res) = self.recv_response().await?; + let res_len = res.len(); + + if code != 250 { + return Err(ConnError::InvalidResponseCode(code)); + } + // ... followed by a final 250 OK + if &res[res.len() - 1] != "OK" { + return Err(ConnError::InvalidFormat); + } + let mut result: HashMap> = HashMap::new(); + + for l in res.into_iter().take(res_len - 1) { + let (k, v) = parse_single_key_value(&l) + .map_err(|_| ConnError::InvalidFormat)?; + if let Some(res_vec) = result.get_mut(k) { + res_vec.push(v.to_string()); + } else { + result.insert(k.to_string(), vec![v.to_string()]); + } + } + Ok(result) + } + + async fn read_get_conf_response(&mut self) -> Result>>, ConnError> { + let (code, res) = self.recv_response().await?; + if code != 250 { + return Err(ConnError::InvalidResponseCode(code)); + } + let mut result: HashMap>> = HashMap::new(); + for line in res { + let mut is_default = true; + for c in line.as_bytes() { + if *c == b'=' { + is_default = false; + } + } + if is_default { + if let Some(v) = result.get_mut(&line) { + v.push(None); + } else { + result.insert(line, vec![None]); + } + } else { + let (k, v) = parse_single_key_value(&line) + .map_err(|_| ConnError::InvalidFormat)?; + // TODO(teawithsand): Apply some restrictions on what is key? + // ensure unique keys? + /* + According to torCP docs: + ``` + Value may be a raw value or a quoted string. Tor will try to use unquoted + values except when the value could be misinterpreted through not being + quoted. (Right now, Tor supports no such misinterpretable values for + configuration options.) + ``` + */ + let v = match unquote_string(v) { + (Some(offset), Ok(unquoted)) if offset == v.len() - 1 => { + unquoted.into_owned() + } + (None, Ok(unquoted)) => { + unquoted.into_owned() + } + _ => { + return Err(ConnError::InvalidFormat); + } + }; + if let Some(result_list) = result.get_mut(k) { + result_list.push(Some(v)); + } else { + result.insert(k.to_string(), vec![Some(v)]); + } + } + } + Ok(result) + } +} + +impl AuthenticatedConn + where + S: AsyncRead + AsyncWrite + Unpin, + H: Fn(AsyncEvent<'static>) -> F, + F: Future>, +{ + /// set_conf_multiple sends `SETCONF` command to remote tor instance + /// which sets one or more configuration values in tor + /// + /// # Notes + /// `new_value` should not be a quoted string as it will be quoted during this function call before send. + /// If `new_value` is `None` default value will be set for given configuration option. + /// + /// # Error + /// It returns error when `config_option` variable is not valid tor keyword. + /// It returns error when tor instance returns an error. + pub async fn set_conf_multiple(&mut self, options: &mut impl Iterator)>) -> Result<(), ConnError> + { + let mut call = String::new(); + call.push_str("SETCONF"); + let mut has_any_option = false; + for (k, value) in options { + has_any_option = true; + if !is_valid_keyword(k) { + return Err(ConnError::AuthenticatedConnError(AuthenticatedConnError::InvalidKeywordValue)); + } + call.push(' '); + call.push_str(k); + if let Some(value) = value { + // string quoting makes value safe to use in context of connection + let value = quote_string(value.as_bytes()); + call.push('='); + call.push_str(&value); + } + } + if !has_any_option { + return Ok(()); + } + call.push_str("\r\n"); + self.conn.write_data(call.as_bytes()).await?; + + // response parsing is simple + // no need for separate fn + let (code, _lines) = self.conn.receive_data().await?; + if code != 250 { + return Err(ConnError::InvalidResponseCode(code)); + } + Ok(()) + } + + /// set_conf is just like `set_conf_multiple` but is simpler for single config options + pub async fn set_conf(&mut self, option: &str, value: Option<&str>) -> Result<(), ConnError> { + self.set_conf_multiple(&mut std::iter::once((option, value))).await + } + + // TODO(teawithsand): multiple versions of get_conf for specific stuff + /// get_conf sends `GETCONF` command to remote tor instance + /// which gets one(or more but it's not implemented, use sequence of calls to this function) + /// configuration value from tor. + /// + /// # Return value + /// As torCP docs says: + /// ```text + /// If an option appears multiple times in the configuration, all of its + /// key-value pairs are returned in order. + /// ``` + /// If option is default one value is represented as `None` + /// + /// # Error + /// It returns error when `config_option` variable is not valid tor keyword. + /// - Valid keyword is considered as ascii letters and digits. String must not be empty as well. + /// It returns an error if tor considers given value an error for instance because it does not exist. + /// If this happens `522` response code is returned from tor according to torCP docs. + /// + /// # TorCP docs + /// Ctrl+F `3.3. GETCONF` + pub async fn get_conf(&mut self, config_option: &str) -> Result>, ConnError> { + if !is_valid_keyword(config_option) { + return Err(ConnError::AuthenticatedConnError(AuthenticatedConnError::InvalidKeywordValue)); + } + + self.conn.write_data(&format!("GETCONF {}\r\n", config_option).as_bytes()).await?; + let res = self.read_get_conf_response().await?; + + /* + // note: for instance query for DISABLENETWORK may be returned as DisableNetwork=0 + return if let Some(res) = res.remove(config_option) { + Ok(res) + } else { + // no given config option in response! Even if default it would be visible in hashmap. + Err(ConnError::InvalidFormat) + }; + */ + if res.len() != 1 { + return Err(ConnError::InvalidFormat); + } + for (k, v) in res { + if k.len() == config_option.len() && + k.as_bytes().iter().cloned().map(|c| c.to_ascii_uppercase()) + .zip(config_option.as_bytes().iter().cloned().map(|c| c.to_ascii_uppercase())) + .all(|(c1, c2)| c1 == c2) + { + return Ok(v); + } + } + return Err(ConnError::InvalidFormat); + } + + /// get_info_multiple sends `GETINFO` command to remote tor controller. + /// Unlike `GETCONF` it may get values which are not part of tor's configuration. + /// + /// # Return value + /// Result hash map is guaranteed to value for all options provided in request. + /// Each one is interpreted as string without unquoting(if tor spec requires to do so for given value it has to be done manually) + /// + /// If same key was provided two or more times it's value will occur in result these amount of times. + /// Values are fetched directly from tor so they probably are same but take a look at torCP docs to be sure about that. + /// + /// # Error + /// `AuthenticatedConnError::InvalidKeywordValue` is returned if one of provided options is invalid option value and may + /// break control flow integrity of transmission. + pub async fn get_info_multiple(&mut self, options: &mut impl Iterator) -> Result>, ConnError> { + let mut call = String::new(); + call.push_str("GETINFO"); + let mut keys = HashMap::new(); + for option in options { + if !is_valid_option(option) { + return Err(ConnError::AuthenticatedConnError(AuthenticatedConnError::InvalidKeywordValue)); + } + if let Some(counter) = keys.get_mut(option) { + *counter += 1; + } else { + keys.insert(option, 1usize); + } + call.push(' '); + call.push_str(option); + } + call.push_str("\r\n"); + if keys.len() == 0 { + return Ok(HashMap::new()); + } + self.conn.write_data(call.as_bytes()).await?; + + let res = self.read_get_info_response().await?; + if res.len() != keys.len() { + return Err(ConnError::InvalidFormat); + } + // res has to contain all the provided keys + for (key, count) in keys { + match res.get(key) { + Some(v) if v.len() == count => {} + _ => { + return Err(ConnError::InvalidFormat); + } + } + } + return Ok(res); + } + + /// get_info_multiple_unquote works just like get_info_multiple but unquotes results + /// If unquoting fails original value is left. + pub async fn get_info_multiple_unquote(&mut self, options: &mut impl Iterator) -> Result>, ConnError> { + self.get_info_multiple(options).await.map(|mut res_map| { + for (_, values) in res_map.iter_mut() { + for val in values.iter_mut() { + let (quote_end, res) = unquote_string(val); + // TODO(teawithsand): + // what if there are many unquoted strings? + if quote_end.is_some() { // if unquotting occurred + if let Ok(unquoted_text) = res{ // and succeed + *val = unquoted_text.into_owned(); + } + } + } + } + res_map + }) + } + + /// get_info is just like `get_info_multiple` but accepts only one parameter and returns only one value. + /// Under the hood it uses `get_info_multiple`. + pub async fn get_info(&mut self, option: &str) -> Result { + let res = self.get_info_multiple(&mut std::iter::once(option)).await?; + if res.len() != 1 { + return Err(ConnError::InvalidFormat); + } + + let v = res.into_iter().map(|(_k, v)| v).nth(0).unwrap(); + if v.len() != 1 { + return Err(ConnError::InvalidFormat); + } + Ok(v.into_iter().nth(0).unwrap()) + } + + + /// get_info_unquote is just like `get_info` but rather than using `self.get_info_multiple` under the hood id uses `self.get_info_multiple_unquote` + pub async fn get_info_unquote(&mut self, option: &str) -> Result { + let res = self.get_info_multiple_unquote(&mut std::iter::once(option)).await?; + if res.len() != 1 { + return Err(ConnError::InvalidFormat); + } + + let v = res.into_iter().map(|(_k, v)| v).nth(0).unwrap(); + if v.len() != 1 { + return Err(ConnError::InvalidFormat); + } + Ok(v.into_iter().nth(0).unwrap()) + } + + + /// drop_guards invokes `DROPGUARDS` which(according to torCP docs): + /// + /// ```text + /// Tells the server to drop all guard nodes. Do not invoke this command + /// lightly; it can increase vulnerability to tracking attacks over time. + /// ``` + pub async fn drop_guards(&mut self) -> Result<(), ConnError> { + self.conn.write_data(b"DROPGUARDS\r\n").await?; + let (code, _) = self.recv_response().await?; + if code != 250 { + return Err(ConnError::InvalidResponseCode(code)); + } + Ok(()) + } + + /// take_ownership invokes `TAKEOWNERSHIP` which(according to torCP docs): + /// + /// ```text + /// This command instructs Tor to shut down when this control + /// connection is closed. This command affects each control connection + /// that sends it independently; if multiple control connections send + /// the TAKEOWNERSHIP command to a Tor instance, Tor will shut down when + /// any of those connections closes. + /// ``` + pub async fn take_ownership(&mut self) -> Result<(), ConnError> { + self.conn.write_data(b"TAKEOWNERSHIP\r\n").await?; + let (code, _) = self.recv_response().await?; + if code != 250 { + return Err(ConnError::InvalidResponseCode(code)); + } + Ok(()) + } + + /// drop_ownership invokes `DROPOWNERSHIP` which(according to torCP docs): + /// + /// ```text + /// This command instructs Tor to relinquish ownership of its control + /// connection. As such tor will not shut down when this control + /// connection is closed. + /// ``` + pub async fn drop_ownership(&mut self) -> Result<(), ConnError> { + self.conn.write_data(b"DROPOWNERSHIP\r\n").await?; + let (code, _) = self.recv_response().await?; + if code != 250 { + return Err(ConnError::InvalidResponseCode(code)); + } + Ok(()) + } + + // TODO(teawithsand): make async resolve with custom future with tokio which parses async event + // and then notifies caller using waker + // same for reverse_resolve + + /// resolve performs dns lookup over tor. It invokes `RESOLVE` command which(according to torCP docs): + /// ```text + /// This command launches a remote hostname lookup request for every specified + /// request (or reverse lookup if "mode=reverse" is specified). + /// ``` + /// Note: there is separate function for reverse requests. + /// + /// # Result + /// Result is passed as `ADDRMAP` event so one should setup event listener to use it. + /// It's `NewAddressMapping` event. + pub async fn resolve(&mut self, hostname: &str) -> Result<(), ConnError> { + if is_valid_hostname(hostname) { + return Err(ConnError::AuthenticatedConnError(AuthenticatedConnError::InvalidHostnameValue)); + } + + self.conn.write_data(&format!("RESOLVE {}\r\n", hostname).as_bytes()).await?; + let (code, _) = self.recv_response().await?; + if code != 250 { + return Err(ConnError::InvalidResponseCode(code)); + } + Ok(()) + } + + /// resolve performs reverse dns lookup over tor. It invokes `RESOLVE` command which(according to torCP docs): + /// ```text + /// This command launches a remote hostname lookup request for every specified + /// request (or reverse lookup if "mode=reverse" is specified). + /// ``` + /// Note: this function always set reverse mode + /// # Ipv6 + /// TorCP doc says: `a hostname or IPv4 address`. In reverse case it may be only ipv4 address. + /// + /// # Result + /// Result is passed as `ADDRMAP` event so one should setup event listener to use it. + /// It's `NewAddressMapping` event. + pub async fn reverse_resolve(&mut self, address: Ipv4Addr) -> Result<(), ConnError> { + // assumption: ip can't provide any malicious contents + self.conn.write_data(&format!("RESOLVE mode=reverse {}\r\n", address.to_string()).as_bytes()).await?; + let (code, _) = self.recv_response().await?; + if code != 250 { + return Err(ConnError::InvalidResponseCode(code)); + } + Ok(()) + } + + // note: there is no \r\n at the end + #[allow(dead_code)] // prevents emitting warnings when v2 and v3 features is skipped + fn setup_onion_service_call<'a>( + is_rsa: bool, + key_blob: &str, + detach: bool, + non_anonymous: bool, + max_streams_close_circuit: bool, + max_num_streams: Option, + listeners: &mut impl Iterator, + ) -> Result { + let mut res = String::new(); + res.push_str("ADD_ONION "); + if is_rsa { + res.push_str("RSA1024"); + } else { + res.push_str("ED25519-V3"); + } + res.push(':'); + res.push_str(key_blob); + res.push(' '); + + { + let mut flags = Vec::new(); + flags.push("DiscardPK"); + if detach { + flags.push("Detach"); + } + if non_anonymous { + flags.push("NonAnonymous"); + } + if max_streams_close_circuit { + flags.push("MaxStreamsCloseCircuit"); + } + if !flags.is_empty() { + res.push_str("Flags="); + res.push_str(&flags.join(",")); + res.push(' '); + } + } + + { + if let Some(max_num_streams) = max_num_streams { + res.push_str(&format!("MaxStreams={} ", max_num_streams)); + res.push_str(" "); + } + } + + { + let mut is_first = true; + let mut ports = HashSet::new(); + for (port, address) in listeners { + if !is_first { + res.push(' '); + } + if ports.contains(port) { + return Err(AuthenticatedConnError::InvalidListenerSpecification); + } + ports.insert(port); + is_first = false; + res.push_str(&format!("Port={},{}", port, address)); + } + // zero iterations of above loop has ran + if is_first { + return Err(AuthenticatedConnError::InvalidListenerSpecification); + } + res.push(' '); + } + + Ok(res) + } + + #[cfg(any(feature = "v3"))] + /// add_onion sends `ADD_ONION` command which spins up new onion service + /// using given tor secret key and some configuration values. + /// + /// For onion service v2 take a look at `add_onion_v2` + /// + /// # Parameters + /// Take a look at `add_onion_v2`. This function accepts same parameters. + /// + /// It does not support tor-side generated keys yet. + pub async fn add_onion_v3( + &mut self, + key: &crate::onion::TorSecretKeyV3, + detach: bool, + non_anonymous: bool, + max_streams_close_circuit: bool, + max_num_streams: Option, + listeners: &mut impl Iterator, + ) -> Result<(), ConnError> { + let mut res = Self::setup_onion_service_call( + false, + &key.as_tor_proto_encoded(), + detach, + non_anonymous, + max_streams_close_circuit, + max_num_streams, + listeners, + )?; + res.push_str("\r\n"); + + self.conn.write_data(res.as_bytes()).await?; + + // we do not really care about contents of response + // we can derive all the data from tor's objects at the torut level + let (code, _) = self.recv_response().await?; + if code != 250 { + return Err(ConnError::InvalidResponseCode(code)); + } + Ok(()) + } + + /// del_onion sends `DEL_ONION` command which stops onion service. + /// + /// It returns an error if identifier is not valid. + pub async fn del_onion(&mut self, identifier_without_dot_onion: &str) -> Result<(), ConnError> { + for c in identifier_without_dot_onion.chars() { // limit to safe chars, so there is no injection + match c { + 'a'..='z' | 'A'..='Z' | '0'..='9' | '/' | '=' => {} + _ => { + return Err(ConnError::AuthenticatedConnError(AuthenticatedConnError::InvalidOnionServiceIdentifier)); + } + } + } + self.conn.write_data(&format!("DEL_ONION {}\r\n", identifier_without_dot_onion).as_bytes()).await?; + let (code, _) = self.recv_response().await?; + if code != 250 { + return Err(ConnError::InvalidResponseCode(code)); + } + Ok(()) + } + + /// set_events sends `SETEVENTS` command which instructs tor process to report controller all the events + /// of given kind that occur to this controller. + /// + /// # Note + /// Call to `set_events` unsets all previously set event listeners. + /// For instance in order to clear event all listeners use `set_events` with empty iterator. + /// To listen for `CIRC` event pass iterator with single `CIRC` entry. + /// To listen for `WARN` and `ERR` log messages but no more to `CIRC` event pass iterator with two entries: `WARN` and `CIRC` + /// + /// # Notes on using options + /// Extended parameter is ignored in tor newer than `0.2.2.1-alpha` and it's always switched on. + /// It should default to false. + pub async fn set_events(&mut self, extended: bool, kinds: &mut impl Iterator) -> Result<(), ConnError> { + let mut req = String::from("SETEVENTS"); + if extended { + req.push_str(" EXTENDED"); + } + for k in kinds { + if !is_valid_event(k) { + return Err(ConnError::AuthenticatedConnError(AuthenticatedConnError::InvalidEventName)); + } + req.push(' '); + req.push_str(k); + } + req.push_str("\r\n"); + self.conn.write_data(req.as_bytes()).await?; + let (code, _) = self.recv_response().await?; + if code != 250 { + return Err(ConnError::InvalidResponseCode(code)); + } + Ok(()) + } + + /// noop implements no-operation call to tor process despite the fact that torCP does not implement it. + /// It's used to poll any async event without blocking. + pub async fn noop(&mut self) -> Result<(), ConnError> { + // right now noop is getting tor's version + // it should do + self.get_info("version").await?; + Ok(()) + } +} + +#[cfg(test)] +mod test { + use std::io::Cursor; + + use crate::utils::block_on; + + use super::*; + + #[test] + fn test_can_parse_getconf_response() { + for (i, o) in [ + ( + b"250 SOCKSPORT=1234\r\n" as &[u8], + Some({ + let mut res: HashMap>> = HashMap::new(); + res.insert("SOCKSPORT".to_string(), vec![ + Some("1234".to_string()) + ]); + res + }) + ), + ( + b"250 SOCKSPORT\r\n", + Some({ + let mut res: HashMap>> = HashMap::new(); + res.insert("SOCKSPORT".to_string(), vec![ + None + ]); + res + }) + ), + ( + concat!( + "250-SOCKSPORT=1234\r\n", + "250 SOCKSPORT=5678\r\n" + ).as_bytes(), + Some({ + let mut res: HashMap>> = HashMap::new(); + res.insert("SOCKSPORT".to_string(), vec![ + Some("1234".to_string()), + Some("5678".to_string()), + ]); + res + }) + ), + ( + concat!( + "250-SOCKSPORT=5678\r\n", + "250 SOCKSPORT=1234\r\n" + ).as_bytes(), + Some({ + let mut res: HashMap>> = HashMap::new(); + res.insert("SOCKSPORT".to_string(), vec![ + Some("5678".to_string()), + Some("1234".to_string()), + ]); + res + }) + ), + ].iter().cloned() { + block_on(async move { + let mut input = Cursor::new(i); + let conn = Conn::new(&mut input); + let mut conn = AuthenticatedConn::from(conn); + conn.set_async_event_handler( + Some(|_| async move { Ok(()) }) + ); + if let Some(o) = o { + let res = conn.read_get_conf_response().await.unwrap(); + assert_eq!(res, o); + } else { + conn.read_get_conf_response().await.unwrap_err(); + } + }) + } + } + + #[test] + fn test_can_parse_getinfo_response() { + for (i, o) in [ + ( + b"250-version=1.2.3.4\r\n250 OK\r\n" as &[u8], + Some({ + let mut res: HashMap> = HashMap::new(); + res.insert("version".to_string(), vec![ + "1.2.3.4".to_string() + ]); + res + }) + ), + ( + // no terminating `250 OK` line + b"250 version=1.2.3.4\r\n" as &[u8], + None, + ), + ( + // multiple responses for same key + b"250-version=1.2.3.4\r\n250-version=4.3.2.1\r\n250 OK\r\n" as &[u8], + Some({ + let mut res: HashMap> = HashMap::new(); + res.insert("version".to_string(), vec![ + "1.2.3.4".to_string(), + "4.3.2.1".to_string() + ]); + res + }) + ), + ( + // multiple responses for multiple keys + b"250-aversion=1.2.3.4\r\n250-reversion=4.3.2.1\r\n250 OK\r\n" as &[u8], + Some({ + let mut res: HashMap> = HashMap::new(); + res.insert("aversion".to_string(), vec![ + "1.2.3.4".to_string(), + ]); + res.insert("reversion".to_string(), vec![ + "4.3.2.1".to_string(), + ]); + res + }) + ), + ].iter().cloned() { + block_on(async move { + let mut input = Cursor::new(i); + let conn = Conn::new(&mut input); + let mut conn = AuthenticatedConn::from(conn); + conn.set_async_event_handler( + Some(|_| async move { Ok(()) }) + ); + if let Some(o) = o { + let res = conn.read_get_info_response().await.unwrap(); + assert_eq!(res, o); + } else { + conn.read_get_info_response().await.unwrap_err(); + } + }) + } + } +} + +// TODO(teawithsand): cleanup testing initialization +#[cfg(all(test, testtor))] +mod test_with_tor { + use std::thread::sleep; + use std::time::Duration; + use std::net::{IpAddr, Ipv4Addr}; + use std::str::FromStr; + + use tokio::net::{TcpStream}; + + use crate::control::{COOKIE_LENGTH, TorAuthData, TorAuthMethod, UnauthenticatedConn}; + use crate::utils::{block_on_with_env, run_testing_tor_instance, TOR_TESTING_PORT}; + + use super::*; + + #[test] + fn test_can_get_configuration_value_set_it_and_get_it_again() { + let _c = run_testing_tor_instance( + &[ + "--DisableNetwork", "1", + "--ControlPort", &TOR_TESTING_PORT.to_string(), + ]); + + block_on_with_env(async move { + let s = TcpStream::connect(&format!("127.0.0.1:{}", TOR_TESTING_PORT)).await.unwrap(); + let mut utc = UnauthenticatedConn::new(s); + let proto_info = utc.load_protocol_info().await.unwrap(); + + assert!(proto_info.auth_methods.contains(&TorAuthMethod::Null)); + utc.authenticate(&TorAuthData::Null).await.unwrap(); + let mut ac = utc.into_authenticated().await; + ac.set_async_event_handler(Some(|_| { + async move { Ok(()) } + })); + + // socks port is default now + { + let res = ac.get_conf("SocksPort").await.unwrap(); + assert_eq!(res.len(), 1); + assert_eq!(res[0].as_ref().map(|r| r as &str), None); + } + + // socks port is default now + { + ac.set_conf("SocksPort", Some("17539")).await.unwrap(); + + { + let res = ac.get_conf("SocksPort").await.unwrap(); + assert_eq!(res.len(), 1); + assert_eq!(res[0].as_ref().map(|r| r as &str), Some("17539")); + } + } + + { + ac.set_conf("SocksPort", Some("17539")).await.unwrap(); + + { + let res = ac.get_conf("SocksPort").await.unwrap(); + assert_eq!(res.len(), 1); + assert_eq!(res[0].as_ref().map(|r| r as &str), Some("17539")); + } + } + + { + ac.set_conf("SocksPort", None).await.unwrap(); + + { + let res = ac.get_conf("SocksPort").await.unwrap(); + assert_eq!(res.len(), 1); + assert_eq!(res[0].as_ref().map(|r| r as &str), None); + } + } + }); + } + + #[test] + fn test_can_get_information_from_tor() { + let _c = run_testing_tor_instance( + &[ + "--DisableNetwork", "1", + "--ControlPort", &TOR_TESTING_PORT.to_string(), + ]); + + block_on_with_env(async move { + let s = TcpStream::connect(&format!("127.0.0.1:{}", TOR_TESTING_PORT)).await.unwrap(); + let mut utc = UnauthenticatedConn::new(s); + let proto_info = utc.load_protocol_info().await.unwrap(); + + assert!(proto_info.auth_methods.contains(&TorAuthMethod::Null)); + utc.authenticate(&TorAuthData::Null).await.unwrap(); + let mut ac = utc.into_authenticated().await; + ac.set_async_event_handler(Some(|_| { + async move { Ok(()) } + })); + + { + ac.set_conf("SocksPort", Some("17245")).await.unwrap(); + ac.set_conf("DisableNetwork", Some("0")).await.unwrap(); + let res = ac.get_info("net/listeners/socks").await.unwrap(); + let (_, v) = unquote_string(&res); + let v = v.unwrap(); + assert_eq!(v.as_ref(), "127.0.0.1:17245"); + } + }); + } + + #[test] + fn test_can_listen_to_events_on_tor() { + let _c = run_testing_tor_instance( + &[ + "--DisableNetwork", "1", + "--ControlPort", &TOR_TESTING_PORT.to_string(), + ]); + + block_on_with_env(async move { + let s = TcpStream::connect(&format!("127.0.0.1:{}", TOR_TESTING_PORT)).await.unwrap(); + let mut utc = UnauthenticatedConn::new(s); + let proto_info = utc.load_protocol_info().await.unwrap(); + + assert!(proto_info.auth_methods.contains(&TorAuthMethod::Null)); + utc.authenticate(&TorAuthData::Null).await.unwrap(); + let mut ac = utc.into_authenticated().await; + ac.set_async_event_handler(Some(|_| { + async move { Ok(()) } + })); + + let _ = ac.set_events(false, &mut [ + "CIRC", "ADDRMAP" + ].iter().map(|v| *v)).await.unwrap(); + }); + } + + #[test] + fn test_can_take_ownership() { + let mut c = run_testing_tor_instance( + &[ + "--DisableNetwork", "1", + "--ControlPort", &TOR_TESTING_PORT.to_string(), + ]); + + block_on_with_env(async move { + let s = TcpStream::connect(&format!("127.0.0.1:{}", TOR_TESTING_PORT)).await.unwrap(); + let mut utc = UnauthenticatedConn::new(s); + let proto_info = utc.load_protocol_info().await.unwrap(); + + assert!(proto_info.auth_methods.contains(&TorAuthMethod::Null)); + utc.authenticate(&TorAuthData::Null).await.unwrap(); + let mut ac = utc.into_authenticated().await; + ac.set_async_event_handler(Some(|_| { + async move { Ok(()) } + })); + + ac.take_ownership().await.unwrap(); + drop(ac); + assert_eq!(c.wait().unwrap().code().unwrap(), 0); + }); + } + + #[test] + fn test_can_take_and_drop_ownership() { + let mut c = run_testing_tor_instance( + &[ + "--DisableNetwork", "1", + "--ControlPort", &TOR_TESTING_PORT.to_string(), + ]); + + block_on_with_env(async move { + let s = TcpStream::connect(&format!("127.0.0.1:{}", TOR_TESTING_PORT)).await.unwrap(); + let mut utc = UnauthenticatedConn::new(s); + let proto_info = utc.load_protocol_info().await.unwrap(); + + assert!(proto_info.auth_methods.contains(&TorAuthMethod::Null)); + utc.authenticate(&TorAuthData::Null).await.unwrap(); + let mut ac = utc.into_authenticated().await; + ac.set_async_event_handler(Some(|_| { + async move { Ok(()) } + })); + + ac.take_ownership().await.unwrap(); + ac.drop_ownership().await.unwrap(); + drop(ac); + sleep(Duration::from_millis(2000)); + assert!(c.try_wait().unwrap().is_none()); + }); + } + + #[test] + fn test_can_create_onion_service_v3() { + let _c = run_testing_tor_instance( + &[ + "--DisableNetwork", "1", + "--ControlPort", &TOR_TESTING_PORT.to_string(), + ]); + + block_on_with_env(async move { + let s = TcpStream::connect(&format!("127.0.0.1:{}", TOR_TESTING_PORT)).await.unwrap(); + let mut utc = UnauthenticatedConn::new(s); + let proto_info = utc.load_protocol_info().await.unwrap(); + + assert!(proto_info.auth_methods.contains(&TorAuthMethod::Null)); + utc.authenticate(&TorAuthData::Null).await.unwrap(); + let mut ac = utc.into_authenticated().await; + ac.set_async_event_handler(Some(|_| { + async move { Ok(()) } + })); + + let key = crate::onion::TorSecretKeyV3::generate(); + + ac.add_onion_v3(&key, false, false, false, None, &mut [ + (15787, SocketAddr::new(IpAddr::from(Ipv4Addr::new(127,0,0,1)), 15787)), + ].iter()).await.unwrap(); + + // additional actions to check if connection is in corrupted state + ac.take_ownership().await.unwrap(); + ac.drop_ownership().await.unwrap(); + + // delete onion service so it works no more + ac.del_onion(&key.public().get_onion_address().get_address_without_dot_onion()).await.unwrap(); + }); + } + + #[test] + fn test_can_create_onion_service_v3_with_flags() { + let _c = run_testing_tor_instance( + &[ + "--DisableNetwork", "1", + "--ControlPort", &TOR_TESTING_PORT.to_string(), + ]); + + block_on_with_env(async move { + let s = TcpStream::connect(&format!("127.0.0.1:{}", TOR_TESTING_PORT)).await.unwrap(); + let mut utc = UnauthenticatedConn::new(s); + let proto_info = utc.load_protocol_info().await.unwrap(); + + assert!(proto_info.auth_methods.contains(&TorAuthMethod::Null)); + utc.authenticate(&TorAuthData::Null).await.unwrap(); + let mut ac = utc.into_authenticated().await; + ac.set_async_event_handler(Some(|_| { + async move { Ok(()) } + })); + + let key = crate::onion::TorSecretKeyV3::generate(); + + ac.add_onion_v3(&key, true, false, true, Some(1234), &mut [ + (15787, SocketAddr::new(IpAddr::from(Ipv4Addr::new(127,0,0,1)), 15787)), + ].iter()).await.unwrap(); + + // additional actions to check if connection is in corrupted state + ac.take_ownership().await.unwrap(); + ac.drop_ownership().await.unwrap(); + + // delete onion service so it works no more + ac.del_onion(&key.public().get_onion_address().get_address_without_dot_onion()).await.unwrap(); + }); + } + + #[test] + fn test_can_issue_getinfo_unquote() { + let _c = run_testing_tor_instance( + &[ + "--DisableNetwork", "1", + "--ControlPort", &TOR_TESTING_PORT.to_string(), + ]); + + block_on_with_env(async move { + let s = TcpStream::connect(&format!("127.0.0.1:{}", TOR_TESTING_PORT)).await.unwrap(); + let mut utc = UnauthenticatedConn::new(s); + let proto_info = utc.load_protocol_info().await.unwrap(); + + assert!(proto_info.auth_methods.contains(&TorAuthMethod::Null)); + utc.authenticate(&TorAuthData::Null).await.unwrap(); + let mut ac = utc.into_authenticated().await; + ac.set_async_event_handler(Some(|_| { + async move { Ok(()) } + })); + + let controlport = ac.get_info_unquote("net/listeners/control").await.unwrap(); + let addr = SocketAddr::from_str(&controlport).unwrap(); + let is_loopback = match addr{ + SocketAddr::V4(a) => a.ip().is_loopback(), + SocketAddr::V6(a) => a.ip().is_loopback(), + }; + assert!(is_loopback, "Tor control protocol is not listening on loopback"); + }); + } + + #[test] + fn test_can_issue_not_existing_getinfo() { + let _c = run_testing_tor_instance( + &[ + "--DisableNetwork", "1", + "--ControlPort", &TOR_TESTING_PORT.to_string(), + ]); + + block_on_with_env(async move { + let s = TcpStream::connect(&format!("127.0.0.1:{}", TOR_TESTING_PORT)).await.unwrap(); + let mut utc = UnauthenticatedConn::new(s); + let proto_info = utc.load_protocol_info().await.unwrap(); + + assert!(proto_info.auth_methods.contains(&TorAuthMethod::Null)); + utc.authenticate(&TorAuthData::Null).await.unwrap(); + let mut ac = utc.into_authenticated().await; + ac.set_async_event_handler(Some(|_| { + async move { Ok(()) } + })); + + ac.get_info_unquote("blah/blah/blah").await.unwrap_err(); + ac.get_info("blah/blah/blah").await.unwrap_err(); + }); + } +} + +#[cfg(fuzzing)] +mod fuzzing { + // TODO(teawithsand): fuzz functions receiving data from tor here +} \ No newline at end of file diff --git a/src/control/conn/conn.rs b/src/control/conn/conn.rs new file mode 100644 index 0000000..c04c1a6 --- /dev/null +++ b/src/control/conn/conn.rs @@ -0,0 +1,399 @@ +use std::convert::TryFrom; +use std::error::Error; +use std::fmt::{self, Display, Formatter}; +use std::io; +use std::num::ParseIntError; +use std::option::Option::None; +use std::str::{FromStr, Utf8Error}; +use std::string::FromUtf8Error; + +use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; + +use crate::control::TorErrorKind; + +/// UnauthenticatedConnError describes subset of `ConnError`s returned by `UnauthenticatedConn` +#[derive(Debug, From)] +pub enum UnauthenticatedConnError { + /// Fetching authentication info twice causes tor to break connections so we forbid that and return + /// this error code when programmer tries to do so. + InfoFetchedTwice, + + /// ServerHashMismatch is returned when SafeCookie auth methods client detects that + /// it connects to wrong server. + /// + /// Right now it's not implemented and is never returned. + ServerHashMismatch, +} + +impl Display for UnauthenticatedConnError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Self::InfoFetchedTwice => write!(f, "Authentication info fetched twice"), + Self::ServerHashMismatch => write!(f, "Tor cookie hashes do not match"), + } + } +} + +impl Error for UnauthenticatedConnError {} + +/// AuthenticatedConnError describes subset of `ConnError`s returned by `AuthenticatedConn` +#[derive(Debug, From)] +pub enum AuthenticatedConnError { + /// InvalidKeywordValue when user-provided keyword is not valid + /// It's also returned when user-provided option is not valid. + InvalidKeywordValue, + + /// InvalidHostnameValue when user-provided domain passed to resolve is not valid + InvalidHostnameValue, + + /// InvalidListenerSpecification is returned when one tries to spin up new onion service and + /// port settings are invalid + InvalidListenerSpecification, + + /// InvalidOnionServiceIdentifier is returned when onion service identifier passed as argument is invalid + InvalidOnionServiceIdentifier, + + /// InvalidEventName is returned when name of given event passed to conn is invalid and may corrupt connection flow + InvalidEventName, +} + +impl Display for AuthenticatedConnError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + // TODO(teawithsand): proper explanation for these errors + // despite the fact that they are low level it's hard to write user facing message for them + write!(f, "{:?} (Read torut's docs)", self) + } +} + +impl Error for AuthenticatedConnError {} + +/// ConnError is able to wrap any error that a connection may return +#[derive(Debug, From)] +pub enum ConnError { + IOError(io::Error), + Utf8Error(Utf8Error), + FromUtf8Error(FromUtf8Error), + ParseIntError(ParseIntError), + + UnauthenticatedConnError(UnauthenticatedConnError), + AuthenticatedConnError(AuthenticatedConnError), + + // TODO(teawithsand): migrate this error to more meaningful one - with explanation or unknown code otherwise + // typed error codes are already implemented; change this before next minor release + /// Invalid(or unexpected) response code was returned from tor controller. + /// Usually this indicates some error on tor's side + InvalidResponseCode(u16), + + InvalidFormat, + InvalidCharacterFound, + NonAsciiByteFound, + ResponseCodeMismatch, + + TooManyBytesRead, +} + +impl Display for ConnError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let src = self.source(); + if let Some(src) = src { + write!(f, "ConnError: {}", src) + } else { + match self { + Self::InvalidResponseCode(code) => { + let typed = TorErrorKind::try_from(*code); + if let Ok(typed) = typed { + write!(f, "Tor returned error response code: {} - {:?}", code, typed) + } else { + write!(f, "Tor returned error response code: {}", code) + } + } + Self::InvalidFormat | Self::InvalidCharacterFound | Self::NonAsciiByteFound | Self::ResponseCodeMismatch => write!(f, "Invalid response got from tor"), + Self::TooManyBytesRead => write!(f, "Tor response was too big to process"), + _ => write!(f, "Unknown ConnError"), + } + } + } +} + +impl Error for ConnError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + Self::IOError(err) => Some(err), + Self::Utf8Error(err) => Some(err), + Self::FromUtf8Error(err) => Some(err), + Self::ParseIntError(err) => Some(err), + Self::UnauthenticatedConnError(err) => Some(err), + Self::AuthenticatedConnError(err) => Some(err), + _ => None + } + } +} + +/// Conn wraps any `AsyncRead + AsyncWrite` stream and implements parsing responses from tor and sending data to it. +/// +/// It's stateless component. It does not contain any information about connection like authentication state. +/// +/// # Note +/// This is fairly low-level connection which does only basic parsing. +/// Unless you need it you should use higher level apis. +pub struct Conn { + stream: S, +} + +impl Conn { + pub fn new(stream: S) -> Self { + Self { + stream + } + } + + pub fn into_inner(self) -> S { + self.stream + } +} + +/// MAX_SINGLE_RECV_BYTES describes how many bytes may be received during single call to `receive_data` +/// It's used to prevent DoS(OOM allocating). +const MAX_SINGLE_RECV_BYTES: usize = 1024 * 1024 * 1;// 1MB + +impl Conn + where S: AsyncRead + Unpin +{ + /// receive_data receives single response from tor + /// + /// # Response format + /// Rather than grouping response by lines sent on proto it groups it on "lines" returned by tor. + /// Take a look at tests to see what's going on. Basically all multiline mode data is put into one string despite + /// the fact that it may contain multiple lines. + /// + /// # Performance considerations + /// This function allocates all stuff and does not allow writing to any preallocated buffer. + /// It neither does not allow for any kind of borrowing from one big buffer. + /// + /// Personally I think it's not needed. It's tor api how many data you want receive from it? + /// Anyway this won't be ran on any embedded device(because it has to be able to run tor, it has to run at least some + /// linux so I probably can allocate a few strings on it...) + /// + /// # Possible performance issues + /// It uses byte-by-byte reading. Thanks to this feature there is no state in `Conn` struct. + /// Use some sort of buffered reader in order to minimize overhead. + pub async fn receive_data(&mut self) -> Result<(u16, Vec), ConnError> { + // ok. let's first think about the format. + // it's rather simple + // docs: https://gitweb.torproject.org/torspec.git/tree/control-spec.txt + // 1. Each line consists of code and data(unless in "multiline read mode") + // 2. Code in each line is same. + // 3. Response is done after reaching line with `XXX DDD...` where XXX is code and DDD is arbitrary data + // 4. Multiline responses are created with `XXX-DDD` where XXX is code and DDD is arbitrary data + // 5. So called(at least I call it) "multiline mode" can be enabled with `XXX+DDD[\r\nDDD]..\r\n.\r\n` + // where XXX is code and DDD are arbitrary data blocks. It's done once single blank line with dot is found. + /* + Appropriate docs section: + 2.3. Replies from Tor to the controller + + Reply = SyncReply / AsyncReply + SyncReply = *(MidReplyLine / DataReplyLine) EndReplyLine + AsyncReply = *(MidReplyLine / DataReplyLine) EndReplyLine + + MidReplyLine = StatusCode "-" ReplyLine + DataReplyLine = StatusCode "+" ReplyLine CmdData + EndReplyLine = StatusCode SP ReplyLine + ReplyLine = [ReplyText] CRLF + ReplyText = XXXX + StatusCode = 3DIGIT + + Multiple lines in a single reply from Tor to the controller are guaranteed to + share the same status code. Specific replies are mentioned below in section 3, + and described more fully in section 4. + + [Compatibility note: versions of Tor before 0.2.0.3-alpha sometimes + generate AsyncReplies of the form "*(MidReplyLine / DataReplyLine)". + This is incorrect, but controllers that need to work with these + versions of Tor should be prepared to get multi-line AsyncReplies with + the final line (usually "650 OK") omitted.] + + # Torut developer note: above compatibility note is not implemented + */ + + let mut lines = Vec::new(); + let mut response_code = None; + + let mut state = 0; + + let mut current_line_buffer = Vec::new(); + let mut bytes_read = 0; + loop { + if bytes_read >= MAX_SINGLE_RECV_BYTES { + return Err(ConnError::TooManyBytesRead); + } + let b = { + let mut buf = [0u8; 1]; + self.stream.read_exact(&mut buf[..]).await?; + buf[0] + }; + + bytes_read += 1; + + // is this check valid? + // is all data valid ascii? + if !b.is_ascii() { + return Err(ConnError::NonAsciiByteFound); + } + + if state == 0 { + if !b.is_ascii_digit() { + return Err(ConnError::InvalidCharacterFound); + } + current_line_buffer.push(b); + + // we found response code! + if current_line_buffer.len() == 3 { + let text = std::str::from_utf8(¤t_line_buffer)?; + let parsed_response_code = u16::from_str(text)?; + + // some fancy behaviour of from str may occur(?) + // let's leave this assert even for prod use + assert!(parsed_response_code < 1000, "Invalid response code"); + + if let Some(response_code) = response_code { + if response_code != parsed_response_code { + return Err(ConnError::ResponseCodeMismatch); + } + } else { + response_code = Some(parsed_response_code); + } + state = 1; + current_line_buffer.clear(); + } + } else if state == 1 { + debug_assert!(current_line_buffer.is_empty()); + debug_assert!(response_code.is_some()); + match b { + // last line + b' ' => { + state = 2; + } + // some of many lines + b'-' => { + state = 3; + } + // multiline mode trigger + b'+' => { + state = 4; + } + // other characters are not allowed + _ => { + return Err(ConnError::InvalidCharacterFound); + } + } + } else if state == 2 || state == 3 { + // as the docs says: + // Tor, however, MUST NOT generate LF instead of CRLF. + current_line_buffer.push(b); + if current_line_buffer.len() >= 2 && + current_line_buffer[current_line_buffer.len() - 2] == b'\r' && + current_line_buffer[current_line_buffer.len() - 1] == b'\n' + { + current_line_buffer.truncate(current_line_buffer.len() - 2); + + let res = { + let mut line_buffer = Vec::new(); + std::mem::swap(&mut current_line_buffer, &mut line_buffer); + String::from_utf8(line_buffer) + }; + // only valid ascii remember? + // if so it's valid utf8 + debug_assert!(res.is_ok()); + let text = res?; + lines.push(text); + + // if it's last line break loop + if state == 2 { + break; + } else { + state = 0; + } + } + } else if state == 4 { + // multiline read mode reads lines until it eventually found \r\n.\r\n sequence + current_line_buffer.push(b); + if current_line_buffer.len() >= 5 && + current_line_buffer[current_line_buffer.len() - 5] == b'\r' && + current_line_buffer[current_line_buffer.len() - 4] == b'\n' && + current_line_buffer[current_line_buffer.len() - 3] == b'.' && + current_line_buffer[current_line_buffer.len() - 2] == b'\r' && + current_line_buffer[current_line_buffer.len() - 1] == b'\n' + { + current_line_buffer.truncate(current_line_buffer.len() - 5); + + let res = { + let mut line_buffer = Vec::new(); + std::mem::swap(&mut current_line_buffer, &mut line_buffer); + String::from_utf8(line_buffer) + }; + + // only valid ascii remember? + // if so it's valid utf8 + debug_assert!(res.is_ok()); + let text = res?; + lines.push(text); + + // there may be more lines incoming after this one + state = 0; + } + } else { + unreachable!("Invalid state!"); + } + } + if response_code.is_none() { + return Err(ConnError::InvalidFormat); + } + return Ok((response_code.unwrap(), lines)); + } +} + +impl Conn where S: AsyncWrite + Unpin { + /// write_data writes *RAW* data into tor controller and flushes stream + pub async fn write_data(&mut self, data: &[u8]) -> Result<(), ConnError> { + self.stream.write_all(data).await?; + self.stream.flush().await?; + Ok(()) + } +} + +#[cfg(test)] +mod test { + use std::io::Cursor; + + use crate::utils::block_on; + + use super::*; + + #[test] + fn test_conn_can_read_response() { + for (input, output) in [ + ("250 Ok line one\r\n", Some((250u16, vec!["Ok line one"]))), + ("250-L1\r\n250 L2\r\n", Some((250, vec!["L1", "L2"]))), + ("250-LANDER=MAAR\r\n250 L2\r\n", Some((250, vec!["LANDER=MAAR", "L2"]))), + ("250-default\r\n250 key=value\r\n", Some((250, vec!["default", "key=value"]))), + ("250-abc\r\n250+abcd\r\n second line\r\n.\r\n250 OK\r\n", Some((250, vec!["abc", "abcd\r\n second line", "OK"]))), + ("250-abc\r\n250+abcd\r\n second line\r\n.\r\n250 OK", None), + ("250-abc\r\n250+abcd\r\n second line\r\n.\r\n", None), + ("250-abc\r\n250+abcd\r\n second line", None), + ].iter().cloned() { + // eprintln!("{:?} -> {:?}", input, output); + block_on(async move { + let mut cursor = Cursor::new(Vec::from(input)); + let mut conn = Conn::new(&mut cursor); + if let Some((valid_code, valid_res)) = output { + let (given_code, given_res) = conn.receive_data().await.unwrap(); + assert_eq!(valid_code, given_code); + let res2_ref = given_res.iter().map(|s| s as &str).collect::>(); + assert_eq!(valid_res, res2_ref); + } else { + conn.receive_data().await.unwrap_err(); + } + }); + } + } +} \ No newline at end of file diff --git a/src/control/conn/mod.rs b/src/control/conn/mod.rs new file mode 100644 index 0000000..9d3d01f --- /dev/null +++ b/src/control/conn/mod.rs @@ -0,0 +1,7 @@ +pub use authenticated_conn::*; +pub use conn::*; +pub use unauthenticated_conn::*; + +mod conn; +mod unauthenticated_conn; +mod authenticated_conn; diff --git a/src/control/conn/unauthenticated_conn.rs b/src/control/conn/unauthenticated_conn.rs new file mode 100644 index 0000000..48a131a --- /dev/null +++ b/src/control/conn/unauthenticated_conn.rs @@ -0,0 +1,555 @@ +use std::borrow::Cow; +use std::collections::{HashMap, HashSet}; +use std::str::FromStr; + +use hmac::{Hmac, Mac}; +use rand::{RngCore, thread_rng}; +use sha2::Sha256; +use tokio::io::AsyncRead; +use tokio::io::AsyncWrite; + +use crate::control::conn::{AuthenticatedConn, Conn, ConnError, UnauthenticatedConnError}; +use crate::control::primitives::{TorAuthData, TorAuthMethod, TorPreAuthInfo}; +use crate::utils::{parse_single_key_value, quote_string, unquote_string}; + +// note: unlike authenticated conn, unauthenticated conn does not do any asynchronous event handling +/// UnauthenticatedConn represents connection to torCP which is not authenticated yet +/// and for this reason only limited amount of operations may be performed on it. +/// +/// It wraps `Conn` +pub struct UnauthenticatedConn { + conn: Conn, + was_protocol_info_loaded: bool, + protocol_info: Option>, +} + +impl From> for UnauthenticatedConn { + fn from(conn: Conn) -> Self { + Self { + conn, + protocol_info: None, + was_protocol_info_loaded: false, + } + } +} + +impl UnauthenticatedConn { + pub fn new(stream: S) -> Self { + Self::from(Conn::new(stream)) + } + + /// get_protocol_info returns tor protocol info reference if one was loaded before + /// with `load_protocol_info` + /// + /// In order to get owned version of `TorPreAuthInfo` use `own_protocol_info`. + pub fn get_protocol_info(&self) -> Option<&TorPreAuthInfo<'static>> { + self.protocol_info.as_ref() + } + + /// take_protocol_info returns tor protocol info value if one was loaded before + /// with `load_protocol_info` + pub fn take_protocol_info(&mut self) -> Option> { + self.protocol_info.take() + } +} + +/// TOR_SAFECOOKIE_CONSTANT is passed to HMAC for `SAFECOOKIE` auth procedure +const TOR_SAFECOOKIE_CONSTANT: &[u8] = b"Tor safe cookie authentication controller-to-server hash"; + +/// AuthChallengeResponse is container for response returned by server after executing +/// `AUTHCHALLENGE` command +// pub crate required due to read_auth_challenge_response pub crate read visibility for fuzzing +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct AuthChallengeResponse { + /// according to TorCP docs it's 32 bytes long always + /// because it's 64 hexadecimal digits + pub server_hash: [u8; 32], + + /// according to TorCP docs it's 32 bytes long always + pub server_nonce: [u8; 32], +} + +impl UnauthenticatedConn + where S: AsyncRead + Unpin +{ + // exposed for testing and fuzzing + pub(crate) async fn read_protocol_info<'a>(&'a mut self) -> Result<&'a TorPreAuthInfo<'static>, ConnError> { + let (code, lines) = self.conn.receive_data().await?; + + // 250 code is hardcoded at spec right now + // we do not expect async events yet + if code != 250 { + return Err(ConnError::InvalidResponseCode(code)); + } + if lines.len() < 3 { + return Err(ConnError::InvalidFormat); + } + if lines[0] != "PROTOCOLINFO 1" { + return Err(ConnError::InvalidFormat); + } + let mut res = HashMap::new(); + for l in &lines[1..lines.len() - 1] { + match parse_single_key_value(l) { + Ok((key, value)) => { + if res.contains_key(key) { + // may keys ve duplicated? + return Err(ConnError::InvalidFormat); + } + res.insert(key, value); + } + Err(_) => { + return Err(ConnError::InvalidFormat); + } + } + } + + if &lines[lines.len() - 1] != "OK" { + return Err(ConnError::InvalidFormat); + } + + let (auth_methods, cookie_path) = if let Some(auth_methods) = res.get("AUTH METHODS") + .or_else(|| res.get("METHODS")) + { + let mut auth_methods_res = HashSet::new(); + + let mut end_methods_idx = 0; + for c in auth_methods.chars() { + if c == ' ' { + break; + } + end_methods_idx += c.len_utf8(); + } + for m in auth_methods[..end_methods_idx] + .split(',') + { + if let Ok(v) = TorAuthMethod::from_str(m) { + if auth_methods_res.contains(&v) { + return Err(ConnError::InvalidFormat); + } + auth_methods_res.insert(v); + } else { + return Err(ConnError::InvalidFormat); + } + } + + let maybe_cookie_str = auth_methods[end_methods_idx..].trim(); + let cookie_path = if maybe_cookie_str.len() > 0 { + let (k, encoded_path) = parse_single_key_value(maybe_cookie_str) + .map_err(|_| ConnError::InvalidFormat)?; + if k != "COOKIEFILE" { + return Err(ConnError::InvalidFormat); + } + match unquote_string(encoded_path) { + // quoted string which is valid utf-8 + // and ends with string + (Some(offset), Ok(path)) if offset == encoded_path.len() - 1 => { + Some(path.into_owned()) + } + _ => { + return Err(ConnError::InvalidFormat); + } + } + } else { + None + }; + // in fact there should be some auth method even null one + if auth_methods_res.len() == 0 { + return Err(ConnError::InvalidFormat); + } + (auth_methods_res, cookie_path) + } else { + return Err(ConnError::InvalidFormat); + }; + + + let version = res.get("VERSION Tor") + .map(|v| unquote_string(v)); + let version = match version { + Some((Some(_), Ok(v))) => { + v.into_owned() + } + // no tor version supplied + _ => { + return Err(ConnError::InvalidFormat); + } + }; + + self.was_protocol_info_loaded = true; + { + self.protocol_info = Some(TorPreAuthInfo { + auth_methods, + cookie_file: cookie_path.map(|v| Cow::Owned(v)), + tor_version: Cow::Owned(version), + }); + } + Ok(self.protocol_info.as_ref().unwrap()) + } + + //noinspection SpellCheckingInspection + // example line: + // Note: '\' at the end is soft line break + // Note #2: part in the round brackets is not in line variable. + // (250 )AUTHCHALLENGE SERVERHASH=3AB21C1D4E7337F2CC4460C9973B13EE42944E6455131A8CA0CF10628BCBACF2 \ + // SERVERNONCE=DB3B06356534DE8732C8C858F543D0E55B8D44A2353F913B5F36E23A61537D86 + pub(crate) async fn read_auth_challenge_response(&mut self) -> Result { + let (code, mut lines) = self.conn.receive_data().await?; + if code != 250 { + return Err(ConnError::InvalidResponseCode(code)); + } + if lines.len() != 1 { + return Err(ConnError::InvalidFormat); + } + let line = lines.swap_remove(0); + + // right now line has fixed length of some letters + 2x 64 hex chars + two spacebars + if line.len() != "AUTHCHALLENGE".len() + "SERVERHASH=".len() + "SERVERNONCE=".len() + 64 * 2 + 2 { + return Err(ConnError::InvalidFormat); + } + // even more! data is at the fixed offsets which allows us to write simple (and robust ofc) parser + let server_hash_text = &line[25..25 + 64]; + let server_nonce_text = &line[90 + 12..90 + 12 + 64]; + let mut res = AuthChallengeResponse { + server_hash: [0u8; 32], + server_nonce: [0u8; 32], + }; + hex::decode_to_slice(server_hash_text, &mut res.server_hash) + .map_err(|_| ConnError::InvalidFormat)?; + hex::decode_to_slice(server_nonce_text, &mut res.server_nonce) + .map_err(|_| ConnError::InvalidFormat)?; + return Ok(res); + } +} + +impl UnauthenticatedConn + where S: AsyncRead + AsyncWrite + Unpin +{ + /// This function issues `PROTOCOLINFO` command on remote tor instance. + /// Because this command may be executed only once it automatically prevents programmer from getting data twice. + /// + /// # TorCP docs + /// Ctrl+F in document: `3.21. PROTOCOLINFO` + pub async fn load_protocol_info<'a>(&'a mut self) -> Result<&'a TorPreAuthInfo<'static>, ConnError> { + // rust borrow checker seems to fail once this code is uncommented + /* + { + if let Some(val) = &self.protocol_info { + return Ok(val); + } + } + */ + + if self.was_protocol_info_loaded { + return Err(ConnError::UnauthenticatedConnError(UnauthenticatedConnError::InfoFetchedTwice)); + } + + self.conn.write_data(b"PROTOCOLINFO 1\r\n").await?; + self.read_protocol_info().await + } + + /// authenticate performs authentication of given connection BUT does not create `AuthenticatedConn` + /// from this one. + /// + /// # Note + /// It does not check if provided auth method associated with given tor auth data is valid. + /// It trusts programmer to do so. + /// In worst case it won't work(tor won't let us in) + pub async fn authenticate(&mut self, data: &TorAuthData<'_>) -> Result<(), ConnError> { + match data { + TorAuthData::Null => { + // this one is easy + self.conn.write_data(b"AUTHENTICATE\r\n").await?; + } + TorAuthData::HashedPassword(password) => { + let password = quote_string(password.as_bytes()); + let mut buf = Vec::new(); + buf.extend_from_slice(b"AUTHENTICATE "); + buf.extend_from_slice(password.as_ref()); + buf.extend_from_slice(b"\r\n"); + self.conn.write_data(&buf).await?; + } + TorAuthData::Cookie(cookie) => { + let mut buf = Vec::new(); + buf.extend_from_slice(b"AUTHENTICATE "); + buf.extend_from_slice(hex::encode_upper(cookie.as_ref()).as_bytes()); + buf.extend_from_slice(b"\r\n"); + self.conn.write_data(&buf).await?; + } + TorAuthData::SafeCookie(cookie) => { + // for safe cookie we need sha256 hmac + // so controller requires sha2 and rand for nonces + + let mut client_nonce = [0u8; 64]; + thread_rng().fill_bytes(&mut client_nonce); + + let cookie_string = hex::encode_upper(&client_nonce[..]); + self.conn.write_data( + format!( + "AUTHCHALLENGE SAFECOOKIE {}\r\n", + cookie_string + ).as_bytes() + ).await?; + let res = self.read_auth_challenge_response().await?; + // panic!("Got ACR: {:#?}", res); + + // TODO(teawithsand): check server hash procedure here. + // Note: it probably requires constant time compare procedure which means more dependencies probably + // or some wild hacks like comparing sha256 hashes of both values(which leaks hashes values but not values itself) + + let client_hash = { + use hmac::NewMac; + let mut hmac = >::new_from_slice(TOR_SAFECOOKIE_CONSTANT) + .expect("Any key len for hmac should be valid. If it's not then rehash data. Right?"); + hmac.update(cookie.as_ref()); + hmac.update(&client_nonce[..]); + hmac.update(&res.server_nonce[..]); + + + let res = hmac.finalize(); + res.into_bytes() + }; + + let mut buf = Vec::new(); + buf.extend_from_slice(b"AUTHENTICATE "); + buf.extend_from_slice(hex::encode_upper(client_hash).as_bytes()); + buf.extend_from_slice(b"\r\n"); + self.conn.write_data(&buf[..]).await?; + } + } + let (code, _) = self.conn.receive_data().await?; + if code != 250 { + return Err(ConnError::InvalidResponseCode(code)); + } + Ok(()) + } + + /// into_authenticated creates `AuthenticatedConn` from this one without checking if it makes any sense. + /// It should be called after successful call to `authenticate`. + pub async fn into_authenticated(self) -> AuthenticatedConn { + AuthenticatedConn::from(self.conn) + } +} + +#[cfg(test)] +mod test { + use std::io::Cursor; + + use crate::utils::block_on; + + use super::*; + + #[test] + fn test_can_parse_response() { + for (i, o) in [ + ( + concat!( + "250-PROTOCOLINFO 1\r\n", + "250-AUTH METHODS=NULL\r\n", + "250-VERSION Tor=\"0.4.2.5\"\r\n", + "250 OK\r\n", + ), + Some( + TorPreAuthInfo { + tor_version: Cow::Owned("0.4.2.5".to_string()), + auth_methods: [ + TorAuthMethod::Null, + ].iter().copied().collect(), + cookie_file: None, + } + ) + ), + ( + concat!( + "250-PROTOCOLINFO 1\r\n", + "250-AUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE=\"/home/user/.tor/control_auth_cookie\"\r\n", + "250-VERSION Tor=\"0.4.2.5\"\r\n", + "250 OK\r\n" + ), + Some( + TorPreAuthInfo { + tor_version: Cow::Owned("0.4.2.5".to_string()), + auth_methods: [ + // sets do not have order! + TorAuthMethod::SafeCookie, + TorAuthMethod::Cookie, + ].iter().copied().collect(), + cookie_file: Some(Cow::Owned("/home/user/.tor/control_auth_cookie".to_string())), + } + ) + ), + ( + concat!( + "250-PROTOCOLINFO 1\r\n", + "250-AUTH METHODS=COOKIE,SAFECOOKIE,HASHEDPASSWORD COOKIEFILE=\"/home/user/.tor/control_auth_cookie\"\r\n", + "250-VERSION Tor=\"0.4.2.5\"\r\n", + "250 OK\r\n" + ), + Some( + TorPreAuthInfo { + tor_version: Cow::Owned("0.4.2.5".to_string()), + auth_methods: [ + // sets do not have order! + TorAuthMethod::SafeCookie, + TorAuthMethod::Cookie, + TorAuthMethod::HashedPassword, + ].iter().copied().collect(), + cookie_file: Some(Cow::Owned("/home/user/.tor/control_auth_cookie".to_string())), + } + ) + ), + ].iter().cloned() { + block_on(async move { + let mut conn = UnauthenticatedConn::new(Cursor::new(i.as_bytes())); + match o { + Some(v) => { + let pai = conn.read_protocol_info().await.unwrap(); + assert_eq!(pai, &v); + } + None => { + let _ = conn.read_protocol_info().await.unwrap_err(); + } + } + }); + } + } +} + +#[cfg(all(test, testtor))] +mod test_tor { + use std::net::IpAddr; + + use tokio::fs::File; + use tokio::net::TcpStream; + use tokio::io::AsyncReadExt; + + use crate::control::COOKIE_LENGTH; + use crate::utils::{AutoKillChild, block_on_with_env, run_testing_tor_instance, TOR_TESTING_PORT}; + + use super::*; + + #[test] + fn test_can_null_authenticate() { + let _c = run_testing_tor_instance(&["--DisableNetwork", "1", "--ControlPort", &TOR_TESTING_PORT.to_string()]); + + block_on_with_env(async move { + let s = TcpStream::connect(&format!("127.0.0.1:{}", TOR_TESTING_PORT)).await.unwrap(); + let mut utc = UnauthenticatedConn::new(s); + let proto_info = utc.load_protocol_info().await.unwrap(); + assert!(proto_info.auth_methods.contains(&TorAuthMethod::Null)); + utc.authenticate(&TorAuthData::Null).await.unwrap(); + // test conn further? + }); + } + + #[test] + fn test_can_cookie_authenticate() { + let _c = run_testing_tor_instance( + &[ + "--DisableNetwork", "1", + "--ControlPort", &TOR_TESTING_PORT.to_string(), + "--CookieAuthentication", "1", + ]); + + block_on_with_env(async move { + let s = TcpStream::connect(&format!("127.0.0.1:{}", TOR_TESTING_PORT)).await.unwrap(); + let mut utc = UnauthenticatedConn::new(s); + let proto_info = utc.load_protocol_info().await.unwrap(); + // panic!("{:#?}", proto_info); + assert!(proto_info.auth_methods.contains(&TorAuthMethod::Cookie)); + assert!(proto_info.cookie_file.is_some()); + let cookie = { + let mut cookie_file = File::open(proto_info.cookie_file.as_ref().unwrap().as_ref()).await.unwrap(); + let mut cookie = Vec::new(); + cookie_file.read_to_end(&mut cookie).await.unwrap(); + assert_eq!(cookie.len(), COOKIE_LENGTH); + cookie + }; + utc.authenticate(&TorAuthData::Cookie(Cow::Owned(cookie))).await.unwrap(); + // test conn further? + }); + } + + #[test] + fn test_authenticate_fails_when_invalid_method() { + let _c = run_testing_tor_instance( + &[ + "--DisableNetwork", "1", + "--ControlPort", &TOR_TESTING_PORT.to_string(), + "--CookieAuthentication", "1", + ]); + + block_on_with_env(async move { + let s = TcpStream::connect(&format!("127.0.0.1:{}", TOR_TESTING_PORT)).await.unwrap(); + let mut utc = UnauthenticatedConn::new(s); + let proto_info = utc.load_protocol_info().await.unwrap(); + assert!(!proto_info.auth_methods.contains(&TorAuthMethod::Null)); + utc.authenticate(&TorAuthData::Null).await.unwrap_err(); + }); + } + + #[test] + fn test_can_safe_cookie_authenticate() { + let _c = run_testing_tor_instance( + &[ + "--DisableNetwork", "1", + "--ControlPort", &TOR_TESTING_PORT.to_string(), + "--CookieAuthentication", "1", + ]); + + block_on_with_env(async move { + let s = TcpStream::connect(&format!("127.0.0.1:{}", TOR_TESTING_PORT)).await.unwrap(); + let mut utc = UnauthenticatedConn::new(s); + let proto_info = utc.load_protocol_info().await.unwrap(); + // panic!("{:#?}", proto_info); + assert!(proto_info.auth_methods.contains(&TorAuthMethod::SafeCookie)); + assert!(proto_info.cookie_file.is_some()); + let cookie = { + let mut cookie_file = File::open(proto_info.cookie_file.as_ref().unwrap().as_ref()).await.unwrap(); + let mut cookie = Vec::new(); + cookie_file.read_to_end(&mut cookie).await.unwrap(); + assert_eq!(cookie.len(), COOKIE_LENGTH); + cookie + }; + utc.authenticate(&TorAuthData::SafeCookie(Cow::Owned(cookie))).await.unwrap(); + // test conn further? + }); + } + + #[test] + fn test_can_auto_auth_with_null() { + let _c = run_testing_tor_instance( + &[ + "--DisableNetwork", "1", + "--ControlPort", &TOR_TESTING_PORT.to_string(), + // "--CookieAuthentication", "1", + ]); + + block_on_with_env(async move { + let s = TcpStream::connect(&format!("127.0.0.1:{}", TOR_TESTING_PORT)).await.unwrap(); + let mut utc = UnauthenticatedConn::new(s); + let proto_info = utc.load_protocol_info().await.unwrap(); + let ad = proto_info.make_auth_data().unwrap().unwrap(); + utc.authenticate(&ad).await.unwrap(); + // test conn further? + }); + } + + #[test] + fn test_can_auto_auth_with_cookie() { + let _c = run_testing_tor_instance( + &[ + "--DisableNetwork", "1", + "--ControlPort", &TOR_TESTING_PORT.to_string(), + "--CookieAuthentication", "1", + ]); + + block_on_with_env(async move { + let s = TcpStream::connect(&format!("127.0.0.1:{}", TOR_TESTING_PORT)).await.unwrap(); + let mut utc = UnauthenticatedConn::new(s); + let proto_info = utc.load_protocol_info().await.unwrap(); + println!("{:#?}", proto_info); + let ad = proto_info.make_auth_data().unwrap().unwrap(); + utc.authenticate(&ad).await.unwrap(); + // test conn further? + }); + } +} diff --git a/src/control/mod.rs b/src/control/mod.rs new file mode 100644 index 0000000..3a0c6aa --- /dev/null +++ b/src/control/mod.rs @@ -0,0 +1,8 @@ +//! Control module implements all the utilities required to talk to tor instance +//! and to give it some orders or get some info form it. + +pub use conn::*; +pub use primitives::*; + +mod primitives; +mod conn; diff --git a/src/control/primitives/auth.rs b/src/control/primitives/auth.rs new file mode 100644 index 0000000..57f5a53 --- /dev/null +++ b/src/control/primitives/auth.rs @@ -0,0 +1,129 @@ +use std::borrow::Cow; +use std::collections::HashSet; +use std::str::FromStr; +use std::io::Read; + +/// TorAuthMethod describes method which tor accepts as authentication method +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))] +pub enum TorAuthMethod { + /// Null - no authentication. Just issue authenticate command to be authenticated + Null, + + /// In order to authenticate password is required + HashedPassword, + + /// Cookie file has to be read in order to authenticate + Cookie, + + /// CookieFile has to be read and hashes with both server's and client's nonce has to match on server side. + /// This way evil server won't be able to copy response and act as an evil proxy + SafeCookie, +} + +impl FromStr for TorAuthMethod { + type Err = (); + + fn from_str(s: &str) -> Result { + let val = match s { + "NULL" => TorAuthMethod::Null, + "HASHEDPASSWORD" => TorAuthMethod::HashedPassword, + "COOKIE" => TorAuthMethod::Cookie, + "SAFECOOKIE" => TorAuthMethod::SafeCookie, + _ => { + return Err(()); + } + }; + Ok(val) + } +} + +/// Length of tor cookie in bytes. +/// Tor cookies have fixed length +pub const COOKIE_LENGTH: usize = 32; + +/// TorPreAuthInfo contains info which can be received from tor process before authentication +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))] +pub struct TorPreAuthInfo<'a> { + pub tor_version: Cow<'a, str>, + pub auth_methods: HashSet, + // for any modern os path is valid string. No need for base64-decode it or something like that. + pub cookie_file: Option>, +} + +impl<'a> TorPreAuthInfo<'a> { + /// make_auth_data is best-intent function which tries to create authentication data for given tor instance using data contained in + /// TorPreAuthInfo. + /// + /// For most usages it's the one which should be used rather than constructing data manually. + /// Exception here is password auth. + /// + /// # Reading files + /// If `Cookie` or `SafeCookie` auth is allowed this function may read cookie file(depending on availability of null auth). + /// It reads cookie from random path specified by `cookie_file`. + /// It reads exactly `COOKIE_LENGTH` bytes always. + /// + /// # Returns + /// It returns `Ok(None)` when make_auth_data is not able to use any authentication method which does not require any additional programmer care. + /// Example of such method which requires care is HashedPassword. It can't be automated by design. + /// + /// It returns `std::io::Error` when reading cookiefile fails. + pub fn make_auth_data(&self) -> Result>, std::io::Error> { + if self.auth_methods.contains(&TorAuthMethod::Null) { + Ok(Some(TorAuthData::Null)) + } else if self.auth_methods.contains(&TorAuthMethod::SafeCookie) && self.cookie_file.is_some() { + let mut f = std::fs::File::open(self.cookie_file.as_ref().unwrap().as_ref())?; + let mut buffer = Vec::new(); + buffer.resize(COOKIE_LENGTH, 0u8); + f.read_exact(&mut buffer[..])?; + + Ok(Some(TorAuthData::Cookie(Cow::Owned(buffer)))) + } else if self.auth_methods.contains(&TorAuthMethod::Cookie) && self.cookie_file.is_some() { + let mut f = std::fs::File::open(self.cookie_file.as_ref().unwrap().as_ref())?; + let mut buffer = Vec::new(); + buffer.resize(COOKIE_LENGTH, 0u8); + f.read_exact(&mut buffer[..])?; + + Ok(Some(TorAuthData::Cookie(Cow::Owned(buffer)))) + } else { + Ok(None) + } + } +} + +// TODO(teawithsand): test make_auth_data without tor + +/// TorAuthData contains all data required to authenticate single `UnauthenticatedConn` +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))] +pub enum TorAuthData<'a> { + /// null auth in fact does not require any data. + /// # Security note + /// Please note that in some cases this may cause secirity issue. + /// It should be NEVER used. + Null, + + /// Password auth requires password + HashedPassword(Cow<'a, str>), + + /// Cookie authentication requires contents of cookie + Cookie(Cow<'a, [u8]>), + + /// In fact it requires the same input as cookie but the procedure is different. + /// Note: It should be preferred over cookie when possible. + SafeCookie(Cow<'a, [u8]>), +} + +impl<'a> TorAuthData<'a> { + pub fn get_method(&self) -> TorAuthMethod { + match self { + TorAuthData::Null => TorAuthMethod::Null, + TorAuthData::HashedPassword(_) => TorAuthMethod::HashedPassword, + TorAuthData::Cookie(_) => TorAuthMethod::Cookie, + TorAuthData::SafeCookie(_) => TorAuthMethod::SafeCookie, + } + } +} + +// testing is in unauthenticated conn rs \ No newline at end of file diff --git a/src/control/primitives/error.rs b/src/control/primitives/error.rs new file mode 100644 index 0000000..312ee31 --- /dev/null +++ b/src/control/primitives/error.rs @@ -0,0 +1,65 @@ +use std::convert::TryFrom; + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))] +pub enum TorErrorKind { + ResourceExhausted, + SyntaxErrorProtocol, + UnrecognizedCmd, + UnimplementedCmd, + SyntaxErrorCmdArg, + UnrecognizedCmdArg, + AuthRequired, + BadAuth, + UnspecifiedTorError, + InternalError, + UnrecognizedEntity, + InvalidConfigValue, + InvalidDescriptor, + UnmanagedEntity, +} + +impl Into for TorErrorKind { + fn into(self) -> u32 { + match self { + TorErrorKind::ResourceExhausted => 451, + TorErrorKind::SyntaxErrorProtocol => 500, + TorErrorKind::UnrecognizedCmd => 510, + TorErrorKind::UnimplementedCmd => 511, + TorErrorKind::SyntaxErrorCmdArg => 512, + TorErrorKind::UnrecognizedCmdArg => 513, + TorErrorKind::AuthRequired => 514, + TorErrorKind::BadAuth => 515, + TorErrorKind::UnspecifiedTorError => 550, + TorErrorKind::InternalError => 551, + TorErrorKind::UnrecognizedEntity => 552, + TorErrorKind::InvalidConfigValue => 553, + TorErrorKind::InvalidDescriptor => 554, + TorErrorKind::UnmanagedEntity => 555, + } + } +} + +impl TryFrom for TorErrorKind { + type Error = (); + + fn try_from(code: u16) -> Result { + match code { + 451 => Ok(TorErrorKind::ResourceExhausted), + 500 => Ok(TorErrorKind::SyntaxErrorProtocol), + 510 => Ok(TorErrorKind::UnrecognizedCmd), + 511 => Ok(TorErrorKind::UnimplementedCmd), + 512 => Ok(TorErrorKind::SyntaxErrorCmdArg), + 513 => Ok(TorErrorKind::UnrecognizedCmdArg), + 514 => Ok(TorErrorKind::AuthRequired), + 515 => Ok(TorErrorKind::BadAuth), + 550 => Ok(TorErrorKind::UnspecifiedTorError), + 551 => Ok(TorErrorKind::InternalError), + 552 => Ok(TorErrorKind::UnrecognizedEntity), + 553 => Ok(TorErrorKind::InvalidConfigValue), + 554 => Ok(TorErrorKind::InvalidDescriptor), + 555 => Ok(TorErrorKind::UnmanagedEntity), + _ => Err(()) + } + } +} diff --git a/src/control/primitives/event.rs b/src/control/primitives/event.rs new file mode 100644 index 0000000..4aac701 --- /dev/null +++ b/src/control/primitives/event.rs @@ -0,0 +1,268 @@ +use std::borrow::Cow; +use std::str::FromStr; + +// note: torut DOES NOT IMPLEMENTS event parsing right now. +// take a look at AsyncEventKind there are so many of them! + +/// AsyncEvent is able to contain all info about async event which has been received from +/// tor process. +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))] +pub struct AsyncEvent<'a> { + /// code is tor's response code for asynchronous reply + /// According to current torCP spec it should be set to 650 always but it may change in future. + pub code: u16, + + /// lines contain raw response from tor process only minimally parsed by tor. + /// Lines' content is not parsed at all. It's listener's responsibility to do so. + pub lines: Vec>, +} + +/// AsyncEventKind right now torCP implements some limited amount of kinds of events +/// `AsyncEventKind` represents these kinds which are known at the moment of writing this code. +/// +/// # TorCP spec +/// Take a look at sections `4.1.*` which contain specification of all asynchronous events. +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))] +pub enum AsyncEventKind { + CircuitStatusChanged, + StreamStatusChanged, + ConnectionStatusChanged, + BandwidthUsedInTheLastSecond, + + // 4.1.5. there are three constant strings after 650 code + LogMessagesDebug, + LogMessagesInfo, + LogMessagesNotice, + LogMessagesWarn, + LogMessagesErr, + + NewDescriptorsAvailable, + NewAddressMapping, + DescriptorsUploadedToUsInOurRoleAsAuthoritativeServer, + OurDescriptorChanged, + + // 4.1.10 there are three constant strings after 650 code + StatusGeneral, + StatusClient, + StatusServer, + + OurSetOfGuardNodesHasChanged, + NetworkStatusHasChanged, + BandwidthUsedOnApplicationStream, + PerCountryClientStats, + NewConsensusNetworkStatusHasArrived, + NewCircuitBuildTimeHasBeenSet, + SignalReceived, + ConfigurationChanged, + CircuitStatusChangedSlightly, + PluggableTransportLaunched, + BandwidthUsedOnOROrDirOrExitConnection, + BandwidthUsedByAllStreamsAttachedToACircuit, + PerCircuitCellStatus, + TokenBucketsRefilled, + HiddenServiceDescriptors, + HiddenServiceDescriptorsContent, + NetworkLivenessHasChanged, + PluggableTransportLogs, + PluggableTransportStatus, +} + +// hacky macro which generates from str and into string based on mapping +// so I do not have to write same strings twice. + +// 2do: move enum definition into this macro +macro_rules! generate_from_into { + { + $typename:ident { + $( + // me: $enum_var:expr => $value:expr + // rustc: "arbitrary expressions aren't allowed in patterns" + $enum_var:ident => $value:tt + ),* + } + } => { + impl $typename { + /// get_identifier returns single word contained after 650 code + /// used to identify event. + /// + /// # Note + /// I haven't seen torCP specify this as "the good" way of identifying event. + /// But seems like until now all events are differentiated from each other this way. + pub fn get_identifier(/*& it's copy type*/ self) -> &'static str { + match self { + $( + Self::$enum_var => $value + ),* + } + } + } + + impl Into<&'static str> for $typename { + fn into(self) -> &'static str{ + self.get_identifier() + } + } + + // TODO(teawithsand): implement some function to get kind from first line of string + // rather than manually parsing it(split by space should work anyway) + + impl FromStr for $typename { + type Err = (); + + fn from_str(s: &str) -> Result { + let res = match s { + $( + $value => Self::$enum_var, + )* + _ => { + return Err(()); + } + }; + Ok(res) + } + } + } +} + +/* +AsyncEventKind::CircuitStatusChanged => "CIRC", +AsyncEventKind::StreamStatusChanged => "STREAM", +AsyncEventKind::ConnectionStatusChanged => "ORCONN", +AsyncEventKind::BandwidthUsedInTheLastSecond => "BW", + +AsyncEventKind::LogMessagesDebug => "DEBUG", +AsyncEventKind::LogMessagesInfo => "INFO", +AsyncEventKind::LogMessagesNotice => "NOTICE", +AsyncEventKind::LogMessagesWarn => "WARN", +AsyncEventKind::LogMessagesErr => "ERR", + +AsyncEventKind::NewDescriptorsAvailable => "NEWDESC", +AsyncEventKind::NewAddressMapping => "ADDRMAP", +AsyncEventKind::DescriptorsUploadedToUsInOurRoleAsAuthoritativeServer => "AUTHDIR_NEWDESCS", +AsyncEventKind::OurDescriptorChanged => "DESCCHANGED", + +AsyncEventKind::StatusGeneral => "STATUS_GENERAL", +AsyncEventKind::StatusClient => "STATUS_CLIENT", +AsyncEventKind::StatusServer => "STATUS_SERVER", + +AsyncEventKind::OurSetOfGuardNodesHasChanged => "GUARD", +AsyncEventKind::NetworkStatusHasChanged => "NS", +AsyncEventKind::BandwidthUsedOnApplicationStream => "STREAM_BW", +AsyncEventKind::PerCountryClientStats => "CLIENTS_SEEN", +AsyncEventKind::NewConsensusNetworkStatusHasArrived => "NEWCONSENSUS", +AsyncEventKind::NewCircuitBuildTimeHasBeenSet => "BUILDTIMEOUT_SET", +AsyncEventKind::SignalReceived => "SIGNAL", +AsyncEventKind::ConfigurationChanged => "CONF_CHANGED", +AsyncEventKind::CircuitStatusChangedSlightly => "CIRC_MINOR", +AsyncEventKind::PluggableTransportLaunched => "TRANSPORT_LAUNCHED", +AsyncEventKind::BandwidthUsedOnOROrDirOrExitConnection => "CONN_BW", +AsyncEventKind::BandwidthUsedByAllStreamsAttachedToACircuit => "CIRC_BW", +AsyncEventKind::PerCircuitCellStatus => "CELL_STATS", +AsyncEventKind::TokenBucketsRefilled => "TB_EMPTY", +AsyncEventKind::HiddenServiceDescriptors => "HS_DESC", +AsyncEventKind::HiddenServiceDescriptorsContent => "HS_DESC_CONTENT", +AsyncEventKind::NetworkLivenessHasChanged => "NETWORK_LIVENESS", +AsyncEventKind::PluggableTransportLogs => "PT_LOG", +AsyncEventKind::PluggableTransportStatus => "PT_STATUS", +*/ + +generate_from_into! { + AsyncEventKind { + CircuitStatusChanged => "CIRC", + StreamStatusChanged => "STREAM", + ConnectionStatusChanged => "ORCONN", + BandwidthUsedInTheLastSecond => "BW", + + LogMessagesDebug => "DEBUG", + LogMessagesInfo => "INFO", + LogMessagesNotice => "NOTICE", + LogMessagesWarn => "WARN", + LogMessagesErr => "ERR", + + NewDescriptorsAvailable => "NEWDESC", + NewAddressMapping => "ADDRMAP", + DescriptorsUploadedToUsInOurRoleAsAuthoritativeServer => "AUTHDIR_NEWDESCS", + OurDescriptorChanged => "DESCCHANGED", + + StatusGeneral => "STATUS_GENERAL", + StatusClient => "STATUS_CLIENT", + StatusServer => "STATUS_SERVER", + + OurSetOfGuardNodesHasChanged => "GUARD", + NetworkStatusHasChanged => "NS", + BandwidthUsedOnApplicationStream => "STREAM_BW", + PerCountryClientStats => "CLIENTS_SEEN", + NewConsensusNetworkStatusHasArrived => "NEWCONSENSUS", + NewCircuitBuildTimeHasBeenSet => "BUILDTIMEOUT_SET", + SignalReceived => "SIGNAL", + ConfigurationChanged => "CONF_CHANGED", + CircuitStatusChangedSlightly => "CIRC_MINOR", + PluggableTransportLaunched => "TRANSPORT_LAUNCHED", + BandwidthUsedOnOROrDirOrExitConnection => "CONN_BW", + BandwidthUsedByAllStreamsAttachedToACircuit => "CIRC_BW", + PerCircuitCellStatus => "CELL_STATS", + TokenBucketsRefilled => "TB_EMPTY", + HiddenServiceDescriptors => "HS_DESC", + HiddenServiceDescriptorsContent => "HS_DESC_CONTENT", + NetworkLivenessHasChanged => "NETWORK_LIVENESS", + PluggableTransportLogs => "PT_LOG", + PluggableTransportStatus => "PT_STATUS" + } +} + +/* +// implemented by above macro +impl AsyncEventKind { + /// get_identifier returns single word contained after 650 code + /// used to identify event. + /// + /// # Note + /// I haven't seen torCP specify this as "the good" way of identifying event. + /// But seems like until now all events are differentiated from each other this way. + pub fn get_identifier(&self) -> &'static str { + match self { + AsyncEventKind::CircuitStatusChanged => "CIRC", + AsyncEventKind::StreamStatusChanged => "STREAM", + AsyncEventKind::ConnectionStatusChanged => "ORCONN", + AsyncEventKind::BandwidthUsedInTheLastSecond => "BW", + + AsyncEventKind::LogMessagesDebug => "DEBUG", + AsyncEventKind::LogMessagesInfo => "INFO", + AsyncEventKind::LogMessagesNotice => "NOTICE", + AsyncEventKind::LogMessagesWarn => "WARN", + AsyncEventKind::LogMessagesErr => "ERR", + + AsyncEventKind::NewDescriptorsAvailable => "NEWDESC", + AsyncEventKind::NewAddressMapping => "ADDRMAP", + AsyncEventKind::DescriptorsUploadedToUsInOurRoleAsAuthoritativeServer => "AUTHDIR_NEWDESCS", + AsyncEventKind::OurDescriptorChanged => "DESCCHANGED", + + AsyncEventKind::StatusGeneral => "STATUS_GENERAL", + AsyncEventKind::StatusClient => "STATUS_CLIENT", + AsyncEventKind::StatusServer => "STATUS_SERVER", + + AsyncEventKind::OurSetOfGuardNodesHasChanged => "GUARD", + AsyncEventKind::NetworkStatusHasChanged => "NS", + AsyncEventKind::BandwidthUsedOnApplicationStream => "STREAM_BW", + AsyncEventKind::PerCountryClientStats => "CLIENTS_SEEN", + AsyncEventKind::NewConsensusNetworkStatusHasArrived => "NEWCONSENSUS", + AsyncEventKind::NewCircuitBuildTimeHasBeenSet => "BUILDTIMEOUT_SET", + AsyncEventKind::SignalReceived => "SIGNAL", + AsyncEventKind::ConfigurationChanged => "CONF_CHANGED", + AsyncEventKind::CircuitStatusChangedSlightly => "CIRC_MINOR", + AsyncEventKind::PluggableTransportLaunched => "TRANSPORT_LAUNCHED", + AsyncEventKind::BandwidthUsedOnOROrDirOrExitConnection => "CONN_BW", + AsyncEventKind::BandwidthUsedByAllStreamsAttachedToACircuit => "CIRC_BW", + AsyncEventKind::PerCircuitCellStatus => "CELL_STATS", + AsyncEventKind::TokenBucketsRefilled => "TB_EMPTY", + AsyncEventKind::HiddenServiceDescriptors => "HS_DESC", + AsyncEventKind::HiddenServiceDescriptorsContent => "HS_DESC_CONTENT", + AsyncEventKind::NetworkLivenessHasChanged => "NETWORK_LIVENESS", + AsyncEventKind::PluggableTransportLogs => "PT_LOG", + AsyncEventKind::PluggableTransportStatus => "PT_STATUS", + } + } +} +*/ \ No newline at end of file diff --git a/src/control/primitives/mod.rs b/src/control/primitives/mod.rs new file mode 100644 index 0000000..0ea4a42 --- /dev/null +++ b/src/control/primitives/mod.rs @@ -0,0 +1,10 @@ +pub use auth::*; +pub use error::*; +pub use event::*; +pub use signal::*; + +mod auth; +mod error; +mod signal; +mod event; + diff --git a/src/control/primitives/signal.rs b/src/control/primitives/signal.rs new file mode 100644 index 0000000..5411abd --- /dev/null +++ b/src/control/primitives/signal.rs @@ -0,0 +1,77 @@ +use std::fmt::Display; +use std::str::FromStr; + +/// TorSignal describes tor's SIGNAL command argument +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))] +pub enum TorSignal { + // https://gitweb.torproject.org/torspec.git/tree/control-spec.txt + // line 429 + Reload, + Shutdown, + Dump, + Debug, + Halt, + Hup, + Int, + Usr1, + Usr2, + Term, + NewNym, + ClearDNSCache, + Heartbeat, + Active, + Dormant, +} + + +impl Display for TorSignal { + //noinspection SpellCheckingInspection + fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + let text = match self { + TorSignal::Reload => "RELOAD", + TorSignal::Shutdown => "SHUTDOWN", + TorSignal::Dump => "DUMP", + TorSignal::Debug => "DEBUG", + TorSignal::Halt => "HALT", + TorSignal::Hup => "HUP", + TorSignal::Int => "INT", + TorSignal::Usr1 => "USR1", + TorSignal::Usr2 => "USR2", + TorSignal::Term => "TERM", + TorSignal::NewNym => "NEWNYM", + TorSignal::ClearDNSCache => "CLEARDNSCACHE", + TorSignal::Heartbeat => "HEARTBEAT", + TorSignal::Active => "ACTIVE", + TorSignal::Dormant => "DORMANT", + }; + write!(f, "{}", text) + } +} + +impl FromStr for TorSignal { + type Err = (); + + //noinspection SpellCheckingInspection + fn from_str(s: &str) -> Result { + let signal = match s { + "RELOAD" => TorSignal::Reload, + "SHUTDOWN" => TorSignal::Shutdown, + "DUMP" => TorSignal::Dump, + "DEBUG" => TorSignal::Debug, + "HALT" => TorSignal::Halt, + "HUP" => TorSignal::Hup, + "INT" => TorSignal::Int, + "USR1" => TorSignal::Usr1, + "USR2" => TorSignal::Usr2, + "TERM" => TorSignal::Term, + "NEWNYM" => TorSignal::NewNym, + "CLEARDNSCACHE" => TorSignal::ClearDNSCache, + "HEARTBEAT" => TorSignal::Heartbeat, + "ACTIVE" => TorSignal::Active, + "DORMANT" => TorSignal::Dormant, + _ => return Err(()), + }; + Ok(signal) + } +} diff --git a/src/fuzz.rs b/src/fuzz.rs new file mode 100644 index 0000000..cdb1ff0 --- /dev/null +++ b/src/fuzz.rs @@ -0,0 +1,67 @@ +use std::io::Cursor; +use std::str::FromStr; + +use crate::control::conn::{Conn, UnauthenticatedConn}; + +#[cfg(feature = "v3")] +use crate::onion::OnionAddressV3; +use crate::utils::{BASE32_ALPHA, block_on, parse_single_key_value, unquote_string}; + +pub fn fuzz_unquote_string(data: &[u8]) { + if let Ok(data) = std::str::from_utf8(data) { + let (offset, _res) = unquote_string(data); + if let Some(offset) = offset { + assert_eq!(data.as_bytes()[offset], b'"'); + } + } +} + +pub fn fuzz_parse_single_key_value(data: &[u8]) { + if data.len() < 1 { + return; + } + let must_be_quoted = data[0] % 2 == 0; + let data = &data[1..]; + if let Ok(data) = std::str::from_utf8(data) { + let _ = parse_single_key_value(data); + } +} + +#[cfg(feature = "control")] +/// Note: in order to run this fuzz fn modify cargo.toml to include full tokio(with runtime) +/// Right now rufuzz.py does not fetches dev dependencies for fuzzing +pub fn fuzz_conn_parse_response(data: &[u8]) { + block_on(async move { + let mut s = Cursor::new(data); + let mut c = Conn::new(s); + if let Ok((code, data)) = c.receive_data().await { + assert!(code >= 0 && code <= 999); + } + }); +} + +#[cfg(feature = "control")] +/// Note: in order to run this fuzz fn modify cargo.toml to include full tokio(with runtime) +/// Right now rufuzz.py does not fetches dev dependencies for fuzzing +pub fn fuzz_unauthenticated_conn_parse_protocol_info(data: &[u8]) { + block_on(async move { + let mut s = Cursor::new(data); + let mut c = Conn::new(s); + let mut c = UnauthenticatedConn::from(c); + let _ = c.read_protocol_info().await; + }); +} + +#[cfg(feature = "v3")] +pub fn fuzz_deserialize_onion_address_v3_from_text(data: &[u8]) { + if let Ok(data) = std::str::from_utf8(data) { + let _ = OnionAddressV3::from_str(data); + } +} + +// TODO(teawithsand): get some deserialization crate which is easy for fuzzer(bincode?) and fuzz deserialization of onion services +// from serde +/* +pub fn fuzz_deserialize_onion_service_v2(data: &[u8]) { +} +*/ \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..da05c2e --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,27 @@ +#![allow(deprecated)] // torut uses deprecated v2 services for now. this quick hack prevents warning from displaying themselves +//! Torut implements tor control protocol [described here](https://gitweb.torproject.org/torspec.git/tree/control-spec.txt) +//! +//! Right now torut does not implement all methods but it gives access to raw calls so you can use it. +//! If something does not work or you would like to see some functionality implemented by design open an issue or PR. +//! +//! # Usage security +//! Take a look at security considerations section of `README.MD` + +#![forbid(unsafe_code)] + +#[macro_use] +extern crate derive_more; +#[cfg(feature = "serialize")] +#[macro_use] +extern crate serde_derive; + +pub mod onion; + +#[cfg(feature = "control")] +pub mod control; + +#[allow(dead_code)] // prevents emitting warnings when control feature is skipped +pub mod utils; + +#[cfg(fuzzing)] +pub mod fuzz; \ No newline at end of file diff --git a/src/onion/builder.rs b/src/onion/builder.rs new file mode 100644 index 0000000..895f2fd --- /dev/null +++ b/src/onion/builder.rs @@ -0,0 +1,84 @@ +//! Builder service module contains things required to spin up new onion service on local machine + +// TODO(teawithsand): Cleanup this builder stuff + +use std::collections::{HashMap, HashSet}; +use std::fmt; +use std::fmt::Display; +use std::net::IpAddr; + +#[cfg(feature = "v3")] +use crate::onion::{TorPublicKeyV3, TorSecretKeyV3}; +use crate::onion::common::TorSecretKey; + +#[derive(Debug, Clone)] +pub struct OnionServiceBuilder { + pub(crate) key: Option, + pub(crate) ports_mapping: HashMap, + pub(crate) max_streams: Option, + pub(crate) client_auth: HashMap, + pub(crate) onion_service_flags: HashSet, +} + +#[derive(Debug, Copy, Clone)] +#[derive(PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))] +pub enum OnionServiceFlag { + DiscardPK, + Detach, + // BasicAuth, // Not set here. Set client_auth in order to set basic_auth flag + NonAnonymous, + MaxStreamsCloseCircuit, +} + +impl Display for OnionServiceFlag { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + let text = match self { + OnionServiceFlag::Detach => "Detach", + OnionServiceFlag::DiscardPK => "DiscardPK", + OnionServiceFlag::NonAnonymous => "NonAnonymous", + OnionServiceFlag::MaxStreamsCloseCircuit => "MaxStreamsCloseCircuit", + }; + write!(f, "{}", text) + } +} + +impl OnionServiceBuilder { + pub fn new() -> Self { + Self { + key: None, + ports_mapping: HashMap::new(), + max_streams: None, + client_auth: HashMap::new(), + onion_service_flags: HashSet::new(), + } + } + + pub fn set_key(&mut self, sk: TorSecretKey) { + self.key = Some(sk); + } + + pub fn set_max_streams(&mut self, streams: u16) { + self.max_streams = Some(streams); + } + + pub fn set_port_mapping(&mut self, local: IpAddr, remote: u16) { + self.ports_mapping.insert(remote, local); + } + + pub fn set_flags(&mut self, flags: HashSet) { + self.onion_service_flags = flags; + } +} + +pub enum RunningOnionServiceKeyPair { + #[cfg(feature = "v3")] + V3(TorPublicKeyV3, TorSecretKeyV3), +} + +/// RunningOnionService represents +pub struct RunningOnionService { + pub flags: HashSet, + pub key_pair: RunningOnionServiceKeyPair, + pub client_auth: HashMap, +} diff --git a/src/onion/common.rs b/src/onion/common.rs new file mode 100644 index 0000000..ca96a5c --- /dev/null +++ b/src/onion/common.rs @@ -0,0 +1,50 @@ +use std::fmt::Display; + + +#[cfg(feature = "v3")] +use crate::onion::{OnionAddressV3, TorPublicKeyV3, TorSecretKeyV3}; + +#[derive(Debug, Clone, PartialEq, Eq, From, TryInto)] +#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))] +pub enum TorSecretKey { + // leave this type, since some day tor may introduce v4 addresses + // for instance quantum secure one + + #[cfg(feature = "v3")] + V3(TorSecretKeyV3), +} + +#[derive(Debug, Clone, PartialEq, Eq, From, TryInto)] +#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))] +pub enum OnionAddress { + // leave this type, since some day tor may introduce v4 addresses + // for instance quantum secure one + + #[cfg(feature = "v3")] + V3(OnionAddressV3), +} + +impl Display for OnionAddress { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + match self { + #[cfg(feature = "v3")] + OnionAddress::V3(a) => a.fmt(f), + } + } +} + +impl OnionAddress { + pub fn get_address_without_dot_onion(&self) -> String { + match self { + #[cfg(feature = "v3")] + OnionAddress::V3(a) => a.get_address_without_dot_onion(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, From, TryInto)] +#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))] +pub enum TorPublicKey { + #[cfg(feature = "v3")] + V3(TorPublicKeyV3), +} \ No newline at end of file diff --git a/src/onion/mod.rs b/src/onion/mod.rs new file mode 100644 index 0000000..3ea4e90 --- /dev/null +++ b/src/onion/mod.rs @@ -0,0 +1,13 @@ +//! Onion module implements all utilities required to work with onion addresses version three +//! Support for these may be enabled using cargo features. + +#[cfg(any(feature = "v3"))] +pub use common::*; +#[cfg(feature = "v3")] +pub use v3::*; +#[cfg(feature = "v3")] +mod v3; + +#[cfg(feature = "v3")] +mod common; + diff --git a/src/onion/v3/key.rs b/src/onion/v3/key.rs new file mode 100644 index 0000000..93f7f69 --- /dev/null +++ b/src/onion/v3/key.rs @@ -0,0 +1,156 @@ +use ed25519_dalek::{ExpandedSecretKey, PublicKey, SecretKey, SignatureError}; +use rand::thread_rng; + +use crate::onion::OnionAddressV3; +use crate::utils::BASE32_ALPHA; + +/// Standardises usage of Tor V3 public keys, which is 32 bytes +/// (equal to Ed25519 public key length) +pub const TORV3_PUBLIC_KEY_LENGTH: usize = ed25519_dalek::PUBLIC_KEY_LENGTH; + +/// Standardises usage of Tor V3 secret keys, which is 65 bytes +/// (equal to Ed25519 extended secret key length) +pub const TORV3_SECRET_KEY_LENGTH: usize = ed25519_dalek::EXPANDED_SECRET_KEY_LENGTH; + +/// TorPublicKeyV3 describes onion service's public key(use to connect to onion service) +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +#[repr(transparent)] +pub struct TorPublicKeyV3(pub(crate) [u8; TORV3_PUBLIC_KEY_LENGTH]); + +impl TorPublicKeyV3 { + /// Convert this Tor public key to a byte array. + #[inline] + pub fn to_bytes(&self) -> [u8; TORV3_PUBLIC_KEY_LENGTH] { + self.0 + } + + /// View this Tor public key as a byte array. + #[inline] + pub fn as_bytes(&self) -> &[u8; TORV3_PUBLIC_KEY_LENGTH] { + &self.0 + } + + /// Constructs Tor public key from a byte sequence, checking the validity + /// of the byte sequence as Ed25519 public key, and returning appropriate + /// error if the sequence does not represent a valid key. + /// + /// # Example + /// + /// ``` + /// # extern crate torut; + /// # + /// use torut::onion::TorPublicKeyV3; + /// use ed25519_dalek::SignatureError; + /// + /// # fn doctest() -> Result { + /// let public_key_bytes: [u8; 32] = [ + /// 215, 90, 152, 1, 130, 177, 10, 183, 213, 75, 254, 211, 201, 100, 7, 58, + /// 14, 225, 114, 243, 218, 166, 35, 37, 175, 2, 26, 104, 247, 7, 81, 26]; + /// + /// let public_key = TorPublicKeyV3::from_bytes(&public_key_bytes)?; + /// # + /// # Ok(public_key) + /// # } + /// # + /// # fn main() { + /// # doctest(); + /// # } + /// ``` + /// + /// # Returns + /// + /// A `Result` whose okay value is a valid `TorPublicKeyV3` or whose error + /// value is a `ed25519_dalek::SignatureError` describing the error that + /// occurred. It will be either: + /// * `InternalError::BytesLengthError` + /// * `InternalError::PointDecompressionError` + #[inline] + pub fn from_bytes(bytes: &[u8; TORV3_PUBLIC_KEY_LENGTH]) -> Result { + PublicKey::from_bytes(bytes).map(|_pk| TorPublicKeyV3(bytes.clone())) + } + + /// get_onion_address creates onion address from public key. + /// + /// It can be used in place of `OnionAddressV3::from`. + pub fn get_onion_address(&self) -> OnionAddressV3 { + OnionAddressV3::from(self) + } +} + +impl std::fmt::Debug for TorPublicKeyV3 { + fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + write!(f, "TorPublicKey({})", base32::encode(BASE32_ALPHA, &self.0)) + } +} + +impl std::fmt::Display for TorPublicKeyV3 { + fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + write!(f, "TorPublicKey({})", base32::encode(BASE32_ALPHA, &self.0)) + } +} + +// TODO(teawithsand): Add memory zeroing on drop +/// TorSecretKeyV3 describes onion service's secret key(used to host onion service) +/// In fact it can be treated as keypair because public key may be derived from secret one quite easily. +/// +/// It uses expanded secret key in order to support importing existing keys from tor. +#[derive(Clone)] +#[repr(transparent)] +#[derive(From, Into)] +pub struct TorSecretKeyV3([u8; TORV3_SECRET_KEY_LENGTH]); + +impl Eq for TorSecretKeyV3 {} + +impl PartialEq for TorSecretKeyV3 { + // is non constant time eq fine here? + fn eq(&self, other: &Self) -> bool { + self.0.iter().zip(other.0.iter()).all(|(b1, b2)| *b1 == *b2) + } +} + +impl TorSecretKeyV3 { + pub(crate) fn as_tor_proto_encoded(&self) -> String { + base64::encode(&self.0[..]) + } + + /// generate generates new `TorSecretKeyV3` + pub fn generate() -> Self { + let sk: SecretKey = SecretKey::generate(&mut thread_rng()); + let esk = ExpandedSecretKey::from(&sk); + TorSecretKeyV3(esk.to_bytes()) + } + + /// creates `TorPublicKeyV3` from this secret key + pub fn public(&self) -> TorPublicKeyV3 { + let esk = ExpandedSecretKey::from_bytes(&self.0).expect("Invalid secret key contained"); + TorPublicKeyV3(PublicKey::from(&esk).to_bytes()) + } + + pub fn as_bytes(&self) -> [u8; 64] { + self.0.clone() + } + + pub fn into_bytes(self) -> [u8; 64] { + self.0 + } +} + +impl std::fmt::Display for TorSecretKeyV3 { + fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + write!(f, "TorSecretKey(****)") + } +} + +impl std::fmt::Debug for TorSecretKeyV3 { + fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + write!(f, "TorSecretKey(****)") + } +} + +/* +impl Drop for TorSecretKeyV3 { + fn drop(&mut self) { + zero_memory(&mut self.0[..]); + } +} +*/ \ No newline at end of file diff --git a/src/onion/v3/mod.rs b/src/onion/v3/mod.rs new file mode 100644 index 0000000..556345d --- /dev/null +++ b/src/onion/v3/mod.rs @@ -0,0 +1,11 @@ +pub use key::*; +pub use onion::*; + +mod key; +mod onion; + +#[cfg(feature = "serialize")] +mod serde_key; + +#[cfg(feature = "serialize")] +mod serde_onion; \ No newline at end of file diff --git a/src/onion/v3/onion.rs b/src/onion/v3/onion.rs new file mode 100644 index 0000000..3211711 --- /dev/null +++ b/src/onion/v3/onion.rs @@ -0,0 +1,199 @@ +//! onion module contains utils for working with Tor's onion services + +use std::error::Error; +use std::fmt::{Display, Formatter}; +use std::fmt; +use std::str::FromStr; + +use sha3::Digest; + +use crate::onion::v3::TorPublicKeyV3; +use crate::utils::BASE32_ALPHA; + +/// 32 public key bytes + 2 bytes of checksum = 34 +/// (in onion address v3 there is one more byte - version eq to 3) +/// Checksum is embedded in order not to recompute it. +/// +/// This variable denotes byte length of OnionAddressV3. +pub const TORV3_ONION_ADDRESS_LENGTH_BYTES: usize = 34; + +/// OnionAddressV3 contains public part of Tor's onion service address version 3., +/// It can't contain invalid onion address +#[derive(Clone, Copy)] +pub struct OnionAddressV3([u8; TORV3_ONION_ADDRESS_LENGTH_BYTES]); + +impl PartialEq for OnionAddressV3 { + #[inline] + fn eq(&self, other: &Self) -> bool { + &self.0[..] == &other.0[..] + } +} + +impl Eq for OnionAddressV3 {} + +impl From<&TorPublicKeyV3> for OnionAddressV3 { + fn from(tpk: &TorPublicKeyV3) -> Self { + let mut buf = [0u8; 34]; + tpk.0.iter().copied().enumerate().for_each(|(i, b)| { + buf[i] = b; + }); + + let mut h = sha3::Sha3_256::new(); + h.update(b".onion checksum"); + h.update(&tpk.0); + h.update(b"\x03"); + + let res_vec = h.finalize().to_vec(); + buf[32] = res_vec[0]; + buf[33] = res_vec[1]; + Self(buf) + } +} + +impl std::fmt::Debug for OnionAddressV3 { + fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + write!( + f, + "OnionAddress({})", + base32::encode(BASE32_ALPHA, &(self.get_raw_bytes())[..]).to_ascii_lowercase(), + ) + } +} + +impl std::fmt::Display for OnionAddressV3 { + fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + write!( + f, + "{}.onion", + base32::encode(BASE32_ALPHA, &(self.get_raw_bytes())[..]).to_ascii_lowercase() + ) + } +} + +impl OnionAddressV3 { + #[inline] + pub fn get_address_without_dot_onion(&self) -> String { + base32::encode(BASE32_ALPHA, &(self.get_raw_bytes())[..]).to_ascii_lowercase() + } + + #[inline] + pub fn get_raw_bytes(&self) -> [u8; 35] { + let mut buf = [0u8; 35]; + buf[..34].clone_from_slice(&self.0); + buf[34] = 3; + buf + } + + #[inline] + pub fn get_public_key(&self) -> TorPublicKeyV3 { + let mut buf = [0u8; 32]; + buf[..].clone_from_slice(&self.0[..32]); + TorPublicKeyV3(buf) + } +} + +// soon it will be the only onion address to parse +// so let's leave name not fixed +// it should be OnionAddressV3ParseError +#[derive(Debug)] +pub enum OnionAddressParseError { + InvalidLength, + Base32Error, + InvalidChecksum, + InvalidVersion, +} + +impl Display for OnionAddressParseError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "Filed to parse OnionAddressV3") + } +} + +impl Error for OnionAddressParseError {} + +impl FromStr for OnionAddressV3 { + type Err = OnionAddressParseError; + + //noinspection SpellCheckingInspection + /// from_str parses OnionAddressV3 from string. + /// + /// Please note that it accepts address *without* .onion only. + fn from_str(raw_onion_address: &str) -> Result { + if raw_onion_address.as_bytes().len() != 56 { + return Err(OnionAddressParseError::InvalidLength); + } + let mut buf = [0u8; 56]; + raw_onion_address.as_bytes().iter().copied().enumerate().for_each(|(i, b)| { + buf[i] = b; + }); + + let res = match base32::decode(BASE32_ALPHA, raw_onion_address) { + None => return Err(OnionAddressParseError::Base32Error), + Some(data) => data, + }; + + // panic!("Out deserialized length: {}", ) + + // is this even possible? + if res.len() != 32 + 2 + 1 { + return Err(OnionAddressParseError::InvalidLength); + } + + if res[34] != 3 { + return Err(OnionAddressParseError::InvalidVersion); + } + + // Onion address v3 structure: + // p53lf57qovyuvwsc6xnrppyply3vtqm7l6pcobkmyqsiofyeznfu5uqd.onion + // 1. public key for ed25519 (32 bytes) + // 2. two first bytes of sha3_256 of checksum (two bytes) + // 3. binary three(0x03) (one byte) + // above things are base32 encoded and .onion is appended + + // onion service checksum = H(".onion checksum" || pubkey || version)[..2] + // where H is sha3_256 + + let mut h = sha3::Sha3_256::new(); + h.update(b".onion checksum"); + h.update(&res[..32]); + h.update(b"\x03"); + + let res_vec = h.finalize().to_vec(); + if res_vec[0] != res[32] || res_vec[1] != res[33] { + return Err(OnionAddressParseError::InvalidChecksum); + } + + let mut buf = [0u8; 34]; + for i in 0..32 { + buf[i] = res[i]; + } + buf[32] = res_vec[0]; + buf[33] = res_vec[1]; + + Ok(Self(buf)) + } +} + +#[cfg(test)] +mod test { + use super::*; + + //noinspection SpellCheckingInspection + #[test] + fn test_can_parse_onion_address() { + let oa = "p53lf57qovyuvwsc6xnrppyply3vtqm7l6pcobkmyqsiofyeznfu5uqd"; + assert_eq!( + OnionAddressV3::from_str(oa).unwrap().to_string(), + "p53lf57qovyuvwsc6xnrppyply3vtqm7l6pcobkmyqsiofyeznfu5uqd.onion" + ); + } + + //noinspection SpellCheckingInspection + #[test] + fn test_can_convert_to_public_key_and_vice_versa() { + let oa = OnionAddressV3::from_str("p53lf57qovyuvwsc6xnrppyply3vtqm7l6pcobkmyqsiofyeznfu5uqd").unwrap(); + let pk = oa.get_public_key(); + let oa2 = pk.get_onion_address(); + assert_eq!(oa, oa2); + } +} diff --git a/src/onion/v3/serde_key.rs b/src/onion/v3/serde_key.rs new file mode 100644 index 0000000..6ff568e --- /dev/null +++ b/src/onion/v3/serde_key.rs @@ -0,0 +1,94 @@ +use std::borrow::Cow; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +use crate::onion::v3::{TorPublicKeyV3, TorSecretKeyV3}; + +impl Serialize for TorSecretKeyV3 { + fn serialize(&self, serializer: S) -> Result<::Ok, ::Error> where + S: Serializer { + serializer.serialize_str(&base64::encode(&self.as_bytes()[..])) + } +} + +impl Serialize for TorPublicKeyV3 { + fn serialize(&self, serializer: S) -> Result<::Ok, ::Error> where + S: Serializer { + serializer.serialize_str(&base64::encode(&self.0[..])) + } +} + +impl<'de> Deserialize<'de> for TorSecretKeyV3 { + fn deserialize(deserializer: D) -> Result>::Error> where + D: Deserializer<'de> { + let text = >::deserialize(deserializer)?; + let raw = base64::decode(&text[..]).map_err(serde::de::Error::custom)?; + if raw.len() != 64 { + return Err(serde::de::Error::custom("Invalid secret key length")); + } + let mut buf = [0u8; 64]; + buf.clone_from_slice(&raw[..]); + Ok(Self::from(buf)) + } +} + +impl<'de> Deserialize<'de> for TorPublicKeyV3 { + fn deserialize(deserializer: D) -> Result>::Error> where + D: Deserializer<'de> { + let text = >::deserialize(deserializer)?; + let raw = base64::decode(&text[..]).map_err(serde::de::Error::custom)?; + if raw.len() != 32 { + return Err(serde::de::Error::custom("Invalid secret key length")); + } + let mut buf = [0u8; 32]; + for i in 0..32 { + buf[i] = raw[i]; + } + Ok(Self(buf)) + } +} + +#[cfg(test)] +mod test { + use std::io::Cursor; + use super::*; + + #[test] + fn test_can_serialize_and_deserialize_secret_key() { + let sk = TorSecretKeyV3::generate(); + let data = serde_json::to_vec(&sk).unwrap(); + let rsk: TorSecretKeyV3 = serde_json::from_slice(&data).unwrap(); + + assert_eq!(sk, rsk); + } + + #[test] + fn test_can_serialize_and_deserialize_public_key() { + let pk = TorSecretKeyV3::generate().public(); + let data = serde_json::to_vec(&pk).unwrap(); + let rpk: TorPublicKeyV3 = serde_json::from_slice(&data).unwrap(); + + assert_eq!(pk, rpk); + } + + #[test] + fn test_can_serialize_and_deserialize_secret_key_with_no_borrowing() { + let sk = TorSecretKeyV3::generate(); + let data = serde_json::to_vec(&sk).unwrap(); + + let mut c = Cursor::new(&data); + let rsk: TorSecretKeyV3 = serde_json::from_reader(&mut c).unwrap(); + + assert_eq!(sk, rsk); + } + + #[test] + fn test_can_serialize_and_deserialize_public_key_with_no_borrowing() { + let pk = TorSecretKeyV3::generate().public(); + let data = serde_json::to_vec(&pk).unwrap(); + + let mut c = Cursor::new(&data); + let rpk: TorPublicKeyV3 = serde_json::from_reader(&mut c).unwrap(); + + assert_eq!(pk, rpk); + } +} \ No newline at end of file diff --git a/src/onion/v3/serde_onion.rs b/src/onion/v3/serde_onion.rs new file mode 100644 index 0000000..80aa3ef --- /dev/null +++ b/src/onion/v3/serde_onion.rs @@ -0,0 +1,24 @@ +use std::str::FromStr; + +use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; + +use crate::onion::OnionAddressV3; + +impl Serialize for OnionAddressV3 { + fn serialize(&self, serializer: S) -> Result<::Ok, ::Error> where + S: Serializer { + let res = self.get_address_without_dot_onion(); + serializer.serialize_str(&res) + } +} + +impl<'de> Deserialize<'de> for OnionAddressV3 { + //noinspection SpellCheckingInspection + fn deserialize(deserializer: D) -> Result>::Error> where + D: Deserializer<'de> { + let raw_onion_addr = <&str>::deserialize(deserializer)?; + Ok(Self::from_str(raw_onion_addr).map_err(de::Error::custom)?) + } +} + +// TODO(teawithsand): testing for these \ No newline at end of file diff --git a/src/utils/connect.rs b/src/utils/connect.rs new file mode 100644 index 0000000..e1b8459 --- /dev/null +++ b/src/utils/connect.rs @@ -0,0 +1 @@ +// TODO(teawithsand): Connecting through tor utils here(de facto socks utils) \ No newline at end of file diff --git a/src/utils/key_value.rs b/src/utils/key_value.rs new file mode 100644 index 0000000..6efcdc6 --- /dev/null +++ b/src/utils/key_value.rs @@ -0,0 +1,80 @@ +/// parse_single_key_value parses response in following format: +/// ```text +/// KEYWORD=VALUE +/// ... +/// ``` +/// +/// # Error +/// It returns an error: +/// - if there is no equal sign +/// - if data before equal sign is not `A-Za-z0-9_ -/$` ascii chars(notice space character) +/// - if value is quoted string and enclosing quote is not last character of text +/// +/// It *does not* return an error when key value is empty string so format is: `="asdf"` +/// +/// # Example +/// ``` +/// use torut::utils::parse_single_key_value; +/// assert_eq!(parse_single_key_value("KEY=VALUE"), Ok(("KEY", "VALUE"))); +/// assert_eq!(parse_single_key_value("INVALID"), Err(())); +/// assert_eq!(parse_single_key_value("VALID="), Ok(("VALID", ""))); +/// assert_eq!(parse_single_key_value("KEY=\"QUOTED VALUE\""), Ok(("KEY", "\"QUOTED VALUE\""))); +/// ``` +pub fn parse_single_key_value(text: &str) -> Result<(&str, &str), ()> +{ + assert!(text.len() <= std::usize::MAX - 1, "too long string provided to `parse_single_key_value`"); // notice this `+ 1` next to key offset + + let mut key_offset = 0; + for c in text.chars() { + if c == '=' { + break; + } + if c != ' ' && c != '-' && c != '_' && c != '/' && c != '$' && !c.is_ascii_alphanumeric() { + return Err(()); + } + key_offset += c.len_utf8(); + } + if key_offset >= text.len() { + return Err(()); // there is no equal sign + } + let key = &text[..key_offset]; + let value = &text[key_offset + 1..]; + + Ok((key, value)) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_can_parse_single_key_value() { + for (i, o) in [ + ( + "KEY=VALUE", + Some(("KEY", "VALUE")) + ), + ( + "$KE$Y=VALUE", + Some(("$KE$Y", "VALUE")) + ), + ( + "KEY=\"VALUE\"", + Some(("KEY", "\"VALUE\"")) + ), + ( + "KEY=Some\nMultiline\nValue\nIt\nHappens\nSometimes", + Some(("KEY", "Some\nMultiline\nValue\nIt\nHappens\nSometimes")), + ) + ].iter().cloned() { + if let Some(o) = o { + let (k, v) = o; + let (key, res) = parse_single_key_value(i).unwrap(); + assert_eq!(key, k); + assert_eq!(res, v); + } else { + let _ = parse_single_key_value(i).unwrap_err(); + } + } + } +} \ No newline at end of file diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 0000000..706c70b --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1,161 @@ +#[allow(unused_imports)] +use std::future::Future; + +mod quoted; +mod key_value; +mod run; +mod connect; + +#[cfg(testtor)] +mod testing; + + +pub use key_value::*; +pub use quoted::*; +pub use run::*; +pub use connect::*; + +#[cfg(testtor)] +pub use testing::*; + + +/// block_on creates tokio runtime for testing +#[cfg(any(test, fuzzing))] +pub(crate) fn block_on(f: F) -> O + where F: Future +{ + let rt = tokio::runtime::Builder::new_current_thread() + .build() + .unwrap(); + rt.block_on(f) +} + + +#[allow(dead_code)] +#[cfg(any(test, fuzzing))] +pub(crate) fn block_on_with_env(f: F) -> O + where F: Future +{ + let rt = tokio::runtime::Builder::new_multi_thread() + .enable_io() + .build() + .unwrap(); + rt.block_on(f) +} + +/// is_valid_keyword checks if given text is valid tor keyword for functions like `GETCONF` or `SETCONF` +/// +/// Note: this function was not tested against torCP but it's simple and robust and should work. +pub(crate) fn is_valid_keyword(config_option: &str) -> bool { + if config_option.is_empty() { + return false; + } + for c in config_option.chars() { + if !c.is_ascii_alphanumeric() { + return false; + } + } + true +} + +/// is_valid_event returns true if name is valid event name or false if it should not be used in context of +/// `SETEVENTS` call +pub(crate) fn is_valid_event(event_name: &str) -> bool { + if event_name.is_empty() { + return false; + } + for c in event_name.chars() { + if !c.is_ascii_uppercase() { + return false; + } + } + true +} + +/// is_valid_hostname checks if given text is valid hostname which can be resolved with tor +pub(crate) fn is_valid_hostname(config_option: &str) -> bool { + if config_option.is_empty() { + return false; + } + for c in config_option.chars() { + if !c.is_ascii_alphanumeric() && c != '.' && c != '-' { + return false; + } + } + true +} + +/// is_valid_keyword checks if given text is valid tor info keyword for `GETINFO` call +/// +/// Note: this function was not tested against torCP but it's simple and robust and should work. +pub(crate) fn is_valid_option(config_option: &str) -> bool { + if config_option.is_empty() { + return false; + } + for c in config_option.chars() { + if !c.is_ascii() || c == '\r' || c == '\n' { + return false; + } + } + if !config_option.chars().nth(0).unwrap().is_ascii_alphanumeric() { + return false; + } + if !config_option.chars().rev().nth(0).unwrap().is_ascii_alphanumeric() { + return false; + } + true +} + + +/// BASE32_ALPHA to use when encoding base32 stuff +#[allow(dead_code)] // not used when onion service v2 enabled +pub(crate) const BASE32_ALPHA: base32::Alphabet = base32::Alphabet::RFC4648 { + padding: false, +}; + +/// octal_ascii_triple_to_byte converts three octal ascii chars to single byte +/// `None` is returned if any char is not valid octal byte OR value is greater than byte +pub(crate) fn octal_ascii_triple_to_byte(data: [u8; 3]) -> Option { + // be more permissive. Allow non-ascii digits AFTER ascii digit sequence + /* + if data.iter().copied().any(|c| c < b'0' || c > b'7') { + return None; + } + */ + let mut res = 0; + let mut pow = 1; + let mut used_any = false; + + for b in data.iter().copied().rev() { + if b < b'0' || b > b'7' { + break; + } + used_any = true; + let b = b as u16; + res += (b - ('0' as u16)) * pow; + pow *= 8; + } + + if !used_any || res > std::u8::MAX as u16 { + return None; + } + return Some(res as u8); +} + + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_can_decode_octal_ascii_triple() { + for (i, o) in [ + (b"\0\00", Some(0)), + (b"123", Some(83)), + (&[0u8, 0, 48], Some(0)), + (&[50u8, 49, 51], Some(139)), + ].iter().cloned() { + assert_eq!(octal_ascii_triple_to_byte(*i), o); + } + } +} diff --git a/src/utils/quoted.rs b/src/utils/quoted.rs new file mode 100644 index 0000000..3d74cd7 --- /dev/null +++ b/src/utils/quoted.rs @@ -0,0 +1,343 @@ +use std::borrow::Cow; +use std::iter; +use std::string::FromUtf8Error; + +use crate::utils::octal_ascii_triple_to_byte; + +#[derive(Debug, From)] +pub struct UnquoteStringError { + pub error: FromUtf8Error +} + +impl Into> for UnquoteStringError { + fn into(self) -> Vec { + self.into_bytes() + } +} + +impl UnquoteStringError { + pub fn into_bytes(self) -> Vec { + self.error.into_bytes() + } +} + +/// unquote_string performs string unquoting +/// According to torCP docs it parses `QuotedString` token. +/// +/// # Note +/// In order to be quoted text MUST start with '"' char(no white chars allowed). +/// +/// # When quoted? +/// String is considered quoted when it starts with quote and contains at least one unescaped quote after first one. +/// Please note that above implies that first 3 chars out of 100 char string may be used to construct it. +/// +/// # Return value +/// If string is not quoted or no escape sequences are in use borrowed cow is returned. +/// If quoted string does not use any escape sequences borrowed cow is returned. +/// If string uses escape sequences which give valid utf8 string owned cow is returned. +/// If quoted string is longer than entire text +/// Otherwise error is returned +/// This function does not if content of quotes is valid utf8/is single line or if it does contains any sequence considered as +/// invalid when looking at standard this implies that return value MAY +/// contain non-ascii chars OR zero bytes. It's caller responsibility to filter them if needed. +/// +/// Second returned offset(first returned one) value is `Some` only when string is quoted. It returns byte offset of last char consumed in string unquoting. +/// Using `text.as_bytes()[idx]` where idx is given value should yield '"' char. +pub fn unquote_string(text: &str) -> (Option, Result, UnquoteStringError>) { + // as the docs says: + // The format is: + // RFC 2822(not entire ofc. Some random things needed to interpret the specification) + // ----- + // qtext = NO-WS-CTL / ; Non white space controls + // + // %d33 / ; The rest of the US-ASCII + // %d35-91 / ; characters not including "\" + // %d93-126 ; or the quote character + // + // qcontent = qtext / quoted-pair + // quoted-pair = ("\" text) / obs-qp + // obs-qp = "\" (%d0-127) + // ----- + // (Note: I guess text is a-zA-Z0-9) + // And from torCP spec: + // DQUOTE is this thing in the middle: ---> " <--- + // QuotedString = DQUOTE *qcontent DQUOTE + // + // + // "All 8-bit characters are permitted unless explicitly disallowed. In QuotedStrings, + // backslashes and quotes must be escaped; other characters need not be + // escaped." + + // quoted printable rules are simple: + // "For future-proofing, controller implementors MAY use the following + // rules to be compatible with buggy Tor implementations and with + // future ones that implement the spec as intended: + // Read \n \t \r and \0 ... \377 as C escapes. + // Treat a backslash followed by any other character as that character." + if text.len() == 0 { + return (None, Ok(Cow::Borrowed(&text[..0]))); + } + if text.len() >= 2 { + let end_of_quoted_string = { + let mut is_ignored = false; + let mut idx = 0; + let mut found = false; + // first one is our first quote(at least potentially) anyway - it can't be last quote + for c in text.chars().skip(1) { + if !is_ignored { + if c == '\\' { + is_ignored = true; + } else if c == '"' { + // we found it! first unquoted quote! + idx += c.len_utf8(); + found = true; + break; + } + } else { + is_ignored = false; + } + idx += c.len_utf8(); + } + if found { + debug_assert!(text.as_bytes()[idx] == b'"'); + Some(idx) + } else { + None + } + }; + return if text.as_bytes()[0] == b'\"' && end_of_quoted_string.is_some() { + let end_of_quoted_string = end_of_quoted_string.unwrap(); + + let text = &text[1..end_of_quoted_string]; + if text.chars().all(|c| c != '\\') { + // no escape sequences! + // just return value + return (Some(end_of_quoted_string), Ok(Cow::Borrowed(&text[..]))); + } + // just put escape seqs to vec and then create string + let mut res = Vec::new(); + let mut is_escaped = false; + + let mut escaped_char_buf = [0u8; 3]; + let mut escaped_char_buf_sz = 0; + // eprintln!("Unquoting: {:?}", text); + for c in text.chars() { + let mut char_to_process = Some(c); + while let Some(c) = char_to_process.take() { + // eprintln!("Got char: {:?}", c); + + if is_escaped { + if escaped_char_buf_sz == 0 { + match c { + 'n' => res.push(b'\n'), + 't' => res.push(b'\t'), + 'r' => res.push(b'\r'), + '"' => res.push(b'\"'), + '\\' => res.push(b'\\'), + c if c.is_ascii_digit() => { + // put char into escaped buffer and go to another iteration of loop + escaped_char_buf[0] = c as u8; + escaped_char_buf_sz += 1; + continue; + } + c => { + // put char as-is + res.extend(iter::repeat(0).take(c.len_utf8())); + let len = res.len(); + c.encode_utf8(&mut res[len - c.len_utf8()..]); + } + } + } else { + + // another octal digit + if c.is_ascii_digit() && /*is valid octal digit*/ (c as u8 - b'0') <= 7 && escaped_char_buf_sz < 3 { + escaped_char_buf[escaped_char_buf_sz] = c as u8; + escaped_char_buf_sz += 1; + continue; + } else { + // current char was not processed + // reschedule it to process + char_to_process = Some(c); + + // note: this code is copy pasted below + // consider fixing it as well when fixing this part + + // rotate buf in case there is less than required amount of chars + // so [1 0 0] sz = 1 becomes [0 0 1] + let len = escaped_char_buf.len(); + escaped_char_buf.rotate_right(len - escaped_char_buf_sz); + // eprintln!("Triple to byte: {:?}", escaped_char_buf); + if let Some(v) = octal_ascii_triple_to_byte(escaped_char_buf) { + // eprintln!("success: {}", v); + + res.push(v); + } else { + // eprintln!("failed: {:?}", escaped_char_buf); + + // push it as raw(not decoded) value without first char + // as if backslash was ignored + res.extend_from_slice(&escaped_char_buf[..escaped_char_buf_sz]); + } + } + } + escaped_char_buf = [0u8; 3]; + escaped_char_buf_sz = 0; + is_escaped = false; + } else { + if c == '\\' { + is_escaped = true; + } + // we have handled all quotes before + /* else if c == '\"' { + // apparently end of quoted string! + break; + } */ else { + res.extend(iter::repeat(0).take(c.len_utf8())); + let len = res.len(); + c.encode_utf8(&mut res[len - c.len_utf8()..]); + } + } + } + } + if escaped_char_buf_sz > 0 { + // eprintln!("Found one more octet(at least potential) to process!"); + + // TODO(teawithsand): clean it up. This is copy paste from above code processing octets. + let len = escaped_char_buf.len(); + escaped_char_buf.rotate_right(len - escaped_char_buf_sz); + // eprintln!("Triple to byte: {:?}", escaped_char_buf); + if let Some(v) = octal_ascii_triple_to_byte(escaped_char_buf) { + // eprintln!("success: {}", v); + + res.push(v); + } else { + // eprintln!("failed: {:?}", escaped_char_buf); + + // push it as raw(not decoded) value without first char + // as if backslash was ignored + res.extend_from_slice(&escaped_char_buf[..escaped_char_buf_sz]); + } + } + // eprintln!("RES: {:?}", res); + let res = String::from_utf8(res) + .map(|v| Cow::Owned(v)) + .map_err(|e| UnquoteStringError::from(e)); + (Some(end_of_quoted_string), res) + } else { + (None, Ok(Cow::Borrowed(&text[..]))) + }; + } + // ofc single char text can't be quoted string + (None, Ok(Cow::Borrowed(&text[..]))) +} + +/// quote_string takes arbitrary binary data and encodes it using octal encoding. +/// For \n \t and \r it uses these backslash notation rather than octal encoding. +/// +/// It's reverse function to `unquote_string`. +/// According to torCP docs it creates `QuotedString` token. +/// +/// # Example +/// ``` +/// use torut::utils::quote_string; +/// assert_eq!(quote_string(b"asdf"), r#""asdf""#); +/// assert_eq!(quote_string("ŁŁ".as_bytes()), r#""\305\201\305\201""#); +/// assert_eq!(quote_string("\n\r\t".as_bytes()), r#""\n\r\t""#); +/// assert_eq!(quote_string("\0\0\0".as_bytes()), r#""\0\0\0""#); +/// ``` +pub fn quote_string(text: &[u8]) -> String { + // res won't be shorter than text ever + let mut res = String::with_capacity(text.len() + 2); + res.push('\"'); + for b in text.iter().copied() { + match b { + b'\n' => res.push_str("\\n"), + b'\r' => res.push_str("\\r"), + b'\t' => res.push_str("\\t"), + b'\\' => res.push_str("\\\\"), + b'"' => res.push_str("\\\""), + b if b.is_ascii_alphanumeric() || b.is_ascii_punctuation() => { + res.push(b as char); + } + b => { + res.push('\\'); + // oct encode given char + let mut b = b; + let mut digit_count = 0; + let mut digits = [0u8; 3]; + if b > 0 { + while b > 0 { + digits[digit_count] = b % 8; + b = b / 8; + digit_count += 1; + } + } else { + // null byte is \0 but above algo won't find it out + digit_count = 1; + } + debug_assert!(digit_count >= 1); + for d in digits.iter().take(digit_count).rev() { + res.push((*d + b'0') as char); + } + } + } + } + res.push('\"'); + res +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_can_quote_and_unquote_string() { + for input in [ + "asdf", + "\0\0\0\0", + "ŁŁŁ", + r#"""""#, + "\n\t\r", + ].iter().cloned() { + assert_eq!( + input, + unquote_string("e_string(input.as_bytes())).1.unwrap().as_ref() + ) + } + } + + #[test] + fn test_can_unquote_string() { + for (input, output) in [ + ("not quoted string", (None, Ok("not quoted string"))), + ("\"and a quoted one\"", (Some(17), Ok("and a quoted one"))), + ("\"esc backslash \\\\ \"", (Some(18), Ok("esc backslash \\ "))), + (r#""\0\0\0\0\213\321\3\123\312\31\221\312""#, ( + Some(38), + Err(&[0u8, 0, 0, 0, 0o213, 0o321, 0o3, 0o123, 0o312, 0o31, 0o221, 0o312] as &[u8]) + )), + (r#""\0\0\0\0\213\321\3\123\312\31\221\31""#, ( + Some(37), + Err(&[0u8, 0, 0, 0, 0o213, 0o321, 0o3, 0o123, 0o312, 0o31, 0o221, 0o31] as &[u8]) + )), + (r#""\0\0\0\0\213\321\3\123\312\31\221\3""#, ( + Some(36), + Err(&[0u8, 0, 0, 0, 0o213, 0o321, 0o3, 0o123, 0o312, 0o31, 0o221, 0o3] as &[u8]) + )), + ("\"q\\\"q\"", (Some(5), Ok("q\"q"))), + ("\"first\"\"second\"", (Some(6), Ok("first"))), + ].iter().cloned() { + let (expected_offset, expected_value) = output; + let (offset, value) = unquote_string(input); + let value = value.map_err(|e| e.into_bytes()); + assert_eq!(offset, expected_offset); + assert_eq!( + value + .as_ref() + .map(|v| v.as_ref()) + .map_err(|e| e.as_ref()), + expected_value + ); + } + } +} \ No newline at end of file diff --git a/src/utils/run.rs b/src/utils/run.rs new file mode 100644 index 0000000..4e3c0be --- /dev/null +++ b/src/utils/run.rs @@ -0,0 +1,134 @@ +use std::io::{BufRead, BufReader}; +use std::ops::{Deref, DerefMut}; +use std::process::{Child, Command, Stdio}; + +/// AutoKillChild is kind of bag which contains `Child`. +/// It makes it automatically commit suicide after it gets dropped. +/// +/// It's designed to be used with tor running in rust application. AKC guarantees killing tor application on exit. +/// Note: It ignores process killing error in Drop. +pub struct AutoKillChild { + child: Option, +} + +impl From for AutoKillChild { + fn from(c: Child) -> Self{ + Self::new(c) + } +} + +impl AutoKillChild { + pub fn new(c: Child) -> Self{ + Self{ + child: Some(c) + } + } + + /// into_inner takes child from AutoKillChild. + /// It prevents child from dying automatically after it's dropped. + pub fn into_inner(mut self) -> Child { + self.child.take().unwrap() + } +} + +impl Drop for AutoKillChild { + fn drop(&mut self) { + if let Some(c) = &mut self.child { + // do not unwrap. Process might have died already. + let _ = c.kill(); + } + } +} + +impl Deref for AutoKillChild { + type Target = Child; + + #[inline] + fn deref(&self) -> &Self::Target { + self.child.as_ref().unwrap() + } +} + +impl DerefMut for AutoKillChild { + #[inline] + fn deref_mut(&mut self) -> &mut Self::Target { + self.child.as_mut().unwrap() + } +} + +// TODO(teawithsand): add bootstrapping runner here +/// run_tor runs new tor from specified path with specified args. +/// It should not be used when control port is disabled. +/// +/// # Parameters +/// * `path` - path to run tor binary. Note: if not found rust will query $PATH see docs for `std::process::Command::new` +/// * `args` - cli args provided to tor binary in raw form - array of strings. Format should be like: ["--DisableNetwork", "1"] +/// +/// For arguments reference take a look at: https://www.torproject.org/docs/tor-manual.html.en +/// +/// # Common parameters +/// 1. CookieAuthentication 1 - enables cookie authentication, since null may not be safe in some contexts +/// 2. ControlPort PORT - sets control port which should be used by tor controller, like torut, to controll this instance of tor. +/// +/// # Result detection note +/// It exists after finding "Opened Control listener" in the stdout. +/// Tor may *not* print such text to stdout. In that case this function will never exit(unless tor process dies). +/// +/// # Stdout note +/// This function uses `std::io::BufReader` to read data from stdout in order to decide if tor is running or not. +/// Dropping buf_reader drops it's internal buffer with data, which may cause partial data loss. +/// +/// For most cases it's fine, so it probably won't be fixed. +/// Alternative to this is char-by-char reading which is slower but should be also fine here. +pub fn run_tor(path: P, args: A) -> Result + where + A: AsRef<[T]>, + T: AsRef, + P: AsRef, +{ + let path = path.as_ref(); + let mut c = Command::new(path) + .args(args.as_ref().iter().map(|t| t.as_ref())) + // .env_clear() + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .stdin(Stdio::piped()) + .spawn()?; + { + // Stdio is piped so this works + { + let mut stdout = BufReader::new(c.stdout.as_mut().unwrap()); + + loop { + // wait until tor starts + // hacky but works + // stem does something simmilar internally but they use regexes to match bootstrapping messages from tor + // + // https://stem.torproject.org/_modules/stem/process.html#launch_tor + + let mut l = String::new(); + match stdout.read_line(&mut l) { + Ok(v) => v, + Err(e) => { + // kill if tor process hasn't died already + // this should make sure that tor process is not alive *almost* always + let _ = c.kill(); + return Err(e); + } + }; + + if l.contains("Opened Control listener") { + break; + } + } + + // buffered stdout is dropped here. + // It may cause partial data loss but it's better than dropping child. + } + } + Ok(c) +} + +// TODO(teawithsand): async run_tor + +// tests for these are in testing.rs \ No newline at end of file diff --git a/src/utils/testing.rs b/src/utils/testing.rs new file mode 100644 index 0000000..27eb977 --- /dev/null +++ b/src/utils/testing.rs @@ -0,0 +1,34 @@ +use crate::utils::{AutoKillChild, run_tor}; + +const ENV_VAR_NAME: &str = "TORUT_TESTING_TOR_BINARY"; + +// TODO(teawithsand): more this port to environment variable +/// TOR_TESTING_PORT used as default testing port for tor control proto listener +pub(crate) const TOR_TESTING_PORT: u16 = 49625; + +/// run_testing_tor_instance creates tor process which is used for testing purposes +/// +/// It takes tor binary path from env during runtime and should be used only during test builds. +/// It also tires to reset tor's env vars in order to provide reproducible tests. +/// +/// It also automatically waits until tor control proto port becomes available +pub(crate) fn run_testing_tor_instance(args: A) -> AutoKillChild + where + A: AsRef<[T]>, + T: AsRef +{ + let tor_path = std::env::var(ENV_VAR_NAME).unwrap(); + let c = AutoKillChild::from(run_tor(tor_path, args).unwrap()); + c +} + +#[cfg(test)] +mod test { + pub use super::*; + + #[test] + fn test_can_run_very_basic_tor_instance() { + let _c = run_testing_tor_instance(&["--DisableNetwork", "1", "--ControlPort", &TOR_TESTING_PORT.to_string()]); + // c.kill().unwrap(); + } +} \ No newline at end of file diff --git a/test_with_tor.sh b/test_with_tor.sh new file mode 100755 index 0000000..8f007da --- /dev/null +++ b/test_with_tor.sh @@ -0,0 +1,2 @@ +#!/bin/sh +RUSTFLAGS="--cfg testtor" cargo test -- --test-threads=1 From 1918d5f5653876b262c9d3b9f99da33883e458c0 Mon Sep 17 00:00:00 2001 From: Casey Marshall Date: Sun, 30 Apr 2023 20:08:27 -0500 Subject: [PATCH 2/2] chore: update dependencies --- Cargo.lock | 576 +++++++++++++++++++++++++++++------------------------ Cargo.toml | 3 + 2 files changed, 323 insertions(+), 256 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e872abe..7430c07 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "aead" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c192eb8f11fc081b0fe4259ba5af04217d4e0faddd02417310a927911abd7c8" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ "crypto-common", "generic-array", @@ -14,13 +14,62 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "0.7.20" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04" dependencies = [ "memchr", ] +[[package]] +name = "anstream" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6342bd4f5a1205d7f41e94a41a901f5647c938cdfa96036338e8533c9d6c2450" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is-terminal", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d" + +[[package]] +name = "anstyle-parse" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "anstyle-wincon" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" +dependencies = [ + "anstyle", + "windows-sys 0.48.0", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -29,9 +78,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "autotools" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8138adefca3e5d2e73bfba83bd6eeaf904b26a7ac1b4a19892cfe16cc7e1701" +checksum = "aef8da1805e028a172334c3b680f93e71126f2327622faef2ec3d893c0a4ad77" dependencies = [ "cc", ] @@ -84,15 +133,15 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" [[package]] name = "cc" -version = "1.0.77" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" [[package]] name = "cfg-if" @@ -102,9 +151,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chacha20" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7fc89c7c5b9e7a02dfe45cd2367bae382f9ed31c61ca8debe5f827c420a2f08" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" dependencies = [ "cfg-if", "cipher", @@ -137,40 +186,51 @@ dependencies = [ [[package]] name = "clap" -version = "4.1.4" +version = "4.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f13b9c79b5d1dd500d20ef541215a6423c75829ef43117e1b4d17fd8af0b5d76" +checksum = "8a1f23fa97e1d1641371b51f35535cb26959b8e27ab50d167a8b996b5bada819" dependencies = [ - "bitflags", + "clap_builder", "clap_derive", - "clap_lex", - "is-terminal", "once_cell", +] + +[[package]] +name = "clap_builder" +version = "4.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fdc5d93c358224b4d6867ef1356d740de2303e9892edc06c5340daeccd96bab" +dependencies = [ + "anstream", + "anstyle", + "bitflags", + "clap_lex", "strsim", - "termcolor", ] [[package]] name = "clap_derive" -version = "4.1.0" +version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "684a277d672e91966334af371f1a7b5833f9aa00b07c84e92fbce95e00208ce8" +checksum = "3f9644cd56d6b87dbe899ef8b053e331c0637664e9e21a33dfcdc36093f5c5c4" dependencies = [ "heck", - "proc-macro-error", "proc-macro2", "quote", - "syn", + "syn 2.0.15", ] [[package]] name = "clap_lex" -version = "0.3.1" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "783fe232adfca04f90f56201b26d79682d4cd2625e0bc7290b95123afe558ade" -dependencies = [ - "os_str_bytes", -] +checksum = "8a2dd5a6fe8c6e3502f568a6353e5273bbb15193ad9a89e457b9970798efbea1" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] name = "convert_case" @@ -180,9 +240,9 @@ checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" [[package]] name = "cpufeatures" -version = "0.2.5" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58" dependencies = [ "libc", ] @@ -246,7 +306,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn", + "syn 1.0.109", ] [[package]] @@ -260,29 +320,30 @@ dependencies = [ [[package]] name = "dirs" -version = "5.0.0" +version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dece029acd3353e3a58ac2e3eb3c8d6c35827a892edc6cc4138ef9c33df46ecd" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" dependencies = [ "dirs-sys", ] [[package]] name = "dirs-sys" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04414300db88f70d74c5ff54e50f9e1d1737d9a5b90f53fcf2e95ca2a9ab554b" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" dependencies = [ "libc", + "option-ext", "redox_users", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] name = "ed25519" -version = "1.5.2" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9c280362032ea4203659fc489832d0204ef09f247a0506f170dafcac08c369" +checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7" dependencies = [ "signature", ] @@ -303,19 +364,19 @@ dependencies = [ [[package]] name = "either" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" [[package]] name = "errno" -version = "0.2.8" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" dependencies = [ "errno-dragonfly", "libc", - "winapi", + "windows-sys 0.48.0", ] [[package]] @@ -330,47 +391,47 @@ dependencies = [ [[package]] name = "fastrand" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" dependencies = [ "instant", ] [[package]] name = "fs_extra" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2022715d62ab30faffd124d40b76f4134a550a87792276512b18d63272333394" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "futures-core" -version = "0.3.25" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" [[package]] name = "futures-macro" -version = "0.3.25" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.15", ] [[package]] name = "futures-task" -version = "0.3.25" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffb393ac5d9a6eaa9d3fdf37ae2776656b706e200c8e16b1bdb227f5198e6ea" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" [[package]] name = "futures-util" -version = "0.3.25" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" dependencies = [ "futures-core", "futures-macro", @@ -382,9 +443,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.6" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", @@ -403,9 +464,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" dependencies = [ "cfg-if", "libc", @@ -420,9 +481,9 @@ checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" -version = "0.1.19" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" dependencies = [ "libc", ] @@ -469,31 +530,32 @@ dependencies = [ [[package]] name = "io-lifetimes" -version = "1.0.5" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1abeb7a0dd0f8181267ff8adc397075586500b81b28a73e8a0208b00fc170fb3" +checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" dependencies = [ + "hermit-abi 0.3.1", "libc", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] name = "is-terminal" -version = "0.4.3" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e18b0a45d56fe973d6db23972bf5bc46f988a4a2385deac9cc29572f09daef" +checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" dependencies = [ "hermit-abi 0.3.1", "io-lifetimes", "rustix", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] name = "itoa" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" [[package]] name = "keccak" @@ -512,9 +574,9 @@ checksum = "6a987beff54b60ffa6d51982e1aa1146bc42f19bd26be28b0586f252fccf5317" [[package]] name = "libtor" -version = "47.8.0+0.4.7.x" +version = "47.13.0+0.4.7.x" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fc8b8da810d01f18307f256eae30f3f107e45637917d75809954ca0c825058c" +checksum = "1be588c6a2f02b860a1c0e3b2a59edcb171058f8da71b8ca0ddd7bb40f102c5c" dependencies = [ "libtor-derive", "libtor-sys", @@ -531,23 +593,23 @@ checksum = "177781b25e83853831c5af66320ceaf5e456e1b6d533426fcd9c7544b5543043" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] name = "libtor-src" -version = "47.10.0+0.4.7.10" +version = "47.13.0+0.4.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a11331b6b6fe7fe70680d3e39df108592117b8251ff54b496573ef75451243e6" +checksum = "e73bef51ecfbe7e63ce5cb8757ebc59d09dca6985da7f7470931ac22eab00719" dependencies = [ "fs_extra", ] [[package]] name = "libtor-sys" -version = "47.10.0+0.4.7.x" +version = "47.13.0+0.4.7.x" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a395e7e98b81fd2511e7bcdd644ab6eee8dcab47ff8e04f1e16b3684494888ac" +checksum = "eb0bc2cfc5d03851617d33508acc511e46f0c2b3cbc3cda85defcb50efa628bb" dependencies = [ "autotools", "cc", @@ -558,9 +620,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9702761c3935f8cc2f101793272e202c72b99da8f4224a19ddcf1279a6450bbf" +checksum = "56ee889ecc9568871456d42f603d6a0ce59ff328d291063a45cbdf0036baf6db" dependencies = [ "cc", "libc", @@ -570,9 +632,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.1.4" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" +checksum = "b64f40e5e03e0d54f03845c8197d0291253cdbedfb1cb46b13c2c117554a9f4c" [[package]] name = "lock_api" @@ -607,14 +669,14 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "mio" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" +checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" dependencies = [ "libc", "log", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.42.0", + "windows-sys 0.45.0", ] [[package]] @@ -629,19 +691,19 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" dependencies = [ - "hermit-abi 0.1.19", + "hermit-abi 0.2.6", "libc", ] [[package]] name = "once_cell" -version = "1.17.0" +version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" +checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" [[package]] name = "onionpipe" @@ -673,11 +735,10 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "openssl-sys" -version = "0.9.79" +version = "0.9.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5454462c0eced1e97f2ec09036abc8da362e66802f66fd20f86854d9d8cbcbc4" +checksum = "8e17f59264b2809d77ae94f0e1ebabc434773f370d6ca667bd223ea10e06cc7e" dependencies = [ - "autocfg", "cc", "libc", "pkg-config", @@ -685,10 +746,10 @@ dependencies = [ ] [[package]] -name = "os_str_bytes" -version = "6.4.1" +name = "option-ext" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "parking_lot" @@ -702,15 +763,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.5" +version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ff9f3fef3968a3ec5945535ed654cb38ff72d7495a25619e2247fb15a2ed9ba" +checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.2.16", "smallvec", - "windows-sys 0.42.0", + "windows-sys 0.45.0", ] [[package]] @@ -748,44 +809,20 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] - [[package]] name = "proc-macro2" -version = "1.0.47" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" +checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.21" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" dependencies = [ "proc-macro2", ] @@ -849,7 +886,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.8", + "getrandom 0.2.9", ] [[package]] @@ -870,22 +907,31 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags", +] + [[package]] name = "redox_users" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" dependencies = [ - "getrandom 0.2.8", - "redox_syscall", + "getrandom 0.2.9", + "redox_syscall 0.2.16", "thiserror", ] [[package]] name = "regex" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a" +checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370" dependencies = [ "aho-corasick", "memchr", @@ -894,18 +940,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.28" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" - -[[package]] -name = "remove_dir_all" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" -dependencies = [ - "winapi", -] +checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c" [[package]] name = "rustc_version" @@ -918,23 +955,23 @@ dependencies = [ [[package]] name = "rustix" -version = "0.36.8" +version = "0.37.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43abb88211988493c1abb44a70efa56ff0ce98f233b7b276146f1f3f7ba9644" +checksum = "8bbfc1d1c7c40c01715f47d71444744a81669ca84e8b63e25a55e169b1f86433" dependencies = [ "bitflags", "errno", "io-lifetimes", "libc", "linux-raw-sys", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] name = "ryu" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" +checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" [[package]] name = "salsa20" @@ -953,35 +990,35 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "semver" -version = "1.0.14" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4" +checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" [[package]] name = "serde" -version = "1.0.150" +version = "1.0.160" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e326c9ec8042f1b5da33252c8a37e9ffbd2c9bef0155215b6e6c80c790e05f91" +checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.150" +version = "1.0.160" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42a3df25b0713732468deadad63ab9da1f1fd75a48a15024b50363f128db627e" +checksum = "291a097c63d8497e00160b166a967a4a79c64f3facdd01cbd7502231688d77df" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.15", ] [[package]] name = "serde_json" -version = "1.0.93" +version = "1.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cad406b69c91885b5107daf2c29572f6c8cdb3c66826821e286c533490c0bc76" +checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" dependencies = [ "itoa", "ryu", @@ -1030,9 +1067,9 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" dependencies = [ "libc", ] @@ -1045,9 +1082,9 @@ checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" [[package]] name = "slab" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" +checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" dependencies = [ "autocfg", ] @@ -1060,9 +1097,9 @@ checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" [[package]] name = "socket2" -version = "0.4.7" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" dependencies = [ "libc", "winapi", @@ -1082,9 +1119,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "1.0.105" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b9b43d45702de4c839cb9b51d9f529c5dd26a4aff255b42b1ebc03e88ee908" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", @@ -1092,70 +1129,58 @@ dependencies = [ ] [[package]] -name = "synstructure" -version = "0.12.6" +name = "syn" +version = "2.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" dependencies = [ "proc-macro2", "quote", - "syn", - "unicode-xid", + "unicode-ident", ] [[package]] name = "tempfile" -version = "3.3.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998" dependencies = [ "cfg-if", "fastrand", - "libc", - "redox_syscall", - "remove_dir_all", - "winapi", -] - -[[package]] -name = "termcolor" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" -dependencies = [ - "winapi-util", + "redox_syscall 0.3.5", + "rustix", + "windows-sys 0.45.0", ] [[package]] name = "thiserror" -version = "1.0.37" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" +checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.37" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" +checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.15", ] [[package]] name = "tokio" -version = "1.23.0" +version = "1.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eab6d665857cc6ca78d6e80303a02cea7a7851e85dfbd77cbdc09bd129f1ef46" +checksum = "c3c786bf8134e5a3a166db9b29ab8f48134739014a3eca7bc6bfa95d673b136f" dependencies = [ "autocfg", "bytes", "libc", - "memchr", "mio", "num_cpus", "parking_lot", @@ -1163,18 +1188,18 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys 0.42.0", + "windows-sys 0.48.0", ] [[package]] name = "tokio-macros" -version = "1.8.2" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.15", ] [[package]] @@ -1192,8 +1217,6 @@ dependencies = [ [[package]] name = "torut" version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99febc413f26cf855b3a309c5872edff5c31e0ffe9c2fce5681868761df36f69" dependencies = [ "base32", "base64 0.13.1", @@ -1217,15 +1240,9 @@ checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" [[package]] name = "unicode-ident" -version = "1.0.5" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" - -[[package]] -name = "unicode-xid" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" +checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" [[package]] name = "universal-hash" @@ -1237,6 +1254,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + [[package]] name = "vcpkg" version = "0.2.15" @@ -1277,15 +1300,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" -[[package]] -name = "winapi-util" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" -dependencies = [ - "winapi", -] - [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -1294,84 +1308,135 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-sys" -version = "0.42.0" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows-targets 0.42.2", ] [[package]] name = "windows-sys" -version = "0.45.0" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets", + "windows-targets 0.48.0", ] [[package]] name = "windows-targets" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", ] +[[package]] +name = "windows-targets" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +dependencies = [ + "windows_aarch64_gnullvm 0.48.0", + "windows_aarch64_msvc 0.48.0", + "windows_i686_gnu 0.48.0", + "windows_i686_msvc 0.48.0", + "windows_x86_64_gnu 0.48.0", + "windows_x86_64_gnullvm 0.48.0", + "windows_x86_64_msvc 0.48.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" -version = "0.42.1" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_aarch64_msvc" -version = "0.42.1" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] name = "windows_i686_gnu" -version = "0.42.1" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" [[package]] name = "windows_i686_msvc" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" [[package]] name = "windows_x86_64_gnu" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] name = "windows_x86_64_gnullvm" -version = "0.42.1" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] name = "windows_x86_64_msvc" -version = "0.42.1" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" [[package]] name = "x25519-dalek" @@ -1399,21 +1464,20 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.5.7" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f" +checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" dependencies = [ "zeroize_derive", ] [[package]] name = "zeroize_derive" -version = "1.3.3" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44bf07cb3e50ea2003396695d58bf46bc9887a1f362260446fad6bc4e79bd36c" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn", - "synstructure", + "syn 2.0.15", ] diff --git a/Cargo.toml b/Cargo.toml index 51e0f97..e2b69a0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,3 +35,6 @@ nom = "7.1.3" crypto_box = "0.8.2" libc = "0.2.142" dirs = "5.0.0" + +[patch.crates-io] +torut = { path = "vendor/torut" }