build(admin): vendor Tailwind + HTMX, drop CDN dependencies
The dashboard pages (index.html, login.html) were fetching tailwindcss
and htmx.org from cdn.tailwindcss.com and unpkg.com at runtime. That
leaks browser request metadata to third parties, makes the dashboard
inoperable on air-gapped deployments, and ties dashboard availability
to two SaaS CDNs the operator doesn't control.
Both files are now embedded in the hbbs binary (include_bytes!) and
served from /admin/assets/{tailwindcss.js,htmx.min.js}. Versions
pinned in source: Tailwind 3.4.16 (Play CDN JIT, the same JS the
<script src="cdn.tailwindcss.com"> tag was previously loading) and
htmx.org 1.9.10. To upgrade either: re-fetch the file at the same
path and rebuild hbbs.
Asset routes are unauthenticated so the login page can load them,
and served with Cache-Control: public, max-age=31536000, immutable
since version bumps roll with binary upgrades anyway.
Bundle size impact: +500 KB in the hbbs binary, fully cached on the
client after first load.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vendored
+1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
-2
@@ -4,8 +4,8 @@
|
||||
<meta charset="utf-8" />
|
||||
<title>RustDesk Admin</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
||||
<script src="/admin/assets/tailwindcss.js"></script>
|
||||
<script src="/admin/assets/htmx.min.js"></script>
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; }
|
||||
.nav-link.active { background: rgb(15 23 42); color: rgb(125 211 252); }
|
||||
|
||||
+2
-2
@@ -4,8 +4,8 @@
|
||||
<meta charset="utf-8" />
|
||||
<title>Sign in — RustDesk Admin</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
||||
<script src="/admin/assets/tailwindcss.js"></script>
|
||||
<script src="/admin/assets/htmx.min.js"></script>
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; }
|
||||
</style>
|
||||
|
||||
+38
-2
@@ -20,7 +20,7 @@ pub mod me;
|
||||
pub mod oidc_login;
|
||||
pub mod pages;
|
||||
|
||||
use axum::http::header;
|
||||
use axum::http::{header, HeaderValue, StatusCode};
|
||||
use axum::response::{Html, IntoResponse, Response};
|
||||
use axum::routing::{get, post};
|
||||
use axum::Router;
|
||||
@@ -31,6 +31,12 @@ use std::sync::Arc;
|
||||
const INDEX_HTML: &str = include_str!("../../../admin_ui/index.html");
|
||||
const LOGIN_HTML: &str = include_str!("../../../admin_ui/login.html");
|
||||
|
||||
/// Third-party JS dependencies vendored under `admin_ui/assets/` so the
|
||||
/// dashboard doesn't fetch from cdn.tailwindcss.com / unpkg.com at runtime.
|
||||
/// See docs/CONFIGURATION.md "Web client" for the upgrade procedure.
|
||||
const TAILWIND_JS: &[u8] = include_bytes!("../../../admin_ui/assets/tailwindcss.js");
|
||||
const HTMX_JS: &[u8] = include_bytes!("../../../admin_ui/assets/htmx.min.js");
|
||||
|
||||
pub fn build(state: Arc<crate::api::state::AppState>) -> Option<Router> {
|
||||
if state.cfg.admin_ui_dir.is_empty() {
|
||||
// Operator opted out by setting the flag to empty.
|
||||
@@ -42,6 +48,10 @@ pub fn build(state: Arc<crate::api::state::AppState>) -> Option<Router> {
|
||||
.route("/admin/", get(serve_index))
|
||||
.route("/admin/index.html", get(serve_index))
|
||||
.route("/admin/login.html", get(serve_login))
|
||||
// Vendored third-party JS — versions pinned in source, so we can
|
||||
// cache aggressively (immutable + 1-year max-age).
|
||||
.route("/admin/assets/tailwindcss.js", get(serve_tailwind))
|
||||
.route("/admin/assets/htmx.min.js", get(serve_htmx))
|
||||
// Dynamic dashboard endpoints.
|
||||
.route("/admin/login", post(auth::login))
|
||||
.route("/admin/logout", post(auth::logout))
|
||||
@@ -183,7 +193,33 @@ fn html_response(body: &'static str) -> Response {
|
||||
let mut resp = Html(body).into_response();
|
||||
resp.headers_mut().insert(
|
||||
header::CACHE_CONTROL,
|
||||
axum::http::HeaderValue::from_static("no-cache"),
|
||||
HeaderValue::from_static("no-cache"),
|
||||
);
|
||||
resp
|
||||
}
|
||||
|
||||
async fn serve_tailwind() -> Response {
|
||||
js_response(TAILWIND_JS)
|
||||
}
|
||||
|
||||
async fn serve_htmx() -> Response {
|
||||
js_response(HTMX_JS)
|
||||
}
|
||||
|
||||
fn js_response(body: &'static [u8]) -> Response {
|
||||
let mut resp = (StatusCode::OK, body).into_response();
|
||||
let h = resp.headers_mut();
|
||||
h.insert(
|
||||
header::CONTENT_TYPE,
|
||||
HeaderValue::from_static("application/javascript; charset=utf-8"),
|
||||
);
|
||||
// Vendored at a pinned version — safe to cache for a year. If we
|
||||
// ever bump the version we should also bump the asset path so
|
||||
// browsers don't keep stale copies; for now the path-pinned version
|
||||
// is implicit in the binary build.
|
||||
h.insert(
|
||||
header::CACHE_CONTROL,
|
||||
HeaderValue::from_static("public, max-age=31536000, immutable"),
|
||||
);
|
||||
resp
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user