diff --git a/Cargo.lock b/Cargo.lock index 325ca5c..a5ac219 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1318,6 +1318,7 @@ dependencies = [ "serde", "serde_json", "tokio", + "url", "walkdir", "webbrowser", "which", diff --git a/Cargo.toml b/Cargo.toml index 069738a..1866309 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ which = "8.0.0" rust-ini = "0.21" freedesktop-icons = "0.4.0" blake3 = "1.8.2" +url = "2.5.7" # implementation ~stolen~ inspired by Tauri - we probably don't need this [target.'cfg(windows)'.dependencies] diff --git a/renderer/src/api/oauth.ts b/renderer/src/api/oauth.ts index 279c29d..a2924c8 100644 --- a/renderer/src/api/oauth.ts +++ b/renderer/src/api/oauth.ts @@ -1,4 +1,5 @@ -import { randomBytes, createHash } from "node:crypto"; +import { randomBytes, createHash, randomUUID } from "node:crypto"; +import * as protocol from "../protocol"; export enum RedirectMethod { Web = "web", @@ -51,7 +52,14 @@ export class PKCEClient { }> { const { codeChallenge, codeVerifier } = generateChallenge(); - const state = base64URLEncode(randomBytes(16)); // i have no idea how oauth works please help + // TODO: figure out what is required in here + const state = btoa( + JSON.stringify({ + providerName: "temp value", + id: randomUUID(), + flavor: "release", + }) + ); let redirectURI = ""; switch (this.options.redirectMethod) { @@ -89,4 +97,19 @@ export class PKCEClient { toURL: () => url, }; } + + public async authorize( + options: + | { + url: string; + } + | { toURL: () => string } + ): Promise<{ authorizationCode: string }> { + const url = "url" in options ? options.url : options.toURL(); + + const parsedUrl = new URL(url); + const state = parsedUrl.searchParams.get("state") ?? ""; + + return protocol.oauthAuthorize(url, state); + } } diff --git a/renderer/src/protocol.ts b/renderer/src/protocol.ts index ea35425..c15405e 100644 --- a/renderer/src/protocol.ts +++ b/renderer/src/protocol.ts @@ -25,7 +25,8 @@ type RustRequest = | { type: "clipboardCopy"; content: ClipboardContent; concealed: boolean } | { type: "clipboardClear" } | { type: "clipboardRead"; offset?: number } - | { type: "openUrl"; url: string }; + | { type: "openUrl"; url: string } + | { type: "oauthAuthorize"; url: string; state: string }; type RustResponse = | { type: "success"; result?: unknown } @@ -186,6 +187,14 @@ export const openUrl = async (url: string): Promise => { await sendRequest({ type: "openUrl", url }); }; +export const oauthAuthorize = async ( + url: string, + state: string +): Promise<{ authorizationCode: string }> => { + const result = await sendRequest({ type: "oauthAuthorize", url, state }); + return result as { authorizationCode: string }; +}; + export const handleRustResponse = (data: Buffer) => { try { const response = unpack(data) as RustResponse & { id: number }; diff --git a/src/deep_link.rs b/src/deep_link.rs index 8c62d67..b68ef5e 100644 --- a/src/deep_link.rs +++ b/src/deep_link.rs @@ -8,7 +8,7 @@ use std::{ #[cfg(windows)] use windows_registry::{CLASSES_ROOT, CURRENT_USER, LOCAL_MACHINE}; -const SCHEMES: &[&str] = &["flare", "raycast"]; +pub const SCHEMES: &[&str] = &["flare", "raycast"]; pub fn get_current() -> Option { let args: Vec = std::env::args().collect(); @@ -22,6 +22,32 @@ pub fn get_current() -> Option { None } +pub fn is_oauth_redirect(url: &str) -> bool { + url.starts_with("raycast://oauth") || url.starts_with("flare://oauth") +} + +pub fn handle_oauth_redirect(url: &str) -> bool { + if !is_oauth_redirect(url) { + return false; + } + + let url = match url::Url::parse(url) { + Ok(u) => u, + Err(_) => return false, + }; + + let params: std::collections::HashMap<_, _> = url.query_pairs().collect(); + + let state = params.get("state").map(|s| s.to_string()); + let code = params.get("code").map(|c| c.to_string()); + + if let (Some(state), Some(code)) = (state, code) { + return crate::handlers::oauth::complete(&state, &code); + } + + false +} + pub fn register_all() -> Result<(), Box> { #[cfg(windows)] { diff --git a/src/globals.rs b/src/globals.rs index f49f72c..7a1bfe3 100644 --- a/src/globals.rs +++ b/src/globals.rs @@ -1,5 +1,6 @@ use crate::message::Message; use crate::runtime::SidecarRuntime; +use crate::transport::Transport; use iced::Rectangle; use iced::futures::SinkExt; use iced::futures::channel::mpsc; @@ -23,6 +24,9 @@ pub static LAYOUT_CACHE: LazyLock>> = pub static CLIPBOARD: LazyLock>> = LazyLock::new(|| Mutex::new(None)); +pub static OAUTH_PENDING: LazyLock>> = + LazyLock::new(|| Mutex::new(HashMap::new())); + pub fn send_callback(callback_id: String, value: Value) { if let Some(mut sender) = RUNTIME_SENDER.lock().unwrap().clone() { std::thread::spawn(move || { diff --git a/src/handlers.rs b/src/handlers.rs index b88e005..a5f4f65 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -166,3 +166,33 @@ pub mod clipboard { .map(Some) } } + +pub mod oauth { + use crate::transport::Transport; + + use super::*; + + pub fn authorize(id: u32, url: String, state: String, transport: &Transport) { + globals::OAUTH_PENDING + .lock() + .unwrap() + .insert(state, (id, transport.clone())); + + let _ = crate::utils::open_url(&url); + } + + pub fn complete(state: &str, code: &str) -> bool { + let entry = globals::OAUTH_PENDING.lock().unwrap().remove(state); + + if let Some((id, transport)) = entry { + let response = crate::types::RustResponse::Success { + id, + result: Some(serde_json::json!({ "authorizationCode": code })), + }; + let _ = transport.send(&response); + true + } else { + false + } + } +} diff --git a/src/ipc.rs b/src/ipc.rs index 816dbed..99c31a3 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -3,6 +3,7 @@ use std::os::unix::net::{UnixListener, UnixStream}; use std::path::PathBuf; const IPC_COMMAND_TOGGLE: &[u8] = b"toggle"; +const IPC_COMMAND_OAUTH_PREFIX: &[u8] = b"oauth:"; const IPC_RESPONSE_OK: &[u8] = b"ok"; fn socket_path() -> PathBuf { @@ -26,8 +27,26 @@ pub fn send_toggle() -> Result<(), Box> { } } +pub fn send_oauth_redirect(url: &str) -> Result<(), Box> { + let path = socket_path(); + let mut stream = UnixStream::connect(&path)?; + + let mut msg = Vec::from(IPC_COMMAND_OAUTH_PREFIX); + msg.extend_from_slice(url.as_bytes()); + stream.write_all(&msg)?; + + let mut response = [0u8; 16]; + let n = stream.read(&mut response)?; + if &response[..n] == IPC_RESPONSE_OK { + Ok(()) + } else { + Err("Unexpected response from daemon".into()) + } +} + pub fn is_daemon_running() -> bool { let path = socket_path(); + println!("Checking daemon socket at: {:?}", path); UnixStream::connect(&path).is_ok() } @@ -46,11 +65,17 @@ where std::thread::spawn(move || { for stream in listener.incoming() { if let Ok(mut stream) = stream { - let mut buf = [0u8; 64]; + let mut buf = [0u8; 2048]; if let Ok(n) = stream.read(&mut buf) { - if &buf[..n] == IPC_COMMAND_TOGGLE { + let data = &buf[..n]; + if data == IPC_COMMAND_TOGGLE { on_toggle(); let _ = stream.write_all(IPC_RESPONSE_OK); + } else if data.starts_with(IPC_COMMAND_OAUTH_PREFIX) { + let url = std::str::from_utf8(&data[IPC_COMMAND_OAUTH_PREFIX.len()..]) + .unwrap_or(""); + crate::deep_link::handle_oauth_redirect(url); + let _ = stream.write_all(IPC_RESPONSE_OK); } } } diff --git a/src/main.rs b/src/main.rs index 2309abf..9f9fcb5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -45,6 +45,8 @@ use crate::view::view; struct Cli { #[command(subcommand)] command: Option, + #[arg(trailing_var_arg = true, allow_hyphen_values = true)] + args: Vec, } #[derive(Subcommand)] @@ -107,6 +109,21 @@ fn subscription(state: &State) -> Subscription { fn main() -> Result<(), Box> { let cli = Cli::parse(); + let deep_link = if cli.args.is_empty() { + deep_link::get_current() + } else { + cli.args.first().cloned() + }; + + if let Some(link) = &deep_link { + if deep_link::is_oauth_redirect(link) { + if ipc::is_daemon_running() { + ipc::send_oauth_redirect(link)?; + } + return Ok(()); + } + } + match cli.command { Some(Command::Toggle) => { if ipc::is_daemon_running() { diff --git a/src/runtime.rs b/src/runtime.rs index 04b974a..dba0ba5 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -132,6 +132,11 @@ fn handle_sidecar_response( } => (id, handlers::clipboard::copy(content, concealed)), SidecarResponse::ClipboardClear { id } => (id, handlers::clipboard::clear()), SidecarResponse::ClipboardRead { id, .. } => (id, handlers::clipboard::read()), + + SidecarResponse::OAuthAuthorize { id, url, state } => { + handlers::oauth::authorize(id, url, state, transport); + return Ok(()); + } }; let rust_response = match result { diff --git a/src/types.rs b/src/types.rs index 5dc7cef..33d91e3 100644 --- a/src/types.rs +++ b/src/types.rs @@ -172,4 +172,10 @@ pub enum SidecarResponse { id: u32, url: String, }, + #[serde(rename = "oauthAuthorize")] + OAuthAuthorize { + id: u32, + url: String, + state: String, + }, }