Cleanup: over-engineering pass

- merge /api/uptime + /api/version into /api/health and /api/stats
- collapse RTMP per-codec dispatch into a single CodecParser path
- dedupe STUN attribute walking (ufrag_pair / parse_xor_mapped_address)
- dedupe StreamCodec <-> str0m Codec mapping via StreamCodec::from_str0m
- dedupe bearer token extraction in WHIP handlers
- replace regex charset validation with stdlib checks
- flatten one-field wrappers (WebRtcProxyConfig, HttpServerConfig, ServerInfo)
- remove SessionCookie extractor (only fed debug logs)
- delete dead code: entity ActiveModel helpers, get_stream_session,
  WebrtcProxy::local_addr, _connected flag, commented-out blocks
- drop unused deps: futures, rand, regex, serde_json
This commit is contained in:
2026-08-21 11:39:02 +01:00
parent 0a039a97c4
commit 98dbb3d36d
11 changed files with 113 additions and 353 deletions
+21 -94
View File
@@ -1,28 +1,20 @@
use std::{
net::SocketAddr,
sync::{
Arc, LazyLock,
Arc,
atomic::{AtomicI32, Ordering},
},
time::Duration,
time::{Duration, Instant},
};
use axum_extra::extract::{CookieJar, cookie::Cookie};
use regex::Regex;
// The Rust `regex` crate is guaranteed linear-time and therefore does NOT
// support lookaround, so the JS source pattern
// /^(?=.*[A-Za-z])[A-Za-z0-9_-]{1,67}$/
// cannot be ported verbatim. The `(?=.*[A-Za-z])` lookahead only means
// "must contain at least one letter" — we drop it from the pattern and
// enforce that condition with a separate `.chars().any(..)` check below.
static KEY_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^[A-Za-z0-9'-]{1,67}$").unwrap());
// Allowed charset for stream-key passwords and custom IDs: letters, numbers,
// dashes and apostrophes only — no spaces. Mirrors the frontend
// `/^[A-Za-z0-9'-]{0,67}$/` used by the keys-page popups; empty clears the value.
static PASSWORD_RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^[A-Za-z0-9'-]{0,67}$").unwrap());
/// Allowed charset for labels, passwords and custom IDs: letters, numbers,
/// dashes and apostrophes — no spaces. Mirrors the frontend
/// `/^[A-Za-z0-9'-]{0,67}$/` used by the keys-page popups.
fn valid_charset(s: &str) -> bool {
s.len() <= 67 && s.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '\'')
}
use axum::{
Json, Router,
@@ -58,23 +50,15 @@ use crate::{
const MAX_USERNAME_LEN: usize = 32;
const MAX_LABEL_LEN: usize = 64;
pub struct HttpServerConfig {
pub signup_code: String,
}
pub struct ServerInfo {
pub version: &'static str,
pub start_time: std::time::Instant,
}
pub struct HttpServer {
pub offer_tx: Sender<(i32, i32, String)>,
pub accept_rx: async_broadcast::InactiveReceiver<(i32, Result<String, String>)>,
pub appstate: Arc<Mutex<AppState>>,
pub request_count: AtomicI32,
pub db: DatabaseConnection,
pub config: Arc<HttpServerConfig>,
pub info: Arc<ServerInfo>,
pub signup_code: String,
pub version: &'static str,
pub start_time: Instant,
}
impl HttpServer {
@@ -112,8 +96,6 @@ impl HttpServer {
.get(get_all_stream_keys)
.patch(edit_stream_key),
)
// .route("/api/admin/server_stats", get(todo!()))
// .route("/api/whip", post(handle_whip_injest))
.route("/api/login", post(login_handler))
.route("/api/stream/{slug}", post(stream_handler))
.route("/api/whip", post(handle_whip_injest))
@@ -121,14 +103,8 @@ impl HttpServer {
"/api/whip/{slug}",
delete(handle_whip_injest_delete).patch(handle_whip_injest_patch),
)
// .route(
// "/api/whip/{slug}",
// delete(handle_whip_injest_delete).patch(handle_whip_injest_patch),
// )
.route("/api/meow", get(meow_handler))
.route("/api/health", get(health_handler))
.route("/api/uptime", get(uptime_handler))
.route("/api/version", get(version_handler))
.route("/api/stats", get(stats_handler))
.layer(cors)
.with_state(state);
@@ -172,25 +148,23 @@ async fn edit_stream_key(
) -> Result<impl IntoResponse, HttpError> {
// Trim surrounding whitespace so labels aren't stored with leading/trailing spaces.
let new_label = payload.new.as_deref().map(str::trim);
// Length (1..=67) and allowed charset ([A-Za-z0-9'-], no spaces).
// Length (1..=67), allowed charset, and at least one letter.
if let Some(label) = new_label {
if !KEY_RE.is_match(label) {
if label.is_empty() || !valid_charset(label) {
return Err(HttpError::BadRequest("invalid label".into()));
}
// Replaces the JS lookahead: label must contain at least one letter.
if !label.chars().any(|c| c.is_ascii_alphabetic()) {
return Err(HttpError::BadRequest("label must contain a letter".into()));
}
}
// Custom IDs and passwords share a charset: letters, numbers, dashes,
// apostrophes — no spaces. Empty values are allowed (they clear the field).
// Empty values are allowed (they clear the field).
if let Some(custom_id) = payload.custom_id.as_deref() {
if !custom_id.is_empty() && !PASSWORD_RE.is_match(custom_id) {
if !custom_id.is_empty() && !valid_charset(custom_id) {
return Err(HttpError::BadRequest("invalid custom id".into()));
}
}
if let Some(pwd) = payload.password.as_deref() {
if !pwd.is_empty() && !PASSWORD_RE.is_match(pwd) {
if !pwd.is_empty() && !valid_charset(pwd) {
return Err(HttpError::BadRequest("invalid password".into()));
}
}
@@ -288,22 +262,6 @@ struct CreateStreamKeyBody {
label: String,
}
struct SessionCookie(Option<String>);
impl<S> FromRequestParts<S> for SessionCookie
where
S: Send + Sync,
{
type Rejection = HttpError;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
let jar = CookieJar::from_request_parts(parts, _state).await.unwrap();
Ok(SessionCookie(
jar.get("session").map(|c| c.value().to_string()),
))
}
}
struct AuthUser(entity::users::Model);
impl FromRequestParts<Arc<HttpServer>> for AuthUser {
@@ -409,13 +367,10 @@ fn cookie_for_token(token: &str, dev: bool) -> Cookie<'static> {
#[axum::debug_handler]
async fn login_handler(
session: SessionCookie,
State(state): State<Arc<HttpServer>>,
DevFlag(dev): DevFlag,
Json(payload): Json<LoginForm>,
) -> Result<impl IntoResponse, HttpError> {
tracing::debug!(session = ?session.0, "login: existing session cookie");
let user = users::Entity::find_by_username(&state.db, payload.username.clone())
.await?
.ok_or_else(|| {
@@ -447,13 +402,11 @@ struct CreateUserForm {
}
async fn create_user_handler(
session: SessionCookie,
State(state): State<Arc<HttpServer>>,
DevFlag(dev): DevFlag,
Json(payload): Json<CreateUserForm>,
) -> Result<impl IntoResponse, HttpError> {
tracing::debug!(session = ?session.0, "create_user: existing session cookie");
if state.config.signup_code.is_empty() || payload.ref_token != state.config.signup_code {
if state.signup_code.is_empty() || payload.ref_token != state.signup_code {
warn!(username = %payload.username, "signup rejected: invalid signup code");
return Err(HttpError::Unauthorized);
}
@@ -596,43 +549,17 @@ struct HealthResponse {
async fn health_handler(
State(state): State<Arc<HttpServer>>,
) -> Result<impl IntoResponse, HttpError> {
let uptime = state.info.start_time.elapsed().as_secs();
let uptime = state.start_time.elapsed().as_secs();
Ok((
StatusCode::OK,
Json(HealthResponse {
status: "ok",
uptime_seconds: uptime,
version: state.info.version,
version: state.version,
}),
))
}
#[derive(Serialize)]
struct UptimeResponse {
uptime_seconds: u64,
}
async fn uptime_handler(
State(state): State<Arc<HttpServer>>,
) -> Result<impl IntoResponse, HttpError> {
let uptime = state.info.start_time.elapsed().as_secs();
Ok((
StatusCode::OK,
Json(UptimeResponse {
uptime_seconds: uptime,
}),
))
}
async fn version_handler(
State(state): State<Arc<HttpServer>>,
) -> Result<impl IntoResponse, HttpError> {
Ok((
StatusCode::OK,
Json(serde_json::json!({ "version": state.info.version })),
))
}
#[derive(Serialize)]
struct StatsResponse {
version: &'static str,
@@ -645,7 +572,7 @@ struct StatsResponse {
async fn stats_handler(
State(state): State<Arc<HttpServer>>,
) -> Result<impl IntoResponse, HttpError> {
let uptime = state.info.start_time.elapsed().as_secs();
let uptime = state.start_time.elapsed().as_secs();
let mut system = System::new();
system.refresh_cpu_all();
let cpu_usage = system.global_cpu_usage();
@@ -657,7 +584,7 @@ async fn stats_handler(
Ok((
StatusCode::OK,
Json(StatsResponse {
version: state.info.version,
version: state.version,
uptime_seconds: uptime,
cpu_usage_percent: cpu_usage,
active_streams,