From eb990f5e5877521a6dcfbc84717dcf035677a75b Mon Sep 17 00:00:00 2001 From: Ministorm3 <4474921+Ministorm3@users.noreply.github.com> Date: Tue, 28 Jul 2026 17:32:49 -0400 Subject: [PATCH 1/2] feat: use X-Forwarded-Proto/Host for generated urls behind a reverse proxy Co-Authored-By: Claude Fable 5 --- crates/ersatztv/src/main.rs | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/crates/ersatztv/src/main.rs b/crates/ersatztv/src/main.rs index 262a2af..5a95047 100644 --- a/crates/ersatztv/src/main.rs +++ b/crates/ersatztv/src/main.rs @@ -312,14 +312,27 @@ async fn xmltv( } fn get_scheme_host(request: &axum::extract::Request) -> String { - // TODO: need scheme, host from reverse proxy - let host = request + let scheme = + get_first_header_value(request, "x-forwarded-proto").unwrap_or_else(|| "http".to_string()); + + let host = get_first_header_value(request, "x-forwarded-host") + .or_else(|| get_first_header_value(request, axum::http::header::HOST.as_str())) + .unwrap_or_else(|| "localhost".to_string()); + + format!("{scheme}://{host}") +} + +// headers set by chained proxies may contain multiple comma-separated values; +// the first value is the one set by the proxy closest to the client +fn get_first_header_value(request: &axum::extract::Request, name: &str) -> Option { + request .headers() - .get(axum::http::header::HOST) + .get(name) .and_then(|v| v.to_str().ok()) - .unwrap_or("localhost"); - - format!("http://{host}") + .and_then(|v| v.split(',').next()) + .map(str::trim) + .filter(|v| !v.is_empty()) + .map(str::to_string) } async fn session_middleware( From 541d549d0dacf68724632a2d7c6bea99b1be941e Mon Sep 17 00:00:00 2001 From: Jason Dove <1695733+jasongdove@users.noreply.github.com> Date: Thu, 30 Jul 2026 19:29:39 -0500 Subject: [PATCH 2/2] perf tweaks --- crates/ersatztv/src/main.rs | 41 ++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/crates/ersatztv/src/main.rs b/crates/ersatztv/src/main.rs index 5a95047..9dc6753 100644 --- a/crates/ersatztv/src/main.rs +++ b/crates/ersatztv/src/main.rs @@ -8,6 +8,7 @@ use std::path::PathBuf; use std::sync::Arc; use axum::extract::{Path, State}; +use axum::http::HeaderMap; use axum::response::IntoResponse; use axum::{Router, routing::get}; use clap::{Parser, Subcommand}; @@ -205,7 +206,7 @@ async fn stream( Err(_) => return Err(LineupError::ChannelNotReady), // 30s deadline } - let content = get_multi_variant(channel, request); + let content = get_multi_variant(channel, request.headers()); Ok(( [( @@ -236,13 +237,13 @@ async fn fix_content_types( response } -fn get_multi_variant(channel: &ChannelModel, request: axum::extract::Request) -> String { +fn get_multi_variant(channel: &ChannelModel, headers: &HeaderMap) -> String { let mut result = String::new(); result.push_str("#EXTM3U\n"); result.push_str("#EXT-X-VERSION:6\n"); result.push_str(&format!( "#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"English\",DEFAULT=YES,AUTOSELECT=YES,FORCED=NO,LANGUAGE=\"en\",URI=\"{}/session/{}/live_sub.m3u8\"\n", - get_scheme_host(&request), + get_scheme_host(headers), channel.number() )); result.push_str(&format!( @@ -251,7 +252,7 @@ fn get_multi_variant(channel: &ChannelModel, request: axum::extract::Request) -> )); result.push_str(&format!( "{}/session/{}/live.m3u8", - get_scheme_host(&request), + get_scheme_host(headers), channel.number() )); @@ -263,7 +264,8 @@ async fn channel_playlist( request: axum::extract::Request, ) -> Result { let mut content = String::new(); - let xmltv_url = format!("{}/xmltv.xml", get_scheme_host(&request)); + let scheme_host = get_scheme_host(request.headers()); + let xmltv_url = format!("{}/xmltv.xml", scheme_host); content.push_str(&format!( "#EXTM3U url-tvg=\"{xmltv_url}\" x-tvg-url=\"{xmltv_url}\"\n" )); @@ -289,7 +291,7 @@ async fn channel_playlist( )); content.push_str(&format!( "{}/channel/{}.m3u8\n", - get_scheme_host(&request), + scheme_host, channel.number() )); } @@ -311,28 +313,29 @@ async fn xmltv( )) } -fn get_scheme_host(request: &axum::extract::Request) -> String { - let scheme = - get_first_header_value(request, "x-forwarded-proto").unwrap_or_else(|| "http".to_string()); +fn get_scheme_host(headers: &HeaderMap) -> String { + let scheme = get_first_header_value(headers, "x-forwarded-proto") + .map(|s| s.to_lowercase()) + .unwrap_or_else(|| "http".to_string()); - let host = get_first_header_value(request, "x-forwarded-host") - .or_else(|| get_first_header_value(request, axum::http::header::HOST.as_str())) - .unwrap_or_else(|| "localhost".to_string()); + let host = get_first_header_value(headers, "x-forwarded-host") + .or_else(|| get_first_header_value(headers, axum::http::header::HOST.as_str())) + .unwrap_or("localhost"); format!("{scheme}://{host}") } // headers set by chained proxies may contain multiple comma-separated values; // the first value is the one set by the proxy closest to the client -fn get_first_header_value(request: &axum::extract::Request, name: &str) -> Option { - request - .headers() - .get(name) - .and_then(|v| v.to_str().ok()) - .and_then(|v| v.split(',').next()) +fn get_first_header_value<'a>(headers: &'a HeaderMap, name: &str) -> Option<&'a str> { + headers + .get(name)? + .to_str() + .ok()? + .split(',') + .next() .map(str::trim) .filter(|v| !v.is_empty()) - .map(str::to_string) } async fn session_middleware(