Implement better error system and segregate errors and commands (#23)
* chore: Progress on amend_settings command Signed-off-by: quexeky <git@quexeky.dev> * chore(errors): Progress on better error handling with segragation of files * chore: Progress on amend_settings command Signed-off-by: quexeky <git@quexeky.dev> * chore(commands): Separated commands under each subdirectory into respective commands.rs files Signed-off-by: quexeky <git@quexeky.dev> * chore(errors): Almost all errors and commands have been segregated * chore(errors): Added drop server error Signed-off-by: quexeky <git@quexeky.dev> * feat(core): Update to using nightly compiler Signed-off-by: quexeky <git@quexeky.dev> * chore(errors): More progress on error handling Signed-off-by: quexeky <git@quexeky.dev> * chore(errors): Implementing Try and FromResidual for UserValue Signed-off-by: quexeky <git@quexeky.dev> * refactor(errors): Segregated errors and commands from code, and made commands return UserValue struct Signed-off-by: quexeky <git@quexeky.dev> * fix(errors): Added missing files * chore(errors): Convert match statement to map_err * feat(settings): Implemented settings editing from UI * feat(errors): Clarified return values from retry_connect command * chore(errors): Moved autostart commands to autostart.rs * chore(process manager): Converted launch_process function for games to use game_id --------- Signed-off-by: quexeky <git@quexeky.dev>
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
use std::{env, sync::Mutex};
|
||||
|
||||
use chrono::Utc;
|
||||
use log::{debug, error, info, warn};
|
||||
use openssl::{ec::EcKey, hash::MessageDigest, pkey::PKey, sign::Signer};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::{AppHandle, Emitter, Manager};
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
database::db::{DatabaseAuth, DatabaseImpls},
|
||||
error::{drop_server_error::DropServerError, remote_access_error::RemoteAccessError},
|
||||
AppState, AppStatus, User, DB,
|
||||
};
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct InitiateRequestBody {
|
||||
name: String,
|
||||
platform: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct HandshakeRequestBody {
|
||||
client_id: String,
|
||||
token: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct HandshakeResponse {
|
||||
private: String,
|
||||
certificate: String,
|
||||
id: String,
|
||||
}
|
||||
|
||||
// TODO: Change return value on Err
|
||||
pub fn sign_nonce(private_key: String, nonce: String) -> Result<String, ()> {
|
||||
let client_private_key = EcKey::private_key_from_pem(private_key.as_bytes()).unwrap();
|
||||
let pkey_private_key = PKey::from_ec_key(client_private_key).unwrap();
|
||||
|
||||
let mut signer = Signer::new(MessageDigest::sha256(), &pkey_private_key).unwrap();
|
||||
signer.update(nonce.as_bytes()).unwrap();
|
||||
let signature = signer.sign_to_vec().unwrap();
|
||||
|
||||
let hex_signature = hex::encode(signature);
|
||||
|
||||
Ok(hex_signature)
|
||||
}
|
||||
|
||||
pub fn generate_authorization_header() -> String {
|
||||
let certs = {
|
||||
let db = DB.borrow_data().unwrap();
|
||||
db.auth.clone().unwrap()
|
||||
};
|
||||
|
||||
let nonce = Utc::now().timestamp_millis().to_string();
|
||||
|
||||
let signature = sign_nonce(certs.private, nonce.clone()).unwrap();
|
||||
|
||||
format!("Nonce {} {} {}", certs.client_id, nonce, signature)
|
||||
}
|
||||
|
||||
pub fn fetch_user() -> Result<User, RemoteAccessError> {
|
||||
let base_url = DB.fetch_base_url();
|
||||
|
||||
let endpoint = base_url.join("/api/v1/client/user")?;
|
||||
let header = generate_authorization_header();
|
||||
|
||||
let client = reqwest::blocking::Client::new();
|
||||
let response = client
|
||||
.get(endpoint.to_string())
|
||||
.header("Authorization", header)
|
||||
.send()?;
|
||||
if response.status() != 200 {
|
||||
let err: DropServerError = response.json().unwrap();
|
||||
warn!("{:?}", err);
|
||||
|
||||
if err.status_message == "Nonce expired" {
|
||||
return Err(RemoteAccessError::OutOfSync);
|
||||
}
|
||||
|
||||
return Err(RemoteAccessError::InvalidResponse(err));
|
||||
}
|
||||
|
||||
response.json::<User>().map_err(|e| e.into())
|
||||
}
|
||||
|
||||
fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAccessError> {
|
||||
let path_chunks: Vec<&str> = path.split("/").collect();
|
||||
if path_chunks.len() != 3 {
|
||||
app.emit("auth/failed", ()).unwrap();
|
||||
return Err(RemoteAccessError::HandshakeFailed(
|
||||
"failed to parse token".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let base_url = {
|
||||
let handle = DB.borrow_data().unwrap();
|
||||
Url::parse(handle.base_url.as_str())?
|
||||
};
|
||||
|
||||
let client_id = path_chunks.get(1).unwrap();
|
||||
let token = path_chunks.get(2).unwrap();
|
||||
let body = HandshakeRequestBody {
|
||||
client_id: client_id.to_string(),
|
||||
token: token.to_string(),
|
||||
};
|
||||
|
||||
let endpoint = base_url.join("/api/v1/client/auth/handshake")?;
|
||||
let client = reqwest::blocking::Client::new();
|
||||
let response = client.post(endpoint).json(&body).send()?;
|
||||
debug!("Handshake responsded with {}", response.status().as_u16());
|
||||
let response_struct: HandshakeResponse = response.json()?;
|
||||
|
||||
{
|
||||
let mut handle = DB.borrow_data_mut().unwrap();
|
||||
handle.auth = Some(DatabaseAuth {
|
||||
private: response_struct.private,
|
||||
cert: response_struct.certificate,
|
||||
client_id: response_struct.id,
|
||||
});
|
||||
drop(handle);
|
||||
DB.save().unwrap();
|
||||
}
|
||||
|
||||
{
|
||||
let app_state = app.state::<Mutex<AppState>>();
|
||||
let mut app_state_handle = app_state.lock().unwrap();
|
||||
app_state_handle.status = AppStatus::SignedIn;
|
||||
app_state_handle.user = Some(fetch_user()?);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn recieve_handshake(app: AppHandle, path: String) {
|
||||
// Tell the app we're processing
|
||||
app.emit("auth/processing", ()).unwrap();
|
||||
|
||||
let handshake_result = recieve_handshake_logic(&app, path);
|
||||
if let Err(e) = handshake_result {
|
||||
warn!("error with authentication: {}", e);
|
||||
app.emit("auth/failed", e.to_string()).unwrap();
|
||||
return;
|
||||
}
|
||||
|
||||
app.emit("auth/finished", ()).unwrap();
|
||||
}
|
||||
|
||||
pub fn auth_initiate_logic() -> Result<(), RemoteAccessError> {
|
||||
let base_url = {
|
||||
let db_lock = DB.borrow_data().unwrap();
|
||||
Url::parse(&db_lock.base_url.clone())?
|
||||
};
|
||||
|
||||
let endpoint = base_url.join("/api/v1/client/auth/initiate")?;
|
||||
let body = InitiateRequestBody {
|
||||
name: "Drop Desktop Client".to_string(),
|
||||
platform: env::consts::OS.to_string(),
|
||||
};
|
||||
|
||||
let client = reqwest::blocking::Client::new();
|
||||
let response = client.post(endpoint.to_string()).json(&body).send()?;
|
||||
|
||||
if response.status() != 200 {
|
||||
let data: DropServerError = response.json()?;
|
||||
error!("Could not start handshake: {}", data.status_message);
|
||||
|
||||
return Err(RemoteAccessError::HandshakeFailed(data.status_message));
|
||||
}
|
||||
|
||||
let redir_url = response.text()?;
|
||||
let complete_redir_url = base_url.join(&redir_url)?;
|
||||
|
||||
debug!("opening web browser to continue authentication");
|
||||
webbrowser::open(complete_redir_url.as_ref()).unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn setup() -> Result<(AppStatus, Option<User>), RemoteAccessError> {
|
||||
let data = DB.borrow_data().unwrap();
|
||||
let auth = data.auth.clone();
|
||||
drop(data);
|
||||
|
||||
if auth.is_some() {
|
||||
let user_result = fetch_user()?;
|
||||
return Ok((AppStatus::SignedIn, Some(user_result)));
|
||||
}
|
||||
|
||||
Ok((AppStatus::SignedOut, None))
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
use std::sync::Mutex;
|
||||
|
||||
use tauri::{AppHandle, Emitter, Manager};
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
error::{remote_access_error::RemoteAccessError, user_error::UserValue},
|
||||
AppState, AppStatus, DB,
|
||||
};
|
||||
|
||||
use super::{
|
||||
auth::{auth_initiate_logic, recieve_handshake, setup},
|
||||
remote::use_remote_logic,
|
||||
};
|
||||
|
||||
#[tauri::command]
|
||||
pub fn use_remote(
|
||||
url: String,
|
||||
state: tauri::State<'_, Mutex<AppState<'_>>>,
|
||||
) -> UserValue<(), RemoteAccessError> {
|
||||
UserValue::Ok(use_remote_logic(url, state)?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn gen_drop_url(path: String) -> UserValue<String, RemoteAccessError> {
|
||||
let base_url = {
|
||||
let handle = DB.borrow_data().unwrap();
|
||||
|
||||
Url::parse(&handle.base_url).map_err(RemoteAccessError::ParsingError)?
|
||||
};
|
||||
|
||||
let url = base_url.join(&path).unwrap();
|
||||
|
||||
UserValue::Ok(url.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn sign_out(app: AppHandle) {
|
||||
// Clear auth from database
|
||||
{
|
||||
let mut handle = DB.borrow_data_mut().unwrap();
|
||||
handle.auth = None;
|
||||
drop(handle);
|
||||
DB.save().unwrap();
|
||||
}
|
||||
|
||||
// Update app state
|
||||
{
|
||||
let app_state = app.state::<Mutex<AppState>>();
|
||||
let mut app_state_handle = app_state.lock().unwrap();
|
||||
app_state_handle.status = AppStatus::SignedOut;
|
||||
app_state_handle.user = None;
|
||||
}
|
||||
|
||||
// Emit event for frontend
|
||||
app.emit("auth/signedout", ()).unwrap();
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn retry_connect(state: tauri::State<'_, Mutex<AppState>>) -> UserValue<(), RemoteAccessError> {
|
||||
let (app_status, user) = setup()?;
|
||||
|
||||
let mut guard = state.lock().unwrap();
|
||||
guard.status = app_status;
|
||||
guard.user = user;
|
||||
drop(guard);
|
||||
UserValue::Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn auth_initiate() -> UserValue<(), RemoteAccessError> {
|
||||
auth_initiate_logic().into()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn manual_recieve_handshake(app: AppHandle, token: String) {
|
||||
recieve_handshake(app, format!("handshake/{}", token));
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod auth;
|
||||
pub mod commands;
|
||||
pub mod remote;
|
||||
@@ -0,0 +1,49 @@
|
||||
use std::{
|
||||
error::Error,
|
||||
fmt::{Display, Formatter},
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use http::StatusCode;
|
||||
use log::{info, warn};
|
||||
use serde::Deserialize;
|
||||
use url::{ParseError, Url};
|
||||
|
||||
use crate::{error::remote_access_error::RemoteAccessError, AppState, AppStatus, DB};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct DropHealthcheck {
|
||||
app_name: String,
|
||||
}
|
||||
|
||||
pub fn use_remote_logic(
|
||||
url: String,
|
||||
state: tauri::State<'_, Mutex<AppState<'_>>>,
|
||||
) -> Result<(), RemoteAccessError> {
|
||||
info!("connecting to url {}", url);
|
||||
let base_url = Url::parse(&url)?;
|
||||
|
||||
// Test Drop url
|
||||
let test_endpoint = base_url.join("/api/v1")?;
|
||||
let response = reqwest::blocking::get(test_endpoint.to_string())?;
|
||||
|
||||
let result: DropHealthcheck = response.json()?;
|
||||
|
||||
if result.app_name != "Drop" {
|
||||
warn!("user entered drop endpoint that connected, but wasn't identified as Drop");
|
||||
return Err(RemoteAccessError::InvalidEndpoint);
|
||||
}
|
||||
|
||||
let mut app_state = state.lock().unwrap();
|
||||
app_state.status = AppStatus::SignedOut;
|
||||
drop(app_state);
|
||||
|
||||
let mut db_state = DB.borrow_data_mut().unwrap();
|
||||
db_state.base_url = base_url.to_string();
|
||||
drop(db_state);
|
||||
|
||||
DB.save().unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user