330 lines
9.6 KiB
Rust
330 lines
9.6 KiB
Rust
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<E: Error> 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<E, S>(err: &E, serializer: S) -> Result<S::Ok, S::Error>
|
|
where
|
|
E: std::error::Error + AsRef<str>,
|
|
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<E, M>(err: &E, map: &mut M) -> Result<(), M::Error>
|
|
where
|
|
E: Error,
|
|
M: serde::ser::SerializeMap,
|
|
{
|
|
let msg = err.source().map(|s| format!("{s}"));
|
|
map.serialize_entry("msg", &msg)?;
|
|
map.serialize_entry("code", &None::<&str>)?;
|
|
map.serialize_entry("source", &None::<&str>)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
|
|
macro_rules! impl_serialize_basic {
|
|
($err_type:ident) => {
|
|
impl Serialize for $err_type {
|
|
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
|
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<u8>),
|
|
#[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<GetSessionTokenError>),
|
|
#[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(transparent)]
|
|
Crypto(#[from] CryptoError),
|
|
#[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),
|
|
}
|
|
|
|
|
|
#[derive(Debug, ThisError, AsRefStr)]
|
|
pub enum CryptoError {
|
|
#[error(transparent)]
|
|
Argon2(#[from] argon2::Error),
|
|
#[error("Invalid passphrase")] // I think this is the only way decryption fails
|
|
Aead(#[from] chacha20poly1305::aead::Error),
|
|
}
|
|
|
|
|
|
// 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),
|
|
#[error(transparent)]
|
|
Io(#[from] std::io::Error),
|
|
}
|
|
|
|
|
|
// =========================
|
|
// Serialize implementations
|
|
// =========================
|
|
|
|
|
|
struct SerializeWrapper<E>(pub E);
|
|
|
|
impl Serialize for SerializeWrapper<&GetSessionTokenError> {
|
|
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
|
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<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
|
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<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
|
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<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
|
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<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
|
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()
|
|
}
|
|
}
|