Compare commits
	
		
			3 Commits
		
	
	
		
			wip
			...
			0d37814cf4
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 0d37814cf4 | |||
| 31532cd76e | |||
| 
						 | 
					a8aafa1519 | 
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -5,4 +5,3 @@ src-tauri/target/
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
# just in case
 | 
					# just in case
 | 
				
			||||||
credentials*
 | 
					credentials*
 | 
				
			||||||
!credentials.rs
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  "name": "creddy",
 | 
					  "name": "creddy",
 | 
				
			||||||
  "version": "0.2.1",
 | 
					  "version": "0.1.0",
 | 
				
			||||||
  "scripts": {
 | 
					  "scripts": {
 | 
				
			||||||
    "dev": "vite",
 | 
					    "dev": "vite",
 | 
				
			||||||
    "build": "vite build",
 | 
					    "build": "vite build",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1 +1 @@
 | 
				
			|||||||
DATABASE_URL=sqlite://C:/Users/Joe/AppData/Roaming/creddy/creddy.dev.db
 | 
					DATABASE_URL=sqlite://creddy.db?mode=rwc
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										2457
									
								
								src-tauri/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2457
									
								
								src-tauri/Cargo.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -1,6 +1,6 @@
 | 
				
			|||||||
[package]
 | 
					[package]
 | 
				
			||||||
name = "app"
 | 
					name = "app"
 | 
				
			||||||
version = "0.2.0"
 | 
					version = "0.1.0"
 | 
				
			||||||
description = "A Tauri App"
 | 
					description = "A Tauri App"
 | 
				
			||||||
authors = ["you"]
 | 
					authors = ["you"]
 | 
				
			||||||
license = ""
 | 
					license = ""
 | 
				
			||||||
@@ -17,8 +17,7 @@ tauri-build = { version = "1.0.4", features = [] }
 | 
				
			|||||||
[dependencies]
 | 
					[dependencies]
 | 
				
			||||||
serde_json = "1.0"
 | 
					serde_json = "1.0"
 | 
				
			||||||
serde = { version = "1.0", features = ["derive"] }
 | 
					serde = { version = "1.0", features = ["derive"] }
 | 
				
			||||||
tauri = { version = "1.2", features = ["dialog", "os-all", "system-tray"] }
 | 
					tauri = { version = "1.0.5", features = ["api-all", "system-tray"] }
 | 
				
			||||||
tauri-plugin-single-instance = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "dev" }
 | 
					 | 
				
			||||||
sodiumoxide = "0.2.7"
 | 
					sodiumoxide = "0.2.7"
 | 
				
			||||||
tokio = { version = ">=1.19", features = ["full"] }
 | 
					tokio = { version = ">=1.19", features = ["full"] }
 | 
				
			||||||
sqlx = { version = "0.6.2", features = ["sqlite", "runtime-tokio-rustls"] }
 | 
					sqlx = { version = "0.6.2", features = ["sqlite", "runtime-tokio-rustls"] }
 | 
				
			||||||
@@ -32,20 +31,14 @@ thiserror = "1.0.38"
 | 
				
			|||||||
once_cell = "1.16.0"
 | 
					once_cell = "1.16.0"
 | 
				
			||||||
strum = "0.24"
 | 
					strum = "0.24"
 | 
				
			||||||
strum_macros = "0.24"
 | 
					strum_macros = "0.24"
 | 
				
			||||||
auto-launch = "0.4.0"
 | 
					 | 
				
			||||||
dirs = "5.0"
 | 
					 | 
				
			||||||
clap = { version = "3.2.23", features = ["derive"] }
 | 
					 | 
				
			||||||
is-terminal = "0.4.7"
 | 
					 | 
				
			||||||
argon2 = { version = "0.5.0", features = ["std"] }
 | 
					 | 
				
			||||||
chacha20poly1305 = { version = "0.10.1", features = ["std"] }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
[features]
 | 
					[features]
 | 
				
			||||||
# by default Tauri runs in production mode
 | 
					# by default Tauri runs in production mode
 | 
				
			||||||
# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL
 | 
					# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL
 | 
				
			||||||
default = ["custom-protocol"]
 | 
					default = [ "custom-protocol" ]
 | 
				
			||||||
# this feature is used used for production builds where `devPath` points to the filesystem
 | 
					# this feature is used used for production builds where `devPath` points to the filesystem
 | 
				
			||||||
# DO NOT remove this
 | 
					# DO NOT remove this
 | 
				
			||||||
custom-protocol = ["tauri/custom-protocol"]
 | 
					custom-protocol = [ "tauri/custom-protocol" ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# [profile.dev.build-override]
 | 
					# [profile.dev.build-override]
 | 
				
			||||||
# opt-level = 3
 | 
					# opt-level = 3
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,7 +8,7 @@ CREATE TABLE credentials (
 | 
				
			|||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CREATE TABLE config (
 | 
					CREATE TABLE config (
 | 
				
			||||||
    name TEXT UNIQUE NOT NULL,
 | 
					    name TEXT NOT NULL,
 | 
				
			||||||
    data TEXT NOT NULL
 | 
					    data TEXT NOT NULL
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,92 +0,0 @@
 | 
				
			|||||||
use std::error::Error;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use once_cell::sync::OnceCell;
 | 
					 | 
				
			||||||
use sqlx::{
 | 
					 | 
				
			||||||
    SqlitePool,
 | 
					 | 
				
			||||||
    sqlite::SqlitePoolOptions,
 | 
					 | 
				
			||||||
    sqlite::SqliteConnectOptions,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
use tauri::{
 | 
					 | 
				
			||||||
    App,
 | 
					 | 
				
			||||||
    AppHandle,
 | 
					 | 
				
			||||||
    Manager,
 | 
					 | 
				
			||||||
    async_runtime as rt,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use crate::{
 | 
					 | 
				
			||||||
    config::{self, AppConfig},
 | 
					 | 
				
			||||||
    credentials::Session,
 | 
					 | 
				
			||||||
    ipc,
 | 
					 | 
				
			||||||
    server::Server,
 | 
					 | 
				
			||||||
    errors::*,
 | 
					 | 
				
			||||||
    state::AppState,
 | 
					 | 
				
			||||||
    tray,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub static APP: OnceCell<AppHandle> = OnceCell::new();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub fn run() -> tauri::Result<()> {
 | 
					 | 
				
			||||||
    tauri::Builder::default()
 | 
					 | 
				
			||||||
        .plugin(tauri_plugin_single_instance::init(|app, _argv, _cwd| {
 | 
					 | 
				
			||||||
            app.get_window("main")
 | 
					 | 
				
			||||||
                .map(|w| w.show().error_popup("Failed to show main window"));
 | 
					 | 
				
			||||||
        }))
 | 
					 | 
				
			||||||
        .system_tray(tray::create())
 | 
					 | 
				
			||||||
        .on_system_tray_event(tray::handle_event)
 | 
					 | 
				
			||||||
        .invoke_handler(tauri::generate_handler![
 | 
					 | 
				
			||||||
            ipc::unlock,
 | 
					 | 
				
			||||||
            ipc::respond,
 | 
					 | 
				
			||||||
            ipc::get_session_status,
 | 
					 | 
				
			||||||
            ipc::save_credentials,
 | 
					 | 
				
			||||||
            ipc::get_config,
 | 
					 | 
				
			||||||
            ipc::save_config,
 | 
					 | 
				
			||||||
        ])
 | 
					 | 
				
			||||||
        .setup(|app| rt::block_on(setup(app)))
 | 
					 | 
				
			||||||
        .build(tauri::generate_context!())?
 | 
					 | 
				
			||||||
        .run(|app, run_event| match run_event {
 | 
					 | 
				
			||||||
            tauri::RunEvent::WindowEvent { label, event, .. } => match event {
 | 
					 | 
				
			||||||
                tauri::WindowEvent::CloseRequested { api, .. } => {
 | 
					 | 
				
			||||||
                    let _ = app.get_window(&label).map(|w| w.hide());
 | 
					 | 
				
			||||||
                    api.prevent_close();
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                _ => ()
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            _ => ()
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Ok(())
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub async fn connect_db() -> Result<SqlitePool, SetupError> {
 | 
					 | 
				
			||||||
    let conn_opts = SqliteConnectOptions::new()
 | 
					 | 
				
			||||||
        .filename(config::get_or_create_db_path()?)
 | 
					 | 
				
			||||||
        .create_if_missing(true);
 | 
					 | 
				
			||||||
    let pool_opts = SqlitePoolOptions::new();
 | 
					 | 
				
			||||||
    let pool: SqlitePool = pool_opts.connect_with(conn_opts).await?;
 | 
					 | 
				
			||||||
    sqlx::migrate!().run(&pool).await?;
 | 
					 | 
				
			||||||
    Ok(pool)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async fn setup(app: &mut App) -> Result<(), Box<dyn Error>> {
 | 
					 | 
				
			||||||
    APP.set(app.handle()).unwrap();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let pool = connect_db().await?;
 | 
					 | 
				
			||||||
    let conf = AppConfig::load(&pool).await?;
 | 
					 | 
				
			||||||
    let session = Session::load(&pool).await?;
 | 
					 | 
				
			||||||
    let srv = Server::new(conf.listen_addr, conf.listen_port, app.handle()).await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    config::set_auto_launch(conf.start_on_login)?;
 | 
					 | 
				
			||||||
    if !conf.start_minimized {
 | 
					 | 
				
			||||||
        app.get_window("main")
 | 
					 | 
				
			||||||
            .ok_or(HandlerError::NoMainWindow)?
 | 
					 | 
				
			||||||
            .show()?;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let state = AppState::new(conf, session, srv, pool);
 | 
					 | 
				
			||||||
    app.manage(state);
 | 
					 | 
				
			||||||
    Ok(())
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,142 +0,0 @@
 | 
				
			|||||||
use std::process::Command as ChildCommand;
 | 
					 | 
				
			||||||
#[cfg(unix)]
 | 
					 | 
				
			||||||
use std::os::unix::process::CommandExt;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use clap::{
 | 
					 | 
				
			||||||
    Command,
 | 
					 | 
				
			||||||
     Arg,
 | 
					 | 
				
			||||||
     ArgMatches,
 | 
					 | 
				
			||||||
     ArgAction
 | 
					 | 
				
			||||||
 };
 | 
					 | 
				
			||||||
use tokio::{
 | 
					 | 
				
			||||||
    net::TcpStream,
 | 
					 | 
				
			||||||
    io::{AsyncReadExt, AsyncWriteExt},
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use crate::app;
 | 
					 | 
				
			||||||
use crate::config::AppConfig;
 | 
					 | 
				
			||||||
use crate::credentials::{BaseCredentials, SessionCredentials};
 | 
					 | 
				
			||||||
use crate::errors::*;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub fn parser() -> Command<'static> {
 | 
					 | 
				
			||||||
    Command::new("creddy")
 | 
					 | 
				
			||||||
        .about("A friendly AWS credentials manager")
 | 
					 | 
				
			||||||
        .subcommand(
 | 
					 | 
				
			||||||
            Command::new("run")
 | 
					 | 
				
			||||||
                .about("Launch Creddy")
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .subcommand(
 | 
					 | 
				
			||||||
            Command::new("show")
 | 
					 | 
				
			||||||
                .about("Fetch and display AWS credentials")
 | 
					 | 
				
			||||||
                .arg(
 | 
					 | 
				
			||||||
                    Arg::new("base")
 | 
					 | 
				
			||||||
                        .short('b')
 | 
					 | 
				
			||||||
                        .long("base")
 | 
					 | 
				
			||||||
                        .action(ArgAction::SetTrue)
 | 
					 | 
				
			||||||
                        .help("Use base credentials instead of session credentials")
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        .subcommand(
 | 
					 | 
				
			||||||
            Command::new("exec")
 | 
					 | 
				
			||||||
                .about("Inject AWS credentials into the environment of another command")
 | 
					 | 
				
			||||||
                .trailing_var_arg(true)
 | 
					 | 
				
			||||||
                .arg(
 | 
					 | 
				
			||||||
                    Arg::new("base")
 | 
					 | 
				
			||||||
                        .short('b')
 | 
					 | 
				
			||||||
                        .long("base")
 | 
					 | 
				
			||||||
                        .action(ArgAction::SetTrue)
 | 
					 | 
				
			||||||
                        .help("Use base credentials instead of session credentials")
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
                .arg(
 | 
					 | 
				
			||||||
                    Arg::new("command")
 | 
					 | 
				
			||||||
                        .multiple_values(true)
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub fn show(args: &ArgMatches) -> Result<(), CliError> {
 | 
					 | 
				
			||||||
    let base = args.get_one("base").unwrap_or(&false);
 | 
					 | 
				
			||||||
    let creds = get_credentials(*base)?;
 | 
					 | 
				
			||||||
    println!("{creds}");
 | 
					 | 
				
			||||||
    Ok(())
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub fn exec(args: &ArgMatches) -> Result<(), CliError> {
 | 
					 | 
				
			||||||
    let base = *args.get_one("base").unwrap_or(&false);
 | 
					 | 
				
			||||||
    let mut cmd_line = args.get_many("command")
 | 
					 | 
				
			||||||
        .ok_or(ExecError::NoCommand)?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let cmd_name: &String = cmd_line.next().unwrap(); // Clap guarantees that there will be at least one
 | 
					 | 
				
			||||||
    let mut cmd = ChildCommand::new(cmd_name);
 | 
					 | 
				
			||||||
    cmd.args(cmd_line);
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    if base {
 | 
					 | 
				
			||||||
        let creds: BaseCredentials = serde_json::from_str(&get_credentials(base)?)
 | 
					 | 
				
			||||||
            .map_err(|_| RequestError::InvalidJson)?;
 | 
					 | 
				
			||||||
        cmd.env("AWS_ACCESS_KEY_ID", creds.access_key_id);
 | 
					 | 
				
			||||||
        cmd.env("AWS_SECRET_ACCESS_KEY", creds.secret_access_key);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    else {
 | 
					 | 
				
			||||||
        let creds: SessionCredentials = serde_json::from_str(&get_credentials(base)?)
 | 
					 | 
				
			||||||
            .map_err(|_| RequestError::InvalidJson)?;
 | 
					 | 
				
			||||||
        cmd.env("AWS_ACCESS_KEY_ID", creds.access_key_id);
 | 
					 | 
				
			||||||
        cmd.env("AWS_SECRET_ACCESS_KEY", creds.secret_access_key);
 | 
					 | 
				
			||||||
        cmd.env("AWS_SESSION_TOKEN", creds.token);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    #[cfg(unix)]
 | 
					 | 
				
			||||||
    cmd.exec().map_err(|e| ExecError::ExecutionFailed(e))?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    #[cfg(windows)]
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        let mut child = cmd.spawn()
 | 
					 | 
				
			||||||
            .map_err(|e| ExecError::ExecutionFailed(e))?;
 | 
					 | 
				
			||||||
        let status = child.wait()
 | 
					 | 
				
			||||||
            .map_err(|e| ExecError::ExecutionFailed(e))?;
 | 
					 | 
				
			||||||
        std::process::exit(status.code().unwrap_or(1));
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[tokio::main]
 | 
					 | 
				
			||||||
async fn get_credentials(base: bool) -> Result<String, RequestError> {
 | 
					 | 
				
			||||||
    let pool = app::connect_db().await?;
 | 
					 | 
				
			||||||
    let config = AppConfig::load(&pool).await?;
 | 
					 | 
				
			||||||
    let path = if base {"/creddy/base-credentials"} else {"/"};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let mut stream = TcpStream::connect((config.listen_addr, config.listen_port)).await?;
 | 
					 | 
				
			||||||
    let req = format!("GET {path} HTTP/1.0\r\n\r\n");
 | 
					 | 
				
			||||||
    stream.write_all(req.as_bytes()).await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // some day we'll have a proper HTTP parser
 | 
					 | 
				
			||||||
    let mut buf = vec![0; 8192];
 | 
					 | 
				
			||||||
    stream.read_to_end(&mut buf).await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let status = buf.split(|&c| &[c] == b" ")
 | 
					 | 
				
			||||||
        .skip(1)
 | 
					 | 
				
			||||||
        .next()
 | 
					 | 
				
			||||||
        .ok_or(RequestError::MalformedHttpResponse)?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if status != b"200" {
 | 
					 | 
				
			||||||
        let s = String::from_utf8_lossy(status).to_string();
 | 
					 | 
				
			||||||
        return Err(RequestError::Failed(s));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let break_idx = buf.windows(4)
 | 
					 | 
				
			||||||
        .position(|w| w == b"\r\n\r\n")
 | 
					 | 
				
			||||||
        .ok_or(RequestError::MalformedHttpResponse)?;
 | 
					 | 
				
			||||||
    let body = &buf[(break_idx + 4)..];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let creds_str = std::str::from_utf8(body)
 | 
					 | 
				
			||||||
        .map_err(|_| RequestError::MalformedHttpResponse)?
 | 
					 | 
				
			||||||
        .to_string();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if creds_str == "Denied!" {
 | 
					 | 
				
			||||||
        return Err(RequestError::Rejected);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    Ok(creds_str)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,37 +1,25 @@
 | 
				
			|||||||
use std::path::PathBuf;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use netstat2::{AddressFamilyFlags, ProtocolFlags, ProtocolSocketInfo};
 | 
					use netstat2::{AddressFamilyFlags, ProtocolFlags, ProtocolSocketInfo};
 | 
				
			||||||
use tauri::Manager;
 | 
					 | 
				
			||||||
use sysinfo::{System, SystemExt, Pid, PidExt, ProcessExt};
 | 
					use sysinfo::{System, SystemExt, Pid, PidExt, ProcessExt};
 | 
				
			||||||
use serde::{Serialize, Deserialize};
 | 
					use serde::{Serialize, Deserialize};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{
 | 
					use crate::errors::*;
 | 
				
			||||||
    app::APP,
 | 
					use crate::get_state;
 | 
				
			||||||
    errors::*,
 | 
					 | 
				
			||||||
    config::AppConfig,
 | 
					 | 
				
			||||||
    state::AppState,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
 | 
					#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)]
 | 
				
			||||||
pub struct Client {
 | 
					pub struct Client {
 | 
				
			||||||
    pub pid: u32,
 | 
					    pub pid: u32,
 | 
				
			||||||
    pub exe: PathBuf,
 | 
					    pub exe: String,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async fn get_associated_pids(local_port: u16) -> Result<Vec<u32>, netstat2::error::Error> {
 | 
					fn get_associated_pids(local_port: u16) -> Result<Vec<u32>, netstat2::error::Error> {
 | 
				
			||||||
    let state = APP.get().unwrap().state::<AppState>();
 | 
					 | 
				
			||||||
    let AppConfig {
 | 
					 | 
				
			||||||
        listen_addr: app_listen_addr,
 | 
					 | 
				
			||||||
        listen_port: app_listen_port,
 | 
					 | 
				
			||||||
        ..
 | 
					 | 
				
			||||||
    } = *state.config.read().await;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let sockets_iter = netstat2::iterate_sockets_info(
 | 
					    let sockets_iter = netstat2::iterate_sockets_info(
 | 
				
			||||||
        AddressFamilyFlags::IPV4,
 | 
					        AddressFamilyFlags::IPV4,
 | 
				
			||||||
        ProtocolFlags::TCP
 | 
					        ProtocolFlags::TCP
 | 
				
			||||||
    )?;
 | 
					    )?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    get_state!(config as app_config);
 | 
				
			||||||
    for item in sockets_iter {
 | 
					    for item in sockets_iter {
 | 
				
			||||||
        let sock_info = item?;
 | 
					        let sock_info = item?;
 | 
				
			||||||
        let proto_info = match sock_info.protocol_socket_info {
 | 
					        let proto_info = match sock_info.protocol_socket_info {
 | 
				
			||||||
@@ -40,9 +28,9 @@ async fn get_associated_pids(local_port: u16) -> Result<Vec<u32>, netstat2::erro
 | 
				
			|||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if proto_info.local_port == local_port
 | 
					        if proto_info.local_port == local_port
 | 
				
			||||||
            && proto_info.remote_port == app_listen_port
 | 
					            && proto_info.remote_port == app_config.listen_port
 | 
				
			||||||
            && proto_info.local_addr == app_listen_addr
 | 
					            && proto_info.local_addr == app_config.listen_addr
 | 
				
			||||||
            && proto_info.remote_addr == app_listen_addr
 | 
					            && proto_info.remote_addr == app_config.listen_addr
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            return Ok(sock_info.associated_pids)
 | 
					            return Ok(sock_info.associated_pids)
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -52,10 +40,10 @@ async fn get_associated_pids(local_port: u16) -> Result<Vec<u32>, netstat2::erro
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Theoretically, on some systems, multiple processes can share a socket
 | 
					// Theoretically, on some systems, multiple processes can share a socket
 | 
				
			||||||
pub async fn get_clients(local_port: u16) -> Result<Vec<Option<Client>>, ClientInfoError> {
 | 
					pub fn get_clients(local_port: u16) -> Result<Vec<Option<Client>>, ClientInfoError> {
 | 
				
			||||||
    let mut clients = Vec::new();    
 | 
					    let mut clients = Vec::new();    
 | 
				
			||||||
    let mut sys = System::new();
 | 
					    let mut sys = System::new();
 | 
				
			||||||
    for p in get_associated_pids(local_port).await? {
 | 
					    for p in get_associated_pids(local_port)? {
 | 
				
			||||||
        let pid = Pid::from_u32(p);
 | 
					        let pid = Pid::from_u32(p);
 | 
				
			||||||
        sys.refresh_process(pid);
 | 
					        sys.refresh_process(pid);
 | 
				
			||||||
        let proc = sys.process(pid)
 | 
					        let proc = sys.process(pid)
 | 
				
			||||||
@@ -63,7 +51,7 @@ pub async fn get_clients(local_port: u16) -> Result<Vec<Option<Client>>, ClientI
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        let client = Client {
 | 
					        let client = Client {
 | 
				
			||||||
            pid: p,
 | 
					            pid: p,
 | 
				
			||||||
            exe: proc.exe().to_path_buf(),
 | 
					            exe: proc.exe().to_string_lossy().into_owned(),
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
        clients.push(Some(client));
 | 
					        clients.push(Some(client));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,8 +1,6 @@
 | 
				
			|||||||
use std::net::Ipv4Addr;
 | 
					use std::net::Ipv4Addr;
 | 
				
			||||||
use std::path::PathBuf;
 | 
					use std::path::PathBuf;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use auto_launch::AutoLaunchBuilder;
 | 
					 | 
				
			||||||
use is_terminal::IsTerminal;
 | 
					 | 
				
			||||||
use serde::{Serialize, Deserialize};
 | 
					use serde::{Serialize, Deserialize};
 | 
				
			||||||
use sqlx::SqlitePool;
 | 
					use sqlx::SqlitePool;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -19,8 +17,6 @@ pub struct AppConfig {
 | 
				
			|||||||
    pub rehide_ms: u64,
 | 
					    pub rehide_ms: u64,
 | 
				
			||||||
    #[serde(default = "default_start_minimized")]
 | 
					    #[serde(default = "default_start_minimized")]
 | 
				
			||||||
    pub start_minimized: bool,
 | 
					    pub start_minimized: bool,
 | 
				
			||||||
    #[serde(default = "default_start_on_login")]
 | 
					 | 
				
			||||||
    pub start_on_login: bool,
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -31,79 +27,40 @@ impl Default for AppConfig {
 | 
				
			|||||||
            listen_port: default_listen_port(),
 | 
					            listen_port: default_listen_port(),
 | 
				
			||||||
            rehide_ms: default_rehide_ms(),
 | 
					            rehide_ms: default_rehide_ms(),
 | 
				
			||||||
            start_minimized: default_start_minimized(),
 | 
					            start_minimized: default_start_minimized(),
 | 
				
			||||||
            start_on_login: default_start_on_login(),
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl AppConfig {
 | 
					pub async fn load(pool: &SqlitePool) -> Result<AppConfig, SetupError> {
 | 
				
			||||||
    pub async fn load(pool: &SqlitePool) -> Result<AppConfig, SetupError> {
 | 
					    let res = sqlx::query!("SELECT * from config where name = 'main'")
 | 
				
			||||||
        let res = sqlx::query!("SELECT * from config where name = 'main'")
 | 
					        .fetch_optional(pool)
 | 
				
			||||||
            .fetch_optional(pool)
 | 
					        .await?;
 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let row = match res {
 | 
					    let row = match res {
 | 
				
			||||||
            Some(row) => row,
 | 
					        Some(row) => row,
 | 
				
			||||||
            None => return Ok(AppConfig::default()),
 | 
					        None => return Ok(AppConfig::default()),
 | 
				
			||||||
        };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Ok(serde_json::from_str(&row.data)?)
 | 
					    Ok(serde_json::from_str(&row.data)?)
 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub async fn save(&self, pool: &SqlitePool) -> Result<(), sqlx::error::Error> {
 | 
					 | 
				
			||||||
        let data = serde_json::to_string(self).unwrap();
 | 
					 | 
				
			||||||
        sqlx::query(
 | 
					 | 
				
			||||||
            "INSERT INTO config (name, data) VALUES ('main', ?)
 | 
					 | 
				
			||||||
            ON CONFLICT (name) DO UPDATE SET data = ?"
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
            .bind(&data)
 | 
					 | 
				
			||||||
            .bind(&data)
 | 
					 | 
				
			||||||
            .execute(pool)
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Ok(())
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub fn set_auto_launch(is_configured: bool) -> Result<(), SetupError> {
 | 
					pub fn get_or_create_db_path() -> PathBuf {
 | 
				
			||||||
    let path_buf = std::env::current_exe()
 | 
					    if cfg!(debug_assertions) {
 | 
				
			||||||
        .map_err(|e| auto_launch::Error::Io(e))?;
 | 
					        return PathBuf::from("./creddy.db");
 | 
				
			||||||
    let path = path_buf
 | 
					 | 
				
			||||||
        .to_string_lossy();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let auto = AutoLaunchBuilder::new()
 | 
					 | 
				
			||||||
        .set_app_name("Creddy")
 | 
					 | 
				
			||||||
        .set_app_path(&path)
 | 
					 | 
				
			||||||
        .build()?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let is_enabled = auto.is_enabled()?;
 | 
					 | 
				
			||||||
    if is_configured && !is_enabled {
 | 
					 | 
				
			||||||
        auto.enable()?;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    else if !is_configured && is_enabled {
 | 
					 | 
				
			||||||
        auto.disable()?;
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Ok(())
 | 
					    let mut parent = std::env::var("HOME")
 | 
				
			||||||
}
 | 
					        .map(|h| {
 | 
				
			||||||
 | 
					            let mut p = PathBuf::from(h);
 | 
				
			||||||
 | 
					            p.push(".config");
 | 
				
			||||||
 | 
					            p
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .unwrap_or(PathBuf::from("."));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    parent.push("creddy.db");
 | 
				
			||||||
pub fn get_or_create_db_path() -> Result<PathBuf, DataDirError> {
 | 
					    parent
 | 
				
			||||||
    let mut path = dirs::data_dir()
 | 
					 | 
				
			||||||
        .ok_or(DataDirError::NotFound)?;
 | 
					 | 
				
			||||||
    path.push("Creddy");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    std::fs::create_dir_all(&path)?;
 | 
					 | 
				
			||||||
    if cfg!(debug_assertions) && std::io::stdout().is_terminal() {
 | 
					 | 
				
			||||||
        path.push("creddy.dev.db");
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    else {
 | 
					 | 
				
			||||||
        path.push("creddy.db");
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Ok(path)
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -117,7 +74,7 @@ fn default_listen_port() -> u16 {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
fn default_listen_addr() -> Ipv4Addr { Ipv4Addr::LOCALHOST }
 | 
					fn default_listen_addr() -> Ipv4Addr { Ipv4Addr::LOCALHOST }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
fn default_rehide_ms() -> u64 { 1000 }
 | 
					fn default_rehide_ms() -> u64 { 1000 }
 | 
				
			||||||
// start minimized and on login only in production mode
 | 
					
 | 
				
			||||||
fn default_start_minimized() -> bool { !cfg!(debug_assertions) }
 | 
					fn default_start_minimized() -> bool { !cfg!(debug_assertions) } // default to start-minimized in production only
 | 
				
			||||||
fn default_start_on_login() -> bool { !cfg!(debug_assertions) }
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,321 +0,0 @@
 | 
				
			|||||||
use std::fmt::{self, Formatter};
 | 
					 | 
				
			||||||
use std::time::{SystemTime, UNIX_EPOCH};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 use aws_smithy_types::date_time::{DateTime, Format};
 | 
					 | 
				
			||||||
use argon2::{
 | 
					 | 
				
			||||||
    Argon2,
 | 
					 | 
				
			||||||
    Algorithm,
 | 
					 | 
				
			||||||
    Version,
 | 
					 | 
				
			||||||
    ParamsBuilder,
 | 
					 | 
				
			||||||
    password_hash::rand_core::{RngCore, OsRng},
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
use chacha20poly1305::{
 | 
					 | 
				
			||||||
    XChaCha20Poly1305,
 | 
					 | 
				
			||||||
    XNonce,
 | 
					 | 
				
			||||||
    aead::{
 | 
					 | 
				
			||||||
        Aead,
 | 
					 | 
				
			||||||
        AeadCore,
 | 
					 | 
				
			||||||
        KeyInit,
 | 
					 | 
				
			||||||
        Error as AeadError,
 | 
					 | 
				
			||||||
        generic_array::GenericArray,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
use serde::{
 | 
					 | 
				
			||||||
    Serialize,
 | 
					 | 
				
			||||||
    Deserialize,
 | 
					 | 
				
			||||||
    Serializer,
 | 
					 | 
				
			||||||
    Deserializer,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
use serde::de::{self, Visitor};
 | 
					 | 
				
			||||||
use sqlx::SqlitePool;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use crate::errors::*;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Clone, Debug)]
 | 
					 | 
				
			||||||
pub enum Session {
 | 
					 | 
				
			||||||
    Unlocked{
 | 
					 | 
				
			||||||
        base: BaseCredentials,
 | 
					 | 
				
			||||||
        session: SessionCredentials,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    Locked(LockedCredentials),
 | 
					 | 
				
			||||||
    Empty,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Session {
 | 
					 | 
				
			||||||
    pub async fn load(pool: &SqlitePool) -> Result<Self, SetupError> {
 | 
					 | 
				
			||||||
        let res = sqlx::query!("SELECT * FROM credentials ORDER BY created_at desc")
 | 
					 | 
				
			||||||
            .fetch_optional(pool)
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
        let row = match res {
 | 
					 | 
				
			||||||
            Some(r) => r,
 | 
					 | 
				
			||||||
            None => {return Ok(Session::Empty);}
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let salt: [u8; 32] = row.salt
 | 
					 | 
				
			||||||
            .try_into()
 | 
					 | 
				
			||||||
            .map_err(|_e| SetupError::InvalidRecord)?;
 | 
					 | 
				
			||||||
        let nonce = XNonce::from_exact_iter(row.nonce.into_iter())
 | 
					 | 
				
			||||||
            .ok_or(SetupError::InvalidRecord)?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let creds = LockedCredentials {
 | 
					 | 
				
			||||||
            access_key_id: row.access_key_id,
 | 
					 | 
				
			||||||
            secret_key_enc: row.secret_key_enc,
 | 
					 | 
				
			||||||
            salt,
 | 
					 | 
				
			||||||
            nonce,
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
        Ok(Session::Locked(creds))
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub async fn renew_if_expired(&mut self) -> Result<bool, GetSessionError> {
 | 
					 | 
				
			||||||
        match self {
 | 
					 | 
				
			||||||
            Session::Unlocked{ref base, ref mut session} => {
 | 
					 | 
				
			||||||
                if !session.is_expired() {
 | 
					 | 
				
			||||||
                    return Ok(false);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                *session = SessionCredentials::from_base(base).await?;
 | 
					 | 
				
			||||||
                Ok(true)
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            Session::Locked(_) => Err(GetSessionError::CredentialsLocked),
 | 
					 | 
				
			||||||
            Session::Empty => Err(GetSessionError::CredentialsEmpty),
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Clone, Debug)]
 | 
					 | 
				
			||||||
pub struct LockedCredentials {
 | 
					 | 
				
			||||||
    pub access_key_id: String,
 | 
					 | 
				
			||||||
    pub secret_key_enc: Vec<u8>,
 | 
					 | 
				
			||||||
    pub salt: [u8; 32],
 | 
					 | 
				
			||||||
    pub nonce: XNonce,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl LockedCredentials {
 | 
					 | 
				
			||||||
    pub async fn save(&self, pool: &SqlitePool) -> Result<(), sqlx::Error> {
 | 
					 | 
				
			||||||
        sqlx::query(
 | 
					 | 
				
			||||||
            "INSERT INTO credentials (access_key_id, secret_key_enc, salt, nonce, created_at)
 | 
					 | 
				
			||||||
            VALUES (?, ?, ?, ?, strftime('%s'))"
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
            .bind(&self.access_key_id)
 | 
					 | 
				
			||||||
            .bind(&self.secret_key_enc)
 | 
					 | 
				
			||||||
            .bind(&self.salt[..])
 | 
					 | 
				
			||||||
            .bind(&self.nonce[..])
 | 
					 | 
				
			||||||
            .execute(pool)
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Ok(())
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub fn decrypt(&self, passphrase: &str) -> Result<BaseCredentials, UnlockError> {
 | 
					 | 
				
			||||||
        let crypto = Crypto::new(passphrase, &self.salt)
 | 
					 | 
				
			||||||
            .map_err(|e| CryptoError::Argon2(e))?;
 | 
					 | 
				
			||||||
        let decrypted = crypto.decrypt(&self.nonce, &self.secret_key_enc)
 | 
					 | 
				
			||||||
            .map_err(|e| CryptoError::Aead(e))?;
 | 
					 | 
				
			||||||
        let secret_access_key = String::from_utf8(decrypted)
 | 
					 | 
				
			||||||
            .map_err(|_| UnlockError::InvalidUtf8)?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let creds = BaseCredentials {
 | 
					 | 
				
			||||||
            access_key_id: self.access_key_id.clone(),
 | 
					 | 
				
			||||||
            secret_access_key,
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
        Ok(creds)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
 | 
					 | 
				
			||||||
#[serde(rename_all = "PascalCase")]
 | 
					 | 
				
			||||||
pub struct BaseCredentials {
 | 
					 | 
				
			||||||
    pub access_key_id: String,
 | 
					 | 
				
			||||||
    pub secret_access_key: String,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl BaseCredentials {
 | 
					 | 
				
			||||||
    pub fn encrypt(&self, passphrase: &str) -> Result<LockedCredentials, CryptoError> {
 | 
					 | 
				
			||||||
        let salt = Crypto::salt();
 | 
					 | 
				
			||||||
        let crypto = Crypto::new(passphrase, &salt)?;
 | 
					 | 
				
			||||||
        let (nonce, secret_key_enc) = crypto.encrypt(self.secret_access_key.as_bytes())?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let locked = LockedCredentials {
 | 
					 | 
				
			||||||
            access_key_id: self.access_key_id.clone(),
 | 
					 | 
				
			||||||
            secret_key_enc,
 | 
					 | 
				
			||||||
            salt,
 | 
					 | 
				
			||||||
            nonce,
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
        Ok(locked)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
 | 
					 | 
				
			||||||
#[serde(rename_all = "PascalCase")]
 | 
					 | 
				
			||||||
pub struct SessionCredentials {
 | 
					 | 
				
			||||||
    pub access_key_id: String,
 | 
					 | 
				
			||||||
    pub secret_access_key: String,
 | 
					 | 
				
			||||||
    pub token: String,
 | 
					 | 
				
			||||||
    #[serde(serialize_with = "serialize_expiration")]
 | 
					 | 
				
			||||||
    #[serde(deserialize_with = "deserialize_expiration")]
 | 
					 | 
				
			||||||
    pub expiration: DateTime,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl SessionCredentials {
 | 
					 | 
				
			||||||
    pub async fn from_base(base: &BaseCredentials) -> Result<Self, GetSessionError> {
 | 
					 | 
				
			||||||
        let req_creds = aws_sdk_sts::Credentials::new(
 | 
					 | 
				
			||||||
            &base.access_key_id,
 | 
					 | 
				
			||||||
            &base.secret_access_key,
 | 
					 | 
				
			||||||
            None, // token
 | 
					 | 
				
			||||||
            None, //expiration
 | 
					 | 
				
			||||||
            "Creddy", // "provider name" apparently
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        let config = aws_config::from_env()
 | 
					 | 
				
			||||||
            .credentials_provider(req_creds)
 | 
					 | 
				
			||||||
            .load()
 | 
					 | 
				
			||||||
            .await;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let client = aws_sdk_sts::Client::new(&config);
 | 
					 | 
				
			||||||
        let resp = client.get_session_token()
 | 
					 | 
				
			||||||
            .duration_seconds(43_200)
 | 
					 | 
				
			||||||
            .send()
 | 
					 | 
				
			||||||
            .await?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let aws_session = resp.credentials().ok_or(GetSessionError::EmptyResponse)?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let access_key_id = aws_session.access_key_id()
 | 
					 | 
				
			||||||
            .ok_or(GetSessionError::EmptyResponse)?
 | 
					 | 
				
			||||||
            .to_string();
 | 
					 | 
				
			||||||
        let secret_access_key = aws_session.secret_access_key()
 | 
					 | 
				
			||||||
            .ok_or(GetSessionError::EmptyResponse)?
 | 
					 | 
				
			||||||
            .to_string();
 | 
					 | 
				
			||||||
        let token = aws_session.session_token()
 | 
					 | 
				
			||||||
            .ok_or(GetSessionError::EmptyResponse)?
 | 
					 | 
				
			||||||
            .to_string();
 | 
					 | 
				
			||||||
        let expiration = aws_session.expiration()
 | 
					 | 
				
			||||||
            .ok_or(GetSessionError::EmptyResponse)?
 | 
					 | 
				
			||||||
            .clone();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let session_creds = SessionCredentials {
 | 
					 | 
				
			||||||
            access_key_id,
 | 
					 | 
				
			||||||
            secret_access_key,
 | 
					 | 
				
			||||||
            token,
 | 
					 | 
				
			||||||
            expiration,
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        #[cfg(debug_assertions)]
 | 
					 | 
				
			||||||
        println!("Got new session:\n{}", serde_json::to_string(&session_creds).unwrap());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Ok(session_creds)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub fn is_expired(&self) -> bool {
 | 
					 | 
				
			||||||
        let current_ts = SystemTime::now()
 | 
					 | 
				
			||||||
            .duration_since(UNIX_EPOCH)
 | 
					 | 
				
			||||||
            .unwrap() // doesn't panic because UNIX_EPOCH won't be later than now()
 | 
					 | 
				
			||||||
            .as_secs();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let expire_ts = self.expiration.secs();
 | 
					 | 
				
			||||||
        let remaining = expire_ts - (current_ts as i64);
 | 
					 | 
				
			||||||
        remaining < 60
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
fn serialize_expiration<S>(exp: &DateTime, serializer: S) -> Result<S::Ok, S::Error>
 | 
					 | 
				
			||||||
where S: Serializer
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
    // this only fails if the d/t is out of range, which it can't be for this format
 | 
					 | 
				
			||||||
    let time_str = exp.fmt(Format::DateTime).unwrap();
 | 
					 | 
				
			||||||
    serializer.serialize_str(&time_str)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
struct DateTimeVisitor;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl<'de> Visitor<'de> for DateTimeVisitor {
 | 
					 | 
				
			||||||
    type Value = DateTime;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
 | 
					 | 
				
			||||||
        write!(formatter, "an RFC 3339 UTC string, e.g. \"2014-01-05T10:17:34Z\"")
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    fn visit_str<E: de::Error>(self, v: &str) -> Result<DateTime, E> {
 | 
					 | 
				
			||||||
        DateTime::from_str(v, Format::DateTime)
 | 
					 | 
				
			||||||
            .map_err(|_| E::custom(format!("Invalid date/time: {v}")))
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
fn deserialize_expiration<'de, D>(deserializer: D) -> Result<DateTime, D::Error>
 | 
					 | 
				
			||||||
where D: Deserializer<'de>
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
    deserializer.deserialize_str(DateTimeVisitor)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
struct Crypto {
 | 
					 | 
				
			||||||
    cipher: XChaCha20Poly1305,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl Crypto {
 | 
					 | 
				
			||||||
    /// Argon2 params rationale:
 | 
					 | 
				
			||||||
    ///
 | 
					 | 
				
			||||||
    /// m_cost is measured in KiB, so 128 * 1024 gives us 128MiB.
 | 
					 | 
				
			||||||
    /// This should roughly double the memory usage of the application
 | 
					 | 
				
			||||||
    /// while deriving the key.
 | 
					 | 
				
			||||||
    ///
 | 
					 | 
				
			||||||
    /// p_cost is irrelevant since (at present) there isn't any parallelism
 | 
					 | 
				
			||||||
    /// implemented, so we leave it at 1.
 | 
					 | 
				
			||||||
    ///
 | 
					 | 
				
			||||||
    /// With the above m_cost, t_cost = 8 results in about 800ms to derive
 | 
					 | 
				
			||||||
    /// a key on my (somewhat older) CPU. This is probably overkill, but
 | 
					 | 
				
			||||||
    /// given that it should only have to happen ~once a day for most 
 | 
					 | 
				
			||||||
    /// usage, it should be acceptable.
 | 
					 | 
				
			||||||
    #[cfg(not(debug_assertions))]
 | 
					 | 
				
			||||||
    const MEM_COST: u32 = 128 * 1024;
 | 
					 | 
				
			||||||
    #[cfg(not(debug_assertions))]
 | 
					 | 
				
			||||||
    const TIME_COST: u32 = 8;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /// But since this takes a million years in an unoptimized build,
 | 
					 | 
				
			||||||
    /// we turn it way down in debug builds.
 | 
					 | 
				
			||||||
    #[cfg(debug_assertions)]
 | 
					 | 
				
			||||||
    const MEM_COST: u32 = 48 * 1024;
 | 
					 | 
				
			||||||
    #[cfg(debug_assertions)]
 | 
					 | 
				
			||||||
    const TIME_COST: u32 = 1;
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    fn new(passphrase: &str, salt: &[u8]) -> argon2::Result<Crypto> {
 | 
					 | 
				
			||||||
        let params = ParamsBuilder::new()
 | 
					 | 
				
			||||||
            .m_cost(Self::MEM_COST)
 | 
					 | 
				
			||||||
            .p_cost(1)
 | 
					 | 
				
			||||||
            .t_cost(Self::TIME_COST)
 | 
					 | 
				
			||||||
            .build()
 | 
					 | 
				
			||||||
            .unwrap(); // only errors if the given params are invalid
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let hasher = Argon2::new(
 | 
					 | 
				
			||||||
            Algorithm::Argon2id,
 | 
					 | 
				
			||||||
            Version::V0x13,
 | 
					 | 
				
			||||||
            params,
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let mut key = [0; 32];
 | 
					 | 
				
			||||||
        hasher.hash_password_into(passphrase.as_bytes(), &salt, &mut key)?;
 | 
					 | 
				
			||||||
        let cipher = XChaCha20Poly1305::new(GenericArray::from_slice(&key));
 | 
					 | 
				
			||||||
        Ok(Crypto { cipher })
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    fn salt() -> [u8; 32] {
 | 
					 | 
				
			||||||
        let mut salt = [0; 32];
 | 
					 | 
				
			||||||
        OsRng.fill_bytes(&mut salt);
 | 
					 | 
				
			||||||
        salt
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    fn encrypt(&self, data: &[u8]) -> Result<(XNonce, Vec<u8>), AeadError> {
 | 
					 | 
				
			||||||
        let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);
 | 
					 | 
				
			||||||
        let ciphertext = self.cipher.encrypt(&nonce, data)?;
 | 
					 | 
				
			||||||
        Ok((nonce, ciphertext))
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    fn decrypt(&self, nonce: &XNonce, data: &[u8]) -> Result<Vec<u8>, AeadError> {
 | 
					 | 
				
			||||||
        self.cipher.decrypt(nonce, data)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,9 +1,9 @@
 | 
				
			|||||||
use std::error::Error;
 | 
					use std::error::Error;
 | 
				
			||||||
use std::convert::AsRef;
 | 
					use std::convert::AsRef;
 | 
				
			||||||
use std::sync::mpsc;
 | 
					 | 
				
			||||||
use strum_macros::AsRefStr;
 | 
					use strum_macros::AsRefStr;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use thiserror::Error as ThisError;
 | 
					use thiserror::Error as ThisError;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use aws_sdk_sts::{
 | 
					use aws_sdk_sts::{
 | 
				
			||||||
    types::SdkError as AwsSdkError, 
 | 
					    types::SdkError as AwsSdkError, 
 | 
				
			||||||
    error::GetSessionTokenError,
 | 
					    error::GetSessionTokenError,
 | 
				
			||||||
@@ -12,29 +12,32 @@ use sqlx::{
 | 
				
			|||||||
    error::Error as SqlxError,
 | 
					    error::Error as SqlxError,
 | 
				
			||||||
    migrate::MigrateError,
 | 
					    migrate::MigrateError,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
use tauri::api::dialog::{
 | 
					
 | 
				
			||||||
    MessageDialogBuilder, 
 | 
					 | 
				
			||||||
    MessageDialogKind,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
use serde::{Serialize, Serializer, ser::SerializeMap};
 | 
					use serde::{Serialize, Serializer, ser::SerializeMap};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub trait ErrorPopup {
 | 
					// pub struct SerializeError<E> {
 | 
				
			||||||
    fn error_popup(self, title: &str);
 | 
					//     pub err: E,
 | 
				
			||||||
}
 | 
					// }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl<E: Error> ErrorPopup for Result<(), E> {
 | 
					// impl<E: std::error::Error> Serialize for SerializeError<E>
 | 
				
			||||||
    fn error_popup(self, title: &str) {
 | 
					// {
 | 
				
			||||||
        if let Err(e) = self {
 | 
					//     fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
 | 
				
			||||||
            let (tx, rx) = mpsc::channel();
 | 
					//         let mut map = serializer.serialize_map(None)?;
 | 
				
			||||||
            MessageDialogBuilder::new(title, format!("{e}"))
 | 
					//         map.serialize_entry("msg", &format!("{}", self.err))?;
 | 
				
			||||||
                .kind(MessageDialogKind::Error)
 | 
					//         if let Some(src) = self.err.source() {
 | 
				
			||||||
                .show(move |_| tx.send(true).unwrap());
 | 
					//             let ser_src = SerializeError { err: src };
 | 
				
			||||||
 | 
					//             map.serialize_entry("source", &ser_src)?;
 | 
				
			||||||
 | 
					//         }
 | 
				
			||||||
 | 
					//         map.end()
 | 
				
			||||||
 | 
					//     }
 | 
				
			||||||
 | 
					// }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            rx.recv().unwrap();
 | 
					// impl<E: std::error::Error> From<E> for SerializeError<E> {
 | 
				
			||||||
        }
 | 
					//     fn from(err: E) -> Self {
 | 
				
			||||||
    }
 | 
					//         SerializeError { err }
 | 
				
			||||||
}
 | 
					//     }
 | 
				
			||||||
 | 
					// }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
fn serialize_basic_err<E, S>(err: &E, serializer: S) -> Result<S::Ok, S::Error>
 | 
					fn serialize_basic_err<E, S>(err: &E, serializer: S) -> Result<S::Ok, S::Error>
 | 
				
			||||||
@@ -84,21 +87,6 @@ pub enum SetupError {
 | 
				
			|||||||
    MigrationError(#[from] MigrateError),
 | 
					    MigrationError(#[from] MigrateError),
 | 
				
			||||||
    #[error("Error parsing configuration from database")]
 | 
					    #[error("Error parsing configuration from database")]
 | 
				
			||||||
    ConfigParseError(#[from] serde_json::Error),
 | 
					    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),
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -106,23 +94,20 @@ pub enum DataDirError {
 | 
				
			|||||||
#[derive(Debug, ThisError, AsRefStr)]
 | 
					#[derive(Debug, ThisError, AsRefStr)]
 | 
				
			||||||
pub enum SendResponseError {
 | 
					pub enum SendResponseError {
 | 
				
			||||||
    #[error("The specified credentials request was not found")]
 | 
					    #[error("The specified credentials request was not found")]
 | 
				
			||||||
    NotFound,
 | 
					    NotFound, // no request with the given id
 | 
				
			||||||
    #[error("The specified request was already closed by the client")]
 | 
					    #[error("The specified request was already closed by the client")]
 | 
				
			||||||
    Abandoned,
 | 
					    Abandoned, // request has already been closed by client
 | 
				
			||||||
    #[error("Could not renew AWS sesssion: {0}")]
 | 
					 | 
				
			||||||
    SessionRenew(#[from] GetSessionError),
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// errors encountered while handling an HTTP request
 | 
					// errors encountered while handling an HTTP request
 | 
				
			||||||
#[derive(Debug, ThisError, AsRefStr)]
 | 
					#[derive(Debug, ThisError, AsRefStr)]
 | 
				
			||||||
pub enum HandlerError {
 | 
					pub enum RequestError {
 | 
				
			||||||
    #[error("Error writing to stream: {0}")]
 | 
					    #[error("Error writing to stream: {0}")]
 | 
				
			||||||
    StreamIOError(#[from] std::io::Error),
 | 
					    StreamIOError(#[from] std::io::Error),
 | 
				
			||||||
    // #[error("Received invalid UTF-8 in request")]
 | 
					    // #[error("Received invalid UTF-8 in request")]
 | 
				
			||||||
    // InvalidUtf8,
 | 
					    // InvalidUtf8,
 | 
				
			||||||
    #[error("HTTP request malformed")]
 | 
					    // MalformedHttpRequest,
 | 
				
			||||||
    BadRequest(Vec<u8>),
 | 
					 | 
				
			||||||
    #[error("HTTP request too large")]
 | 
					    #[error("HTTP request too large")]
 | 
				
			||||||
    RequestTooLarge,
 | 
					    RequestTooLarge,
 | 
				
			||||||
    #[error("Error accessing credentials: {0}")]
 | 
					    #[error("Error accessing credentials: {0}")]
 | 
				
			||||||
@@ -148,13 +133,9 @@ pub enum GetCredentialsError {
 | 
				
			|||||||
#[derive(Debug, ThisError, AsRefStr)]
 | 
					#[derive(Debug, ThisError, AsRefStr)]
 | 
				
			||||||
pub enum GetSessionError {
 | 
					pub enum GetSessionError {
 | 
				
			||||||
    #[error("Request completed successfully but no credentials were returned")]
 | 
					    #[error("Request completed successfully but no credentials were returned")]
 | 
				
			||||||
    EmptyResponse, // SDK returned successfully but credentials are None
 | 
					    NoCredentials, // SDK returned successfully but credentials are None
 | 
				
			||||||
    #[error("Error response from AWS SDK: {0}")]
 | 
					    #[error("Error response from AWS SDK: {0}")]
 | 
				
			||||||
    SdkError(#[from] AwsSdkError<GetSessionTokenError>),
 | 
					    SdkError(#[from] AwsSdkError<GetSessionTokenError>),
 | 
				
			||||||
    #[error("Could not construt session: credentials are locked")]
 | 
					 | 
				
			||||||
    CredentialsLocked,
 | 
					 | 
				
			||||||
    #[error("Could not construct session: no credentials are known")]
 | 
					 | 
				
			||||||
    CredentialsEmpty,
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -164,8 +145,8 @@ pub enum UnlockError {
 | 
				
			|||||||
    NotLocked,
 | 
					    NotLocked,
 | 
				
			||||||
    #[error("No saved credentials were found")]
 | 
					    #[error("No saved credentials were found")]
 | 
				
			||||||
    NoCredentials,
 | 
					    NoCredentials,
 | 
				
			||||||
    #[error(transparent)]
 | 
					    #[error("Invalid passphrase")]
 | 
				
			||||||
    Crypto(#[from] CryptoError),
 | 
					    BadPassphrase,
 | 
				
			||||||
    #[error("Data was found to be corrupt after decryption")]
 | 
					    #[error("Data was found to be corrupt after decryption")]
 | 
				
			||||||
    InvalidUtf8, // Somehow we got invalid utf-8 even though decryption succeeded
 | 
					    InvalidUtf8, // Somehow we got invalid utf-8 even though decryption succeeded
 | 
				
			||||||
    #[error("Database error: {0}")]
 | 
					    #[error("Database error: {0}")]
 | 
				
			||||||
@@ -175,15 +156,6 @@ pub enum UnlockError {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[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
 | 
					// Errors encountered while trying to figure out who's on the other end of a request
 | 
				
			||||||
#[derive(Debug, ThisError, AsRefStr)]
 | 
					#[derive(Debug, ThisError, AsRefStr)]
 | 
				
			||||||
pub enum ClientInfoError {
 | 
					pub enum ClientInfoError {
 | 
				
			||||||
@@ -194,43 +166,6 @@ pub enum ClientInfoError {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 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
 | 
					// Serialize implementations
 | 
				
			||||||
// =========================
 | 
					// =========================
 | 
				
			||||||
@@ -252,35 +187,20 @@ impl Serialize for SerializeWrapper<&GetSessionTokenError> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl_serialize_basic!(SetupError);
 | 
					impl_serialize_basic!(SetupError);
 | 
				
			||||||
 | 
					impl_serialize_basic!(SendResponseError);
 | 
				
			||||||
impl_serialize_basic!(GetCredentialsError);
 | 
					impl_serialize_basic!(GetCredentialsError);
 | 
				
			||||||
impl_serialize_basic!(ClientInfoError);
 | 
					impl_serialize_basic!(ClientInfoError);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl Serialize for HandlerError {
 | 
					impl Serialize for RequestError {
 | 
				
			||||||
    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
 | 
					    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
 | 
				
			||||||
        let mut map = serializer.serialize_map(None)?;
 | 
					        let mut map = serializer.serialize_map(None)?;
 | 
				
			||||||
        map.serialize_entry("code", self.as_ref())?;
 | 
					        map.serialize_entry("code", self.as_ref())?;
 | 
				
			||||||
        map.serialize_entry("msg", &format!("{self}"))?;
 | 
					        map.serialize_entry("msg", &format!("{self}"))?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        match self {
 | 
					        match self {
 | 
				
			||||||
            HandlerError::NoCredentials(src) => map.serialize_entry("source", &src)?,
 | 
					            RequestError::NoCredentials(src) => map.serialize_entry("source", &src)?,
 | 
				
			||||||
            HandlerError::ClientInfo(src) => map.serialize_entry("source", &src)?,
 | 
					            RequestError::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)?,
 | 
					            _ => serialize_upstream_err(self, &mut map)?,
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,18 +1,16 @@
 | 
				
			|||||||
use serde::{Serialize, Deserialize};
 | 
					use serde::{Serialize, Deserialize};
 | 
				
			||||||
use tauri::State;
 | 
					use tauri::State;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::config::AppConfig;
 | 
					 | 
				
			||||||
use crate::credentials::{Session,BaseCredentials};
 | 
					 | 
				
			||||||
use crate::errors::*;
 | 
					use crate::errors::*;
 | 
				
			||||||
 | 
					use crate::config::AppConfig;
 | 
				
			||||||
use crate::clientinfo::Client;
 | 
					use crate::clientinfo::Client;
 | 
				
			||||||
use crate::state::AppState;
 | 
					use crate::state::{AppState, Session, Credentials};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
 | 
					#[derive(Clone, Debug, Serialize, Deserialize)]
 | 
				
			||||||
pub struct Request {
 | 
					pub struct Request {
 | 
				
			||||||
    pub id: u64,
 | 
					    pub id: u64,
 | 
				
			||||||
    pub clients: Vec<Option<Client>>,
 | 
					    pub clients: Vec<Option<Client>>,
 | 
				
			||||||
    pub base: bool,
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -31,50 +29,48 @@ pub enum Approval {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[tauri::command]
 | 
					#[tauri::command]
 | 
				
			||||||
pub async fn respond(response: RequestResponse, app_state: State<'_, AppState>) -> Result<(), SendResponseError> {
 | 
					pub fn respond(response: RequestResponse, app_state: State<'_, AppState>) -> Result<(), String> {
 | 
				
			||||||
    app_state.send_response(response).await
 | 
					    app_state.send_response(response)
 | 
				
			||||||
 | 
					        .map_err(|e| format!("Error responding to request: {e}"))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[tauri::command]
 | 
					#[tauri::command]
 | 
				
			||||||
pub async fn unlock(passphrase: String, app_state: State<'_, AppState>) -> Result<(), UnlockError> {
 | 
					pub async fn unlock(passphrase: String, app_state: State<'_, AppState>) -> Result<(), UnlockError> {
 | 
				
			||||||
    app_state.unlock(&passphrase).await
 | 
					    app_state.decrypt(&passphrase).await
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[tauri::command]
 | 
					#[tauri::command]
 | 
				
			||||||
pub async fn get_session_status(app_state: State<'_, AppState>) -> Result<String, ()> {
 | 
					pub fn get_session_status(app_state: State<'_, AppState>) -> String {
 | 
				
			||||||
    let session = app_state.session.read().await;
 | 
					    let session = app_state.session.read().unwrap();
 | 
				
			||||||
    let status = match *session {
 | 
					    match *session {
 | 
				
			||||||
        Session::Locked(_) => "locked".into(),
 | 
					        Session::Locked(_) => "locked".into(),
 | 
				
			||||||
        Session::Unlocked{..} => "unlocked".into(),
 | 
					        Session::Unlocked(_) => "unlocked".into(),
 | 
				
			||||||
        Session::Empty => "empty".into()
 | 
					        Session::Empty => "empty".into()
 | 
				
			||||||
    };
 | 
					    }
 | 
				
			||||||
    Ok(status)
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[tauri::command]
 | 
					#[tauri::command]
 | 
				
			||||||
pub async fn save_credentials(
 | 
					pub async fn save_credentials(
 | 
				
			||||||
    credentials: BaseCredentials,
 | 
					    credentials: Credentials,
 | 
				
			||||||
    passphrase: String,
 | 
					    passphrase: String,
 | 
				
			||||||
    app_state: State<'_, AppState>
 | 
					    app_state: State<'_, AppState>
 | 
				
			||||||
) -> Result<(), UnlockError> {
 | 
					) -> Result<(), UnlockError> {
 | 
				
			||||||
    app_state.new_creds(credentials, &passphrase).await
 | 
					    app_state.save_creds(credentials, &passphrase).await
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[tauri::command]
 | 
					#[tauri::command]
 | 
				
			||||||
pub async fn get_config(app_state: State<'_, AppState>) -> Result<AppConfig, ()> {
 | 
					pub fn get_config(app_state: State<'_, AppState>) -> AppConfig {
 | 
				
			||||||
    let config = app_state.config.read().await;
 | 
					    let config = app_state.config.read().unwrap();
 | 
				
			||||||
    Ok(config.clone())
 | 
					    config.clone()
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[tauri::command]
 | 
					#[tauri::command]
 | 
				
			||||||
pub async fn save_config(config: AppConfig, app_state: State<'_, AppState>) -> Result<(), String>  {
 | 
					pub fn save_config(config: AppConfig, app_state: State<'_, AppState>) {
 | 
				
			||||||
    app_state.update_config(config)
 | 
					    let mut prev_config = app_state.config.write().unwrap();
 | 
				
			||||||
        .await
 | 
					    *prev_config = config;
 | 
				
			||||||
        .map_err(|e| format!("Error saving config: {e}"))?;
 | 
					 | 
				
			||||||
        Ok(())
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,11 +3,10 @@
 | 
				
			|||||||
    windows_subsystem = "windows"
 | 
					    windows_subsystem = "windows"
 | 
				
			||||||
)]
 | 
					)]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use tauri::{AppHandle, Manager, async_runtime as rt};
 | 
				
			||||||
 | 
					use once_cell::sync::OnceCell;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
mod app;
 | 
					 | 
				
			||||||
mod cli;
 | 
					 | 
				
			||||||
mod config;
 | 
					mod config;
 | 
				
			||||||
mod credentials;
 | 
					 | 
				
			||||||
mod errors;
 | 
					mod errors;
 | 
				
			||||||
mod clientinfo;
 | 
					mod clientinfo;
 | 
				
			||||||
mod ipc;
 | 
					mod ipc;
 | 
				
			||||||
@@ -15,22 +14,88 @@ mod state;
 | 
				
			|||||||
mod server;
 | 
					mod server;
 | 
				
			||||||
mod tray;
 | 
					mod tray;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::errors::*;
 | 
				
			||||||
 | 
					use state::AppState;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::errors::ErrorPopup;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub static APP: OnceCell<AppHandle> = OnceCell::new();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
fn main() {
 | 
					fn main() {
 | 
				
			||||||
    let res = match cli::parser().get_matches().subcommand() {
 | 
					    let initial_state = match rt::block_on(state::AppState::load()) {
 | 
				
			||||||
        None | Some(("run", _)) => {
 | 
					        Ok(state) => state,
 | 
				
			||||||
            app::run().error_popup("Creddy failed to start");
 | 
					        Err(e) => {eprintln!("{}", e); return;}
 | 
				
			||||||
            Ok(())
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        Some(("show", m)) => cli::show(m),
 | 
					 | 
				
			||||||
        Some(("exec", m)) => cli::exec(m),
 | 
					 | 
				
			||||||
        _ => unreachable!(),
 | 
					 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if let Err(e) = res {
 | 
					    tauri::Builder::default()
 | 
				
			||||||
        eprintln!("Error: {e}");
 | 
					        .manage(initial_state)
 | 
				
			||||||
 | 
					        .system_tray(tray::create())
 | 
				
			||||||
 | 
					        .on_system_tray_event(tray::handle_event)
 | 
				
			||||||
 | 
					        .invoke_handler(tauri::generate_handler![
 | 
				
			||||||
 | 
					            ipc::unlock,
 | 
				
			||||||
 | 
					            ipc::respond,
 | 
				
			||||||
 | 
					            ipc::get_session_status,
 | 
				
			||||||
 | 
					            ipc::save_credentials,
 | 
				
			||||||
 | 
					            ipc::get_config,
 | 
				
			||||||
 | 
					            ipc::save_config,
 | 
				
			||||||
 | 
					        ])
 | 
				
			||||||
 | 
					        .setup(|app| {
 | 
				
			||||||
 | 
					            APP.set(app.handle()).unwrap();
 | 
				
			||||||
 | 
					            let state = app.state::<AppState>();
 | 
				
			||||||
 | 
					            let config = state.config.read().unwrap();
 | 
				
			||||||
 | 
					            let addr = std::net::SocketAddrV4::new(config.listen_addr, config.listen_port);
 | 
				
			||||||
 | 
					            tauri::async_runtime::spawn(server::serve(addr, app.handle()));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if !config.start_minimized {
 | 
				
			||||||
 | 
					                app.get_window("main")
 | 
				
			||||||
 | 
					                    .ok_or(RequestError::NoMainWindow)?
 | 
				
			||||||
 | 
					                    .show()?;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            Ok(())
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .build(tauri::generate_context!())
 | 
				
			||||||
 | 
					        .expect("error while running tauri application")
 | 
				
			||||||
 | 
					        .run(|app, run_event| match run_event {
 | 
				
			||||||
 | 
					            tauri::RunEvent::WindowEvent { label, event, .. } => match event {
 | 
				
			||||||
 | 
					                tauri::WindowEvent::CloseRequested { api, .. } => {
 | 
				
			||||||
 | 
					                    let _ = app.get_window(&label).map(|w| w.hide());
 | 
				
			||||||
 | 
					                    api.prevent_close();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                _ => ()
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            _ => ()
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					macro_rules! get_state {
 | 
				
			||||||
 | 
					    ($prop:ident as $name:ident) => {
 | 
				
			||||||
 | 
					        use tauri::Manager;
 | 
				
			||||||
 | 
					        let app = crate::APP.get().unwrap(); // as long as the app is running, this is fine
 | 
				
			||||||
 | 
					        let state = app.state::<crate::state::AppState>();
 | 
				
			||||||
 | 
					        let $name = state.$prop.read().unwrap(); // only panics if another thread has already panicked
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    (config.$prop:ident as $name:ident) => {
 | 
				
			||||||
 | 
					        use tauri::Manager;
 | 
				
			||||||
 | 
					        let app = crate::APP.get().unwrap();
 | 
				
			||||||
 | 
					        let state = app.state::<crate::state::AppState>();
 | 
				
			||||||
 | 
					        let config = state.config.read().unwrap();
 | 
				
			||||||
 | 
					        let $name = config.$prop;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    (mut $prop:ident as $name:ident) => {
 | 
				
			||||||
 | 
					        use tauri::Manager;
 | 
				
			||||||
 | 
					        let app = crate::APP.get().unwrap();
 | 
				
			||||||
 | 
					        let state = app.state::<crate::state::AppState>();
 | 
				
			||||||
 | 
					        let $name = state.$prop.write().unwrap();
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    (mut config.$prop:ident as $name:ident) => {
 | 
				
			||||||
 | 
					        use tauri::Manager;
 | 
				
			||||||
 | 
					        let app = crate::APP.get().unwrap();
 | 
				
			||||||
 | 
					        let state = app.state::<crate::state::AppState>();
 | 
				
			||||||
 | 
					        let config = state.config.write().unwrap();
 | 
				
			||||||
 | 
					        let $name = config.$prop;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub(crate) use get_state;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,21 +1,12 @@
 | 
				
			|||||||
use core::time::Duration;
 | 
					use core::time::Duration;
 | 
				
			||||||
use std::io;
 | 
					use std::io;
 | 
				
			||||||
use std::net::{
 | 
					use std::net::{SocketAddr, SocketAddrV4};
 | 
				
			||||||
    Ipv4Addr,
 | 
					use tokio::net::{TcpListener, TcpStream};
 | 
				
			||||||
    SocketAddr,
 | 
					 | 
				
			||||||
    SocketAddrV4,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
use tokio::net::{
 | 
					 | 
				
			||||||
    TcpListener, 
 | 
					 | 
				
			||||||
    TcpStream,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
 | 
					use tokio::io::{AsyncReadExt, AsyncWriteExt};
 | 
				
			||||||
use tokio::sync::oneshot;
 | 
					use tokio::sync::oneshot;
 | 
				
			||||||
use tokio::time::sleep;
 | 
					use tokio::time::sleep;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use tauri::{AppHandle, Manager};
 | 
					use tauri::{AppHandle, Manager};
 | 
				
			||||||
use tauri::async_runtime as rt;
 | 
					 | 
				
			||||||
use tauri::async_runtime::JoinHandle;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{clientinfo, clientinfo::Client};
 | 
					use crate::{clientinfo, clientinfo::Client};
 | 
				
			||||||
use crate::errors::*;
 | 
					use crate::errors::*;
 | 
				
			||||||
@@ -31,10 +22,10 @@ struct Handler {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl Handler {
 | 
					impl Handler {
 | 
				
			||||||
    async fn new(stream: TcpStream, app: AppHandle) -> Self {
 | 
					    fn new(stream: TcpStream, app: AppHandle) -> Self {
 | 
				
			||||||
        let state = app.state::<AppState>();
 | 
					        let state = app.state::<AppState>();
 | 
				
			||||||
        let (chan_send, chan_recv) = oneshot::channel();
 | 
					        let (chan_send, chan_recv) = oneshot::channel();
 | 
				
			||||||
        let request_id = state.register_request(chan_send).await;
 | 
					        let request_id = state.register_request(chan_send);
 | 
				
			||||||
        Handler { 
 | 
					        Handler { 
 | 
				
			||||||
            request_id,
 | 
					            request_id,
 | 
				
			||||||
            stream,
 | 
					            stream,
 | 
				
			||||||
@@ -48,40 +39,28 @@ impl Handler {
 | 
				
			|||||||
            eprintln!("{e}");
 | 
					            eprintln!("{e}");
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        let state = self.app.state::<AppState>();
 | 
					        let state = self.app.state::<AppState>();
 | 
				
			||||||
        state.unregister_request(self.request_id).await;
 | 
					        state.unregister_request(self.request_id);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async fn try_handle(&mut self) -> Result<(), HandlerError> {
 | 
					    async fn try_handle(&mut self) -> Result<(), RequestError> {
 | 
				
			||||||
        let req_path = self.recv_request().await?;
 | 
					        let _ = self.recv_request().await?;
 | 
				
			||||||
        let clients = self.get_clients().await?;
 | 
					        let clients = self.get_clients()?;
 | 
				
			||||||
        if self.includes_banned(&clients).await {
 | 
					        if self.includes_banned(&clients) {
 | 
				
			||||||
            self.stream.write(b"HTTP/1.0 403 Access Denied\r\n\r\n").await?;
 | 
					            self.stream.write(b"HTTP/1.0 403 Access Denied\r\n\r\n").await?;
 | 
				
			||||||
            return Ok(())
 | 
					            return Ok(())
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        let base = req_path == b"/creddy/base-credentials";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let req = Request {id: self.request_id, clients, base};
 | 
					        let req = Request {id: self.request_id, clients};
 | 
				
			||||||
        self.app.emit_all("credentials-request", &req)?;
 | 
					        self.app.emit_all("credentials-request", &req)?;
 | 
				
			||||||
        let starting_visibility = self.show_window()?;
 | 
					        let starting_visibility = self.show_window()?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        match self.wait_for_response().await? {
 | 
					        match self.wait_for_response().await? {
 | 
				
			||||||
            Approval::Approved => {
 | 
					            Approval::Approved => self.send_credentials().await?,
 | 
				
			||||||
                let state = self.app.state::<AppState>();
 | 
					 | 
				
			||||||
                let creds = if base {
 | 
					 | 
				
			||||||
                    state.serialize_base_creds().await?
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                else {
 | 
					 | 
				
			||||||
                    state.serialize_session_creds().await?
 | 
					 | 
				
			||||||
                };
 | 
					 | 
				
			||||||
                self.send_body(creds.as_bytes()).await?;
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            Approval::Denied => {
 | 
					            Approval::Denied => {
 | 
				
			||||||
                let state = self.app.state::<AppState>();
 | 
					                let state = self.app.state::<AppState>();
 | 
				
			||||||
                for client in req.clients {
 | 
					                for client in req.clients {
 | 
				
			||||||
                    state.add_ban(client).await;
 | 
					                    state.add_ban(client, self.app.clone());
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                self.send_body(b"Denied!").await?;
 | 
					 | 
				
			||||||
                self.stream.shutdown().await?;
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -89,62 +68,51 @@ impl Handler {
 | 
				
			|||||||
        // and b) there are no other pending requests
 | 
					        // and b) there are no other pending requests
 | 
				
			||||||
        let state = self.app.state::<AppState>();
 | 
					        let state = self.app.state::<AppState>();
 | 
				
			||||||
        let delay = {
 | 
					        let delay = {
 | 
				
			||||||
            let config = state.config.read().await;
 | 
					            let config = state.config.read().unwrap();
 | 
				
			||||||
            Duration::from_millis(config.rehide_ms)
 | 
					            Duration::from_millis(config.rehide_ms)
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
        sleep(delay).await;
 | 
					        sleep(delay).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if !starting_visibility && state.req_count().await == 0 {
 | 
					        if !starting_visibility && state.req_count() == 0 {
 | 
				
			||||||
            let window = self.app.get_window("main").ok_or(HandlerError::NoMainWindow)?;
 | 
					            let window = self.app.get_window("main").ok_or(RequestError::NoMainWindow)?;
 | 
				
			||||||
            window.hide()?;
 | 
					            window.hide()?;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Ok(())
 | 
					        Ok(())
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async fn recv_request(&mut self) -> Result<Vec<u8>, HandlerError> {
 | 
					    async fn recv_request(&mut self) -> Result<Vec<u8>, RequestError> {
 | 
				
			||||||
        let mut buf = vec![0; 8192]; // it's what tokio's BufReader uses
 | 
					        let mut buf = vec![0; 8192]; // it's what tokio's BufReader uses
 | 
				
			||||||
        let mut n = 0;
 | 
					        let mut n = 0;
 | 
				
			||||||
        loop {
 | 
					        loop {
 | 
				
			||||||
            n += self.stream.read(&mut buf[n..]).await?;
 | 
					            n += self.stream.read(&mut buf[n..]).await?;
 | 
				
			||||||
            if n >= 4 && &buf[(n - 4)..n] == b"\r\n\r\n" {break;}
 | 
					            if n >= 4 && &buf[(n - 4)..n] == b"\r\n\r\n" {break;}
 | 
				
			||||||
            if n == buf.len() {return Err(HandlerError::RequestTooLarge);}
 | 
					            if n == buf.len() {return Err(RequestError::RequestTooLarge);}
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let path = buf.split(|&c| &[c] == b" ")
 | 
					        if cfg!(debug_assertions) {
 | 
				
			||||||
            .skip(1)
 | 
					 | 
				
			||||||
            .next()
 | 
					 | 
				
			||||||
            .ok_or(HandlerError::BadRequest(buf.clone()))?;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        #[cfg(debug_assertions)] {
 | 
					 | 
				
			||||||
            println!("Path: {}", std::str::from_utf8(&path).unwrap());
 | 
					 | 
				
			||||||
            println!("{}", std::str::from_utf8(&buf).unwrap());
 | 
					            println!("{}", std::str::from_utf8(&buf).unwrap());
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Ok(path.into())
 | 
					        Ok(buf)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async fn get_clients(&self) -> Result<Vec<Option<Client>>, HandlerError> {
 | 
					    fn get_clients(&self) -> Result<Vec<Option<Client>>, RequestError> {
 | 
				
			||||||
        let peer_addr = match self.stream.peer_addr()? {
 | 
					        let peer_addr = match self.stream.peer_addr()? {
 | 
				
			||||||
            SocketAddr::V4(addr) => addr,
 | 
					            SocketAddr::V4(addr) => addr,
 | 
				
			||||||
            _ => unreachable!(), // we only listen on IPv4
 | 
					            _ => unreachable!(), // we only listen on IPv4
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
        let clients = clientinfo::get_clients(peer_addr.port()).await?;
 | 
					        let clients = clientinfo::get_clients(peer_addr.port())?;
 | 
				
			||||||
        Ok(clients)
 | 
					        Ok(clients)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async fn includes_banned(&self, clients: &Vec<Option<Client>>) -> bool {
 | 
					    fn includes_banned(&self, clients: &Vec<Option<Client>>) -> bool {
 | 
				
			||||||
        let state = self.app.state::<AppState>();
 | 
					        let state = self.app.state::<AppState>();
 | 
				
			||||||
        for client in clients {
 | 
					        clients.iter().any(|c| state.is_banned(c))
 | 
				
			||||||
            if state.is_banned(client).await {
 | 
					 | 
				
			||||||
                return true;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        false
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    fn show_window(&self) -> Result<bool, HandlerError> {
 | 
					    fn show_window(&self) -> Result<bool, RequestError> {
 | 
				
			||||||
        let window = self.app.get_window("main").ok_or(HandlerError::NoMainWindow)?;
 | 
					        let window = self.app.get_window("main").ok_or(RequestError::NoMainWindow)?;
 | 
				
			||||||
        let starting_visibility = window.is_visible()?;
 | 
					        let starting_visibility = window.is_visible()?;
 | 
				
			||||||
        if !starting_visibility {
 | 
					        if !starting_visibility {
 | 
				
			||||||
            window.unminimize()?;
 | 
					            window.unminimize()?;
 | 
				
			||||||
@@ -154,7 +122,7 @@ impl Handler {
 | 
				
			|||||||
        Ok(starting_visibility)
 | 
					        Ok(starting_visibility)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async fn wait_for_response(&mut self) -> Result<Approval, HandlerError> {
 | 
					    async fn wait_for_response(&mut self) -> Result<Approval, RequestError> {
 | 
				
			||||||
        self.stream.write(b"HTTP/1.0 200 OK\r\n").await?;
 | 
					        self.stream.write(b"HTTP/1.0 200 OK\r\n").await?;
 | 
				
			||||||
        self.stream.write(b"Content-Type: application/json\r\n").await?;
 | 
					        self.stream.write(b"Content-Type: application/json\r\n").await?;
 | 
				
			||||||
        self.stream.write(b"X-Creddy-delaying-tactic: ").await?;
 | 
					        self.stream.write(b"X-Creddy-delaying-tactic: ").await?;
 | 
				
			||||||
@@ -177,66 +145,31 @@ impl Handler {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async fn send_body(&mut self, body: &[u8]) -> Result<(), HandlerError> {
 | 
					    async fn send_credentials(&mut self) -> Result<(), RequestError> {
 | 
				
			||||||
 | 
					        let state = self.app.state::<AppState>();
 | 
				
			||||||
 | 
					        let creds = state.get_creds_serialized()?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.stream.write(b"\r\nContent-Length: ").await?;
 | 
					        self.stream.write(b"\r\nContent-Length: ").await?;
 | 
				
			||||||
        self.stream.write(body.len().to_string().as_bytes()).await?;
 | 
					        self.stream.write(creds.as_bytes().len().to_string().as_bytes()).await?;
 | 
				
			||||||
 | 
					        self.stream.write(b"\r\n\r\n").await?;
 | 
				
			||||||
 | 
					        self.stream.write(creds.as_bytes()).await?;
 | 
				
			||||||
        self.stream.write(b"\r\n\r\n").await?;
 | 
					        self.stream.write(b"\r\n\r\n").await?;
 | 
				
			||||||
        self.stream.write(body).await?;
 | 
					 | 
				
			||||||
        self.stream.shutdown().await?;
 | 
					 | 
				
			||||||
        Ok(())
 | 
					        Ok(())
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Debug)]
 | 
					pub async fn serve(addr: SocketAddrV4, app_handle: AppHandle) -> io::Result<()> {
 | 
				
			||||||
pub struct Server {
 | 
					    let listener = TcpListener::bind(&addr).await?;
 | 
				
			||||||
    addr: Ipv4Addr,
 | 
					    println!("Listening on {addr}");
 | 
				
			||||||
    port: u16,
 | 
					    loop {
 | 
				
			||||||
    app_handle: AppHandle,
 | 
					        match listener.accept().await {
 | 
				
			||||||
    task: JoinHandle<()>,
 | 
					            Ok((stream, _)) => {
 | 
				
			||||||
}
 | 
					                let handler = Handler::new(stream, app_handle.app_handle());
 | 
				
			||||||
 | 
					                tauri::async_runtime::spawn(handler.handle());
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
impl Server {
 | 
					            Err(e) => {
 | 
				
			||||||
    pub async fn new(addr: Ipv4Addr, port: u16, app_handle: AppHandle) -> io::Result<Server> {
 | 
					                eprintln!("Error accepting connection: {e}");
 | 
				
			||||||
        let task = Self::start_server(addr, port, app_handle.app_handle()).await?;
 | 
					 | 
				
			||||||
        Ok(Server { addr, port, app_handle, task})
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub async fn rebind(&mut self, addr: Ipv4Addr, port: u16) -> io::Result<()> {
 | 
					 | 
				
			||||||
        if addr == self.addr && port == self.port {
 | 
					 | 
				
			||||||
            return Ok(())
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let new_task = Self::start_server(addr, port, self.app_handle.app_handle()).await?;
 | 
					 | 
				
			||||||
        self.task.abort();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self.addr = addr;
 | 
					 | 
				
			||||||
        self.port = port;
 | 
					 | 
				
			||||||
        self.task = new_task;
 | 
					 | 
				
			||||||
        Ok(())
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // construct the listener before spawning the task so that we can return early if it fails
 | 
					 | 
				
			||||||
    async fn start_server(addr: Ipv4Addr, port: u16, app_handle: AppHandle) -> io::Result<JoinHandle<()>> {
 | 
					 | 
				
			||||||
        let sock_addr = SocketAddrV4::new(addr, port);
 | 
					 | 
				
			||||||
        let listener = TcpListener::bind(&sock_addr).await?;
 | 
					 | 
				
			||||||
        let task = rt::spawn(
 | 
					 | 
				
			||||||
            Self::serve(listener, app_handle.app_handle())
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        Ok(task)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async fn serve(listener: TcpListener, app_handle: AppHandle) {
 | 
					 | 
				
			||||||
        loop {
 | 
					 | 
				
			||||||
            match listener.accept().await {
 | 
					 | 
				
			||||||
                Ok((stream, _)) => {
 | 
					 | 
				
			||||||
                    let handler = Handler::new(stream, app_handle.app_handle()).await;
 | 
					 | 
				
			||||||
                    rt::spawn(handler.handle());
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
                Err(e) => {
 | 
					 | 
				
			||||||
                    eprintln!("Error accepting connection: {e}");
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,26 +1,59 @@
 | 
				
			|||||||
 | 
					use core::time::Duration;
 | 
				
			||||||
use std::collections::{HashMap, HashSet};
 | 
					use std::collections::{HashMap, HashSet};
 | 
				
			||||||
use std::time::Duration;
 | 
					use std::sync::RwLock;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use tokio::{
 | 
					use serde::{Serialize, Deserialize};
 | 
				
			||||||
    sync::oneshot::Sender,
 | 
					use tokio::sync::oneshot::Sender;
 | 
				
			||||||
    sync::RwLock,
 | 
					use tokio::time::sleep;
 | 
				
			||||||
    time::sleep,
 | 
					use sqlx::{SqlitePool, sqlite::SqlitePoolOptions, sqlite::SqliteConnectOptions};
 | 
				
			||||||
 | 
					use sodiumoxide::crypto::{
 | 
				
			||||||
 | 
					        pwhash,
 | 
				
			||||||
 | 
					        pwhash::Salt, 
 | 
				
			||||||
 | 
					        secretbox, 
 | 
				
			||||||
 | 
					        secretbox::{Nonce, Key}
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
use sqlx::SqlitePool;
 | 
					 | 
				
			||||||
use tauri::async_runtime as runtime;
 | 
					use tauri::async_runtime as runtime;
 | 
				
			||||||
use tauri::Manager;
 | 
					use tauri::Manager;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::app::APP;
 | 
					 | 
				
			||||||
use crate::credentials::{
 | 
					 | 
				
			||||||
    Session,
 | 
					 | 
				
			||||||
    BaseCredentials,
 | 
					 | 
				
			||||||
    SessionCredentials,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
use crate::{config, config::AppConfig};
 | 
					use crate::{config, config::AppConfig};
 | 
				
			||||||
use crate::ipc::{self, Approval};
 | 
					use crate::ipc;
 | 
				
			||||||
use crate::clientinfo::Client;
 | 
					use crate::clientinfo::Client;
 | 
				
			||||||
use crate::errors::*;
 | 
					use crate::errors::*;
 | 
				
			||||||
use crate::server::Server;
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, Serialize, Deserialize)]
 | 
				
			||||||
 | 
					#[serde(untagged)]
 | 
				
			||||||
 | 
					pub enum Credentials {
 | 
				
			||||||
 | 
					    #[serde(rename_all = "PascalCase")]
 | 
				
			||||||
 | 
					    LongLived {
 | 
				
			||||||
 | 
					        access_key_id: String,
 | 
				
			||||||
 | 
					        secret_access_key: String,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    #[serde(rename_all = "PascalCase")]
 | 
				
			||||||
 | 
					    ShortLived {
 | 
				
			||||||
 | 
					        access_key_id: String,
 | 
				
			||||||
 | 
					        secret_access_key: String,
 | 
				
			||||||
 | 
					        token: String,
 | 
				
			||||||
 | 
					        expiration: String,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug)]
 | 
				
			||||||
 | 
					pub struct LockedCredentials {
 | 
				
			||||||
 | 
					    access_key_id: String,
 | 
				
			||||||
 | 
					    secret_key_enc: Vec<u8>,
 | 
				
			||||||
 | 
					    salt: Salt,
 | 
				
			||||||
 | 
					    nonce: Nonce,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug)]
 | 
				
			||||||
 | 
					pub enum Session {
 | 
				
			||||||
 | 
					    Unlocked(Credentials),
 | 
				
			||||||
 | 
					    Locked(LockedCredentials),
 | 
				
			||||||
 | 
					    Empty,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Debug)]
 | 
					#[derive(Debug)]
 | 
				
			||||||
@@ -30,79 +63,117 @@ pub struct AppState {
 | 
				
			|||||||
    pub request_count: RwLock<u64>,
 | 
					    pub request_count: RwLock<u64>,
 | 
				
			||||||
    pub open_requests: RwLock<HashMap<u64, Sender<ipc::Approval>>>,
 | 
					    pub open_requests: RwLock<HashMap<u64, Sender<ipc::Approval>>>,
 | 
				
			||||||
    pub bans: RwLock<std::collections::HashSet<Option<Client>>>,
 | 
					    pub bans: RwLock<std::collections::HashSet<Option<Client>>>,
 | 
				
			||||||
    server: RwLock<Server>,
 | 
					    pool: SqlitePool,
 | 
				
			||||||
    pool: sqlx::SqlitePool,
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl AppState {
 | 
					impl AppState {
 | 
				
			||||||
    pub fn new(config: AppConfig, session: Session, server: Server, pool: SqlitePool) -> AppState {
 | 
					    pub async fn load() -> Result<Self, SetupError> {
 | 
				
			||||||
        AppState {
 | 
					        let conn_opts = SqliteConnectOptions::new()
 | 
				
			||||||
            config: RwLock::new(config),
 | 
					            .filename(config::get_or_create_db_path())
 | 
				
			||||||
            session: RwLock::new(session),
 | 
					            .create_if_missing(true);
 | 
				
			||||||
 | 
					        let pool_opts = SqlitePoolOptions::new();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let pool: SqlitePool = pool_opts.connect_with(conn_opts).await?;
 | 
				
			||||||
 | 
					        sqlx::migrate!().run(&pool).await?;
 | 
				
			||||||
 | 
					        let creds = Self::load_creds(&pool).await?;
 | 
				
			||||||
 | 
					        let conf = config::load(&pool).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let state = AppState {
 | 
				
			||||||
 | 
					            config: RwLock::new(conf),
 | 
				
			||||||
 | 
					            session: RwLock::new(creds),
 | 
				
			||||||
            request_count: RwLock::new(0),
 | 
					            request_count: RwLock::new(0),
 | 
				
			||||||
            open_requests: RwLock::new(HashMap::new()),
 | 
					            open_requests: RwLock::new(HashMap::new()),
 | 
				
			||||||
            bans: RwLock::new(HashSet::new()),
 | 
					            bans: RwLock::new(HashSet::new()),
 | 
				
			||||||
            server: RwLock::new(server),
 | 
					 | 
				
			||||||
            pool,
 | 
					            pool,
 | 
				
			||||||
        }
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(state)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub async fn new_creds(&self, base_creds: BaseCredentials, passphrase: &str) -> Result<(), UnlockError> {
 | 
					    async fn load_creds(pool: &SqlitePool) -> Result<Session, SetupError> {
 | 
				
			||||||
        let locked = base_creds.encrypt(passphrase)?;
 | 
					        let res = sqlx::query!("SELECT * FROM credentials ORDER BY created_at desc")
 | 
				
			||||||
 | 
					            .fetch_optional(pool)
 | 
				
			||||||
 | 
					            .await?;
 | 
				
			||||||
 | 
					        let row = match res {
 | 
				
			||||||
 | 
					            Some(r) => r,
 | 
				
			||||||
 | 
					            None => {return Ok(Session::Empty);}
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let salt_buf: [u8; 32] = row.salt
 | 
				
			||||||
 | 
					            .try_into()
 | 
				
			||||||
 | 
					            .map_err(|_e| SetupError::InvalidRecord)?;
 | 
				
			||||||
 | 
					        let nonce_buf: [u8; 24] = row.nonce
 | 
				
			||||||
 | 
					            .try_into()
 | 
				
			||||||
 | 
					            .map_err(|_e| SetupError::InvalidRecord)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let creds = LockedCredentials {
 | 
				
			||||||
 | 
					            access_key_id: row.access_key_id,
 | 
				
			||||||
 | 
					            secret_key_enc: row.secret_key_enc,
 | 
				
			||||||
 | 
					            salt: Salt(salt_buf),
 | 
				
			||||||
 | 
					            nonce: Nonce(nonce_buf),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        Ok(Session::Locked(creds))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub async fn save_creds(&self, creds: Credentials, passphrase: &str) -> Result<(), UnlockError> {
 | 
				
			||||||
 | 
					        let (key_id, secret_key) = match creds {
 | 
				
			||||||
 | 
					            Credentials::LongLived {access_key_id, secret_access_key} => {
 | 
				
			||||||
 | 
					                (access_key_id, secret_access_key)
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            _ => unreachable!(),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // do this first so that if it fails we don't save bad credentials
 | 
					        // do this first so that if it fails we don't save bad credentials
 | 
				
			||||||
        self.new_session(base_creds).await?;
 | 
					        self.new_session(&key_id, &secret_key).await?;
 | 
				
			||||||
        locked.save(&self.pool).await?;
 | 
					
 | 
				
			||||||
 | 
					        let salt = pwhash::gen_salt();
 | 
				
			||||||
 | 
					        let mut key_buf = [0; secretbox::KEYBYTES];
 | 
				
			||||||
 | 
					        pwhash::derive_key_interactive(&mut key_buf, passphrase.as_bytes(), &salt).unwrap();
 | 
				
			||||||
 | 
					        let key = Key(key_buf);
 | 
				
			||||||
 | 
					        // not sure we need both salt AND nonce given that we generate a
 | 
				
			||||||
 | 
					        // fresh salt every time we encrypt, but better safe than sorry
 | 
				
			||||||
 | 
					        let nonce = secretbox::gen_nonce();
 | 
				
			||||||
 | 
					        let secret_key_enc = secretbox::seal(secret_key.as_bytes(), &nonce, &key);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        sqlx::query(
 | 
				
			||||||
 | 
					            "INSERT INTO credentials (access_key_id, secret_key_enc, salt, nonce, created_at)
 | 
				
			||||||
 | 
					            VALUES (?, ?, ?, ?, strftime('%s'))"
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					            .bind(&key_id)
 | 
				
			||||||
 | 
					            .bind(&secret_key_enc)
 | 
				
			||||||
 | 
					            .bind(&salt.0[0..])
 | 
				
			||||||
 | 
					            .bind(&nonce.0[0..])
 | 
				
			||||||
 | 
					            .execute(&self.pool)
 | 
				
			||||||
 | 
					            .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Ok(())
 | 
					        Ok(())
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub async fn update_config(&self, new_config: AppConfig) -> Result<(), SetupError> {
 | 
					    pub fn register_request(&self, chan: Sender<ipc::Approval>) -> u64 {
 | 
				
			||||||
        let mut live_config = self.config.write().await;
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        if new_config.start_on_login != live_config.start_on_login {
 | 
					 | 
				
			||||||
            config::set_auto_launch(new_config.start_on_login)?;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if new_config.listen_addr != live_config.listen_addr 
 | 
					 | 
				
			||||||
            || new_config.listen_port != live_config.listen_port 
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            let mut sv = self.server.write().await;
 | 
					 | 
				
			||||||
            sv.rebind(new_config.listen_addr, new_config.listen_port).await?;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        new_config.save(&self.pool).await?;
 | 
					 | 
				
			||||||
        *live_config = new_config;
 | 
					 | 
				
			||||||
        Ok(())
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub async fn register_request(&self, chan: Sender<ipc::Approval>) -> u64 {
 | 
					 | 
				
			||||||
        let count = {
 | 
					        let count = {
 | 
				
			||||||
            let mut c = self.request_count.write().await;
 | 
					            let mut c = self.request_count.write().unwrap();
 | 
				
			||||||
            *c += 1;
 | 
					            *c += 1;
 | 
				
			||||||
            c
 | 
					            c
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let mut open_requests = self.open_requests.write().await;
 | 
					        let mut open_requests = self.open_requests.write().unwrap();
 | 
				
			||||||
        open_requests.insert(*count, chan); // `count` is the request id
 | 
					        open_requests.insert(*count, chan); // `count` is the request id
 | 
				
			||||||
        *count
 | 
					        *count
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub async fn unregister_request(&self, id: u64) {
 | 
					    pub fn unregister_request(&self, id: u64) {
 | 
				
			||||||
        let mut open_requests = self.open_requests.write().await;
 | 
					        let mut open_requests = self.open_requests.write().unwrap();
 | 
				
			||||||
        open_requests.remove(&id);
 | 
					        open_requests.remove(&id);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub async fn req_count(&self) -> usize {
 | 
					    pub fn req_count(&self) -> usize {
 | 
				
			||||||
        let open_requests = self.open_requests.read().await;
 | 
					        let open_requests = self.open_requests.read().unwrap();
 | 
				
			||||||
        open_requests.len()
 | 
					        open_requests.len()
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub async fn send_response(&self, response: ipc::RequestResponse) -> Result<(), SendResponseError> {
 | 
					    pub fn send_response(&self, response: ipc::RequestResponse) -> Result<(), SendResponseError> {
 | 
				
			||||||
        if let Approval::Approved = response.approval {
 | 
					        let mut open_requests = self.open_requests.write().unwrap();
 | 
				
			||||||
            let mut session = self.session.write().await;
 | 
					 | 
				
			||||||
            session.renew_if_expired().await?;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let mut open_requests = self.open_requests.write().await;
 | 
					 | 
				
			||||||
        let chan = open_requests
 | 
					        let chan = open_requests
 | 
				
			||||||
            .remove(&response.id)
 | 
					            .remove(&response.id)
 | 
				
			||||||
            .ok_or(SendResponseError::NotFound)
 | 
					            .ok_or(SendResponseError::NotFound)
 | 
				
			||||||
@@ -112,57 +183,105 @@ impl AppState {
 | 
				
			|||||||
            .map_err(|_e| SendResponseError::Abandoned)
 | 
					            .map_err(|_e| SendResponseError::Abandoned)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub async fn add_ban(&self, client: Option<Client>) {
 | 
					    pub fn add_ban(&self, client: Option<Client>, app: tauri::AppHandle) {
 | 
				
			||||||
        let mut bans = self.bans.write().await;
 | 
					        let mut bans = self.bans.write().unwrap();
 | 
				
			||||||
        bans.insert(client.clone());
 | 
					        bans.insert(client.clone());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        runtime::spawn(async move {
 | 
					        runtime::spawn(async move {
 | 
				
			||||||
            sleep(Duration::from_secs(5)).await;
 | 
					            sleep(Duration::from_secs(5)).await;
 | 
				
			||||||
            let app = APP.get().unwrap();
 | 
					 | 
				
			||||||
            let state = app.state::<AppState>();
 | 
					            let state = app.state::<AppState>();
 | 
				
			||||||
            let mut bans = state.bans.write().await;
 | 
					            let mut bans = state.bans.write().unwrap();
 | 
				
			||||||
            bans.remove(&client);
 | 
					            bans.remove(&client);
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub async fn is_banned(&self, client: &Option<Client>) -> bool {
 | 
					    pub fn is_banned(&self, client: &Option<Client>) -> bool {
 | 
				
			||||||
        self.bans.read().await.contains(&client)
 | 
					        self.bans.read().unwrap().contains(&client)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub async fn unlock(&self, passphrase: &str) -> Result<(), UnlockError> {
 | 
					    pub async fn decrypt(&self, passphrase: &str) -> Result<(), UnlockError> {
 | 
				
			||||||
        let base_creds = match *self.session.read().await {
 | 
					        let (key_id, secret) = {
 | 
				
			||||||
            Session::Empty => {return Err(UnlockError::NoCredentials);},
 | 
					            // do this all in a block so that we aren't holding a lock across an await
 | 
				
			||||||
            Session::Unlocked{..} => {return Err(UnlockError::NotLocked);},
 | 
					            let session = self.session.read().unwrap();
 | 
				
			||||||
            Session::Locked(ref locked) => locked.decrypt(passphrase)?,
 | 
					            let locked = match *session {
 | 
				
			||||||
 | 
					                Session::Empty => {return Err(UnlockError::NoCredentials);},
 | 
				
			||||||
 | 
					                Session::Unlocked(_) => {return Err(UnlockError::NotLocked);},
 | 
				
			||||||
 | 
					                Session::Locked(ref c) => c,
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            let mut key_buf = [0; secretbox::KEYBYTES];
 | 
				
			||||||
 | 
					            // pretty sure this only fails if we're out of memory
 | 
				
			||||||
 | 
					            pwhash::derive_key_interactive(&mut key_buf, passphrase.as_bytes(), &locked.salt).unwrap();
 | 
				
			||||||
 | 
					            let decrypted = secretbox::open(&locked.secret_key_enc, &locked.nonce, &Key(key_buf))
 | 
				
			||||||
 | 
					                .map_err(|_e| UnlockError::BadPassphrase)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            let secret_str = String::from_utf8(decrypted).map_err(|_e| UnlockError::InvalidUtf8)?;
 | 
				
			||||||
 | 
					            (locked.access_key_id.clone(), secret_str)
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
        // Read lock is dropped here, so this doesn't deadlock
 | 
					
 | 
				
			||||||
        self.new_session(base_creds).await?;
 | 
					        self.new_session(&key_id, &secret).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Ok(())
 | 
					        Ok(())
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub async fn serialize_base_creds(&self) -> Result<String, GetCredentialsError> {
 | 
					    pub fn get_creds_serialized(&self) -> Result<String, GetCredentialsError> {
 | 
				
			||||||
        let session = self.session.read().await;
 | 
					        let session = self.session.read().unwrap();
 | 
				
			||||||
        match *session {
 | 
					        match *session {
 | 
				
			||||||
            Session::Unlocked{ref base, ..} => Ok(serde_json::to_string(base).unwrap()),
 | 
					            Session::Unlocked(ref creds) => Ok(serde_json::to_string(creds).unwrap()),
 | 
				
			||||||
            Session::Locked(_) => Err(GetCredentialsError::Locked),
 | 
					            Session::Locked(_) => Err(GetCredentialsError::Locked),
 | 
				
			||||||
            Session::Empty => Err(GetCredentialsError::Empty),
 | 
					            Session::Empty => Err(GetCredentialsError::Empty),
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub async fn serialize_session_creds(&self) -> Result<String, GetCredentialsError> {
 | 
					    async fn new_session(&self, key_id: &str, secret_key: &str) -> Result<(), GetSessionError> {
 | 
				
			||||||
        let session = self.session.read().await;
 | 
					        let creds = aws_sdk_sts::Credentials::new(
 | 
				
			||||||
        match *session {
 | 
					            key_id,
 | 
				
			||||||
            Session::Unlocked{ref session, ..} => Ok(serde_json::to_string(session).unwrap()),
 | 
					            secret_key,
 | 
				
			||||||
            Session::Locked(_) => Err(GetCredentialsError::Locked),
 | 
					            None, // token
 | 
				
			||||||
            Session::Empty => Err(GetCredentialsError::Empty),
 | 
					            None, // expiration
 | 
				
			||||||
        }
 | 
					            "creddy", // "provider name" apparently
 | 
				
			||||||
    }
 | 
					        );
 | 
				
			||||||
 | 
					        let config = aws_config::from_env()
 | 
				
			||||||
 | 
					            .credentials_provider(creds)
 | 
				
			||||||
 | 
					            .load()
 | 
				
			||||||
 | 
					            .await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let client = aws_sdk_sts::Client::new(&config);
 | 
				
			||||||
 | 
					        let resp = client.get_session_token()
 | 
				
			||||||
 | 
					            .duration_seconds(43_200)
 | 
				
			||||||
 | 
					            .send()
 | 
				
			||||||
 | 
					            .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let aws_session = resp.credentials().ok_or(GetSessionError::NoCredentials)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let access_key_id = aws_session.access_key_id()
 | 
				
			||||||
 | 
					            .ok_or(GetSessionError::NoCredentials)?
 | 
				
			||||||
 | 
					            .to_string();
 | 
				
			||||||
 | 
					        let secret_access_key = aws_session.secret_access_key()
 | 
				
			||||||
 | 
					            .ok_or(GetSessionError::NoCredentials)?
 | 
				
			||||||
 | 
					            .to_string();
 | 
				
			||||||
 | 
					        let token = aws_session.session_token()
 | 
				
			||||||
 | 
					            .ok_or(GetSessionError::NoCredentials)?
 | 
				
			||||||
 | 
					            .to_string();
 | 
				
			||||||
 | 
					        let expiration = aws_session.expiration()
 | 
				
			||||||
 | 
					            .ok_or(GetSessionError::NoCredentials)?
 | 
				
			||||||
 | 
					            .fmt(aws_smithy_types::date_time::Format::DateTime)
 | 
				
			||||||
 | 
					            .unwrap(); // only fails if the d/t is out of range, which it can't be for this format
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let mut app_session = self.session.write().unwrap();
 | 
				
			||||||
 | 
					        let session_creds = Credentials::ShortLived {
 | 
				
			||||||
 | 
					                access_key_id,
 | 
				
			||||||
 | 
					                secret_access_key,
 | 
				
			||||||
 | 
					                token,
 | 
				
			||||||
 | 
					                expiration,
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if cfg!(debug_assertions) {
 | 
				
			||||||
 | 
					            println!("Got new session:\n{}", serde_json::to_string(&session_creds).unwrap());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        *app_session = Session::Unlocked(session_creds);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async fn new_session(&self, base: BaseCredentials) -> Result<(), GetSessionError> {
 | 
					 | 
				
			||||||
        let session = SessionCredentials::from_base(&base).await?;
 | 
					 | 
				
			||||||
        let mut app_session = self.session.write().await;
 | 
					 | 
				
			||||||
        *app_session = Session::Unlocked {base, session};
 | 
					 | 
				
			||||||
        Ok(())
 | 
					        Ok(())
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,11 +8,11 @@
 | 
				
			|||||||
  },
 | 
					  },
 | 
				
			||||||
  "package": {
 | 
					  "package": {
 | 
				
			||||||
    "productName": "creddy",
 | 
					    "productName": "creddy",
 | 
				
			||||||
    "version": "0.2.0"
 | 
					    "version": "0.1.0"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "tauri": {
 | 
					  "tauri": {
 | 
				
			||||||
    "allowlist": {
 | 
					    "allowlist": {
 | 
				
			||||||
      "os": {"all": true}
 | 
					      "all": true
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "bundle": {
 | 
					    "bundle": {
 | 
				
			||||||
      "active": true,
 | 
					      "active": true,
 | 
				
			||||||
@@ -48,10 +48,7 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "security": {
 | 
					    "security": {
 | 
				
			||||||
      "csp": {
 | 
					      "csp": null
 | 
				
			||||||
        "default-src": ["'self'"],
 | 
					 | 
				
			||||||
        "style-src": ["'self'", "'unsafe-inline'"]
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "updater": {
 | 
					    "updater": {
 | 
				
			||||||
      "active": false
 | 
					      "active": false
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,22 +1,21 @@
 | 
				
			|||||||
<script>
 | 
					<script>
 | 
				
			||||||
import { onMount } from 'svelte';
 | 
					import { emit, listen } from '@tauri-apps/api/event';
 | 
				
			||||||
import { listen } from '@tauri-apps/api/event';
 | 
					 | 
				
			||||||
import { invoke } from '@tauri-apps/api/tauri';
 | 
					import { invoke } from '@tauri-apps/api/tauri';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { appState, acceptRequest } from './lib/state.js';
 | 
					import { appState } from './lib/state.js';
 | 
				
			||||||
import { views, currentView, navigate } from './lib/routing.js';
 | 
					import { navigate, currentView } from './lib/routing.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
$views = import.meta.glob('./views/*.svelte', {eager: true});
 | 
					 | 
				
			||||||
navigate('Home');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
invoke('get_config').then(config => $appState.config = config);
 | 
					invoke('get_config').then(config => $appState.config = config);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
listen('credentials-request', (tauriEvent) => {
 | 
					listen('credentials-request', (tauriEvent) => {
 | 
				
			||||||
    $appState.pendingRequests.put(tauriEvent.payload);
 | 
					    $appState.pendingRequests.put(tauriEvent.payload);
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
acceptRequest();
 | 
					// can't set this in routing.js directly for some reason
 | 
				
			||||||
 | 
					if (!$currentView) {
 | 
				
			||||||
 | 
					    navigate('Home');
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| 
		 Before Width: | Height: | Size: 14 KiB  | 
@@ -1,11 +1,14 @@
 | 
				
			|||||||
import { writable, get } from 'svelte/store';
 | 
					import { writable } from 'svelte/store';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export let views = writable();
 | 
					const VIEWS = import.meta.glob('../views/*.svelte', {eager: true});
 | 
				
			||||||
export let currentView = writable();
 | 
					export let currentView = writable();
 | 
				
			||||||
export let previousView = writable();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function navigate(viewName) {
 | 
					export function navigate(viewName) {
 | 
				
			||||||
    let v = get(views)[`./views/${viewName}.svelte`].default;
 | 
					    let view = VIEWS[`../views/${viewName}.svelte`].default;
 | 
				
			||||||
    currentView.set(v)
 | 
					    currentView.set(view);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function getView(viewName) {
 | 
				
			||||||
 | 
					    return VIEWS[`../views/${viewName}.svelte`].default;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,33 +1,9 @@
 | 
				
			|||||||
import { writable, get } from 'svelte/store';
 | 
					import { writable } from 'svelte/store';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import queue from './queue.js';
 | 
					import queue from './queue.js';
 | 
				
			||||||
import { navigate, currentView, previousView } from './routing.js';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export let appState = writable({
 | 
					export let appState = writable({
 | 
				
			||||||
    currentRequest: null,
 | 
					    currentRequest: null,
 | 
				
			||||||
    pendingRequests: queue(),
 | 
					    pendingRequests: queue(),
 | 
				
			||||||
    credentialStatus: 'locked',
 | 
					    credentialStatus: 'locked',
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export async function acceptRequest() {
 | 
					 | 
				
			||||||
    let req = await get(appState).pendingRequests.get();
 | 
					 | 
				
			||||||
    appState.update($appState => {
 | 
					 | 
				
			||||||
        $appState.currentRequest = req;
 | 
					 | 
				
			||||||
        return $appState;
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
    previousView.set(get(currentView));
 | 
					 | 
				
			||||||
    navigate('Approve');
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function completeRequest() {
 | 
					 | 
				
			||||||
    appState.update($appState => {
 | 
					 | 
				
			||||||
        $appState.currentRequest = null;
 | 
					 | 
				
			||||||
        return $appState;
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
    currentView.set(get(previousView));
 | 
					 | 
				
			||||||
    previousView.set(null);
 | 
					 | 
				
			||||||
    acceptRequest();
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,3 @@
 | 
				
			|||||||
@tailwind base;
 | 
					@tailwind base;
 | 
				
			||||||
@tailwind components;
 | 
					@tailwind components;
 | 
				
			||||||
@tailwind utilities;
 | 
					@tailwind utilities;
 | 
				
			||||||
 | 
					 | 
				
			||||||
.btn-alert-error {
 | 
					 | 
				
			||||||
    @apply bg-transparent hover:bg-[#cd5a5a] border border-error-content text-error-content
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,7 +2,7 @@
 | 
				
			|||||||
    import { onMount } from 'svelte';
 | 
					    import { onMount } from 'svelte';
 | 
				
			||||||
    import { slide } from 'svelte/transition';
 | 
					    import { slide } from 'svelte/transition';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let extraClasses = "";
 | 
					    let extraClasses;
 | 
				
			||||||
    export {extraClasses as class};
 | 
					    export {extraClasses as class};
 | 
				
			||||||
    export let slideDuration = 150;
 | 
					    export let slideDuration = 150;
 | 
				
			||||||
    let animationClass = "";
 | 
					    let animationClass = "";
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,5 @@
 | 
				
			|||||||
<script>
 | 
					<script>
 | 
				
			||||||
    import { navigate } from '../lib/routing.js';
 | 
					    import { navigate, currentView } from '../lib/routing.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    export let target;
 | 
					    export let target;
 | 
				
			||||||
    export let hotkey = null;
 | 
					    export let hotkey = null;
 | 
				
			||||||
@@ -7,9 +7,6 @@
 | 
				
			|||||||
    export let alt = false;
 | 
					    export let alt = false;
 | 
				
			||||||
    export let shift = false;
 | 
					    export let shift = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let classes = "";
 | 
					 | 
				
			||||||
    export {classes as class};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    function click() {
 | 
					    function click() {
 | 
				
			||||||
        if (typeof target === 'string') {
 | 
					        if (typeof target === 'string') {
 | 
				
			||||||
            navigate(target);
 | 
					            navigate(target);
 | 
				
			||||||
@@ -29,7 +26,10 @@
 | 
				
			|||||||
        if (alt && !event.altKey) return;
 | 
					        if (alt && !event.altKey) return;
 | 
				
			||||||
        if (shift && !event.shiftKey) return;
 | 
					        if (shift && !event.shiftKey) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (event.key === hotkey) {
 | 
					        if (event.code === hotkey) {
 | 
				
			||||||
 | 
					            click();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        else if (hotkey === 'Enter' && event.code === 'NumpadEnter') {
 | 
				
			||||||
            click();
 | 
					            click();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -38,6 +38,6 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
<svelte:window on:keydown={handleHotkey} />
 | 
					<svelte:window on:keydown={handleHotkey} />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<a href="/{target}" on:click|preventDefault="{click}" class={classes}>
 | 
					<a href="#" on:click="{click}">
 | 
				
			||||||
    <slot></slot>
 | 
					    <slot></slot>
 | 
				
			||||||
</a>
 | 
					</a>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,27 +1,21 @@
 | 
				
			|||||||
<script>
 | 
					<script>
 | 
				
			||||||
    import Link from './Link.svelte';
 | 
					    import Link from './Link.svelte';
 | 
				
			||||||
    import Icon from './Icon.svelte';
 | 
					    import Icon from './Icon.svelte';
 | 
				
			||||||
 | 
					 | 
				
			||||||
    export let position = "sticky";
 | 
					 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<nav class="{position} top-0 bg-base-100 w-full flex justify-between items-center p-2">
 | 
					<nav class="fixed top-0 grid grid-cols-2 w-full p-2">
 | 
				
			||||||
    <div>
 | 
					    <div>
 | 
				
			||||||
        <Link target="Home">
 | 
					        <Link target="Home">
 | 
				
			||||||
            <button class="btn btn-square btn-ghost align-middle">
 | 
					            <button class="btn btn-squre btn-ghost align-middle">
 | 
				
			||||||
                <Icon name="home" class="w-8 h-8 stroke-2" />
 | 
					                <Icon name="home" class="w-8 h-8 stroke-2" />
 | 
				
			||||||
            </button>
 | 
					            </button>
 | 
				
			||||||
        </Link>
 | 
					        </Link>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    {#if $$slots.title}
 | 
					    <div class="justify-self-end">
 | 
				
			||||||
        <slot name="title"></slot>
 | 
					 | 
				
			||||||
    {/if}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <div>
 | 
					 | 
				
			||||||
        <Link target="Settings">
 | 
					        <Link target="Settings">
 | 
				
			||||||
            <button class="btn btn-square btn-ghost align-middle ">
 | 
					            <button class="align-middle btn btn-square btn-ghost">
 | 
				
			||||||
                <Icon name="cog-8-tooth" class="w-8 h-8 stroke-2" />
 | 
					                <Icon name="cog-8-tooth" class="w-8 h-8 stroke-2" />
 | 
				
			||||||
            </button>
 | 
					            </button>
 | 
				
			||||||
        </Link>
 | 
					        </Link>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,113 +0,0 @@
 | 
				
			|||||||
<script>
 | 
					 | 
				
			||||||
    export let color = 'base-content';
 | 
					 | 
				
			||||||
    export let thickness = '2px';
 | 
					 | 
				
			||||||
    let classes = '';
 | 
					 | 
				
			||||||
    export { classes as class };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const colorVars = {
 | 
					 | 
				
			||||||
        'primary': 'p',
 | 
					 | 
				
			||||||
        'primary-focus': 'pf',
 | 
					 | 
				
			||||||
        'primary-content': 'pc',
 | 
					 | 
				
			||||||
        'secondary': 's',
 | 
					 | 
				
			||||||
        'secondary-focus': 'sf',
 | 
					 | 
				
			||||||
        'secondary-content': 'sc',
 | 
					 | 
				
			||||||
        'accent': 'a',
 | 
					 | 
				
			||||||
        'accent-focus': 'af',
 | 
					 | 
				
			||||||
        'accent-content': 'ac',
 | 
					 | 
				
			||||||
        'neutral': 'n',
 | 
					 | 
				
			||||||
        'neutral-focus': 'nf',
 | 
					 | 
				
			||||||
        'neutral-content': 'nc',
 | 
					 | 
				
			||||||
        'base-100': 'b1',
 | 
					 | 
				
			||||||
        'base-200': 'b2',
 | 
					 | 
				
			||||||
        'base-300': 'b3',
 | 
					 | 
				
			||||||
        'base-content': 'bc',
 | 
					 | 
				
			||||||
        'info': 'in',
 | 
					 | 
				
			||||||
        'info-content': 'inc',
 | 
					 | 
				
			||||||
        'success': 'su',
 | 
					 | 
				
			||||||
        'success-content': 'suc',
 | 
					 | 
				
			||||||
        'warning': 'wa',
 | 
					 | 
				
			||||||
        'warning-content': 'wac',
 | 
					 | 
				
			||||||
        'error': 'er',
 | 
					 | 
				
			||||||
        'error-content': 'erc',
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let arcStyle = `border-width: ${thickness};`;
 | 
					 | 
				
			||||||
    arcStyle += `border-color: hsl(var(--${colorVars[color]})) transparent transparent transparent;`;
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<style>
 | 
					 | 
				
			||||||
    #spinner {
 | 
					 | 
				
			||||||
        position: relative;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        animation: spin;
 | 
					 | 
				
			||||||
        animation-duration: 1.5s;
 | 
					 | 
				
			||||||
        animation-iteration-count: infinite;
 | 
					 | 
				
			||||||
        animation-timing-function: linear;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @keyframes spin {
 | 
					 | 
				
			||||||
        50% { transform: rotate(225deg); }
 | 
					 | 
				
			||||||
        100% { transform: rotate(360deg); }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    .arc {
 | 
					 | 
				
			||||||
        position: absolute;
 | 
					 | 
				
			||||||
        top: 0;
 | 
					 | 
				
			||||||
        left: 0;
 | 
					 | 
				
			||||||
        border-radius: 9999px;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    .arc-top {
 | 
					 | 
				
			||||||
        transform: rotate(-45deg);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    .arc-right {
 | 
					 | 
				
			||||||
        animation: spin-right;
 | 
					 | 
				
			||||||
        animation-duration: 3s;
 | 
					 | 
				
			||||||
        animation-iteration-count: infinite;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    .arc-bottom {
 | 
					 | 
				
			||||||
        animation: spin-bottom;
 | 
					 | 
				
			||||||
        animation-duration: 3s;
 | 
					 | 
				
			||||||
        animation-iteration-count: infinite;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    .arc-left {
 | 
					 | 
				
			||||||
        animation: spin-left;
 | 
					 | 
				
			||||||
        animation-duration: 3s;
 | 
					 | 
				
			||||||
        animation-iteration-count: infinite;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @keyframes spin-top {
 | 
					 | 
				
			||||||
        0% { transform: rotate(-45deg); }
 | 
					 | 
				
			||||||
        50% { transform: rotate(315deg); }
 | 
					 | 
				
			||||||
        100% { transform: rotate(-45deg); }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @keyframes spin-right {
 | 
					 | 
				
			||||||
        0% { transform: rotate(45deg); }
 | 
					 | 
				
			||||||
        50% { transform: rotate(315deg); }
 | 
					 | 
				
			||||||
        100% { transform: rotate(405deg); }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @keyframes spin-bottom {
 | 
					 | 
				
			||||||
        0% { transform: rotate(135deg); }
 | 
					 | 
				
			||||||
        50% { transform: rotate(315deg); }
 | 
					 | 
				
			||||||
        100% { transform: rotate(495deg); }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @keyframes spin-left {
 | 
					 | 
				
			||||||
        0% { transform: rotate(225deg); }
 | 
					 | 
				
			||||||
        50% { transform: rotate(315deg); }
 | 
					 | 
				
			||||||
        100% { transform: rotate(585deg); }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
</style>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<div id="spinner" class="w-6 h-6 {classes}">
 | 
					 | 
				
			||||||
    <div class="arc arc-top w-full h-full" style={arcStyle}></div>
 | 
					 | 
				
			||||||
    <div class="arc arc-right w-full h-full" style={arcStyle}></div>
 | 
					 | 
				
			||||||
    <div class="arc arc-bottom w-full h-full" style={arcStyle}></div>
 | 
					 | 
				
			||||||
    <div class="arc arc-left w-full h-full" style={arcStyle}></div>
 | 
					 | 
				
			||||||
</div>
 | 
					 | 
				
			||||||
@@ -11,43 +11,26 @@
 | 
				
			|||||||
    export let decimal = false;
 | 
					    export let decimal = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const dispatch = createEventDispatcher();
 | 
					    const dispatch = createEventDispatcher();
 | 
				
			||||||
 | 
					 | 
				
			||||||
    $: localValue = value.toString();
 | 
					 | 
				
			||||||
    let lastInputTime = null;
 | 
					 | 
				
			||||||
    function debounce(event) {
 | 
					 | 
				
			||||||
        lastInputTime = Date.now();
 | 
					 | 
				
			||||||
        localValue = localValue.replace(/[^-0-9.]/g, '');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const eventTime = lastInputTime;
 | 
					 | 
				
			||||||
        const pendingValue = localValue;
 | 
					 | 
				
			||||||
        window.setTimeout(
 | 
					 | 
				
			||||||
            () => {
 | 
					 | 
				
			||||||
                // if no other inputs have occured since then
 | 
					 | 
				
			||||||
                if (eventTime === lastInputTime) {
 | 
					 | 
				
			||||||
                    updateValue(pendingValue);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            500
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let error = null;
 | 
					    let error = null;
 | 
				
			||||||
    function updateValue(newValue) {
 | 
					    function validate(event) {
 | 
				
			||||||
        // Don't update the value, but also don't error, if it's empty
 | 
					        let v = event.target.value;
 | 
				
			||||||
        // or if it could be the start of a negative or decimal number
 | 
					
 | 
				
			||||||
        if (newValue.match(/^$|^-$|^\.$/) !== null) {
 | 
					        if (v === '') {
 | 
				
			||||||
            error = null;
 | 
					            error = null;
 | 
				
			||||||
            return;
 | 
					            return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const num = parseFloat(newValue);
 | 
					        let num = parseFloat(v);
 | 
				
			||||||
        if (num % 1 !== 0 && !decimal) {
 | 
					        if (Number.isNaN(num)) {
 | 
				
			||||||
 | 
					            error = `"${v}" is not a number`;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        else if (num % 1 !== 0 && !decimal) {
 | 
				
			||||||
            error = `${num} is not a whole number`;
 | 
					            error = `${num} is not a whole number`;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        else if (min !== null && num < min) {
 | 
					        else if (min && num < min) {
 | 
				
			||||||
            error = `Too low (minimum ${min})`;
 | 
					            error = `Too low (minimum ${min})`;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        else if (max !== null && num > max) {
 | 
					        else if (max && num > max) {
 | 
				
			||||||
            error = `Too large (maximum ${max})`
 | 
					            error = `Too large (maximum ${max})`
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        else {
 | 
					        else {
 | 
				
			||||||
@@ -59,19 +42,18 @@
 | 
				
			|||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<Setting {title}>
 | 
					<Setting {title} {error}>
 | 
				
			||||||
    <div slot="input">
 | 
					    <div slot="input">
 | 
				
			||||||
        {#if unit}
 | 
					        {#if unit}
 | 
				
			||||||
            <span class="mr-2">{unit}:</span>
 | 
					            <span class="mr-2">{unit}:</span>
 | 
				
			||||||
        {/if}
 | 
					        {/if}
 | 
				
			||||||
        <div class="tooltip tooltip-error" class:tooltip-open={error !== null} data-tip="{error}">
 | 
					        <div class="tooltip tooltip-error" class:tooltip-open={error !== null} data-tip={error}>
 | 
				
			||||||
            <input 
 | 
					            <input 
 | 
				
			||||||
                type="text" 
 | 
					                type="text" 
 | 
				
			||||||
                class="input input-sm input-bordered text-right" 
 | 
					                class="input input-sm input-bordered text-right max-w-[4rem]" 
 | 
				
			||||||
                size="{Math.max(5, localValue.length)}"
 | 
					 | 
				
			||||||
                class:input-error={error} 
 | 
					                class:input-error={error} 
 | 
				
			||||||
                bind:value={localValue} 
 | 
					                value={value} 
 | 
				
			||||||
                on:input="{debounce}"
 | 
					                on:input="{validate}"
 | 
				
			||||||
            />
 | 
					            />
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,6 +3,7 @@
 | 
				
			|||||||
    import ErrorAlert from '../ErrorAlert.svelte';
 | 
					    import ErrorAlert from '../ErrorAlert.svelte';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    export let title;
 | 
					    export let title;
 | 
				
			||||||
 | 
					    export let error = null;
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -12,8 +13,6 @@
 | 
				
			|||||||
    <slot name="input"></slot>
 | 
					    <slot name="input"></slot>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{#if $$slots.description}
 | 
					<p class="mt-3">
 | 
				
			||||||
    <p class="mt-3">
 | 
					    <slot name="description"></slot>
 | 
				
			||||||
        <slot name="description"></slot>
 | 
					</p>
 | 
				
			||||||
    </p>
 | 
					 | 
				
			||||||
{/if}
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,7 +10,7 @@
 | 
				
			|||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<Setting {title}>
 | 
					<Setting title="Start minimized">
 | 
				
			||||||
    <input 
 | 
					    <input 
 | 
				
			||||||
        slot="input" 
 | 
					        slot="input" 
 | 
				
			||||||
        type="checkbox" 
 | 
					        type="checkbox" 
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,35 +1,16 @@
 | 
				
			|||||||
<script>
 | 
					<script>
 | 
				
			||||||
    import { onMount } from 'svelte';
 | 
					 | 
				
			||||||
    import { invoke } from '@tauri-apps/api/tauri';
 | 
					    import { invoke } from '@tauri-apps/api/tauri';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    import { navigate } from '../lib/routing.js';
 | 
					    import { navigate } from '../lib/routing.js';
 | 
				
			||||||
    import { appState, completeRequest } from '../lib/state.js';
 | 
					    import { appState } from '../lib/state.js';
 | 
				
			||||||
    import ErrorAlert from '../ui/ErrorAlert.svelte';
 | 
					 | 
				
			||||||
    import Link from '../ui/Link.svelte';
 | 
					    import Link from '../ui/Link.svelte';
 | 
				
			||||||
 | 
					    import Icon from '../ui/Icon.svelte';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Send response to backend, display error if applicable
 | 
					 | 
				
			||||||
    let error, alert;
 | 
					 | 
				
			||||||
    async function respond() {
 | 
					 | 
				
			||||||
        let {id, approval} = $appState.currentRequest;
 | 
					 | 
				
			||||||
        try {
 | 
					 | 
				
			||||||
            await invoke('respond', {response: {id, approval}});
 | 
					 | 
				
			||||||
            navigate('ShowResponse');
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        catch (e) {
 | 
					 | 
				
			||||||
            if (error) {
 | 
					 | 
				
			||||||
                alert.shake();
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            error = e;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Approval has one of several outcomes depending on current credential state
 | 
					 | 
				
			||||||
    async function approve() {
 | 
					    async function approve() {
 | 
				
			||||||
        $appState.currentRequest.approval = 'Approved';
 | 
					 | 
				
			||||||
        let status = await invoke('get_session_status');
 | 
					        let status = await invoke('get_session_status');
 | 
				
			||||||
        if (status === 'unlocked') {
 | 
					        if (status === 'unlocked') {
 | 
				
			||||||
            await respond();
 | 
					            navigate('ShowApproved');
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        else if (status === 'locked') {
 | 
					        else if (status === 'locked') {
 | 
				
			||||||
            navigate('Unlock');
 | 
					            navigate('Unlock');
 | 
				
			||||||
@@ -39,81 +20,34 @@
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Denial has only one
 | 
					    var appName = null;
 | 
				
			||||||
    async function deny() {
 | 
					 | 
				
			||||||
        $appState.currentRequest.approval = 'Denied';
 | 
					 | 
				
			||||||
        await respond();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Extract executable name from full path
 | 
					 | 
				
			||||||
    let appName = null;
 | 
					 | 
				
			||||||
    if ($appState.currentRequest.clients.length === 1) {
 | 
					    if ($appState.currentRequest.clients.length === 1) {
 | 
				
			||||||
        let path = $appState.currentRequest.clients[0].exe;
 | 
					        let path = $appState.currentRequest.clients[0].exe;
 | 
				
			||||||
        let m = path.match(/\/([^/]+?$)|\\([^\\]+?$)/);
 | 
					        let m = path.match(/\/([^/]+?$)|\\([^\\]+?$)/);
 | 
				
			||||||
        appName = m[1] || m[2];
 | 
					        appName = m[1] || m[2];
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Executable paths can be long, so ensure they only break on \ or /
 | 
					 | 
				
			||||||
    function breakPath(client) {
 | 
					 | 
				
			||||||
        return client.exe.replace(/(\\|\/)/g, '$1<wbr>');
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // if the request has already been approved/denied, send response immediately
 | 
					 | 
				
			||||||
    onMount(async () => {
 | 
					 | 
				
			||||||
        if ($appState.currentRequest.approval) {
 | 
					 | 
				
			||||||
            await respond();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<!-- Don't render at all if we're just going to immediately proceed to the next screen -->
 | 
					<div class="flex flex-col space-y-4 p-4 m-auto max-w-max h-screen justify-center">
 | 
				
			||||||
{#if error || !$appState.currentRequest.approval}
 | 
					    <!-- <div class="p-4 rounded-box border-2 border-neutral-content"> -->
 | 
				
			||||||
    <div class="flex flex-col space-y-4 p-4 m-auto max-w-xl h-screen items-center justify-center">
 | 
					 | 
				
			||||||
        {#if error}
 | 
					 | 
				
			||||||
            <ErrorAlert bind:this={alert}>
 | 
					 | 
				
			||||||
                {error}
 | 
					 | 
				
			||||||
                <svelte:fragment slot="buttons">
 | 
					 | 
				
			||||||
                    <button class="btn btn-sm btn-alert-error" on:click={completeRequest}>Cancel</button>
 | 
					 | 
				
			||||||
                    <button class="btn btn-sm btn-alert-error" on:click={respond}>Retry</button>
 | 
					 | 
				
			||||||
                </svelte:fragment>
 | 
					 | 
				
			||||||
            </ErrorAlert>
 | 
					 | 
				
			||||||
        {/if}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        {#if $appState.currentRequest.base}
 | 
					 | 
				
			||||||
            <div class="alert alert-warning shadow-lg">
 | 
					 | 
				
			||||||
                <div>
 | 
					 | 
				
			||||||
                    <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current flex-shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>
 | 
					 | 
				
			||||||
                    <span>
 | 
					 | 
				
			||||||
                        WARNING: This application is requesting your base (long-lived) AWS credentials. 
 | 
					 | 
				
			||||||
                        These credentials are less secure than session credentials, since they don't expire automatically.
 | 
					 | 
				
			||||||
                    </span>
 | 
					 | 
				
			||||||
                </div>
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
        {/if}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <div class="space-y-1 mb-4">
 | 
					        <div class="space-y-1 mb-4">
 | 
				
			||||||
            <h2 class="text-xl font-bold">{appName ? `"${appName}"` : 'An appplication'} would like to access your AWS credentials.</h2>
 | 
					            <h2 class="text-xl font-bold">{appName ? `"${appName}"` : 'An appplication'} would like to access your AWS credentials.</h2>
 | 
				
			||||||
 | 
					            {#each $appState.currentRequest.clients as client}
 | 
				
			||||||
            <div class="grid grid-cols-[auto_1fr] gap-x-3">
 | 
					                <p>Path: {client ? client.exe : 'Unknown'}</p>
 | 
				
			||||||
                {#each $appState.currentRequest.clients as client}
 | 
					                <p>PID: {client ? client.pid : 'Unknown'}</p>
 | 
				
			||||||
                    <div class="text-right">Path:</div>
 | 
					            {/each}
 | 
				
			||||||
                    <code class="">{@html client ? breakPath(client) : 'Unknown'}</code>
 | 
					 | 
				
			||||||
                    <div class="text-right">PID:</div>
 | 
					 | 
				
			||||||
                    <code>{client ? client.pid : 'Unknown'}</code>
 | 
					 | 
				
			||||||
                {/each}
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <div class="w-full flex justify-between">
 | 
					        <div class="grid grid-cols-2">
 | 
				
			||||||
            <Link target={deny} hotkey="Escape">
 | 
					            <Link target="ShowDenied" hotkey="Escape">
 | 
				
			||||||
                <button class="btn btn-error justify-self-start">
 | 
					                <button class="btn btn-error justify-self-start">
 | 
				
			||||||
                    Deny
 | 
					                    Deny
 | 
				
			||||||
                    <kbd class="ml-2 normal-case px-1 py-0.5 rounded border border-neutral">Esc</kbd>
 | 
					                    <kbd class="ml-2 normal-case px-1 py-0.5 rounded border border-neutral">Esc</kbd>
 | 
				
			||||||
                </button>
 | 
					                </button>
 | 
				
			||||||
            </Link>
 | 
					            </Link>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <Link target={approve} hotkey="Enter" shift="{true}">
 | 
					            <Link target="{approve}" hotkey="Enter" shift="{true}">
 | 
				
			||||||
                <button class="btn btn-success justify-self-end">
 | 
					                <button class="btn btn-success justify-self-end">
 | 
				
			||||||
                    Approve
 | 
					                    Approve
 | 
				
			||||||
                    <kbd class="ml-2 normal-case px-1 py-0.5 rounded border border-neutral">Shift</kbd>
 | 
					                    <kbd class="ml-2 normal-case px-1 py-0.5 rounded border border-neutral">Shift</kbd>
 | 
				
			||||||
@@ -122,5 +56,4 @@
 | 
				
			|||||||
                </button>
 | 
					                </button>
 | 
				
			||||||
            </Link>
 | 
					            </Link>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
    </div>
 | 
					</div>
 | 
				
			||||||
{/if}
 | 
					 | 
				
			||||||
@@ -7,32 +7,20 @@
 | 
				
			|||||||
    import { navigate } from '../lib/routing.js';
 | 
					    import { navigate } from '../lib/routing.js';
 | 
				
			||||||
    import Link from '../ui/Link.svelte';
 | 
					    import Link from '../ui/Link.svelte';
 | 
				
			||||||
    import ErrorAlert from '../ui/ErrorAlert.svelte';
 | 
					    import ErrorAlert from '../ui/ErrorAlert.svelte';
 | 
				
			||||||
    import Spinner from '../ui/Spinner.svelte';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let errorMsg = null;
 | 
					    let errorMsg = null;
 | 
				
			||||||
    let alert;
 | 
					    let alert;
 | 
				
			||||||
    let AccessKeyId, SecretAccessKey, passphrase, confirmPassphrase
 | 
					    let AccessKeyId, SecretAccessKey, passphrase
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function confirm() {
 | 
					 | 
				
			||||||
        if (passphrase !== confirmPassphrase) {
 | 
					 | 
				
			||||||
            errorMsg = 'Passphrases do not match.'
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let saving = false;
 | 
					 | 
				
			||||||
    async function save() {
 | 
					    async function save() {
 | 
				
			||||||
        if (passphrase !== confirmPassphrase) {
 | 
					        console.log('Saving credentials.');
 | 
				
			||||||
            alert.shake();
 | 
					 | 
				
			||||||
            return;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let credentials = {AccessKeyId, SecretAccessKey};
 | 
					        let credentials = {AccessKeyId, SecretAccessKey};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            saving = true;
 | 
					 | 
				
			||||||
            await invoke('save_credentials', {credentials, passphrase});
 | 
					            await invoke('save_credentials', {credentials, passphrase});
 | 
				
			||||||
            if ($appState.currentRequest) {
 | 
					            if ($appState.currentRequest) {
 | 
				
			||||||
                navigate('Approve');
 | 
					                navigate('ShowApproved');
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            else {
 | 
					            else {
 | 
				
			||||||
                navigate('Home');
 | 
					                navigate('Home');
 | 
				
			||||||
@@ -50,8 +38,6 @@
 | 
				
			|||||||
            if (alert) {
 | 
					            if (alert) {
 | 
				
			||||||
                alert.shake();
 | 
					                alert.shake();
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					 | 
				
			||||||
            saving = false;
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
@@ -68,15 +54,8 @@
 | 
				
			|||||||
    <input type="text" placeholder="AWS Access Key ID" bind:value="{AccessKeyId}" class="input input-bordered" />
 | 
					    <input type="text" placeholder="AWS Access Key ID" bind:value="{AccessKeyId}" class="input input-bordered" />
 | 
				
			||||||
    <input type="password" placeholder="AWS Secret Access Key" bind:value="{SecretAccessKey}" class="input input-bordered" />
 | 
					    <input type="password" placeholder="AWS Secret Access Key" bind:value="{SecretAccessKey}" class="input input-bordered" />
 | 
				
			||||||
    <input type="password" placeholder="Passphrase" bind:value="{passphrase}" class="input input-bordered" />
 | 
					    <input type="password" placeholder="Passphrase" bind:value="{passphrase}" class="input input-bordered" />
 | 
				
			||||||
    <input type="password" placeholder="Re-enter passphrase" bind:value={confirmPassphrase} class="input input-bordered" on:change={confirm} />
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <button type="submit" class="btn btn-primary">
 | 
					    <input type="submit" class="btn btn-primary" />
 | 
				
			||||||
        {#if saving}
 | 
					 | 
				
			||||||
            <Spinner class="w-5 h-5" color="primary-content" thickness="2px"/>
 | 
					 | 
				
			||||||
        {:else}
 | 
					 | 
				
			||||||
            Submit
 | 
					 | 
				
			||||||
        {/if}
 | 
					 | 
				
			||||||
    </button>
 | 
					 | 
				
			||||||
    <Link target="Home" hotkey="Escape">
 | 
					    <Link target="Home" hotkey="Escape">
 | 
				
			||||||
        <button class="btn btn-sm btn-outline w-full">Cancel</button>
 | 
					        <button class="btn btn-sm btn-outline w-full">Cancel</button>
 | 
				
			||||||
    </Link>
 | 
					    </Link>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,41 +8,34 @@
 | 
				
			|||||||
    import Icon from '../ui/Icon.svelte';
 | 
					    import Icon from '../ui/Icon.svelte';
 | 
				
			||||||
    import Link from '../ui/Link.svelte';
 | 
					    import Link from '../ui/Link.svelte';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    import vaultDoorSvg from '../assets/vault_door.svg?raw';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    onMount(async () => {
 | 
				
			||||||
    // onMount(async () => {
 | 
					        // will block until a request comes in
 | 
				
			||||||
    //     // will block until a request comes in
 | 
					        let req = await $appState.pendingRequests.get();
 | 
				
			||||||
    //     let req = await $appState.pendingRequests.get();
 | 
					        $appState.currentRequest = req;
 | 
				
			||||||
    //     $appState.currentRequest = req;
 | 
					        navigate('Approve');
 | 
				
			||||||
    //     navigate('Approve');
 | 
					    });
 | 
				
			||||||
    // });
 | 
					 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<Nav position="fixed">
 | 
					<Nav />
 | 
				
			||||||
    <h2 slot="title" class="text-3xl font-bold">Creddy</h2>
 | 
					 | 
				
			||||||
</Nav>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div class="flex flex-col h-screen items-center justify-center p-4 space-y-4">
 | 
					<div class="flex flex-col h-screen justify-center items-center space-y-4">
 | 
				
			||||||
    {#await invoke('get_session_status') then status}
 | 
					    {#await invoke('get_session_status') then status}
 | 
				
			||||||
        {#if status === 'locked'}
 | 
					        {#if status === 'locked'}
 | 
				
			||||||
 | 
					            <img src="/static/padlock-closed.svg" alt="A locked padlock" class="w-32" />
 | 
				
			||||||
            {@html vaultDoorSvg}
 | 
					 | 
				
			||||||
            <h2 class="text-2xl font-bold">Creddy is locked</h2>
 | 
					            <h2 class="text-2xl font-bold">Creddy is locked</h2>
 | 
				
			||||||
            <Link target="Unlock" hotkey="Enter" class="w-64">
 | 
					            <Link target="Unlock">
 | 
				
			||||||
                <button class="btn btn-primary w-full">Unlock</button>
 | 
					                <button class="btn btn-primary">Unlock</button>
 | 
				
			||||||
            </Link>
 | 
					            </Link>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        {:else if status === 'unlocked'}
 | 
					        {:else if status === 'unlocked'}
 | 
				
			||||||
            {@html vaultDoorSvg}
 | 
					            <img src="/static/padlock-open.svg" alt="An unlocked padlock" class="w-24" />
 | 
				
			||||||
            <h2 class="text-2xl font-bold">Waiting for requests</h2>
 | 
					            <h2 class="text-2xl font-bold">Waiting for requests</h2>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        {:else if status === 'empty'}
 | 
					        {:else if status === 'empty'}
 | 
				
			||||||
            {@html vaultDoorSvg}
 | 
					            <Link target="EnterCredentials">
 | 
				
			||||||
            <h2 class="text-2xl font-bold">No credentials found</h2>
 | 
					                <button class="btn btn-primary">Enter Credentials</button>
 | 
				
			||||||
            <Link target="EnterCredentials" hotkey="Enter" class="w-64">
 | 
					 | 
				
			||||||
                <button class="btn btn-primary w-full">Enter Credentials</button>
 | 
					 | 
				
			||||||
            </Link>
 | 
					            </Link>
 | 
				
			||||||
        {/if}
 | 
					        {/if}
 | 
				
			||||||
    {/await}
 | 
					    {/await}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,46 +1,24 @@
 | 
				
			|||||||
<script>
 | 
					<script>
 | 
				
			||||||
    import { invoke } from '@tauri-apps/api/tauri';
 | 
					    import { invoke } from '@tauri-apps/api/tauri';
 | 
				
			||||||
    import { type } from '@tauri-apps/api/os';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    import { appState } from '../lib/state.js';
 | 
					    import { appState } from '../lib/state.js';
 | 
				
			||||||
    import Nav from '../ui/Nav.svelte';
 | 
					    import Nav from '../ui/Nav.svelte';
 | 
				
			||||||
    import Link from '../ui/Link.svelte';
 | 
					    import Link from '../ui/Link.svelte';
 | 
				
			||||||
    import ErrorAlert from '../ui/ErrorAlert.svelte';
 | 
					    import ErrorAlert from '../ui/ErrorAlert.svelte';
 | 
				
			||||||
 | 
					    // import Setting from '../ui/settings/Setting.svelte';
 | 
				
			||||||
    import { Setting, ToggleSetting, NumericSetting } from '../ui/settings';
 | 
					    import { Setting, ToggleSetting, NumericSetting } from '../ui/settings';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    import { fly } from 'svelte/transition';
 | 
					 | 
				
			||||||
    import { backInOut } from 'svelte/easing';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let error = null;
 | 
					 | 
				
			||||||
    async function save() {
 | 
					    async function save() {
 | 
				
			||||||
        try {
 | 
					        await invoke('save_config', {config: $appState.config});
 | 
				
			||||||
            await invoke('save_config', {config: $appState.config});
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        catch (e) {
 | 
					 | 
				
			||||||
            error = e;
 | 
					 | 
				
			||||||
            $appState.config = await invoke('get_config');
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					 | 
				
			||||||
    let osType = '';
 | 
					 | 
				
			||||||
    type().then(t => osType = t);
 | 
					 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<Nav>
 | 
					<Nav />
 | 
				
			||||||
    <h2 slot="title" class="text-2xl font-bold">Settings</h2>
 | 
					 | 
				
			||||||
</Nav>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
{#await invoke('get_config') then config}
 | 
					{#await invoke('get_config') then config}
 | 
				
			||||||
    <div class="max-w-md mx-auto mt-1.5 p-4">
 | 
					    <div class="mx-auto mt-3 max-w-md">
 | 
				
			||||||
        <!-- <h2 class="text-2xl font-bold text-center">Settings</h2> -->
 | 
					        <h2 class="text-2xl font-bold text-center">Settings</h2>
 | 
				
			||||||
 | 
					 | 
				
			||||||
        <ToggleSetting title="Start on login" bind:value={$appState.config.start_on_login} on:update={save}>
 | 
					 | 
				
			||||||
            <svelte:fragment slot="description">
 | 
					 | 
				
			||||||
                Start Creddy when you log in to your computer.
 | 
					 | 
				
			||||||
            </svelte:fragment>
 | 
					 | 
				
			||||||
        </ToggleSetting>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <ToggleSetting title="Start minimized" bind:value={$appState.config.start_minimized} on:update={save}>
 | 
					        <ToggleSetting title="Start minimized" bind:value={$appState.config.start_minimized} on:update={save}>
 | 
				
			||||||
            <svelte:fragment slot="description">
 | 
					            <svelte:fragment slot="description">
 | 
				
			||||||
@@ -56,18 +34,6 @@
 | 
				
			|||||||
            </svelte:fragment>
 | 
					            </svelte:fragment>
 | 
				
			||||||
        </NumericSetting>
 | 
					        </NumericSetting>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <NumericSetting
 | 
					 | 
				
			||||||
            title="Listen port"
 | 
					 | 
				
			||||||
            bind:value={$appState.config.listen_port}
 | 
					 | 
				
			||||||
            min={osType === 'Windows_NT' ? 1 : 0}
 | 
					 | 
				
			||||||
            on:update={save}
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
            <svelte:fragment slot="description">
 | 
					 | 
				
			||||||
                Listen for credentials requests on this port. 
 | 
					 | 
				
			||||||
                (Should be used with <code>$AWS_CONTAINER_CREDENTIALS_FULL_URI</code>)
 | 
					 | 
				
			||||||
            </svelte:fragment>
 | 
					 | 
				
			||||||
        </NumericSetting>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <Setting title="Update credentials">
 | 
					        <Setting title="Update credentials">
 | 
				
			||||||
            <Link slot="input" target="EnterCredentials">
 | 
					            <Link slot="input" target="EnterCredentials">
 | 
				
			||||||
                <button class="btn btn-sm btn-primary">Update</button>
 | 
					                <button class="btn btn-sm btn-primary">Update</button>
 | 
				
			||||||
@@ -78,17 +44,3 @@
 | 
				
			|||||||
        </Setting>
 | 
					        </Setting>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
{/await}
 | 
					{/await}
 | 
				
			||||||
 | 
					 | 
				
			||||||
{#if error}
 | 
					 | 
				
			||||||
    <div transition:fly={{y: 100, easing: backInOut, duration: 400}} class="toast">
 | 
					 | 
				
			||||||
        <div class="alert alert-error no-animation">
 | 
					 | 
				
			||||||
            <div>
 | 
					 | 
				
			||||||
                <span>{error}</span>
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            <div>
 | 
					 | 
				
			||||||
                <button class="btn btn-sm btn-alert-error" on:click={() => error = null}>Ok</button>
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
{/if}
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										75
									
								
								src/views/ShowApproved.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								src/views/ShowApproved.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,75 @@
 | 
				
			|||||||
 | 
					<script>
 | 
				
			||||||
 | 
					    import { onMount } from 'svelte';
 | 
				
			||||||
 | 
					    import { draw, fade } from 'svelte/transition';
 | 
				
			||||||
 | 
					    import { emit } from '@tauri-apps/api/event';
 | 
				
			||||||
 | 
					    import { invoke } from '@tauri-apps/api/tauri';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    import { appState } from '../lib/state.js';
 | 
				
			||||||
 | 
					    import { navigate } from '../lib/routing.js';
 | 
				
			||||||
 | 
					    import ErrorAlert from '../ui/ErrorAlert.svelte';
 | 
				
			||||||
 | 
					    import Icon from '../ui/Icon.svelte';
 | 
				
			||||||
 | 
					    import Link from '../ui/Link.svelte';
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    let success = false;
 | 
				
			||||||
 | 
					    let error = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async function respond() {
 | 
				
			||||||
 | 
					        let response = {
 | 
				
			||||||
 | 
					            id: $appState.currentRequest.id,
 | 
				
			||||||
 | 
					            approval: 'Approved',
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            await invoke('respond', {response});
 | 
				
			||||||
 | 
					            success = true;
 | 
				
			||||||
 | 
					            $appState.currentRequest = null;
 | 
				
			||||||
 | 
					            window.setTimeout(() => navigate('Home'), 1000);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        catch (e) {
 | 
				
			||||||
 | 
					            error = e;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    onMount(respond);
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style>
 | 
				
			||||||
 | 
					    :global(body) {
 | 
				
			||||||
 | 
					        overflow: hidden;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{#if error}
 | 
				
			||||||
 | 
					    <div class="flex flex-col h-screen items-center justify-center m-auto max-w-lg">
 | 
				
			||||||
 | 
					        <ErrorAlert>
 | 
				
			||||||
 | 
					            {error}
 | 
				
			||||||
 | 
					            <svelte:fragment slot="buttons">
 | 
				
			||||||
 | 
					                <Link target="Home">
 | 
				
			||||||
 | 
					                    <button class="btn btn-sm bg-transparent hover:bg-[#cd5a5a] border border-error-content text-error-content">
 | 
				
			||||||
 | 
					                        Ok
 | 
				
			||||||
 | 
					                    </button>
 | 
				
			||||||
 | 
					                </Link>
 | 
				
			||||||
 | 
					            </svelte:fragment>
 | 
				
			||||||
 | 
					        </ErrorAlert>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					{:else if success}
 | 
				
			||||||
 | 
					    <div class="flex flex-col h-screen items-center justify-center max-w-max m-auto">
 | 
				
			||||||
 | 
					        <svg xmlns="http://www.w3.org/2000/svg" class="w-36 h-36" fill="none" viewBox="0 0 24 24" stroke-width="1" stroke="currentColor">
 | 
				
			||||||
 | 
					          <path in:draw="{{duration: 500}}" stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
 | 
				
			||||||
 | 
					        </svg>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div in:fade="{{delay: 200, duration: 300}}" class="text-2xl font-bold">Approved!</div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					{/if}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<!-- 
 | 
				
			||||||
 | 
					{#if error}
 | 
				
			||||||
 | 
					    <div class="text-red-400">{error}</div>
 | 
				
			||||||
 | 
					{:else}
 | 
				
			||||||
 | 
					    <h1 class="text-4xl text-gray-300">Approved!</h1>
 | 
				
			||||||
 | 
					{/if}
 | 
				
			||||||
 | 
					 -->
 | 
				
			||||||
							
								
								
									
										56
									
								
								src/views/ShowDenied.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								src/views/ShowDenied.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,56 @@
 | 
				
			|||||||
 | 
					<script>
 | 
				
			||||||
 | 
					    import { onMount } from 'svelte';
 | 
				
			||||||
 | 
					    import { draw, fade } from 'svelte/transition';
 | 
				
			||||||
 | 
					    import { emit } from '@tauri-apps/api/event';
 | 
				
			||||||
 | 
					    import { invoke } from '@tauri-apps/api/tauri';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    import { appState } from '../lib/state.js';
 | 
				
			||||||
 | 
					    import { navigate } from '../lib/routing.js';
 | 
				
			||||||
 | 
					    import ErrorAlert from '../ui/ErrorAlert.svelte';
 | 
				
			||||||
 | 
					    import Icon from '../ui/Icon.svelte';
 | 
				
			||||||
 | 
					    import Link from '../ui/Link.svelte';
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    let error = null;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    async function respond() {
 | 
				
			||||||
 | 
					        let response = {
 | 
				
			||||||
 | 
					            id: $appState.currentRequest.id,
 | 
				
			||||||
 | 
					            approval: 'Denied',
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            await invoke('respond', {response});
 | 
				
			||||||
 | 
					            $appState.currentRequest = null;
 | 
				
			||||||
 | 
					            window.setTimeout(() => navigate('Home'), 1000);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        catch (e) {
 | 
				
			||||||
 | 
					            error = e;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    onMount(respond);
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{#if error}
 | 
				
			||||||
 | 
					    <div class="flex flex-col h-screen items-center justify-center m-auto max-w-lg">
 | 
				
			||||||
 | 
					        <ErrorAlert>
 | 
				
			||||||
 | 
					            {error}
 | 
				
			||||||
 | 
					            <svelte:fragment slot="buttons">
 | 
				
			||||||
 | 
					                <Link target="Home">
 | 
				
			||||||
 | 
					                    <button class="btn btn-sm bg-transparent hover:bg-[#cd5a5a] border border-error-content text-error-content" on:click="{() => navigate('Home')}">
 | 
				
			||||||
 | 
					                        Ok
 | 
				
			||||||
 | 
					                    </button>
 | 
				
			||||||
 | 
					                </Link>
 | 
				
			||||||
 | 
					            </svelte:fragment>
 | 
				
			||||||
 | 
					        </ErrorAlert>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					{:else}
 | 
				
			||||||
 | 
					    <div class="flex flex-col items-center justify-center h-screen max-w-max m-auto">
 | 
				
			||||||
 | 
					        <svg xmlns="http://www.w3.org/2000/svg" class="w-36 h-36" fill="none" viewBox="0 0 24 24" stroke-width="1" stroke="currentColor">
 | 
				
			||||||
 | 
					            <path in:draw="{{duration: 500}}" stroke-linecap="round" stroke-linejoin="round" d="M9.75 9.75l4.5 4.5m0-4.5l-4.5 4.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
 | 
				
			||||||
 | 
					        </svg>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div in:fade="{{delay: 200, duration: 300}}" class="text-2xl font-bold">Denied!</div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					{/if}
 | 
				
			||||||
@@ -1,38 +0,0 @@
 | 
				
			|||||||
<script>
 | 
					 | 
				
			||||||
    import { onMount } from 'svelte';
 | 
					 | 
				
			||||||
    import { draw, fade } from 'svelte/transition';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    import { appState, completeRequest } from '../lib/state.js';
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    let success = false;
 | 
					 | 
				
			||||||
    let error = null;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let drawDuration = $appState.config.rehide_ms >= 750 ? 500 : 0;
 | 
					 | 
				
			||||||
    let fadeDuration = drawDuration * 0.6;
 | 
					 | 
				
			||||||
    let fadeDelay = drawDuration * 0.4;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    onMount(() => {
 | 
					 | 
				
			||||||
        window.setTimeout(
 | 
					 | 
				
			||||||
            completeRequest,
 | 
					 | 
				
			||||||
            // Extra 50ms so the window can finish disappearing before the redraw
 | 
					 | 
				
			||||||
            Math.min(5000, $appState.config.rehide_ms + 50),
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<div class="flex flex-col h-screen items-center justify-center max-w-max m-auto">
 | 
					 | 
				
			||||||
    {#if $appState.currentRequest.approval === 'Approved'}
 | 
					 | 
				
			||||||
        <svg xmlns="http://www.w3.org/2000/svg" class="w-36 h-36" fill="none" viewBox="0 0 24 24" stroke-width="1" stroke="currentColor">
 | 
					 | 
				
			||||||
          <path in:draw="{{duration: drawDuration}}" stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
 | 
					 | 
				
			||||||
        </svg>
 | 
					 | 
				
			||||||
    {:else}
 | 
					 | 
				
			||||||
        <svg xmlns="http://www.w3.org/2000/svg" class="w-36 h-36" fill="none" viewBox="0 0 24 24" stroke-width="1" stroke="currentColor">
 | 
					 | 
				
			||||||
            <path in:draw="{{duration: 500}}" stroke-linecap="round" stroke-linejoin="round" d="M9.75 9.75l4.5 4.5m0-4.5l-4.5 4.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
 | 
					 | 
				
			||||||
        </svg>
 | 
					 | 
				
			||||||
    {/if}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <div in:fade="{{duration: fadeDuration, delay: fadeDelay}}" class="text-2xl font-bold">
 | 
					 | 
				
			||||||
        {$appState.currentRequest.approval}!
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
</div>
 | 
					 | 
				
			||||||
@@ -1,33 +1,22 @@
 | 
				
			|||||||
<script>
 | 
					<script>
 | 
				
			||||||
    import { invoke } from '@tauri-apps/api/tauri';
 | 
					    import { invoke } from '@tauri-apps/api/tauri';
 | 
				
			||||||
    import { onMount } from 'svelte';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    import { appState } from '../lib/state.js';
 | 
					    import { appState } from '../lib/state.js';
 | 
				
			||||||
    import { navigate } from '../lib/routing.js';
 | 
					    import { navigate } from '../lib/routing.js';
 | 
				
			||||||
    import { getRootCause } from '../lib/errors.js';
 | 
					    import { getRootCause } from '../lib/errors.js';
 | 
				
			||||||
    import ErrorAlert from '../ui/ErrorAlert.svelte';
 | 
					    import ErrorAlert from '../ui/ErrorAlert.svelte';
 | 
				
			||||||
    import Link from '../ui/Link.svelte';
 | 
					    import Link from '../ui/Link.svelte';
 | 
				
			||||||
    import Spinner from '../ui/Spinner.svelte';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let errorMsg = null;
 | 
					    let errorMsg = null;
 | 
				
			||||||
    let alert;
 | 
					    let alert;
 | 
				
			||||||
    let passphrase = '';
 | 
					    let passphrase = '';
 | 
				
			||||||
    let loadTime = 0;
 | 
					 | 
				
			||||||
    let saving = false;
 | 
					 | 
				
			||||||
    async function unlock() {
 | 
					    async function unlock() {
 | 
				
			||||||
        // The hotkey for navigating here from homepage is Enter, which also
 | 
					 | 
				
			||||||
        // happens to trigger the form submit event
 | 
					 | 
				
			||||||
        if (Date.now() - loadTime < 10) {
 | 
					 | 
				
			||||||
            return;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            saving = true;
 | 
					 | 
				
			||||||
            let r = await invoke('unlock', {passphrase});
 | 
					            let r = await invoke('unlock', {passphrase});
 | 
				
			||||||
            $appState.credentialStatus = 'unlocked';
 | 
					            $appState.credentialStatus = 'unlocked';
 | 
				
			||||||
            if ($appState.currentRequest) {
 | 
					            if ($appState.currentRequest) {
 | 
				
			||||||
                navigate('Approve');
 | 
					                navigate('ShowApproved');
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            else {
 | 
					            else {
 | 
				
			||||||
                navigate('Home');
 | 
					                navigate('Home');
 | 
				
			||||||
@@ -46,14 +35,8 @@
 | 
				
			|||||||
            if (alert) {
 | 
					            if (alert) {
 | 
				
			||||||
                alert.shake();
 | 
					                alert.shake();
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					 | 
				
			||||||
            saving = true;
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					 | 
				
			||||||
    onMount(() => {
 | 
					 | 
				
			||||||
        loadTime = Date.now();
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -64,17 +47,9 @@
 | 
				
			|||||||
        <ErrorAlert bind:this="{alert}">{errorMsg}</ErrorAlert>
 | 
					        <ErrorAlert bind:this="{alert}">{errorMsg}</ErrorAlert>
 | 
				
			||||||
    {/if}
 | 
					    {/if}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <!-- svelte-ignore a11y-autofocus -->
 | 
					 | 
				
			||||||
    <input autofocus name="password" type="password" placeholder="correct horse battery staple" bind:value="{passphrase}" class="input input-bordered" />
 | 
					    <input autofocus name="password" type="password" placeholder="correct horse battery staple" bind:value="{passphrase}" class="input input-bordered" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <button type="submit" class="btn btn-primary">
 | 
					    <input type="submit" class="btn btn-primary" />
 | 
				
			||||||
        {#if saving}
 | 
					 | 
				
			||||||
            <Spinner class="w-5 h-5" color="primary-content" thickness="2px"/>
 | 
					 | 
				
			||||||
        {:else}
 | 
					 | 
				
			||||||
            Submit
 | 
					 | 
				
			||||||
        {/if}
 | 
					 | 
				
			||||||
    </button>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <Link target="Home" hotkey="Escape">
 | 
					    <Link target="Home" hotkey="Escape">
 | 
				
			||||||
        <button class="btn btn-outline btn-sm w-full">Cancel</button>
 | 
					        <button class="btn btn-outline btn-sm w-full">Cancel</button>
 | 
				
			||||||
    </Link>
 | 
					    </Link>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										430
									
								
								static/padlock-closed.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										430
									
								
								static/padlock-closed.svg
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| 
		 After Width: | Height: | Size: 34 KiB  | 
							
								
								
									
										467
									
								
								static/padlock-open.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										467
									
								
								static/padlock-open.svg
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| 
		 After Width: | Height: | Size: 46 KiB  | 
		Reference in New Issue
	
	Block a user