Sidecar: auth token for the website-facing API
config.rs loads all runtime settings from an external sidecar.toml (path via $UOLINK_CONFIG), with env-var overrides (UOLINK_WEB_TOKEN, UOLINK_WEB_BIND, UOLINK_SHARD_BIND, UOLINK_DB_PATH). Nothing is compiled into the binary. On first run the file is generated with a random 24-byte auth token, so the sidecar is secured out of the box and the operator just copies the token to the website. An axum middleware rejects any request to a non-/health route that does not present the token, as Authorization: Bearer, X-Api-Key, or ?token= (the last so browser WebSocket clients, which cannot set handshake headers, can authenticate). The comparison is constant-time. An empty token disables auth and is only tolerated on a loopback bind; binding to 0.0.0.0 with no token logs a warning. Verified: /health open (200); /history 401 without a token, 401 with a wrong one, 200 with the right one via either Bearer or X-Api-Key; an authed shard query falls through to 503 when no shard is connected; WS rejected (401) with a bad ?token= and upgraded (101) with the right one. sidecar.toml is gitignored (holds the secret); sidecar.toml.example is committed as the reference. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -3,27 +3,34 @@
|
||||
//! Terminates the loopback link to the ServUO shard and exposes a website-facing HTTP surface. So
|
||||
//! far: the shard link (bidirectional) and a WebSocket live feed. REST queries and SQLite come next.
|
||||
|
||||
mod config;
|
||||
mod rpc;
|
||||
mod shard;
|
||||
mod store;
|
||||
mod web;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use tokio::sync::{broadcast, mpsc};
|
||||
use tracing::info;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
const SHARD_ADDR: &str = "127.0.0.1:7788";
|
||||
const WEB_ADDR: &str = "127.0.0.1:8080";
|
||||
const DB_PATH: &str = "uo-link.db";
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
init_tracing();
|
||||
info!("uo-link sidecar starting");
|
||||
|
||||
let cfg = config::Config::load()?;
|
||||
info!(
|
||||
shard = %cfg.shard.bind,
|
||||
web = %cfg.web.bind,
|
||||
auth = cfg.auth_required(),
|
||||
"configuration loaded"
|
||||
);
|
||||
|
||||
// Shard link: events in, commands out.
|
||||
let (event_tx, mut event_rx) = mpsc::unbounded_channel::<shard::ShardEvent>();
|
||||
let handle = shard::serve(SHARD_ADDR, event_tx).await?;
|
||||
let handle = shard::serve(&cfg.shard.bind, event_tx).await?;
|
||||
|
||||
// Live feed: every shard event fans out to all connected website WebSocket clients.
|
||||
let (bcast_tx, _) = broadcast::channel::<String>(1024);
|
||||
@@ -32,7 +39,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
let rpc = rpc::Rpc::new();
|
||||
|
||||
// Durable store: event history, economy series, cached profiles, link map.
|
||||
let store = store::Store::open(DB_PATH).await?;
|
||||
let store = store::Store::open(&cfg.store.path).await?;
|
||||
|
||||
// Website-facing HTTP server.
|
||||
let web_state = web::AppState {
|
||||
@@ -40,9 +47,11 @@ async fn main() -> anyhow::Result<()> {
|
||||
shard: handle.clone(),
|
||||
rpc: rpc.clone(),
|
||||
store: store.clone(),
|
||||
token: Arc::new(cfg.web.auth_token.clone()),
|
||||
};
|
||||
let web_bind = cfg.web.bind.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = web::serve(WEB_ADDR, web_state).await {
|
||||
if let Err(e) = web::serve(&web_bind, web_state).await {
|
||||
tracing::error!(error = %e, "web server exited");
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user