mirror of
https://github.com/ruvnet/RuView
synced 2026-08-01 19:01:42 +00:00
fix(homecore-api): close WS auth bypass + reply-theater, harden dev bin (ADR-161 A1/A2/A8)
A1 (CRITICAL): the /api/websocket handshake accepted any non-empty token, ignoring the LongLivedTokenStore whitelist the REST path enforces — a full WS auth bypass. Now validates via state.tokens().is_valid() before auth_ok; wrong tokens get auth_invalid + close. A2 (HIGH): WS command replies were pushed into an mpsc whose only consumer logged and discarded them — no result/pong/event reached the client. Split the socket with futures StreamExt::split; a dedicated writer task drains the response channel onto the wire. A8 (HIGH): the homecore-api dev bin bound 0.0.0.0 with unconditional allow-any auth and no env path. Wired the HOMECORE_TOKENS env path (dev fallback warn-logged when unset) and defaulted the bind to 127.0.0.1 (HOMECORE_BIND to opt into LAN). Tests (fail on old source): - ws_handshake::wrong_token_is_rejected (old → auth_ok) - ws_handshake::result_reply_is_received / ping_pong_reply_is_received (old → timeout) - server_bin_auth::provisioned_bin_rejects_wrong_bearer / from_env_path_enforces_whitelist Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -1,15 +1,31 @@
|
||||
//! `homecore-api-server` binary. Boots a HomeCore runtime and serves
|
||||
//! the HA-compat REST + WS API on `:8123`.
|
||||
//! the HA-compat REST + WS API.
|
||||
//!
|
||||
//! P1: bare-minimum bring-up. No persistence, no plugins, no auth
|
||||
//! beyond "any non-empty bearer". Useful for `curl` smoke tests of
|
||||
//! the wire format from the existing HA companion app:
|
||||
//! ## Auth (ADR-161, HC-WS-08)
|
||||
//!
|
||||
//! Token provisioning matches `homecore-server`: if `HOMECORE_TOKENS`
|
||||
//! is set (comma-separated bearer tokens) the API enforces that
|
||||
//! whitelist on both the REST and WS paths. If it is **unset**, the
|
||||
//! binary falls back to an explicitly-logged DEV mode (any non-empty
|
||||
//! bearer accepted) — before this fix the bin unconditionally used
|
||||
//! `allow_any_non_empty()` with no env path, so a provisioned operator
|
||||
//! had no way to lock it down.
|
||||
//!
|
||||
//! ## Bind address
|
||||
//!
|
||||
//! Defaults to `127.0.0.1` (loopback only) so a bare `cargo run` of
|
||||
//! this dev binary is not network-exposed. Override with
|
||||
//! `HOMECORE_BIND=0.0.0.0:8123` for a LAN deployment (and provision
|
||||
//! `HOMECORE_TOKENS` when you do).
|
||||
//!
|
||||
//! cargo run -p homecore-api --bin homecore-api-server
|
||||
//! curl -H "Authorization: Bearer test" http://127.0.0.1:8123/api/
|
||||
//! HOMECORE_TOKENS=secret curl -H "Authorization: Bearer secret" \
|
||||
//! http://127.0.0.1:8123/api/
|
||||
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use homecore::HomeCore;
|
||||
use homecore_api::{router, SharedState, DEFAULT_PORT};
|
||||
use homecore_api::{router, LongLivedTokenStore, SharedState, DEFAULT_PORT};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
@@ -21,10 +37,34 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.init();
|
||||
|
||||
let homecore = HomeCore::new();
|
||||
let state = SharedState::new(homecore);
|
||||
|
||||
// Token provisioning (HC-WS-08). Prefer the HOMECORE_TOKENS env
|
||||
// whitelist; fall back to DEV mode (warn-logged) only when unset.
|
||||
let tokens = if std::env::var("HOMECORE_TOKENS")
|
||||
.map(|v| !v.trim().is_empty())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
let s = LongLivedTokenStore::from_env();
|
||||
let n = s.len().await;
|
||||
tracing::info!("LongLivedTokenStore provisioned with {n} bearer token(s) from HOMECORE_TOKENS");
|
||||
s
|
||||
} else {
|
||||
tracing::warn!(
|
||||
"HOMECORE_TOKENS not set — token store in DEV mode (any non-empty bearer \
|
||||
accepted). Set HOMECORE_TOKENS before exposing this binary to the network."
|
||||
);
|
||||
LongLivedTokenStore::allow_any_non_empty()
|
||||
};
|
||||
|
||||
let state = SharedState::with_tokens(homecore, "Home", env!("CARGO_PKG_VERSION"), tokens);
|
||||
let app = router(state);
|
||||
|
||||
let addr = std::net::SocketAddr::from(([0, 0, 0, 0], DEFAULT_PORT));
|
||||
// Default to loopback so `cargo run` is not network-exposed; allow
|
||||
// an explicit HOMECORE_BIND override for LAN deployments.
|
||||
let addr: SocketAddr = match std::env::var("HOMECORE_BIND") {
|
||||
Ok(v) if !v.trim().is_empty() => v.parse()?,
|
||||
_ => SocketAddr::from(([127, 0, 0, 1], DEFAULT_PORT)),
|
||||
};
|
||||
tracing::info!("HOMECORE-API listening on http://{addr} (HA-compat /api + /api/websocket)");
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(addr).await?;
|
||||
|
||||
Reference in New Issue
Block a user