From 397928b8f1a30bc1ab0365239010440de1a04134 Mon Sep 17 00:00:00 2001 From: Joseph Montanaro Date: Mon, 28 Nov 2022 16:16:33 -0800 Subject: [PATCH] reorganize backend --- src-tauri/src/{http => }/errors.rs | 24 ++++++--- src-tauri/src/ipc.rs | 35 +++++++++++-- src-tauri/src/main.rs | 56 ++++---------------- src-tauri/src/{http/mod.rs => server.rs} | 67 +++++++++--------------- src-tauri/src/state.rs | 57 +++++++++++--------- src-tauri/src/storage.rs | 31 +++++++---- 6 files changed, 134 insertions(+), 136 deletions(-) rename src-tauri/src/{http => }/errors.rs (60%) rename src-tauri/src/{http/mod.rs => server.rs} (55%) diff --git a/src-tauri/src/http/errors.rs b/src-tauri/src/errors.rs similarity index 60% rename from src-tauri/src/http/errors.rs rename to src-tauri/src/errors.rs index bf3fd2a..60bafa8 100644 --- a/src-tauri/src/http/errors.rs +++ b/src-tauri/src/errors.rs @@ -2,10 +2,25 @@ use std::fmt::{Display, Formatter}; use std::convert::From; use std::str::Utf8Error; -// use tokio::sync::oneshot::error::RecvError; + +// error when attempting to tell a request handler whether to release or deny crednetials +pub enum SendResponseError { + NotFound, // no request with the given id + Abandoned, // request has already been closed by client +} + +impl Display for SendResponseError { + fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> { + use SendResponseError::*; + match self { + NotFound => write!(f, "The specified command was not found."), + Abandoned => write!(f, "The specified request was closed by the client."), + } + } +} -// Represents errors encountered while handling an HTTP request +// errors encountered while handling an HTTP request pub enum RequestError { StreamIOError(std::io::Error), InvalidUtf8, @@ -23,11 +38,6 @@ impl From for RequestError { RequestError::InvalidUtf8 } } -// impl From for RequestError { -// fn from (_e: RecvError) -> RequestError { -// RequestError:: -// } -// } impl Display for RequestError { fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> { diff --git a/src-tauri/src/ipc.rs b/src-tauri/src/ipc.rs index e353283..1527d25 100644 --- a/src-tauri/src/ipc.rs +++ b/src-tauri/src/ipc.rs @@ -1,10 +1,35 @@ -pub enum RequestResponse { +use serde::{Serialize, Deserialize}; +use tauri::State; + +use crate::state::AppState; +use crate::storage; + + +#[derive(Serialize, Deserialize)] +pub struct RequestResponse { + pub id: u64, + pub approval: Approval, +} + + +#[derive(Serialize, Deserialize)] +pub enum Approval { Approved, Denied, } -pub struct Request { - pub id: u64, - pub response: RequestResponse, -} \ No newline at end of file +#[tauri::command] +pub fn respond(response: RequestResponse, app_state: State<'_, AppState>) -> Result<(), String> { + app_state.send_response(response) + .map_err(|e| format!("Error responding to request: {e}")) +} + + +#[tauri::command] +pub fn unlock(passphrase: String, app_state: State<'_, AppState>) -> bool { + let root_credentials = storage::load(&passphrase); + // get new session from AWS and store in app state + true +} + diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index f61b328..1c0f872 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -3,28 +3,24 @@ windows_subsystem = "windows" )] -use std::collections::HashMap; use std::str::FromStr; -use std::sync::Mutex; -// use tokio::runtime::Runtime; - -use serde::{Serialize, Deserialize}; -use tauri::{Manager, State}; -use tokio::sync::oneshot; +mod errors; +mod ipc; +mod state; +mod server; mod storage; -mod http; fn main() { + let initial_state = state::AppState::new(state::SessionStatus::Locked, None); + tauri::Builder::default() - .manage(CurrentSession) - .manage(RequestCount) - .manage(OpenRequests) - .invoke_handler(tauri::generate_handler![unlock]) + .manage(initial_state) + .invoke_handler(tauri::generate_handler![ipc::unlock, ipc::respond]) .setup(|app| { let addr = std::net::SocketAddrV4::from_str("127.0.0.1:12345").unwrap(); - tauri::async_runtime::spawn(http::serve(addr, app.handle())); + tauri::async_runtime::spawn(server::serve(addr, app.handle())); Ok(()) }) .run(tauri::generate_context!()) @@ -37,36 +33,4 @@ fn main() { // let creds = std::fs::read_to_string("credentials.json").unwrap(); // storage::save(&creds, "correct horse battery staple"); -} - -#[derive(Serialize, Deserialize)] -pub enum Session { - Unlocked(String), - Locked, - Empty, -} - -type CurrentSession = Mutex; -type RequestCount = Mutex; -type OpenRequests = Mutex>; - -#[derive(Clone, Serialize, Deserialize)] -pub struct CredentialsRequest { - pub request_id: u64, -} - - -// struct Session { -// key_id: String, -// secret_key: String, -// token: String, -// expires: u64, -// } - - -#[tauri::command] -fn unlock(passphrase: String, current_session: State<'_, CurrentSession>) -> bool { - let credentials = storage::load(&passphrase); - *current_session.lock().unwrap() = CredentialStatus::Unlocked(credentials); - true -} +} \ No newline at end of file diff --git a/src-tauri/src/http/mod.rs b/src-tauri/src/server.rs similarity index 55% rename from src-tauri/src/http/mod.rs rename to src-tauri/src/server.rs index 035c164..a7b4191 100644 --- a/src-tauri/src/http/mod.rs +++ b/src-tauri/src/server.rs @@ -2,11 +2,12 @@ use std::io; use std::net::SocketAddrV4; use tokio::net::{TcpListener, TcpStream}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::sync::oneshot; use tauri::{AppHandle, Manager}; -mod errors; -use errors::RequestError; +use crate::errors::RequestError; +use crate::ipc::Approval; pub async fn serve(addr: SocketAddrV4, app_handle: AppHandle) -> io::Result<()> { @@ -30,8 +31,8 @@ pub async fn serve(addr: SocketAddrV4, app_handle: AppHandle) -> io::Result<()> } -// it doesn't really return a String, we just need to placate the compiler -async fn stall(stream: &mut TcpStream) -> Result { +// it doesn't really return Approval, we just need to placate the compiler +async fn stall(stream: &mut TcpStream) -> Result { let delay = std::time::Duration::from_secs(1); loop { tokio::time::sleep(delay).await; @@ -41,6 +42,10 @@ async fn stall(stream: &mut TcpStream) -> Result { async fn handle(mut stream: TcpStream, app_handle: AppHandle) -> Result<(), RequestError> { + let (chan_send, chan_recv) = oneshot::channel(); + let app_state = app_handle.state::(); + let request_id = app_state.register_request(chan_send); + let mut buf = [0; 8192]; // it's what tokio's BufReader uses let mut n = 0; loop { @@ -55,11 +60,23 @@ async fn handle(mut stream: TcpStream, app_handle: AppHandle) -> Result<(), Requ stream.write(b"Content-Type: application/json\r\n").await?; stream.write(b"X-Creddy-delaying-tactic: ").await?; - let creds = tokio::select!{ - r = stall(&mut stream) => r?, // this will never return Ok, just Err if it can't write to the stream - c = get_creds(&app_handle) => c?, + let approval = tokio::select!{ + e = stall(&mut stream) => e?, // this will never return Ok, just Err if it can't write to the stream + r = chan_recv => r.unwrap(), // only panics if the sender is dropped without sending, which shouldn't happen }; + if matches!(approval, Approval::Denied) { + // because we own the stream, it gets closed when we return. + // Unfortunately we've already signaled 200 OK, there's no way around this - + // we have to write the status code first thing, and we have to assume that the user + // might need more time than that gives us (especially if entering the passphrase). + // Fortunately most AWS libs automatically retry if the request dies uncompleted, allowing + // us to respond with a proper error status. + return Ok(()); + } + + let creds = app_state.get_creds_serialized(); + stream.write(b"\r\nContent-Length: ").await?; stream.write(creds.as_bytes().len().to_string().as_bytes()).await?; stream.write(b"\r\n\r\n").await?; @@ -67,39 +84,3 @@ async fn handle(mut stream: TcpStream, app_handle: AppHandle) -> Result<(), Requ stream.write(b"\r\n\r\n").await?; Ok(()) } - - -use tokio::io::{stdin, stdout, BufReader, AsyncBufReadExt}; -use crate::storage; - -use tokio::sync::oneshot; -use std::sync::Mutex; - -async fn get_creds(app_handle: &AppHandle) -> io::Result { - { - let state_guard = app_handle.state::>(); - let mut state = state_guard.lock().unwrap(); - state.num_requests += 1; - let req = crate::CredentialsRequest { - request_id: state.num_requests - }; - app_handle.emit_all("credentials-request", req).unwrap(); - // lock gets released here in case somebody else needs app state while we're waiting - } - - let (tx, rx) = oneshot::channel(); - app_handle.once_global("request-response", |event| { - let response = event.payload().unwrap_or("").to_string(); - tx.send(response).unwrap(); - }); - // Error is only returned if the rx is closed/dropped before receiving, which should never happen - // LOL who am I kidding this happens all the time - // fix it later - - // todo: handle "denied" response - let _response = rx.await.unwrap(); - let state_guard = app_handle.state::>(); - let state = state_guard.lock().unwrap(); - - Ok(state.current_session.clone().unwrap()) -} diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs index 29e3506..67512cb 100644 --- a/src-tauri/src/state.rs +++ b/src-tauri/src/state.rs @@ -1,49 +1,56 @@ +use std::collections::HashMap; use std::sync::RwLock; use serde::{Serialize, Deserialize}; use tokio::sync::oneshot::Sender; use crate::ipc; +use crate::errors::*; #[derive(Serialize, Deserialize)] +#[serde(rename_all = "PascalCase")] +#[serde(untagged)] pub enum Credentials { LongLived { - key_id: String, - secret_key: String, + access_key_id: String, + secret_access_key: String, }, ShortLived { - key_id: String, - secret_key: String, - session_token: String, + access_key_id: String, + secret_access_key: String, + token: String, + expiration: String, }, } #[derive(Serialize, Deserialize)] -pub enum CurrentSession { - Unlocked(String), +pub enum SessionStatus { + Unlocked, Locked, Empty, } pub struct AppState { - current_session: RwLock, + status: RwLock, + credentials: RwLock>, request_count: RwLock, - open_requests: RwLock>, + open_requests: RwLock>>, } impl AppState { - pub fn new(current_session: CurrentSession) -> Self { + pub fn new(status: SessionStatus, creds: Option) -> Self { AppState { - current_session, - request_count: 0, - open_requests: HashMap::new(), + status: RwLock::new(status), + credentials: RwLock::new(creds), + request_count: RwLock::new(0), + open_requests: RwLock::new(HashMap::new()), } } - pub fn register_request(&mut self, chan: Sender) -> u64 { + pub fn register_request(&self, chan: Sender) -> u64 { let count = { let c = self.request_count.write().unwrap(); *c += 1; @@ -51,24 +58,24 @@ impl AppState { }; let open_requests = self.open_requests.write().unwrap(); - self.open_requests.insert(count, chan); - count + open_requests.insert(*count, chan); + *count } - pub fn send_response(&mut self, req_id: u64, response: ipc::RequestResponse) -> Result<(), SendResponseError> { + pub fn send_response(&self, response: ipc::RequestResponse) -> Result<(), SendResponseError> { let mut open_requests = self.open_requests.write().unwrap(); - let chan = self.open_requests - .remove(&req_id) + let chan = open_requests + .remove(&response.id) .ok_or(SendResponseError::NotFound) ?; - chan.send(response) + chan.send(response.approval) .map_err(|_e| SendResponseError::Abandoned) } -} - -pub enum SendResponseError { - NotFound, // no request with the given id - Abandoned, // request has already been closed by client + pub fn get_creds_serialized(&self) -> String { + let creds = self.credentials.read().unwrap(); + // fix this at some point + serde_json::to_string(&creds.unwrap()).unwrap() + } } diff --git a/src-tauri/src/storage.rs b/src-tauri/src/storage.rs index fa0ef28..6e3a5b6 100644 --- a/src-tauri/src/storage.rs +++ b/src-tauri/src/storage.rs @@ -1,5 +1,7 @@ use sodiumoxide::crypto::{pwhash, secretbox}; +use crate::state; + pub fn save(data: &str, passphrase: &str) { let salt = pwhash::Salt([0; 32]); // yes yes, just for now @@ -17,15 +19,24 @@ pub fn save(data: &str, passphrase: &str) { } -pub fn load(passphrase: &str) -> String { - let salt = pwhash::Salt([0; 32]); - let mut kbuf = [0; secretbox::KEYBYTES]; - pwhash::derive_key_interactive(&mut kbuf, passphrase.as_bytes(), &salt) - .expect("Couldn't compute password hash. Are you out of memory?"); - let key = secretbox::Key(kbuf); - let nonce = secretbox::Nonce([0; 24]); +// pub fn load(passphrase: &str) -> String { +// let salt = pwhash::Salt([0; 32]); +// let mut kbuf = [0; secretbox::KEYBYTES]; +// pwhash::derive_key_interactive(&mut kbuf, passphrase.as_bytes(), &salt) +// .expect("Couldn't compute password hash. Are you out of memory?"); +// let key = secretbox::Key(kbuf); +// let nonce = secretbox::Nonce([0; 24]); - let encrypted = std::fs::read("credentials.enc").expect("Failed to read file."); - let decrypted = secretbox::open(&encrypted, &nonce, &key).expect("Failed to decrypt."); - String::from_utf8(decrypted).expect("Invalid utf-8") +// let encrypted = std::fs::read("credentials.enc").expect("Failed to read file."); +// let decrypted = secretbox::open(&encrypted, &nonce, &key).expect("Failed to decrypt."); +// String::from_utf8(decrypted).expect("Invalid utf-8") +// } + +pub fn load(passphrase: &str) -> state::Credentials { + state::Credentials::ShortLived { + access_key_id: "ASIAZ7WSVLORKQI27QGB".to_string(), + secret_access_key: "blah".to_string(), + token: "gah".to_string(), + expiration: "2022-11-29T10:45:12Z".to_string(), + } }