From 13117e4237135d89824864fb00cd440a06b3dcb0 Mon Sep 17 00:00:00 2001 From: _ <_@_> Date: Sun, 1 Nov 2020 21:34:50 -0600 Subject: [PATCH] Add tripcodes for a little security --- Cargo.toml | 2 + src/bin/relay.rs | 17 +++++++- src/bin/server.rs | 18 +++++--- src/lib.rs | 24 ++++++++--- src/relay/mod.rs | 107 ++++++++++++++++++++++++++++++++++++++++------ src/server/mod.rs | 43 ++++++++++++++++--- todo.md | 4 +- 7 files changed, 179 insertions(+), 36 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2f8286c..ed2a448 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ handlebars = "3.5.1" http = "0.2.1" hyper = "0.13.8" lazy_static = "1.4.0" +maplit = "1.0.2" percent-encoding = "2.1.0" regex = "1.4.1" reqwest = { version = "0.10.8", features = ["stream"] } @@ -25,4 +26,5 @@ rmp-serde = "0.14.4" serde = {version = "1.0.117", features = ["derive"]} structopt = "0.3.20" tokio = { version = "0.2.22", features = ["full"] } +toml = "0.5.7" ulid = "0.4.1" diff --git a/src/bin/relay.rs b/src/bin/relay.rs index ad5bc2c..71e3c76 100644 --- a/src/bin/relay.rs +++ b/src/bin/relay.rs @@ -1,6 +1,19 @@ -use std::error::Error; +use std::{ + error::Error, + fs::File, +}; #[tokio::main] async fn main () -> Result <(), Box > { - ptth::relay::main ().await + use std::io::Read; + + let mut f = File::open ("ptth_relay.toml").unwrap (); + let mut buffer = vec! [0u8; 4096]; + let bytes_read = f.read (&mut buffer).unwrap (); + buffer.truncate (bytes_read); + + let config_s = String::from_utf8 (buffer).unwrap (); + let config_file: ptth::relay::ConfigFile = toml::from_str (&config_s).unwrap (); + + ptth::relay::main (config_file).await } diff --git a/src/bin/server.rs b/src/bin/server.rs index b142c39..7b71449 100644 --- a/src/bin/server.rs +++ b/src/bin/server.rs @@ -10,22 +10,28 @@ struct Opt { #[structopt (name = "RELAY_URL")] relay_url: String, - #[structopt (name = "SERVER_NAME")] - server_name: String, - #[structopt (long)] file_server_root: Option , } #[tokio::main] async fn main () -> Result <(), Box > { + use std::io::Read; + + let mut f = std::fs::File::open ("ptth_server.toml").unwrap (); + let mut buffer = vec! [0u8; 4096]; + let bytes_read = f.read (&mut buffer).unwrap (); + buffer.truncate (bytes_read); + + let config_s = String::from_utf8 (buffer).unwrap (); + let config_file: ptth::server::ConfigFile = toml::from_str (&config_s).unwrap (); + let opt = Opt::from_args (); let opt = ptth::server::Opt { relay_url: opt.relay_url, - server_name: opt.server_name, - file_server_root: opt.file_server_root.unwrap_or ("/home/user".into ()), + file_server_root: opt.file_server_root.unwrap_or_else (|| "/home/user".into ()), }; - ptth::server::main (opt).await + ptth::server::main (config_file, opt).await } diff --git a/src/lib.rs b/src/lib.rs index c154d07..fecadc7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,15 +31,25 @@ mod tests { #[test] fn end_to_end () { + use maplit::*; use reqwest::Client; let mut rt = Runtime::new ().unwrap (); // Spawn the root task rt.block_on (async { - let relay_url = "http://127.0.0.1:4000"; + let server_name = "alien_wildlands"; + let api_key = "AnacondaHardcoverGrannyUnlatchLankinessMutate"; + let tripcode = base64::encode (blake3::hash (api_key.as_bytes ()).as_bytes ()); + println! ("Relay is expecting tripcode {}", tripcode); + let config_file = relay::ConfigFile { + port: None, + server_tripcodes: hashmap! { + server_name.into () => tripcode, + }, + }; - let relay_state = Arc::new (relay::RelayState::default ()); + let relay_state = Arc::new (relay::RelayState::from (&config_file)); let relay_state_2 = relay_state.clone (); spawn (async move { @@ -48,18 +58,20 @@ mod tests { assert! (relay_state.list_servers ().await.is_empty ()); + let relay_url = "http://127.0.0.1:4000"; let relay_url_2 = relay_url.into (); - let server_name = "alien_wildlands"; - let server_name_2 = server_name.into (); + let config_file = server::ConfigFile { + name: server_name.into (), + api_key: api_key.into (), + }; spawn (async move { let opt = server::Opt { relay_url: relay_url_2, - server_name: server_name_2, file_server_root: "./".into (), }; - server::main (opt).await.unwrap (); + server::main (config_file, opt).await.unwrap (); }); tokio::time::delay_for (std::time::Duration::from_millis (500)).await; diff --git a/src/relay/mod.rs b/src/relay/mod.rs index d3dbb05..d75f861 100644 --- a/src/relay/mod.rs +++ b/src/relay/mod.rs @@ -4,6 +4,7 @@ use std::{ error::Error, collections::*, convert::Infallible, + iter::FromIterator, net::SocketAddr, sync::{ Arc @@ -22,7 +23,10 @@ use hyper::{ StatusCode, }; use hyper::service::{make_service_fn, service_fn}; -use serde::Serialize; +use serde::{ + Deserialize, + Serialize, +}; use tokio::{ sync::Mutex, }; @@ -67,7 +71,42 @@ enum RequestRendezvous { type ResponseRendezvous = oneshot::Sender <(http_serde::ResponseParts, Body)>; +// Stuff we need to load from the config file and use to +// set up the HTTP server + +#[derive (Default, Deserialize)] +pub struct ConfigFile { + pub port: Option , + pub server_tripcodes: HashMap , +} + +// Stuff we actually need at runtime + +struct Config { + server_tripcodes: HashMap , +} + +impl From <&ConfigFile> for Config { + fn from (f: &ConfigFile) -> Self { + let trips = HashMap::from_iter (f.server_tripcodes.iter () + .map (|(k, v)| { + use std::convert::TryInto; + let bytes: Vec = base64::decode (v).unwrap (); + let bytes: [u8; 32] = (&bytes [..]).try_into ().unwrap (); + + let v = blake3::Hash::from (bytes); + + (k.clone (), v) + })); + + Self { + server_tripcodes: trips, + } + } +} + pub struct RelayState { + config: Config, handlebars: Arc >, // Key: Server ID @@ -80,6 +119,18 @@ pub struct RelayState { impl Default for RelayState { fn default () -> Self { Self { + config: Config::from (&ConfigFile::default ()), + handlebars: Arc::new (load_templates ().unwrap ()), + request_rendezvous: Default::default (), + response_rendezvous: Default::default (), + } + } +} + +impl From <&ConfigFile> for RelayState { + fn from (config_file: &ConfigFile) -> Self { + Self { + config: Config::from (config_file), handlebars: Arc::new (load_templates ().unwrap ()), request_rendezvous: Default::default (), response_rendezvous: Default::default (), @@ -101,9 +152,29 @@ fn status_reply > (status: StatusCode, b: B) Response::builder ().status (status).body (b.into ()).unwrap () } -async fn handle_http_listen (state: Arc , watcher_code: String) +async fn handle_http_listen ( + state: Arc , + watcher_code: String, + api_key: &[u8], +) -> Response { + let trip_error = status_reply (StatusCode::UNAUTHORIZED, "Bad X-ApiKey"); + + let expected_tripcode = match state.config.server_tripcodes.get (&watcher_code) { + None => { + eprintln! ("Denied http_listen for non-existent server name {}", watcher_code); + return trip_error; + }, + Some (x) => x, + }; + let actual_tripcode = blake3::hash (api_key); + + if expected_tripcode != &actual_tripcode { + eprintln! ("Denied http_listen for bad tripcode {}", base64::encode (actual_tripcode.as_bytes ())); + return trip_error; + } + use RequestRendezvous::*; let (tx, rx) = oneshot::channel (); @@ -236,6 +307,8 @@ async fn handle_all (req: Request , state: Arc ) let path = req.uri ().path (); //println! ("{}", path); + let api_key = req.headers ().get ("X-ApiKey"); + if req.method () == Method::POST { // This is stuff the server can use. Clients can't // POST right now @@ -250,7 +323,11 @@ async fn handle_all (req: Request , state: Arc ) } Ok (if let Some (listen_code) = prefix_match (path, "/7ZSFUKGV_http_listen/") { - handle_http_listen (state, listen_code.into ()).await + let api_key = match api_key { + None => return Ok (status_reply (StatusCode::UNAUTHORIZED, "Can't register as server without an API key")), + Some (x) => x, + }; + handle_http_listen (state, listen_code.into (), api_key.as_bytes ()).await } else if let Some (rest) = prefix_match (path, "/servers/") { if rest == "" { @@ -315,9 +392,17 @@ pub fn load_templates () Ok (handlebars) } -pub async fn run_relay (state: Arc ) -> Result <(), Box > +pub async fn run_relay ( + state: Arc +) + -> Result <(), Box > { - let addr = SocketAddr::from(([0, 0, 0, 0], 4000)); + let addr = SocketAddr::from (( + [0, 0, 0, 0], + 4000, + )); + + eprintln! ("Loaded {} server tripcodes", state.config.server_tripcodes.len ()); let make_svc = make_service_fn (|_conn| { let state = state.clone (); @@ -338,14 +423,10 @@ pub async fn run_relay (state: Arc ) -> Result <(), Box > Ok (()) } -pub async fn main () -> Result <(), Box > { - let state = RelayState { - handlebars: Arc::new (load_templates ()?), - request_rendezvous: Default::default (), - response_rendezvous: Default::default (), - }; - - let state = Arc::new (state); +pub async fn main (config_file: ConfigFile) +-> Result <(), Box > +{ + let state = Arc::new (RelayState::from (&config_file)); run_relay (state).await } diff --git a/src/server/mod.rs b/src/server/mod.rs index c87bd79..3238f5e 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -10,6 +10,7 @@ use hyper::{ StatusCode, }; use reqwest::Client; +use serde::Deserialize; use tokio::{ time::delay_for, }; @@ -64,15 +65,34 @@ async fn handle_req_resp <'a> ( } } +#[derive (Default, Deserialize)] +pub struct ConfigFile { + pub name: String, + pub api_key: String, +} + #[derive (Clone)] pub struct Opt { pub relay_url: String, - pub server_name: String, pub file_server_root: PathBuf, } -pub async fn main (opt: Opt) -> Result <(), Box > { - let client = Arc::new (Client::new ()); +pub async fn main (config_file: ConfigFile, opt: Opt) +-> Result <(), Box > +{ + use std::convert::TryInto; + + let tripcode = base64::encode (blake3::hash (config_file.api_key.as_bytes ()).as_bytes ()); + + println! ("Our tripcode is {}", tripcode); + + let mut headers = reqwest::header::HeaderMap::new (); + headers.insert ("X-ApiKey", config_file.api_key.try_into ().unwrap ()); + + // TODO: (FN46S2M2) Combine these Arcs + let client = Arc::new (Client::builder () + .default_headers (headers) + .build ().unwrap ()); let opt = Arc::new (opt); let handlebars = Arc::new (file_server::load_templates ()?); @@ -83,12 +103,14 @@ pub async fn main (opt: Opt) -> Result <(), Box > { delay_for (Duration::from_millis (backoff_delay)).await; } - let req_req = client.get (&format! ("{}/7ZSFUKGV_http_listen/{}", opt.relay_url, opt.server_name)); + let req_req = client.get (&format! ("{}/7ZSFUKGV_http_listen/{}", opt.relay_url, config_file.name)); + + let err_backoff_delay = std::cmp::min (30_000, backoff_delay * 2 + 500); let req_resp = match req_req.send ().await { Err (e) => { - println! ("Err: {:?}", e); - backoff_delay = backoff_delay * 2 + 500; + eprintln! ("Err: {:?}", e); + backoff_delay = err_backoff_delay; continue; }, Ok (r) => { @@ -97,13 +119,20 @@ pub async fn main (opt: Opt) -> Result <(), Box > { }, }; + if req_resp.status () != StatusCode::OK { + eprintln! ("{}", req_resp.status ()); + eprintln! ("{}", String::from_utf8 (req_resp.bytes ().await.unwrap ().to_vec ()).unwrap ()); + backoff_delay = err_backoff_delay; + continue; + } + // Spawn another task for each request so we can // immediately listen for the next connection let client = client.clone (); let opt = opt.clone (); - let handlebars = handlebars.clone (); + tokio::spawn (async move { handle_req_resp (&opt, handlebars, client, req_resp).await; }); diff --git a/todo.md b/todo.md index f651d58..f97d589 100644 --- a/todo.md +++ b/todo.md @@ -1,5 +1,5 @@ -- Set up tokens or privkeys or tripcodes or something so -clients can't trivially impersonate servers +- FN46S2M2 Combine Arcs +- Log / audit log? - Prevent directory traversal attacks - Error handling