diff --git a/tunnelto_server/Cargo.toml b/tunnelto_server/Cargo.toml index 14c644a..d05f730 100644 --- a/tunnelto_server/Cargo.toml +++ b/tunnelto_server/Cargo.toml @@ -40,7 +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" \ No newline at end of file +# 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 = ["rusoto_core", "rusoto_dynamodb", "rusoto_credential"] +sqlite = ["rusqlite"] \ 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..6a9a6a1 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; @@ -78,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 new file mode 100644 index 0000000..689af88 --- /dev/null +++ b/tunnelto_server/src/auth/sqlite_auth_db.rs @@ -0,0 +1,190 @@ +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; +use std::str::FromStr; +use thiserror::Error; +use uuid::Uuid; +use std::sync::Mutex; + +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(&CONFIG.db_connection_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/config.rs b/tunnelto_server/src/config.rs index 432a2c5..2ddef15 100644 --- a/tunnelto_server/src/config.rs +++ b/tunnelto_server/src/config.rs @@ -38,6 +38,8 @@ pub struct Config { /// Blocked IP addresses pub blocked_ips: Vec, + /// Connection string for "old timey" database engines + pub db_connection_string: String, /// The host on which we create tunnels on pub tunnel_host: String, } @@ -74,6 +76,10 @@ 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(), + }; let tunnel_host = std::env::var("TUNNEL_HOST").unwrap_or("tunnelto.dev".to_string()); Config { @@ -87,6 +93,7 @@ impl Config { honeycomb_api_key, instance_id, blocked_ips, + db_connection_string, tunnel_host, } } diff --git a/tunnelto_server/src/main.rs b/tunnelto_server/src/main.rs index 533d5f3..27a9ed0 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; @@ -42,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]