From 75637f095c3f0c4d3a857879c6b3712a1f079434 Mon Sep 17 00:00:00 2001 From: Chris F Ravenscroft Date: Sat, 15 May 2021 01:27:57 -0700 Subject: [PATCH 1/2] dynamodb and sqlite are both supported as distinct features. --- tunnelto_server/Cargo.toml | 11 +- .../auth/{auth_db.rs => dynamo_auth_db.rs} | 0 tunnelto_server/src/auth/mod.rs | 5 +- tunnelto_server/src/auth/sqlite_auth_db.rs | 193 ++++++++++++++++++ tunnelto_server/src/main.rs | 10 +- 5 files changed, 215 insertions(+), 4 deletions(-) rename tunnelto_server/src/auth/{auth_db.rs => dynamo_auth_db.rs} (100%) create mode 100644 tunnelto_server/src/auth/sqlite_auth_db.rs diff --git a/tunnelto_server/Cargo.toml b/tunnelto_server/Cargo.toml index 14c644a..b71fa22 100644 --- a/tunnelto_server/Cargo.toml +++ b/tunnelto_server/Cargo.toml @@ -43,4 +43,13 @@ tracing-honeycomb = { git = "https://github.com/agrinman/tracing-honeycomb", rev # auth handler rusoto_core = "0.46" rusoto_dynamodb = "0.46" -rusoto_credential = "0.46" \ No newline at end of file +rusoto_credential = "0.46" + +[dependencies.rusqlite] +version = "0.21.0" +features = ["bundled"] + +[features] +default = ["dynamodb"] +dynamodb = [] +sqlite = [] \ No newline at end of file diff --git a/tunnelto_server/src/auth/auth_db.rs b/tunnelto_server/src/auth/dynamo_auth_db.rs similarity index 100% rename from tunnelto_server/src/auth/auth_db.rs rename to tunnelto_server/src/auth/dynamo_auth_db.rs diff --git a/tunnelto_server/src/auth/mod.rs b/tunnelto_server/src/auth/mod.rs index b6a5ba1..2a1d386 100644 --- a/tunnelto_server/src/auth/mod.rs +++ b/tunnelto_server/src/auth/mod.rs @@ -4,7 +4,10 @@ use serde::{Deserialize, Serialize}; use std::convert::TryInto; use std::fmt::Formatter; -pub mod auth_db; +#[cfg(feature = "dynamodb")] +pub mod dynamo_auth_db; +#[cfg(feature = "sqlite")] +pub mod sqlite_auth_db; pub mod client_auth; pub mod reconnect_token; diff --git a/tunnelto_server/src/auth/sqlite_auth_db.rs b/tunnelto_server/src/auth/sqlite_auth_db.rs new file mode 100644 index 0000000..9074fd0 --- /dev/null +++ b/tunnelto_server/src/auth/sqlite_auth_db.rs @@ -0,0 +1,193 @@ +use rusqlite::{params, NO_PARAMS, Connection}; + +use super::AuthResult; +use crate::auth::AuthService; +use async_trait::async_trait; +use sha2::Digest; +use std::str::FromStr; +use thiserror::Error; +use uuid::Uuid; +use std::sync::Mutex; + +mod sqlite_conf { + pub const DB_PATH:&'static str = "./tunnelto.db"; +} + +mod domain_db { + pub const TABLE_NAME: &'static str = "tunnelto_domains"; + pub const PRIMARY_KEY: &'static str = "subdomain"; + pub const ACCOUNT_ID: &'static str = "account_id"; +} + +mod key_db { + pub const TABLE_NAME: &'static str = "tunnelto_auth"; + pub const PRIMARY_KEY: &'static str = "auth_key_hash"; + pub const ACCOUNT_ID: &'static str = "account_id"; +} + +mod record_db { + pub const TABLE_NAME: &'static str = "tunnelto_record"; + pub const PRIMARY_KEY: &'static str = "account_id"; + pub const SUBSCRIPTION_ID: &'static str = "subscription_id"; +} + +pub struct AuthDbService { + connection: Mutex, +} + +impl AuthDbService { + pub fn new() -> Result> { + let conn = Connection::open(sqlite_conf::DB_PATH.to_string())?; + conn.execute( + &format!("CREATE TABLE IF NOT EXISTS {} ( + {} TEXT NOT NULL, + {} TEXT NOT NULL + )", + domain_db::TABLE_NAME, + domain_db::PRIMARY_KEY, + domain_db::ACCOUNT_ID + ), + NO_PARAMS, + )?; + conn.execute( + &format!("CREATE TABLE IF NOT EXISTS {} ( + {} TEXT NOT NULL, + {} TEXT NOT NULL + )", + key_db::TABLE_NAME, + key_db::PRIMARY_KEY, + key_db::ACCOUNT_ID + ), + NO_PARAMS, + )?; + conn.execute( + &format!("CREATE TABLE IF NOT EXISTS {} ( + {} TEXT NOT NULL, + {} TEXT NOT NULL + )", + record_db::TABLE_NAME, + record_db::PRIMARY_KEY, + record_db::SUBSCRIPTION_ID + ), + NO_PARAMS, + )?; + Ok( Self{connection: Mutex::new(conn)} ) + } +} + +impl Drop for AuthDbService { + fn drop(&mut self) { + let c = &*self.connection.lock().unwrap(); + drop(c); + } +} + +fn key_id(auth_key: &str) -> String { + let hash = sha2::Sha256::digest(auth_key.as_bytes()).to_vec(); + base64::encode_config(&hash, base64::URL_SAFE_NO_PAD) +} + +#[derive(Error, Debug)] +pub enum Error { + #[error("The authentication key is invalid")] + AccountNotFound, + + #[error("The authentication key is invalid")] + InvalidAccountId(#[from] uuid::Error), + + #[error("The subdomain is not authorized")] + SubdomainNotAuthorized, +} + +#[async_trait] +impl AuthService for AuthDbService { + type Error = Error; + type AuthKey = String; + + async fn auth_sub_domain( + &self, + auth_key: &String, + subdomain: &str, + ) -> Result { + let authenticated_account_id = self.get_account_id_for_auth_key(auth_key).await?; + let is_pro_account = self + .is_account_in_good_standing(authenticated_account_id) + .await?; + + tracing::info!(account=%authenticated_account_id.to_string(), requested_subdomain=%subdomain, is_pro=%is_pro_account, "authenticated client"); + + if let Some(account_id) = self.get_account_id_for_subdomain(subdomain).await? { + // check you reserved it + if authenticated_account_id != account_id { + tracing::info!(account=%authenticated_account_id.to_string(), "reserved by other"); + return Ok(AuthResult::ReservedByOther); + } + + // next ensure that the account is in good standing + if !is_pro_account { + tracing::warn!(account=%authenticated_account_id.to_string(), "delinquent"); + return Ok(AuthResult::ReservedByYouButDelinquent); + } + + return Ok(AuthResult::ReservedByYou); + } + + if is_pro_account { + Ok(AuthResult::Available) + } else { + Ok(AuthResult::PaymentRequired) + } + } +} + +impl AuthDbService { + async fn get_account_id_for_auth_key(&self, auth_key: &str) -> Result { + let auth_key_hash = key_id(auth_key); + + let conn:&Connection = &*self.connection.lock().unwrap(); + let row: Result = conn.query_row( + &format!("SELECT {} FROM {} WHERE {}=?", + key_db::ACCOUNT_ID, + key_db::TABLE_NAME, + key_db::PRIMARY_KEY + ), + params![auth_key_hash,], + |row| row.get(0) + ); + Ok(Uuid::from_str(&row.map_err(|_| Error::AccountNotFound)?)?) + } + + async fn is_account_in_good_standing(&self, account_id: Uuid) -> Result { + let conn:&Connection = &*self.connection.lock().unwrap(); + let row: Result = conn.query_row( + &format!("SELECT {} FROM {} WHERE {}=?", + record_db::SUBSCRIPTION_ID, + record_db::TABLE_NAME, + record_db::PRIMARY_KEY + ), + params![account_id.to_string(),], + |row| row.get(0) + ); + Ok(row.map_or_else(|_| false, |_| true)) + } + + async fn get_account_id_for_subdomain(&self, subdomain: &str) -> Result, Error> { + let conn:&Connection = &*self.connection.lock().unwrap(); + let row: Result = conn.query_row( + &format!("SELECT {} FROM {} WHERE {}=?", + domain_db::ACCOUNT_ID, + domain_db::TABLE_NAME, + domain_db::PRIMARY_KEY + ), + params![subdomain,], + |row| row.get(0) + ); + let account_str = row.map_or_else(|_| None, |v| Some(v)); + + if let Some(account_str) = account_str { + Ok(Some(Uuid::from_str(&account_str)?)) + } else { + Ok(None) + } + } +} \ No newline at end of file diff --git a/tunnelto_server/src/main.rs b/tunnelto_server/src/main.rs index 1622ac6..2803298 100644 --- a/tunnelto_server/src/main.rs +++ b/tunnelto_server/src/main.rs @@ -18,10 +18,16 @@ mod active_stream; use self::active_stream::*; mod auth; -pub use self::auth::auth_db; +#[cfg(feature = "dynamodb")] +pub use self::auth::dynamo_auth_db; +#[cfg(feature = "sqlite")] +pub use self::auth::sqlite_auth_db; pub use self::auth::client_auth; -pub use self::auth_db::AuthDbService; +#[cfg(feature = "dynamodb")] +pub use self::dynamo_auth_db::AuthDbService; +#[cfg(feature = "sqlite")] +pub use self::sqlite_auth_db::AuthDbService; mod control_server; mod remote; From 3c63607399c1ade1be0a8b825ee78e2e2bdafe42 Mon Sep 17 00:00:00 2001 From: Chris F Ravenscroft Date: Sat, 29 May 2021 13:12:28 -0700 Subject: [PATCH 2/2] Updating per feedback. --- tunnelto_server/Cargo.toml | 18 ++++++++---------- tunnelto_server/src/auth/mod.rs | 2 +- tunnelto_server/src/auth/sqlite_auth_db.rs | 7 ++----- tunnelto_server/src/config.rs | 9 +++++++++ tunnelto_server/src/main.rs | 12 ++++++++---- 5 files changed, 28 insertions(+), 20 deletions(-) diff --git a/tunnelto_server/Cargo.toml b/tunnelto_server/Cargo.toml index b71fa22..d05f730 100644 --- a/tunnelto_server/Cargo.toml +++ b/tunnelto_server/Cargo.toml @@ -40,16 +40,14 @@ tracing = "0.1.25" tracing-subscriber = "0.2.17" tracing-honeycomb = { git = "https://github.com/agrinman/tracing-honeycomb", rev = "687bafa722ccd584f45aa470fbb637bc57c999cd" } -# auth handler -rusoto_core = "0.46" -rusoto_dynamodb = "0.46" -rusoto_credential = "0.46" - -[dependencies.rusqlite] -version = "0.21.0" -features = ["bundled"] +# auth handler: dynamodb +rusoto_core = { version = "0.46", optional = true } +rusoto_dynamodb = { version = "0.46", optional = true } +rusoto_credential = { version = "0.46", optional = true } +# auth handler: sqlite +rusqlite = { version = "0.21.0", features = ["bundled"], optional = true } [features] default = ["dynamodb"] -dynamodb = [] -sqlite = [] \ No newline at end of file +dynamodb = ["rusoto_core", "rusoto_dynamodb", "rusoto_credential"] +sqlite = ["rusqlite"] \ No newline at end of file diff --git a/tunnelto_server/src/auth/mod.rs b/tunnelto_server/src/auth/mod.rs index 2a1d386..6a9a6a1 100644 --- a/tunnelto_server/src/auth/mod.rs +++ b/tunnelto_server/src/auth/mod.rs @@ -81,7 +81,7 @@ pub struct NoAuth; #[async_trait] impl AuthService for NoAuth { type Error = (); - type AuthKey = (); + type AuthKey = String; /// Authenticate a subdomain with an AuthKey async fn auth_sub_domain( diff --git a/tunnelto_server/src/auth/sqlite_auth_db.rs b/tunnelto_server/src/auth/sqlite_auth_db.rs index 9074fd0..689af88 100644 --- a/tunnelto_server/src/auth/sqlite_auth_db.rs +++ b/tunnelto_server/src/auth/sqlite_auth_db.rs @@ -1,6 +1,7 @@ use rusqlite::{params, NO_PARAMS, Connection}; use super::AuthResult; +use super::super::CONFIG; use crate::auth::AuthService; use async_trait::async_trait; use sha2::Digest; @@ -9,10 +10,6 @@ use thiserror::Error; use uuid::Uuid; use std::sync::Mutex; -mod sqlite_conf { - pub const DB_PATH:&'static str = "./tunnelto.db"; -} - mod domain_db { pub const TABLE_NAME: &'static str = "tunnelto_domains"; pub const PRIMARY_KEY: &'static str = "subdomain"; @@ -37,7 +34,7 @@ pub struct AuthDbService { impl AuthDbService { pub fn new() -> Result> { - let conn = Connection::open(sqlite_conf::DB_PATH.to_string())?; + let conn = Connection::open(&CONFIG.db_connection_string)?; conn.execute( &format!("CREATE TABLE IF NOT EXISTS {} ( {} TEXT NOT NULL, diff --git a/tunnelto_server/src/config.rs b/tunnelto_server/src/config.rs index 443ed62..7c3b686 100644 --- a/tunnelto_server/src/config.rs +++ b/tunnelto_server/src/config.rs @@ -37,6 +37,9 @@ pub struct Config { /// Blocked IP addresses pub blocked_ips: Vec, + + /// Connection string for "old timey" database engines + pub db_connection_string: String, } impl Config { @@ -71,6 +74,11 @@ impl Config { }) .unwrap_or(vec![]); + let db_connection_string = match std::env::var("DB_CONNECTION_STRING") { + Ok(connection_string) => connection_string, + _ => "./tunnelto.db".to_string(), + }; + Config { allowed_hosts, blocked_sub_domains, @@ -82,6 +90,7 @@ impl Config { honeycomb_api_key, instance_id, blocked_ips, + db_connection_string, } } } diff --git a/tunnelto_server/src/main.rs b/tunnelto_server/src/main.rs index 2803298..5b11a2d 100644 --- a/tunnelto_server/src/main.rs +++ b/tunnelto_server/src/main.rs @@ -48,12 +48,16 @@ use tracing::{error, info, Instrument}; lazy_static! { pub static ref CONNECTIONS: Connections = Connections::new(); pub static ref ACTIVE_STREAMS: ActiveStreams = Arc::new(DashMap::new()); + pub static ref CONFIG: Config = Config::from_env(); +} +#[cfg(any(feature = "dynamodb", feature="sqlite"))] +lazy_static! { pub static ref AUTH_DB_SERVICE: AuthDbService = AuthDbService::new().expect("failed to init auth-service"); - pub static ref CONFIG: Config = Config::from_env(); - - // To disable all authentication: - // pub static ref AUTH_DB_SERVICE: crate::auth::NoAuth = crate::auth::NoAuth; +} +#[cfg(not(any(feature = "dynamodb", feature="sqlite")))] +lazy_static! { + pub static ref AUTH_DB_SERVICE: crate::auth::NoAuth = crate::auth::NoAuth; } #[tokio::main]