diff --git a/Cargo.lock b/Cargo.lock index facd298..7e6f235 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1042,6 +1042,15 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "mktemp" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "975de676448231fcde04b9149d2543077e166b78fc29eae5aa219e7928410da2" +dependencies = [ + "uuid", +] + [[package]] name = "nom" version = "7.1.1" @@ -1602,6 +1611,7 @@ dependencies = [ "futures", "hyper-rustls", "log", + "mktemp", "pem", "rand", "reqwest", @@ -1873,6 +1883,15 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom", +] + [[package]] name = "version_check" version = "0.9.4" diff --git a/Cargo.toml b/Cargo.toml index a789475..ff673c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,4 +27,5 @@ rustls-pemfile = "1.0.1" rustls = "0.20.6" [dev-dependencies] -rand = "0.8.5" \ No newline at end of file +rand = "0.8.5" +mktemp = "0.4.1" \ No newline at end of file diff --git a/src/base/err_utils.rs b/src/base/err_utils.rs new file mode 100644 index 0000000..d9c10a7 --- /dev/null +++ b/src/base/err_utils.rs @@ -0,0 +1,7 @@ +use std::error::Error; +use std::io::ErrorKind; + +/// Encapsulate errors in [`std::io::Error`] with a message +pub fn encpasulate_error(e: E, msg: &str) -> std::io::Error { + std::io::Error::new(ErrorKind::Other, format!("{}: {}", msg, e)) +} diff --git a/src/base/mod.rs b/src/base/mod.rs index c09934b..b908837 100644 --- a/src/base/mod.rs +++ b/src/base/mod.rs @@ -1,4 +1,5 @@ pub mod cert_utils; +pub mod err_utils; mod structs; pub use structs::{RelayedPort, RemoteConfig}; diff --git a/src/tcp_relay_server/mod.rs b/src/tcp_relay_server/mod.rs index aefea39..8905c72 100644 --- a/src/tcp_relay_server/mod.rs +++ b/src/tcp_relay_server/mod.rs @@ -3,8 +3,8 @@ use std::sync::Arc; use actix_web::web::Data; use actix_web::{middleware, web, App, HttpRequest, HttpResponse, HttpServer, Responder}; +use crate::base::err_utils::encpasulate_error; use crate::base::{cert_utils, RelayedPort}; - use crate::tcp_relay_server::relay_ws::relay_ws; use crate::tcp_relay_server::server_config::ServerConfig; use crate::tcp_relay_server::tls_cert_client_verifier::CustomCertClientVerifier; @@ -54,7 +54,7 @@ pub async fn run_app(mut config: ServerConfig) -> std::io::Result<()> { // Read tokens from file, if any if let Some(file) = &config.tokens_file { std::fs::read_to_string(file) - .expect("Failed to read tokens file!") + .map_err(|e| encpasulate_error(e, "Failed to read tokens file!"))? .split('\n') .filter(|l| !l.is_empty()) .for_each(|t| config.tokens.push(t.to_string())); diff --git a/src/test/client_try_tls_while_there_is_no_tls.rs b/src/test/client_try_tls_while_there_is_no_tls.rs new file mode 100644 index 0000000..73590eb --- /dev/null +++ b/src/test/client_try_tls_while_there_is_no_tls.rs @@ -0,0 +1,55 @@ +use tokio::task; + +use crate::tcp_relay_client::client_config::ClientConfig; +use crate::tcp_relay_server::server_config::ServerConfig; +use crate::test::dummy_tcp_sockets::{wait_for_port, DummyTCPServer}; +use crate::test::{get_port_number, PortsAllocation, LOCALHOST}; + +const VALID_TOKEN: &str = "AvalidTOKEN"; + +fn port(index: u16) -> u16 { + get_port_number(PortsAllocation::ClientTryTLSWhileThereIsNoTLS, index) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 5)] +async fn valid_with_token_auth() { + let _ = env_logger::builder().is_test(true).try_init(); + + // Start internal service + let local_server = DummyTCPServer::start(port(1)).await; + tokio::spawn(async move { + local_server.loop_conn_square_operations().await; + }); + + let local_set = task::LocalSet::new(); + local_set + .run_until(async move { + wait_for_port(port(1)).await; + + // Start server relay + task::spawn_local(crate::tcp_relay_server::run_app(ServerConfig { + tokens: vec![VALID_TOKEN.to_string()], + tokens_file: None, + ports: vec![port(1)], + upstream_server: "127.0.0.1".to_string(), + listen_address: format!("127.0.0.1:{}", port(0)), + increment_ports: 1, + tls_cert: None, + tls_key: None, + tls_client_auth_root_cert: None, + tls_revocation_list: None, + })); + wait_for_port(port(0)).await; + + crate::tcp_relay_client::run_app(ClientConfig { + token: Some(VALID_TOKEN.to_string()), + relay_url: format!("https://{}:{}", LOCALHOST, port(0)), + listen_address: LOCALHOST.to_string(), + root_certificate: None, + ..Default::default() + }) + .await + .unwrap_err(); + }) + .await; +} diff --git a/src/test/dummy_tcp_sockets.rs b/src/test/dummy_tcp_sockets.rs index 03262f5..8b36ee2 100644 --- a/src/test/dummy_tcp_sockets.rs +++ b/src/test/dummy_tcp_sockets.rs @@ -91,6 +91,38 @@ impl DummyTCPServer { self.next_conn_square_operations().await } } + + /// Perform complex exchange: receive numbers from client and respond with their value + a given number + pub async fn next_conn_add_operations(&self, inc: i32) { + let (mut conn, _addr) = self + .0 + .accept() + .await + .expect("Could not open next connection!"); + + let mut buff: [u8; 100] = [0; 100]; + loop { + let size = conn.read(&mut buff).await.unwrap(); + if size == 0 { + break; + } + + let content = String::from_utf8_lossy(&buff[0..size]) + .to_string() + .parse::() + .unwrap(); + + conn.write_all((content + inc as i64).to_string().as_bytes()) + .await + .unwrap(); + } + } + + pub async fn loop_next_conn_add_operations(&self, inc: i32) { + loop { + self.next_conn_add_operations(inc).await + } + } } pub async fn dummy_tcp_client_read_conn(port: u16) -> Vec { @@ -152,6 +184,33 @@ pub async fn dummy_tcp_client_square_root_requests(port: u16, num_exchanges: usi } } +pub async fn dummy_tcp_client_additions_requests(port: u16, inc: i32, num_exchanges: usize) { + let mut socket = TcpStream::connect(format!("127.0.0.1:{}", port)) + .await + .expect("Failed to connect to dummy TCP server!"); + + let mut rng = rand::thread_rng(); + let mut buff: [u8; 100] = [0; 100]; + + for _ in 0..num_exchanges { + let num = rng.gen::() % 100; + socket.write_all(num.to_string().as_bytes()).await.unwrap(); + + let size = socket.read(&mut buff).await.unwrap(); + + if size == 0 { + panic!("Got empty response!"); + } + + let got = String::from_utf8_lossy(&buff[0..size]) + .to_string() + .parse::() + .unwrap(); + println!("{} + {} = {} (based on server response)", num, inc, got); + assert_eq!((num + inc) as i64, got); + } +} + /// Check whether a given port is open or not pub async fn is_port_open(port: u16) -> bool { match TcpStream::connect(("127.0.0.1", port)).await { @@ -175,8 +234,9 @@ pub async fn wait_for_port(port: u16) { mod test { use crate::test::dummy_tcp_sockets::{ - dummy_tcp_client_read_conn, dummy_tcp_client_square_root_requests, - dummy_tcp_client_write_conn, dummy_tcp_client_write_then_read_conn, DummyTCPServer, + dummy_tcp_client_additions_requests, dummy_tcp_client_read_conn, + dummy_tcp_client_square_root_requests, dummy_tcp_client_write_conn, + dummy_tcp_client_write_then_read_conn, DummyTCPServer, }; use crate::test::{get_port_number, PortsAllocation}; @@ -253,4 +313,27 @@ mod test { handle.await.unwrap(); } + + #[tokio::test] + async fn socket_dummy_addition_calculator() { + let listener = DummyTCPServer::start(port(5)).await; + let handle = tokio::spawn(async move { + listener.next_conn_add_operations(4).await; + }); + let data = dummy_tcp_client_write_then_read_conn(port(5), "5".as_bytes()).await; + assert_eq!(data, "9".as_bytes()); + + handle.await.unwrap(); + } + + #[tokio::test] + async fn socket_dummy_addition_calculator_multiple() { + let listener = DummyTCPServer::start(port(6)).await; + let handle = tokio::spawn(async move { + listener.next_conn_add_operations(7).await; + }); + dummy_tcp_client_additions_requests(port(6), 7, 10).await; + + handle.await.unwrap(); + } } diff --git a/src/test/invalid_token_file.rs b/src/test/invalid_token_file.rs new file mode 100644 index 0000000..b16b7c0 --- /dev/null +++ b/src/test/invalid_token_file.rs @@ -0,0 +1,28 @@ +use crate::tcp_relay_server::server_config::ServerConfig; +use crate::test::{get_port_number, PortsAllocation}; + +const INVALID_TOKEN: &str = "/tmp/a/token/file/that/does/not/exists"; + +fn port(index: u16) -> u16 { + get_port_number(PortsAllocation::InvalidTokenFile, index) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 5)] +async fn invalid_with_token_auth() { + let _ = env_logger::builder().is_test(true).try_init(); + + crate::tcp_relay_server::run_app(ServerConfig { + tokens: vec![], + tokens_file: Some(INVALID_TOKEN.to_string()), + ports: vec![port(1)], + upstream_server: "127.0.0.1".to_string(), + listen_address: format!("127.0.0.1:{}", port(0)), + increment_ports: 1, + tls_cert: None, + tls_key: None, + tls_client_auth_root_cert: None, + tls_revocation_list: None, + }) + .await + .unwrap_err(); +} diff --git a/src/test/mod.rs b/src/test/mod.rs index fd8d92c..9135a92 100644 --- a/src/test/mod.rs +++ b/src/test/mod.rs @@ -3,6 +3,12 @@ enum PortsAllocation { DummyTCPServer, ValidWithTokenAuth, InvalidWithTokenAuth, + ValidWithMultipleTokenAuth, + ValidWithTokenFile, + InvalidTokenFile, + ClientTryTLSWhileThereIsNoTLS, + ValidTokenWithCustomIncrement, + ValidWithTokenAuthMultiplePorts, } fn get_port_number(alloc: PortsAllocation, index: u16) -> u16 { @@ -13,5 +19,11 @@ const LOCALHOST: &str = "127.0.0.1"; mod dummy_tcp_sockets; +mod client_try_tls_while_there_is_no_tls; +mod invalid_token_file; mod invalid_with_token_auth; +mod valid_token_with_custom_increment; +mod valid_with_multiple_token_auth; mod valid_with_token_auth; +mod valid_with_token_auth_multiple_ports; +mod valid_with_token_file; diff --git a/src/test/valid_token_with_custom_increment.rs b/src/test/valid_token_with_custom_increment.rs new file mode 100644 index 0000000..c466b74 --- /dev/null +++ b/src/test/valid_token_with_custom_increment.rs @@ -0,0 +1,59 @@ +use tokio::task; + +use crate::tcp_relay_client::client_config::ClientConfig; +use crate::tcp_relay_server::server_config::ServerConfig; +use crate::test::dummy_tcp_sockets::{ + dummy_tcp_client_square_root_requests, wait_for_port, DummyTCPServer, +}; +use crate::test::{get_port_number, PortsAllocation, LOCALHOST}; + +const VALID_TOKEN: &str = "AvalidTOKEN"; + +fn port(index: u16) -> u16 { + get_port_number(PortsAllocation::ValidTokenWithCustomIncrement, index) +} + +#[tokio::test(flavor = "multi_thread")] +async fn valid_with_token_auth() { + let _ = env_logger::builder().is_test(true).try_init(); + + // Start internal service + let local_server = DummyTCPServer::start(port(1)).await; + tokio::spawn(async move { + local_server.loop_conn_square_operations().await; + }); + + let local_set = task::LocalSet::new(); + local_set + .run_until(async move { + wait_for_port(port(1)).await; + + // Start server relay + task::spawn_local(crate::tcp_relay_server::run_app(ServerConfig { + tokens: vec![VALID_TOKEN.to_string()], + tokens_file: None, + ports: vec![port(1)], + upstream_server: "127.0.0.1".to_string(), + listen_address: format!("127.0.0.1:{}", port(0)), + increment_ports: 3, + tls_cert: None, + tls_key: None, + tls_client_auth_root_cert: None, + tls_revocation_list: None, + })); + wait_for_port(port(0)).await; + + // Start client relay + task::spawn(crate::tcp_relay_client::run_app(ClientConfig { + token: Some(VALID_TOKEN.to_string()), + relay_url: format!("http://{}:{}", LOCALHOST, port(0)), + listen_address: LOCALHOST.to_string(), + root_certificate: None, + ..Default::default() + })); + wait_for_port(port(4)).await; + + dummy_tcp_client_square_root_requests(port(4), 10).await; + }) + .await; +} diff --git a/src/test/valid_with_multiple_token_auth.rs b/src/test/valid_with_multiple_token_auth.rs new file mode 100644 index 0000000..5b03c2e --- /dev/null +++ b/src/test/valid_with_multiple_token_auth.rs @@ -0,0 +1,63 @@ +use tokio::task; + +use crate::tcp_relay_client::client_config::ClientConfig; +use crate::tcp_relay_server::server_config::ServerConfig; +use crate::test::dummy_tcp_sockets::{ + dummy_tcp_client_square_root_requests, wait_for_port, DummyTCPServer, +}; +use crate::test::{get_port_number, PortsAllocation, LOCALHOST}; + +const VALID_TOKEN: &str = "AvalidTOKEN"; + +fn port(index: u16) -> u16 { + get_port_number(PortsAllocation::ValidWithTokenFile, index) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 5)] +async fn valid_with_multiple_token_auth() { + let _ = env_logger::builder().is_test(true).try_init(); + + // Start internal service + let local_server = DummyTCPServer::start(port(1)).await; + tokio::spawn(async move { + local_server.loop_conn_square_operations().await; + }); + + let local_set = task::LocalSet::new(); + local_set + .run_until(async move { + wait_for_port(port(1)).await; + + // Start server relay + task::spawn_local(crate::tcp_relay_server::run_app(ServerConfig { + tokens: vec![ + "tok0".to_string(), + VALID_TOKEN.to_string(), + "tok2".to_string(), + ], + tokens_file: None, + ports: vec![port(1)], + upstream_server: "127.0.0.1".to_string(), + listen_address: format!("127.0.0.1:{}", port(0)), + increment_ports: 1, + tls_cert: None, + tls_key: None, + tls_client_auth_root_cert: None, + tls_revocation_list: None, + })); + wait_for_port(port(0)).await; + + // Start client relay + task::spawn(crate::tcp_relay_client::run_app(ClientConfig { + token: Some(VALID_TOKEN.to_string()), + relay_url: format!("http://{}:{}", LOCALHOST, port(0)), + listen_address: LOCALHOST.to_string(), + root_certificate: None, + ..Default::default() + })); + wait_for_port(port(2)).await; + + dummy_tcp_client_square_root_requests(port(2), 10).await; + }) + .await; +} diff --git a/src/test/valid_with_token_auth_multiple_ports.rs b/src/test/valid_with_token_auth_multiple_ports.rs new file mode 100644 index 0000000..fc68f4f --- /dev/null +++ b/src/test/valid_with_token_auth_multiple_ports.rs @@ -0,0 +1,67 @@ +use tokio::task; + +use crate::tcp_relay_client::client_config::ClientConfig; +use crate::tcp_relay_server::server_config::ServerConfig; +use crate::test::dummy_tcp_sockets::{ + dummy_tcp_client_additions_requests, dummy_tcp_client_square_root_requests, wait_for_port, + DummyTCPServer, +}; +use crate::test::{get_port_number, PortsAllocation, LOCALHOST}; + +const VALID_TOKEN: &str = "AvalidTOKEN"; +const NUM_INC: i32 = 5; + +fn port(index: u16) -> u16 { + get_port_number(PortsAllocation::ValidWithTokenAuthMultiplePorts, index) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 5)] +async fn valid_with_token_auth() { + let _ = env_logger::builder().is_test(true).try_init(); + + // Start internal services + let local_server_1 = DummyTCPServer::start(port(1)).await; + tokio::spawn(async move { + local_server_1.loop_conn_square_operations().await; + }); + + let local_server_2 = DummyTCPServer::start(port(3)).await; + tokio::spawn(async move { + local_server_2.loop_next_conn_add_operations(NUM_INC).await; + }); + + let local_set = task::LocalSet::new(); + local_set + .run_until(async move { + wait_for_port(port(1)).await; + + // Start server relay + task::spawn_local(crate::tcp_relay_server::run_app(ServerConfig { + tokens: vec![VALID_TOKEN.to_string()], + tokens_file: None, + ports: vec![port(1), port(3)], + upstream_server: "127.0.0.1".to_string(), + listen_address: format!("127.0.0.1:{}", port(0)), + increment_ports: 1, + tls_cert: None, + tls_key: None, + tls_client_auth_root_cert: None, + tls_revocation_list: None, + })); + wait_for_port(port(0)).await; + + // Start client relay + task::spawn(crate::tcp_relay_client::run_app(ClientConfig { + token: Some(VALID_TOKEN.to_string()), + relay_url: format!("http://{}:{}", LOCALHOST, port(0)), + listen_address: LOCALHOST.to_string(), + root_certificate: None, + ..Default::default() + })); + wait_for_port(port(2)).await; + + dummy_tcp_client_square_root_requests(port(2), 10).await; + dummy_tcp_client_additions_requests(port(4), NUM_INC, 10).await; + }) + .await; +} diff --git a/src/test/valid_with_token_file.rs b/src/test/valid_with_token_file.rs new file mode 100644 index 0000000..e411afe --- /dev/null +++ b/src/test/valid_with_token_file.rs @@ -0,0 +1,62 @@ +use tokio::task; + +use crate::tcp_relay_client::client_config::ClientConfig; +use crate::tcp_relay_server::server_config::ServerConfig; +use crate::test::dummy_tcp_sockets::{ + dummy_tcp_client_square_root_requests, wait_for_port, DummyTCPServer, +}; +use crate::test::{get_port_number, PortsAllocation, LOCALHOST}; + +const VALID_TOKEN: &str = "AvalidTOKEN"; + +fn port(index: u16) -> u16 { + get_port_number(PortsAllocation::ValidWithMultipleTokenAuth, index) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 5)] +async fn valid_with_token_file() { + let _ = env_logger::builder().is_test(true).try_init(); + + // Start internal service + let local_server = DummyTCPServer::start(port(1)).await; + tokio::spawn(async move { + local_server.loop_conn_square_operations().await; + }); + + let local_set = task::LocalSet::new(); + local_set + .run_until(async move { + wait_for_port(port(1)).await; + + let token_file = mktemp::Temp::new_file().unwrap(); + std::fs::write(&token_file, format!("tok1\ntok2\n{}\ntok4", VALID_TOKEN)).unwrap(); + + // Start server relay + task::spawn_local(crate::tcp_relay_server::run_app(ServerConfig { + tokens: vec![], + tokens_file: Some(token_file.to_str().unwrap().to_string()), + ports: vec![port(1)], + upstream_server: "127.0.0.1".to_string(), + listen_address: format!("127.0.0.1:{}", port(0)), + increment_ports: 1, + tls_cert: None, + tls_key: None, + tls_client_auth_root_cert: None, + tls_revocation_list: None, + })); + wait_for_port(port(0)).await; + + // Start client relay + task::spawn(crate::tcp_relay_client::run_app(ClientConfig { + token: Some(VALID_TOKEN.to_string()), + relay_url: format!("http://{}:{}", LOCALHOST, port(0)), + listen_address: LOCALHOST.to_string(), + root_certificate: None, + ..Default::default() + })); + wait_for_port(port(2)).await; + + dummy_tcp_client_square_root_requests(port(2), 10).await; + }) + .await; +}