use std::error::Error; use std::convert::AsRef; use std::sync::mpsc; use strum_macros::AsRefStr; use thiserror::Error as ThisError; use aws_sdk_sts::{ types::SdkError as AwsSdkError, error::GetSessionTokenError, }; use sqlx::{ error::Error as SqlxError, migrate::MigrateError, }; use tauri::api::dialog::{ MessageDialogBuilder, MessageDialogKind, }; use serde::{Serialize, Serializer, ser::SerializeMap}; pub trait ErrorPopup { fn error_popup(self, title: &str); } impl ErrorPopup for Result<(), E> { fn error_popup(self, title: &str) { if let Err(e) = self { let (tx, rx) = mpsc::channel(); MessageDialogBuilder::new(title, format!("{e}")) .kind(MessageDialogKind::Error) .show(move |_| tx.send(true).unwrap()); rx.recv().unwrap(); } } } fn serialize_basic_err(err: &E, serializer: S) -> Result where E: std::error::Error + AsRef, S: Serializer, { let mut map = serializer.serialize_map(None)?; map.serialize_entry("code", err.as_ref())?; map.serialize_entry("msg", &format!("{err}"))?; if let Some(src) = err.source() { map.serialize_entry("source", &format!("{src}"))?; } map.end() } fn serialize_upstream_err(err: &E, map: &mut M) -> Result<(), M::Error> where E: Error, M: serde::ser::SerializeMap, { let src = err.source().map(|s| format!("{s}")); map.serialize_entry("source", &src) } macro_rules! impl_serialize_basic { ($err_type:ident) => { impl Serialize for $err_type { fn serialize(&self, serializer: S) -> Result { serialize_basic_err(self, serializer) } } } } // error during initial setup (primarily loading state from db) #[derive(Debug, ThisError, AsRefStr)] pub enum SetupError { #[error("Invalid database record")] InvalidRecord, // e.g. wrong size blob for nonce or salt #[error("Error from database: {0}")] DbError(#[from] SqlxError), #[error("Error running migrations: {0}")] MigrationError(#[from] MigrateError), #[error("Error parsing configuration from database")] ConfigParseError(#[from] serde_json::Error), #[error("Failed to set up start-on-login: {0}")] AutoLaunchError(#[from] auto_launch::Error), #[error("Failed to start listener: {0}")] ServerSetupError(#[from] std::io::Error), #[error("Failed to resolve data directory: {0}")] DataDir(#[from] DataDirError), } #[derive(Debug, ThisError, AsRefStr)] pub enum DataDirError { #[error("Could not determine data directory")] NotFound, #[error("Failed to create data directory: {0}")] Io(#[from] std::io::Error), } // error when attempting to tell a request handler whether to release or deny credentials #[derive(Debug, ThisError, AsRefStr)] pub enum SendResponseError { #[error("The specified credentials request was not found")] NotFound, #[error("The specified request was already closed by the client")] Abandoned, #[error("Could not renew AWS sesssion: {0}")] SessionRenew(#[from] GetSessionError), } // errors encountered while handling an HTTP request #[derive(Debug, ThisError, AsRefStr)] pub enum HandlerError { #[error("Error writing to stream: {0}")] StreamIOError(#[from] std::io::Error), // #[error("Received invalid UTF-8 in request")] // InvalidUtf8, #[error("HTTP request malformed")] BadRequest(Vec), #[error("HTTP request too large")] RequestTooLarge, #[error("Error accessing credentials: {0}")] NoCredentials(#[from] GetCredentialsError), #[error("Error getting client details: {0}")] ClientInfo(#[from] ClientInfoError), #[error("Error from Tauri: {0}")] Tauri(#[from] tauri::Error), #[error("No main application window found")] NoMainWindow, } #[derive(Debug, ThisError, AsRefStr)] pub enum GetCredentialsError { #[error("Credentials are currently locked")] Locked, #[error("No credentials are known")] Empty, } #[derive(Debug, ThisError, AsRefStr)] pub enum GetSessionError { #[error("Request completed successfully but no credentials were returned")] EmptyResponse, // SDK returned successfully but credentials are None #[error("Error response from AWS SDK: {0}")] SdkError(#[from] AwsSdkError), #[error("Could not construt session: credentials are locked")] CredentialsLocked, #[error("Could not construct session: no credentials are known")] CredentialsEmpty, } #[derive(Debug, ThisError, AsRefStr)] pub enum UnlockError { #[error("App is not locked")] NotLocked, #[error("No saved credentials were found")] NoCredentials, #[error("Invalid passphrase")] BadPassphrase, #[error("Data was found to be corrupt after decryption")] InvalidUtf8, // Somehow we got invalid utf-8 even though decryption succeeded #[error("Database error: {0}")] DbError(#[from] SqlxError), #[error("Failed to create AWS session: {0}")] GetSession(#[from] GetSessionError), } // Errors encountered while trying to figure out who's on the other end of a request #[derive(Debug, ThisError, AsRefStr)] pub enum ClientInfoError { #[error("Found PID for client socket, but no corresponding process")] ProcessNotFound, #[error("Couldn't get client socket details: {0}")] NetstatError(#[from] netstat2::error::Error), } // Errors encountered while requesting credentials via CLI (creddy show, creddy exec) #[derive(Debug, ThisError, AsRefStr)] pub enum RequestError { #[error("Credentials request failed: HTTP {0}")] Failed(String), #[error("Credentials request was rejected")] Rejected, #[error("Couldn't interpret the server's response")] MalformedHttpResponse, #[error("The server did not respond with valid JSON")] InvalidJson, #[error("Error reading/writing stream: {0}")] StreamIOError(#[from] std::io::Error), #[error("Error loading configuration data: {0}")] Setup(#[from] SetupError), } // Errors encountered while running a subprocess (via creddy exec) #[derive(Debug, ThisError, AsRefStr)] pub enum ExecError { #[error("Please specify a command")] NoCommand, #[error("Failed to execute command: {0}")] ExecutionFailed(#[from] std::io::Error) } #[derive(Debug, ThisError, AsRefStr)] pub enum CliError { #[error(transparent)] Request(#[from] RequestError), #[error(transparent)] Exec(#[from] ExecError), } // ========================= // Serialize implementations // ========================= struct SerializeWrapper(pub E); impl Serialize for SerializeWrapper<&GetSessionTokenError> { fn serialize(&self, serializer: S) -> Result { let err = self.0; let mut map = serializer.serialize_map(None)?; map.serialize_entry("code", &err.code())?; map.serialize_entry("msg", &err.message())?; map.serialize_entry("source", &None::<&str>)?; map.end() } } impl_serialize_basic!(SetupError); impl_serialize_basic!(GetCredentialsError); impl_serialize_basic!(ClientInfoError); impl Serialize for HandlerError { fn serialize(&self, serializer: S) -> Result { let mut map = serializer.serialize_map(None)?; map.serialize_entry("code", self.as_ref())?; map.serialize_entry("msg", &format!("{self}"))?; match self { HandlerError::NoCredentials(src) => map.serialize_entry("source", &src)?, HandlerError::ClientInfo(src) => map.serialize_entry("source", &src)?, _ => serialize_upstream_err(self, &mut map)?, } map.end() } } impl Serialize for SendResponseError { fn serialize(&self, serializer: S) -> Result { let mut map = serializer.serialize_map(None)?; map.serialize_entry("code", self.as_ref())?; map.serialize_entry("msg", &format!("{self}"))?; match self { SendResponseError::SessionRenew(src) => map.serialize_entry("source", &src)?, _ => serialize_upstream_err(self, &mut map)?, } map.end() } } impl Serialize for GetSessionError { fn serialize(&self, serializer: S) -> Result { let mut map = serializer.serialize_map(None)?; map.serialize_entry("code", self.as_ref())?; map.serialize_entry("msg", &format!("{self}"))?; match self { GetSessionError::SdkError(AwsSdkError::ServiceError(se_wrapper)) => { let err = se_wrapper.err(); map.serialize_entry("source", &SerializeWrapper(err))? } _ => serialize_upstream_err(self, &mut map)?, } map.end() } } impl Serialize for UnlockError { fn serialize(&self, serializer: S) -> Result { let mut map = serializer.serialize_map(None)?; map.serialize_entry("code", self.as_ref())?; map.serialize_entry("msg", &format!("{self}"))?; match self { UnlockError::GetSession(src) => map.serialize_entry("source", &src)?, _ => serialize_upstream_err(self, &mut map)?, } map.end() } }