mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-29 02:02:23 +01:00
Fix: Copy url in frontend does not resolve provider:// (#673)
* Fix: Copy url in frontend does not resolve provider:// * Fix: Filter editor on demand validation * Fix: redirect to login screen if auto logout happens
This commit is contained in:
@@ -7,14 +7,14 @@ use crate::{api::{
|
||||
xtream_api::xtream_get_stream_info_response,
|
||||
},
|
||||
model::AppState,
|
||||
}, auth::create_access_token, auth::permission_layer, model::{parse_xmltv_for_web_ui_from_url, ConfigInput, ConfigInputFlags, ConfigInputOptions}, repository::xtream_get_item_for_stream_id};
|
||||
}, auth::create_access_token, auth::permission_layer, model::{parse_xmltv_for_web_ui_from_url, AppConfig, ConfigInput, ConfigInputFlags, ConfigInputOptions}, repository::xtream_get_item_for_stream_id};
|
||||
use axum::{response::IntoResponse, Router};
|
||||
use log::{debug, error};
|
||||
use serde_json::json;
|
||||
use shared::{
|
||||
model::{
|
||||
permission::Permission,
|
||||
InputType, PlaylistEpgRequest, PlaylistRequest, ProxyType, TargetType, UiPlaylistItem, WebplayerUrlRequest,
|
||||
InputType, PlaylistEpgRequest, PlaylistRequest, PlaylistUrlResolveRequest, ProxyType, TargetType, UiPlaylistItem,
|
||||
XtreamCluster,
|
||||
},
|
||||
utils::{sanitize_sensitive_info, Internable},
|
||||
@@ -60,6 +60,46 @@ fn create_config_input_for_xtream(username: &str, password: &str, host: &str) ->
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_provider_url_with_input(input: &ConfigInput, url: &str) -> String {
|
||||
match input.resolve_url(url) {
|
||||
Ok(resolved) => resolved.into_owned(),
|
||||
Err(err) => {
|
||||
let sanitized_url = sanitize_sensitive_info(url);
|
||||
let err_text = err.to_string();
|
||||
let sanitized_err = sanitize_sensitive_info(&err_text);
|
||||
error!("resolve_provider_url_with_input failed for url '{sanitized_url}': {sanitized_err}");
|
||||
url.to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_provider_url_for_request(app_config: &AppConfig, playlist_request: &PlaylistRequest, url: &str) -> String {
|
||||
if !url.starts_with(shared::utils::PROVIDER_SCHEME_PREFIX) {
|
||||
return url.to_string();
|
||||
}
|
||||
|
||||
match playlist_request {
|
||||
PlaylistRequest::Input(input_id) => app_config
|
||||
.get_input_by_id(*input_id)
|
||||
.map_or_else(|| url.to_string(), |input| resolve_provider_url_with_input(input.as_ref(), url)),
|
||||
PlaylistRequest::Target(target_id) => app_config
|
||||
.get_target_by_id(*target_id)
|
||||
.and_then(|target| app_config.get_inputs_for_target(&target.name))
|
||||
.and_then(|inputs| {
|
||||
let mut matches = inputs
|
||||
.into_iter()
|
||||
.filter(|input| input.get_resolve_provider(url).is_some());
|
||||
let first = matches.next()?;
|
||||
if matches.next().is_some() {
|
||||
return None;
|
||||
}
|
||||
Some(resolve_provider_url_with_input(first.as_ref(), url))
|
||||
})
|
||||
.unwrap_or_else(|| url.to_string()),
|
||||
PlaylistRequest::CustomXtream(_) | PlaylistRequest::CustomM3u(_) => url.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn playlist_update(
|
||||
axum::extract::State(app_state): axum::extract::State<Arc<AppState>>,
|
||||
axum::extract::Json(targets): axum::extract::Json<Vec<String>>,
|
||||
@@ -213,9 +253,11 @@ async fn playlist_series_info(
|
||||
axum::http::StatusCode::NO_CONTENT.into_response()
|
||||
}
|
||||
|
||||
async fn playlist_webplayer(
|
||||
fn playlist_webplayer(
|
||||
axum::extract::State(app_state): axum::extract::State<Arc<AppState>>,
|
||||
axum::extract::Json(playlist_item): axum::extract::Json<WebplayerUrlRequest>,
|
||||
target_id: u16,
|
||||
virtual_id: u32,
|
||||
cluster: XtreamCluster,
|
||||
) -> impl axum::response::IntoResponse + Send {
|
||||
let access_token = create_access_token(&app_state.app_config.access_token_secret, 30);
|
||||
let config = app_state.app_config.config.load();
|
||||
@@ -228,9 +270,9 @@ async fn playlist_webplayer(
|
||||
let base_url = server_info.get_base_url();
|
||||
format!(
|
||||
"{base_url}/token/{access_token}/{}/{}/{}",
|
||||
playlist_item.target_id,
|
||||
playlist_item.cluster.as_stream_type(),
|
||||
playlist_item.virtual_id
|
||||
target_id,
|
||||
cluster.as_stream_type(),
|
||||
virtual_id
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
@@ -282,9 +324,29 @@ async fn playlist_resource(
|
||||
}
|
||||
}
|
||||
|
||||
async fn playlist_resolve_url(
|
||||
axum::extract::State(app_state): axum::extract::State<Arc<AppState>>,
|
||||
axum::extract::Json(request): axum::extract::Json<PlaylistUrlResolveRequest>,
|
||||
) -> impl IntoResponse + Send {
|
||||
match request {
|
||||
PlaylistUrlResolveRequest::Webplayer { target_id, virtual_id, cluster } => {
|
||||
playlist_webplayer(
|
||||
axum::extract::State(app_state),
|
||||
target_id,
|
||||
virtual_id,
|
||||
cluster,
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
PlaylistUrlResolveRequest::Provider { playlist_request, url } => {
|
||||
resolve_provider_url_for_request(&app_state.app_config, &playlist_request, &url).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn v1_api_playlist_register_protected(router: Router<Arc<AppState>>) -> axum::Router<Arc<AppState>> {
|
||||
router
|
||||
.route("/playlist/webplayer", axum::routing::post(playlist_webplayer))
|
||||
.route("/playlist/resolve_url", axum::routing::post(playlist_resolve_url))
|
||||
.route("/playlist/update", axum::routing::post(playlist_update))
|
||||
.route("/playlist/epg", axum::routing::post(playlist_epg))
|
||||
.route("/playlist/live", axum::routing::post(playlist_content_live))
|
||||
@@ -309,9 +371,9 @@ pub fn v1_api_playlist_register_with_permissions(
|
||||
.route("/live", axum::routing::post(playlist_content_live))
|
||||
.route("/vod", axum::routing::post(playlist_content_vod))
|
||||
.route("/series", axum::routing::post(playlist_content_series))
|
||||
.route("/resolve_url", axum::routing::post(playlist_resolve_url))
|
||||
.route("/series_info/{virtual_id}/{provider_id}", axum::routing::post(playlist_series_info))
|
||||
.route("/series/episode/{virtual_id}", axum::routing::post(playlist_episode_item))
|
||||
.route("/webplayer", axum::routing::post(playlist_webplayer))
|
||||
.layer(permission_layer!(app_state, Permission::PlaylistRead));
|
||||
|
||||
let write_routes = Router::new()
|
||||
@@ -329,6 +391,259 @@ pub fn v1_api_playlist_register_with_permissions(
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::resolve_provider_url_for_request;
|
||||
use crate::model::{AppConfig, Config, ConfigInput, ConfigProvider, ConfigSource, ConfigTarget, SourcesConfig};
|
||||
use arc_swap::{ArcSwap, ArcSwapOption};
|
||||
use shared::foundation::Filter;
|
||||
use shared::{
|
||||
model::{ConfigPaths, ConfigProviderDto, PlaylistRequest},
|
||||
utils::Internable,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
fn test_app_config(input: Arc<ConfigInput>, source: ConfigSource) -> AppConfig {
|
||||
let sources = SourcesConfig {
|
||||
batch_files: vec![],
|
||||
provider: vec![],
|
||||
inputs: vec![input],
|
||||
sources: vec![source],
|
||||
templates: None,
|
||||
};
|
||||
|
||||
AppConfig {
|
||||
config: Arc::new(ArcSwap::from_pointee(Config::default())),
|
||||
sources: Arc::new(ArcSwap::from_pointee(sources)),
|
||||
hdhomerun: Arc::new(ArcSwapOption::empty()),
|
||||
api_proxy: Arc::new(ArcSwapOption::empty()),
|
||||
file_locks: Arc::new(crate::utils::FileLockManager::default()),
|
||||
paths: Arc::new(ArcSwap::from_pointee(ConfigPaths {
|
||||
home_path: String::new(),
|
||||
config_path: String::new(),
|
||||
storage_path: String::new(),
|
||||
config_file_path: String::new(),
|
||||
sources_file_path: String::new(),
|
||||
mapping_file_path: None,
|
||||
mapping_files_used: None,
|
||||
template_file_path: None,
|
||||
template_files_used: None,
|
||||
api_proxy_file_path: String::new(),
|
||||
custom_stream_response_path: None,
|
||||
})),
|
||||
custom_stream_response: Arc::new(ArcSwapOption::empty()),
|
||||
access_token_secret: [0; 32],
|
||||
encrypt_secret: [0; 16],
|
||||
media_tools: Arc::new(crate::model::MediaToolCapabilities::default()),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_provider_url_for_input_request_rewrites_provider_scheme() {
|
||||
let provider = ConfigProvider::from(&ConfigProviderDto {
|
||||
name: "demo".intern(),
|
||||
urls: vec!["http://provider.example".intern()],
|
||||
dns: None,
|
||||
});
|
||||
let input = Arc::new(ConfigInput {
|
||||
id: 7,
|
||||
name: "input".intern(),
|
||||
provider_configs: Some(vec![Arc::new(provider)]),
|
||||
..Default::default()
|
||||
});
|
||||
let source = ConfigSource { inputs: vec![Arc::clone(&input.name)], targets: vec![] };
|
||||
let app_config = test_app_config(input, source);
|
||||
let resolved = resolve_provider_url_for_request(
|
||||
&app_config,
|
||||
&PlaylistRequest::Input(7),
|
||||
"provider://demo/live/user/pass/1359.ts",
|
||||
);
|
||||
|
||||
assert_eq!(resolved, "http://provider.example/live/user/pass/1359.ts");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_provider_url_for_target_request_rewrites_provider_scheme() {
|
||||
let provider = ConfigProvider::from(&ConfigProviderDto {
|
||||
name: "demo".intern(),
|
||||
urls: vec!["http://provider.example".intern()],
|
||||
dns: None,
|
||||
});
|
||||
let input = Arc::new(ConfigInput {
|
||||
id: 7,
|
||||
name: "input".intern(),
|
||||
provider_configs: Some(vec![Arc::new(provider)]),
|
||||
..Default::default()
|
||||
});
|
||||
let target = Arc::new(ConfigTarget {
|
||||
id: 11,
|
||||
enabled: true,
|
||||
name: "target".to_string(),
|
||||
options: None,
|
||||
sort: None,
|
||||
filter: Filter::default(),
|
||||
output: vec![],
|
||||
rename: None,
|
||||
mapping_ids: None,
|
||||
mapping: Arc::default(),
|
||||
favourites: None,
|
||||
processing_order: Default::default(),
|
||||
watch: None,
|
||||
use_memory_cache: false,
|
||||
});
|
||||
let source = ConfigSource { inputs: vec![Arc::clone(&input.name)], targets: vec![target] };
|
||||
let app_config = test_app_config(input, source);
|
||||
let resolved = resolve_provider_url_for_request(
|
||||
&app_config,
|
||||
&PlaylistRequest::Target(11),
|
||||
"provider://demo/live/user/pass/1359.ts",
|
||||
);
|
||||
|
||||
assert_eq!(resolved, "http://provider.example/live/user/pass/1359.ts");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_provider_url_passthrough_for_unresolved_provider_input_request() {
|
||||
let provider = ConfigProvider::from(&ConfigProviderDto {
|
||||
name: "demo".intern(),
|
||||
urls: vec!["http://provider.example".intern()],
|
||||
dns: None,
|
||||
});
|
||||
let input = Arc::new(ConfigInput {
|
||||
id: 7,
|
||||
name: "input".intern(),
|
||||
provider_configs: Some(vec![Arc::new(provider)]),
|
||||
..Default::default()
|
||||
});
|
||||
let source = ConfigSource { inputs: vec![Arc::clone(&input.name)], targets: vec![] };
|
||||
let app_config = test_app_config(input, source);
|
||||
let original = "provider://unknown/live/user/pass/1359.ts";
|
||||
let resolved = resolve_provider_url_for_request(&app_config, &PlaylistRequest::Input(7), original);
|
||||
|
||||
assert_eq!(resolved, original);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_provider_url_passthrough_for_unresolved_provider_target_request() {
|
||||
let provider = ConfigProvider::from(&ConfigProviderDto {
|
||||
name: "demo".intern(),
|
||||
urls: vec!["http://provider.example".intern()],
|
||||
dns: None,
|
||||
});
|
||||
let input = Arc::new(ConfigInput {
|
||||
id: 7,
|
||||
name: "input".intern(),
|
||||
provider_configs: Some(vec![Arc::new(provider)]),
|
||||
..Default::default()
|
||||
});
|
||||
let target = Arc::new(ConfigTarget {
|
||||
id: 11,
|
||||
enabled: true,
|
||||
name: "target".to_string(),
|
||||
options: None,
|
||||
sort: None,
|
||||
filter: Filter::default(),
|
||||
output: vec![],
|
||||
rename: None,
|
||||
mapping_ids: None,
|
||||
mapping: Arc::default(),
|
||||
favourites: None,
|
||||
processing_order: Default::default(),
|
||||
watch: None,
|
||||
use_memory_cache: false,
|
||||
});
|
||||
let source = ConfigSource { inputs: vec![Arc::clone(&input.name)], targets: vec![target] };
|
||||
let app_config = test_app_config(input, source);
|
||||
let original = "provider://unknown/live/user/pass/1359.ts";
|
||||
let resolved = resolve_provider_url_for_request(&app_config, &PlaylistRequest::Target(11), original);
|
||||
|
||||
assert_eq!(resolved, original);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_provider_url_passthrough_for_ambiguous_target_request() {
|
||||
let provider_a = ConfigProvider::from(&ConfigProviderDto {
|
||||
name: "shared".intern(),
|
||||
urls: vec!["http://provider-a.example".intern()],
|
||||
dns: None,
|
||||
});
|
||||
let provider_b = ConfigProvider::from(&ConfigProviderDto {
|
||||
name: "shared".intern(),
|
||||
urls: vec!["http://provider-b.example".intern()],
|
||||
dns: None,
|
||||
});
|
||||
let input_a = Arc::new(ConfigInput {
|
||||
id: 7,
|
||||
name: "input-a".intern(),
|
||||
provider_configs: Some(vec![Arc::new(provider_a)]),
|
||||
..Default::default()
|
||||
});
|
||||
let input_b = Arc::new(ConfigInput {
|
||||
id: 8,
|
||||
name: "input-b".intern(),
|
||||
provider_configs: Some(vec![Arc::new(provider_b)]),
|
||||
..Default::default()
|
||||
});
|
||||
let target = Arc::new(ConfigTarget {
|
||||
id: 11,
|
||||
enabled: true,
|
||||
name: "target".to_string(),
|
||||
options: None,
|
||||
sort: None,
|
||||
filter: Filter::default(),
|
||||
output: vec![],
|
||||
rename: None,
|
||||
mapping_ids: None,
|
||||
mapping: Arc::default(),
|
||||
favourites: None,
|
||||
processing_order: Default::default(),
|
||||
watch: None,
|
||||
use_memory_cache: false,
|
||||
});
|
||||
let source = ConfigSource {
|
||||
inputs: vec![Arc::clone(&input_a.name), Arc::clone(&input_b.name)],
|
||||
targets: vec![target],
|
||||
};
|
||||
let sources = SourcesConfig {
|
||||
batch_files: vec![],
|
||||
provider: vec![],
|
||||
inputs: vec![input_a, input_b],
|
||||
sources: vec![source],
|
||||
templates: None,
|
||||
};
|
||||
|
||||
let app_config = AppConfig {
|
||||
config: Arc::new(ArcSwap::from_pointee(Config::default())),
|
||||
sources: Arc::new(ArcSwap::from_pointee(sources)),
|
||||
hdhomerun: Arc::new(ArcSwapOption::empty()),
|
||||
api_proxy: Arc::new(ArcSwapOption::empty()),
|
||||
file_locks: Arc::new(crate::utils::FileLockManager::default()),
|
||||
paths: Arc::new(ArcSwap::from_pointee(ConfigPaths {
|
||||
home_path: String::new(),
|
||||
config_path: String::new(),
|
||||
storage_path: String::new(),
|
||||
config_file_path: String::new(),
|
||||
sources_file_path: String::new(),
|
||||
mapping_file_path: None,
|
||||
mapping_files_used: None,
|
||||
template_file_path: None,
|
||||
template_files_used: None,
|
||||
api_proxy_file_path: String::new(),
|
||||
custom_stream_response_path: None,
|
||||
})),
|
||||
custom_stream_response: Arc::new(ArcSwapOption::empty()),
|
||||
access_token_secret: [0; 32],
|
||||
encrypt_secret: [0; 16],
|
||||
media_tools: Arc::new(crate::model::MediaToolCapabilities::default()),
|
||||
};
|
||||
|
||||
let original = "provider://shared/live/user/pass/1359.ts";
|
||||
let resolved = resolve_provider_url_for_request(&app_config, &PlaylistRequest::Target(11), original);
|
||||
|
||||
assert_eq!(resolved, original);
|
||||
}
|
||||
}
|
||||
|
||||
async fn playlist_episode_item(
|
||||
axum::extract::Path(virtual_id): axum::extract::Path<String>,
|
||||
axum::extract::State(app_state): axum::extract::State<Arc<AppState>>,
|
||||
|
||||
Reference in New Issue
Block a user