feat(console): embed the management UI in release binaries

- build locked Web dependencies automatically for release profiles
- keep debug Cargo builds independent from the Vite development server
- embed generated Host, Page, manifest, and service worker assets
- serve exact control-plane assets with MIME and cache headers
- reject non-canonical and unknown paths without an SPA fallback
- cover embedded document boundaries and path validation
This commit is contained in:
Maofeng
2026-07-30 09:22:04 +08:00
parent 70ee5e33da
commit f6ad94e948
7 changed files with 239 additions and 0 deletions
+1
View File
@@ -99,6 +99,7 @@ pub fn app(console: Arc<Console>, allowed_origins: Vec<HeaderValue>) -> Router {
.on_response(DefaultOnResponse::new().level(Level::INFO)),
)
.with_state(console)
.merge(web::router())
}
/// Builds the public data-plane API around the same resident Runtime.
+1
View File
@@ -36,6 +36,7 @@ mod api;
mod invocation_persistence;
mod kv_backend;
mod persistence;
mod web;
mod wit_registry;
use std::{
+108
View File
@@ -0,0 +1,108 @@
//! Embedded Solid management UI served by the control-plane listener.
//!
//! The router is empty in normal Debug builds because Vite owns Web assets
//! during development. Release builds embed Cargo `OUT_DIR/web`; requests only
//! resolve exact files, so an unknown URL cannot accidentally receive the Host
//! document as an SPA fallback.
use axum::Router;
#[cfg(wasmeld_embedded_web)]
use axum::{
body::{Body, Bytes},
extract::Path,
http::{StatusCode, header},
response::{IntoResponse, Response},
routing::get,
};
#[cfg(wasmeld_embedded_web)]
use include_dir::{Dir, include_dir};
#[cfg(wasmeld_embedded_web)]
static WEB_ASSETS: Dir<'static> = include_dir!("$OUT_DIR/web");
pub(crate) fn router() -> Router {
#[cfg(wasmeld_embedded_web)]
{
Router::new()
.route("/", get(index))
.route("/{*path}", get(asset))
}
#[cfg(not(wasmeld_embedded_web))]
{
Router::new()
}
}
#[cfg(wasmeld_embedded_web)]
async fn index() -> Response {
response("index.html")
}
#[cfg(wasmeld_embedded_web)]
async fn asset(Path(path): Path<String>) -> Response {
response(&path)
}
#[cfg(wasmeld_embedded_web)]
fn response(path: &str) -> Response {
// The wildcard route is public input. Reject non-canonical paths before
// looking inside the compile-time directory, even though `include_dir`
// itself cannot access the host filesystem at runtime.
if !valid_asset_path(path) {
return StatusCode::NOT_FOUND.into_response();
}
let Some(file) = WEB_ASSETS.get_file(path) else {
return StatusCode::NOT_FOUND.into_response();
};
let content_type = mime_guess::from_path(path)
.first_or_octet_stream()
.essence_str()
.to_owned();
let cache_control = if path.starts_with("assets/") {
"public, max-age=31536000, immutable"
} else {
"no-cache"
};
let mut builder = Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, content_type)
.header(header::CACHE_CONTROL, cache_control)
.header("x-content-type-options", "nosniff");
if path == "sw.js" {
builder = builder.header("service-worker-allowed", "/");
}
builder
.body(Body::from(Bytes::from_static(file.contents())))
.unwrap_or_else(|_| StatusCode::INTERNAL_SERVER_ERROR.into_response())
}
#[cfg(wasmeld_embedded_web)]
fn valid_asset_path(path: &str) -> bool {
!path.is_empty()
&& !path.starts_with('/')
&& !path.contains('\\')
&& path
.split('/')
.all(|segment| !matches!(segment, "" | "." | ".."))
}
#[cfg(all(test, wasmeld_embedded_web))]
mod tests {
use super::*;
#[test]
fn embeds_both_document_boundaries_and_pwa_assets() {
for path in ["index.html", "page.html", "manifest.webmanifest", "sw.js"] {
assert!(WEB_ASSETS.get_file(path).is_some(), "missing {path}");
}
}
#[test]
fn rejects_non_canonical_asset_paths() {
assert!(!valid_asset_path("../index.html"));
assert!(!valid_asset_path("assets//app.js"));
assert!(!valid_asset_path(r"assets\app.js"));
}
}