From 97e3ac2fbec89d31e4112e2482b51db1dc13a607 Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Sun, 15 Jun 2025 22:03:46 +0000 Subject: [PATCH] chore: replace `hub-embed` with proxy to 127.0.0.1:5080 for /ui/ path in dev --- Cargo.lock | 130 +-------------- docker/dev-full/frontend-hub/entrypoint.sh | 4 +- docker/dev-full/rivet-server/config.jsonc | 2 + frontend/apps/hub/vite.config.ts | 7 + .../config/src/config/server/rivet/mod.rs | 11 ++ packages/common/hub-embed/Cargo.toml | 15 -- packages/common/hub-embed/build.rs | 65 -------- packages/common/hub-embed/src/lib.rs | 7 - packages/core/api/ui/Cargo.toml | 3 +- packages/core/api/ui/src/route.rs | 157 +++++++++--------- yarn.lock | 10 ++ 11 files changed, 113 insertions(+), 298 deletions(-) delete mode 100644 packages/common/hub-embed/Cargo.toml delete mode 100644 packages/common/hub-embed/build.rs delete mode 100644 packages/common/hub-embed/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 3000c5b0f0..6713c86807 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1321,15 +1321,14 @@ dependencies = [ "http 0.2.12", "hyper 0.14.31", "lazy_static", - "mime_guess", "prost 0.10.4", + "reqwest 0.12.12", "rivet-auth-server", "rivet-cache", "rivet-claims", "rivet-config", "rivet-convert", "rivet-health-checks", - "rivet-hub-embed", "rivet-operation", "rivet-pools", "serde", @@ -1351,15 +1350,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "arbitrary" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" -dependencies = [ - "derive_arbitrary", -] - [[package]] name = "arc-swap" version = "1.7.1" @@ -4343,12 +4333,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "deflate64" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b" - [[package]] name = "deno-embed" version = "25.4.2" @@ -4359,7 +4343,7 @@ dependencies = [ "serde_json", "tempfile", "tokio", - "zip 0.5.13", + "zip", ] [[package]] @@ -5352,17 +5336,6 @@ dependencies = [ "serde", ] -[[package]] -name = "derive_arbitrary" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.90", -] - [[package]] name = "derive_builder" version = "0.12.0" @@ -8792,12 +8765,6 @@ dependencies = [ "scopeguard", ] -[[package]] -name = "lockfree-object-pool" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e" - [[package]] name = "log" version = "0.4.22" @@ -8863,16 +8830,6 @@ dependencies = [ "twox-hash", ] -[[package]] -name = "lzma-rs" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" -dependencies = [ - "byteorder", - "crc", -] - [[package]] name = "malloc_buf" version = "0.0.6" @@ -12487,16 +12444,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "rivet-hub-embed" -version = "25.4.2" -dependencies = [ - "fs_extra", - "include_dir", - "reqwest 0.12.12", - "zip 2.2.2", -] - [[package]] name = "rivet-identity" version = "0.0.14" @@ -13023,7 +12970,7 @@ dependencies = [ "vergen", "which 5.0.0", "windows 0.48.0", - "zip 0.5.13", + "zip", ] [[package]] @@ -18094,74 +18041,3 @@ dependencies = [ "thiserror 1.0.69", "time 0.1.45", ] - -[[package]] -name = "zip" -version = "2.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae9c1ea7b3a5e1f4b922ff856a129881167511563dc219869afe3787fc0c1a45" -dependencies = [ - "aes", - "arbitrary", - "bzip2", - "constant_time_eq", - "crc32fast", - "crossbeam-utils", - "deflate64", - "displaydoc", - "flate2", - "hmac 0.12.1", - "indexmap 2.7.0", - "lzma-rs", - "memchr", - "pbkdf2", - "rand 0.8.5", - "sha1", - "thiserror 2.0.12", - "time 0.3.37", - "zeroize", - "zopfli", - "zstd", -] - -[[package]] -name = "zopfli" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946" -dependencies = [ - "bumpalo", - "crc32fast", - "lockfree-object-pool", - "log", - "once_cell", - "simd-adler32", -] - -[[package]] -name = "zstd" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "7.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059" -dependencies = [ - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.13+zstd.1.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa" -dependencies = [ - "cc", - "pkg-config", -] diff --git a/docker/dev-full/frontend-hub/entrypoint.sh b/docker/dev-full/frontend-hub/entrypoint.sh index df41456ec3..f483c6e735 100755 --- a/docker/dev-full/frontend-hub/entrypoint.sh +++ b/docker/dev-full/frontend-hub/entrypoint.sh @@ -9,5 +9,7 @@ cd /app yarn install # Start dev server +# +# Set base to /ui since this is where the UI is hosted in the dev server cd /app/frontend/apps/hub -yarn dev --host 0.0.0.0 --port 5080 +yarn dev --base=/ui diff --git a/docker/dev-full/rivet-server/config.jsonc b/docker/dev-full/rivet-server/config.jsonc index 6651d129e1..da0fd94196 100644 --- a/docker/dev-full/rivet-server/config.jsonc +++ b/docker/dev-full/rivet-server/config.jsonc @@ -5,6 +5,8 @@ "access_kind": "development" }, "ui": { + "enable": true, + "proxy_origin": "http://frontend-hub:5080", "public_origin_regex": ".*" }, "guard": { diff --git a/frontend/apps/hub/vite.config.ts b/frontend/apps/hub/vite.config.ts index 4fff3adc8e..9b881ec460 100644 --- a/frontend/apps/hub/vite.config.ts +++ b/frontend/apps/hub/vite.config.ts @@ -46,7 +46,14 @@ export default defineConfig({ process.env.DEBUG_BUNDLE ? visualizer() : null, ], server: { + host: "0.0.0.0", port: 5080, + allowedHosts: true, + // Listen on a different port since we don't proxy WebSockets on /ui + hmr: { + port: 5080, + host: "127.0.0.1" + } }, preview: { port: 5080, diff --git a/packages/common/config/src/config/server/rivet/mod.rs b/packages/common/config/src/config/server/rivet/mod.rs index 976b857a9b..9ddaedf7f8 100644 --- a/packages/common/config/src/config/server/rivet/mod.rs +++ b/packages/common/config/src/config/server/rivet/mod.rs @@ -730,6 +730,11 @@ pub struct Ui { /// /// If disabled, the UI can be hosted separately. pub enable: Option, + /// Origin to proxy UI requests to. This should be the server serving the actula files fro the + /// frontend. + /// + /// This is frequently either Vite for a development setup or Nginx for a simple setup. + pub proxy_origin: Option, /// The origin URL for the UI. pub public_origin: Option, /// Regular expression to match valid UI origins. @@ -741,6 +746,12 @@ impl Ui { self.enable.unwrap_or(true) } + pub fn proxy_origin(&self) -> Url { + self.proxy_origin + .clone() + .unwrap_or_else(|| Url::parse(&format!("http://127.0.0.1:5080")).unwrap()) + } + pub fn public_origin(&self) -> Url { self.public_origin.clone().unwrap_or_else(|| { Url::parse(&format!( diff --git a/packages/common/hub-embed/Cargo.toml b/packages/common/hub-embed/Cargo.toml deleted file mode 100644 index eeb43e01e0..0000000000 --- a/packages/common/hub-embed/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "rivet-hub-embed" -version.workspace = true -authors.workspace = true -license.workspace = true -edition.workspace = true - -[build-dependencies] -fs_extra = "1.3.0" -reqwest = { version = "0.12.8", features = ["blocking"] } -zip = "2.2.0" - -[dependencies] -include_dir = "0.7.4" - diff --git a/packages/common/hub-embed/build.rs b/packages/common/hub-embed/build.rs deleted file mode 100644 index 9d10190437..0000000000 --- a/packages/common/hub-embed/build.rs +++ /dev/null @@ -1,65 +0,0 @@ -use std::{env, fs, path::Path, path::PathBuf, process::Command}; - -fn main() -> Result<(), Box> { - // Check if yarn is installed - let yarn_check = Command::new("yarn").arg("--version").status(); - assert!( - yarn_check.is_ok() && yarn_check.unwrap().success(), - "yarn is not installed, please install yarn to build this project" - ); - - // Get the output directory from the cargo environment variable - let target_dir = env::var("OUT_DIR")?; - let manifest_dir = env::var("CARGO_MANIFEST_DIR")?; - let out_dir = Path::new(&target_dir); - - // Clear out dir - if out_dir.exists() { - fs::remove_dir_all(out_dir)?; - } - - if std::env::var("RIVET_BUILD_HUB").map_or(false, |x| x == "1") { - // Build hub - let project_root = PathBuf::from(manifest_dir.clone()).join("../../.."); - let hub_path = project_root.join("frontend/apps/hub"); - - println!("Running yarn install"); - let output = Command::new("yarn") - .arg("install") - .arg("--immutable") - .current_dir(&hub_path) - .output()?; - println!("stdout:\n{}", String::from_utf8_lossy(&output.stdout)); - println!("stderr:\n{}", String::from_utf8_lossy(&output.stderr)); - assert!(output.status.success(), "yarn install failed"); - - println!("Running yarn build"); - let output = Command::new("yarn") - .current_dir(&hub_path) - .args(["dlx", "turbo", "run", "build:embedded"]) - .env("VITE_APP_API_URL", "__APP_API_URL__") - .output()?; - println!("stdout:\n{}", String::from_utf8_lossy(&output.stdout)); - println!("stderr:\n{}", String::from_utf8_lossy(&output.stderr)); - assert!(output.status.success(), "hub build failed"); - - // Copy dist directory to out_dir - let dist_path = hub_path.join("dist"); - fs_extra::dir::copy( - dist_path.clone(), - out_dir, - &fs_extra::dir::CopyOptions::new().content_only(true), - )?; - - println!("cargo:rerun-if-changed={}", hub_path.display()); - println!("cargo:rerun-if-env-changed=FONTAWESOME_PACKAGE_TOKEN"); - } else { - // Create empty dist dir - std::fs::create_dir_all(out_dir)?; - } - - // Set the path in the env - println!("cargo:rustc-env=HUB_PATH={}", out_dir.display()); - - Ok(()) -} diff --git a/packages/common/hub-embed/src/lib.rs b/packages/common/hub-embed/src/lib.rs deleted file mode 100644 index 3737db7316..0000000000 --- a/packages/common/hub-embed/src/lib.rs +++ /dev/null @@ -1,7 +0,0 @@ -use include_dir::{include_dir, Dir}; - -const HUB_DIR: Dir = include_dir!("$HUB_PATH"); - -pub fn get_file_content(path: &str) -> Option<&'static [u8]> { - HUB_DIR.get_file(path).map(|file| file.contents()) -} diff --git a/packages/core/api/ui/Cargo.toml b/packages/core/api/ui/Cargo.toml index 40c80da9c7..3a11f8d161 100644 --- a/packages/core/api/ui/Cargo.toml +++ b/packages/core/api/ui/Cargo.toml @@ -30,7 +30,6 @@ tracing = "0.1" tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt", "json", "ansi"] } url = "2.2.2" uuid = { version = "1", features = ["v4"] } -rivet-hub-embed.workspace = true -mime_guess = "2.0.5" rivet-config.workspace = true global-error.workspace = true +reqwest = { version = "0.12", default-features = false, features = ["json"] } diff --git a/packages/core/api/ui/src/route.rs b/packages/core/api/ui/src/route.rs index fb716e1ed8..02f7011ab6 100644 --- a/packages/core/api/ui/src/route.rs +++ b/packages/core/api/ui/src/route.rs @@ -1,29 +1,9 @@ -use hyper::{Body, Request, Response}; +use hyper::{Body, Method, Request, Response}; use rivet_operation::prelude::*; pub struct Router; impl Router { - fn replace_vite_app_api_url( - content: &[u8], - config: &rivet_config::Config, - ) -> GlobalResult> { - let content_str = std::str::from_utf8(content)?; - - let replacement_count = content_str.matches("__APP_API_URL__").count(); - ensure!( - replacement_count > 0, - "Expected at least one occurrence of __APP_API_URL__, found {}", - replacement_count - ); - - let public_origin = - util::url::to_string_without_slash(&config.server()?.rivet.api_public.public_origin()); - let replaced_content = content_str.replace("__APP_API_URL__", &public_origin); - - Ok(replaced_content.into_bytes()) - } - #[doc(hidden)] #[tracing::instrument(skip_all)] pub async fn __inner( @@ -57,74 +37,89 @@ impl Router { return Ok(None); } - // Strip the prefix to: - // - Strip the mount path of /ui. - // - Strip the starting slash in order to match the format `include_dir` needs. - let path = request.uri().path().trim_start_matches("/ui/"); - let content = rivet_hub_embed::get_file_content(path); - - match content { - Some(content) => { - let content_type = mime_guess::from_path(path).first_or_octet_stream(); - tracing::debug!( - path = ?path, - ?content_type, - length = ?content.len(), - "serving file" - ); - if let Some(headers) = response.headers_mut() { - headers.insert( - hyper::header::CONTENT_TYPE, - hyper::header::HeaderValue::from_str(content_type.as_ref()).unwrap(), - ); - } + // Build proxy URL by joining request path to base URL + let mut proxy_url = config.server()?.rivet.ui.proxy_origin().clone(); + + // Remove leading slash from request path since join() expects relative paths + let request_path = request.uri().path().strip_prefix('/').unwrap_or(request.uri().path()); + + // Join the request path to the base URL + proxy_url = match proxy_url.join(request_path) { + Ok(url) => url, + Err(e) => bail!("Failed to build proxy URL: {}", e), + }; + + // Set query string if present + proxy_url.set_query(request.uri().query()); + + let full_proxy_url = proxy_url.to_string(); - // Replace VITE_APP_API_URL if the file is index.html - let content = if path == "index.html" { - Self::replace_vite_app_api_url(content, &config)? - } else { - content.to_vec() - }; + tracing::debug!( + original_path = ?path, + proxy_url = ?full_proxy_url, + "proxying request" + ); - Ok(Some(content)) + // Build reqwest request + let client = reqwest::Client::new(); + let method = match reqwest::Method::from_bytes(request.method().as_str().as_bytes()) { + Ok(method) => method, + Err(e) => bail!("Invalid HTTP method: {}", e), + }; + let mut req_builder = client.request(method, &full_proxy_url); + + // Forward headers + for (name, value) in request.headers() { + if let Ok(value_str) = value.to_str() { + req_builder = req_builder.header(name.as_str(), value_str); } - None => { - // HACK(FRONT-545): Paths with tokens in them are interpreted as a file, so we - // include certain exceptions - if path.ends_with(".html") || !path.contains('.') || path.contains("device.") { - tracing::debug!( - path = ?path, - "file not found, serving index.html" - ); - - // Serve index.html content - let index_content = unwrap!( - rivet_hub_embed::get_file_content("index.html"), - "index.html not found" - ); - - // Replace VITE_APP_API_URL in index.html - let index_content = Self::replace_vite_app_api_url(index_content, &config)?; - - if let Some(headers) = response.headers_mut() { - headers.insert( - hyper::header::CONTENT_TYPE, - hyper::header::HeaderValue::from_static("text/html"), - ); - } + } - Ok(Some(index_content)) - } else { - tracing::debug!( - path = ?path, - "file not found, returning 404" - ); + // Forward body for non-GET requests + if request.method() != Method::GET && request.method() != Method::HEAD { + let body_bytes = match hyper::body::to_bytes(std::mem::replace(request.body_mut(), Body::empty())).await { + Ok(bytes) => bytes, + Err(e) => bail!("Failed to read request body: {}", e), + }; + req_builder = req_builder.body(body_bytes.to_vec()); + } - *response = std::mem::take(response).status(hyper::StatusCode::NOT_FOUND); - Ok(Some("Not Found".into())) + // Make the request + let proxy_response = match req_builder.send().await { + Ok(response) => response, + Err(e) => bail!("Proxy request failed: {}", e), + }; + + // Set response status + let status_code = proxy_response.status(); + let hyper_status = match hyper::StatusCode::from_u16(status_code.as_u16()) { + Ok(status) => status, + Err(e) => bail!("Invalid status code: {}", e), + }; + *response = std::mem::take(response).status(hyper_status); + + // Forward response headers + if let Some(headers) = response.headers_mut() { + for (name, value) in proxy_response.headers() { + if let Ok(header_name) = + hyper::header::HeaderName::from_bytes(name.as_str().as_bytes()) + { + if let Ok(header_value) = + hyper::header::HeaderValue::from_bytes(value.as_bytes()) + { + headers.insert(header_name, header_value); + } } } } + + // Get response body + let body_bytes = match proxy_response.bytes().await { + Ok(bytes) => bytes, + Err(e) => bail!("Failed to read proxy response body: {}", e), + }; + + Ok(Some(body_bytes.to_vec())) } #[tracing::instrument(skip_all)] diff --git a/yarn.lock b/yarn.lock index 90f7aeb286..7570806cc9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7476,6 +7476,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:^22.15.30": + version: 22.15.31 + resolution: "@types/node@npm:22.15.31" + dependencies: + undici-types: "npm:~6.21.0" + checksum: 10c0/ef7d5dc890da41cfd554d35ab8998bc18be9e3a0caa642e720599ac4410a94a4879766e52b3c9cafa06c66b7b8aebdc51f322cf67df23a6489927890196a316d + languageName: node + linkType: hard + "@types/pg-pool@npm:2.0.6": version: 2.0.6 resolution: "@types/pg-pool@npm:2.0.6" @@ -9171,6 +9180,7 @@ __metadata: "@hono/node-server": "npm:^1.14.4" "@rivet-gg/api": "npm:^25.4.2" "@types/eventsource": "npm:^1.1.15" + "@types/node": "npm:^22.15.30" eventsource: "npm:^4.0.0" get-port: "npm:^7.1.0" hono: "npm:^4.7.11"