v0.25.1
This commit is contained in:
1 parent
87e571c170
commit
fa7abc9e53
308 files changed
+49746
-50237
No files matched your search
@@ -0,0 +1,451 @@
|
||||
mod documentation;
|
||||
mod public;
|
||||
use crate::{
|
||||
auth::{self, Session},
|
||||
config::Update,
|
||||
runtime::{ApiError, App},
|
||||
server::{PeerAddr, Transport},
|
||||
};
|
||||
use axum::{
|
||||
extract::{DefaultBodyLimit, Path, Query, Request, State},
|
||||
middleware::{self, Next},
|
||||
response::{
|
||||
sse::{Event, KeepAlive, Sse},
|
||||
IntoResponse, Response,
|
||||
},
|
||||
routing::{delete, get, post, put},
|
||||
Extension, Json, Router,
|
||||
};
|
||||
use http::{
|
||||
header::{AUTHORIZATION, HOST, ORIGIN, SET_COOKIE},
|
||||
HeaderMap, HeaderValue, Method, StatusCode, Version,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
use std::{
|
||||
convert::Infallible,
|
||||
net::IpAddr,
|
||||
sync::{atomic::Ordering, Arc},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
fn admin_app_javascript() -> &'static str {
|
||||
static ASSET: std::sync::LazyLock<String> = std::sync::LazyLock::new(|| {
|
||||
[
|
||||
include_str!("web/app/core.js"),
|
||||
include_str!("web/app/runtime.js"),
|
||||
include_str!("web/app/settings.js"),
|
||||
].join("\n")
|
||||
});
|
||||
ASSET.as_str()
|
||||
}
|
||||
|
||||
fn admin_vhosts_javascript() -> &'static str {
|
||||
static ASSET: std::sync::LazyLock<String> = std::sync::LazyLock::new(|| {
|
||||
[
|
||||
"(() => {",
|
||||
include_str!("web/vhosts/core.js"),
|
||||
include_str!("web/vhosts/editor.js"),
|
||||
include_str!("web/vhosts/render.js"),
|
||||
"})();",
|
||||
].join("\n")
|
||||
});
|
||||
ASSET.as_str()
|
||||
}
|
||||
|
||||
pub fn router(state: Arc<App>) -> Router {
|
||||
let authentication = Router::new()
|
||||
.route("/api/auth/status", get(auth_status))
|
||||
.route("/api/auth/setup", post(setup))
|
||||
.route("/api/auth/login", post(login))
|
||||
.route("/api/auth/session", get(session))
|
||||
.route("/api/auth/logout", post(logout))
|
||||
.route("/api/auth/logout-all", post(logout_all))
|
||||
.route("/api/auth/password", post(password))
|
||||
.layer(DefaultBodyLimit::max(4096));
|
||||
Router::new()
|
||||
.merge(authentication)
|
||||
.route("/", get(public::index))
|
||||
.route("/overview", get(public::index))
|
||||
.route("/vhosts", get(public::index))
|
||||
.route("/web-server", get(public::index))
|
||||
.route("/docker-vhosts", get(public::index))
|
||||
.route("/redirects", get(public::index))
|
||||
.route("/404-hosts", get(public::index))
|
||||
.route("/streams", get(public::index))
|
||||
.route("/acl-profiles", get(public::index))
|
||||
.route("/configuration", get(public::index))
|
||||
.route("/tls", get(public::index))
|
||||
.route("/traffic", get(public::index))
|
||||
.route("/cache", get(public::index))
|
||||
.route("/transform", get(public::index))
|
||||
.route("/logging", get(public::index))
|
||||
.route("/settings", get(public::index))
|
||||
.route(
|
||||
"/assets/login.js",
|
||||
get(|| async {
|
||||
(
|
||||
[("content-type", "text/javascript; charset=utf-8")],
|
||||
include_str!("web/login.js"),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/assets/login-langs.json",
|
||||
get(|| async {
|
||||
(
|
||||
[("content-type", "application/json; charset=utf-8")],
|
||||
include_str!(concat!(env!("OUT_DIR"), "/login-langs.json")),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.route("/docs", get(documentation::index))
|
||||
.route("/docs/", get(documentation::index))
|
||||
.route("/docs/openapi.json", get(documentation::schema))
|
||||
.route("/docs/content.json", get(documentation::content))
|
||||
.route("/docs/init.js", get(documentation::script))
|
||||
.route("/docs/style.css", get(documentation::style))
|
||||
.route(
|
||||
"/docs/swagger-ui-bundle.js",
|
||||
get(|| async { documentation::asset("swagger-ui-bundle.js") }),
|
||||
)
|
||||
.route(
|
||||
"/docs/swagger-ui.css",
|
||||
get(|| async { documentation::asset("swagger-ui.css") }),
|
||||
)
|
||||
.route(
|
||||
"/docs/licenses.txt",
|
||||
get(|| async { documentation::asset("licenses.txt") }),
|
||||
)
|
||||
.route(
|
||||
"/assets/langs.json",
|
||||
get(|| async {
|
||||
(
|
||||
[("content-type", "application/json; charset=utf-8")],
|
||||
include_str!(concat!(env!("OUT_DIR"), "/langs.json")),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/assets/app.js",
|
||||
get(|| async {
|
||||
(
|
||||
[("content-type", "text/javascript; charset=utf-8")],
|
||||
admin_app_javascript(),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/assets/management.js",
|
||||
get(|| async {
|
||||
(
|
||||
[("content-type", "text/javascript; charset=utf-8")],
|
||||
include_str!("web/management.js"),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/assets/integrations.js",
|
||||
get(|| async {
|
||||
(
|
||||
[("content-type", "text/javascript; charset=utf-8")],
|
||||
include_str!("web/integrations.js"),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/assets/traffic-panel.js",
|
||||
get(|| async {
|
||||
(
|
||||
[("content-type", "text/javascript; charset=utf-8")],
|
||||
include_str!("web/traffic-panel.js"),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/assets/cache-panel.js",
|
||||
get(|| async {
|
||||
(
|
||||
[("content-type", "text/javascript; charset=utf-8")],
|
||||
include_str!("web/cache-panel.js"),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/assets/transform-panel.js",
|
||||
get(|| async {
|
||||
(
|
||||
[("content-type", "text/javascript; charset=utf-8")],
|
||||
include_str!("web/transform-panel.js"),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/assets/settings.js",
|
||||
get(|| async {
|
||||
(
|
||||
[("content-type", "text/javascript; charset=utf-8")],
|
||||
include_str!("web/settings.js"),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/assets/acme.js",
|
||||
get(|| async {
|
||||
(
|
||||
[("content-type", "text/javascript; charset=utf-8")],
|
||||
include_str!("web/acme.js"),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/assets/vhosts.js",
|
||||
get(|| async {
|
||||
(
|
||||
[("content-type", "text/javascript; charset=utf-8")],
|
||||
admin_vhosts_javascript(),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/assets/web-server.js",
|
||||
get(|| async {
|
||||
(
|
||||
[("content-type", "text/javascript; charset=utf-8")],
|
||||
include_str!("web/web-server.js"),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/assets/docker.js",
|
||||
get(|| async {
|
||||
(
|
||||
[("content-type", "text/javascript; charset=utf-8")],
|
||||
include_str!("web/docker.js"),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/assets/streams.js",
|
||||
get(|| async {
|
||||
(
|
||||
[("content-type", "text/javascript; charset=utf-8")],
|
||||
include_str!("web/streams.js"),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/assets/redirects.js",
|
||||
get(|| async {
|
||||
(
|
||||
[("content-type", "text/javascript; charset=utf-8")],
|
||||
include_str!("web/redirects.js"),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/assets/host404.js",
|
||||
get(|| async {
|
||||
(
|
||||
[("content-type", "text/javascript; charset=utf-8")],
|
||||
include_str!("web/host404.js"),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/assets/style.css",
|
||||
get(|| async {
|
||||
(
|
||||
[("content-type", "text/css; charset=utf-8")],
|
||||
include_str!("web/style.css"),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/favicon.svg",
|
||||
get(|| async {
|
||||
(
|
||||
[("content-type", "image/svg+xml")],
|
||||
include_str!("web/favicon.svg"),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.route("/healthz", get(|| async { Json(json!({"status":"ok"})) }))
|
||||
.route("/api/status", get(status))
|
||||
.route("/api/backends", get(backend_status))
|
||||
.route("/api/docker", get(docker_status))
|
||||
.route("/api/docker/refresh", post(docker_refresh))
|
||||
.route("/api/certificates", get(certificate_inventory))
|
||||
.route(
|
||||
"/api/basic-auth/hash",
|
||||
post(basic_hash).layer(DefaultBodyLimit::max(4096)),
|
||||
)
|
||||
.route("/api/acme", get(acme_status))
|
||||
.route(
|
||||
"/api/acme/renew",
|
||||
post(acme_renew).layer(DefaultBodyLimit::max(4096)),
|
||||
)
|
||||
.route(
|
||||
"/api/acme/issue",
|
||||
post(acme_issue).layer(DefaultBodyLimit::max(16384)),
|
||||
)
|
||||
.route(
|
||||
"/api/certificates/upload",
|
||||
post(certificate_upload).layer(DefaultBodyLimit::max(300 * 1024)),
|
||||
)
|
||||
.route(
|
||||
"/api/certificates/deploy",
|
||||
post(certificate_deploy).layer(DefaultBodyLimit::max(300 * 1024)),
|
||||
)
|
||||
.route(
|
||||
"/api/certificates/reload",
|
||||
post(certificate_reload).layer(DefaultBodyLimit::max(4096)),
|
||||
)
|
||||
.route(
|
||||
"/api/certificates/{certificate_id}/reload",
|
||||
post(certificate_reload_one).layer(DefaultBodyLimit::max(4096)),
|
||||
)
|
||||
.route(
|
||||
"/api/certificate-api",
|
||||
get(certificate_api_status)
|
||||
.put(certificate_api_rotate)
|
||||
.delete(certificate_api_revoke_all)
|
||||
.layer(DefaultBodyLimit::max(4096)),
|
||||
)
|
||||
.route(
|
||||
"/api/certificate-api/{key_id}",
|
||||
delete(certificate_api_revoke).layer(DefaultBodyLimit::max(4096)),
|
||||
)
|
||||
.route("/api/traffic", get(traffic_status))
|
||||
.route("/api/cache", get(cache_status))
|
||||
.route("/api/transform", get(transform_status))
|
||||
.route("/api/cache/purge", post(cache_purge))
|
||||
.route(
|
||||
"/api/cache/invalidate",
|
||||
post(cache_invalidate).layer(DefaultBodyLimit::max(16 * 1024)),
|
||||
)
|
||||
.route("/api/cache/bans", get(cache_bans))
|
||||
.route("/api/cache/bans/{id}", delete(cache_ban_delete))
|
||||
.route("/api/tunnels", get(active_tunnels))
|
||||
.route(
|
||||
"/api/tunnels/{id}/kill",
|
||||
post(kill_tunnel).layer(DefaultBodyLimit::max(4096)),
|
||||
)
|
||||
.route(
|
||||
"/api/cache/cold/cleanup",
|
||||
post(cache_cold_cleanup).layer(DefaultBodyLimit::max(4096)),
|
||||
)
|
||||
.route("/api/cache/cold/cleanup/{id}", get(cache_cold_cleanup_job))
|
||||
.route("/api/config", get(configuration).merge(put(update)))
|
||||
.route("/api/config/{section}", put(update_section))
|
||||
.route("/api/logs", get(logs))
|
||||
.route("/api/live-logs", get(live_logs_stream))
|
||||
.route("/api/preferences", get(preferences).put(update_preferences))
|
||||
.route("/api/audit", get(audit))
|
||||
.route("/api/events", get(events))
|
||||
.route("/metrics", get(metrics))
|
||||
.route("/api/metrics", get(metrics))
|
||||
.fallback(|| async { reject(StatusCode::NOT_FOUND, "not_found") })
|
||||
.layer(DefaultBodyLimit::max(
|
||||
crate::config::MAX_CONFIG_BYTES + 1024,
|
||||
))
|
||||
.layer(middleware::from_fn_with_state(state.clone(), guard))
|
||||
.layer(middleware::from_fn_with_state(state.clone(), observe_admin))
|
||||
.with_state(state)
|
||||
}
|
||||
// Separate from proxy exchanges: measure only time to admin response headers,
|
||||
// including access denials. SSE stream lifetime is intentionally not included.
|
||||
async fn observe_admin(State(state): State<Arc<App>>, request: Request, next: Next) -> Response {
|
||||
use std::sync::atomic::Ordering;
|
||||
state.metrics.admin_requests.fetch_add(1, Ordering::Relaxed);
|
||||
let started = std::time::Instant::now();
|
||||
let response = next.run(request).await;
|
||||
let class = (response.status().as_u16() as usize / 100).clamp(1, 5) - 1;
|
||||
state.metrics.admin_status[class].fetch_add(1, Ordering::Relaxed);
|
||||
state.metrics.admin_header_duration_us.fetch_add(
|
||||
started.elapsed().as_micros().min(u64::MAX as u128) as u64,
|
||||
Ordering::Relaxed,
|
||||
);
|
||||
state
|
||||
.metrics
|
||||
.admin_response_headers
|
||||
.fetch_add(1, Ordering::Relaxed);
|
||||
response
|
||||
}
|
||||
fn reject(status: StatusCode, message: &str) -> Response {
|
||||
(status, Json(json!({"error":message}))).into_response()
|
||||
}
|
||||
fn secure(mut response: Response) -> Response {
|
||||
let h = response.headers_mut();
|
||||
h.insert(
|
||||
"cache-control",
|
||||
HeaderValue::from_static("private, no-store, no-cache"),
|
||||
);
|
||||
h.insert(
|
||||
"x-content-type-options",
|
||||
HeaderValue::from_static("nosniff"),
|
||||
);
|
||||
h.insert("referrer-policy", HeaderValue::from_static("no-referrer"));
|
||||
h.insert("x-frame-options", HeaderValue::from_static("DENY"));
|
||||
h.insert("content-security-policy", HeaderValue::from_static("default-src 'self'; script-src 'self'; style-src 'self'; connect-src 'self'; img-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'"));
|
||||
h.insert(
|
||||
"permissions-policy",
|
||||
HeaderValue::from_static("camera=(), microphone=(), geolocation=()"),
|
||||
);
|
||||
// Do not let downstream middleware add implementation/version fingerprints.
|
||||
h.remove("server");
|
||||
h.remove("x-powered-by");
|
||||
response
|
||||
}
|
||||
// This is a method-and-path allowlist, NOT a path prefix. Everything else,
|
||||
// including unknown routes, schema, metrics, assets and method mismatches,
|
||||
// requires an active administrator session before extractors read a body.
|
||||
fn panel_document_path(path: &str) -> bool {
|
||||
matches!(
|
||||
path,
|
||||
"/" | "/overview"
|
||||
| "/vhosts"
|
||||
| "/web-server"
|
||||
| "/docker-vhosts"
|
||||
| "/redirects"
|
||||
| "/404-hosts"
|
||||
| "/streams"
|
||||
| "/acl-profiles"
|
||||
| "/configuration"
|
||||
| "/tls"
|
||||
| "/traffic"
|
||||
| "/cache"
|
||||
| "/transform"
|
||||
| "/logging"
|
||||
| "/settings"
|
||||
)
|
||||
}
|
||||
fn anonymous_allowed(method: &Method, path: &str) -> bool {
|
||||
if method == Method::GET || method == Method::HEAD {
|
||||
panel_document_path(path)
|
||||
|| matches!(
|
||||
path,
|
||||
"/assets/login.js"
|
||||
| "/assets/login-langs.json"
|
||||
| "/assets/style.css"
|
||||
| "/favicon.svg"
|
||||
| "/healthz"
|
||||
| "/api/auth/status"
|
||||
)
|
||||
} else {
|
||||
method == Method::POST && matches!(path, "/api/auth/setup" | "/api/auth/login")
|
||||
}
|
||||
}
|
||||
|
||||
const CERTIFICATE_API_KEY_HEADER: &str = "x-rustproxy-certificate-key";
|
||||
|
||||
fn certificate_api_operation(method: &Method, path: &str) -> bool {
|
||||
if method != Method::POST {
|
||||
return false;
|
||||
}
|
||||
matches!(path, "/api/certificates/deploy" | "/api/certificates/reload")
|
||||
|| path
|
||||
.strip_prefix("/api/certificates/")
|
||||
.and_then(|tail| tail.strip_suffix("/reload"))
|
||||
.is_some_and(crate::config::valid_id)
|
||||
}
|
||||
Reference in new issue
Block a user