mirror of
https://github.com/ruvnet/RuView
synced 2026-06-09 10:13:17 +00:00
feat(docker+sensing-server): refresh Docker publish + opt-in bearer-token API auth
Closes #520, #514, #443. ## #520 / #514 — stale Docker image, missing UI assets `ruvnet/wifi-densepose:latest` was published before `ui/observatory*` and `ui/pose-fusion*` were added; users see /app/ui missing those files and the v0.6+ packet format doesn't reach the server. Two fixes: 1. `docker/Dockerfile.rust` now `RUN`s a build-time guard after `COPY ui/` that fails the build if `index.html` / `observatory.html` / `pose-fusion.html` / `viz.html` (or the `observatory/` / `pose-fusion/` / `components/` / `services/` directories) are missing, plus an exec-bit check on `/app/sensing-server`. A stale image can never be silently produced again. 2. New `.github/workflows/sensing-server-docker.yml` rebuilds + pushes on every change to the Dockerfile, the server crate, the signal/vitals/ wifiscan crates, the workspace manifests, the `ui/` tree, or itself — plus `v*` tags and manual dispatch. Pushes to both `docker.io/ruvnet/ wifi-densepose` AND `ghcr.io/ruvnet/wifi-densepose` with `latest` + `vX.Y.Z` + `sha-<short>` tags, then post-push smoke-tests the artifact: /health, /api/v1/info, the observatory + pose-fusion HTML, AND the bearer-auth path (no token → 401, wrong → 401, correct → 200). Uses the `DOCKERHUB_USERNAME`/`DOCKERHUB_TOKEN` repo secrets; ghcr.io rides on the workflow's GITHUB_TOKEN. ## #443 — sensing-server REST API auth model QE security audit raised that 40+ /api/v1/* routes have no auth layer with a default `0.0.0.0` bind. New `wifi_densepose_sensing_server::bearer_auth` module + middleware: - Env-var-gated: `RUVIEW_API_TOKEN` unset/empty ⇒ middleware is a no-op (current LAN-mode behaviour preserved — **no default change**); set ⇒ every `/api/v1/*` request must carry `Authorization: Bearer <token>` or the server returns 401. - Constant-time byte compare via local `ct_eq` (no new dep). - `/health*`, `/ws/sensing`, and `/ui/*` are intentionally never gated (orchestrator probes + local browsers). - Startup logs which mode is active and warns when auth is ON with a `0.0.0.0` bind. - 8 unit tests on the middleware via `tower::ServiceExt::oneshot` (sensing-server lib tests 191 → 199, 0 failures). Verified locally: `cargo build --workspace --no-default-features` ✓, `cargo test -p wifi-densepose-sensing-server --no-default-features` ✓. Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
Generated
+3
-183
@@ -944,15 +944,6 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
|
||||
dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cookie"
|
||||
version = "0.18.1"
|
||||
@@ -1294,7 +1285,7 @@ version = "0.99.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f"
|
||||
dependencies = [
|
||||
"convert_case 0.4.0",
|
||||
"convert_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustc_version",
|
||||
@@ -3200,7 +3191,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf"
|
||||
dependencies = [
|
||||
"gtk-sys",
|
||||
"libloading 0.7.4",
|
||||
"libloading",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
@@ -3220,16 +3211,6 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libloading"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
version = "0.2.16"
|
||||
@@ -3643,63 +3624,6 @@ dependencies = [
|
||||
"getrandom 0.2.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "napi"
|
||||
version = "2.16.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55740c4ae1d8696773c78fdafd5d0e5fe9bc9f1b071c7ba493ba5c413a9184f3"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"ctor",
|
||||
"napi-derive",
|
||||
"napi-sys",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "napi-build"
|
||||
version = "2.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d376940fd5b723c6893cd1ee3f33abbfd86acb1cd1ec079f3ab04a2a3bc4d3b1"
|
||||
|
||||
[[package]]
|
||||
name = "napi-derive"
|
||||
version = "2.16.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7cbe2585d8ac223f7d34f13701434b9d5f4eb9c332cccce8dee57ea18ab8ab0c"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"convert_case 0.6.0",
|
||||
"napi-derive-backend",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "napi-derive-backend"
|
||||
version = "1.0.75"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1639aaa9eeb76e91c6ae66da8ce3e89e921cd3885e99ec85f4abacae72fc91bf"
|
||||
dependencies = [
|
||||
"convert_case 0.6.0",
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"semver",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "napi-sys"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "427802e8ec3a734331fec1035594a210ce1ff4dc5bc1950530920ab717964ea3"
|
||||
dependencies = [
|
||||
"libloading 0.8.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "native-tls"
|
||||
version = "0.2.18"
|
||||
@@ -5955,111 +5879,6 @@ version = "2.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "178f93f84a4a72c582026a45d9b8710acf188df4a22a25434c5dbba1df6c4cac"
|
||||
|
||||
[[package]]
|
||||
name = "rvcsi-adapter-file"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"rvcsi-core",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rvcsi-adapter-nexmon"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"rvcsi-core",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rvcsi-cli"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"rvcsi-adapter-file",
|
||||
"rvcsi-adapter-nexmon",
|
||||
"rvcsi-core",
|
||||
"rvcsi-runtime",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rvcsi-core"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rvcsi-dsp"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"rvcsi-core",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rvcsi-events"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"rvcsi-core",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rvcsi-node"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"napi",
|
||||
"napi-build",
|
||||
"napi-derive",
|
||||
"rvcsi-adapter-nexmon",
|
||||
"rvcsi-core",
|
||||
"rvcsi-runtime",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rvcsi-runtime"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"rvcsi-adapter-file",
|
||||
"rvcsi-adapter-nexmon",
|
||||
"rvcsi-core",
|
||||
"rvcsi-dsp",
|
||||
"rvcsi-events",
|
||||
"rvcsi-ruvector",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rvcsi-ruvector"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"rvcsi-core",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.23"
|
||||
@@ -8706,6 +8525,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"tower 0.4.13",
|
||||
"tower-http 0.5.2",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
|
||||
@@ -52,3 +52,5 @@ wifi-densepose-signal = { version = "0.3.0", path = "../wifi-densepose-signal",
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.10"
|
||||
# `tower::ServiceExt::oneshot` for in-process Router tests (bearer_auth).
|
||||
tower = { workspace = true }
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
//! Opt-in bearer-token auth for the sensing-server HTTP API (#443).
|
||||
//!
|
||||
//! When the `RUVIEW_API_TOKEN` environment variable is set, every request
|
||||
//! whose path begins with `/api/v1/` must carry a matching
|
||||
//! `Authorization: Bearer <token>` header, otherwise the server responds with
|
||||
//! `401 Unauthorized`. When the env var is unset (or empty), the middleware is
|
||||
//! a no-op and the API stays unauthenticated — preserving the long-standing
|
||||
//! LAN-only deployment posture documented in the issue. This is a binary,
|
||||
//! deployment-time switch with **no default authentication change**.
|
||||
//!
|
||||
//! Endpoints outside `/api/v1/*` (`/health*`, `/ws/sensing`, the static `/ui/*`
|
||||
//! mount, `/`) are intentionally **not** gated:
|
||||
//! * `/health*` is the liveness/readiness probe that orchestrators hit
|
||||
//! anonymously;
|
||||
//! * `/ws/sensing` and `/ui/*` are served to local browsers that can't easily
|
||||
//! inject headers — the sensitive control plane is the `/api/v1/*` tree, and
|
||||
//! that is what this layer protects.
|
||||
//!
|
||||
//! The header check uses a length-then-byte constant-time compare to avoid
|
||||
//! leaking the token through timing.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::{
|
||||
extract::{Request, State},
|
||||
http::{header::AUTHORIZATION, StatusCode},
|
||||
middleware::Next,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
|
||||
/// Environment variable that gates the middleware. Unset / empty ⇒ auth off.
|
||||
pub const API_TOKEN_ENV: &str = "RUVIEW_API_TOKEN";
|
||||
|
||||
/// Path prefix the middleware protects when auth is enabled.
|
||||
pub const PROTECTED_PREFIX: &str = "/api/v1/";
|
||||
|
||||
/// Cheap, cloneable handle to the configured token (or `None`).
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct AuthState {
|
||||
/// The expected bearer token, if any. `None` ⇒ middleware is a no-op.
|
||||
token: Option<Arc<String>>,
|
||||
}
|
||||
|
||||
impl AuthState {
|
||||
/// Build an [`AuthState`] from an explicit string. Empty ⇒ disabled.
|
||||
pub fn from_token(t: impl Into<String>) -> Self {
|
||||
let s = t.into();
|
||||
if s.is_empty() {
|
||||
AuthState { token: None }
|
||||
} else {
|
||||
AuthState { token: Some(Arc::new(s)) }
|
||||
}
|
||||
}
|
||||
|
||||
/// Read [`API_TOKEN_ENV`] from the process environment. Returns
|
||||
/// `AuthState { token: None }` when the variable is unset or empty.
|
||||
pub fn from_env() -> Self {
|
||||
match std::env::var(API_TOKEN_ENV) {
|
||||
Ok(s) if !s.is_empty() => AuthState::from_token(s),
|
||||
_ => AuthState::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the middleware will enforce auth on `/api/v1/*` requests.
|
||||
pub fn is_enabled(&self) -> bool {
|
||||
self.token.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
/// Constant-time byte slice equality. Returns `false` immediately on length
|
||||
/// mismatch (lengths are not secret here — both sides are fixed tokens).
|
||||
fn ct_eq(a: &[u8], b: &[u8]) -> bool {
|
||||
if a.len() != b.len() {
|
||||
return false;
|
||||
}
|
||||
let mut diff = 0u8;
|
||||
for (x, y) in a.iter().zip(b.iter()) {
|
||||
diff |= x ^ y;
|
||||
}
|
||||
diff == 0
|
||||
}
|
||||
|
||||
/// Axum middleware: enforces `Authorization: Bearer <token>` on `/api/v1/*`
|
||||
/// requests when [`AuthState::is_enabled`] returns `true`. Wires up via
|
||||
/// [`axum::middleware::from_fn_with_state`].
|
||||
pub async fn require_bearer(
|
||||
State(auth): State<AuthState>,
|
||||
request: Request,
|
||||
next: Next,
|
||||
) -> Response {
|
||||
let Some(expected) = auth.token.clone() else {
|
||||
return next.run(request).await;
|
||||
};
|
||||
if !request.uri().path().starts_with(PROTECTED_PREFIX) {
|
||||
return next.run(request).await;
|
||||
}
|
||||
let supplied = request
|
||||
.headers()
|
||||
.get(AUTHORIZATION)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|s| s.strip_prefix("Bearer "));
|
||||
let ok = supplied
|
||||
.map(|s| ct_eq(s.as_bytes(), expected.as_bytes()))
|
||||
.unwrap_or(false);
|
||||
if ok {
|
||||
next.run(request).await
|
||||
} else {
|
||||
(
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"missing or invalid bearer token (set Authorization: Bearer <RUVIEW_API_TOKEN>)\n",
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use axum::{
|
||||
body::Body,
|
||||
http::{Request, StatusCode},
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
use tower::ServiceExt;
|
||||
|
||||
fn ok_handler() -> Router {
|
||||
Router::new()
|
||||
.route("/health", get(|| async { "ok" }))
|
||||
.route("/api/v1/info", get(|| async { "ok" }))
|
||||
.route("/api/v1/sensitive", axum::routing::post(|| async { "ok" }))
|
||||
.route("/ui/index.html", get(|| async { "<html/>" }))
|
||||
}
|
||||
|
||||
fn wrap(auth: AuthState) -> Router {
|
||||
ok_handler()
|
||||
.layer(axum::middleware::from_fn_with_state(auth, require_bearer))
|
||||
}
|
||||
|
||||
async fn status(router: Router, method: &str, path: &str, auth: Option<&str>) -> StatusCode {
|
||||
let mut req = Request::builder()
|
||||
.method(method)
|
||||
.uri(path)
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
if let Some(t) = auth {
|
||||
req.headers_mut()
|
||||
.insert(AUTHORIZATION, format!("Bearer {t}").parse().unwrap());
|
||||
}
|
||||
router.oneshot(req).await.unwrap().status()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn middleware_is_no_op_when_token_unset() {
|
||||
let r = wrap(AuthState::default());
|
||||
assert_eq!(status(r.clone(), "GET", "/api/v1/info", None).await, StatusCode::OK);
|
||||
assert_eq!(status(r.clone(), "POST", "/api/v1/sensitive", None).await, StatusCode::OK);
|
||||
assert_eq!(status(r.clone(), "GET", "/health", None).await, StatusCode::OK);
|
||||
assert_eq!(status(r, "GET", "/ui/index.html", None).await, StatusCode::OK);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn enabled_blocks_api_without_bearer() {
|
||||
let r = wrap(AuthState::from_token("s3cr3t"));
|
||||
assert_eq!(status(r.clone(), "GET", "/api/v1/info", None).await, StatusCode::UNAUTHORIZED);
|
||||
assert_eq!(
|
||||
status(r, "POST", "/api/v1/sensitive", None).await,
|
||||
StatusCode::UNAUTHORIZED
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn enabled_blocks_api_with_wrong_bearer() {
|
||||
let r = wrap(AuthState::from_token("s3cr3t"));
|
||||
assert_eq!(
|
||||
status(r.clone(), "GET", "/api/v1/info", Some("nope")).await,
|
||||
StatusCode::UNAUTHORIZED
|
||||
);
|
||||
// Wrong scheme (Basic / token) — only "Bearer <token>" is accepted.
|
||||
let mut req = Request::builder()
|
||||
.method("GET")
|
||||
.uri("/api/v1/info")
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
req.headers_mut()
|
||||
.insert(AUTHORIZATION, "Basic s3cr3t".parse().unwrap());
|
||||
assert_eq!(r.oneshot(req).await.unwrap().status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn enabled_allows_api_with_correct_bearer() {
|
||||
let r = wrap(AuthState::from_token("s3cr3t"));
|
||||
assert_eq!(
|
||||
status(r.clone(), "GET", "/api/v1/info", Some("s3cr3t")).await,
|
||||
StatusCode::OK
|
||||
);
|
||||
assert_eq!(
|
||||
status(r, "POST", "/api/v1/sensitive", Some("s3cr3t")).await,
|
||||
StatusCode::OK
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn enabled_never_gates_paths_outside_api_v1() {
|
||||
let r = wrap(AuthState::from_token("s3cr3t"));
|
||||
// Even with auth ON, `/health` and `/ui/*` are reachable without a token:
|
||||
// orchestrator probes and the local UI need to load unchallenged.
|
||||
assert_eq!(status(r.clone(), "GET", "/health", None).await, StatusCode::OK);
|
||||
assert_eq!(status(r, "GET", "/ui/index.html", None).await, StatusCode::OK);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ct_eq_basics() {
|
||||
assert!(ct_eq(b"abc", b"abc"));
|
||||
assert!(!ct_eq(b"abc", b"abd"));
|
||||
assert!(!ct_eq(b"abc", b"ab")); // length mismatch
|
||||
assert!(!ct_eq(b"", b"x"));
|
||||
assert!(ct_eq(b"", b""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_env_treats_empty_as_disabled() {
|
||||
// Avoid touching the real env in a thread-shared test — exercise the
|
||||
// string ctor directly with the same trim logic.
|
||||
assert!(!AuthState::from_token("").is_enabled());
|
||||
assert!(AuthState::from_token("x").is_enabled());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn protected_prefix_and_env_constants_are_stable() {
|
||||
// These are documented in the issue body and the README; keep them locked.
|
||||
assert_eq!(API_TOKEN_ENV, "RUVIEW_API_TOKEN");
|
||||
assert_eq!(PROTECTED_PREFIX, "/api/v1/");
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,9 @@
|
||||
//! This crate provides:
|
||||
//! - Vital sign detection from WiFi CSI amplitude data
|
||||
//! - RVF (RuVector Format) binary container for model weights
|
||||
//! - Opt-in bearer-token auth for the `/api/v1/*` HTTP surface (`bearer_auth`)
|
||||
|
||||
pub mod bearer_auth;
|
||||
pub mod vital_signs;
|
||||
pub mod rvf_container;
|
||||
pub mod rvf_pipeline;
|
||||
|
||||
@@ -4861,6 +4861,26 @@ async fn main() {
|
||||
let bind_ip: std::net::IpAddr = args.bind_addr.parse()
|
||||
.expect("Invalid --bind-addr (use 127.0.0.1 or 0.0.0.0)");
|
||||
|
||||
// #443: optional bearer-token auth on `/api/v1/*`. `RUVIEW_API_TOKEN`
|
||||
// unset/empty ⇒ middleware is a no-op (LAN-mode default preserved); set ⇒
|
||||
// every `/api/v1/*` request must carry `Authorization: Bearer <token>`.
|
||||
let bearer_auth_state = wifi_densepose_sensing_server::bearer_auth::AuthState::from_env();
|
||||
if bearer_auth_state.is_enabled() {
|
||||
info!(
|
||||
"API auth: bearer-token enforcement ON for /api/v1/* (RUVIEW_API_TOKEN set)"
|
||||
);
|
||||
if bind_ip.is_unspecified() {
|
||||
warn!(
|
||||
"API auth ON but bind-addr is {} — consider --bind-addr 127.0.0.1 for LAN-only deployments",
|
||||
bind_ip
|
||||
);
|
||||
}
|
||||
} else {
|
||||
info!(
|
||||
"API auth: OFF — /api/v1/* is unauthenticated. Set RUVIEW_API_TOKEN=<token> to enforce bearer auth."
|
||||
);
|
||||
}
|
||||
|
||||
// WebSocket server on dedicated port (8765)
|
||||
let ws_state = state.clone();
|
||||
let ws_app = Router::new()
|
||||
@@ -4947,6 +4967,14 @@ async fn main() {
|
||||
axum::http::header::CACHE_CONTROL,
|
||||
HeaderValue::from_static("no-cache, no-store, must-revalidate"),
|
||||
))
|
||||
// Opt-in bearer-token auth on `/api/v1/*` (#443). When `RUVIEW_API_TOKEN`
|
||||
// is unset/empty the middleware is a no-op — the default stays
|
||||
// LAN-mode-friendly. `/health*`, `/ws/sensing`, and `/ui/*` are never
|
||||
// gated (orchestrator probes + local browsers).
|
||||
.layer(axum::middleware::from_fn_with_state(
|
||||
bearer_auth_state.clone(),
|
||||
wifi_densepose_sensing_server::bearer_auth::require_bearer,
|
||||
))
|
||||
.with_state(state.clone());
|
||||
|
||||
let http_addr = SocketAddr::from((bind_ip, args.http_port));
|
||||
|
||||
Reference in New Issue
Block a user