From 1047818fdcfb174e8ed683daf779cc53e80b83c0 Mon Sep 17 00:00:00 2001 From: Joseph Montanaro Date: Mon, 18 Sep 2023 20:13:29 -0700 Subject: [PATCH 1/7] basic implementation of named-pipe server --- doc/todo.md | 1 + package-lock.json | 4 +- src-tauri/Cargo.lock | 48 ++-- src-tauri/Cargo.toml | 2 +- src-tauri/src/app.rs | 4 +- src-tauri/src/bin/creddy_cli.rs | 3 +- src-tauri/src/cli.rs | 130 ++++++----- src-tauri/src/clientinfo.rs | 158 ++++++++----- src-tauri/src/config.rs | 18 -- src-tauri/src/credentials.rs | 16 +- src-tauri/src/errors.rs | 72 ++++-- src-tauri/src/ipc.rs | 4 +- src-tauri/src/main.rs | 3 +- src-tauri/src/server.rs | 403 +++++++++++++------------------- src-tauri/src/state.rs | 80 +++---- src-tauri/src/terminal.rs | 2 +- src/views/Approve.svelte | 23 +- src/views/Settings.svelte | 12 - 18 files changed, 464 insertions(+), 519 deletions(-) diff --git a/doc/todo.md b/doc/todo.md index f032674..e7a38b3 100644 --- a/doc/todo.md +++ b/doc/todo.md @@ -16,3 +16,4 @@ * Generalize Request across both credentials and terminal launch? * Make hotkey configuration a little more tolerant of slight mistiming * Distinguish between request that was denied and request that was canceled (e.g. due to error) +* Use atomic types for primitive state values instead of RwLock'd types diff --git a/package-lock.json b/package-lock.json index 799ee81..0a9206a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "creddy", - "version": "0.3.1", + "version": "0.3.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "creddy", - "version": "0.3.1", + "version": "0.3.3", "dependencies": { "@tauri-apps/api": "^1.0.2", "daisyui": "^2.51.5" diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index c03df5c..9d2a462 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1035,7 +1035,7 @@ dependencies = [ [[package]] name = "creddy" -version = "0.3.2" +version = "0.3.3" dependencies = [ "argon2", "auto-launch", @@ -1047,7 +1047,6 @@ dependencies = [ "clap", "dirs 5.0.1", "is-terminal", - "netstat2", "once_cell", "serde", "serde_json", @@ -1062,6 +1061,7 @@ dependencies = [ "thiserror", "tokio", "which", + "windows 0.51.1", ] [[package]] @@ -2641,20 +2641,6 @@ dependencies = [ "jni-sys", ] -[[package]] -name = "netstat2" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0faa3f4ad230fd2bf2a5dad71476ecbaeaed904b3c7e7e5b1f266c415c03761f" -dependencies = [ - "bitflags 1.3.2", - "byteorder", - "libc", - "num-derive", - "num-traits", - "thiserror", -] - [[package]] name = "new_debug_unreachable" version = "1.0.4" @@ -2708,17 +2694,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "num-derive" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "num-integer" version = "0.1.45" @@ -5261,6 +5236,16 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca229916c5ee38c2f2bc1e9d8f04df975b4bd93f9955dc69fabb5d91270045c9" +dependencies = [ + "windows-core", + "windows-targets 0.48.5", +] + [[package]] name = "windows-bindgen" version = "0.39.0" @@ -5271,6 +5256,15 @@ dependencies = [ "windows-tokens", ] +[[package]] +name = "windows-core" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-implement" version = "0.39.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 2779185..d7266ec 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -30,7 +30,6 @@ tauri-plugin-single-instance = { git = "https://github.com/tauri-apps/plugins-wo sodiumoxide = "0.2.7" tokio = { version = ">=1.19", features = ["full"] } sqlx = { version = "0.6.2", features = ["sqlite", "runtime-tokio-rustls"] } -netstat2 = "0.9.1" sysinfo = "0.26.8" aws-types = "0.52.0" aws-sdk-sts = "0.22.0" @@ -47,6 +46,7 @@ is-terminal = "0.4.7" argon2 = { version = "0.5.0", features = ["std"] } chacha20poly1305 = { version = "0.10.1", features = ["std"] } which = "4.4.0" +windows = { version = "0.51.1", features = ["Win32_Foundation", "Win32_System_Pipes"] } [features] # by default Tauri runs in production mode diff --git a/src-tauri/src/app.rs b/src-tauri/src/app.rs index 63cc441..b5df31b 100644 --- a/src-tauri/src/app.rs +++ b/src-tauri/src/app.rs @@ -93,7 +93,7 @@ async fn setup(app: &mut App) -> Result<(), Box> { }; let session = Session::load(&pool).await?; - let srv = Server::new(conf.listen_addr, conf.listen_port, app.handle()).await?; + Server::start(app.handle())?; config::set_auto_launch(conf.start_on_login)?; if let Err(_e) = config::set_auto_launch(conf.start_on_login) { @@ -110,7 +110,7 @@ async fn setup(app: &mut App) -> Result<(), Box> { .show()?; } - let state = AppState::new(conf, session, srv, pool, setup_errors); + let state = AppState::new(conf, session, pool, setup_errors); app.manage(state); Ok(()) } diff --git a/src-tauri/src/bin/creddy_cli.rs b/src-tauri/src/bin/creddy_cli.rs index 02ebd4b..5a13637 100644 --- a/src-tauri/src/bin/creddy_cli.rs +++ b/src-tauri/src/bin/creddy_cli.rs @@ -19,13 +19,14 @@ fn main() { let res = match args.subcommand() { None | Some(("run", _)) => launch_gui(), - Some(("show", m)) => cli::show(m), + Some(("get", m)) => cli::get(m), Some(("exec", m)) => cli::exec(m), _ => unreachable!(), }; if let Err(e) = res { eprintln!("Error: {e}"); + process::exit(1); } } diff --git a/src-tauri/src/cli.rs b/src-tauri/src/cli.rs index ea91820..f37d085 100644 --- a/src-tauri/src/cli.rs +++ b/src-tauri/src/cli.rs @@ -1,7 +1,6 @@ use std::ffi::OsString; use std::process::Command as ChildCommand; -#[cfg(unix)] -use std::os::unix::process::CommandExt; +use std::time::Duration; use clap::{ Command, @@ -9,16 +8,24 @@ use clap::{ ArgMatches, ArgAction }; -use tokio::{ - net::TcpStream, - io::{AsyncReadExt, AsyncWriteExt}, +use tokio::io::{AsyncReadExt, AsyncWriteExt}; + +use crate::credentials::Credentials; +use crate::errors::*; +use crate::server::{Request, Response}; + +#[cfg(unix)] +use { + std::os::unix::process::CommandExt, + std::path::Path, + tokio::net::UnixStream, }; - -use crate::app; -use crate::config::AppConfig; -use crate::credentials::{BaseCredentials, SessionCredentials}; -use crate::errors::*; +#[cfg(windows)] +use { + tokio::net::windows::named_pipe::{NamedPipeClient, ClientOptions}, + windows::Win32::Foundation::ERROR_PIPE_BUSY, +}; pub fn parser() -> Command<'static> { @@ -30,8 +37,8 @@ pub fn parser() -> Command<'static> { .about("Launch Creddy") ) .subcommand( - Command::new("show") - .about("Fetch and display AWS credentials") + Command::new("get") + .about("Request AWS credentials from Creddy and output to stdout") .arg( Arg::new("base") .short('b') @@ -59,10 +66,13 @@ pub fn parser() -> Command<'static> { } -pub fn show(args: &ArgMatches) -> Result<(), CliError> { +pub fn get(args: &ArgMatches) -> Result<(), CliError> { let base = args.get_one("base").unwrap_or(&false); - let creds = get_credentials(*base)?; - println!("{creds}"); + let output = match get_credentials(*base)? { + Credentials::Base(creds) => serde_json::to_string(&creds).unwrap(), + Credentials::Session(creds) => serde_json::to_string(&creds).unwrap(), + }; + println!("{output}"); Ok(()) } @@ -76,18 +86,16 @@ pub fn exec(args: &ArgMatches) -> Result<(), CliError> { 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); + match get_credentials(base)? { + Credentials::Base(creds) => { + cmd.env("AWS_ACCESS_KEY_ID", creds.access_key_id); + cmd.env("AWS_SECRET_ACCESS_KEY", creds.secret_access_key); + }, + Credentials::Session(creds) => { + 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.session_token); + } } #[cfg(unix)] @@ -122,40 +130,44 @@ pub fn exec(args: &ArgMatches) -> Result<(), CliError> { #[tokio::main] -async fn get_credentials(base: bool) -> Result { - let pool = app::connect_db().await?; - let config = AppConfig::load(&pool).await?; - let path = if base {"/creddy/base-credentials"} else {"/"}; +async fn get_credentials(base: bool) -> Result { + let req = Request::GetAwsCredentials { base }; + let mut data = serde_json::to_string(&req).unwrap(); + // server expects newline marking end of request + data.push('\n'); - 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?; + let mut stream = connect().await?; + stream.write_all(&data.as_bytes()).await?; - // some day we'll have a proper HTTP parser - let mut buf = vec![0; 8192]; + let mut buf = Vec::with_capacity(1024); 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 res: Result = serde_json::from_slice(&buf)?; + match res { + Ok(Response::Aws(creds)) => Ok(creds), + // Eventually we will want this + // Ok(r) => Err(RequestError::Unexpected(r)), + Err(e) => Err(RequestError::Server(e)), } - - 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) +} + + +#[cfg(windows)] +async fn connect() -> Result { + // apparently attempting to connect can fail if there's already a client connected + loop { + match ClientOptions::new().open(r"\\.\pipe\creddy-requests") { + Ok(stream) => return Ok(stream), + Err(e) if e.raw_os_error() == Some(ERROR_PIPE_BUSY.0 as i32) => (), + Err(e) => return Err(e), + } + tokio::time::sleep(Duration::from_millis(10)).await; + } +} + + +#[cfg(unix)] +async fn connect() -> Result { + let path = Path::from("/tmp/creddy-requests"); + std::fs::remove_file(path)?; + UnixStream::connect(path) } diff --git a/src-tauri/src/clientinfo.rs b/src-tauri/src/clientinfo.rs index f8d4b6f..518809e 100644 --- a/src-tauri/src/clientinfo.rs +++ b/src-tauri/src/clientinfo.rs @@ -1,76 +1,122 @@ -use std::path::PathBuf; +use std::path::{Path, PathBuf}; -use netstat2::{AddressFamilyFlags, ProtocolFlags, ProtocolSocketInfo}; -use tauri::Manager; use sysinfo::{System, SystemExt, Pid, PidExt, ProcessExt}; use serde::{Serialize, Deserialize}; +use std::os::windows::io::AsRawHandle; -use crate::{ - app::APP, - errors::*, - config::AppConfig, - state::AppState, +#[cfg(windows)] +use { + tokio::net::windows::named_pipe::NamedPipeServer, + windows::Win32::{ + Foundation::HANDLE, + System::Pipes::GetNamedPipeClientProcessId, + }, }; +#[cfg(unix)] +use tokio::net::UnixStream; + +use crate::errors::*; + #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Hash)] pub struct Client { pub pid: u32, - pub exe: PathBuf, + pub exe: Option, } -async fn get_associated_pids(local_port: u16) -> Result, netstat2::error::Error> { - let state = APP.get().unwrap().state::(); - let AppConfig { - listen_addr: app_listen_addr, - listen_port: app_listen_port, - .. - } = *state.config.read().await; - - let sockets_iter = netstat2::iterate_sockets_info( - AddressFamilyFlags::IPV4, - ProtocolFlags::TCP - )?; - for item in sockets_iter { - let sock_info = item?; - let proto_info = match sock_info.protocol_socket_info { - ProtocolSocketInfo::Tcp(tcp_info) => tcp_info, - ProtocolSocketInfo::Udp(_) => {continue;} - }; - - if proto_info.local_port == local_port - && proto_info.remote_port == app_listen_port - && proto_info.local_addr == app_listen_addr - && proto_info.remote_addr == app_listen_addr - { - return Ok(sock_info.associated_pids) - } - } - Ok(vec![]) +#[cfg(unix)] +pub fn get_client_parent(stream: &UnixStream) -> Result { + let pid = stream.peer_cred()?; + get_process_parent_info(pid)? } +#[cfg(windows)] +pub fn get_client_parent(stream: &NamedPipeServer) -> Result { + let raw_handle = stream.as_raw_handle(); + let mut pid = 0u32; + let handle = HANDLE(raw_handle as _); + unsafe { GetNamedPipeClientProcessId(handle, &mut pid as *mut u32)? }; + + get_process_parent_info(pid) +} + + +fn get_process_parent_info(pid: u32) -> Result { + let sys_pid = Pid::from_u32(pid); + let mut sys = System::new(); + sys.refresh_process(sys_pid); + let proc = sys.process(sys_pid) + .ok_or(ClientInfoError::ProcessNotFound)?; + + let parent_pid_sys = proc.parent() + .ok_or(ClientInfoError::ParentPidNotFound)?; + sys.refresh_process(parent_pid_sys); + let parent = sys.process(parent_pid_sys) + .ok_or(ClientInfoError::ParentProcessNotFound)?; + + let exe = match parent.exe() { + p if p == Path::new("") => None, + p => Some(PathBuf::from(p)), + }; + + Ok(Client { pid: parent_pid_sys.as_u32(), exe }) +} + + +// async fn get_associated_pids(local_port: u16) -> Result, netstat2::error::Error> { +// let state = APP.get().unwrap().state::(); +// let AppConfig { +// listen_addr: app_listen_addr, +// listen_port: app_listen_port, +// .. +// } = *state.config.read().await; + +// let sockets_iter = netstat2::iterate_sockets_info( +// AddressFamilyFlags::IPV4, +// ProtocolFlags::TCP +// )?; +// for item in sockets_iter { +// let sock_info = item?; +// let proto_info = match sock_info.protocol_socket_info { +// ProtocolSocketInfo::Tcp(tcp_info) => tcp_info, +// ProtocolSocketInfo::Udp(_) => {continue;} +// }; + +// if proto_info.local_port == local_port +// && proto_info.remote_port == app_listen_port +// && proto_info.local_addr == app_listen_addr +// && proto_info.remote_addr == app_listen_addr +// { +// return Ok(sock_info.associated_pids) +// } +// } +// Ok(vec![]) +// } + + // Theoretically, on some systems, multiple processes can share a socket -pub async fn get_clients(local_port: u16) -> Result>, ClientInfoError> { - let mut clients = Vec::new(); - let mut sys = System::new(); - for p in get_associated_pids(local_port).await? { - let pid = Pid::from_u32(p); - sys.refresh_process(pid); - let proc = sys.process(pid) - .ok_or(ClientInfoError::ProcessNotFound)?; +// pub async fn get_clients(local_port: u16) -> Result>, ClientInfoError> { +// let mut clients = Vec::new(); +// let mut sys = System::new(); +// for p in get_associated_pids(local_port).await? { +// let pid = Pid::from_u32(p); +// sys.refresh_process(pid); +// let proc = sys.process(pid) +// .ok_or(ClientInfoError::ProcessNotFound)?; - let client = Client { - pid: p, - exe: proc.exe().to_path_buf(), - }; - clients.push(Some(client)); - } +// let client = Client { +// pid: p, +// exe: proc.exe().to_path_buf(), +// }; +// clients.push(Some(client)); +// } - if clients.is_empty() { - clients.push(None); - } +// if clients.is_empty() { +// clients.push(None); +// } - Ok(clients) -} +// Ok(clients) +// } diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index 37e2891..c3d706b 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -1,4 +1,3 @@ -use std::net::Ipv4Addr; use std::path::PathBuf; use auto_launch::AutoLaunchBuilder; @@ -42,10 +41,6 @@ pub struct HotkeysConfig { #[derive(Clone, Debug, Serialize, Deserialize)] pub struct AppConfig { - #[serde(default = "default_listen_addr")] - pub listen_addr: Ipv4Addr, - #[serde(default = "default_listen_port")] - pub listen_port: u16, #[serde(default = "default_rehide_ms")] pub rehide_ms: u64, #[serde(default = "default_start_minimized")] @@ -62,8 +57,6 @@ pub struct AppConfig { impl Default for AppConfig { fn default() -> Self { AppConfig { - listen_addr: default_listen_addr(), - listen_port: default_listen_port(), rehide_ms: default_rehide_ms(), start_minimized: default_start_minimized(), start_on_login: default_start_on_login(), @@ -144,16 +137,6 @@ pub fn get_or_create_db_path() -> Result { } -fn default_listen_port() -> u16 { - if cfg!(debug_assertions) { - 12_345 - } - else { - 19_923 - } -} - - fn default_term_config() -> TermConfig { #[cfg(windows)] { @@ -238,7 +221,6 @@ pub fn register_hotkeys(hotkeys: &HotkeysConfig) -> tauri::Result<()> { } -fn default_listen_addr() -> Ipv4Addr { Ipv4Addr::LOCALHOST } fn default_rehide_ms() -> u64 { 1000 } // start minimized and on login only in production mode fn default_start_minimized() -> bool { !cfg!(debug_assertions) } diff --git a/src-tauri/src/credentials.rs b/src-tauri/src/credentials.rs index 91b1588..c54c516 100644 --- a/src-tauri/src/credentials.rs +++ b/src-tauri/src/credentials.rs @@ -162,9 +162,10 @@ impl BaseCredentials { #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "PascalCase")] pub struct SessionCredentials { + pub version: usize, pub access_key_id: String, pub secret_access_key: String, - pub token: String, + pub session_token: String, #[serde(serialize_with = "serialize_expiration")] #[serde(deserialize_with = "deserialize_expiration")] pub expiration: DateTime, @@ -198,7 +199,7 @@ impl SessionCredentials { let secret_access_key = aws_session.secret_access_key() .ok_or(GetSessionError::EmptyResponse)? .to_string(); - let token = aws_session.session_token() + let session_token = aws_session.session_token() .ok_or(GetSessionError::EmptyResponse)? .to_string(); let expiration = aws_session.expiration() @@ -206,9 +207,10 @@ impl SessionCredentials { .clone(); let session_creds = SessionCredentials { + version: 1, access_key_id, secret_access_key, - token, + session_token, expiration, }; @@ -230,6 +232,14 @@ impl SessionCredentials { } } + +#[derive(Debug, Serialize, Deserialize)] +pub enum Credentials { + Base(BaseCredentials), + Session(SessionCredentials), +} + + fn serialize_expiration(exp: &DateTime, serializer: S) -> Result where S: Serializer { diff --git a/src-tauri/src/errors.rs b/src-tauri/src/errors.rs index f56a2df..a36ca09 100644 --- a/src-tauri/src/errors.rs +++ b/src-tauri/src/errors.rs @@ -2,6 +2,7 @@ use std::error::Error; use std::convert::AsRef; use std::ffi::OsString; use std::sync::mpsc; +use std::string::FromUtf8Error; use strum_macros::AsRefStr; use thiserror::Error as ThisError; @@ -17,7 +18,12 @@ use tauri::api::dialog::{ MessageDialogBuilder, MessageDialogKind, }; -use serde::{Serialize, Serializer, ser::SerializeMap}; +use serde::{ + Serialize, + Serializer, + ser::SerializeMap, + Deserialize, +}; pub trait ErrorPopup { @@ -137,12 +143,14 @@ pub enum SendResponseError { pub enum HandlerError { #[error("Error writing to stream: {0}")] StreamIOError(#[from] std::io::Error), - // #[error("Received invalid UTF-8 in request")] - // InvalidUtf8, + #[error("Received invalid UTF-8 in request")] + InvalidUtf8(#[from] FromUtf8Error), #[error("HTTP request malformed")] - BadRequest(Vec), + BadRequest(#[from] serde_json::Error), #[error("HTTP request too large")] RequestTooLarge, + #[error("Internal server error")] + Internal, #[error("Error accessing credentials: {0}")] NoCredentials(#[from] GetCredentialsError), #[error("Error getting client details: {0}")] @@ -151,6 +159,8 @@ pub enum HandlerError { Tauri(#[from] tauri::Error), #[error("No main application window found")] NoMainWindow, + #[error("Request was denied")] + Denied, } @@ -207,26 +217,49 @@ pub enum CryptoError { pub enum ClientInfoError { #[error("Found PID for client socket, but no corresponding process")] ProcessNotFound, - #[error("Couldn't get client socket details: {0}")] - NetstatError(#[from] netstat2::error::Error), + #[error("Could not determine parent PID of connected client")] + ParentPidNotFound, + #[error("Found PID for parent process of client, but no corresponding process")] + ParentProcessNotFound, + #[error("Could not determine PID of connected client")] + WindowsError(#[from] windows::core::Error), + #[error(transparent)] + Io(#[from] std::io::Error), +} + + +// Technically also an error, but formatted as a struct for easy deserialization +#[derive(Debug, Serialize, Deserialize)] +pub struct ServerError { + code: String, + msg: String, +} + +impl std::fmt::Display for ServerError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + write!(f, "{} ({})", self.msg, self.code)?; + Ok(()) + } } // 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("Error response from server: {0}")] + Server(ServerError), + #[error("Unexpected response from server")] + Unexpected(crate::server::Response), #[error("The server did not respond with valid JSON")] - InvalidJson, + InvalidJson(#[from] serde_json::Error), #[error("Error reading/writing stream: {0}")] StreamIOError(#[from] std::io::Error), - #[error("Error loading configuration data: {0}")] - Setup(#[from] SetupError), +} + +impl From for RequestError { + fn from(s: ServerError) -> Self { + Self::Server(s) + } } @@ -298,13 +331,6 @@ impl Serialize for HandlerError { let mut map = serializer.serialize_map(None)?; map.serialize_entry("code", self.as_ref())?; map.serialize_entry("msg", &format!("{self}"))?; - - match self { - HandlerError::NoCredentials(src) => map.serialize_entry("source", &src)?, - HandlerError::ClientInfo(src) => map.serialize_entry("source", &src)?, - _ => serialize_upstream_err(self, &mut map)?, - } - map.end() } } @@ -353,6 +379,8 @@ impl Serialize for UnlockError { match self { UnlockError::GetSession(src) => map.serialize_entry("source", &src)?, + // The string representation of the AEAD error is not very helpful, so skip it + UnlockError::Crypto(_src) => map.serialize_entry("source", &None::<&str>)?, _ => serialize_upstream_err(self, &mut map)?, } map.end() diff --git a/src-tauri/src/ipc.rs b/src-tauri/src/ipc.rs index 0b9428d..03e487a 100644 --- a/src-tauri/src/ipc.rs +++ b/src-tauri/src/ipc.rs @@ -10,9 +10,9 @@ use crate::terminal; #[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Request { +pub struct AwsRequestNotification { pub id: u64, - pub clients: Vec>, + pub client: Client, pub base: bool, } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 982367e..547992f 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -16,12 +16,13 @@ fn main() { app::run().error_popup("Creddy failed to start"); Ok(()) }, - Some(("show", m)) => cli::show(m), + Some(("get", m)) => cli::get(m), Some(("exec", m)) => cli::exec(m), _ => unreachable!(), }; if let Err(e) = res { eprintln!("Error: {e}"); + std::process::exit(1); } } diff --git a/src-tauri/src/server.rs b/src-tauri/src/server.rs index 241afcf..461b84f 100644 --- a/src-tauri/src/server.rs +++ b/src-tauri/src/server.rs @@ -1,275 +1,184 @@ -use core::time::Duration; -use std::io; -use std::net::{ - Ipv4Addr, - SocketAddr, - SocketAddrV4, -}; -use tokio::net::{ - TcpListener, - TcpStream, +use std::time::Duration; + +#[cfg(windows)] +use tokio::net::windows::named_pipe::{ + NamedPipeServer, + ServerOptions, }; use tokio::io::{AsyncReadExt, AsyncWriteExt}; -use tokio::sync::oneshot::{self, Sender, Receiver}; -use tokio::time::sleep; +use tokio::sync::oneshot; -use tauri::{AppHandle, Manager}; -use tauri::async_runtime as rt; -use tauri::async_runtime::JoinHandle; +use serde::{Serialize, Deserialize}; + +use tauri::{ + AppHandle, + Manager, + async_runtime as rt, +}; -use crate::{clientinfo, clientinfo::Client}; use crate::errors::*; -use crate::ipc::{Request, Approval}; +use crate::clientinfo::{self, Client}; +use crate::credentials::Credentials; +use crate::ipc::{Approval, AwsRequestNotification}; use crate::state::AppState; -#[derive(Debug)] -pub struct RequestWaiter { - pub rehide_after: bool, - pub sender: Option>, -} - -impl RequestWaiter { - pub fn notify(&mut self, approval: Approval) -> Result<(), SendResponseError> { - let chan = self.sender - .take() - .ok_or(SendResponseError::Fulfilled)?; - - chan.send(approval) - .map_err(|_| SendResponseError::Abandoned) - } +#[derive(Serialize, Deserialize)] +pub enum Request { + GetAwsCredentials{ + base: bool, + }, } -struct Handler { - request_id: u64, - stream: TcpStream, - rehide_after: bool, - receiver: Option>, - app: AppHandle, -} - -impl Handler { - async fn new(stream: TcpStream, app: AppHandle) -> Result { - let state = app.state::(); - - // determine whether we should re-hide the window after handling this request - let is_currently_visible = app.get_window("main") - .ok_or(HandlerError::NoMainWindow)? - .is_visible()?; - let rehide_after = state.current_rehide_status() - .await - .unwrap_or(!is_currently_visible); - - let (chan_send, chan_recv) = oneshot::channel(); - let waiter = RequestWaiter {rehide_after, sender: Some(chan_send)}; - let request_id = state.register_request(waiter).await; - let handler = Handler { - request_id, - stream, - rehide_after, - receiver: Some(chan_recv), - app - }; - Ok(handler) - } - - async fn handle(mut self) { - if let Err(e) = self.try_handle().await { - eprintln!("{e}"); - } - let state = self.app.state::(); - state.unregister_request(self.request_id).await; - } - - async fn try_handle(&mut self) -> Result<(), HandlerError> { - let req_path = self.recv_request().await?; - let clients = self.get_clients().await?; - if self.includes_banned(&clients).await { - self.stream.write(b"HTTP/1.0 403 Access Denied\r\n\r\n").await?; - return Ok(()) - } - let base = req_path == b"/creddy/base-credentials"; - - let req = Request {id: self.request_id, clients, base}; - self.app.emit_all("credentials-request", &req)?; - self.show_window()?; - - match self.wait_for_response().await? { - Approval::Approved => { - let state = self.app.state::(); - let creds = if base { - state.serialize_base_creds().await? - } - else { - state.serialize_session_creds().await? - }; - self.send_body(creds.as_bytes()).await?; - }, - Approval::Denied => { - let state = self.app.state::(); - for client in req.clients { - state.add_ban(client).await; - } - self.send_body(b"Denied!").await?; - self.stream.shutdown().await?; - } - } - - // only hide the window if a) it was hidden to start with - // and b) there are no other pending requests - let state = self.app.state::(); - let delay = { - let config = state.config.read().await; - Duration::from_millis(config.rehide_ms) - }; - sleep(delay).await; - - if self.rehide_after && state.req_count().await == 1 { - self.app - .get_window("main") - .ok_or(HandlerError::NoMainWindow)? - .hide()?; - } - - Ok(()) - } - - async fn recv_request(&mut self) -> Result, HandlerError> { - let mut buf = vec![0; 8192]; // it's what tokio's BufReader uses - let mut n = 0; - loop { - n += self.stream.read(&mut buf[n..]).await?; - if n >= 4 && &buf[(n - 4)..n] == b"\r\n\r\n" {break;} - if n == buf.len() {return Err(HandlerError::RequestTooLarge);} - } - - let path = buf.split(|&c| &[c] == b" ") - .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()); - } - - Ok(path.into()) - } - - async fn get_clients(&self) -> Result>, HandlerError> { - let peer_addr = match self.stream.peer_addr()? { - SocketAddr::V4(addr) => addr, - _ => unreachable!(), // we only listen on IPv4 - }; - let clients = clientinfo::get_clients(peer_addr.port()).await?; - Ok(clients) - } - - async fn includes_banned(&self, clients: &Vec>) -> bool { - let state = self.app.state::(); - for client in clients { - if state.is_banned(client).await { - return true; - } - } - false - } - - fn show_window(&self) -> Result<(), HandlerError> { - let window = self.app.get_window("main").ok_or(HandlerError::NoMainWindow)?; - if !window.is_visible()? { - window.unminimize()?; - window.show()?; - } - window.set_focus()?; - Ok(()) - } - - async fn wait_for_response(&mut self) -> Result { - 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"X-Creddy-delaying-tactic: ").await?; - - #[allow(unreachable_code)] // seems necessary for type inference - let stall = async { - let delay = std::time::Duration::from_secs(1); - loop { - tokio::time::sleep(delay).await; - self.stream.write(b"x").await?; - } - Ok(Approval::Denied) - }; - - // this is the only place we even read this field, so it's safe to unwrap - let receiver = self.receiver.take().unwrap(); - tokio::select!{ - r = receiver => Ok(r.unwrap()), // only panics if the sender is dropped without sending, which shouldn't be possible - e = stall => e, - } - } - - async fn send_body(&mut self, body: &[u8]) -> Result<(), HandlerError> { - self.stream.write(b"\r\nContent-Length: ").await?; - self.stream.write(body.len().to_string().as_bytes()).await?; - self.stream.write(b"\r\n\r\n").await?; - self.stream.write(body).await?; - self.stream.shutdown().await?; - Ok(()) - } +#[derive(Debug, Serialize, Deserialize)] +pub enum Response { + Aws(Credentials) } -#[derive(Debug)] pub struct Server { - addr: Ipv4Addr, - port: u16, + listener: tokio::net::windows::named_pipe::NamedPipeServer, app_handle: AppHandle, - task: JoinHandle<()>, } - impl Server { - pub async fn new(addr: Ipv4Addr, port: u16, app_handle: AppHandle) -> io::Result { - let task = Self::start_server(addr, port, app_handle.app_handle()).await?; - Ok(Server { addr, port, app_handle, task}) - } + pub fn start(app_handle: AppHandle) -> std::io::Result<()> { + let listener = ServerOptions::new() + .first_pipe_instance(true) + .create(r"\\.\pipe\creddy-requests")?; - 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; + let srv = Server {listener, app_handle}; + rt::spawn(srv.serve()); 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> { - 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) { + async fn serve(mut self) { loop { - match listener.accept().await { - Ok((stream, _)) => { - match Handler::new(stream, app_handle.app_handle()).await { - Ok(handler) => { rt::spawn(handler.handle()); } - Err(e) => { eprintln!("Error handling request: {e}"); } - } - }, - Err(e) => { eprintln!("Error accepting connection: {e}"); } + if let Err(e) = self.try_serve().await { + eprintln!("Error accepting connection: {e}"); } } } + + async fn try_serve(&mut self) -> std::io::Result<()> { + // connect() just waits for a client to connect, it doesn't return anything + self.listener.connect().await?; + + // create a new pipe instance to listen for the next client, and swap it in + let new_listener = ServerOptions::new().create(r"\\.\pipe\creddy-requests")?; + let mut stream = std::mem::replace(&mut self.listener, new_listener); + let new_handle = self.app_handle.app_handle(); + rt::spawn(async move { + let res = serde_json::to_string( + &handle(&mut stream, new_handle).await + ).unwrap(); + if let Err(e) = stream.write_all(res.as_bytes()).await { + eprintln!("Error responding to request: {e}"); + } + }); + + Ok(()) + } +} + + +async fn handle(stream: &mut NamedPipeServer, app_handle: AppHandle) -> Result { + // read from stream until delimiter is reached + let mut buf: Vec = Vec::with_capacity(1024); // requests are small, 1KiB is more than enough + let mut n = 0; + loop { + n += stream.read_buf(&mut buf).await?; + if let Some(&b'\n') = buf.last() { + break; + } + else if n >= 1024 { + return Err(HandlerError::RequestTooLarge); + } + } + + let client = clientinfo::get_client_parent(&stream)?; + + let req: Request = serde_json::from_slice(&buf)?; + match req { + Request::GetAwsCredentials{ base } => get_aws_credentials(base, client, app_handle).await, + // etc + } +} + + +async fn get_aws_credentials(base: bool, client: Client, app_handle: AppHandle) -> Result { + let state = app_handle.state::(); + + let main_window = app_handle.get_window("main").ok_or(HandlerError::NoMainWindow)?; + let is_currently_visible = main_window.is_visible()?; + let rehide_after = state.get_or_set_rehide(!is_currently_visible).await; + + let (chan_send, chan_recv) = oneshot::channel(); + let request_id = state.register_request(chan_send).await; + + // if an error occurs in any of the following, we want to abort the operation + // but ? returns immediately, and we want to unregister the request before returning + // so we bundle it all up in an async block and return a Result so we can handle errors + let proceed = async { + let notification = AwsRequestNotification {id: request_id, client, base}; + app_handle.emit_all("credentials-request", ¬ification)?; + + if !main_window.is_visible()? { + main_window.unminimize()?; + main_window.show()?; + } + main_window.set_focus()?; + + match chan_recv.await { + Ok(Approval::Approved) => { + if base { + let creds = state.base_creds_cloned().await?; + Ok(Response::Aws(Credentials::Base(creds))) + } + else { + let creds = state.session_creds_cloned().await?; + Ok(Response::Aws(Credentials::Session(creds))) + } + }, + Ok(Approval::Denied) => Err(HandlerError::Denied), + Err(_e) => Err(HandlerError::Internal), + } + }; + + let result = match proceed.await { + Ok(r) => Ok(r), + Err(e) => { + state.unregister_request(request_id).await; + Err(e) + } + }; + + rt::spawn( + handle_rehide(rehide_after, app_handle.app_handle()) + ); + result +} + + +async fn handle_rehide(rehide_after: bool, app_handle: AppHandle) { + let state = app_handle.state::(); + let delay = { + let config = state.config.read().await; + Duration::from_millis(config.rehide_ms) + }; + tokio::time::sleep(delay).await; + + // if there are no other pending requests, set rehide status back to None + if state.req_count().await == 0 { + state.clear_rehide().await; + // and hide the window if necessary + if rehide_after { + app_handle.get_window("main").map(|w| { + if let Err(e) = w.hide() { + eprintln!("{e}"); + } + }); + } + } } diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs index eda9a81..7cd266c 100644 --- a/src-tauri/src/state.rs +++ b/src-tauri/src/state.rs @@ -1,15 +1,11 @@ -use std::collections::{HashMap, HashSet}; -use std::time::Duration; +use std::collections::HashMap; use tokio::{ sync::RwLock, - time::sleep, + sync::oneshot::Sender, }; use sqlx::SqlitePool; -use tauri::async_runtime as runtime; -use tauri::Manager; -use crate::app::APP; use crate::credentials::{ Session, BaseCredentials, @@ -17,9 +13,7 @@ use crate::credentials::{ }; use crate::{config, config::AppConfig}; use crate::ipc::{self, Approval}; -use crate::clientinfo::Client; use crate::errors::*; -use crate::server::{Server, RequestWaiter}; #[derive(Debug)] @@ -27,12 +21,11 @@ pub struct AppState { pub config: RwLock, pub session: RwLock, pub request_count: RwLock, - pub waiting_requests: RwLock>, + pub waiting_requests: RwLock>>, + pub current_rehide_status: RwLock>, pub pending_terminal_request: RwLock, - pub bans: RwLock>>, // setup_errors is never modified and so doesn't need to be wrapped in RwLock pub setup_errors: Vec, - server: RwLock, pool: sqlx::SqlitePool, } @@ -40,7 +33,6 @@ impl AppState { pub fn new( config: AppConfig, session: Session, - server: Server, pool: SqlitePool, setup_errors: Vec, ) -> AppState { @@ -49,10 +41,9 @@ impl AppState { session: RwLock::new(session), request_count: RwLock::new(0), waiting_requests: RwLock::new(HashMap::new()), + current_rehide_status: RwLock::new(None), pending_terminal_request: RwLock::new(false), - bans: RwLock::new(HashSet::new()), setup_errors, - server: RwLock::new(server), pool, } } @@ -73,13 +64,7 @@ impl AppState { if new_config.start_on_login != live_config.start_on_login { config::set_auto_launch(new_config.start_on_login)?; } - // rebind socket if necessary - 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?; - } + // re-register hotkeys if necessary if new_config.hotkeys.show_window != live_config.hotkeys.show_window || new_config.hotkeys.launch_terminal != live_config.hotkeys.launch_terminal @@ -92,7 +77,7 @@ impl AppState { Ok(()) } - pub async fn register_request(&self, waiter: RequestWaiter) -> u64 { + pub async fn register_request(&self, sender: Sender) -> u64 { let count = { let mut c = self.request_count.write().await; *c += 1; @@ -100,7 +85,7 @@ impl AppState { }; let mut waiting_requests = self.waiting_requests.write().await; - waiting_requests.insert(*count, waiter); // `count` is the request id + waiting_requests.insert(*count, sender); // `count` is the request id *count } @@ -114,11 +99,20 @@ impl AppState { waiting_requests.len() } - pub async fn current_rehide_status(&self) -> Option { - // since all requests that are pending at a given time should have the same - // value for rehide_after, it doesn't matter which one we use - let waiting_requests = self.waiting_requests.read().await; - waiting_requests.iter().next().map(|(_id, w)| w.rehide_after) + pub async fn get_or_set_rehide(&self, new_value: bool) -> bool { + let mut rehide = self.current_rehide_status.write().await; + match *rehide { + Some(original) => original, + None => { + *rehide = Some(new_value); + new_value + } + } + } + + pub async fn clear_rehide(&self) { + let mut rehide = self.current_rehide_status.write().await; + *rehide = None; } pub async fn send_response(&self, response: ipc::RequestResponse) -> Result<(), SendResponseError> { @@ -129,26 +123,10 @@ impl AppState { let mut waiting_requests = self.waiting_requests.write().await; waiting_requests - .get_mut(&response.id) + .remove(&response.id) .ok_or(SendResponseError::NotFound)? - .notify(response.approval) - } - - pub async fn add_ban(&self, client: Option) { - let mut bans = self.bans.write().await; - bans.insert(client.clone()); - - runtime::spawn(async move { - sleep(Duration::from_secs(5)).await; - let app = APP.get().unwrap(); - let state = app.state::(); - let mut bans = state.bans.write().await; - bans.remove(&client); - }); - } - - pub async fn is_banned(&self, client: &Option) -> bool { - self.bans.read().await.contains(&client) + .send(response.approval) + .map_err(|_| SendResponseError::Abandoned) } pub async fn unlock(&self, passphrase: &str) -> Result<(), UnlockError> { @@ -168,16 +146,16 @@ impl AppState { matches!(*session, Session::Unlocked{..}) } - pub async fn serialize_base_creds(&self) -> Result { + pub async fn base_creds_cloned(&self) -> Result { let app_session = self.session.read().await; let (base, _session) = app_session.try_get()?; - Ok(serde_json::to_string(base).unwrap()) + Ok(base.clone()) } - pub async fn serialize_session_creds(&self) -> Result { + pub async fn session_creds_cloned(&self) -> Result { let app_session = self.session.read().await; let (_bsae, session) = app_session.try_get()?; - Ok(serde_json::to_string(session).unwrap()) + Ok(session.clone()) } async fn new_session(&self, base: BaseCredentials) -> Result<(), GetSessionError> { diff --git a/src-tauri/src/terminal.rs b/src-tauri/src/terminal.rs index fcac4db..fa95736 100644 --- a/src-tauri/src/terminal.rs +++ b/src-tauri/src/terminal.rs @@ -63,7 +63,7 @@ pub async fn launch(use_base: bool) -> Result<(), LaunchTerminalError> { else { cmd.env("AWS_ACCESS_KEY_ID", &session_creds.access_key_id); cmd.env("AWS_SECRET_ACCESS_KEY", &session_creds.secret_access_key); - cmd.env("AWS_SESSION_TOKEN", &session_creds.token); + cmd.env("AWS_SESSION_TOKEN", &session_creds.session_token); } } diff --git a/src/views/Approve.svelte b/src/views/Approve.svelte index d07e874..6ccc86c 100644 --- a/src/views/Approve.svelte +++ b/src/views/Approve.svelte @@ -47,16 +47,13 @@ } // Extract executable name from full path - let appName = null; - if ($appState.currentRequest.clients.length === 1) { - let path = $appState.currentRequest.clients[0].exe; - let m = path.match(/\/([^/]+?$)|\\([^\\]+?$)/); - appName = m[1] || m[2]; - } + const client = $appState.currentRequest.client; + const m = client.exe?.match(/\/([^/]+?$)|\\([^\\]+?$)/); + const 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'); + function breakPath(path) { + return path.replace(/(\\|\/)/g, '$1'); } // if the request has already been approved/denied, send response immediately @@ -97,12 +94,10 @@

{appName ? `"${appName}"` : 'An appplication'} would like to access your AWS credentials.

- {#each $appState.currentRequest.clients as client} -
Path:
- {@html client ? breakPath(client) : 'Unknown'} -
PID:
- {client ? client.pid : 'Unknown'} - {/each} +
Path:
+ {@html client.exe ? breakPath(client.exe) : 'Unknown'} +
PID:
+ {client.pid}
diff --git a/src/views/Settings.svelte b/src/views/Settings.svelte index 8411436..aaea8bf 100644 --- a/src/views/Settings.svelte +++ b/src/views/Settings.svelte @@ -58,18 +58,6 @@ - - - Listen for credentials requests on this port. - (Should be used with $AWS_CONTAINER_CREDENTIALS_FULL_URI) - - - From 47a3e1cfef589b96f4d4769c839a3e0084369168 Mon Sep 17 00:00:00 2001 From: Joseph Montanaro Date: Mon, 18 Sep 2023 20:13:56 -0700 Subject: [PATCH 2/7] start work on invoking shortcuts from CLI --- src-tauri/src/shortcuts.rs | 51 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 src-tauri/src/shortcuts.rs diff --git a/src-tauri/src/shortcuts.rs b/src-tauri/src/shortcuts.rs new file mode 100644 index 0000000..c40d758 --- /dev/null +++ b/src-tauri/src/shortcuts.rs @@ -0,0 +1,51 @@ +use serde::{Serialize, Deserialize}; + +use tauri::{ + AppHandle, + Manager, +}; + +use crate::app::APP; +use crate::config::HotkeysConfig; +use crate::terminal; + + +#[derive(Debug, Serialize, Deserialize)] +pub enum ShortcutAction { + ShowWindow, + LaunchTerminal, +} + + +pub fn exec_shortcut(action: ShortcutAction) { + match action { + ShowWindow => { + let app = APP.get().unwrap(); + app.get_window("main").map(|w| w.show()); + }, + LaunchTerminal => terminal::launch(false), + } +} + + +pub fn register_hotkeys(hotkeys: &HotkeysConfig) -> tauri::Result<()> { + let app = APP.get().unwrap(); + let mut manager = app.global_shortcut_manager(); + manager.unregister_all()?; + + if hotkeys.show_window.enabled { + manager.register( + hotkeys.show_window.keys, + || exec_shortcut(ShortcutAction::ShowWindow) + )?; + } + + if hotkeys.launch_terminal.enabled { + manager.register( + &hotkeys.launch_terminal.keys, + || exec_shortcut(ShortcutAction::LaunchTerminal) + )?; + } + + Ok(()) +} From 4b06dce7f4dac5ccf05d4954dd2cb0d40f8f5090 Mon Sep 17 00:00:00 2001 From: Joseph Montanaro Date: Thu, 21 Sep 2023 10:44:35 -0700 Subject: [PATCH 3/7] keep working on cli shortcuts, unify visibility management --- src-tauri/src/app.rs | 3 +- src-tauri/src/bin/creddy_cli.rs | 3 +- src-tauri/src/cli.rs | 56 ++++++++++++++---- src-tauri/src/errors.rs | 28 ++++++++- src-tauri/src/lib.rs | 1 + src-tauri/src/main.rs | 2 +- src-tauri/src/server.rs | 58 ++++++------------ src-tauri/src/shortcuts.rs | 19 ++++-- src-tauri/src/state.rs | 102 +++++++++++++++++++++++++------- src-tauri/src/terminal.rs | 10 +--- 10 files changed, 190 insertions(+), 92 deletions(-) diff --git a/src-tauri/src/app.rs b/src-tauri/src/app.rs index b5df31b..aeff75f 100644 --- a/src-tauri/src/app.rs +++ b/src-tauri/src/app.rs @@ -19,6 +19,7 @@ use crate::{ ipc, server::Server, errors::*, + shortcuts, state::AppState, tray, }; @@ -99,7 +100,7 @@ async fn setup(app: &mut App) -> Result<(), Box> { if let Err(_e) = config::set_auto_launch(conf.start_on_login) { setup_errors.push("Error: Failed to manage autolaunch.".into()); } - if let Err(e) = config::register_hotkeys(&conf.hotkeys) { + if let Err(e) = shortcuts::register_hotkeys(&conf.hotkeys) { setup_errors.push(format!("{e}")); } diff --git a/src-tauri/src/bin/creddy_cli.rs b/src-tauri/src/bin/creddy_cli.rs index 5a13637..59e4c4f 100644 --- a/src-tauri/src/bin/creddy_cli.rs +++ b/src-tauri/src/bin/creddy_cli.rs @@ -21,7 +21,8 @@ fn main() { None | Some(("run", _)) => launch_gui(), Some(("get", m)) => cli::get(m), Some(("exec", m)) => cli::exec(m), - _ => unreachable!(), + Some(("shortcut", m)) => cli::invoke_shortcut(m), + _ => unreachable!("Unknown subcommand"), }; if let Err(e) = res { diff --git a/src-tauri/src/cli.rs b/src-tauri/src/cli.rs index f37d085..c4b8b86 100644 --- a/src-tauri/src/cli.rs +++ b/src-tauri/src/cli.rs @@ -4,15 +4,17 @@ use std::time::Duration; use clap::{ Command, - Arg, - ArgMatches, - ArgAction + Arg, + ArgMatches, + ArgAction, + builder::PossibleValuesParser, }; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use crate::credentials::Credentials; use crate::errors::*; use crate::server::{Request, Response}; +use crate::shortcuts::ShortcutAction; #[cfg(unix)] use { @@ -63,6 +65,16 @@ pub fn parser() -> Command<'static> { .multiple_values(true) ) ) + .subcommand( + Command::new("shortcut") + .about("Invoke an action normally trigged by hotkey (e.g. launch terminal)") + .arg( + Arg::new("action") + .value_parser( + PossibleValuesParser::new(["show_window", "launch_terminal"]) + ) + ) + ) } @@ -129,10 +141,35 @@ pub fn exec(args: &ArgMatches) -> Result<(), CliError> { } -#[tokio::main] -async fn get_credentials(base: bool) -> Result { +pub fn invoke_shortcut(args: &ArgMatches) -> Result<(), CliError> { + let action = match args.get_one::("action").map(|s| s.as_str()) { + Some("show_window") => ShortcutAction::ShowWindow, + Some("launch_terminal") => ShortcutAction::LaunchTerminal, + Some(&_) | None => unreachable!("Unknown shortcut action"), // guaranteed by clap + }; + + let req = Request::InvokeShortcut(action); + match make_request(&req) { + Ok(Response::Empty) => Ok(()), + Ok(r) => Err(RequestError::Unexpected(r).into()), + Err(e) => Err(e.into()), + } +} + + +fn get_credentials(base: bool) -> Result { let req = Request::GetAwsCredentials { base }; - let mut data = serde_json::to_string(&req).unwrap(); + match make_request(&req) { + Ok(Response::Aws(creds)) => Ok(creds), + Ok(r) => Err(RequestError::Unexpected(r)), + Err(e) => Err(e), + } +} + + +#[tokio::main] +async fn make_request(req: &Request) -> Result { + let mut data = serde_json::to_string(req).unwrap(); // server expects newline marking end of request data.push('\n'); @@ -142,12 +179,7 @@ async fn get_credentials(base: bool) -> Result { let mut buf = Vec::with_capacity(1024); stream.read_to_end(&mut buf).await?; let res: Result = serde_json::from_slice(&buf)?; - match res { - Ok(Response::Aws(creds)) => Ok(creds), - // Eventually we will want this - // Ok(r) => Err(RequestError::Unexpected(r)), - Err(e) => Err(RequestError::Server(e)), - } + Ok(res?) } diff --git a/src-tauri/src/errors.rs b/src-tauri/src/errors.rs index a36ca09..537980d 100644 --- a/src-tauri/src/errors.rs +++ b/src-tauri/src/errors.rs @@ -26,12 +26,14 @@ use serde::{ }; -pub trait ErrorPopup { +pub trait ShowError { fn error_popup(self, title: &str); fn error_popup_nowait(self, title: &str); + fn error_print(self); + fn error_print_prefix(self, prefix: &str); } -impl ErrorPopup for Result<(), E> { +impl ShowError for Result<(), E> { fn error_popup(self, title: &str) { if let Err(e) = self { let (tx, rx) = mpsc::channel(); @@ -50,6 +52,18 @@ impl ErrorPopup for Result<(), E> { .show(|_| {}) } } + + fn error_print(self) { + if let Err(e) = self { + eprintln!("{e}"); + } + } + + fn error_print_prefix(self, prefix: &str) { + if let Err(e) = self { + eprintln!("{prefix}: {e}"); + } + } } @@ -164,6 +178,15 @@ pub enum HandlerError { } +#[derive(Debug, ThisError, AsRefStr)] +pub enum WindowError { + #[error("Failed to find main application window")] + NoMainWindow, + #[error(transparent)] + ManageFailure(#[from] tauri::Error), +} + + #[derive(Debug, ThisError, AsRefStr)] pub enum GetCredentialsError { #[error("Credentials are currently locked")] @@ -324,6 +347,7 @@ impl Serialize for SerializeWrapper<&GetSessionTokenError> { impl_serialize_basic!(SetupError); impl_serialize_basic!(GetCredentialsError); impl_serialize_basic!(ClientInfoError); +impl_serialize_basic!(WindowError); impl Serialize for HandlerError { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 2c1fbeb..70c2e98 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -7,5 +7,6 @@ mod clientinfo; mod ipc; mod state; mod server; +mod shortcuts; mod terminal; mod tray; diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 547992f..6b6f9ba 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -6,7 +6,7 @@ use creddy::{ app, cli, - errors::ErrorPopup, + errors::ShowError, }; diff --git a/src-tauri/src/server.rs b/src-tauri/src/server.rs index 461b84f..0afa5c6 100644 --- a/src-tauri/src/server.rs +++ b/src-tauri/src/server.rs @@ -1,5 +1,3 @@ -use std::time::Duration; - #[cfg(windows)] use tokio::net::windows::named_pipe::{ NamedPipeServer, @@ -21,6 +19,7 @@ use crate::clientinfo::{self, Client}; use crate::credentials::Credentials; use crate::ipc::{Approval, AwsRequestNotification}; use crate::state::AppState; +use crate::shortcuts::{self, ShortcutAction}; #[derive(Serialize, Deserialize)] @@ -28,12 +27,14 @@ pub enum Request { GetAwsCredentials{ base: bool, }, + InvokeShortcut(ShortcutAction), } #[derive(Debug, Serialize, Deserialize)] pub enum Response { - Aws(Credentials) + Aws(Credentials), + Empty, } @@ -102,17 +103,25 @@ async fn handle(stream: &mut NamedPipeServer, app_handle: AppHandle) -> Result get_aws_credentials(base, client, app_handle).await, - // etc + Request::InvokeShortcut(action) => invoke_shortcut(action).await, } } +async fn invoke_shortcut(action: ShortcutAction) -> Result { + shortcuts::exec_shortcut(action); + Ok(Response::Empty) +} + + async fn get_aws_credentials(base: bool, client: Client, app_handle: AppHandle) -> Result { let state = app_handle.state::(); - - let main_window = app_handle.get_window("main").ok_or(HandlerError::NoMainWindow)?; - let is_currently_visible = main_window.is_visible()?; - let rehide_after = state.get_or_set_rehide(!is_currently_visible).await; + let rehide_ms = { + let config = state.config.read().await; + config.rehide_ms + }; + let lease = state.acquire_visibility_lease(rehide_ms).await + .map_err(|_e| HandlerError::NoMainWindow)?; // automate this conversion eventually? let (chan_send, chan_recv) = oneshot::channel(); let request_id = state.register_request(chan_send).await; @@ -124,12 +133,6 @@ async fn get_aws_credentials(base: bool, client: Client, app_handle: AppHandle) let notification = AwsRequestNotification {id: request_id, client, base}; app_handle.emit_all("credentials-request", ¬ification)?; - if !main_window.is_visible()? { - main_window.unminimize()?; - main_window.show()?; - } - main_window.set_focus()?; - match chan_recv.await { Ok(Approval::Approved) => { if base { @@ -154,31 +157,6 @@ async fn get_aws_credentials(base: bool, client: Client, app_handle: AppHandle) } }; - rt::spawn( - handle_rehide(rehide_after, app_handle.app_handle()) - ); + lease.release(); result } - - -async fn handle_rehide(rehide_after: bool, app_handle: AppHandle) { - let state = app_handle.state::(); - let delay = { - let config = state.config.read().await; - Duration::from_millis(config.rehide_ms) - }; - tokio::time::sleep(delay).await; - - // if there are no other pending requests, set rehide status back to None - if state.req_count().await == 0 { - state.clear_rehide().await; - // and hide the window if necessary - if rehide_after { - app_handle.get_window("main").map(|w| { - if let Err(e) = w.hide() { - eprintln!("{e}"); - } - }); - } - } -} diff --git a/src-tauri/src/shortcuts.rs b/src-tauri/src/shortcuts.rs index c40d758..84abdc0 100644 --- a/src-tauri/src/shortcuts.rs +++ b/src-tauri/src/shortcuts.rs @@ -1,12 +1,14 @@ use serde::{Serialize, Deserialize}; use tauri::{ - AppHandle, + GlobalShortcutManager, Manager, + async_runtime as rt, }; use crate::app::APP; use crate::config::HotkeysConfig; +use crate::errors::*; use crate::terminal; @@ -19,11 +21,18 @@ pub enum ShortcutAction { pub fn exec_shortcut(action: ShortcutAction) { match action { - ShowWindow => { + ShortcutAction::ShowWindow => { let app = APP.get().unwrap(); - app.get_window("main").map(|w| w.show()); + app.get_window("main") + .ok_or("Couldn't find application main window") + .map(|w| w.show().error_popup("Failed to show window")) + .error_popup("Failed to show window"); + }, + ShortcutAction::LaunchTerminal => { + rt::spawn(async { + terminal::launch(false).await.error_popup("Failed to launch terminal"); + }); }, - LaunchTerminal => terminal::launch(false), } } @@ -35,7 +44,7 @@ pub fn register_hotkeys(hotkeys: &HotkeysConfig) -> tauri::Result<()> { if hotkeys.show_window.enabled { manager.register( - hotkeys.show_window.keys, + &hotkeys.show_window.keys, || exec_shortcut(ShortcutAction::ShowWindow) )?; } diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs index 7cd266c..cd30af1 100644 --- a/src-tauri/src/state.rs +++ b/src-tauri/src/state.rs @@ -1,10 +1,15 @@ use std::collections::HashMap; +use std::time::Duration; use tokio::{ sync::RwLock, - sync::oneshot::Sender, + sync::oneshot::{self, Sender}, }; use sqlx::SqlitePool; +use tauri::{ + Manager, + async_runtime as rt, +}; use crate::credentials::{ Session, @@ -14,6 +19,73 @@ use crate::credentials::{ use crate::{config, config::AppConfig}; use crate::ipc::{self, Approval}; use crate::errors::*; +use crate::shortcuts; + + +#[derive(Debug)] +struct Visibility { + leases: usize, + original: Option, +} + +impl Visibility { + fn new() -> Self { + Visibility { leases: 0, original: None } + } + + fn acquire(&mut self, delay_ms: u64) -> Result { + let app = crate::app::APP.get().unwrap(); + let window = app.get_window("main") + .ok_or(WindowError::NoMainWindow)?; + + self.leases += 1; + if self.original.is_none() { + let is_visible = window.is_visible()?; + self.original = Some(is_visible); + if !is_visible { + window.show()?; + } + } + window.set_focus()?; + + let (tx, rx) = oneshot::channel(); + let lease = VisibilityLease { notify: tx }; + + let delay = Duration::from_millis(delay_ms); + let handle = app.app_handle(); + rt::spawn(async move { + // We don't care if it's an error; lease being dropped should be handled identically + let _ = rx.await; + tokio::time::sleep(delay).await; + // we can't use `self` here because we would have to move it into the async block + let state = handle.state::(); + let mut visibility = state.visibility.write().await; + visibility.leases -= 1; + if visibility.leases == 0 { + if let Some(false) = visibility.original { + window.hide().error_print(); + } + visibility.original = None; + } + }); + + Ok(lease) + } +} + +pub struct VisibilityLease { + notify: Sender<()>, +} + +impl VisibilityLease { + pub fn release(self) { + rt::spawn(async move { + if let Err(_) = self.notify.send(()) { + eprintln!("Error releasing visibility lease") + } + }); + } +} #[derive(Debug)] @@ -22,11 +94,11 @@ pub struct AppState { pub session: RwLock, pub request_count: RwLock, pub waiting_requests: RwLock>>, - pub current_rehide_status: RwLock>, pub pending_terminal_request: RwLock, // setup_errors is never modified and so doesn't need to be wrapped in RwLock pub setup_errors: Vec, pool: sqlx::SqlitePool, + visibility: RwLock, } impl AppState { @@ -41,10 +113,10 @@ impl AppState { session: RwLock::new(session), request_count: RwLock::new(0), waiting_requests: RwLock::new(HashMap::new()), - current_rehide_status: RwLock::new(None), pending_terminal_request: RwLock::new(false), setup_errors, pool, + visibility: RwLock::new(Visibility::new()), } } @@ -69,7 +141,7 @@ impl AppState { if new_config.hotkeys.show_window != live_config.hotkeys.show_window || new_config.hotkeys.launch_terminal != live_config.hotkeys.launch_terminal { - config::register_hotkeys(&new_config.hotkeys)?; + shortcuts::register_hotkeys(&new_config.hotkeys)?; } new_config.save(&self.pool).await?; @@ -94,25 +166,9 @@ impl AppState { waiting_requests.remove(&id); } - pub async fn req_count(&self) -> usize { - let waiting_requests = self.waiting_requests.read().await; - waiting_requests.len() - } - - pub async fn get_or_set_rehide(&self, new_value: bool) -> bool { - let mut rehide = self.current_rehide_status.write().await; - match *rehide { - Some(original) => original, - None => { - *rehide = Some(new_value); - new_value - } - } - } - - pub async fn clear_rehide(&self) { - let mut rehide = self.current_rehide_status.write().await; - *rehide = None; + pub async fn acquire_visibility_lease(&self, delay: u64) -> Result { + let mut visibility = self.visibility.write().await; + visibility.acquire(delay) } pub async fn send_response(&self, response: ipc::RequestResponse) -> Result<(), SendResponseError> { diff --git a/src-tauri/src/terminal.rs b/src-tauri/src/terminal.rs index fa95736..4fb7f6d 100644 --- a/src-tauri/src/terminal.rs +++ b/src-tauri/src/terminal.rs @@ -26,13 +26,8 @@ pub async fn launch(use_base: bool) -> Result<(), LaunchTerminalError> { // if session is unlocked or empty, wait for credentials from frontend if !state.is_unlocked().await { app.emit_all("launch-terminal-request", ())?; - let window = app.get_window("main") - .ok_or(LaunchTerminalError::NoMainWindow)?; - if !window.is_visible()? { - window.unminimize()?; - window.show()?; - } - window.set_focus()?; + let lease = state.acquire_visibility_lease(0).await + .map_err(|_e| LaunchTerminalError::NoMainWindow)?; // automate conversion eventually? let (tx, rx) = tokio::sync::oneshot::channel(); app.once_global("credentials-event", move |e| { @@ -47,6 +42,7 @@ pub async fn launch(use_base: bool) -> Result<(), LaunchTerminalError> { state.unregister_terminal_request().await; return Ok(()); // request was canceled by user } + lease.release(); } // more lock-management From d4fa8966b274135ffd9bc6d701ea9ec7dfa52bb3 Mon Sep 17 00:00:00 2001 From: Joseph Montanaro Date: Sat, 23 Sep 2023 11:10:07 -0700 Subject: [PATCH 4/7] add unix listener, split win/unix into separate submodules --- .gitignore | 3 + src-tauri/.env | 1 - src-tauri/src/cli.rs | 6 +- src-tauri/src/clientinfo.rs | 34 +-------- src-tauri/src/config.rs | 42 ----------- src-tauri/src/errors.rs | 1 + src-tauri/src/{server.rs => server/mod.rs} | 84 +++++++--------------- src-tauri/src/server/server_unix.rs | 59 +++++++++++++++ src-tauri/src/server/server_win.rs | 76 ++++++++++++++++++++ 9 files changed, 167 insertions(+), 139 deletions(-) delete mode 100644 src-tauri/.env rename src-tauri/src/{server.rs => server/mod.rs} (63%) create mode 100644 src-tauri/src/server/server_unix.rs create mode 100644 src-tauri/src/server/server_win.rs diff --git a/.gitignore b/.gitignore index e1b2934..8e46064 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,9 @@ dist **/node_modules src-tauri/target/ **/creddy.db +# .env is system-specific +.env +.vscode # just in case credentials* diff --git a/src-tauri/.env b/src-tauri/.env deleted file mode 100644 index da27150..0000000 --- a/src-tauri/.env +++ /dev/null @@ -1 +0,0 @@ -DATABASE_URL=sqlite://C:/Users/Joe/AppData/Roaming/creddy/creddy.dev.db diff --git a/src-tauri/src/cli.rs b/src-tauri/src/cli.rs index c4b8b86..c4c1b2d 100644 --- a/src-tauri/src/cli.rs +++ b/src-tauri/src/cli.rs @@ -1,5 +1,6 @@ use std::ffi::OsString; use std::process::Command as ChildCommand; +#[cfg(windows)] use std::time::Duration; use clap::{ @@ -19,7 +20,6 @@ use crate::shortcuts::ShortcutAction; #[cfg(unix)] use { std::os::unix::process::CommandExt, - std::path::Path, tokio::net::UnixStream, }; @@ -199,7 +199,5 @@ async fn connect() -> Result { #[cfg(unix)] async fn connect() -> Result { - let path = Path::from("/tmp/creddy-requests"); - std::fs::remove_file(path)?; - UnixStream::connect(path) + UnixStream::connect("/tmp/creddy.sock").await } diff --git a/src-tauri/src/clientinfo.rs b/src-tauri/src/clientinfo.rs index 518809e..4cff73d 100644 --- a/src-tauri/src/clientinfo.rs +++ b/src-tauri/src/clientinfo.rs @@ -2,19 +2,6 @@ use std::path::{Path, PathBuf}; use sysinfo::{System, SystemExt, Pid, PidExt, ProcessExt}; use serde::{Serialize, Deserialize}; -use std::os::windows::io::AsRawHandle; - -#[cfg(windows)] -use { - tokio::net::windows::named_pipe::NamedPipeServer, - windows::Win32::{ - Foundation::HANDLE, - System::Pipes::GetNamedPipeClientProcessId, - }, -}; - -#[cfg(unix)] -use tokio::net::UnixStream; use crate::errors::*; @@ -26,25 +13,8 @@ pub struct Client { } -#[cfg(unix)] -pub fn get_client_parent(stream: &UnixStream) -> Result { - let pid = stream.peer_cred()?; - get_process_parent_info(pid)? -} - - -#[cfg(windows)] -pub fn get_client_parent(stream: &NamedPipeServer) -> Result { - let raw_handle = stream.as_raw_handle(); - let mut pid = 0u32; - let handle = HANDLE(raw_handle as _); - unsafe { GetNamedPipeClientProcessId(handle, &mut pid as *mut u32)? }; - - get_process_parent_info(pid) -} - - -fn get_process_parent_info(pid: u32) -> Result { +pub fn get_process_parent_info(pid: u32) -> Result { + dbg!(pid); let sys_pid = Pid::from_u32(pid); let mut sys = System::new(); sys.refresh_process(sys_pid); diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index c3d706b..4a9537e 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -4,11 +4,6 @@ use auto_launch::AutoLaunchBuilder; use is_terminal::IsTerminal; use serde::{Serialize, Deserialize}; use sqlx::SqlitePool; -use tauri::{ - Manager, - GlobalShortcutManager, - async_runtime as rt, -}; use crate::errors::*; @@ -183,43 +178,6 @@ fn default_hotkey_config() -> HotkeysConfig { } } -// note: will panic if called before APP is set -pub fn register_hotkeys(hotkeys: &HotkeysConfig) -> tauri::Result<()> { - let app = crate::app::APP.get().unwrap(); - - let mut manager = app.global_shortcut_manager(); - manager.unregister_all()?; - - if hotkeys.show_window.enabled { - let handle = app.app_handle(); - manager.register( - &hotkeys.show_window.keys, - move || { - handle.get_window("main") - .map(|w| w.show().error_popup("Failed to show")) - .ok_or(HandlerError::NoMainWindow) - .error_popup("No main window"); - }, - )?; - } - - if hotkeys.launch_terminal.enabled { - // register() doesn't take an async fn, so we have to use spawn - manager.register( - &hotkeys.launch_terminal.keys, - || { - rt::spawn(async { - crate::terminal::launch(false) - .await - .error_popup("Failed to launch"); - }); - } - )?; - } - - Ok(()) -} - fn default_rehide_ms() -> u64 { 1000 } // start minimized and on login only in production mode diff --git a/src-tauri/src/errors.rs b/src-tauri/src/errors.rs index 537980d..de7c823 100644 --- a/src-tauri/src/errors.rs +++ b/src-tauri/src/errors.rs @@ -244,6 +244,7 @@ pub enum ClientInfoError { ParentPidNotFound, #[error("Found PID for parent process of client, but no corresponding process")] ParentProcessNotFound, + #[cfg(windows)] #[error("Could not determine PID of connected client")] WindowsError(#[from] windows::core::Error), #[error(transparent)] diff --git a/src-tauri/src/server.rs b/src-tauri/src/server/mod.rs similarity index 63% rename from src-tauri/src/server.rs rename to src-tauri/src/server/mod.rs index 0afa5c6..e1f8463 100644 --- a/src-tauri/src/server.rs +++ b/src-tauri/src/server/mod.rs @@ -1,18 +1,9 @@ -#[cfg(windows)] -use tokio::net::windows::named_pipe::{ - NamedPipeServer, - ServerOptions, -}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::sync::oneshot; use serde::{Serialize, Deserialize}; -use tauri::{ - AppHandle, - Manager, - async_runtime as rt, -}; +use tauri::{AppHandle, Manager}; use crate::errors::*; use crate::clientinfo::{self, Client}; @@ -21,6 +12,20 @@ use crate::ipc::{Approval, AwsRequestNotification}; use crate::state::AppState; use crate::shortcuts::{self, ShortcutAction}; +#[cfg(windows)] +mod server_win; +#[cfg(windows)] +pub use server_win::Server; +#[cfg(windows)] +use server_win::Stream; + +#[cfg(unix)] +mod server_unix; +#[cfg(unix)] +pub use server_unix::Server; +#[cfg(unix)] +use server_unix::Stream; + #[derive(Serialize, Deserialize)] pub enum Request { @@ -38,53 +43,8 @@ pub enum Response { } -pub struct Server { - listener: tokio::net::windows::named_pipe::NamedPipeServer, - app_handle: AppHandle, -} - -impl Server { - pub fn start(app_handle: AppHandle) -> std::io::Result<()> { - let listener = ServerOptions::new() - .first_pipe_instance(true) - .create(r"\\.\pipe\creddy-requests")?; - - let srv = Server {listener, app_handle}; - rt::spawn(srv.serve()); - Ok(()) - } - - async fn serve(mut self) { - loop { - if let Err(e) = self.try_serve().await { - eprintln!("Error accepting connection: {e}"); - } - } - } - - async fn try_serve(&mut self) -> std::io::Result<()> { - // connect() just waits for a client to connect, it doesn't return anything - self.listener.connect().await?; - - // create a new pipe instance to listen for the next client, and swap it in - let new_listener = ServerOptions::new().create(r"\\.\pipe\creddy-requests")?; - let mut stream = std::mem::replace(&mut self.listener, new_listener); - let new_handle = self.app_handle.app_handle(); - rt::spawn(async move { - let res = serde_json::to_string( - &handle(&mut stream, new_handle).await - ).unwrap(); - if let Err(e) = stream.write_all(res.as_bytes()).await { - eprintln!("Error responding to request: {e}"); - } - }); - - Ok(()) - } -} - - -async fn handle(stream: &mut NamedPipeServer, app_handle: AppHandle) -> Result { +async fn handle(mut stream: Stream, app_handle: AppHandle, client_pid: u32) -> Result<(), HandlerError> +{ // read from stream until delimiter is reached let mut buf: Vec = Vec::with_capacity(1024); // requests are small, 1KiB is more than enough let mut n = 0; @@ -98,13 +58,17 @@ async fn handle(stream: &mut NamedPipeServer, app_handle: AppHandle) -> Result get_aws_credentials(base, client, app_handle).await, Request::InvokeShortcut(action) => invoke_shortcut(action).await, - } + }; + + let res = serde_json::to_vec(&res).unwrap(); + stream.write_all(&res).await?; + Ok(()) } diff --git a/src-tauri/src/server/server_unix.rs b/src-tauri/src/server/server_unix.rs new file mode 100644 index 0000000..bd3b4ff --- /dev/null +++ b/src-tauri/src/server/server_unix.rs @@ -0,0 +1,59 @@ +use std::io::ErrorKind; +use tokio::net::{UnixListener, UnixStream}; +use tauri::{ + AppHandle, + Manager, + async_runtime as rt, +}; + +use crate::errors::*; + + +pub type Stream = UnixStream; + + +pub struct Server { + listener: UnixListener, + app_handle: AppHandle, +} + +impl Server { + pub fn start(app_handle: AppHandle) -> std::io::Result<()> { + match std::fs::remove_file("/tmp/creddy.sock") { + Ok(_) => (), + Err(e) if e.kind() == ErrorKind::NotFound => (), + Err(e) => return Err(e), + } + + let listener = UnixListener::bind("/tmp/creddy.sock")?; + let srv = Server { listener, app_handle }; + rt::spawn(srv.serve()); + Ok(()) + } + + async fn serve(self) { + loop { + self.try_serve() + .await + .error_print_prefix("Error accepting request: "); + } + } + + async fn try_serve(&self) -> Result<(), HandlerError> { + let (stream, _addr) = self.listener.accept().await?; + let new_handle = self.app_handle.app_handle(); + let client_pid = get_client_pid(&stream)?; + rt::spawn(async move { + super::handle(stream, new_handle, client_pid) + .await + .error_print_prefix("Error responding to request: "); + }); + Ok(()) + } +} + + +fn get_client_pid(stream: &UnixStream) -> std::io::Result { + let cred = stream.peer_cred()?; + Ok(cred.pid().unwrap() as u32) +} diff --git a/src-tauri/src/server/server_win.rs b/src-tauri/src/server/server_win.rs new file mode 100644 index 0000000..4b25106 --- /dev/null +++ b/src-tauri/src/server/server_win.rs @@ -0,0 +1,76 @@ +use tokio::{ + net::windows::named_pipe::{ + NamedPipeServer, + ServerOptions, + }, + sync::oneshot, +}; + +#[cfg(windows)] +use windows::Win32:: { + Foundation::HANDLE, + System::Pipes::GetNamedPipeClientProcessId, +}; +#[cfg(windows)] +use std::os::windows::io::AsRawHandle; + +use tauri::async_runtime as rt; + +use crate::errors::*; + + +// used by parent module +pub type Stream = NamedPipeServer; + + +pub struct Server { + listener: NamedPipeServer, + app_handle: AppHandle, +} + +impl Server { + pub fn start(app_handle: AppHandle) -> std::io::Result<()> { + let listener = ServerOptions::new() + .first_pipe_instance(true) + .create(r"\\.\pipe\creddy-requests")?; + + let srv = Server {listener, app_handle}; + rt::spawn(srv.serve()); + Ok(()) + } + + async fn serve(mut self) { + loop { + if let Err(e) = self.try_serve().await { + eprintln!("Error accepting connection: {e}"); + } + } + } + + async fn try_serve(&mut self) -> Result<(), HandlerError> { + // connect() just waits for a client to connect, it doesn't return anything + self.listener.connect().await?; + + // create a new pipe instance to listen for the next client, and swap it in + let new_listener = ServerOptions::new().create(r"\\.\pipe\creddy-requests")?; + let mut stream = std::mem::replace(&mut self.listener, new_listener); + let new_handle = self.app_handle.app_handle(); + let client_pid = get_client_pid(&stream)?; + rt::spawn(async move { + super::handle(stream, app_handle) + .await + .error_print_prefix("Error responding to request: "); + }); + + Ok(()) + } +} + + +fn get_client_pid(pipe: &NamedPipeServer) -> Result { + let raw_handle = pipe.as_raw_handle(); + let mut pid = 0u32; + let handle = HANDLE(raw_handle as _); + unsafe { GetNamedPipeClientProcessId(handle, &mut pid as *mut u32)? }; + pid +} From 5e0ffc1155fe70544d610b1ae2203bf9db1bb06e Mon Sep 17 00:00:00 2001 From: Joseph Montanaro Date: Sun, 8 Oct 2023 22:06:30 -0700 Subject: [PATCH 5/7] use save dialog for settings instead of autosaving --- src-tauri/src/main.rs | 1 + src-tauri/src/server/server_win.rs | 3 +- src/ui/settings/NumericSetting.svelte | 10 ++- src/views/Settings.svelte | 113 ++++++++++++++------------ 4 files changed, 73 insertions(+), 54 deletions(-) diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 6b6f9ba..9e0fc5e 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -18,6 +18,7 @@ fn main() { }, Some(("get", m)) => cli::get(m), Some(("exec", m)) => cli::exec(m), + Some(("shortcut", m)) => cli::invoke_shortcut(m), _ => unreachable!(), }; diff --git a/src-tauri/src/server/server_win.rs b/src-tauri/src/server/server_win.rs index 4b25106..07dd7ca 100644 --- a/src-tauri/src/server/server_win.rs +++ b/src-tauri/src/server/server_win.rs @@ -6,12 +6,11 @@ use tokio::{ sync::oneshot, }; -#[cfg(windows)] use windows::Win32:: { Foundation::HANDLE, System::Pipes::GetNamedPipeClientProcessId, }; -#[cfg(windows)] + use std::os::windows::io::AsRawHandle; use tauri::async_runtime as rt; diff --git a/src/ui/settings/NumericSetting.svelte b/src/ui/settings/NumericSetting.svelte index 4c8e4c2..b4a34cd 100644 --- a/src/ui/settings/NumericSetting.svelte +++ b/src/ui/settings/NumericSetting.svelte @@ -10,15 +10,21 @@ export let min = null; export let max = null; export let decimal = false; + export let debounceInterval = 0; const dispatch = createEventDispatcher(); $: localValue = value.toString(); let lastInputTime = null; function debounce(event) { - lastInputTime = Date.now(); localValue = localValue.replace(/[^-0-9.]/g, ''); + if (debounceInterval === 0) { + updateValue(localValue); + return; + } + + lastInputTime = Date.now(); const eventTime = lastInputTime; const pendingValue = localValue; window.setTimeout( @@ -28,7 +34,7 @@ updateValue(pendingValue); } }, - 500 + debounceInterval, ) } diff --git a/src/views/Settings.svelte b/src/views/Settings.svelte index aaea8bf..affdaaf 100644 --- a/src/views/Settings.svelte +++ b/src/views/Settings.svelte @@ -14,15 +14,19 @@ import { backInOut } from 'svelte/easing'; + // make an independent copy so it can differ from the main config object + let config = JSON.parse(JSON.stringify($appState.config)); + $: configModified = JSON.stringify(config) !== JSON.stringify($appState.config); + let error = null; async function save() { console.log('updating config'); try { - await invoke('save_config', {config: $appState.config}); + await invoke('save_config', {config}); + $appState.config = await invoke('get_config'); } catch (e) { error = e; - $appState.config = await invoke('get_config'); } } @@ -35,62 +39,60 @@

Settings

-{#await invoke('get_config') then config} -
- - - - Start Creddy when you log in to your computer. - - +
+ + + + Start Creddy when you log in to your computer. + + - - - Minimize to the system tray at startup. - - + + + Minimize to the system tray at startup. + + - - - How long to wait after a request is approved/denied before minimizing - the window to tray. Only applicable if the window was minimized - to tray before the request was received. - - + + + How long to wait after a request is approved/denied before minimizing + the window to tray. Only applicable if the window was minimized + to tray before the request was received. + + - - - - - - Update or re-enter your encrypted credentials. - - + + + + + + Update or re-enter your encrypted credentials. + + - - - Choose your preferred terminal emulator (e.g. gnome-terminal or wt.exe.) May be an absolute path or an executable discoverable on $PATH. - - - + + + Choose your preferred terminal emulator (e.g. gnome-terminal or wt.exe.) May be an absolute path or an executable discoverable on $PATH. + + + - -
-

Click on a keybinding to modify it. Use the checkbox to enable or disable a keybinding entirely.

+ +
+

Click on a keybinding to modify it. Use the checkbox to enable or disable a keybinding entirely.

-
- - -
+
+ +
- +
+
-
-{/await} +
{#if error}
@@ -104,4 +106,15 @@
+{:else if configModified} +
+
+ You have unsaved changes. + +
+ + Save +
+
+
{/if} From 2079f99d041856372ee2f263396ea4127250f24a Mon Sep 17 00:00:00 2001 From: Joseph Montanaro Date: Sun, 8 Oct 2023 22:53:22 -0700 Subject: [PATCH 6/7] bump version to 0.3.4 --- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 41fdfcd..cadea19 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "creddy", - "version": "0.3.3", + "version": "0.3.4", "scripts": { "dev": "vite", "build": "vite build", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 9d2a462..737bfca 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1035,7 +1035,7 @@ dependencies = [ [[package]] name = "creddy" -version = "0.3.3" +version = "0.3.4" dependencies = [ "argon2", "auto-launch", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index d7266ec..93f1047 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "creddy" -version = "0.3.3" +version = "0.3.4" description = "A friendly AWS credentials manager" authors = ["Joseph Montanaro"] license = "" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 6fcdc2a..e9f80b7 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -8,7 +8,7 @@ }, "package": { "productName": "creddy", - "version": "0.3.3" + "version": "0.3.4" }, "tauri": { "allowlist": { From 1b749a857c1ed4c2db52e2cc1ed25c543e3db3a2 Mon Sep 17 00:00:00 2001 From: Joseph Montanaro Date: Mon, 9 Oct 2023 08:50:31 -0700 Subject: [PATCH 7/7] disable hotkeys if initial registration fails --- src-tauri/src/app.rs | 10 +++++++--- src-tauri/src/config.rs | 7 +++++++ src/views/EnterCredentials.svelte | 1 + src/views/Settings.svelte | 1 - 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src-tauri/src/app.rs b/src-tauri/src/app.rs index aeff75f..7b53eea 100644 --- a/src-tauri/src/app.rs +++ b/src-tauri/src/app.rs @@ -82,7 +82,7 @@ async fn setup(app: &mut App) -> Result<(), Box> { let pool = connect_db().await?; let mut setup_errors: Vec = vec![]; - let conf = match AppConfig::load(&pool).await { + let mut conf = match AppConfig::load(&pool).await { Ok(c) => c, Err(SetupError::ConfigParseError(_)) => { setup_errors.push( @@ -100,8 +100,12 @@ async fn setup(app: &mut App) -> Result<(), Box> { if let Err(_e) = config::set_auto_launch(conf.start_on_login) { setup_errors.push("Error: Failed to manage autolaunch.".into()); } - if let Err(e) = shortcuts::register_hotkeys(&conf.hotkeys) { - setup_errors.push(format!("{e}")); + + // if hotkeys fail to register, disable them so that this error doesn't have to keep showing up + if let Err(_e) = shortcuts::register_hotkeys(&conf.hotkeys) { + conf.hotkeys.disable_all(); + conf.save(&pool).await?; + setup_errors.push("Failed to register hotkeys. Hotkey settings have been disabled.".into()); } // if session is empty, this is probably the first launch, so don't autohide diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index 4a9537e..5cfb352 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -33,6 +33,13 @@ pub struct HotkeysConfig { pub launch_terminal: Hotkey, } +impl HotkeysConfig { + pub fn disable_all(&mut self) { + self.show_window.enabled = false; + self.launch_terminal.enabled = false; + } +} + #[derive(Clone, Debug, Serialize, Deserialize)] pub struct AppConfig { diff --git a/src/views/EnterCredentials.svelte b/src/views/EnterCredentials.svelte index 2570a8b..4a61004 100644 --- a/src/views/EnterCredentials.svelte +++ b/src/views/EnterCredentials.svelte @@ -1,6 +1,7 @@