mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-18 05:42:16 +02:00
Added kick duration for web_ui kick
This commit is contained in:
@@ -35,6 +35,8 @@
|
||||
- Added extended debug logging for client requests and ID chain (request/action/virtual) to trace stream resolution.
|
||||
- Fixed xtream series/catchup lookups using the series-info virtual_id so episode requests now keep their own virtual_id/session.
|
||||
- Made cache storage more robust. Incomplete downloads will be deleted from cache.
|
||||
- `kick_secs` added to config.yaml `web_ui` config. Default 30 seconds, if a user is kicked from the webui, he can't connect for this duration.
|
||||
|
||||
|
||||
# 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.18"
|
||||
version = "3.2.19"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
@@ -3793,7 +3793,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "shared"
|
||||
version = "3.2.18"
|
||||
version = "3.2.19"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"bitflags 2.10.0",
|
||||
@@ -4356,7 +4356,7 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||
|
||||
[[package]]
|
||||
name = "tuliprox"
|
||||
version = "3.2.18"
|
||||
version = "3.2.19"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"async-compression",
|
||||
|
||||
@@ -412,11 +412,13 @@ log:
|
||||
```
|
||||
|
||||
### 1.10 `web_ui`
|
||||
- enabled: default is true, if set to false the web_ui is disabled
|
||||
- user_ui_enabled, true or false, for user bouquet editor
|
||||
- content_security_policy: configure Content-Security-Policy headers. When `enabled` is true, the default directives `default-src 'self'`, `script-src 'self' 'wasm-unsafe-eval' 'nonce-{nonce_b64}'`, and `frame-ancestors 'none'` are applied. Additional directives can be added via `custom-attributes`. Enabling CSP may block external images/logos unless allowed via directives like `img-src`.
|
||||
- path is for web_ui path like `/ui` for reverse proxy integration if necessary.
|
||||
- auth for authentication settings
|
||||
- `enabled`: default is true, if set to false the web_ui is disabled
|
||||
- `user_ui_enabled`, true or false, for user group editor
|
||||
- `content_security_policy`: configure Content-Security-Policy headers. When `enabled` is true, the default directives `default-src 'self'`, `script-src 'self' 'wasm-unsafe-eval' 'nonce-{nonce_b64}'`, and `frame-ancestors 'none'` are applied. Additional directives can be added via `custom-attributes`. Enabling CSP may block external images/logos unless allowed via directives like `img-src`.
|
||||
- `path` is for web_ui path like `/ui` for reverse proxy integration if necessary.
|
||||
- `player_server` optional, if set the server setting is used for the web-ui-player.
|
||||
- `kick_secs` default 30 seconds, if a user is kicked from the webui, he can't connect for this duration.
|
||||
- `auth` for authentication settings
|
||||
- `enabled` can be deactivated if `enabled` is set to `false`. If not set default is `true`.
|
||||
- `issuer`
|
||||
- `secret` is used for jwt token generation.
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tuliprox"
|
||||
version = "3.2.18"
|
||||
version = "3.2.19"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -61,6 +61,10 @@ pub(in crate::api) async fn handle_hls_stream_request(
|
||||
req_headers: &HeaderMap,
|
||||
connection_permission: UserConnectionPermission,
|
||||
) -> impl IntoResponse + Send {
|
||||
if app_state.active_users.is_user_blocked_for_stream(&user.username, virtual_id).await {
|
||||
return axum::http::StatusCode::BAD_REQUEST.into_response();
|
||||
}
|
||||
|
||||
let url = replace_url_extension(hls_url, HLS_EXT);
|
||||
let server_info = app_state.app_config.get_user_server_info(user);
|
||||
|
||||
|
||||
@@ -118,6 +118,11 @@ async fn m3u_api_stream(
|
||||
format!("Failed to read m3u item for stream id {req_virtual_id}")
|
||||
);
|
||||
let virtual_id = pli.virtual_id;
|
||||
|
||||
if app_state.active_users.is_user_blocked_for_stream(stream_req.username, virtual_id).await {
|
||||
return axum::http::StatusCode::BAD_REQUEST.into_response();
|
||||
}
|
||||
|
||||
let input = try_option_bad_request!(
|
||||
app_state
|
||||
.app_config
|
||||
|
||||
@@ -136,7 +136,7 @@ async fn handle_protocol_message(
|
||||
Ok(ProtocolMessage::UserAction(cmd)) => {
|
||||
if let Some(token) = mem.token.as_ref() {
|
||||
if !auth_required || verify_auth_admin_token(token, secret_key) {
|
||||
Some(ProtocolMessage::UserActionResponse(handle_user_action(app_state, cmd)))
|
||||
Some(ProtocolMessage::UserActionResponse(handle_user_action(app_state, cmd).await))
|
||||
} else {
|
||||
Some(ProtocolMessage::UserActionResponse(false))
|
||||
}
|
||||
@@ -314,8 +314,8 @@ async fn handle_socket(mut socket: WebSocket, app_state: Arc<AppState>, auth_req
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_user_action(app_state: &Arc<AppState>, cmd: UserCommand) -> bool {
|
||||
async fn handle_user_action(app_state: &Arc<AppState>, cmd: UserCommand) -> bool {
|
||||
match cmd {
|
||||
UserCommand::Kick(addr) => app_state.connection_manager.kick_connection(&addr),
|
||||
UserCommand::Kick(addr, virtual_id, secs) => app_state.connection_manager.kick_connection(&addr, virtual_id, secs).await,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,7 +229,6 @@ async fn xtream_player_api_stream(
|
||||
// debug!("{}", sanitize_sensitive_info(&message));
|
||||
// }
|
||||
|
||||
|
||||
let (user, target) = try_option_bad_request!(
|
||||
get_user_target_by_credentials( stream_req.username, stream_req.password, api_req, app_state),
|
||||
false,
|
||||
@@ -256,6 +255,10 @@ async fn xtream_player_api_stream(
|
||||
format!("Failed to read xtream item for stream id {req_virtual_id}")
|
||||
);
|
||||
let virtual_id = pli.virtual_id;
|
||||
if app_state.active_users.is_user_blocked_for_stream(stream_req.username, virtual_id).await {
|
||||
return axum::http::StatusCode::BAD_REQUEST.into_response();
|
||||
}
|
||||
|
||||
let input = try_option_bad_request!(
|
||||
app_state.app_config.get_input_by_name(pli.input_name.as_str()),
|
||||
true,
|
||||
@@ -267,9 +270,10 @@ async fn xtream_player_api_stream(
|
||||
} else {
|
||||
(pli.xtream_cluster, pli.item_type)
|
||||
};
|
||||
|
||||
|
||||
debug_if_enabled!(
|
||||
"ID chain for xtream endpoint: request_stream_id={} -> action_stream_id={action_stream_id} -> req_virtual_id={req_virtual_id} -> virtual_id={virtual_id}");
|
||||
"ID chain for xtream endpoint: request_stream_id={} -> action_stream_id={action_stream_id} -> req_virtual_id={req_virtual_id} -> virtual_id={virtual_id}",
|
||||
stream_req.stream_id);
|
||||
let session_key = create_session_fingerprint(&fingerprint.key, &user.username, virtual_id);
|
||||
let user_session = app_state
|
||||
.active_users
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::utils::GeoIp;
|
||||
use arc_swap::ArcSwapOption;
|
||||
use jsonwebtoken::get_current_timestamp;
|
||||
use log::{debug, info};
|
||||
use shared::model::{ActiveUserConnectionChange, StreamChannel, StreamInfo, UserConnectionPermission};
|
||||
use shared::model::{ActiveUserConnectionChange, StreamChannel, StreamInfo, UserConnectionPermission, VirtualId};
|
||||
use shared::utils::{current_time_secs, default_grace_period_millis, default_grace_period_timeout_secs, sanitize_sensitive_info, strip_port};
|
||||
use std::borrow::Cow;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
@@ -75,6 +75,7 @@ impl UserConnectionData {
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct UserConnections {
|
||||
kicked: HashMap<String, (u64, VirtualId)>,
|
||||
by_key: HashMap<String, UserConnectionData>,
|
||||
key_by_addr: HashMap<SocketAddr, String>,
|
||||
}
|
||||
@@ -124,7 +125,6 @@ impl ActiveUserManager {
|
||||
self.last_logged_user_count.store(last_user_count, Ordering::Relaxed);
|
||||
self.last_logged_user_connection_count.store(last_connection_count, Ordering::Relaxed);
|
||||
info!("Active Users: {user_count}, Active User Connections: {user_connection_count}");
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -496,6 +496,22 @@ impl ActiveUserManager {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn is_user_blocked_for_stream(&self, username: &str, virtual_id: VirtualId) -> bool {
|
||||
let connections = self.connections.read().await;
|
||||
let now = current_time_secs();
|
||||
matches!(connections.kicked.get(username), Some((expires_at, vid)) if *vid == virtual_id && *expires_at > now)
|
||||
}
|
||||
|
||||
pub async fn block_user_for_stream(&self, addr: &SocketAddr, virtual_id: VirtualId, blocked_secs: u64) {
|
||||
let mut connections = self.connections.write().await;
|
||||
let now = current_time_secs();
|
||||
connections.kicked.retain(|_, (expires_at, _)| *expires_at > now);
|
||||
if let Some(username) = connections.key_by_addr.get(addr).cloned() {
|
||||
let expires_at = now + blocked_secs;
|
||||
connections.kicked.insert(username, (expires_at, virtual_id));
|
||||
}
|
||||
}
|
||||
|
||||
fn gc(&self) {
|
||||
if let Some(gc_ts) = &self.gc_ts {
|
||||
let ts = gc_ts.load(Ordering::Acquire);
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::net::SocketAddr;
|
||||
use crate::api::model::{ActiveProviderManager, ActiveUserManager, CustomVideoStreamType, EventManager, EventMessage, ProviderHandle, SharedStreamManager};
|
||||
use std::sync::Arc;
|
||||
use log::{debug, warn};
|
||||
use shared::model::{ActiveUserConnectionChange, StreamChannel};
|
||||
use shared::model::{ActiveUserConnectionChange, StreamChannel, VirtualId};
|
||||
use crate::auth::Fingerprint;
|
||||
|
||||
pub struct ConnectionManager {
|
||||
@@ -36,7 +36,10 @@ impl ConnectionManager {
|
||||
self.close_socket_signal_tx.subscribe()
|
||||
}
|
||||
|
||||
pub fn kick_connection(&self, addr: &SocketAddr) -> bool {
|
||||
pub async fn kick_connection(&self, addr: &SocketAddr, virtual_id: VirtualId, block_secs: u64) -> bool {
|
||||
if block_secs > 0 {
|
||||
self.user_manager.block_user_for_stream(addr, virtual_id, block_secs).await;
|
||||
}
|
||||
if let Err(e) = self.close_socket_signal_tx.send(*addr) {
|
||||
debug!("No active receivers for close signal ({addr}): {e:?}");
|
||||
return false;
|
||||
@@ -74,7 +77,7 @@ impl ConnectionManager {
|
||||
self.event_manager.send_event(EventMessage::ActiveUser(ActiveUserConnectionChange::Updated(stream_info)));
|
||||
} else {
|
||||
warn!("Failed to register connection for user {username} at {}; disconnecting client", fingerprint.addr);
|
||||
let _ = self.kick_connection(&fingerprint.addr);
|
||||
let _ = self.kick_connection(&fingerprint.addr,0, 0).await;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use shared::error::TuliproxError;
|
||||
use shared::model::{ContentSecurityPolicyConfigDto, WebUiConfigDto};
|
||||
use shared::utils::default_kick_secs;
|
||||
use crate::model::{macros, WebAuthConfig};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -16,6 +17,7 @@ pub struct WebUiConfig {
|
||||
pub path: Option<String>,
|
||||
pub auth: Option<WebAuthConfig>,
|
||||
pub player_server: Option<String>,
|
||||
pub kick_secs: u64,
|
||||
}
|
||||
|
||||
impl WebUiConfig {
|
||||
@@ -27,6 +29,9 @@ impl WebUiConfig {
|
||||
self.auth = None;
|
||||
}
|
||||
}
|
||||
if self.kick_secs == 0 {
|
||||
self.kick_secs = default_kick_secs();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -61,6 +66,7 @@ impl From<&WebUiConfigDto> for WebUiConfig {
|
||||
path: dto.path.clone(),
|
||||
auth: dto.auth.as_ref().map(Into::into),
|
||||
player_server: dto.player_server.clone(),
|
||||
kick_secs: dto.kick_secs,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,6 +79,7 @@ impl From<&WebUiConfig> for WebUiConfigDto {
|
||||
path: instance.path.clone(),
|
||||
auth: instance.auth.as_ref().map(Into::into),
|
||||
player_server: instance.player_server.clone(),
|
||||
kick_secs: instance.kick_secs,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,10 +1,10 @@
|
||||
[package]
|
||||
name = "frontend"
|
||||
version = "3.2.18"
|
||||
version = "3.2.19"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
shared = { version = "3.2.18", path = "../shared" }
|
||||
shared = { version = "3.2.19", path = "../shared" }
|
||||
chrono = "0"
|
||||
yew = "0.21"
|
||||
yew-router = "0.18"
|
||||
|
||||
@@ -127,6 +127,7 @@
|
||||
"SECRET": "Secret",
|
||||
"USERFILE": "Userfile",
|
||||
"PLAYER_SERVER": "Player Server",
|
||||
"KICK_DURATION": "Kick duration (secs)",
|
||||
"VERSION": "Version",
|
||||
"SERVER_TIME": "Server time",
|
||||
"BUILD_TIME": "Build time",
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::app::components::{AppIcon, Card, Chip};
|
||||
use crate::app::context::ConfigContext;
|
||||
use crate::app::components::config::config_view_context::ConfigViewContext;
|
||||
use crate::app::components::config::config_page::{ConfigForm, LABEL_WEB_UI_CONFIG};
|
||||
use crate::{config_field, config_field_bool, config_field_child, config_field_hide, config_field_optional, edit_field_bool, edit_field_list_option, edit_field_number, edit_field_text, edit_field_text_option, generate_form_reducer, html_if};
|
||||
use crate::{config_field, config_field_bool, config_field_child, config_field_hide, config_field_optional, edit_field_bool, edit_field_list_option, edit_field_number, edit_field_number_u64, edit_field_text, edit_field_text_option, generate_form_reducer, html_if};
|
||||
use yew::prelude::*;
|
||||
use yew_i18n::use_translation;
|
||||
use shared::model::{WebUiConfigDto, ContentSecurityPolicyConfigDto, WebAuthConfigDto};
|
||||
@@ -15,6 +15,7 @@ const LABEL_SECRET: &str = "LABEL.SECRET";
|
||||
const LABEL_TOKEN_TTL_MINS: &str = "LABEL.TOKEN_TTL_MINS";
|
||||
const LABEL_USERFILE: &str = "LABEL.USERFILE";
|
||||
const LABEL_PLAYER_SERVER: &str = "LABEL.PLAYER_SERVER";
|
||||
const LABEL_KICK_DURATION: &str = "LABEL.KICK_DURATION";
|
||||
const LABEL_USER_UI_ENABLED: &str = "LABEL.USER_UI_ENABLED";
|
||||
const LABEL_CONTENT_SECURITY_POLICY: &str = "LABEL.CONTENT_SECURITY_POLICY";
|
||||
const LABEL_CONTENT_SECURITY_POLICY_CUSTOM_ATTRIBUTES: &str = "LABEL.CUSTOM_ATTRIBUTES";
|
||||
@@ -29,6 +30,7 @@ generate_form_reducer!(
|
||||
UserUiEnabled => user_ui_enabled: bool,
|
||||
Path => path: Option<String>,
|
||||
PlayerServer => player_server: Option<String>,
|
||||
KickSecs => kick_secs: u64,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -126,6 +128,7 @@ pub fn WebUiConfigView() -> Html {
|
||||
{ config_field_bool!(webui_state.form, translate.t(LABEL_USER_UI_ENABLED), user_ui_enabled) }
|
||||
{ config_field_optional!(webui_state.form, translate.t(LABEL_PATH), path) }
|
||||
{ config_field_optional!(webui_state.form, translate.t(LABEL_PLAYER_SERVER), player_server) }
|
||||
{ config_field!(webui_state.form, translate.t(LABEL_KICK_DURATION), kick_secs) }
|
||||
<Card class="tp__config-view__card">
|
||||
<h1>{translate.t(LABEL_CONTENT_SECURITY_POLICY)}</h1>
|
||||
{ config_field_bool!(csp_state.form, translate.t(LABEL_ENABLED), enabled) }
|
||||
@@ -163,6 +166,7 @@ pub fn WebUiConfigView() -> Html {
|
||||
{ edit_field_bool!(webui_state, translate.t(LABEL_USER_UI_ENABLED), user_ui_enabled, WebUiConfigFormAction::UserUiEnabled) }
|
||||
{ edit_field_text_option!(webui_state, translate.t(LABEL_PATH), path, WebUiConfigFormAction::Path) }
|
||||
{ edit_field_text_option!(webui_state, translate.t(LABEL_PLAYER_SERVER), player_server, WebUiConfigFormAction::PlayerServer) }
|
||||
{ edit_field_number_u64!(webui_state, translate.t(LABEL_KICK_DURATION), kick_secs, WebUiConfigFormAction::KickSecs) }
|
||||
<Card class="tp__config-view__card">
|
||||
<h1>{translate.t(LABEL_CONTENT_SECURITY_POLICY)}</h1>
|
||||
{ edit_field_bool!(csp_state, translate.t(LABEL_ENABLED), enabled, CspConfigFormAction::Enabled) }
|
||||
|
||||
@@ -8,7 +8,7 @@ use gloo_timers::callback::Interval;
|
||||
use gloo_utils::window;
|
||||
use shared::error::{create_tuliprox_error_result, TuliproxError, TuliproxErrorKind};
|
||||
use shared::model::{PlaylistItemType, ProtocolMessage, SortOrder, StreamChannel, StreamInfo, UserCommand};
|
||||
use shared::utils::{current_time_secs, strip_port};
|
||||
use shared::utils::{current_time_secs, default_kick_secs, strip_port};
|
||||
use std::fmt::Display;
|
||||
use std::rc::Rc;
|
||||
use std::str::FromStr;
|
||||
@@ -84,7 +84,7 @@ pub fn StreamsTable(props: &StreamsTableProps) -> Html {
|
||||
let popup_is_open = use_state(|| false);
|
||||
let selected_dto = use_state(|| None::<Rc<StreamInfo>>);
|
||||
|
||||
let headers = use_memo(config_ctx, |cfg| {
|
||||
let headers = use_memo(config_ctx.clone(), |cfg| {
|
||||
let include_country = if let Some(app_cfg) = &cfg.config {
|
||||
app_cfg.config.is_geoip_enabled()
|
||||
} else {
|
||||
@@ -241,12 +241,15 @@ pub fn StreamsTable(props: &StreamsTableProps) -> Html {
|
||||
let services = service_ctx.clone();
|
||||
let selected_dto = selected_dto.clone();
|
||||
let copy_to_clipboard = copy_to_clipboard.clone();
|
||||
let kick_secs = config_ctx.config.as_ref().and_then(|app_cfg| app_cfg.config.web_ui.as_ref())
|
||||
.map(|web_ui| web_ui.kick_secs)
|
||||
.unwrap_or_else(default_kick_secs);
|
||||
Callback::from(move |(name, _): (String, _)| {
|
||||
if let Ok(action) = StreamsTableAction::from_str(&name) {
|
||||
match action {
|
||||
StreamsTableAction::Kick => {
|
||||
if let Some(dto) = (*selected_dto).as_ref() {
|
||||
if !services.websocket.send_message(ProtocolMessage::UserAction(UserCommand::Kick(dto.addr))) {
|
||||
if !services.websocket.send_message(ProtocolMessage::UserAction(UserCommand::Kick(dto.addr, dto.channel.virtual_id, kick_secs))) {
|
||||
services.toastr.error(translate.t("MESSAGES.FAILED_TO_KICK_USER_STREAM"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,7 +172,7 @@ pub fn ConfigTargetView(props: &ConfigTargetViewProps) -> Html {
|
||||
{ config_field_child!(translate.t(LABEL_FILTER), {
|
||||
html! {
|
||||
<FilterInput filter={target_form_state_2.form.filter.clone()} on_change={Callback::from(move |new_filter: Option<String>| {
|
||||
target_form_state_2.dispatch(ConfigTargetFormAction::Filter(new_filter.unwrap_or_else(String::new)));
|
||||
target_form_state_2.dispatch(ConfigTargetFormAction::Filter(new_filter.unwrap_or_default()));
|
||||
})} />
|
||||
}
|
||||
})}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "shared"
|
||||
version = "3.2.18"
|
||||
version = "3.2.19"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::error::{TuliproxError, TuliproxErrorKind};
|
||||
use crate::model::WebAuthConfigDto;
|
||||
use crate::utils::{default_as_true, is_blank_optional_string};
|
||||
use crate::utils::{default_as_true, is_blank_optional_string, default_kick_secs};
|
||||
|
||||
const RESERVED_PATHS: &[&str] = &[
|
||||
"cvs",
|
||||
@@ -82,6 +82,8 @@ pub struct WebUiConfigDto {
|
||||
pub auth: Option<WebAuthConfigDto>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub player_server: Option<String>,
|
||||
#[serde(default = "default_kick_secs")]
|
||||
pub kick_secs: u64,
|
||||
}
|
||||
|
||||
impl Default for WebUiConfigDto {
|
||||
@@ -93,6 +95,7 @@ impl Default for WebUiConfigDto {
|
||||
path: None,
|
||||
auth: None,
|
||||
player_server: None,
|
||||
kick_secs: default_kick_secs(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -104,6 +107,7 @@ impl WebUiConfigDto {
|
||||
&& self.user_ui_enabled == empty.user_ui_enabled
|
||||
&& is_blank_optional_string(&self.path)
|
||||
&& is_blank_optional_string(&self.player_server)
|
||||
&& self.kick_secs == default_kick_secs()
|
||||
&& (self.content_security_policy.is_none()
|
||||
|| self
|
||||
.content_security_policy
|
||||
@@ -130,6 +134,7 @@ impl WebUiConfigDto {
|
||||
if is_blank_optional_string(&self.player_server) {
|
||||
self.player_server = None;
|
||||
}
|
||||
self.kick_secs = default_kick_secs();
|
||||
}
|
||||
|
||||
pub fn prepare(&mut self) -> Result<(), TuliproxError> {
|
||||
|
||||
@@ -10,6 +10,7 @@ use std::str::FromStr;
|
||||
// https://siptv.eu/howto/playlist.html
|
||||
|
||||
pub type UUIDType = [u8; 32];
|
||||
pub type VirtualId = u32;
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, Hash, PartialEq, Serialize, Deserialize, Default)]
|
||||
#[repr(u8)]
|
||||
@@ -138,7 +139,7 @@ pub trait FieldSetAccessor {
|
||||
}
|
||||
|
||||
pub trait PlaylistEntry: Send + Sync {
|
||||
fn get_virtual_id(&self) -> u32;
|
||||
fn get_virtual_id(&self) -> VirtualId;
|
||||
fn get_provider_id(&self) -> Option<u32>;
|
||||
fn get_category_id(&self) -> Option<u32>;
|
||||
fn get_provider_url(&self) -> String;
|
||||
@@ -151,7 +152,7 @@ pub struct PlaylistItemHeader {
|
||||
#[serde(skip)]
|
||||
pub uuid: UUIDType, // calculated
|
||||
pub id: String, // provider id
|
||||
pub virtual_id: u32, // virtual id
|
||||
pub virtual_id: VirtualId, // virtual id
|
||||
pub name: String,
|
||||
pub chno: String,
|
||||
pub logo: String,
|
||||
@@ -290,7 +291,7 @@ generate_field_accessor_impl_for_playlist_item_header!(id, /*virtual_id,*/ name,
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct M3uPlaylistItem {
|
||||
pub virtual_id: u32,
|
||||
pub virtual_id: VirtualId,
|
||||
pub provider_id: String,
|
||||
pub name: String,
|
||||
pub chno: String,
|
||||
@@ -367,7 +368,7 @@ impl M3uPlaylistItem {
|
||||
|
||||
impl PlaylistEntry for M3uPlaylistItem {
|
||||
#[inline]
|
||||
fn get_virtual_id(&self) -> u32 {
|
||||
fn get_virtual_id(&self) -> VirtualId {
|
||||
self.virtual_id
|
||||
}
|
||||
|
||||
@@ -421,7 +422,7 @@ impl From<M3uPlaylistItem> for CommonPlaylistItem {
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct XtreamPlaylistItem {
|
||||
pub virtual_id: u32,
|
||||
pub virtual_id: VirtualId,
|
||||
pub provider_id: u32,
|
||||
pub name: String,
|
||||
pub logo: String,
|
||||
@@ -477,7 +478,7 @@ impl XtreamPlaylistItem {
|
||||
|
||||
impl PlaylistEntry for XtreamPlaylistItem {
|
||||
#[inline]
|
||||
fn get_virtual_id(&self) -> u32 {
|
||||
fn get_virtual_id(&self) -> VirtualId {
|
||||
self.virtual_id
|
||||
}
|
||||
#[inline]
|
||||
@@ -718,7 +719,7 @@ impl PlaylistItem {
|
||||
|
||||
impl PlaylistEntry for PlaylistItem {
|
||||
#[inline]
|
||||
fn get_virtual_id(&self) -> u32 {
|
||||
fn get_virtual_id(&self) -> VirtualId {
|
||||
self.header.virtual_id
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use std::net::SocketAddr;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::model::VirtualId;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum UserCommand {
|
||||
Kick(SocketAddr)
|
||||
Kick(SocketAddr, VirtualId, u64),
|
||||
}
|
||||
@@ -21,4 +21,6 @@ pub fn default_secret() -> String {
|
||||
}
|
||||
|
||||
out.iter().map(|b| format!("{:02X}", b)).collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn default_kick_secs() -> u64 { 30 }
|
||||
Reference in New Issue
Block a user