From 17c2649d76007f917dbe1b413f4c047c25badf0f Mon Sep 17 00:00:00 2001 From: PThorpe92 Date: Wed, 8 Nov 2023 21:59:42 -0500 Subject: [PATCH] fix: authentication #93, improve error handling --- Cargo.lock | 11 +++ Cargo.toml | 2 +- src/app.rs | 19 ++++- src/database/db.rs | 2 +- src/display/mod.rs | 10 +-- src/request/curl.rs | 145 +++++++++++++++++++++---------------- src/screens/auth.rs | 21 +++--- src/screens/input/input.rs | 20 +++-- 8 files changed, 141 insertions(+), 89 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e50cebf..9afc094 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -383,6 +383,7 @@ checksum = "70c44a72e830f0e40ad90dda8a6ab6ed6314d39776599a58a2e5e37fbc6db5b9" dependencies = [ "cc", "libc", + "libnghttp2-sys", "libz-sys", "openssl-sys", "pkg-config", @@ -836,6 +837,16 @@ version = "0.2.148" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" +[[package]] +name = "libnghttp2-sys" +version = "0.1.8+1.55.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fae956c192dadcdb5dace96db71fa0b827333cce7c7b38dc71446f024d8a340" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "libsqlite3-sys" version = "0.26.0" diff --git a/Cargo.toml b/Cargo.toml index ac93926..909f343 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ lazy_static = "1.4.0" rusqlite = { version = "0.29.0", features = ["bundled"] } serde_json = { version = "1.0.108", features = ["std"] } serde = { version = "1.0.190", features = ["derive"] } -curl = "0.4.44" +curl = { features = ["spnego", "ntlm", "http2"], version = "0.4.44" } mockito = "1.2.0" regex = "1.10.2" dirs = "5.0.1" diff --git a/src/app.rs b/src/app.rs index 3dc073b..8eb100a 100644 --- a/src/app.rs +++ b/src/app.rs @@ -2,7 +2,7 @@ use crate::database::db::{SavedCommand, SavedKey, DB}; use crate::display::menuopts::OPTION_PADDING_MID; use crate::display::AppOptions; use crate::request::command::{CmdOpts, CMD}; -use crate::request::curl::Curl; +use crate::request::curl::{Curl, AuthKind}; use crate::screens::screen::Screen; use crate::Config; use std::{error, mem}; @@ -189,6 +189,7 @@ impl<'a> App<'a> { .as_mut() .unwrap() .execute(Some(&mut self.db)) + } pub fn get_saved_keys(&self) -> Result, rusqlite::Error> { @@ -386,6 +387,8 @@ impl<'a> App<'a> { AppOptions::TcpKeepAlive => self.command.as_mut().unwrap().set_tcp_keepalive(true), AppOptions::SaveToken => self.command.as_mut().unwrap().save_token(true), + // Auth will be toggled for all types except for Basic, Bearer and digest + AppOptions::Auth(ref kind) => self.command.as_mut().unwrap().set_auth(kind.clone()), _ => {} } self.opts.push(opt); @@ -402,6 +405,20 @@ impl<'a> App<'a> { if self.should_add_option(&opt) { self.opts.push(opt.clone()); match opt { + // other options will be set at the input menu + // TODO: Consolidate this garbage spaghetti nonsense + AppOptions::Auth(authkind) => match authkind { + AuthKind::Spnego => { + self.command.as_mut().unwrap().set_auth(authkind); + } + AuthKind::Ntlm => { + self.command.as_mut().unwrap().set_auth(authkind); + } + AuthKind::AwsSigv4 => { + self.command.as_mut().unwrap().set_auth(authkind); + } + _ => {} + } AppOptions::UnixSocket(socket) => self.command.as_mut().unwrap().set_unix_socket(&socket), AppOptions::Headers(value) => self.command.as_mut().unwrap().add_headers(value), diff --git a/src/database/db.rs b/src/database/db.rs index 3d76e0c..26e189c 100644 --- a/src/database/db.rs +++ b/src/database/db.rs @@ -66,7 +66,7 @@ impl DB { conn.execute("BEGIN;", params![])?; conn.execute( - "CREATE TABLE IF NOT EXISTS commands (id INTEGER PRIMARY KEY, command TEXT, curl_json JSON);", + "CREATE TABLE IF NOT EXISTS commands (id INTEGER PRIMARY KEY, command TEXT, curl_json TEXT);", params![], )?; diff --git a/src/display/mod.rs b/src/display/mod.rs index ca6b604..9741217 100644 --- a/src/display/mod.rs +++ b/src/display/mod.rs @@ -1,5 +1,6 @@ -use crate::display::menuopts::{ - DISPLAY_OPT_MAX_REC, DISPLAY_OPT_MAX_REDIRECTS, DISPLAY_OPT_REFERRER, +use crate::{ + display::menuopts::{DISPLAY_OPT_MAX_REC, DISPLAY_OPT_MAX_REDIRECTS, DISPLAY_OPT_REFERRER}, + request::curl::AuthKind, }; use self::menuopts::{ @@ -34,7 +35,7 @@ pub enum AppOptions { SaveCommand, Response(String), RecDownload(usize), - Auth(String), + Auth(AuthKind), SaveToken, UnixSocket(String), FollowRedirects, @@ -73,9 +74,6 @@ impl AppOptions { AppOptions::RecDownload(ref mut level) => { *level = val.parse::().unwrap(); } - AppOptions::Auth(ref mut auth) => { - *auth = val; - } AppOptions::UnixSocket(ref mut socket) => { *socket = val; } diff --git a/src/request/curl.rs b/src/request/curl.rs index 9168352..7a88c35 100644 --- a/src/request/curl.rs +++ b/src/request/curl.rs @@ -266,7 +266,7 @@ impl Display for AuthKind { AuthKind::None => write!(f, "None"), AuthKind::Ntlm => write!(f, "NTLM"), AuthKind::Basic(login) => write!(f, "Basic: {}", login), - AuthKind::Bearer(token) => write!(f, "Bearer: {}", token), + AuthKind::Bearer(token) => write!(f, "Authorization: Bearer {}", token), AuthKind::Digest(login) => write!(f, "Digest Auth: {}", login), AuthKind::AwsSigv4 => write!(f, "AWS SignatureV4"), AuthKind::Spnego => write!(f, "SPNEGO Auth"), @@ -338,74 +338,93 @@ impl<'a> CmdOpts for Curl<'a> { fn execute(&mut self, mut db: Option<&mut Box>) -> Result<(), String> { let mut list = List::new(); + curl::init(); + // Setup auth if we have it, will return whether we appended to the list let mut has_headers = self.handle_auth_exec(&mut list); - if self.headers.is_some() { - has_headers = true; - self.headers - .as_ref() - .unwrap() + + // Handle headers + if let Some(ref headers) = self.headers { + headers .iter() .for_each(|h| list.append(h.as_str()).unwrap()); + has_headers = true; } + + // Save command to DB if self.will_save_command() { - let _ = db.as_mut().unwrap().add_command( - &self.get_command_string(), - serde_json::to_string(&self).unwrap_or(String::from("Error serializing command")), - ); + if let Some(ref mut db) = db { + let command_string = &self.get_command_string(); + let command_json = serde_json::to_string(&self) + .map_err(|e| format!("Error serializing command: {}", e))?; + if db.add_command(command_string, command_json).is_err() { + println!("Error saving command to DB"); + } + } } + // Save token to DB if self.will_save_token() { - let _ = db - .unwrap() - .add_key(&self.auth.get_token().unwrap_or_default()); + if let Some(ref mut db) = db { + if db + .add_key(&self.auth.get_token().unwrap_or_default()) + .is_err() + { + println!("Error saving token to DB"); + } + } } - // We have to append the list of headers all at once - // but if we never appended to the list, we skip this + // Append headers if needed if has_headers { - self.curl.http_headers(list).unwrap(); + self.curl + .http_headers(list) + .map_err(|e| format!("Error setting headers: {:?}", e))?; } - // If we are uploading a file... + // Upload file if specified if let Some(ref upload_file) = self.upload_file { - let file = std::fs::File::open(upload_file).unwrap(); - let mut buff: Vec = Vec::new(); - let mut reader = std::io::BufReader::new(file); - let _ = reader.read_to_end(&mut buff); - // set connect only + establish connection to the URL - self.curl.connect_only(true).unwrap(); - if self.curl.perform().is_ok() { - // Upload the file contents - if self.curl.send(buff.as_slice()).is_ok() { - Ok(()) - } else { - Err(String::from("Error with upload")) - } - } else { - Err(String::from("Error making connection")) - } - } else if self.curl.perform().is_ok() { - let contents = self.curl.get_ref(); - let res = String::from_utf8_lossy(&contents.0); - if let Ok(json) = - serde_json::from_str::(&String::from_utf8_lossy(&contents.0)) - { - self.resp = Some(serde_json::to_string_pretty(&json).unwrap()); - Ok(()) - } else { - self.resp = Some(res.to_string()); - Ok(()) + if let Ok(file) = std::fs::File::open(upload_file) { + let mut buff: Vec = Vec::new(); + let mut reader = std::io::BufReader::new(file); + reader + .read_to_end(&mut buff) + .map_err(|e| format!("Error reading file: {}", e))?; + + // set connect only + establish connection to the URL + self.curl + .connect_only(true) + .map_err(|e| format!("Error connecting: {:?}", e))?; + + // Handle upload errors + self.curl + .perform() + .map_err(|err| format!("Error making connection: {:?}", err))?; + self.curl + .send(buff.as_slice()) + .map_err(|e| format!("Error with upload: {}", e))?; } + } + + // Perform the main request + self.curl + .perform() + .map_err(|err| format!("Error: {:?}", err))?; + let contents = self.curl.get_ref(); + let res = String::from_utf8_lossy(&contents.0); + if let Ok(json) = serde_json::from_str::(&res) { + self.resp = Some(serde_json::to_string_pretty(&json).unwrap()); } else { - return Err(String::from("Error executing command")); + self.resp = Some(res.to_string()); } + Ok(()) } } + impl<'a> CurlOpts for Curl<'a> { fn set_auth(&mut self, auth: AuthKind) { match auth { - AuthKind::Basic(info) => self.set_basic_auth(info), + AuthKind::Basic(ref info) => self.set_basic_auth(info), AuthKind::Ntlm => self.set_ntlm_auth(), - AuthKind::Bearer(token) => self.set_bearer_auth(token), + AuthKind::Bearer(ref token) => self.set_bearer_auth(token), AuthKind::AwsSigv4 => self.set_aws_sigv4_auth(), AuthKind::Digest(login) => self.set_digest_auth(&login), AuthKind::Spnego => self.set_spnego_auth(), @@ -428,6 +447,7 @@ impl<'a> CurlOpts for Curl<'a> { _ => {} } } + fn set_cert_info(&mut self, opt: bool) { let flag = CurlFlag::CertInfo(CurlFlagType::CertInfo.get_value(), None); self.toggle_flag(&flag); @@ -622,7 +642,7 @@ impl<'a> Curl<'a> { > 0 } - // This is a hack because when we deseialize from the DB, we get a curl struct with no curl::Easy + // This is a hack because when we deseialize json from the DB, we get a curl struct with no curl::Easy // field, so we have to manually add, then set the options one at a time from the opts vector. // ANY time we get a command from the database to run, we have to call this method first. pub fn easy_from_opts(&mut self) { @@ -725,12 +745,12 @@ impl<'a> Curl<'a> { let _ = self.curl.http_auth(&Auth::new()); } - pub fn set_basic_auth(&mut self, login: String) { + pub fn set_basic_auth(&mut self, login: &str) { self.add_flag(CurlFlag::Basic( CurlFlagType::Basic.get_value(), Some(login.to_string()), )); - self.auth = AuthKind::Basic(login); + self.auth = AuthKind::Basic(String::from(login)); } pub fn toggle_flag(&mut self, flag: &CurlFlag<'a>) { @@ -805,12 +825,12 @@ impl<'a> Curl<'a> { self.auth = AuthKind::Ntlm; } - pub fn set_bearer_auth(&mut self, token: String) { + pub fn set_bearer_auth(&mut self, token: &str) { self.add_flag(CurlFlag::Bearer( CurlFlagType::Bearer.get_value(), - Some(format!("Authorization: Bearer {}", token)), + Some(format!("Authorization: Bearer {token}")), )); - self.auth = AuthKind::Bearer(token); + self.auth = AuthKind::Bearer(String::from(token)); } pub fn show_headers(&mut self, file: &str) { @@ -843,9 +863,7 @@ impl<'a> Curl<'a> { self.cmd = cmd.join(" ").trim().to_string(); } - pub fn handle_auth_exec(&mut self, list: &mut List) -> bool { - // we need to know if we have appended to this list - let mut list_edited = false; + fn handle_auth_exec(&mut self, list: &mut List) -> bool { match &self.auth { AuthKind::None => {} AuthKind::Basic(login) => { @@ -855,12 +873,13 @@ impl<'a> Curl<'a> { self.curl .password(login.split(':').last().unwrap()) .unwrap(); - println!("login: {}", login); + println!("login: {login}"); let _ = self.curl.http_auth(Auth::new().basic(true)); } - AuthKind::Bearer(token) => { - list_edited = true; - let _ = list.append(&format!("Authorization: Bearer {}", token.clone())); + AuthKind::Bearer(ref token) => { + list.append(&format!("Authorization: Bearer {token}")) + .unwrap(); + return true; } AuthKind::Digest(login) => { self.curl @@ -880,8 +899,8 @@ impl<'a> Curl<'a> { AuthKind::AwsSigv4 => { let _ = self.curl.http_auth(Auth::new().aws_sigv4(true)); } - }; - list_edited + } + false } pub fn url_encode(&mut self, data: &str) { @@ -1237,7 +1256,7 @@ mod tests { fn test_set_basic_auth() { let mut curl = Curl::new(); let usr_pwd = "username:password"; - curl.set_basic_auth(usr_pwd.to_string()); + curl.set_basic_auth(usr_pwd); assert_eq!(curl.opts.len(), 1); assert!(curl.opts.contains(&CurlFlag::Basic( CurlFlagType::Basic.get_value(), diff --git a/src/screens/auth.rs b/src/screens/auth.rs index ed08973..ab782e1 100644 --- a/src/screens/auth.rs +++ b/src/screens/auth.rs @@ -12,10 +12,9 @@ use crate::request::curl::AuthKind; use crate::screens::screen::Screen; // This is the display auth not to be confused with the request auth - +// it needs to be done away with and combined into one #[derive(Debug, Clone, PartialEq)] pub enum AuthType { - // OAuth looks impossible to implement Basic, Bearer, Digest, @@ -24,15 +23,16 @@ pub enum AuthType { SPNEGO, } +#[rustfmt::skip] impl Display for AuthType { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { let auth = match self { - AuthType::Basic => "Basic", - AuthType::Bearer => "Bearer", - AuthType::Digest => "Digest", + AuthType::Basic => "Basic", + AuthType::Bearer => "Bearer", + AuthType::Digest => "Digest", AuthType::AWSSignatureV4 => "AWS Signature V4", - AuthType::NTLM => "NTLM", - AuthType::SPNEGO => "SPNEGO", + AuthType::NTLM => "NTLM", + AuthType::SPNEGO => "SPNEGO", }; write!(f, "{}", auth) } @@ -47,18 +47,17 @@ pub fn handle_authentication_screen(app: &mut App, frame: &mut Frame 3 => { if varify_aws_auth() { app.goto_screen(Screen::RequestMenu(String::from(AWS_AUTH_MSG))); - app.add_app_option(AppOptions::Auth(AuthType::AWSSignatureV4.to_string())); + app.add_app_option(AppOptions::Auth(AuthKind::AwsSigv4)); } else { app.goto_screen(Screen::RequestMenu(String::from(AWS_AUTH_ERROR_MSG))); } } 4 => { - app.command.as_mut().unwrap().set_auth(AuthKind::Spnego); - app.add_app_option(AppOptions::Auth(AuthType::SPNEGO.to_string())); + app.add_app_option(AppOptions::Auth(AuthKind::Spnego)); app.goto_screen(Screen::RequestMenu(String::from(""))); } 5 => { - app.command.as_mut().unwrap().set_auth(AuthKind::Ntlm); + app.add_app_option(AppOptions::Auth(AuthKind::Ntlm)); app.goto_screen(Screen::RequestMenu(String::from( "Alert: NTLM Auth Enabled", ))); diff --git a/src/screens/input/input.rs b/src/screens/input/input.rs index 00479c6..3ed0878 100644 --- a/src/screens/input/input.rs +++ b/src/screens/input/input.rs @@ -256,17 +256,25 @@ fn validate_path(path: &str) -> bool { } fn parse_auth(auth: AuthType, app: &mut App, message: &str) { - if app.has_app_option(&AppOptions::Auth(String::new())) { - app.remove_app_option(&AppOptions::Auth(String::new())); + if app.has_app_option(&AppOptions::Auth(AuthKind::None)) { + // will till toggle no matter what kind of auth due to mem::descriminate + app.remove_app_option(&AppOptions::Auth(AuthKind::None)); } + println!("Auth set: {message}"); app.command.as_mut().unwrap().set_auth(match auth { AuthType::Basic => AuthKind::Basic(String::from(message)), AuthType::Bearer => AuthKind::Bearer(String::from(message)), AuthType::Digest => AuthKind::Digest(String::from(message)), - AuthType::AWSSignatureV4 => AuthKind::AwsSigv4, - AuthType::SPNEGO => AuthKind::Spnego, - AuthType::NTLM => AuthKind::Ntlm, + // above are the only auth options that would ever send us here + _ => AuthKind::None, }); - app.add_app_option(AppOptions::Auth(String::from(message))); + + app.add_app_option(AppOptions::Auth(match auth { + AuthType::Basic => AuthKind::Basic(String::from(message)), + AuthType::Bearer => AuthKind::Bearer(String::from(message)), + AuthType::Digest => AuthKind::Digest(String::from(message)), + // above are the only auth options that would ever send us here + _ => AuthKind::None, + })); app.goto_screen(Screen::RequestMenu(String::new())); }