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:
+21
-94
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user