Resource rewrite secret key to ensure rewritten url validity

Fixed epg rewrite
This commit is contained in:
euzu
2025-11-21 17:09:14 +01:00
parent 799c745d60
commit ddcd56b1ea
16 changed files with 208 additions and 47 deletions
+2
View File
@@ -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.
Generated
+3 -3
View File
@@ -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",
+17
View File
@@ -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.
+1 -1
View File
@@ -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
+25 -3
View File
@@ -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"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n").await {
error!("EPG: Failed to write xml header {err}");
}
if let Err(err) =encoder.write_all(b"<!DOCTYPE tv SYSTEM \"xmltv.dtd\">\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 &quote;xmltv.dtd&quote;
// 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()
+5
View File
@@ -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 = <Arc<ArcSwap<Config>> as Access<Config>>::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<ConfigTarget>)> {
match user_target {
Some((user, target_name)) => {
+4 -1
View File
@@ -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<ReverseProxyDisabledHeaderConfig>,
pub stream: Option<StreamConfig>,
@@ -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,
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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"
+1
View File
@@ -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",
@@ -93,6 +93,20 @@ macro_rules! config_field_custom {
};
}
#[macro_export]
macro_rules! config_field_custom_hide {
($label:expr, $value:expr) => {
html! {
<div class="tp__form-field tp__form-field__text">
<label>{$label}</label>
<span class="tp__form-field__value">
<$crate::app::components::HideContent content={$value}></$crate::app::components::HideContent>
</span>
</div>
}
};
}
#[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! {
<div class="tp__form-field tp__form-field__text">
<$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)));
})}
/>
</div>
}
}};
}
#[macro_export]
macro_rules! edit_field_bool {
($instance:expr, $label:expr, $field:ident, $action:path) => {{
@@ -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::<ConfigViewContext>().expect("ConfigViewContext not found");
let reverse_proxy_state: UseReducerHandle<ReverseProxyConfigFormState> = use_reducer(|| {
ReverseProxyConfigFormState { form: ReverseProxyConfigDto::default(), modified: false }
ReverseProxyConfigFormState { form: ReverseProxyConfigDto { rewrite_secret: default_secret(), ..Default::default() }, modified: false }
});
let disabled_header_state: UseReducerHandle<ReverseProxyDisabledHeaderConfigFormState> = use_reducer(|| {
ReverseProxyDisabledHeaderConfigFormState { form: ReverseProxyDisabledHeaderConfigDto::default(), modified: false }
@@ -270,6 +274,7 @@ pub fn ReverseProxyConfigView() -> Html {
<Card class="tp__config-view__card">
<h1>{translate.t(LABEL_SETTINGS)}</h1>
{ 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) }
</Card>
}
};
@@ -279,6 +284,7 @@ pub fn ReverseProxyConfigView() -> Html {
<Card class="tp__config-view__card">
<h1>{translate.t(LABEL_SETTINGS)}</h1>
{ 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) }
</Card>
}
};
@@ -337,16 +343,56 @@ pub fn ReverseProxyConfigView() -> Html {
}
};
let render_geoip_edit = || html ! {
<Card class="tp__config-view__card">
<h1>{translate.t(LABEL_GEOIP)}</h1>
{ edit_field_bool!(geoip_state, translate.t(LABEL_ENABLED), enabled, GeoIpConfigFormAction::Enabled) }
{ edit_field_text!(geoip_state, translate.t(LABEL_URL), url, GeoIpConfigFormAction::Url) }
</Card>
};
let render_cache_edit = || html! {
<Card class="tp__config-view__card">
<h1>{translate.t(LABEL_CACHE)}</h1>
{ 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) }
</Card>
};
let render_rate_limit_edit = || html! {
<Card class="tp__config-view__card">
<h1>{translate.t(LABEL_RATE_LIMIT)}</h1>
{ 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) }
</Card>
};
let render_stream_edit = || html! {
<Card class="tp__config-view__card">
<h1>{translate.t(LABEL_STREAM)}</h1>
{ 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) }
</Card>
};
let render_view_mode = || {
html! {
<div class="tp__reverse-proxy-config-view__body tp__config-view-page__body">
{ 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() }
</div>
}
};
@@ -354,35 +400,12 @@ pub fn ReverseProxyConfigView() -> Html {
let render_edit_mode = || html! {
<div class="tp__reverse-proxy-config-view__body tp__config-view-page__body">
{ render_settings_edit() }
{ render_resource_retry_edit() }
{ render_disabled_header_edit() }
<Card class="tp__config-view__card">
<h1>{translate.t(LABEL_CACHE)}</h1>
{ 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) }
</Card>
<Card class="tp__config-view__card">
<h1>{translate.t(LABEL_RATE_LIMIT)}</h1>
{ 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) }
</Card>
<Card class="tp__config-view__card">
<h1>{translate.t(LABEL_STREAM)}</h1>
{ 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) }
</Card>
<Card class="tp__config-view__card">
<h1>{translate.t(LABEL_GEOIP)}</h1>
{ edit_field_bool!(geoip_state, translate.t(LABEL_ENABLED), enabled, GeoIpConfigFormAction::Enabled) }
{ edit_field_text!(geoip_state, translate.t(LABEL_URL), url, GeoIpConfigFormAction::Url) }
</Card>
{ render_geoip_edit() }
{ render_cache_edit() }
{ render_resource_retry_edit() }
{ render_rate_limit_edit() }
{ render_stream_edit() }
</div>
};
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "shared"
version = "3.2.8"
version = "3.2.9"
edition = "2021"
[dependencies]
+6 -2
View File
@@ -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<ResourceRetryConfigDto>,
#[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()?;
}
+8
View File
@@ -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()
}
+35
View File
@@ -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<S>(bytes: &[u8; 16], serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&u8_16_to_hex(bytes))
}