mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-20 23:02:34 +02:00
Resource rewrite secret key to ensure rewritten url validity
Fixed epg rewrite
This commit is contained in:
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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 "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()
|
||||
|
||||
@@ -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)) => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "shared"
|
||||
version = "3.2.8"
|
||||
version = "3.2.9"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -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()?;
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
Reference in New Issue
Block a user