diff --git a/doc/todo.md b/doc/todo.md index 58d1d22..bae1f99 100644 --- a/doc/todo.md +++ b/doc/todo.md @@ -1,7 +1,7 @@ ## Definitely * ~~Switch to "process" provider for AWS credentials (much less hacky)~~ -* Frontend needs to react when request is cancelled from backend +* ~~Frontend needs to react when request is cancelled from backend~~ * Session timeout (plain duration, or activity-based?) * ~~Fix rehide behavior when new request comes in while old one is still being resolved~~ * Additional hotkey configuration (approve/deny at the very least) diff --git a/package.json b/package.json index edb7a27..423d3af 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "creddy", - "version": "0.4.3", + "version": "0.4.4", "scripts": { "dev": "vite", "build": "vite build", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 6563432..9f66c7b 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1035,7 +1035,7 @@ dependencies = [ [[package]] name = "creddy" -version = "0.4.3" +version = "0.4.4" dependencies = [ "argon2", "auto-launch", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 508cd65..27bf7c7 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "creddy" -version = "0.4.3" +version = "0.4.4" description = "A friendly AWS credentials manager" authors = ["Joseph Montanaro"] license = "" diff --git a/src-tauri/src/clientinfo.rs b/src-tauri/src/clientinfo.rs index bb3dbb8..bd8cf91 100644 --- a/src-tauri/src/clientinfo.rs +++ b/src-tauri/src/clientinfo.rs @@ -14,7 +14,6 @@ pub struct Client { 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/errors.rs b/src-tauri/src/errors.rs index 4e329bc..96294a8 100644 --- a/src-tauri/src/errors.rs +++ b/src-tauri/src/errors.rs @@ -83,15 +83,33 @@ where } +struct SerializeUpstream(pub E); + +impl Serialize for SerializeUpstream { + fn serialize(&self, serializer: S) -> Result { + let msg = format!("{}", self.0); + let mut map = serializer.serialize_map(None)?; + map.serialize_entry("msg", &msg)?; + map.serialize_entry("code", &None::<&str>)?; + map.serialize_entry("source", &None::<&str>)?; + map.end() + } +} + fn serialize_upstream_err(err: &E, map: &mut M) -> Result<(), M::Error> where E: Error, M: serde::ser::SerializeMap, { - let msg = err.source().map(|s| format!("{s}")); - map.serialize_entry("msg", &msg)?; - map.serialize_entry("code", &None::<&str>)?; - map.serialize_entry("source", &None::<&str>)?; + // let msg = err.source().map(|s| format!("{s}")); + // map.serialize_entry("msg", &msg)?; + // map.serialize_entry("code", &None::<&str>)?; + // map.serialize_entry("source", &None::<&str>)?; + + match err.source() { + Some(src) => map.serialize_entry("source", &SerializeUpstream(src))?, + None => map.serialize_entry("source", &None::<&str>)?, + } Ok(()) } @@ -153,7 +171,7 @@ pub enum SendResponseError { } -// errors encountered while handling an HTTP request +// errors encountered while handling a client request #[derive(Debug, ThisError, AsRefStr)] pub enum HandlerError { #[error("Error writing to stream: {0}")] @@ -164,6 +182,8 @@ pub enum HandlerError { BadRequest(#[from] serde_json::Error), #[error("HTTP request too large")] RequestTooLarge, + #[error("Connection closed early by client")] + Abandoned, #[error("Internal server error")] Internal(#[from] RecvError), #[error("Error accessing credentials: {0}")] @@ -345,7 +365,6 @@ impl Serialize for SerializeWrapper<&GetSessionTokenError> { } - impl_serialize_basic!(SetupError); impl_serialize_basic!(GetCredentialsError); impl_serialize_basic!(ClientInfoError); diff --git a/src-tauri/src/server/mod.rs b/src-tauri/src/server/mod.rs index 584ac70..69e2388 100644 --- a/src-tauri/src/server/mod.rs +++ b/src-tauri/src/server/mod.rs @@ -43,6 +43,24 @@ pub enum Response { } +struct CloseWaiter<'s> { + stream: &'s mut Stream, +} + +impl<'s> CloseWaiter<'s> { + async fn wait_for_close(&mut self) -> std::io::Result<()> { + let mut buf = [0u8; 8]; + loop { + match self.stream.read(&mut buf).await { + Ok(0) => break Ok(()), + Ok(_) => (), + Err(e) => break Err(e), + } + } + } +} + + async fn handle(mut stream: Stream, app_handle: AppHandle, client_pid: u32) -> Result<(), HandlerError> { // read from stream until delimiter is reached @@ -59,13 +77,21 @@ async fn handle(mut stream: Stream, app_handle: AppHandle, client_pid: u32) -> R } let client = clientinfo::get_process_parent_info(client_pid)?; + let waiter = CloseWaiter { stream: &mut stream }; let req: Request = serde_json::from_slice(&buf)?; let res = match req { - Request::GetAwsCredentials{ base } => get_aws_credentials(base, client, app_handle).await, + Request::GetAwsCredentials{ base } => get_aws_credentials( + base, client, app_handle, waiter + ).await, Request::InvokeShortcut(action) => invoke_shortcut(action).await, }; + // doesn't make sense to send the error to the client if the client has already left + if let Err(HandlerError::Abandoned) = res { + return Err(HandlerError::Abandoned); + } + let res = serde_json::to_vec(&res).unwrap(); stream.write_all(&res).await?; Ok(()) @@ -78,7 +104,12 @@ async fn invoke_shortcut(action: ShortcutAction) -> Result Result { +async fn get_aws_credentials( + base: bool, + client: Client, + app_handle: AppHandle, + mut waiter: CloseWaiter<'_>, +) -> Result { let state = app_handle.state::(); let rehide_ms = { let config = state.config.read().await; @@ -97,7 +128,14 @@ 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)?; - let response = chan_recv.await?; + let response = tokio::select! { + r = chan_recv => r?, + _ = waiter.wait_for_close() => { + app_handle.emit_all("request-cancelled", request_id)?; + return Err(HandlerError::Abandoned); + }, + }; + match response.approval { Approval::Approved => { if response.base { diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 91d11dc..0de6641 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -8,7 +8,7 @@ }, "package": { "productName": "creddy", - "version": "0.4.3" + "version": "0.4.4" }, "tauri": { "allowlist": { diff --git a/src/App.svelte b/src/App.svelte index 66659e6..9903050 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -3,7 +3,7 @@ import { onMount } from 'svelte'; import { listen } from '@tauri-apps/api/event'; import { invoke } from '@tauri-apps/api/tauri'; -import { appState, acceptRequest } from './lib/state.js'; +import { appState, acceptRequest, cleanupRequest } from './lib/state.js'; import { views, currentView, navigate } from './lib/routing.js'; @@ -16,6 +16,16 @@ listen('credentials-request', (tauriEvent) => { $appState.pendingRequests.put(tauriEvent.payload); }); +listen('request-cancelled', (tauriEvent) => { + const id = tauriEvent.payload; + if (id === $appState.currentRequest?.id) { + cleanupRequest() + } + else { + const found = $appState.pendingRequests.find_remove(r => r.id === id); + } +}); + listen('launch-terminal-request', async (tauriEvent) => { if ($appState.currentRequest === null) { let status = await invoke('get_session_status'); diff --git a/src/lib/queue.js b/src/lib/queue.js index 15816af..a4a07da 100644 --- a/src/lib/queue.js +++ b/src/lib/queue.js @@ -30,5 +30,15 @@ export default function() { return this.items.shift(); }, + + find_remove(pred) { + for (let i=0; i { $appState.currentRequest = null; return $appState; diff --git a/src/ui/Link.svelte b/src/ui/Link.svelte index ce799fc..c8a85af 100644 --- a/src/ui/Link.svelte +++ b/src/ui/Link.svelte @@ -30,7 +30,6 @@ && alt === event.altKey && shift === event.shiftKey ) { - console.log({hotkey, ctrl, alt, shift}); click(); } } diff --git a/src/views/Approve.svelte b/src/views/Approve.svelte index a4796a2..a8959f1 100644 --- a/src/views/Approve.svelte +++ b/src/views/Approve.svelte @@ -3,7 +3,7 @@ import { invoke } from '@tauri-apps/api/tauri'; import { navigate } from '../lib/routing.js'; - import { appState, completeRequest } from '../lib/state.js'; + import { appState, cleanupRequest } from '../lib/state.js'; import ErrorAlert from '../ui/ErrorAlert.svelte'; import Link from '../ui/Link.svelte'; import KeyCombo from '../ui/KeyCombo.svelte'; @@ -71,29 +71,29 @@ if ($appState.currentRequest.response) { await respond(); } - }) + }); -{#if error || !$appState.currentRequest.response} +{#if error || !$appState.currentRequest?.response}
{#if error} - {error} + {error.msg} - + {/if} - {#if $appState.currentRequest.base} + {#if $appState.currentRequest?.base}
- WARNING: This application is requesting your long-lived AWS credentials. + WARNING: This application is requesting your base AWS credentials. These credentials are less secure than session credentials, since they don't expire automatically.
@@ -113,7 +113,7 @@
- {#if !$appState.currentRequest.base} + {#if !$appState.currentRequest?.base}

Approve with session credentials

@@ -126,7 +126,7 @@

- {#if $appState.currentRequest.base} + {#if $appState.currentRequest?.base} Approve {:else} Approve with base credentials diff --git a/src/views/ShowResponse.svelte b/src/views/ShowResponse.svelte index 8fec450..321c8aa 100644 --- a/src/views/ShowResponse.svelte +++ b/src/views/ShowResponse.svelte @@ -2,7 +2,7 @@ import { onMount } from 'svelte'; import { draw, fade } from 'svelte/transition'; - import { appState, completeRequest } from '../lib/state.js'; + import { appState, cleanupRequest } from '../lib/state.js'; let success = false; let error = null; @@ -13,7 +13,7 @@ onMount(() => { window.setTimeout( - completeRequest, + cleanupRequest, // Extra 50ms so the window can finish disappearing before the redraw Math.min(5000, $appState.config.rehide_ms + 50), )