use std::future::Future; use tauri::{ AppHandle, async_runtime as rt, Manager, }; use tokio::io::AsyncReadExt; use tokio::sync::oneshot; use serde::{Serialize, Deserialize}; use crate::clientinfo::Client; use crate::credentials::{ AwsBaseCredential, AwsSessionCredential, Credential, DockerCredential, }; use crate::errors::*; use crate::ipc::{RequestNotification, RequestNotificationDetail, RequestResponse}; use crate::shortcuts::ShortcutAction; use crate::state::AppState; pub mod creddy_server; pub mod agent; use platform::Stream; // These types match what's defined in creddy_cli, but they are separate types // so that we avoid polluting the standalone CLI with a bunch of dependencies // that would make it impossible to build a completely static-linked version #[derive(Debug, Serialize, Deserialize)] pub enum CliRequest { GetAwsCredential { name: Option, base: bool, }, GetDockerCredential { server_url: String, }, SaveCredential { name: String, is_default: bool, credential: Credential, }, InvokeShortcut(ShortcutAction), } #[derive(Debug, Serialize, Deserialize)] pub enum CliResponse { Credential(CliCredential), Empty, } #[derive(Debug, Serialize, Deserialize)] pub enum CliCredential { AwsBase(AwsBaseCredential), AwsSession(AwsSessionCredential), Docker(DockerCredential), } 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), } } } } fn serve(sock_name: &str, app_handle: AppHandle, handler: H) -> std::io::Result<()> where H: Copy + Send + Fn(Stream, AppHandle, u32) -> F + 'static, F: Send + Future>, { let (mut listener, addr) = platform::bind(sock_name)?; rt::spawn(async move { loop { let (stream, client_pid) = match platform::accept(&mut listener, &addr).await { Ok((s, c)) => (s, c), Err(e) => { eprintln!("Error accepting request: {e}"); continue; }, }; let new_handle = app_handle.clone(); rt::spawn(async move { handler(stream, new_handle, client_pid) .await .error_print_prefix("Error responding to request: "); }); } }); Ok(()) } async fn send_credentials_request( detail: RequestNotificationDetail, app_handle: AppHandle, mut waiter: CloseWaiter<'_> ) -> Result { let state = app_handle.state::(); 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)?; let (chan_send, chan_recv) = oneshot::channel(); let request_id = state.register_request(chan_send).await; let notification = RequestNotification { id: request_id, detail }; // the following could fail in various ways, but we want to make sure // the request gets unregistered on any failure, so we wrap this all // up in an async block so that we only have to handle the error case once let proceed = async { app_handle.emit("credential-request", ¬ification)?; tokio::select! { r = chan_recv => Ok(r?), _ = waiter.wait_for_close() => { app_handle.emit("request-cancelled", request_id)?; Err(HandlerError::Abandoned) }, } }; let res = proceed.await; if let Err(_) = &res { state.unregister_request(request_id).await; } lease.release(); res } #[cfg(unix)] mod platform { use std::io::ErrorKind; use std::path::PathBuf; use tokio::net::{UnixListener, UnixStream}; use super::*; pub type Stream = UnixStream; pub fn bind(sock_name: &str) -> std::io::Result<(UnixListener, PathBuf)> { let path = creddy_cli::server_addr(sock_name); match std::fs::remove_file(&path) { Ok(_) => (), Err(e) if e.kind() == ErrorKind::NotFound => (), Err(e) => return Err(e), } let listener = UnixListener::bind(&path)?; Ok((listener, path)) } pub async fn accept(listener: &mut UnixListener, _addr: &PathBuf) -> Result<(UnixStream, u32), HandlerError> { let (stream, _addr) = listener.accept().await?; let pid = stream.peer_cred()? .pid() .ok_or(ClientInfoError::PidNotFound)? as u32; Ok((stream, pid)) } } #[cfg(windows)] mod platform { use std::os::windows::io::AsRawHandle; use tokio::net::windows::named_pipe::{ NamedPipeServer, ServerOptions, }; use windows::Win32::{ Foundation::HANDLE, System::Pipes::GetNamedPipeClientProcessId, }; use super::*; pub type Stream = NamedPipeServer; pub fn bind(sock_name: &str) -> std::io::Result<(String, NamedPipeServer)> { let addr = creddy_cli::server_addr(sock_name); let listener = ServerOptions::new() .first_pipe_instance(true) .create(&addr)?; Ok((listener, addr)) } pub async fn accept(listener: &mut NamedPipeServer, addr: &String) -> Result<(NamedPipeServer, u32), HandlerError> { // connect() just waits for a client to connect, it doesn't return anything listener.connect().await?; // unlike Unix sockets, a Windows NamedPipeServer *becomes* the open stream // once a client connects. If we want to keep listening, we have to construct // a new server and swap it in. let new_listener = ServerOptions::new().create(addr)?; let stream = std::mem::replace(listener, new_listener); 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)? }; Ok((stream, pid)) } }