diff --git a/components/InitiateAuthModule.vue b/components/InitiateAuthModule.vue index 8aa6bf7..4a1e290 100644 --- a/components/InitiateAuthModule.vue +++ b/components/InitiateAuthModule.vue @@ -13,11 +13,7 @@
- + + Use a code → + - +

Having trouble?

@@ -121,6 +126,7 @@ diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 65da817..c321c4c 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -345,6 +345,22 @@ dependencies = [ "syn 2.0.101", ] +[[package]] +name = "async-tungstenite" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef0f7efedeac57d9b26170f72965ecfd31473ca52ca7a64e925b0b6f5f079886" +dependencies = [ + "atomic-waker", + "futures-core", + "futures-io", + "futures-task", + "futures-util", + "log", + "pin-project-lite", + "tungstenite", +] + [[package]] name = "atk" version = "0.18.2" @@ -1280,6 +1296,7 @@ dependencies = [ "droplet-rs", "dynfmt", "filetime", + "futures-lite", "gethostname", "hex 0.4.3", "http 1.3.1", @@ -1296,6 +1313,7 @@ dependencies = [ "reqwest 0.12.16", "reqwest-middleware 0.4.2", "reqwest-middleware-cache", + "reqwest-websocket", "rustbreak", "rustix 0.38.44", "schemars", @@ -4357,6 +4375,24 @@ dependencies = [ "url", ] +[[package]] +name = "reqwest-websocket" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f91a811daaa8b54faeaec9d507a336897a3d243834a4965254a17d39da8b5c9" +dependencies = [ + "async-tungstenite", + "bytes", + "futures-util", + "reqwest 0.12.16", + "thiserror 2.0.12", + "tokio", + "tokio-util", + "tracing", + "tungstenite", + "web-sys", +] + [[package]] name = "rfd" version = "0.15.3" @@ -5831,6 +5867,7 @@ checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" dependencies = [ "bytes", "futures-core", + "futures-io", "futures-sink", "pin-project-lite", "tokio", @@ -6009,6 +6046,23 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" +dependencies = [ + "bytes", + "data-encoding", + "http 1.3.1", + "httparse", + "log", + "rand 0.9.1", + "sha1", + "thiserror 2.0.12", + "utf-8", +] + [[package]] name = "typeid" version = "1.0.3" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index fe8ae93..c0909e7 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -68,6 +68,8 @@ known-folders = "1.2.0" native_model = { version = "0.6.1", features = ["rmp_serde_1_3"] } tauri-plugin-opener = "2.4.0" bitcode = "0.6.6" +reqwest-websocket = "0.5.0" +futures-lite = "2.6.0" # tailscale = { path = "./tailscale" } [dependencies.dynfmt] diff --git a/src-tauri/src/error/remote_access_error.rs b/src-tauri/src/error/remote_access_error.rs index 1d60937..fdc4b0c 100644 --- a/src-tauri/src/error/remote_access_error.rs +++ b/src-tauri/src/error/remote_access_error.rs @@ -13,6 +13,7 @@ use super::drop_server_error::DropServerError; #[derive(Debug, SerializeDisplay)] pub enum RemoteAccessError { FetchError(Arc), + FetchErrorWS(Arc), ParsingError(ParseError), InvalidEndpoint, HandshakeFailed(String), @@ -29,7 +30,10 @@ impl Display for RemoteAccessError { match self { RemoteAccessError::FetchError(error) => { if error.is_connect() { - return write!(f, "Failed to connect to Drop server. Check if you access Drop through a browser, and then try again."); + return write!( + f, + "Failed to connect to Drop server. Check if you access Drop through a browser, and then try again." + ); } write!( @@ -42,20 +46,40 @@ impl Display for RemoteAccessError { .or_else(|| Some("Unknown error".to_string())) .unwrap() ) - }, + } + RemoteAccessError::FetchErrorWS(error) => write!( + f, + "{}: {}", + error, + error + .source() + .map(|e| e.to_string()) + .or_else(|| Some("Unknown error".to_string())) + .unwrap() + ), RemoteAccessError::ParsingError(parse_error) => { write!(f, "{parse_error}") } RemoteAccessError::InvalidEndpoint => write!(f, "invalid drop endpoint"), - RemoteAccessError::HandshakeFailed(message) => write!(f, "failed to complete handshake: {message}"), + RemoteAccessError::HandshakeFailed(message) => { + write!(f, "failed to complete handshake: {message}") + } RemoteAccessError::GameNotFound(id) => write!(f, "could not find game on server: {id}"), - RemoteAccessError::InvalidResponse(error) => write!(f, "server returned an invalid response: {}, {}", error.status_code, error.status_message), - RemoteAccessError::UnparseableResponse(error) => write!(f, "server returned an invalid response: {error}"), - RemoteAccessError::ManifestDownloadFailed(status, response) => write!( + RemoteAccessError::InvalidResponse(error) => write!( f, - "failed to download game manifest: {status} {response}" + "server returned an invalid response: {}, {}", + error.status_code, error.status_message + ), + RemoteAccessError::UnparseableResponse(error) => { + write!(f, "server returned an invalid response: {error}") + } + RemoteAccessError::ManifestDownloadFailed(status, response) => { + write!(f, "failed to download game manifest: {status} {response}") + } + RemoteAccessError::OutOfSync => write!( + f, + "server's and client's time are out of sync. Please ensure they are within at least 30 seconds of each other" ), - RemoteAccessError::OutOfSync => write!(f, "server's and client's time are out of sync. Please ensure they are within at least 30 seconds of each other"), RemoteAccessError::Cache(error) => write!(f, "Cache Error: {error}"), } } @@ -66,6 +90,11 @@ impl From for RemoteAccessError { RemoteAccessError::FetchError(Arc::new(err)) } } +impl From for RemoteAccessError { + fn from(err: reqwest_websocket::Error) -> Self { + RemoteAccessError::FetchErrorWS(Arc::new(err)) + } +} impl From for RemoteAccessError { fn from(err: ParseError) -> Self { RemoteAccessError::ParsingError(err) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 0049dfb..13f6525 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -12,6 +12,7 @@ mod process; mod remote; use crate::process::commands::open_process_logs; +use crate::remote::commands::auth_initiate_code; use crate::{database::db::DatabaseImpls, games::downloads::commands::resume_download}; use bitcode::{Decode, Encode}; use client::commands::fetch_state; @@ -267,6 +268,7 @@ pub fn run() { fetch_settings, // Auth auth_initiate, + auth_initiate_code, retry_connect, manual_recieve_handshake, sign_out, diff --git a/src-tauri/src/remote/auth.rs b/src-tauri/src/remote/auth.rs index 3f4b63b..712f323 100644 --- a/src-tauri/src/remote/auth.rs +++ b/src-tauri/src/remote/auth.rs @@ -9,12 +9,12 @@ use tauri::{AppHandle, Emitter, Manager}; use url::Url; use crate::{ + AppState, AppStatus, User, database::{ db::{borrow_db_checked, borrow_db_mut_checked}, models::data::DatabaseAuth, }, error::{drop_server_error::DropServerError, remote_access_error::RemoteAccessError}, - AppState, AppStatus, User, }; use super::{ @@ -32,6 +32,7 @@ struct InitiateRequestBody { name: String, platform: String, capabilities: HashMap, + mode: String, } #[derive(Serialize)] @@ -166,7 +167,7 @@ pub fn recieve_handshake(app: AppHandle, path: String) { app.emit("auth/finished", ()).unwrap(); } -pub fn auth_initiate_logic() -> Result<(), RemoteAccessError> { +pub fn auth_initiate_logic(mode: String) -> Result { let base_url = { let db_lock = borrow_db_checked(); Url::parse(&db_lock.base_url.clone())? @@ -182,6 +183,7 @@ pub fn auth_initiate_logic() -> Result<(), RemoteAccessError> { ("peerAPI".to_owned(), CapabilityConfiguration {}), ("cloudSaves".to_owned(), CapabilityConfiguration {}), ]), + mode, }; let client = reqwest::blocking::Client::new(); @@ -194,13 +196,9 @@ pub fn auth_initiate_logic() -> Result<(), RemoteAccessError> { return Err(RemoteAccessError::HandshakeFailed(data.status_message)); } - let redir_url = response.text()?; - let complete_redir_url = base_url.join(&redir_url)?; + let response = response.text()?; - debug!("opening web browser to continue authentication"); - webbrowser::open(complete_redir_url.as_ref()).unwrap(); - - Ok(()) + Ok(response) } pub fn setup() -> (AppStatus, Option) { diff --git a/src-tauri/src/remote/commands.rs b/src-tauri/src/remote/commands.rs index 81462ef..8b4e98e 100644 --- a/src-tauri/src/remote/commands.rs +++ b/src-tauri/src/remote/commands.rs @@ -1,15 +1,18 @@ use std::sync::Mutex; -use log::debug; +use futures_lite::StreamExt; +use log::{debug, warn}; use reqwest::blocking::Client; +use reqwest_websocket::{Message, RequestBuilderExt}; +use serde::Deserialize; use tauri::{AppHandle, Emitter, Manager}; use url::Url; use crate::{ + AppState, AppStatus, database::db::{borrow_db_checked, borrow_db_mut_checked}, error::remote_access_error::RemoteAccessError, remote::{auth::generate_authorization_header, requests::make_request}, - AppState, AppStatus, }; use super::{ @@ -91,7 +94,78 @@ pub fn retry_connect(state: tauri::State<'_, Mutex>) { #[tauri::command] pub fn auth_initiate() -> Result<(), RemoteAccessError> { - auth_initiate_logic() + let base_url = { + let db_lock = borrow_db_checked(); + Url::parse(&db_lock.base_url.clone())? + }; + + let redir_url = auth_initiate_logic("callback".to_string())?; + 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(()) +} + +#[derive(Deserialize)] +struct CodeWebsocketResponse { + #[serde(rename = "type")] + response_type: String, + value: String, +} + +#[tauri::command] +pub fn auth_initiate_code(app: AppHandle) -> Result { + let base_url = { + let db_lock = borrow_db_checked(); + Url::parse(&db_lock.base_url.clone())? + }; + + let code = auth_initiate_logic("code".to_string())?; + let header_code = code.clone(); + + tauri::async_runtime::spawn(async move { + let load = async || -> Result<(), RemoteAccessError> { + let ws_url = base_url.join("/api/v1/client/auth/code/ws")?; + let response = reqwest::Client::default() + .get(ws_url) + .header("Authorization", header_code) + .upgrade() + .send() + .await?; + + let mut websocket = response.into_websocket().await?; + + while let Some(token) = websocket.try_next().await? { + if let Message::Text(response) = token { + let response = serde_json::from_str::(&response) + .map_err(|e| RemoteAccessError::UnparseableResponse(e.to_string()))?; + match response.response_type.as_str() { + "token" => { + let recieve_app = app.clone(); + tauri::async_runtime::spawn_blocking(move || { + manual_recieve_handshake(recieve_app, response.value); + }); + return Ok(()); + } + _ => return Err(RemoteAccessError::HandshakeFailed(response.value)), + } + } + } + + Err(RemoteAccessError::HandshakeFailed( + "Failed to connect to websocket".to_string(), + )) + }; + + let result = load().await; + if let Err(err) = result { + warn!("{err}"); + app.emit("auth/failed", err.to_string()).unwrap(); + } + }); + + Ok(code) } #[tauri::command]