From 0a090c9b2622350c6c64097a00eef7b7be1cbe36 Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Tue, 7 Feb 2023 17:56:26 +0800 Subject: [PATCH] use httpz + localhost server for Linux --- Cargo.lock | 24 ++++++- Cargo.toml | 2 + apps/desktop/src-tauri/Cargo.toml | 5 ++ apps/desktop/src-tauri/src/main.rs | 105 ++++++++++++++++------------- apps/desktop/src/App.tsx | 19 +++++- apps/server/Cargo.toml | 1 + apps/server/src/lib.rs | 1 + apps/server/src/main.rs | 46 ++----------- core/Cargo.toml | 2 +- core/src/custom_uri.rs | 39 ++++++++--- 10 files changed, 144 insertions(+), 100 deletions(-) create mode 100644 apps/server/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 3461aa7a9d38..ef22e3fcc442 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2549,6 +2549,22 @@ dependencies = [ "tokio", ] +[[package]] +name = "httpz" +version = "0.0.3" +source = "git+https://github.com/oscartbeaumont/httpz?rev=a5185f2ed2fdefeb2f582dce38a692a1bf76d1d6#a5185f2ed2fdefeb2f582dce38a692a1bf76d1d6" +dependencies = [ + "axum", + "form_urlencoded", + "futures", + "http", + "hyper", + "percent-encoding", + "tauri", + "thiserror", + "tokio", +] + [[package]] name = "humantime" version = "2.1.0" @@ -5311,7 +5327,7 @@ source = "git+https://github.com/oscartbeaumont/rspc?rev=c03872c0ba29d2429e9c059 dependencies = [ "async-stream", "futures", - "httpz", + "httpz 0.0.3 (git+https://github.com/oscartbeaumont/httpz.git?rev=a5020adecb15b55d84a8330b4680eba82fa6b820)", "serde", "serde_json", "specta 0.0.6", @@ -5558,8 +5574,8 @@ dependencies = [ "futures", "globset", "hostname", - "http", "http-range", + "httpz 0.0.3 (git+https://github.com/oscartbeaumont/httpz?rev=a5185f2ed2fdefeb2f582dce38a692a1bf76d1d6)", "image", "include_dir", "int-enum", @@ -5992,6 +6008,7 @@ dependencies = [ "axum", "ctrlc", "http", + "httpz 0.0.3 (git+https://github.com/oscartbeaumont/httpz?rev=a5185f2ed2fdefeb2f582dce38a692a1bf76d1d6)", "hyper", "rspc", "sd-core", @@ -6171,11 +6188,14 @@ dependencies = [ name = "spacedrive" version = "0.1.0" dependencies = [ + "axum", "http", + "httpz 0.0.3 (git+https://github.com/oscartbeaumont/httpz?rev=a5185f2ed2fdefeb2f582dce38a692a1bf76d1d6)", "percent-encoding", "rspc", "sd-core", "serde", + "server", "swift-rs", "tauri", "tauri-build", diff --git a/Cargo.toml b/Cargo.toml index 61dbf3ed504b..768288e20ea8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ prisma-client-rust-sdk = { git = "https://github.com/Brendonovich/prisma-client- rspc = { version = "0.1.2" } specta = { version = "0.0.6" } +httpz = { version = "0.0.3" } swift-rs = { git = "https://github.com/Brendonovich/swift-rs.git", rev = "833e29ba333f1dfe303eaa21de78c4f8c5a3f2ff" } @@ -41,3 +42,4 @@ openssl-sys = { git = "https://github.com/spacedriveapp/rust-openssl", rev = "92 rspc = { git = "https://github.com/oscartbeaumont/rspc", rev = "c03872c0ba29d2429e9c059dfb235cdd03e15e8c" } # TODO: Move back to crates.io when new jsonrpc executor + `tokio::spawn` in the Tauri IPC plugin + upgraded Tauri version is released specta = { git = "https://github.com/oscartbeaumont/rspc", rev = "c03872c0ba29d2429e9c059dfb235cdd03e15e8c" } +httpz = { git = "https://github.com/oscartbeaumont/httpz", rev = "a5185f2ed2fdefeb2f582dce38a692a1bf76d1d6" } \ No newline at end of file diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index 7e85992ab143..7f95ffeb4bb9 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -12,6 +12,7 @@ build = "build.rs" [dependencies] tauri = { version = "1.2.4", features = ["api-all", "linux-protocol-headers", "macos-private-api"] } rspc = { workspace = true, features = ["tauri"] } +httpz = { workspace = true, features = ["axum", "tauri"] } # TODO: The `axmu` feature should be only enabled on Linux but this currently can't be done: https://github.com/rust-lang/cargo/issues/1197 sd-core = { path = "../../../core", features = ["ffmpeg", "location-watcher"] } tokio = { workspace = true, features = ["sync"] } window-shadows = "0.2.0" @@ -20,6 +21,10 @@ serde = "1.0.145" percent-encoding = "2.2.0" http = "0.2.8" +[target.'cfg(target_os = "linux")'.dependencies] +axum = "0.6.4" +server = { path = "../../server" } + [target.'cfg(target_os = "macos")'.dependencies] swift-rs.workspace = true diff --git a/apps/desktop/src-tauri/src/main.rs b/apps/desktop/src-tauri/src/main.rs index 6a817a86242e..30a3763f9b6b 100644 --- a/apps/desktop/src-tauri/src/main.rs +++ b/apps/desktop/src-tauri/src/main.rs @@ -4,15 +4,15 @@ )] use std::error::Error; +use std::net::SocketAddr; use std::path::PathBuf; use std::time::Duration; -use http::Request; -use sd_core::{custom_uri::handle_custom_uri, Node}; -use tauri::async_runtime::block_on; -use tauri::{api::path, http::ResponseBuilder, Manager, RunEvent}; -use tokio::task::block_in_place; -use tokio::time::sleep; +use sd_core::{custom_uri::create_custom_uri_endpoint, Node}; +use tauri::plugin::TauriPlugin; +use tauri::Runtime; +use tauri::{api::path, async_runtime::block_on, Manager, RunEvent}; +use tokio::{task::block_in_place, time::sleep}; use tracing::{debug, error}; #[cfg(target_os = "macos")] @@ -27,6 +27,15 @@ async fn app_ready(app_handle: tauri::AppHandle) { window.show().unwrap(); } +pub fn spacedrive_plugin_init(listen_addr: SocketAddr) -> TauriPlugin { + tauri::plugin::Builder::new("spacedrive") + .js_init_script(format!( + r#"window.__SD_CUSTOM_URI_SERVER__ = "http://{}";"#, + listen_addr + )) + .build() +} + #[tokio::main] async fn main() -> Result<(), Box> { let data_dir = path::data_dir() @@ -35,44 +44,48 @@ async fn main() -> Result<(), Box> { let (node, router) = Node::new(data_dir).await?; - let app = tauri::Builder::default() - .plugin(rspc::integrations::tauri::plugin(router, { - let node = node.clone(); - move || node.get_request_context() - })) - .register_uri_scheme_protocol("spacedrive", { - let node = node.clone(); - move |_, req| { - let uri = req.uri(); - let uri = uri - .replace("spacedrive://localhost/", "http://spacedrive.localhost/") // Windows - .replace("spacedrive://", "http://spacedrive.localhost/"); // Unix style - - // Encoded by `convertFileSrc` on the frontend - let uri = percent_encoding::percent_decode(uri.as_bytes()) - .decode_utf8_lossy() - .to_string(); - - let mut r = Request::builder().method(req.method()).uri(uri); - for (key, value) in req.headers() { - r = r.header(key, value); - } - let r = r.body(req.body().clone()).unwrap(); // TODO: This clone feels so unnecessary but Tauri pass `req` as a reference so we can get the owned value. - - // TODO: This blocking sucks but is required for now. https://github.com/tauri-apps/wry/issues/420 - let resp = block_in_place(|| block_on(handle_custom_uri(&node, r))) - .unwrap_or_else(|err| err.into_response().unwrap()); - let mut r = ResponseBuilder::new() - .version(resp.version()) - .status(resp.status()); - - for (key, value) in resp.headers() { - r = r.header(key, value); - } - - r.body(resp.into_body()) - } - }) + let app = tauri::Builder::default().plugin(rspc::integrations::tauri::plugin(router, { + let node = node.clone(); + move || node.get_request_context() + })); + + // This is a super cringe workaround for: https://github.com/tauri-apps/tauri/issues/3725 & https://bugs.webkit.org/show_bug.cgi?id=146351#c5 + // TODO: Secure this server against other apps on the users machine making requests to it using a HTTP header and random token or something + let endpoint = create_custom_uri_endpoint(node.clone()); + #[cfg(target_os = "linux")] + let app = { + use axum::routing::get; + use std::net::TcpListener; + + let signal = server::utils::axum_shutdown_signal(node.clone()); + + let axum_app = axum::Router::new() + .route("/", get(|| async { "Spacedrive Server!" })) + .nest("/spacedrive", endpoint.axum()) + .fallback(|| async { "404 Not Found: We're past the event horizon..." }); + + let listener = TcpListener::bind("127.0.0.1:0").expect("Error creating localhost server!"); // Only allow current device to access it and randomise port + let listen_addr = listener + .local_addr() + .expect("Error getting localhost server listen addr!"); + debug!("Localhost server listening on: http://{:?}", listen_addr); + + tokio::spawn(async move { + axum::Server::from_tcp(listener) + .expect("error creating HTTP server!") + .serve(axum_app.into_make_service()) + .with_graceful_shutdown(signal) + .await + .expect("Error with HTTP server!"); + }); + + app.plugin(spacedrive_plugin_init(listen_addr)) + }; + + #[cfg(not(target_os = "linux"))] + let app = app.register_uri_scheme_protocol("spacedrive", endpoint.tauri_uri_scheme("spacedrive")); + + let app = app .setup(|app| { let app = app.handle(); app.windows().iter().for_each(|(_, window)| { @@ -83,7 +96,9 @@ async fn main() -> Result<(), Box> { async move { sleep(Duration::from_secs(3)).await; if !window.is_visible().unwrap_or(true) { - println!("Window did not emit `app_ready` event fast enough. Showing window..."); + println!( + "Window did not emit `app_ready` event fast enough. Showing window..." + ); let _ = window.show(); } } diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index 5520066662a1..5956a06177f1 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -31,11 +31,26 @@ async function getOs(): Promise { } } +let customUriServerUrl = (window as any).__SD_CUSTOM_URI_SERVER__ as string | undefined; + +if (customUriServerUrl && !customUriServerUrl?.endsWith('/')) { + customUriServerUrl += '/'; +} + +function getCustomUriURL(path: string): string { + if (customUriServerUrl) { + console.log(customUriServerUrl, path); + return customUriServerUrl + 'spacedrive/' + path; + } else { + return convertFileSrc(path, 'spacedrive'); + } +} + const platform: Platform = { platform: 'tauri', - getThumbnailUrlById: (casId) => convertFileSrc(`thumbnail/${casId}`, 'spacedrive'), + getThumbnailUrlById: (casId) => getCustomUriURL(`thumbnail/${casId}`), getFileUrl: (libraryId, locationLocalId, filePathId) => - convertFileSrc(`file/${libraryId}/${locationLocalId}/${filePathId}`, 'spacedrive'), + getCustomUriURL(`file/${libraryId}/${locationLocalId}/${filePathId}`), openLink: shell.open, getOs, openDirectoryPickerDialog: () => dialog.open({ directory: true }), diff --git a/apps/server/Cargo.toml b/apps/server/Cargo.toml index 66504197c072..a02a59d3a2cd 100644 --- a/apps/server/Cargo.toml +++ b/apps/server/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" [dependencies] sd-core = { path = "../../core", features = ["ffmpeg"] } rspc = { workspace = true, features = ["axum"] } +httpz = { workspace = true, features = ["axum"] } axum = "0.6.4" tokio = { workspace = true, features = ["sync", "rt-multi-thread", "signal"] } tracing = "0.1.36" diff --git a/apps/server/src/lib.rs b/apps/server/src/lib.rs new file mode 100644 index 000000000000..b5614dd82335 --- /dev/null +++ b/apps/server/src/lib.rs @@ -0,0 +1 @@ +pub mod utils; diff --git a/apps/server/src/main.rs b/apps/server/src/main.rs index 92e3448c8ca3..acfbad1ef7af 100644 --- a/apps/server/src/main.rs +++ b/apps/server/src/main.rs @@ -1,13 +1,7 @@ use std::{env, net::SocketAddr, path::Path}; -use axum::{ - body::{Body, Full}, - http::Request, - response::Response, - routing::get, -}; -use hyper::body::to_bytes; -use sd_core::{custom_uri::handle_custom_uri, Node}; +use axum::routing::get; +use sd_core::{custom_uri::create_custom_uri_endpoint, Node}; use tracing::info; mod utils; @@ -40,38 +34,10 @@ async fn main() { let app = axum::Router::new() .route("/", get(|| async { "Spacedrive Server!" })) .route("/health", get(|| async { "OK" })) - .route("/spacedrive/*id", { - let node = node.clone(); - get(|req: Request| async move { - let (parts, body) = req.into_parts(); - let mut r = - Request::builder().method(parts.method).uri( - parts.uri.path().strip_prefix("/spacedrive").expect( - "Error decoding Spacedrive URL prefix. This should be impossible!", - ), - ); - for (key, value) in parts.headers { - if let Some(key) = key { - r = r.header(key, value); - } - } - let r = r.body(to_bytes(body).await.unwrap().to_vec()).unwrap(); - - let resp = handle_custom_uri(&node, r) - .await - .unwrap_or_else(|err| err.into_response().unwrap()); - - let mut r = Response::builder() - .version(resp.version()) - .status(resp.status()); - - for (key, value) in resp.headers() { - r = r.header(key, value); - } - - r.body(Full::from(resp.into_body())).unwrap() - }) - }) + .nest( + "/spacedrive", + create_custom_uri_endpoint(node.clone()).axum(), + ) .nest( "/rspc", router.endpoint(move || node.get_request_context()).axum(), diff --git a/core/Cargo.toml b/core/Cargo.toml index 4097788b95ea..559a2eb4a302 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -32,6 +32,7 @@ blake3 = "1.3.1" # Project dependencies rspc = { workspace = true, features = ["uuid", "chrono", "tracing"] } +httpz = { workspace = true } prisma-client-rust = { workspace = true } specta = { workspace = true } uuid = { version = "1.1.2", features = ["v4", "serde"] } @@ -68,7 +69,6 @@ notify = { version = "5.0.0", default-features = false, features = [ "macos_fsevent", ], optional = true } uhlc = "0.5.1" -http = "0.2.8" http-range = "0.1.5" mini-moka = "0.10.0" serde_with = "2.2.0" diff --git a/core/src/custom_uri.rs b/core/src/custom_uri.rs index 7cd9e1078af9..af9d87d151a0 100644 --- a/core/src/custom_uri.rs +++ b/core/src/custom_uri.rs @@ -1,11 +1,20 @@ -use crate::prisma::file_path; -use crate::Node; -use http::{Request, Response, StatusCode}; +use std::{ + cmp::min, + io, + path::{Path, PathBuf}, + str::FromStr, + sync::Arc, +}; + +use crate::{prisma::file_path, Node}; use http_range::HttpRange; +use httpz::{ + http::{Method, Response, StatusCode}, + Endpoint, GenericEndpoint, HttpEndpoint, Request, +}; use mini_moka::sync::Cache; use once_cell::sync::Lazy; use prisma_client_rust::QueryError; -use std::{cmp::min, io, path::Path, path::PathBuf, str::FromStr}; use thiserror::Error; use tokio::{ fs::File, @@ -23,10 +32,7 @@ static FILE_METADATA_CACHE: Lazy>, -) -> Result>, HandleCustomUriError> { +async fn handler(node: Arc, req: Request) -> Result>, HandleCustomUriError> { let path = req .uri() .path() @@ -34,6 +40,7 @@ pub async fn handle_custom_uri( .unwrap_or_else(|| req.uri().path()) .split('/') .collect::>(); + match path.first().copied() { Some("thumbnail") => { let file_cas_id = path @@ -243,10 +250,22 @@ pub async fn handle_custom_uri( } } +pub fn create_custom_uri_endpoint(node: Arc) -> Endpoint { + GenericEndpoint::new("/*any", [Method::GET, Method::POST], move |req: Request| { + let node = node.clone(); + async move { + match handler(node, req).await { + Ok(resp) => resp, + Err(err) => err.into_response().unwrap(), + } + } + }) +} + #[derive(Error, Debug)] pub enum HandleCustomUriError { #[error("error creating http request/response: {0}")] - Http(#[from] http::Error), + Http(#[from] httpz::http::Error), #[error("io error: {0}")] Io(#[from] io::Error), #[error("query error: {0}")] @@ -258,7 +277,7 @@ pub enum HandleCustomUriError { } impl HandleCustomUriError { - pub fn into_response(self) -> http::Result>> { + pub fn into_response(self) -> httpz::http::Result>> { match self { HandleCustomUriError::Http(err) => { error!("Error creating http request/response: {}", err);