diff --git a/Cargo.lock b/Cargo.lock index 4718b74e2..6026ee72e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2668,6 +2668,19 @@ dependencies = [ "web30", ] +[[package]] +name = "rita_client_registration" +version = "0.1.0" +dependencies = [ + "althea_types", + "awc", + "clarity", + "lazy_static", + "log", + "phonenumber", + "serde", +] + [[package]] name = "rita_common" version = "0.21.2" @@ -2728,6 +2741,7 @@ dependencies = [ "num256", "phonenumber", "reqwest", + "rita_client_registration", "rita_common", "serde", "serde_derive", diff --git a/Cargo.toml b/Cargo.toml index ef7fc522d..b3eac0262 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,14 +5,29 @@ authors = ["Stan Drozd "] edition = "2018" [workspace] -members = ["althea_kernel_interface", "settings", "clu", "exit_db", "antenna_forwarding_client", "antenna_forwarding_protocol", "auto_bridge","rita_common","rita_exit","rita_client", "rita_bin", "test_runner", "integration_tests"] +members = [ + "althea_kernel_interface", + "settings", + "clu", + "exit_db", + "antenna_forwarding_client", + "antenna_forwarding_protocol", + "auto_bridge", + "rita_common", + "rita_exit", + "rita_client", + "rita_client_registration", + "rita_bin", + "test_runner", + "integration_tests", +] # Production relase profile, every trick is used to reduce binary size [profile.release] opt-level = "z" strip = true lto = true -codegen-units=1 +codegen-units = 1 incremental = false # testrunner should be fast to execute but also to compile @@ -23,4 +38,4 @@ inherits = "dev" opt-level = 2 [workspace.dependencies] -deep_space = {version = "2", features = ["althea"], default-features=false} \ No newline at end of file +deep_space = { version = "2", features = ["althea"], default-features = false } diff --git a/rita_client_registration/Cargo.toml b/rita_client_registration/Cargo.toml new file mode 100644 index 000000000..2fbe077ce --- /dev/null +++ b/rita_client_registration/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "rita_client_registration" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +lazy_static = "1.4" +althea_types = { path = "../althea_types" } +log = { version = "0.4", features = ["release_max_level_info"] } +serde = "1.0" +clarity = "1.2" +phonenumber = "0.3" +awc = "3.1" diff --git a/rita_client_registration/src/client_db.rs b/rita_client_registration/src/client_db.rs new file mode 100644 index 000000000..8ebefcece --- /dev/null +++ b/rita_client_registration/src/client_db.rs @@ -0,0 +1,28 @@ +use std::net::IpAddr; + +use althea_types::{Identity, WgKey}; +use clarity::Address; + +pub fn get_all_regsitered_clients() -> Vec { + unimplemented!() +} + +pub fn get_registered_client_using_wgkey(_key: WgKey) -> Option { + unimplemented!() +} + +pub fn get_registered_client_using_ethkey(_key: Address) -> Option { + unimplemented!() +} + +pub fn get_registered_client_using_meship(_ip: IpAddr) -> Option { + unimplemented!() +} + +pub fn get_clients_exit_cluster_list(_key: WgKey) -> Vec { + unimplemented!() +} + +pub fn add_client_to_registered_list(_c: Identity) { + unimplemented!() +} diff --git a/rita_client_registration/src/lib.rs b/rita_client_registration/src/lib.rs new file mode 100644 index 000000000..871bdcb84 --- /dev/null +++ b/rita_client_registration/src/lib.rs @@ -0,0 +1,269 @@ +#![deny(unused_crate_dependencies)] +use std::{ + collections::HashMap, + sync::{Arc, RwLock}, +}; + +use althea_types::{ExitClientIdentity, WgKey}; +use phonenumber::PhoneNumber; +use serde::{Deserialize, Serialize}; + +use crate::client_db::{ + add_client_to_registered_list, get_registered_client_using_ethkey, + get_registered_client_using_meship, get_registered_client_using_wgkey, +}; + +#[macro_use] +extern crate log; +#[macro_use] +extern crate lazy_static; + +pub mod client_db; + +lazy_static! { + /// A map that stores number of texts sent to a client during registration + static ref TEXTS_SENT: Arc>> = Arc::new(RwLock::new(HashMap::new())); +} + +/// Return struct from check_text and Send Text. Verified indicates status from api http req, +/// bad phone number is an error parsing clients phone number +/// Internal server error is an error while querying api endpoint +enum TextApiError { + BadPhoneNumber, + InternalServerError { error: String }, +} + +/// Return struct from Registration server to exit +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum ExitSignupReturn { + RegistrationOk, + PendingRegistration, + BadPhoneNumber, + InternalServerError { e: String }, +} +// Lazy static setters and getters +fn increment_texts_sent(key: WgKey) { + let lock = &mut *TEXTS_SENT.write().unwrap(); + let txt_sent = lock.get_mut(&key); + if let Some(val) = txt_sent { + *val += 1; + } else { + lock.insert(key, 1); + } +} + +fn reset_texts_sent(key: WgKey) { + TEXTS_SENT.write().unwrap().remove(&key); +} + +fn get_texts_sent(key: WgKey) -> u8 { + *TEXTS_SENT.read().unwrap().get(&key).unwrap_or(&0u8) +} + +#[derive(Serialize)] +pub struct SmsCheck { + api_key: String, + verification_code: String, + phone_number: String, + country_code: String, +} + +#[derive(Serialize)] +pub struct SmsRequest { + api_key: String, + via: String, + phone_number: String, + country_code: String, +} + +/// True if there is any client with the same eth address, wg key, or ip address already registered +pub fn client_conflict(client: &ExitClientIdentity) -> bool { + // we can't possibly have a conflict if we have exactly this client already + // since client exists checks all major details this is safe and will return false + // if it's not exactly the same client + if client_exists(client) { + return false; + } + trace!("Checking if client exists"); + let ip = client.global.mesh_ip; + let wg = client.global.wg_public_key; + let key = client.global.eth_address; + + let ip_exists = get_registered_client_using_meship(ip).is_some(); + let wg_exists = get_registered_client_using_wgkey(wg).is_some(); + let eth_exists = get_registered_client_using_ethkey(key).is_some(); + + info!( + "Signup conflict ip {} eth {} wg {}", + ip_exists, eth_exists, wg_exists + ); + ip_exists || eth_exists || wg_exists +} + +fn client_exists(client: &ExitClientIdentity) -> bool { + trace!("Checking if client exists"); + let c_id = get_registered_client_using_wgkey(client.global.wg_public_key); + match c_id { + Some(a) => client.global == a, + None => false, + } +} + +/// Handles the minutia of phone registration states +pub async fn handle_sms_registration( + client: ExitClientIdentity, + api_key: String, + magic_number: Option, +) -> ExitSignupReturn { + info!( + "Handling phone registration for {}", + client.global.wg_public_key + ); + + // Get magic phone number + let magic_phone_number = magic_number; + + let text_num = get_texts_sent(client.global.wg_public_key); + let sent_more_than_allowed_texts = text_num > 10; + + match ( + client.reg_details.phone.clone(), + client.reg_details.phone_code.clone(), + sent_more_than_allowed_texts, + ) { + // all texts exhausted, but they can still submit the correct code + (Some(number), Some(code), true) => { + let is_magic = + magic_phone_number.is_some() && magic_phone_number.unwrap() == number.clone(); + let check_text = match check_text(number.clone(), code, api_key).await { + Ok(a) => a, + Err(e) => return return_api_error(e), + }; + let result = is_magic || check_text; + if result { + info!( + "Phone registration complete for {}", + client.global.wg_public_key + ); + add_client_to_registered_list(client.global); + reset_texts_sent(client.global.wg_public_key); + ExitSignupReturn::RegistrationOk + } else { + ExitSignupReturn::PendingRegistration + } + } + // user has exhausted attempts but is still not submitting code + (Some(_number), None, true) => ExitSignupReturn::PendingRegistration, + // user has attempts remaining and is requesting the code be resent + (Some(number), None, false) => { + if let Err(e) = send_text(number, api_key).await { + return return_api_error(e); + } + increment_texts_sent(client.global.wg_public_key); + ExitSignupReturn::PendingRegistration + } + // user has attempts remaining and is submitting a code + (Some(number), Some(code), false) => { + let is_magic = + magic_phone_number.is_some() && magic_phone_number.unwrap() == number.clone(); + let check_text = match check_text(number.clone(), code, api_key).await { + Ok(a) => a, + Err(e) => return return_api_error(e), + }; + + let result = is_magic | check_text; + trace!("Check text returned {}", result); + if result { + info!( + "Phone registration complete for {}", + client.global.wg_public_key + ); + add_client_to_registered_list(client.global); + reset_texts_sent(client.global.wg_public_key); + ExitSignupReturn::RegistrationOk + } else { + ExitSignupReturn::PendingRegistration + } + } + // user did not submit a phonenumber + (None, _, _) => ExitSignupReturn::BadPhoneNumber, + } +} + +fn return_api_error(e: TextApiError) -> ExitSignupReturn { + match e { + TextApiError::BadPhoneNumber => ExitSignupReturn::BadPhoneNumber, + TextApiError::InternalServerError { error } => { + ExitSignupReturn::InternalServerError { e: error } + } + } +} + +/// Posts to the validation endpoint with the code, will return success if the code +/// is the same as the one sent to the user +async fn check_text(number: String, code: String, api_key: String) -> Result { + trace!("About to check text message status for {}", number); + let number: PhoneNumber = match number.parse() { + Ok(number) => number, + Err(e) => { + error!("Phone parse error: {}", e); + return Err(TextApiError::BadPhoneNumber); + } + }; + let url = "https://api.authy.com/protected/json/phones/verification/check"; + + let client = awc::Client::default(); + let response = match client + .get(url) + .send_form(&SmsCheck { + api_key, + verification_code: code, + phone_number: number.national().to_string(), + country_code: number.code().value().to_string(), + }) + .await + { + Ok(a) => a, + Err(e) => { + return Err(TextApiError::InternalServerError { + error: e.to_string(), + }) + } + }; + + trace!("Got {} back from check text", response.status()); + Ok(response.status().is_success()) +} + +/// Sends the authy verification text by hitting the api endpoint +async fn send_text(number: String, api_key: String) -> Result<(), TextApiError> { + info!("Sending message for {}", number); + let url = "https://api.authy.com/protected/json/phones/verification/start"; + let number: PhoneNumber = match number.parse() { + Ok(number) => number, + Err(e) => { + error!("Parse phone number error {}", e); + return Err(TextApiError::BadPhoneNumber); + } + }; + + let client = awc::Client::default(); + match client + .post(url) + .send_form(&SmsRequest { + api_key, + via: "sms".to_string(), + phone_number: number.national().to_string(), + country_code: number.code().value().to_string(), + }) + .await + { + Ok(_a) => Ok(()), + Err(e) => { + error!("Send text error! {}", e); + Err(TextApiError::InternalServerError { + error: e.to_string(), + }) + } + } +} diff --git a/rita_exit/Cargo.toml b/rita_exit/Cargo.toml index e1d66314d..0099e2f78 100644 --- a/rita_exit/Cargo.toml +++ b/rita_exit/Cargo.toml @@ -10,6 +10,7 @@ license = "Apache-2.0" sodiumoxide = "0.2" num256 = "0.5" rita_common = { path = "../rita_common" } +rita_client_registration = { path = "../rita_client_registration" } althea_kernel_interface = { path = "../althea_kernel_interface" } althea_types = { path = "../althea_types" } settings = { path = "../settings" } diff --git a/rita_exit/src/database/database_tools.rs b/rita_exit/src/database/database_tools.rs index 818d3798d..a007090cf 100644 --- a/rita_exit/src/database/database_tools.rs +++ b/rita_exit/src/database/database_tools.rs @@ -1,4 +1,3 @@ -use althea_types::{Identity, WgKey}; use ipnetwork::{IpNetwork, Ipv6Network}; use crate::RitaExitError; @@ -63,18 +62,6 @@ pub fn generate_iterative_client_subnet( } } -pub fn get_all_regsitered_clients() -> Vec { - unimplemented!() -} - -pub fn get_registered_client_using_wgkey(_key: WgKey) -> Option { - unimplemented!() -} - -pub fn get_clients_exit_cluster_list(_key: WgKey) -> Vec { - unimplemented!() -} - #[cfg(test)] mod tests { use super::*; diff --git a/rita_exit/src/database/mod.rs b/rita_exit/src/database/mod.rs index 14c1c27dc..49c2b8899 100644 --- a/rita_exit/src/database/mod.rs +++ b/rita_exit/src/database/mod.rs @@ -8,7 +8,6 @@ use crate::database::struct_tools::display_hashset; use crate::database::struct_tools::get_client_internal_ip; use crate::database::struct_tools::get_client_ipv6; use crate::database::struct_tools::to_exit_client; -use crate::get_registered_client_using_wgkey; use crate::rita_loop::EXIT_INTERFACE; use crate::rita_loop::EXIT_LOOP_TIMEOUT; use crate::rita_loop::LEGACY_INTERFACE; @@ -18,6 +17,8 @@ use althea_kernel_interface::ExitClient; use althea_types::Identity; use althea_types::WgKey; use althea_types::{ExitClientDetails, ExitClientIdentity, ExitDetails, ExitState, ExitVerifMode}; +use rita_client_registration::client_db::get_registered_client_using_wgkey; +use rita_client_registration::ExitSignupReturn; use rita_common::blockchain_oracle::calculate_close_thresh; use rita_common::debt_keeper::get_debts_list; use rita_common::debt_keeper::DebtAction; @@ -40,14 +41,6 @@ pub const ONE_DAY: i64 = 86400; /// Timeout when requesting client registration pub const CLIENT_REGISTER_TIMEOUT: Duration = Duration::from_secs(5); -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum ExitSignupReturn { - RegistrationOk, - PendingRegistration, - BadPhoneNumber, - InternalServerError { e: String }, -} - pub fn get_exit_info() -> ExitDetails { let exit_settings = get_rita_exit(); ExitDetails { diff --git a/rita_exit/src/network_endpoints/mod.rs b/rita_exit/src/network_endpoints/mod.rs index 24b907061..934f2eb88 100644 --- a/rita_exit/src/network_endpoints/mod.rs +++ b/rita_exit/src/network_endpoints/mod.rs @@ -2,7 +2,6 @@ //! these are called by rita instances to operate the mesh use crate::database::{client_status, get_exit_info, signup_client}; -use crate::get_clients_exit_cluster_list; #[cfg(feature = "development")] use crate::rita_exit::database::db_client::DbClient; #[cfg(feature = "development")] @@ -20,6 +19,7 @@ use althea_types::{ use althea_types::{EncryptedExitList, Identity}; use althea_types::{ExitList, WgKey}; use num256::Int256; +use rita_client_registration::client_db::get_clients_exit_cluster_list; use rita_common::blockchain_oracle::potential_payment_issues_detected; use rita_common::debt_keeper::get_debts_list; use rita_common::payment_validator::calculate_unverified_payments; diff --git a/rita_exit/src/rita_loop/mod.rs b/rita_exit/src/rita_loop/mod.rs index 067bfc5c4..0bf1eec1d 100644 --- a/rita_exit/src/rita_loop/mod.rs +++ b/rita_exit/src/rita_loop/mod.rs @@ -10,7 +10,7 @@ //! Two threads are generated by this, one actual worker thread and a watchdog restarting thread that only //! wakes up to restart the inner thread if anything goes wrong. -use crate::{get_all_regsitered_clients, network_endpoints::*}; +use crate::network_endpoints::*; use crate::database::{ enforce_exit_clients, setup_clients, validate_clients_region, ExitClientSetupStates, @@ -23,6 +23,7 @@ use althea_kernel_interface::ExitClient; use althea_types::{Identity, WgKey}; use babel_monitor::{open_babel_stream, parse_routes}; +use rita_client_registration::client_db::get_all_regsitered_clients; use rita_common::debt_keeper::DebtAction; use std::collections::{HashMap, HashSet};