From ddcd56b1ea07e9b21beb6bb569d67dcbd5311b1d Mon Sep 17 00:00:00 2001 From: euzu Date: Fri, 21 Nov 2025 17:09:14 +0100 Subject: [PATCH] Resource rewrite secret key to ensure rewritten url validity Fixed epg rewrite --- CHANGELOG.md | 2 + Cargo.lock | 6 +- README.md | 17 ++++ backend/Cargo.toml | 2 +- backend/src/api/endpoints/xmltv_api.rs | 28 +++++- backend/src/model/config/app.rs | 5 ++ backend/src/model/config/reverse_proxy.rs | 5 +- backend/src/repository/epg_repository.rs | 2 +- frontend/Cargo.toml | 4 +- frontend/public/assets/i18n/en.json | 1 + frontend/src/app/components/config/macros.rs | 41 +++++++++ .../config/reverse_proxy_config_view.rs | 89 ++++++++++++------- shared/Cargo.toml | 2 +- shared/src/model/config/reverse_proxy.rs | 8 +- shared/src/utils/default_utils.rs | 8 ++ shared/src/utils/serde_utils.rs | 35 ++++++++ 16 files changed, 208 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bd555937..f99bd5747 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog # 3.3.0 (2025-11-xx) - !BREAKING CHANGE! `config.yml` `threads` attribute is now renamed to `process_parallel` and is a boolean value true or false. +- !BREAKING CHANGE! `config.yml` added reverse proxy config `rewrite_secret` field to keep resource URLs valid after restart. - Avoid blocking the runtime when warming cache - Normalize FileLockManager paths so aliases share the same lock - Use async file operations for playlist persistence to avoid blocking async paths @@ -21,6 +22,7 @@ - Shared stream shutdown now drops registry locks before releasing provider handles to prevent cross-lock stalls. - Added `order: none` support for group/channel sorting so mappings can opt out of any reordering and keep the source order. - Session tracking now matches repeated HLS segment connections by session token so a single user keeps one active connection count even when new TCP sockets are opened. +- EPG icon urls are now rewritten on reverse proxy mode. # 3.2.0 (2025-11-14) - Added `name` attribute to Staged Input. diff --git a/Cargo.lock b/Cargo.lock index 99a8dabd1..b5808543a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1096,7 +1096,7 @@ dependencies = [ [[package]] name = "frontend" -version = "3.2.8" +version = "3.2.9" dependencies = [ "anyhow", "base64", @@ -3765,7 +3765,7 @@ dependencies = [ [[package]] name = "shared" -version = "3.2.8" +version = "3.2.9" dependencies = [ "base64", "bitflags 2.10.0", @@ -4314,7 +4314,7 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "tuliprox" -version = "3.2.8" +version = "3.2.9" dependencies = [ "arc-swap", "async-compression", diff --git a/README.md b/README.md index ddad9acd6..48b85f75c 100644 --- a/README.md +++ b/README.md @@ -357,6 +357,23 @@ Example: 1.0.4.0,1.0.7.255,AU ``` +#### 1.6.8 `rewrite_secret` +The `rewrite_secret` field is used to keep generated resource URLs stable across application restarts. +Some parts of the system generate URLs that include a hashed or signed component based on an internal secret value. +Normally, this secret would change after every restart, which would invalidate previously generated URLs. + +By explicitly setting a `rewrite_secret`, you ensure that the same value is reused on every startup. +This guarantees that resource URLs remain valid, even if the application restarts or updates. + +In short: +`rewrite_secret` provides a persistent secret used for generating and verifying rewrite URLs, preventing them from breaking after a restart. + +It must be a 32-character hexadecimal string (16 bytes), for example: +```yaml +reverse_proxy: + rewrite_secret: A1B2C3D4E5F60718293A4B5C6D7E8F90 +``` + ### 1.7 `backup_dir` is the directory where the backup configuration files written, when saved from the ui. diff --git a/backend/Cargo.toml b/backend/Cargo.toml index e99f5f7e0..28f84ce15 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tuliprox" -version = "3.2.8" +version = "3.2.9" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/backend/src/api/endpoints/xmltv_api.rs b/backend/src/api/endpoints/xmltv_api.rs index d24a45273..90c6c6726 100644 --- a/backend/src/api/endpoints/xmltv_api.rs +++ b/backend/src/api/endpoints/xmltv_api.rs @@ -154,6 +154,7 @@ async fn serve_epg( if let Ok(exists) = tokio::fs::try_exists(epg_path).await { if exists { let rewrite_resources = app_state.app_config.is_reverse_proxy_resource_rewrite_enabled(); + let encrypt_secret = app_state.app_config.get_reverse_proxy_rewrite_secret().unwrap_or_else(|| app_state.app_config.encrypt_secret); // If redirect is true → rewrite_urls = false → keep original // If redirect is false and rewrite_resources is true → rewrite_urls = true → rewriting allowed @@ -169,7 +170,7 @@ async fn serve_epg( let base_url = format!("{}/{}/{}/{}/", server_info.get_base_url(), storage_const::EPG_RESOURCE_PATH, &user.username, &user.password); // Apply timeshift and/or rewrite URLs - serve_epg_with_rewrites(epg_path, timeshift, rewrite_urls, &app_state.app_config.encrypt_secret, &base_url).await + serve_epg_with_rewrites(epg_path, timeshift, rewrite_urls, &encrypt_secret, &base_url).await } else { // Neither timeshift nor rewrite needed, serve original file serve_file(epg_path, mime::TEXT_XML).await.into_response() @@ -206,9 +207,28 @@ async fn serve_epg_with_rewrites( let reader = tokio::io::BufReader::new(file); let (tx, rx) = tokio::io::duplex(8192); tokio::spawn(async move { - let encoder = async_compression::tokio::write::GzipEncoder::new(tx); + let mut encoder = async_compression::tokio::write::GzipEncoder::new(tx); + + // Work-Around BytesText DocType escape, see below + if let Err(err) =encoder.write_all(b"\n").await { + error!("EPG: Failed to write xml header {err}"); + } + + if let Err(err) =encoder.write_all(b"\n").await { + error!("EPG: Failed to write epg doc type {err}"); + } + let mut xml_reader = quick_xml::reader::Reader::from_reader(tokio::io::BufReader::new(reader)); let mut xml_writer = quick_xml::writer::Writer::new(encoder); + + // TODO howto avoid BytesText to escape the doctype "xmltv.dtd" which is written as "e;xmltv.dtd"e; + // if let Err(err) =xml_writer.write_event_async(Event::Decl(quick_xml::events::BytesDecl::new("1.0", Some("utf-8"), None))).await { + // error!("EPG: Failed to write epg header {err}"); + // } + // if let Err(err) = xml_writer.write_event_async(Event::DocType( quick_xml::events::BytesText::new(r#"tv SYSTEM "xmltv.dtd""#))).await { + // error!("EPG: Failed to write epg doc type {err}"); + // } + let mut buf = Vec::with_capacity(4096); let duration = Duration::minutes(i64::from(offset_minutes)); @@ -300,6 +320,7 @@ async fn serve_epg_with_rewrites( } } } + Ok(Event::Decl(_) | Event::DocType(_)) => {}, Ok(Event::Eof) => break, // End of file Ok(event) => { // Write any other event as is @@ -388,7 +409,8 @@ async fn epg_api_resource( return axum::http::StatusCode::FORBIDDEN.into_response(); } - if let Ok(resource_url) = deobscure_text(&app_state.app_config.encrypt_secret, &resource) { + let encrypt_secret = app_state.app_config.get_reverse_proxy_rewrite_secret().unwrap_or_else(|| app_state.app_config.encrypt_secret); + if let Ok(resource_url) = deobscure_text(&encrypt_secret, &resource) { resource_response(&app_state, &resource_url, &req_headers, None).await.into_response() } else { axum::http::StatusCode::BAD_REQUEST.into_response() diff --git a/backend/src/model/config/app.rs b/backend/src/model/config/app.rs index 71b24e978..7ae2f2d64 100644 --- a/backend/src/model/config/app.rs +++ b/backend/src/model/config/app.rs @@ -144,6 +144,11 @@ impl AppConfig { config.reverse_proxy.as_ref().is_none_or(|r| !r.resource_rewrite_disabled) } + pub fn get_reverse_proxy_rewrite_secret(&self) -> Option<[u8; 16]> { + let config = > as Access>::load(&self.config); + config.reverse_proxy.as_ref().map(|r| r.rewrite_secret) + } + fn intern_get_target_for_user(&self, user_target: Option<(ProxyUserCredentials, String)>) -> Option<(ProxyUserCredentials, Arc)> { match user_target { Some((user, target_name)) => { diff --git a/backend/src/model/config/reverse_proxy.rs b/backend/src/model/config/reverse_proxy.rs index 49df2ee89..0ac7f86b2 100644 --- a/backend/src/model/config/reverse_proxy.rs +++ b/backend/src/model/config/reverse_proxy.rs @@ -1,7 +1,7 @@ use crate::model::config::cache::CacheConfig; use crate::model::{macros, GeoIpConfig, RateLimitConfig, StreamConfig}; use shared::model::{ResourceRetryConfigDto, ReverseProxyConfigDto, ReverseProxyDisabledHeaderConfigDto}; -use shared::utils::{default_resource_retry_attempts, default_resource_retry_backoff_ms, default_resource_retry_backoff_multiplier}; +use shared::utils::{default_resource_retry_attempts, default_resource_retry_backoff_ms, default_resource_retry_backoff_multiplier, hex_to_u8_16, u8_16_to_hex}; use std::cmp::max; #[derive(Debug, Clone)] @@ -95,6 +95,7 @@ impl From<&ResourceRetryConfig> for ResourceRetryConfigDto { #[derive(Debug, Clone)] pub struct ReverseProxyConfig { pub resource_rewrite_disabled: bool, + pub rewrite_secret: [u8; 16], pub resource_retry: ResourceRetryConfig, pub disabled_header: Option, pub stream: Option, @@ -109,6 +110,7 @@ impl From<&ReverseProxyConfigDto> for ReverseProxyConfig { fn from(dto: &ReverseProxyConfigDto) -> Self { Self { resource_rewrite_disabled: dto.resource_rewrite_disabled, + rewrite_secret: hex_to_u8_16(&dto.rewrite_secret).unwrap_or_default(), resource_retry: dto .resource_retry .as_ref() @@ -130,6 +132,7 @@ impl From<&ReverseProxyConfig> for ReverseProxyConfigDto { fn from(instance: &ReverseProxyConfig) -> Self { Self { resource_rewrite_disabled: instance.resource_rewrite_disabled, + rewrite_secret: u8_16_to_hex(&instance.rewrite_secret), resource_retry: Some(ResourceRetryConfigDto::from(&instance.resource_retry)), disabled_header: instance.disabled_header.as_ref().map(|d| ReverseProxyDisabledHeaderConfigDto { referer_header: d.referer_header, diff --git a/backend/src/repository/epg_repository.rs b/backend/src/repository/epg_repository.rs index c24bac705..9faad0e5e 100644 --- a/backend/src/repository/epg_repository.rs +++ b/backend/src/repository/epg_repository.rs @@ -18,7 +18,7 @@ pub async fn epg_write_file(target: &ConfigTarget, epg: &Epg, path: &Path) -> Re .await.map_err(|e| notify_err!(format!("failed to write XML header: {}", e)))?; // DOCTYPE - writer.write_event_async(quick_xml::events::Event::DocType(quick_xml::events::BytesText::new("tv SYSTEM \"xmltv.dtd\""))) + writer.write_event_async(quick_xml::events::Event::DocType(quick_xml::events::BytesText::new(r#"tv SYSTEM "xmltv.dtd""#))) .await.map_err(|e| notify_err!(format!("failed to write doctype: {}", e)))?; // EPG Content diff --git a/frontend/Cargo.toml b/frontend/Cargo.toml index bc7a924b7..cd8b0acc3 100644 --- a/frontend/Cargo.toml +++ b/frontend/Cargo.toml @@ -1,10 +1,10 @@ [package] name = "frontend" -version = "3.2.8" +version = "3.2.9" edition = "2021" [dependencies] -shared = { version = "3.2.8", path = "../shared" } +shared = { version = "3.2.9", path = "../shared" } chrono = "0" yew = "0.21" yew-router = "0.18" diff --git a/frontend/public/assets/i18n/en.json b/frontend/public/assets/i18n/en.json index 91a5f3c33..1c32f8fd6 100644 --- a/frontend/public/assets/i18n/en.json +++ b/frontend/public/assets/i18n/en.json @@ -224,6 +224,7 @@ "WEB_SEARCH": "Web Search", "USER_UI_ENABLED": "User-UI Enabled", "RESOURCE_REWRITE_DISABLED": "Resource Rewrite Disabled", + "REWRITE_SECRET": "Rewrite Secret", "DEVICE_AUTH": "Auth", "TOKEN_TTL_MINS": "Token ttl (mins)", "AUTH": "Auth", diff --git a/frontend/src/app/components/config/macros.rs b/frontend/src/app/components/config/macros.rs index 98dc7a65f..d9acb1589 100644 --- a/frontend/src/app/components/config/macros.rs +++ b/frontend/src/app/components/config/macros.rs @@ -93,6 +93,20 @@ macro_rules! config_field_custom { }; } +#[macro_export] +macro_rules! config_field_custom_hide { + ($label:expr, $value:expr) => { + html! { +
+ + + <$crate::app::components::HideContent content={$value}> + +
+ } + }; +} + #[macro_export] macro_rules! config_field_child { ($label:expr, $body:block) => { @@ -181,6 +195,33 @@ macro_rules! edit_field_text { }}; } +#[macro_export] +macro_rules! edit_field_text_custom { + ($instance:expr, $label:expr, $field:ident, $action:path, $read_value:expr, $write_value:expr) => { + $crate::edit_field_text!(@inner $instance, $label, $field, $action, false, $read_value, $write_value) + }; + ($instance:expr, $label:expr, $field:ident, $action:path, $hidden:expr, $read_value:expr, $write_value:expr) => { + $crate::edit_field_text!(@inner $instance, $label, $field, $action, $hidden, $read_value, $write_value) + }; + (@inner $instance:expr, $label:expr, $field:ident, $action:path, $hidden:expr) => {{ + let instance = $instance.clone(); + html! { +
+ <$crate::app::components::input::Input + label={$label} + hidden={$hidden} + name={stringify!($field)} + autocomplete={true} + value={$read_value(instance.form.$field.clone())} + on_change={Callback::from(move |value: String| { + instance.dispatch($write_value($action(value))); + })} + /> +
+ } + }}; +} + #[macro_export] macro_rules! edit_field_bool { ($instance:expr, $label:expr, $field:ident, $action:path) => {{ diff --git a/frontend/src/app/components/config/reverse_proxy_config_view.rs b/frontend/src/app/components/config/reverse_proxy_config_view.rs index a071e331a..70d31bd3c 100644 --- a/frontend/src/app/components/config/reverse_proxy_config_view.rs +++ b/frontend/src/app/components/config/reverse_proxy_config_view.rs @@ -3,12 +3,14 @@ use yew::prelude::*; use yew_i18n::use_translation; use shared::model::{CacheConfigDto, GeoIpConfigDto, RateLimitConfigDto, ResourceRetryConfigDto, ReverseProxyConfigDto, ReverseProxyDisabledHeaderConfigDto, StreamConfigDto}; -use shared::utils::format_float_localized; +use shared::utils::{default_secret, format_float_localized}; use crate::app::context::ConfigContext; use crate::app::components::config::config_view_context::ConfigViewContext; use crate::app::components::config::config_page::{ConfigForm, LABEL_REVERSE_PROXY_CONFIG}; use crate::app::components::{Card}; -use crate::{config_field, config_field_bool, config_field_custom, config_field_optional, edit_field_bool, edit_field_list, edit_field_number, edit_field_number_f64, edit_field_number_u64, edit_field_text, edit_field_text_option, generate_form_reducer}; +use crate::{config_field, config_field_bool, config_field_custom, config_field_hide, config_field_optional, + edit_field_bool, edit_field_list, edit_field_number, edit_field_number_f64, + edit_field_number_u64, edit_field_text, edit_field_text_option, generate_form_reducer}; const LABEL_CACHE: &str = "LABEL.CACHE"; const LABEL_ENABLED: &str = "LABEL.ENABLED"; @@ -30,6 +32,7 @@ const LABEL_SHARED_BURST_BUFFER_MB: &str = "LABEL.SHARED_BURST_BUFFER_BYTES"; const LABEL_SETTINGS: &str = "LABEL.SETTINGS"; const LABEL_RESOURCE_REWRITE_DISABLED: &str = "LABEL.RESOURCE_REWRITE_DISABLED"; +const LABEL_REWRITE_SECRET: &str = "LABEL.REWRITE_SECRET"; const LABEL_RESOURCE_RETRY: &str = "LABEL.RESOURCE_RETRY"; const LABEL_MAX_ATTEMPTS: &str = "LABEL.MAX_ATTEMPTS"; const LABEL_BACKOFF_MILLIS: &str = "LABEL.BACKOFF_MILLIS"; @@ -100,6 +103,7 @@ generate_form_reducer!( action_name: ReverseProxyConfigFormAction, fields { ResourceRewriteDisabled => resource_rewrite_disabled: bool, + RewriteSecret => rewrite_secret: String, } ); @@ -120,7 +124,7 @@ pub fn ReverseProxyConfigView() -> Html { let config_view_ctx = use_context::().expect("ConfigViewContext not found"); let reverse_proxy_state: UseReducerHandle = use_reducer(|| { - ReverseProxyConfigFormState { form: ReverseProxyConfigDto::default(), modified: false } + ReverseProxyConfigFormState { form: ReverseProxyConfigDto { rewrite_secret: default_secret(), ..Default::default() }, modified: false } }); let disabled_header_state: UseReducerHandle = use_reducer(|| { ReverseProxyDisabledHeaderConfigFormState { form: ReverseProxyDisabledHeaderConfigDto::default(), modified: false } @@ -270,6 +274,7 @@ pub fn ReverseProxyConfigView() -> Html {

{translate.t(LABEL_SETTINGS)}

{ config_field_bool!(reverse_proxy_state.form, translate.t(LABEL_RESOURCE_REWRITE_DISABLED), resource_rewrite_disabled) } + { config_field_hide!(reverse_proxy_state.form, translate.t(LABEL_REWRITE_SECRET), rewrite_secret) }
} }; @@ -279,6 +284,7 @@ pub fn ReverseProxyConfigView() -> Html {

{translate.t(LABEL_SETTINGS)}

{ edit_field_bool!(reverse_proxy_state, translate.t(LABEL_RESOURCE_REWRITE_DISABLED), resource_rewrite_disabled, ReverseProxyConfigFormAction::ResourceRewriteDisabled) } + { edit_field_text!(reverse_proxy_state, translate.t(LABEL_REWRITE_SECRET), rewrite_secret, ReverseProxyConfigFormAction::RewriteSecret, true) }
} }; @@ -337,16 +343,56 @@ pub fn ReverseProxyConfigView() -> Html { } }; + let render_geoip_edit = || html ! { + +

{translate.t(LABEL_GEOIP)}

+ { edit_field_bool!(geoip_state, translate.t(LABEL_ENABLED), enabled, GeoIpConfigFormAction::Enabled) } + { edit_field_text!(geoip_state, translate.t(LABEL_URL), url, GeoIpConfigFormAction::Url) } +
+ }; + + let render_cache_edit = || html! { + +

{translate.t(LABEL_CACHE)}

+ { edit_field_bool!(cache_state, translate.t(LABEL_ENABLED), enabled, CacheConfigFormAction::Enabled) } + { edit_field_text_option!(cache_state, translate.t(LABEL_SIZE), size, CacheConfigFormAction::Size) } + { edit_field_text_option!(cache_state, translate.t(LABEL_DIRECTORY), dir, CacheConfigFormAction::Dir) } +
+ }; + + let render_rate_limit_edit = || html! { + +

{translate.t(LABEL_RATE_LIMIT)}

+ { edit_field_bool!(rate_limit_state, translate.t(LABEL_ENABLED), enabled, RateLimitConfigFormAction::Enabled) } + { edit_field_number_u64!(rate_limit_state, translate.t(LABEL_PERIOD_MILLIS), period_millis, RateLimitConfigFormAction::PeriodMillis) } + { edit_field_number!(rate_limit_state, translate.t(LABEL_BURST_SIZE), burst_size, RateLimitConfigFormAction::BurstSize) } +
+ }; + + let render_stream_edit = || html! { + +

{translate.t(LABEL_STREAM)}

+ { edit_field_bool!(stream_state, translate.t(LABEL_RETRY), retry, StreamConfigFormAction::Retry) } + { edit_field_text_option!(stream_state, translate.t(LABEL_THROTTLE), throttle, StreamConfigFormAction::Throttle) } + { edit_field_number_u64!(stream_state, translate.t(LABEL_GRACE_PERIOD_MILLIS), grace_period_millis, StreamConfigFormAction::GracePeriodMillis) } + { edit_field_number_u64!(stream_state, translate.t(LABEL_GRACE_PERIOD_TIMEOUT_SECS), grace_period_timeout_secs, StreamConfigFormAction::GracePeriodTimeoutSecs) } + //{ edit_field_number!(stream_state, translate.t(LABEL_FORCED_RETRY_INTERVAL_SECS), forced_retry_interval_secs, StreamConfigFormAction::ForcedRetryIntervalSecs) } + { edit_field_number_u64!(stream_state, translate.t(LABEL_THROTTLE_KBPS), throttle_kbps, StreamConfigFormAction::ThrottleKbps) } + { edit_field_number_u64!(stream_state, translate.t(LABEL_SHARED_BURST_BUFFER_MB), shared_burst_buffer_mb, StreamConfigFormAction::SharedBurstBufferMb) } +
+ }; + + let render_view_mode = || { html! {
{ render_settings_view() } - { render_resource_retry_view() } { render_disabled_header_view() } + { render_geoip() } { render_cache() } + { render_resource_retry_view() } { render_rate_limit() } { render_stream() } - { render_geoip() }
} }; @@ -354,35 +400,12 @@ pub fn ReverseProxyConfigView() -> Html { let render_edit_mode = || html! {
{ render_settings_edit() } - { render_resource_retry_edit() } { render_disabled_header_edit() } - -

{translate.t(LABEL_CACHE)}

- { edit_field_bool!(cache_state, translate.t(LABEL_ENABLED), enabled, CacheConfigFormAction::Enabled) } - { edit_field_text_option!(cache_state, translate.t(LABEL_SIZE), size, CacheConfigFormAction::Size) } - { edit_field_text_option!(cache_state, translate.t(LABEL_DIRECTORY), dir, CacheConfigFormAction::Dir) } -
- -

{translate.t(LABEL_RATE_LIMIT)}

- { edit_field_bool!(rate_limit_state, translate.t(LABEL_ENABLED), enabled, RateLimitConfigFormAction::Enabled) } - { edit_field_number_u64!(rate_limit_state, translate.t(LABEL_PERIOD_MILLIS), period_millis, RateLimitConfigFormAction::PeriodMillis) } - { edit_field_number!(rate_limit_state, translate.t(LABEL_BURST_SIZE), burst_size, RateLimitConfigFormAction::BurstSize) } -
- -

{translate.t(LABEL_STREAM)}

- { edit_field_bool!(stream_state, translate.t(LABEL_RETRY), retry, StreamConfigFormAction::Retry) } - { edit_field_text_option!(stream_state, translate.t(LABEL_THROTTLE), throttle, StreamConfigFormAction::Throttle) } - { edit_field_number_u64!(stream_state, translate.t(LABEL_GRACE_PERIOD_MILLIS), grace_period_millis, StreamConfigFormAction::GracePeriodMillis) } - { edit_field_number_u64!(stream_state, translate.t(LABEL_GRACE_PERIOD_TIMEOUT_SECS), grace_period_timeout_secs, StreamConfigFormAction::GracePeriodTimeoutSecs) } - //{ edit_field_number!(stream_state, translate.t(LABEL_FORCED_RETRY_INTERVAL_SECS), forced_retry_interval_secs, StreamConfigFormAction::ForcedRetryIntervalSecs) } - { edit_field_number_u64!(stream_state, translate.t(LABEL_THROTTLE_KBPS), throttle_kbps, StreamConfigFormAction::ThrottleKbps) } - { edit_field_number_u64!(stream_state, translate.t(LABEL_SHARED_BURST_BUFFER_MB), shared_burst_buffer_mb, StreamConfigFormAction::SharedBurstBufferMb) } -
- -

{translate.t(LABEL_GEOIP)}

- { edit_field_bool!(geoip_state, translate.t(LABEL_ENABLED), enabled, GeoIpConfigFormAction::Enabled) } - { edit_field_text!(geoip_state, translate.t(LABEL_URL), url, GeoIpConfigFormAction::Url) } -
+ { render_geoip_edit() } + { render_cache_edit() } + { render_resource_retry_edit() } + { render_rate_limit_edit() } + { render_stream_edit() }
}; diff --git a/shared/Cargo.toml b/shared/Cargo.toml index 238ab14f6..13c83d580 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "shared" -version = "3.2.8" +version = "3.2.9" edition = "2021" [dependencies] diff --git a/shared/src/model/config/reverse_proxy.rs b/shared/src/model/config/reverse_proxy.rs index 94873e223..979bb7e45 100644 --- a/shared/src/model/config/reverse_proxy.rs +++ b/shared/src/model/config/reverse_proxy.rs @@ -1,6 +1,6 @@ -use crate::error::TuliproxError; +use crate::error::{TuliproxError, TuliproxErrorKind}; use crate::model::{CacheConfigDto, GeoIpConfigDto, RateLimitConfigDto, StreamConfigDto}; -use crate::utils::{default_resource_retry_attempts, default_resource_retry_backoff_ms, default_resource_retry_backoff_multiplier}; +use crate::utils::{default_resource_retry_attempts, default_resource_retry_backoff_ms, default_resource_retry_backoff_multiplier, hex_to_u8_16}; use log::warn; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default, PartialEq)] @@ -31,6 +31,7 @@ impl ReverseProxyDisabledHeaderConfigDto { pub struct ReverseProxyConfigDto { #[serde(default)] pub resource_rewrite_disabled: bool, + pub rewrite_secret: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub resource_retry: Option, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -88,6 +89,9 @@ impl ReverseProxyConfigDto { } pub(crate) fn prepare(&mut self, working_dir: &str) -> Result<(), TuliproxError> { + + hex_to_u8_16(&self.rewrite_secret).map_err(|e| TuliproxError::new(TuliproxErrorKind::Info, e))?; + if let Some(stream) = self.stream.as_mut() { stream.prepare()?; } diff --git a/shared/src/utils/default_utils.rs b/shared/src/utils/default_utils.rs index 54e641016..7be9d57d0 100644 --- a/shared/src/utils/default_utils.rs +++ b/shared/src/utils/default_utils.rs @@ -14,3 +14,11 @@ pub const fn default_connect_timeout_secs() -> u32 { 6 } pub const fn default_resource_retry_attempts() -> u32 { 3 } pub const fn default_resource_retry_backoff_ms() -> u64 { 250 } pub const fn default_resource_retry_backoff_multiplier() -> f64 { 1.0 } +pub fn default_secret() -> String { + let mut out = [0u8; 16]; + for x in &mut out { + *x = fastrand::u8(..); + } + + out.iter().map(|b| format!("{:02X}", b)).collect() +} \ No newline at end of file diff --git a/shared/src/utils/serde_utils.rs b/shared/src/utils/serde_utils.rs index 663e19d43..50fe265d9 100644 --- a/shared/src/utils/serde_utils.rs +++ b/shared/src/utils/serde_utils.rs @@ -111,3 +111,38 @@ where { ciborium::de::from_reader(value).map_err(to_io_error) } + + +pub fn u8_16_to_hex(bytes: &[u8; 16]) -> String { + bytes.iter().map(|b| format!("{:02X}", b)).collect() +} + +pub fn hex_to_u8_16(hex: &str) -> Result<[u8; 16], String> { + if hex.len() != 32 { + return Err("Hex string must be exactly 32 characters".into()); + } + + let mut out = [0u8; 16]; + + for (i, chunk) in hex.as_bytes().chunks(2).enumerate() { + let s = std::str::from_utf8(chunk).map_err(|_| "Invalid UTF-8")?; + out[i] = u8::from_str_radix(s, 16).map_err(|_| "Invalid hex")?; + } + + Ok(out) +} + +pub fn hex_to_secret<'de, D>(deserializer: D) -> Result<[u8; 16], D::Error> +where + D: serde::Deserializer<'de>, +{ + let s: String = Deserialize::deserialize(deserializer)?; + hex_to_u8_16(&s).map_err(serde::de::Error::custom) +} + +pub fn secret_to_hex(bytes: &[u8; 16], serializer: S) -> Result +where + S: serde::Serializer, +{ + serializer.serialize_str(&u8_16_to_hex(bytes)) +} \ No newline at end of file