use tauri::{AppHandle, Manager}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use crate::clientinfo::{self, Client}; use crate::credentials::{ self, Credential, CredentialRecord, DockerCredential, }; use crate::errors::*; use crate::ipc::{ Approval, RequestAction, RequestNotificationDetail }; use crate::shortcuts::{self, ShortcutAction}; use crate::state::AppState; use super::{ CloseWaiter, CliCredential, CliRequest, CliResponse, Stream, }; pub fn serve(app_handle: AppHandle) -> std::io::Result<()> { super::serve("creddy-server", app_handle, handle) } 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; loop { n += stream.read_buf(&mut buf).await?; if let Some(&b'\n') = buf.last() { break; } // sanity check, no request should ever be within a mile of 1MB else if n >= (1024 * 1024) { return Err(HandlerError::RequestTooLarge); } } let client = clientinfo::get_client(client_pid, true)?; let waiter = CloseWaiter { stream: &mut stream }; let req: CliRequest = serde_json::from_slice(&buf)?; let res = match req { CliRequest::GetAwsCredential{ name, base } => get_aws_credentials( name, base, client, app_handle, waiter ).await, CliRequest::GetDockerCredential{ server_url } => get_docker_credential ( server_url, client, app_handle, waiter ).await, CliRequest::StoreDockerCredential(docker_credential) => store_docker_credential( docker_credential, app_handle, client, waiter ).await, CliRequest::EraseDockerCredential { server_url } => erase_docker_credential( server_url, app_handle, client, waiter ).await, CliRequest::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(()) } async fn invoke_shortcut(action: ShortcutAction) -> Result { shortcuts::exec_shortcut(action); Ok(CliResponse::Empty) } async fn get_aws_credentials( name: Option, base: bool, client: Client, app_handle: AppHandle, waiter: CloseWaiter<'_>, ) -> Result { let detail = RequestNotificationDetail::new_aws(client, name.clone(), base); let response = super::send_credentials_request(detail, app_handle.clone(), waiter).await?; match response.approval { Approval::Approved => { let state = app_handle.state::(); if response.base { let creds = state.get_aws_base(name).await?; Ok(CliResponse::Credential(CliCredential::AwsBase(creds))) } else { let creds = state.get_aws_session(name).await?.clone(); Ok(CliResponse::Credential(CliCredential::AwsSession(creds))) } }, Approval::Denied => Err(HandlerError::Denied), } } async fn get_docker_credential( server_url: String, client: Client, app_handle: AppHandle, waiter: CloseWaiter<'_>, ) -> Result { let state = app_handle.state::(); let meta = state.docker_credential_meta(&server_url).await.unwrap_or(None); if meta.is_none() { return Err( HandlerError::NoCredentials( GetCredentialsError::Load( LoadCredentialsError::NoCredentials ) ) ); } let detail = RequestNotificationDetail::new_docker( RequestAction::Access, client, server_url.clone() ); let response = super::send_credentials_request(detail, app_handle.clone(), waiter).await?; match response.approval { Approval::Approved => { let creds = state.get_docker_credential(&server_url).await?; Ok(CliResponse::Credential(CliCredential::Docker(creds))) }, Approval::Denied => { Err(HandlerError::Denied) }, } } async fn store_docker_credential( docker_credential: DockerCredential, app_handle: AppHandle, client: Client, waiter: CloseWaiter<'_>, ) -> Result { let state = app_handle.state::(); // We want to do this before asking for confirmation from the user, because Docker has an annoying // habit of calling `get` and then immediately turning around and calling `store` with the same // data. In that case we want to avoid asking for confirmation at all. match state.get_docker_credential(&docker_credential.server_url).await { // if there is already a credential with this server_url, and it is unchanged, we're done Ok(c) if c == docker_credential => return Ok(CliResponse::Empty), // otherwise we are making an update, so proceed Ok(_) => (), // if the app is locked, then this isn't the situation described above, so proceed Err(GetCredentialsError::Locked) => (), // if the app is unlocked, and there is no matching credential, proceed Err(GetCredentialsError::Load(LoadCredentialsError::NoCredentials)) => (), // any other error is a failure Err(e) => return Err(e.into()), }; let detail = RequestNotificationDetail::new_docker( RequestAction::Save, client, docker_credential.server_url.clone(), ); let response = super::send_credentials_request(detail, app_handle.clone(), waiter).await?; if matches!(response.approval, Approval::Denied) { return Err(HandlerError::Denied); } let (id, name) = state.docker_credential_meta(&docker_credential.server_url) .await .map_err(|e| GetCredentialsError::Load(e))? .unwrap_or_else(|| (credentials::random_uuid(), docker_credential.server_url.clone())); let record = CredentialRecord { id, name, is_default: false, credential: Credential::Docker(docker_credential) }; state.save_credential(record).await?; Ok(CliResponse::Empty) } async fn erase_docker_credential( server_url: String, app_handle: AppHandle, client: Client, waiter: CloseWaiter<'_> ) -> Result { let state = app_handle.state::(); let detail = RequestNotificationDetail::new_docker( RequestAction::Delete, client, server_url.clone(), ); let resp = super::send_credentials_request(detail, app_handle.clone(), waiter).await?; match resp.approval { Approval::Approved => { state.delete_credential_by_name(&server_url).await?; Ok(CliResponse::Empty) } Approval::Denied => { Err(HandlerError::Denied) } } }