mirror of
https://github.com/ByteAtATime/raycast-linux.git
synced 2025-12-23 10:11:57 +00:00
feat: implement PKCEClient.authorize method
This commit is contained in:
parent
1f76b6e8ef
commit
18a46dfb72
11 changed files with 153 additions and 6 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -1318,6 +1318,7 @@ dependencies = [
|
|||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"url",
|
||||
"walkdir",
|
||||
"webbrowser",
|
||||
"which",
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<void> => {
|
|||
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 };
|
||||
|
|
|
|||
|
|
@ -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<String> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
|
|
@ -22,6 +22,32 @@ pub fn get_current() -> Option<String> {
|
|||
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<dyn std::error::Error>> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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<Mutex<HashMap<usize, Rectangle>>> =
|
|||
pub static CLIPBOARD: LazyLock<Mutex<Option<arboard::Clipboard>>> =
|
||||
LazyLock::new(|| Mutex::new(None));
|
||||
|
||||
pub static OAUTH_PENDING: LazyLock<Mutex<HashMap<String, (u32, Transport)>>> =
|
||||
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 || {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
29
src/ipc.rs
29
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<dyn std::error::Error>> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn send_oauth_redirect(url: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
17
src/main.rs
17
src/main.rs
|
|
@ -45,6 +45,8 @@ use crate::view::view;
|
|||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Option<Command>,
|
||||
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
|
||||
args: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
|
|
@ -107,6 +109,21 @@ fn subscription(state: &State) -> Subscription<Message> {
|
|||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
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() {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -172,4 +172,10 @@ pub enum SidecarResponse {
|
|||
id: u32,
|
||||
url: String,
|
||||
},
|
||||
#[serde(rename = "oauthAuthorize")]
|
||||
OAuthAuthorize {
|
||||
id: u32,
|
||||
url: String,
|
||||
state: String,
|
||||
},
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue