Files
terminal/server/src/lib.rs
T
Michael Shimeles 0690de8c6c Harden the gateway: optional token for /stats and /metrics, per-address cap, flood close, sandboxed unit (#33)
* Gate /stats and /metrics behind an optional token and harden the gateway

- TR_METRICS_TOKEN: when set, /stats and /metrics answer 404 unless the
  request sends it as a bearer token. /healthz and /readyz stay public.
  The token never shows in Debug output or errors.
- TR_MAX_CLIENTS_PER_IP caps connections per address (IPv6 per /64), off
  by default. TR_CLIENT_IP_HEADER takes the address from the edge's
  header. The first connection logs whether x-forwarded-for was there,
  never its value, so the cap can be checked behind Freestyle's edge.
- A client that keeps sending after a budget's worth of RateLimited
  replies is closed with 1008 instead of being answered forever.
- cli stats sends TR_METRICS_TOKEN when set.
- vm-setup.sh: optional /etc/tr-gateway/env, stop timeout, memory and
  task caps, and systemd sandboxing (security score 8.3 to 3.1).

* Let the site's server URL come from a repository variable

deploy-site.yml passes the TR_SERVER_URL repository variable to
build-web.sh when it's set, so moving the site to the gateway's custom
domain is a variable and a redeploy, not a code change.

* Accept the bearer scheme in any case

HTTP auth scheme names are case-insensitive, so "bearer <token>" now
works too. The token itself is still compared exactly.
2026-11-02 15:39:07 -05:00

98 lines
3.0 KiB
Rust

//! `tr-gateway`, the Terminal data server (`docs/architecture/server.md`).
//!
//! For now it carries Hyperliquid's public feed ([`hl::HlPublicProvider`]): one shared set of
//! upstream connections, fanned out to every client over the protocol in `tr-proto`.
pub mod access;
pub mod config;
pub mod hl;
pub mod http;
pub mod limit;
pub mod metrics;
pub mod provider;
pub mod session;
pub mod store;
use std::net::SocketAddr;
use std::sync::Arc;
use anyhow::{Context, Result};
use tokio::sync::{oneshot, watch};
use tokio::task::JoinHandle;
use tr_core::Bar;
pub use config::Config;
use crate::hl::HlPublicProvider;
use crate::metrics::Metrics;
use crate::store::Hub;
/// Keep at most `limit` bars opening before `end`.
pub fn clip(mut bars: Vec<Bar>, end: Option<i64>, limit: usize) -> Vec<Bar> {
if let Some(end) = end {
bars.retain(|b| b.ts < end);
}
if bars.len() > limit {
bars.drain(..bars.len() - limit);
}
bars
}
/// A running gateway.
pub struct Gateway {
pub addr: SocketAddr,
pub state: http::AppState,
stop: Option<oneshot::Sender<()>>,
closing: watch::Sender<bool>,
task: JoinHandle<()>,
}
impl Gateway {
pub fn hub(&self) -> &Arc<Hub> {
&self.state.hub
}
/// Stop accepting, close every client session and upstream connection, and wait for the
/// server to finish. Nothing keeps running afterwards.
pub async fn shutdown(mut self) {
self.closing.send_replace(true);
if let Some(s) = self.stop.take() {
let _ = s.send(());
}
self.state.hub.stop();
let _ = (&mut self.task).await;
}
/// Run until the process is asked to stop.
pub async fn wait(self) {
let _ = self.task.await;
}
}
/// Bind, connect upstream and serve in the background.
pub async fn start(cfg: Config) -> Result<Gateway> {
let metrics = Arc::new(Metrics::default());
let provider = Arc::new(HlPublicProvider::new(cfg.hl.clone(), metrics.clone())?);
let hub = Hub::new(provider, cfg.hub.clone(), metrics);
hub.start();
let (closing, closing_rx) = watch::channel(false);
let access = http::Access::new(cfg.metrics_token.clone(), cfg.max_clients_per_ip, cfg.client_ip_header.clone());
let state = http::AppState::new(hub, cfg.session.clone(), cfg.max_clients, closing_rx).with_access(access);
let listener = tokio::net::TcpListener::bind(cfg.bind).await.context("binding the listen address")?;
let addr = listener.local_addr()?;
let (stop, stopped) = oneshot::channel::<()>();
let app = http::router(state.clone());
let task = tokio::spawn(async move {
let shutdown = async {
let _ = stopped.await;
};
if let Err(e) = axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>())
.with_graceful_shutdown(shutdown)
.await
{
tracing::error!("server: {e}");
}
});
Ok(Gateway { addr, state, stop: Some(stop), closing, task })
}