From 976f460f8e54382540b06b246c053e0e7a40bc63 Mon Sep 17 00:00:00 2001 From: euzu Date: Mon, 23 Oct 2023 09:58:36 +0200 Subject: [PATCH] Added optional token to user credentials for using instead of uname/pwd --- README.md | 13 +++++-- api-proxy.yml | 2 +- src/api/api_utils.rs | 21 +++++++++-- src/api/m3u_api.rs | 14 +++----- src/api/main_api.rs | 4 +-- src/api/model_api.rs | 13 +++++++ src/api/xtream_player_api.rs | 17 +++------ src/config_reader.rs | 2 +- src/main.rs | 4 +-- src/model/api_proxy.rs | 54 ++++++++++++++++++++++++---- src/model/config.rs | 39 +++++++++++++------- src/processing/playlist_processor.rs | 2 +- src/processing/playlist_watch.rs | 12 ++++--- 13 files changed, 140 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index d523e6ec8..247391c76 100644 --- a/README.md +++ b/README.md @@ -575,15 +575,24 @@ you need to create a `api-proxy.yml` configuration. You can specify the path for the file with the `-a` cli argument. +`username` and `password`are mandatory for credentials. `username` is unique. +The `token` is _optional_. If defined it should be unique. The `token`can be used +instead of username+password + To access the api for: - `xtream` use url like `http://192.169.1.2/player_api.php?username={}&password={}` - `m3u` use url `http://192.169.1.2/get.php?username={}&password={}` +or with token +- `xtream` use url like `http://192.169.1.2/player_api.php?token={}` +- `m3u` use url `http://192.169.1.2/get.php?token={}` -_Do not forget to replace `{}` with credentials._ + +_Do not forget to replace `{}` with credentials._ When you define credentials for a `target`, ensure that this target has `output` format `xtream`or `m3u`. + ```yaml server: protocol: http @@ -596,7 +605,7 @@ server: user: - target: pl1 credentials: - - {username: x3452, password: ztrhgrGZrt83hjerter} + - {username: x3452, password: ztrhgrGZrt83hjerter, token: 4342sdfr3424} ``` important: if the given `target`is of type diff --git a/api-proxy.yml b/api-proxy.yml index 80e4aa92f..f94f24973 100644 --- a/api-proxy.yml +++ b/api-proxy.yml @@ -9,4 +9,4 @@ server: user: - target: pl1 credentials: - - {username: x3452, password: ztrhgrGZrt83hjerter} \ No newline at end of file + - {username: x3452, password: ztrhgrGZrt83hjerter, token: 4243234wer234er234} \ No newline at end of file diff --git a/src/api/api_utils.rs b/src/api/api_utils.rs index 5635527ef..3ece577bb 100644 --- a/src/api/api_utils.rs +++ b/src/api/api_utils.rs @@ -1,8 +1,10 @@ use std::path::PathBuf; use actix_web::http::header::{CACHE_CONTROL, HeaderValue}; -use actix_web::{HttpRequest, HttpResponse}; +use actix_web::{HttpRequest, HttpResponse, web}; +use crate::api::model_api::{AppState, UserApiRequest}; +use crate::model::config::ConfigTarget; -pub async fn serve_file(file_path: &PathBuf, req: &HttpRequest) -> HttpResponse { +pub(crate) async fn serve_file(file_path: &PathBuf, req: &HttpRequest) -> HttpResponse { if file_path.exists() { let file = actix_files::NamedFile::open_async(file_path).await.unwrap() .set_content_type(mime::APPLICATION_JSON) @@ -14,4 +16,19 @@ pub async fn serve_file(file_path: &PathBuf, req: &HttpRequest) -> HttpResponse } else { HttpResponse::NoContent().finish() } +} + +pub(crate) fn get_user_target<'a>(api_req: &'a web::Query, app_state: &'a web::Data) -> Option<&'a ConfigTarget> { + let username = api_req.username.as_str(); + let password = api_req.password.as_str(); + if !username.is_empty() && !password.is_empty() { + app_state.config.get_target_for_user(username, password) + } else { + let token = api_req.token.as_str(); + if !token.is_empty() { + app_state.config.get_target_for_user_by_token(token) + } else { + None + } + } } \ No newline at end of file diff --git a/src/api/m3u_api.rs b/src/api/m3u_api.rs index e7038ca16..cbcae9447 100644 --- a/src/api/m3u_api.rs +++ b/src/api/m3u_api.rs @@ -1,22 +1,16 @@ use actix_web::{get, HttpRequest, HttpResponse, web}; -use crate::api::api_utils::serve_file; -use crate::api::model_api::AppState; +use crate::api::api_utils::{get_user_target, serve_file}; +use crate::api::model_api::{AppState, UserApiRequest}; use crate::repository::m3u_repository::get_m3u_file_path; -#[derive(serde::Serialize, serde::Deserialize)] -pub struct M3uApiRequest { - username: String, - password: String, -} - #[get("/get.php")] pub(crate) async fn m3u_api( - api_req: web::Query, + api_req: web::Query, req: HttpRequest, _app_state: web::Data, ) -> HttpResponse { - match _app_state.config.get_target_for_user(api_req.username.as_str(), api_req.password.as_str()) { + match get_user_target(&api_req, &_app_state) { Some(target) => { let filename = target.get_m3u_filename(); match get_m3u_file_path(&_app_state.config, &filename) { diff --git a/src/api/main_api.rs b/src/api/main_api.rs index 115a5e57f..2d720e651 100644 --- a/src/api/main_api.rs +++ b/src/api/main_api.rs @@ -18,7 +18,7 @@ use crate::api::model_api::{AppState, PlaylistRequest, ServerConfig}; use crate::api::xtream_player_api::xtream_player_api; use crate::download::get_m3u_playlist; use crate::model::config::{Config, ConfigInput, InputType, ProcessTargets}; -use crate::processing::playlist_processor::start_processing; +use crate::processing::playlist_processor::exec_processing; #[get("/")] async fn index( @@ -129,7 +129,7 @@ async fn start_scheduler(expression: &str, data: Data) -> ! { if let Some(datetime) = upcoming.next() { if datetime.timestamp() <= local.timestamp() { - start_processing(data.config.clone(), data.targets.clone()); + exec_processing(data.config.clone(), data.targets.clone()); } } } diff --git a/src/api/model_api.rs b/src/api/model_api.rs index 45108e037..acfd719cf 100644 --- a/src/api/model_api.rs +++ b/src/api/model_api.rs @@ -2,6 +2,7 @@ use std::sync::Arc; use actix_web::web; use serde::{Deserialize, Serialize}; use crate::model::config::{Config, ProcessTargets}; +use crate::model::model_config::{default_as_empty_str}; pub(crate) struct AppState { pub config: Arc, @@ -59,3 +60,15 @@ pub(crate) struct XtreamAuthorizationResponse { pub server_info: XtreamServerInfo, } + +#[derive(serde::Serialize, serde::Deserialize)] +pub(crate) struct UserApiRequest { + #[serde(default = "default_as_empty_str")] + pub username: String, + #[serde(default = "default_as_empty_str")] + pub password: String, + #[serde(default = "default_as_empty_str")] + pub token: String, + #[serde(default = "default_as_empty_str")] + pub action: String, +} \ No newline at end of file diff --git a/src/api/xtream_player_api.rs b/src/api/xtream_player_api.rs index 0de7e97d9..bba5187cb 100644 --- a/src/api/xtream_player_api.rs +++ b/src/api/xtream_player_api.rs @@ -3,10 +3,10 @@ use actix_web::{get, HttpRequest, HttpResponse, web}; use chrono::{Duration, Local}; -use crate::api::api_utils::serve_file; -use crate::api::model_api::{AppState, XtreamAuthorizationResponse, XtreamServerInfo, XtreamUserInfo}; +use crate::api::api_utils::{get_user_target, serve_file}; +use crate::api::model_api::{AppState, UserApiRequest, XtreamAuthorizationResponse, XtreamServerInfo, XtreamUserInfo}; use crate::model::config::Config; -use crate::model::model_config::{default_as_empty_str, TargetType}; +use crate::model::model_config::{TargetType}; use crate::repository::xtream_repository::{COL_CAT_LIVE, COL_CAT_SERIES, COL_CAT_VOD, COL_LIVE, COL_SERIES, COL_VOD, xtream_get_all}; fn get_user_info(user_name: &str, cfg: &Config) -> XtreamAuthorizationResponse { @@ -39,21 +39,14 @@ fn get_user_info(user_name: &str, cfg: &Config) -> XtreamAuthorizationResponse { } } -#[derive(serde::Serialize, serde::Deserialize)] -pub struct XtreamApiRequest { - username: String, - password: String, - #[serde(default = "default_as_empty_str")] - action: String, -} #[get("/player_api.php")] pub(crate) async fn xtream_player_api( - api_req: web::Query, + api_req: web::Query, req: HttpRequest, _app_state: web::Data, ) -> HttpResponse { - match _app_state.config.get_target_for_user(api_req.username.as_str(), api_req.password.as_str()) { + match get_user_target(&api_req, &_app_state) { Some(target) => { let target_name = &target.name; if target.has_output(&TargetType::Xtream) { diff --git a/src/config_reader.rs b/src/config_reader.rs index 952c969d0..89d03eba3 100644 --- a/src/config_reader.rs +++ b/src/config_reader.rs @@ -76,7 +76,7 @@ pub(crate) fn read_api_proxy(api_proxy_file: &str) -> Option { Ok(file) => { let mapping: Result = serde_yaml::from_reader(file); match mapping { - Ok(result) => { + Ok(mut result) => { match result.prepare() { Ok(_) => Some(result), Err(err) => { diff --git a/src/main.rs b/src/main.rs index d872f3a49..15f536d34 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,7 +11,7 @@ use log::{debug, error, info, LevelFilter}; use crate::config_reader::{read_api_proxy_config, read_config, read_mappings}; use crate::model::config::{Config, ProcessTargets, validate_targets}; -use crate::processing::playlist_processor::start_processing; +use crate::processing::playlist_processor::exec_processing; mod m3u_filter_error; mod config_reader; @@ -80,7 +80,7 @@ fn main() { } fn start_in_cli_mode(cfg: Arc, targets: Arc) { - start_processing(cfg, targets); + exec_processing(cfg, targets); } fn start_in_server_mode(cfg: Arc, targets: Arc) { diff --git a/src/model/api_proxy.rs b/src/model/api_proxy.rs index 0e266d4a5..96a694161 100644 --- a/src/model/api_proxy.rs +++ b/src/model/api_proxy.rs @@ -1,10 +1,26 @@ -use std::collections::{HashSet}; +use std::collections::HashSet; + use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind}; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub(crate) struct UserCredentials { pub username: String, pub password: String, + pub token: Option, +} + +impl UserCredentials { + pub fn matches_token(&self, token: &str) -> bool { + if let Some(tkn) = &self.token { + return tkn.eq(token); + } + false + } + + pub fn matches(&self, username: &str, password: &str) -> bool { + self.username.eq(username) && self.password.eq(password) + } + } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] @@ -14,9 +30,14 @@ pub(crate) struct TargetUser { } impl TargetUser { - pub fn get_target_name(&self, username: &str, password: &str) -> Option<&str> { - if self.credentials.iter().any(|c| c.username.eq_ignore_ascii_case(username) && c.password.eq(password)) { + if self.credentials.iter().any(|c| c.matches(username, password)) { + return Some(&self.target); + } + None + } + pub fn get_target_name_by_token(&self, token: &str) -> Option<&str> { + if self.credentials.iter().any(|c| c.matches_token(token)) { return Some(&self.target); } None @@ -24,6 +45,7 @@ impl TargetUser { } fn default_as_443() -> u16 { 443 } + fn default_as_1935() -> u16 { 1935 } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] @@ -46,17 +68,26 @@ pub(crate) struct ApiProxyConfig { } impl ApiProxyConfig { - - pub fn prepare(&self) -> Result<(), M3uFilterError>{ + pub fn prepare(&mut self) -> Result<(), M3uFilterError> { let mut usernames = HashSet::new(); + let mut tokens = HashSet::new(); let mut errors = Vec::new(); - for target_user in &self.user { - for user in &target_user.credentials { + for target_user in &mut self.user { + for user in &mut target_user.credentials { if usernames.contains(&user.username) { errors.push(format!("Non unique username found {}", &user.username)); } else { usernames.insert(user.username.to_string()); } + if let Some(token) = &user.token { + if token.is_empty() { + user.token = None + } else if tokens.contains(token) { + errors.push(format!("Non unique token found {}", &user.username)); + } else { + tokens.insert(token.to_string()); + } + } } } if errors.is_empty() { @@ -74,4 +105,13 @@ impl ApiProxyConfig { } None } + + pub fn get_target_name_by_token(&self, token: &str) -> Option<&str> { + for target_user in &self.user { + if let Some(target_name) = target_user.get_target_name_by_token(token) { + return Some(target_name); + }; + } + None + } } \ No newline at end of file diff --git a/src/model/config.rs b/src/model/config.rs index 2624dcaa6..d2b31b8e5 100644 --- a/src/model/config.rs +++ b/src/model/config.rs @@ -406,22 +406,35 @@ impl Config { self._api_proxy = api_proxy; } + fn _get_target_for_user(&self, user_target: Option<&str>) -> Option<&ConfigTarget> { + match user_target { + Some(target_name) => { + for source in &self.sources { + for target in &source.targets { + if target_name.eq_ignore_ascii_case(&target.name) { + return Some(target); + } + } + } + None + } + None => None + } + } + pub fn get_target_for_user(&self, username: &str, password: &str) -> Option<&ConfigTarget> { match &self._api_proxy { Some(api_proxy) => { - match api_proxy.get_target_name(username, password) { - Some(target_name) => { - for source in &self.sources { - for target in &source.targets { - if target_name.eq_ignore_ascii_case(&target.name) { - return Some(target); - } - } - } - None - } - None => None - } + self._get_target_for_user(api_proxy.get_target_name(username, password)) + } + _ => None + } + } + + pub fn get_target_for_user_by_token(&self, token: &str) -> Option<&ConfigTarget> { + match &self._api_proxy { + Some(api_proxy) => { + self._get_target_for_user(api_proxy.get_target_name_by_token(token)) } _ => None } diff --git a/src/processing/playlist_processor.rs b/src/processing/playlist_processor.rs index 6b56857c3..390ebef08 100644 --- a/src/processing/playlist_processor.rs +++ b/src/processing/playlist_processor.rs @@ -465,7 +465,7 @@ fn persist_playlist(playlist: &[PlaylistGroup], if errors.is_empty() { Err(errors) } else { Ok(()) } } -pub(crate) fn start_processing(cfg: Arc, targets: Arc) { +pub(crate) fn exec_processing(cfg: Arc, targets: Arc) { let (stats, errors) = process_sources(cfg.clone(), targets.clone()); let stats_msg = format!("Stats: {}", stats.iter().map(|stat| stat.to_string()).collect::>().join("\n")); // print stats diff --git a/src/processing/playlist_watch.rs b/src/processing/playlist_watch.rs index 0bc9326fc..19c46fe2f 100644 --- a/src/processing/playlist_watch.rs +++ b/src/processing/playlist_watch.rs @@ -21,6 +21,7 @@ pub(crate) fn process_group_watch(cfg: &Config, target_name: &str, pl: &Playlist match utils::get_file_path(&cfg.working_dir, Some(std::path::PathBuf::from(&watch_filename))) { Some(path) => { let save_path = path.clone(); + let mut changed = false; if path.exists() { match load_tree(&path) { Some(loaded_tree) => { @@ -28,6 +29,7 @@ pub(crate) fn process_group_watch(cfg: &Config, target_name: &str, pl: &Playlist let added_difference: BTreeSet = new_tree.difference(&loaded_tree).cloned().collect(); let removed_difference: BTreeSet = loaded_tree.difference(&new_tree).cloned().collect(); if !added_difference.is_empty() || !removed_difference.is_empty() { + changed = true; handle_watch_notification(cfg, added_difference, removed_difference, target_name, &pl.title); } } @@ -36,10 +38,12 @@ pub(crate) fn process_group_watch(cfg: &Config, target_name: &str, pl: &Playlist } } } - match save_tree(&save_path, new_tree) { - Ok(_) => {} - Err(err) => { - error!("failed to write watch_file {}: {}", &save_path.to_str().unwrap(), err) + if changed { + match save_tree(&save_path, new_tree) { + Ok(_) => {} + Err(err) => { + error!("failed to write watch_file {}: {}", &save_path.to_str().unwrap(), err) + } } } }