diff --git a/CHANGELOG.md b/CHANGELOG.md index ee52ebfc1..1c547f2d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ - `Strm` output has an additional option `strm_props`. These props are written to the strm file. If `kodi_style` set to `true` the property `#KODIPROP:seekable=true|false` is added. If `strm_props` is not given `#KODIPROP:inputstream=inputstream.ffmpeg`, `"#KODIPROP:http-reconnect=true` are set too for `kody_style`. - Fixed xtream affix-processed output. +- `log_sanitize_sensitive_info` added to `config.yml`. Default is `true`. # 2.1.1 (2025-01-19) - added new path `/status` which is an alias to `healthcheck` diff --git a/README.md b/README.md index 375fda9e1..350bb00b3 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,7 @@ Top level entries in the config files are: * `web_ui_enabled` _optional_ * `web_auth` _optional_ * `reverse_proxy` _optional_ +* `log_sanitize_sensitive_info` _optional_, default true ### 1.1. `threads` If you are running on a cpu which has multiple cores, you can set for example `threads: 2` to run two threads. diff --git a/frontend/src/component/main-config-view/main-config-view.tsx b/frontend/src/component/main-config-view/main-config-view.tsx index ed1d656a5..2fd82c633 100644 --- a/frontend/src/component/main-config-view/main-config-view.tsx +++ b/frontend/src/component/main-config-view/main-config-view.tsx @@ -107,6 +107,7 @@ export default function MainConfigView(props: MainConfigViewProps) { threads: mainConfig.threads, messaging: cfgMessaging, video: cfgVideo, + log_sanitize_sensitive_info: mainConfig.log_sanitize_sensitive_info, update_on_boot: mainConfig.update_on_boot, web_ui_enabled: mainConfig.web_ui_enabled, web_auth: mainConfig.web_auth, diff --git a/frontend/src/model/server-config.ts b/frontend/src/model/server-config.ts index a9a542cd6..3579f4d3e 100644 --- a/frontend/src/model/server-config.ts +++ b/frontend/src/model/server-config.ts @@ -193,6 +193,7 @@ export interface ServerMainConfig { schedules?: Schedule[]; messaging?: MessagingConfig; video?: VideoConfig; + log_sanitize_sensitive_info: boolean, update_on_boot: boolean, web_ui_enabled: boolean, web_auth?: WebAuthConfig, diff --git a/src/api/api_utils.rs b/src/api/api_utils.rs index 559655c83..687075a9b 100644 --- a/src/api/api_utils.rs +++ b/src/api/api_utils.rs @@ -8,14 +8,12 @@ use crate::model::api_proxy::ProxyUserCredentials; use crate::model::config::{ConfigInput, ConfigTarget}; use crate::model::playlist::PlaylistItemType; use crate::utils::request_utils; -use crate::utils::request_utils::mask_sensitive_info; +use crate::utils::request_utils::sanitize_sensitive_info; use actix_files::NamedFile; use actix_web::body::{BodyStream}; -use actix_web::http::header::DATE; use actix_web::http::header::{HeaderValue, CACHE_CONTROL}; use actix_web::{HttpRequest, HttpResponse}; use bytes::Bytes; -use chrono::Utc; use log::{error, log_enabled, trace}; use std::collections::HashMap; use std::path::{Path}; @@ -83,7 +81,7 @@ async fn create_broadcast_stream( pub async fn stream_response(app_state: &AppState, stream_url: &str, req: &HttpRequest, input: Option<&ConfigInput>, item_type: PlaylistItemType, target: &ConfigTarget) -> HttpResponse { - if log_enabled!(log::Level::Trace) { trace!("Try to open stream {}", mask_sensitive_info(stream_url)); } + if log_enabled!(log::Level::Trace) { trace!("Try to open stream {}", sanitize_sensitive_info(stream_url)); } let share_stream = is_stream_share_enabled(item_type, target); if share_stream { @@ -132,18 +130,15 @@ pub async fn stream_response(app_state: &AppState, stream_url: &str, }; } } - error!("Cant open stream {}", mask_sensitive_info(stream_url)); + error!("Cant open stream {}", sanitize_sensitive_info(stream_url)); HttpResponse::BadRequest().finish() } async fn shared_stream_response(app_state: &AppState, stream_url: &str) -> Option { if let Some(stream) = create_broadcast_stream(app_state, stream_url).await { - debug_if_enabled!("Using shared channel {}", mask_sensitive_info(stream_url)); + debug_if_enabled!("Using shared channel {}", sanitize_sensitive_info(stream_url)); if let Some((headers,_)) = app_state.shared_streams.lock().await.get(stream_url) { let mut response_builder = get_stream_response_with_headers(Some((headers.clone(), StatusCode::OK)), stream_url); - let current_date = Utc::now().format("%a, %d %b %Y %H:%M:%S GMT").to_string(); - response_builder.insert_header((DATE, current_date.as_bytes())); - // response_builder.insert_header((ACCEPT_RANGES, "bytes".as_bytes())); return Some(response_builder.body(BodyStream::new(stream))); } } @@ -192,12 +187,12 @@ pub async fn resource_response(app_state: &AppState, resource_url: &str, req: &H let mut guard = cache.lock().await; if let Some(resource_path) = guard.get_content(resource_url).await { if let Ok(named_file) = NamedFile::open_async(resource_path).await { - debug_if_enabled!("Cached resource {}", mask_sensitive_info(resource_url)); + debug_if_enabled!("Cached resource {}", sanitize_sensitive_info(resource_url)); return named_file.into_response(req); } } } - debug_if_enabled!("Try to fetch resource {}", mask_sensitive_info(resource_url)); + debug_if_enabled!("Try to fetch resource {}", sanitize_sensitive_info(resource_url)); if let Ok(url) = Url::parse(resource_url) { let client = request_utils::get_client_request(&app_state.http_client, input.map(|i| &i.headers), &url, Some(&req_headers)); match client.send().await { @@ -224,14 +219,14 @@ pub async fn resource_response(app_state: &AppState, resource_url: &str, req: &H } return response_builder.body(BodyStream::new(byte_stream)); } - debug_if_enabled!("Failed to open resource got status {} for {}", status, mask_sensitive_info(resource_url)); + debug_if_enabled!("Failed to open resource got status {} for {}", status, sanitize_sensitive_info(resource_url)); } Err(err) => { - error!("Received failure from server {}: {}", mask_sensitive_info(resource_url), err); + error!("Received failure from server {}: {}", sanitize_sensitive_info(resource_url), err); } } } else { - error!("Url is malformed {}", mask_sensitive_info(resource_url)); + error!("Url is malformed {}", sanitize_sensitive_info(resource_url)); } HttpResponse::BadRequest().finish() } diff --git a/src/api/m3u_api.rs b/src/api/m3u_api.rs index ab66251ec..f16be0144 100644 --- a/src/api/m3u_api.rs +++ b/src/api/m3u_api.rs @@ -12,7 +12,7 @@ use crate::model::playlist::FieldGetAccessor; use crate::repository::m3u_playlist_iterator::{M3U_STREAM_PATH, M3U_RESOURCE_PATH}; use crate::repository::m3u_repository::{m3u_get_file_paths, m3u_get_item_for_stream_id, m3u_load_rewrite_playlist}; use crate::repository::storage::get_target_storage_path; -use crate::utils::request_utils::mask_sensitive_info; +use crate::utils::request_utils::sanitize_sensitive_info; async fn m3u_api( api_req: &UserApiRequest, @@ -29,7 +29,7 @@ async fn m3u_api( .streaming(content_stream) } Err(err) => { - error!("{}", mask_sensitive_info(err.to_string().as_str())); + error!("{}", sanitize_sensitive_info(err.to_string().as_str())); HttpResponse::NoContent().finish() } } @@ -73,14 +73,14 @@ async fn m3u_api_stream( let m3u_item = match m3u_get_item_for_stream_id(&app_state.config, m3u_stream_id, &m3u_path, &idx_path).await { Ok(item) => item, Err(err) => { - error!("Failed to get m3u url: {}", mask_sensitive_info(err.to_string().as_str())); + error!("Failed to get m3u url: {}", sanitize_sensitive_info(err.to_string().as_str())); return HttpResponse::BadRequest().finish(); } }; if user.proxy == ProxyType::Redirect { let stream_url = m3u_item.url; - debug!("Redirecting stream request to {}", mask_sensitive_info(&stream_url)); + debug!("Redirecting stream request to {}", sanitize_sensitive_info(&stream_url)); return HttpResponse::Found().insert_header(("Location", stream_url.to_string())).finish(); } @@ -110,7 +110,7 @@ async fn m3u_api_resource( let m3u_item = match m3u_get_item_for_stream_id(&app_state.config, m3u_stream_id, &m3u_path, &idx_path).await { Ok(item) => item, Err(err) => { - error!("Failed to get m3u url: {}", mask_sensitive_info(err.to_string().as_str())); + error!("Failed to get m3u url: {}", sanitize_sensitive_info(err.to_string().as_str())); return HttpResponse::BadRequest().finish(); } }; @@ -120,7 +120,7 @@ async fn m3u_api_resource( None => HttpResponse::NotFound().finish(), Some(url) => { if user.proxy == ProxyType::Redirect { - debug!("Redirecting stream request to {}", mask_sensitive_info(&url)); + debug!("Redirecting stream request to {}", sanitize_sensitive_info(&url)); HttpResponse::Found().insert_header(("Location", url.as_str())).finish() } else { resource_response(&app_state, url.as_str(), &req, None).await diff --git a/src/api/model/client_stream.rs b/src/api/model/client_stream.rs index 9bd5079a8..b4ce7b807 100644 --- a/src/api/model/client_stream.rs +++ b/src/api/model/client_stream.rs @@ -8,7 +8,7 @@ use log::debug; use futures::{Stream}; use crate::api::model::stream_error::StreamError; use crate::utils::atomic_once_flag::AtomicOnceFlag; -use crate::utils::request_utils::mask_sensitive_info; +use crate::utils::request_utils::sanitize_sensitive_info; /// This stream counts the send bytes for reconnecting to the actual position and /// sets the `close_signal` if the client drops the connection. @@ -57,7 +57,7 @@ impl Stream for ClientStream { impl Drop for ClientStream { fn drop(&mut self) { - debug!("Client disconnected {}", mask_sensitive_info(&self.url)); + debug!("Client disconnected {}", sanitize_sensitive_info(&self.url)); self.close_signal.notify(); } } \ No newline at end of file diff --git a/src/api/model/model_utils.rs b/src/api/model/model_utils.rs index a7ca79f38..c0af9c3b2 100644 --- a/src/api/model/model_utils.rs +++ b/src/api/model/model_utils.rs @@ -4,7 +4,7 @@ use actix_web::{HttpResponseBuilder}; use reqwest::{Response, StatusCode}; use std::collections::{HashSet}; use std::str::FromStr; -use crate::utils::request_utils::mask_sensitive_info; +use crate::utils::request_utils::sanitize_sensitive_info; const MEDIA_STREAM_HEADERS: &[&str] = &["accept", "content-type", "content-length", "connection", "accept-ranges", "content-range", "vary", "transfer-encoding", "access-control-allow-origin", "access-control-allow-credentials", "icy-metadata"]; @@ -52,7 +52,7 @@ pub fn get_stream_response_with_headers(custom: Option<(Vec<(String, String)>, S } let mut response_builder = actix_web::HttpResponse::build(actix_web::http::StatusCode::from_u16(status).unwrap()); - debug_if_enabled!("Responding stream {} with status {status}, headers {headers:?}", mask_sensitive_info(stream_url)); + debug_if_enabled!("Responding stream {} with status {status}, headers {headers:?}", sanitize_sensitive_info(stream_url)); for header in headers { response_builder.insert_header(header); } diff --git a/src/api/model/provider_stream.rs b/src/api/model/provider_stream.rs index d54f8e78d..bd1c39a49 100644 --- a/src/api/model/provider_stream.rs +++ b/src/api/model/provider_stream.rs @@ -2,7 +2,7 @@ use crate::api::api_utils::get_headers_from_request; use crate::api::model::provider_stream_factory::{create_provider_stream, BufferStreamOptions}; use crate::debug_if_enabled; use crate::model::config::ConfigInput; -use crate::utils::request_utils::{get_request_headers, mask_sensitive_info}; +use crate::utils::request_utils::{get_request_headers, sanitize_sensitive_info}; use actix_web::{HttpRequest}; use bytes::Bytes; use futures::stream::BoxStream; @@ -39,7 +39,7 @@ pub async fn get_provider_pipe_stream(http_client: &Arc, } } Err(err) => { - let masked_url = mask_sensitive_info(stream_url.as_str()); + let masked_url = sanitize_sensitive_info(stream_url.as_str()); error!("Failed to open stream {masked_url} {err}"); (None, None) } diff --git a/src/api/model/provider_stream_factory.rs b/src/api/model/provider_stream_factory.rs index a8a955b81..6a4ad84ca 100644 --- a/src/api/model/provider_stream_factory.rs +++ b/src/api/model/provider_stream_factory.rs @@ -6,7 +6,7 @@ use crate::api::model::stream_error::StreamError; use crate::debug_if_enabled; use crate::model::config::ConfigInput; use crate::model::playlist::PlaylistItemType; -use crate::utils::request_utils::{get_request_headers, mask_sensitive_info}; +use crate::utils::request_utils::{get_request_headers, sanitize_sensitive_info}; use actix_web::HttpRequest; use bytes::Bytes; use futures::stream::{self, BoxStream}; @@ -201,7 +201,7 @@ async fn provider_request(request_client: Arc, initial_info: bo // We need some header information from the provider, we extract the necessary headers and forward them to the client debug_if_enabled!("Provider response status: '{}' headers: {:?}", response.status(), response.headers_mut()); let response_headers: Vec<(String, String)> = get_response_headers(&mut response); - // debug!("First headers {headers:?} {} {}", mask_sensitive_info(url.as_str())); + // debug!("First headers {headers:?} {} {}", sanitize_sensitive_info(url.as_str())); Some((response_headers, response.status())) } else { None @@ -223,7 +223,7 @@ async fn stream_provider(client: Arc, stream_options: ProviderS let headers = stream_options.get_headers(); while stream_options.should_continue() { - debug_if_enabled!("Reconnecting stream {}", mask_sensitive_info(url.as_str())); + debug_if_enabled!("Reconnecting stream {}", sanitize_sensitive_info(url.as_str())); let (client, _) = prepare_client(&client, url, headers, range_start); match client.send().await { Ok(response) => { @@ -251,7 +251,7 @@ async fn stream_provider(client: Arc, stream_options: ProviderS } actix_web::rt::time::sleep(Duration::from_millis(100)).await; } - debug_if_enabled!("Stopped seconnecting stream {}", mask_sensitive_info(url.as_str())); + debug_if_enabled!("Stopped seconnecting stream {}", sanitize_sensitive_info(url.as_str())); None } @@ -265,12 +265,12 @@ async fn get_initial_stream(client: Arc, stream_options: &Provi Ok(Some(value)) => return Some(value), Ok(None) => { if connect_err > ERR_MAX_RETRY_COUNT { - warn!("The stream could be unavailable. {}", mask_sensitive_info(stream_options.get_url().as_str())); + warn!("The stream could be unavailable. {}", sanitize_sensitive_info(stream_options.get_url().as_str())); } } Err(status) => { if connect_err > ERR_MAX_RETRY_COUNT { - warn!("The stream could be unavailable. ({status}) {}", mask_sensitive_info(stream_options.get_url().as_str())); + warn!("The stream could be unavailable. ({status}) {}", sanitize_sensitive_info(stream_options.get_url().as_str())); } } }; @@ -278,7 +278,7 @@ async fn get_initial_stream(client: Arc, stream_options: &Provi break; } if start.elapsed().as_secs() > RETRY_SECONDS { - warn!("The stream could be unavailable. Giving up after {RETRY_SECONDS} seconds. {}", mask_sensitive_info(stream_options.get_url().as_str())); + warn!("The stream could be unavailable. Giving up after {RETRY_SECONDS} seconds. {}", sanitize_sensitive_info(stream_options.get_url().as_str())); break; } connect_err += 1; diff --git a/src/api/model/shared_stream.rs b/src/api/model/shared_stream.rs index a2bc46cf1..28814209b 100644 --- a/src/api/model/shared_stream.rs +++ b/src/api/model/shared_stream.rs @@ -1,7 +1,7 @@ use crate::api::model::app_state::AppState; use crate::api::model::provider_stream_factory::STREAM_QUEUE_SIZE; use crate::debug_if_enabled; -use crate::utils::request_utils::mask_sensitive_info; +use crate::utils::request_utils::sanitize_sensitive_info; use bytes::Bytes; use std::sync::Arc; use std::time::Duration; @@ -63,7 +63,7 @@ impl SharedStream { } actix_web::rt::time::sleep(Duration::from_millis(20)).await; } else { - debug_if_enabled!("No active subscribers. Closing stream {}", mask_sensitive_info(&streaming_url)); + debug_if_enabled!("No active subscribers. Closing stream {}", sanitize_sensitive_info(&streaming_url)); // Cleanup for removing unused shared streams let mut shared_streams = shared_streams_map.lock().await; shared_streams.remove(&streaming_url); diff --git a/src/api/v1_api.rs b/src/api/v1_api.rs index 23a871041..8f0d71e33 100644 --- a/src/api/v1_api.rs +++ b/src/api/v1_api.rs @@ -15,7 +15,7 @@ use crate::m3u_filter_error::M3uFilterError; use crate::model::api_proxy::{ApiProxyConfig, ApiProxyServerInfo, ProxyUserCredentials, TargetUser}; use crate::model::config::{validate_targets, Config, ConfigDto, ConfigInput, ConfigInputOptions, ConfigSource, ConfigTarget, InputType}; use crate::processing::playlist_processor; -use crate::utils::request_utils::mask_sensitive_info; +use crate::utils::request_utils::sanitize_sensitive_info; use crate::utils::{config_reader, download}; fn intern_save_config_api_proxy(backup_dir: &str, api_proxy: &ApiProxyConfig, file_path: &str) -> Option { @@ -107,7 +107,7 @@ async fn playlist_update( HttpResponse::Ok().finish() } Err(err) => { - error!("Failed playlist update {}", mask_sensitive_info(err.to_string().as_str())); + error!("Failed playlist update {}", sanitize_sensitive_info(err.to_string().as_str())); HttpResponse::BadRequest().json(json!({"error": err.to_string()})) } } diff --git a/src/api/xtream_api.rs b/src/api/xtream_api.rs index df8610890..5e4c5de4a 100644 --- a/src/api/xtream_api.rs +++ b/src/api/xtream_api.rs @@ -29,7 +29,7 @@ use crate::repository::target_id_mapping::TargetIdMapping; use crate::repository::xtream_repository; use crate::repository::xtream_repository::{TAG_EPISODES, TAG_INFO_DATA, TAG_SEASONS_DATA}; use crate::utils::json_utils::get_u32_from_serde_value; -use crate::utils::request_utils::{extract_extension_from_url, mask_sensitive_info}; +use crate::utils::request_utils::{extract_extension_from_url, sanitize_sensitive_info}; use crate::utils::{download, json_utils, request_utils}; use crate::{debug_if_enabled, info_err}; @@ -193,12 +193,12 @@ async fn xtream_player_api_stream( if pli.item_type == PlaylistItemType::LiveHls { let stream_url = pli.url.to_string(); - debug_if_enabled!("Redirecting stream request to {}", mask_sensitive_info(&stream_url)); + debug_if_enabled!("Redirecting stream request to {}", sanitize_sensitive_info(&stream_url)); return HttpResponse::Found().insert_header(("Location", stream_url)).finish(); } if user.proxy == ProxyType::Redirect { - debug_if_enabled!("Redirecting stream request to {}", mask_sensitive_info(&pli.url)); + debug_if_enabled!("Redirecting stream request to {}", sanitize_sensitive_info(&pli.url)); return HttpResponse::Found().insert_header(("Location", pli.url.as_str())).finish(); } @@ -215,7 +215,7 @@ async fn xtream_player_api_stream( stream_req.context.to_string().as_str(), &query_path, pli.url.as_str()), true, format!("Cant find stream url for target {target_name}, context {}, stream_id {virtual_id}", stream_req.context)); - debug_if_enabled!("Streaming stream request from {}", mask_sensitive_info(&stream_url)); + debug_if_enabled!("Streaming stream request from {}", sanitize_sensitive_info(&stream_url)); stream_response(app_state, &stream_url, req, Some(input), pli.item_type, target).await } @@ -371,10 +371,10 @@ async fn xtream_player_api_resource( None => HttpResponse::NotFound().finish(), Some(url) => { if user.proxy == ProxyType::Redirect { - debug!("Redirecting resource request to {}", mask_sensitive_info(&url)); + debug!("Redirecting resource request to {}", sanitize_sensitive_info(&url)); HttpResponse::Found().insert_header(("Location", url.as_str())).finish() } else { - debug_if_enabled!("Resource request to {}", mask_sensitive_info(&url)); + debug_if_enabled!("Resource request to {}", sanitize_sensitive_info(&url)); resource_response(app_state, url.as_str(), req, None).await } } @@ -496,7 +496,7 @@ async fn xtream_get_short_epg(app_state: &AppState, user: &ProxyUserCredentials, return match request_utils::download_text_content(Arc::clone(&app_state.http_client), input, info_url.as_str(), None).await { Ok(content) => HttpResponse::Ok().content_type(mime::APPLICATION_JSON).body(content), Err(err) => { - error!("Failed to download epg {}", mask_sensitive_info(err.to_string().as_str())); + error!("Failed to download epg {}", sanitize_sensitive_info(err.to_string().as_str())); HttpResponse::NoContent().finish() } }; diff --git a/src/main.rs b/src/main.rs index a96e83e10..4a9e26563 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,6 +18,8 @@ use crate::utils::{config_reader, file_utils}; use clap::Parser; use env_logger::Builder; use log::{error, info, LevelFilter}; +use crate::utils::request_utils::set_sanitize_sensitive_info; + mod api; mod auth; mod filter; @@ -110,6 +112,8 @@ fn main() { let sources_file: String = args.source_file.unwrap_or_else(|| file_utils::get_default_sources_file_path(&config_path)); let mut cfg = config_reader::read_config(config_path.as_str(), config_file.as_str(), sources_file.as_str()).unwrap_or_else(|err| exit!("{}", err)); + set_sanitize_sensitive_info(cfg.log_sanitize_sensitive_info); + if args.genpwd { match generate_password() { Ok(pwd) => println!("{pwd}"), diff --git a/src/model/config.rs b/src/model/config.rs index c60381983..3367aed09 100644 --- a/src/model/config.rs +++ b/src/model/config.rs @@ -780,6 +780,8 @@ pub struct ConfigDto { pub schedules: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] pub messaging: Option, + #[serde(default = "default_as_true")] + pub log_sanitize_sensitive_info: bool, #[serde(default)] pub update_on_boot: bool, #[serde(default = "default_as_true")] @@ -995,6 +997,8 @@ pub struct Config { pub video: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub schedules: Option>, + #[serde(default = "default_as_true")] + pub log_sanitize_sensitive_info: bool, #[serde(default)] pub update_on_boot: bool, #[serde(default = "default_as_true")] diff --git a/src/processing/playlist_processor.rs b/src/processing/playlist_processor.rs index 043729f14..170ffe077 100644 --- a/src/processing/playlist_processor.rs +++ b/src/processing/playlist_processor.rs @@ -31,7 +31,7 @@ use crate::processing::xtream_processor_vod::playlist_resolve_vod; use crate::repository::playlist_repository::persist_playlist; use crate::utils::default_utils::default_as_default; use crate::utils::download; -use crate::utils::request_utils::mask_sensitive_info; +use crate::utils::request_utils::sanitize_sensitive_info; use crate::{debug_if_enabled, get_errors_notify_message, model::config, notify_err, Config}; fn is_valid(pli: &PlaylistItem, target: &ConfigTarget) -> bool { @@ -318,7 +318,7 @@ async fn process_source(client: Arc, cfg: Arc, source_i let channel_count = playlistgroups.iter() .map(|group| group.channels.len()) .sum(); - let input_name = input.name.as_ref().map_or_else(|| mask_sensitive_info(input.url.as_str()), std::string::ToString::to_string); + let input_name = input.name.as_ref().map_or_else(|| sanitize_sensitive_info(input.url.as_str()), std::string::ToString::to_string); if playlistgroups.is_empty() { info!("Source is empty {input_name}"); errors.push(notify_err!(format!("Source is empty {input_name}"))); diff --git a/src/utils/request_utils.rs b/src/utils/request_utils.rs index 5cd582786..0a8f964a1 100644 --- a/src/utils/request_utils.rs +++ b/src/utils/request_utils.rs @@ -4,6 +4,7 @@ use std::fs; use std::fs::File; use std::io::{BufWriter, Error, ErrorKind, Read, Write}; use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::LazyLock; use std::time::Instant; @@ -29,12 +30,12 @@ pub const fn bytes_to_megabytes(bytes: u64) -> u64 { } pub async fn get_input_text_content_as_file(client: Arc, input: &ConfigInput, working_dir: &str, url_str: &str, persist_filepath: Option) -> Result { - debug_if_enabled!("getting input text content working_dir: {}, url: {}", working_dir, mask_sensitive_info(url_str)); + debug_if_enabled!("getting input text content working_dir: {}, url: {}", working_dir, sanitize_sensitive_info(url_str)); if url_str.parse::().is_ok() { match download_text_content_as_file(client, input, url_str, working_dir, persist_filepath).await { Ok(content) => Ok(content), Err(e) => { - error!("cant download input url: {} => {}", mask_sensitive_info(url_str), mask_sensitive_info(e.to_string().as_str())); + error!("cant download input url: {} => {}", sanitize_sensitive_info(url_str), sanitize_sensitive_info(e.to_string().as_str())); create_m3u_filter_error_result!(M3uFilterErrorKind::Notify, "Failed to download") } } @@ -66,7 +67,7 @@ pub async fn get_input_text_content_as_file(client: Arc, input: }; result.map_or_else(|| { - let msg = format!("cant read input url: {}", mask_sensitive_info(url_str)); + let msg = format!("cant read input url: {}", sanitize_sensitive_info(url_str)); error!("{}", msg); create_m3u_filter_error_result!(M3uFilterErrorKind::Notify, "{}", msg) }, Ok) @@ -75,13 +76,13 @@ pub async fn get_input_text_content_as_file(client: Arc, input: pub async fn get_input_text_content(client: Arc, input: &ConfigInput, working_dir: &str, url_str: &str, persist_filepath: Option) -> Result { - debug_if_enabled!("getting input text content working_dir: {}, url: {}", working_dir, mask_sensitive_info(url_str)); + debug_if_enabled!("getting input text content working_dir: {}, url: {}", working_dir, sanitize_sensitive_info(url_str)); if url_str.parse::().is_ok() { match download_text_content(client, input, url_str, persist_filepath).await { Ok(content) => Ok(content), Err(e) => { - error!("cant download input url: {} => {}", mask_sensitive_info(url_str), mask_sensitive_info(e.to_string().as_str())); + error!("cant download input url: {} => {}", sanitize_sensitive_info(url_str), sanitize_sensitive_info(e.to_string().as_str())); create_m3u_filter_error_result!(M3uFilterErrorKind::Notify, "Failed to download") } } @@ -113,7 +114,7 @@ pub async fn get_input_text_content(client: Arc, input: &Config None => None }; result.map_or_else(|| { - let msg = format!("cant read input url: {}", mask_sensitive_info(url_str)); + let msg = format!("cant read input url: {}", sanitize_sensitive_info(url_str)); error!("{}", msg); create_m3u_filter_error_result!(M3uFilterErrorKind::Notify, "{}", msg) }, Ok) @@ -206,10 +207,10 @@ async fn get_remote_content_as_file(client: Arc, input: &Config debug!("File downloaded successfully to {file_path:?}, took:{}", format_elapsed_time(elapsed)); Ok(file_path.to_path_buf()) } else { - Err(str_to_io_error(&format!("Request failed with status {} {}", response.status(), mask_sensitive_info(url.as_str())))) + Err(str_to_io_error(&format!("Request failed with status {} {}", response.status(), sanitize_sensitive_info(url.as_str())))) } } - Err(err) => Err(str_to_io_error(&format!("Request failed: {} {err}", mask_sensitive_info(url.as_str())))), + Err(err) => Err(str_to_io_error(&format!("Request failed: {} {err}", sanitize_sensitive_info(url.as_str())))), } } @@ -256,30 +257,30 @@ async fn get_remote_content(client: Arc, input: &ConfigInput, u if decode_buffer.is_empty() { match String::from_utf8(bytes.to_vec()) { Ok(decoded_content) => { - debug_if_enabled!("Request took:{} {}", format_elapsed_time(start_time.elapsed().as_secs()), mask_sensitive_info(url.as_str())); + debug_if_enabled!("Request took:{} {}", format_elapsed_time(start_time.elapsed().as_secs()), sanitize_sensitive_info(url.as_str())); Ok(decoded_content) } Err(err) => Err(str_to_io_error(&format!("failed to plain text content {err}"))) } } else { - debug_if_enabled!("Request took:{}, {}", format_elapsed_time(start_time.elapsed().as_secs()), mask_sensitive_info(url.as_str())); + debug_if_enabled!("Request took:{}, {}", format_elapsed_time(start_time.elapsed().as_secs()), sanitize_sensitive_info(url.as_str())); Ok(decode_buffer) } } - Err(err) => Err(str_to_io_error(&format!("failed to read response {} {err}", mask_sensitive_info(url.as_str())))) + Err(err) => Err(str_to_io_error(&format!("failed to read response {} {err}", sanitize_sensitive_info(url.as_str())))) } } else { - Err(str_to_io_error(&format!("Request failed with status {} {}", response.status(), mask_sensitive_info(url.as_str())))) + Err(str_to_io_error(&format!("Request failed with status {} {}", response.status(), sanitize_sensitive_info(url.as_str())))) } } - Err(err) => Err(str_to_io_error(&format!("Request failed {} {err}", mask_sensitive_info(url.as_str())))) + Err(err) => Err(str_to_io_error(&format!("Request failed {} {err}", sanitize_sensitive_info(url.as_str())))) } } pub async fn download_text_content_as_file(client: Arc, input: &ConfigInput, url_str: &str, working_dir: &str, persist_filepath: Option) -> Result { if let Ok(url) = url_str.parse::() { if url.scheme() == "file" { - url.to_file_path().map_or_else(|()| Err(Error::new(ErrorKind::Unsupported, format!("Unknown file {}", mask_sensitive_info(url_str)))), |file_path| if file_path.exists() { + url.to_file_path().map_or_else(|()| Err(Error::new(ErrorKind::Unsupported, format!("Unknown file {}", sanitize_sensitive_info(url_str)))), |file_path| if file_path.exists() { Ok(file_path) } else { Err(Error::new(ErrorKind::NotFound, format!("Unknown file {file_path:?}"))) @@ -297,7 +298,7 @@ pub async fn download_text_content_as_file(client: Arc, input: } } } else { - Err(std::io::Error::new(ErrorKind::Unsupported, format!("Malformed URL {}", mask_sensitive_info(url_str)))) + Err(std::io::Error::new(ErrorKind::Unsupported, format!("Malformed URL {}", sanitize_sensitive_info(url_str)))) } } @@ -305,7 +306,7 @@ pub async fn download_text_content_as_file(client: Arc, input: pub async fn download_text_content(client: Arc, input: &ConfigInput, url_str: &str, persist_filepath: Option) -> Result { if let Ok(url) = url_str.parse::() { let result = if url.scheme() == "file" { - url.to_file_path().map_or_else(|()| Err(str_to_io_error(&format!("Unknown file {}", mask_sensitive_info(url_str)))), |file_path| get_local_file_content(&file_path)) + url.to_file_path().map_or_else(|()| Err(str_to_io_error(&format!("Unknown file {}", sanitize_sensitive_info(url_str)))), |file_path| get_local_file_content(&file_path)) } else { get_remote_content(client, input, &url).await }; @@ -319,12 +320,12 @@ pub async fn download_text_content(client: Arc, input: &ConfigI Err(err) => Err(err) } } else { - Err(str_to_io_error(&format!("Malformed URL {}", mask_sensitive_info(url_str)))) + Err(str_to_io_error(&format!("Malformed URL {}", sanitize_sensitive_info(url_str)))) } } async fn download_json_content(client: Arc, input: &ConfigInput, url: &str, persist_filepath: Option) -> Result { - debug_if_enabled!("downloading json content from {}", mask_sensitive_info(url)); + debug_if_enabled!("downloading json content from {}", sanitize_sensitive_info(url)); match download_text_content(client, input, url, persist_filepath).await { Ok(content) => { match serde_json::from_str::(&content) { @@ -339,7 +340,7 @@ async fn download_json_content(client: Arc, input: &ConfigInput pub async fn get_input_json_content(client: Arc, input: &ConfigInput, url: &str, persist_filepath: Option) -> Result { match download_json_content(client, input, url, persist_filepath).await { Ok(content) => Ok(content), - Err(e) => create_m3u_filter_error_result!(M3uFilterErrorKind::Notify, "cant download input url: {} => {}", mask_sensitive_info(url), mask_sensitive_info(e.to_string().as_str())) + Err(e) => create_m3u_filter_error_result!(M3uFilterErrorKind::Notify, "cant download input url: {} => {}", sanitize_sensitive_info(url), sanitize_sensitive_info(e.to_string().as_str())) } } // @@ -360,13 +361,22 @@ static PASSWORD_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"(p static TOKEN_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"(token=)[^&]*").unwrap()); static STREAM_URL: LazyLock = LazyLock::new(|| Regex::new(r"(.*://).*/(live|video|movie|series|m3u-stream)/\w+/\w+").unwrap()); -pub fn mask_sensitive_info(query: &str) -> String { - // Replace with "***" - let masked_query = USERNAME_REGEX.replace_all(query, "$1***"); - let masked_query = PASSWORD_REGEX.replace_all(&masked_query, "$1***"); - let masked_query = TOKEN_REGEX.replace_all(&masked_query, "$1***"); - let masked_query =STREAM_URL.replace_all(&masked_query, "$1***/$2/***"); - masked_query.to_string() +static SANITIZE_SENSITIVE_INFO: LazyLock = LazyLock::new(|| AtomicBool::new(true)); + +pub fn set_sanitize_sensitive_info(value: bool) { + SANITIZE_SENSITIVE_INFO.store(value, Ordering::Relaxed); +} +pub fn sanitize_sensitive_info(query: &str) -> String { + if SANITIZE_SENSITIVE_INFO.load(Ordering::Relaxed) { + // Replace with "***" + let masked_query = USERNAME_REGEX.replace_all(query, "$1***"); + let masked_query = PASSWORD_REGEX.replace_all(&masked_query, "$1***"); + let masked_query = TOKEN_REGEX.replace_all(&masked_query, "$1***"); + let masked_query = STREAM_URL.replace_all(&masked_query, "$1***/$2/***"); + masked_query.to_string() + } else { + query.to_string() + } } pub fn extract_extension_from_url(url: &str) -> Option<&str> {