2025-12-12 14:35:00 +01:00
use futures ::{ StreamExt , TryStreamExt };
use log ::{ debug , error , log_enabled , trace , Level };
use reqwest ::header ::CONTENT_ENCODING ;
use reqwest ::header ::{ HeaderMap , HeaderName , HeaderValue };
2023-12-02 20:47:26 +01:00
use std ::collections ::{ HashMap , HashSet };
2025-11-19 15:07:30 -06:00
use std ::io ::{ Error , ErrorKind };
2024-11-02 16:44:38 +01:00
use std ::path ::{ Path , PathBuf };
2025-09-30 10:54:30 +02:00
use std ::pin ::Pin ;
2025-12-12 14:35:00 +01:00
use std ::sync ::Arc ;
2025-06-13 14:03:18 +02:00
use std ::time ::{ Duration , Instant };
2025-11-19 15:07:30 -06:00
use tokio ::fs ::File ;
2025-12-11 15:27:48 +01:00
use tokio ::io ::{ AsyncBufReadExt , AsyncRead , AsyncReadExt , AsyncWriteExt };
2025-09-30 10:54:30 +02:00
use tokio_util ::io ::StreamReader ;
2024-09-10 20:05:29 +02:00
use url ::Url ;
2021-09-29 20:48:32 +02:00
2025-10-31 17:04:35 +01:00
use crate ::model ::{ format_elapsed_time , AppConfig , InputSource , ReverseProxyDisabledHeaderConfig };
2025-12-12 14:35:00 +01:00
use crate ::model ::ConfigInput ;
use crate ::repository ::storage ::get_input_storage_path ;
2025-04-17 12:11:51 +02:00
use crate ::repository ::storage_const ;
use crate ::utils ::compression ::compression_utils ::{ is_deflate , is_gzip };
2025-12-11 19:50:31 +01:00
use crate ::utils ::{ async_file_reader , async_file_writer , debug_if_enabled , IO_BUFFER_SIZE };
2025-05-14 18:23:22 +02:00
use crate ::utils ::{ get_file_path , persist_file };
2025-12-12 14:35:00 +01:00
use shared ::error ::create_tuliprox_error_result ;
use shared ::error ::{ str_to_io_error , TuliproxError , TuliproxErrorKind };
use shared ::model ::{ InputFetchMethod , DEFAULT_USER_AGENT };
use shared ::utils ::{ filter_request_header , human_readable_byte_size , sanitize_sensitive_info , short_hash , ENCODING_DEFLATE , ENCODING_GZIP };
use crate ::api ::model ::persist_pipe_stream ::tee_dyn_reader ;
2025-06-24 01:06:25 +02:00
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum MimeCategory {
Unknown ,
Video ,
M3U8 ,
Image ,
Json ,
Xml ,
Text ,
Unclassified ,
}
pub fn classify_content_type ( headers : & [( String , String )]) -> MimeCategory {
headers . iter ()
. find_map ( | ( k , v ) | {
( k == axum ::http ::header ::CONTENT_TYPE . as_str ()). then_some ( v )
})
. map_or ( MimeCategory ::Unknown , | v | match v . to_lowercase (). as_str () {
v if v . starts_with ( "video/" ) || v == "application/octet-stream" => MimeCategory ::Video ,
v if v . contains ( "mpegurl" ) => MimeCategory ::M3U8 ,
v if v . starts_with ( "image/" ) => MimeCategory ::Image ,
v if v . starts_with ( "application/json" ) || v . ends_with ( "+json" ) => MimeCategory ::Json ,
v if v . starts_with ( "application/xml" ) || v . ends_with ( "+xml" ) || v == "text/xml" => MimeCategory ::Xml ,
v if v . starts_with ( "text/" ) => MimeCategory ::Text ,
_ => MimeCategory ::Unclassified ,
})
}
2021-09-29 20:48:32 +02:00
2025-12-11 09:53:49 +01:00
pub async fn get_input_epg_content_as_file ( client : & reqwest ::Client , input : & ConfigInput , working_dir : & str , url_str : & str , persist_filepath : Option < PathBuf > ) -> Result < PathBuf , TuliproxError > {
2025-05-30 12:52:41 +02:00
debug_if_enabled! ( "getting input epg content working_dir: {}, url: {}" , working_dir , sanitize_sensitive_info ( url_str ));
2024-11-02 16:44:38 +01:00
if url_str . parse ::< url ::Url > (). is_ok () {
2025-05-19 15:47:00 +02:00
match download_epg_content_as_file ( client , input , url_str , working_dir , persist_filepath ). await {
2024-11-02 16:44:38 +01:00
Ok ( content ) => Ok ( content ),
Err ( e ) => {
2025-11-13 21:39:50 +01:00
error! ( "cant download input {} epg url: {} => {}" , input . name , sanitize_sensitive_info ( url_str ), sanitize_sensitive_info ( e . to_string (). as_str ()));
2025-05-13 21:50:08 +02:00
create_tuliprox_error_result! ( TuliproxErrorKind ::Notify , "Failed to download" )
2024-11-02 16:44:38 +01:00
}
}
} else {
let result = match get_file_path ( working_dir , Some ( PathBuf ::from ( url_str ))) {
Some ( filepath ) => {
if filepath . exists () {
if let Some ( persist_file_value ) = persist_filepath {
let to_file = & persist_file_value ;
2025-11-14 08:58:11 -06:00
if let Err ( e ) = tokio ::fs ::copy ( & filepath , to_file ). await {
error! ( "cant persist to: {} => {}" , to_file . to_str (). unwrap_or ( "?" ), e );
return create_tuliprox_error_result! ( TuliproxErrorKind ::Notify , "Failed to persist: {} => {}" , to_file . to_str (). unwrap_or ( "?" ), e );
2024-11-02 16:44:38 +01:00
}
2025-04-10 00:06:48 +02:00
}
2024-11-02 16:44:38 +01:00
if filepath . exists () {
Some ( filepath )
} else {
2025-05-13 21:50:08 +02:00
return create_tuliprox_error_result! ( TuliproxErrorKind ::Notify , "Failed: file does not exists {filepath:?}" );
2024-11-02 16:44:38 +01:00
}
} else {
None
}
}
None => None
};
2024-11-04 18:46:56 +01:00
result . map_or_else ( || {
2025-01-23 11:50:27 +01:00
let msg = format! ( "cant read input url: {} " , sanitize_sensitive_info ( url_str ));
2025-04-10 00:06:48 +02:00
error! ( "{msg}" );
2025-05-13 21:50:08 +02:00
create_tuliprox_error_result! ( TuliproxErrorKind ::Notify , "{msg}" )
2024-11-04 18:46:56 +01:00
}, Ok )
2024-11-02 16:44:38 +01:00
}
}
2025-12-11 09:53:49 +01:00
pub async fn get_input_text_content ( client : & reqwest ::Client , input : & InputSource , working_dir : & str , persist_filepath : Option < PathBuf > ) -> Result < String , TuliproxError > {
2025-08-13 13:40:41 +02:00
debug_if_enabled! ( "getting input text content working_dir: {}, url: {}" , working_dir , sanitize_sensitive_info ( & input . url ));
2024-05-10 12:01:48 +02:00
2025-08-13 13:40:41 +02:00
if input . url . parse ::< url ::Url > (). is_ok () {
2025-12-11 09:53:49 +01:00
match download_text_content ( client , None , input , None , persist_filepath ). await {
2025-04-15 20:16:47 +02:00
Ok (( content , _response_url )) => Ok ( content ),
2021-09-29 20:48:32 +02:00
Err ( e ) => {
2025-11-10 01:52:13 +01:00
error! ( "Failed to download input '{}': {}" , & input . name , sanitize_sensitive_info ( e . to_string (). as_str ()));
2025-05-13 21:50:08 +02:00
create_tuliprox_error_result! ( TuliproxErrorKind ::Notify , "Failed to download" )
2021-09-29 20:48:32 +02:00
}
}
2024-05-10 12:01:48 +02:00
} else {
2025-08-13 13:40:41 +02:00
let result = match get_file_path ( working_dir , Some ( PathBuf ::from ( & input . url ))) {
2024-05-10 12:01:48 +02:00
Some ( filepath ) => {
if filepath . exists () {
if let Some ( persist_file_value ) = persist_filepath {
let to_file = & persist_file_value ;
2025-11-14 08:58:11 -06:00
if let Err ( e ) = tokio ::fs ::copy ( & filepath , to_file ). await {
error! ( "cant persist to: {} => {}" , to_file . to_str (). unwrap_or ( "?" ), e );
return create_tuliprox_error_result! ( TuliproxErrorKind ::Notify , "Failed to persist: {} => {}" , to_file . to_str (). unwrap_or ( "?" ), e );
2024-05-10 12:01:48 +02:00
}
2025-04-10 00:06:48 +02:00
}
2024-09-23 16:51:50 +02:00
2025-11-14 08:58:11 -06:00
match get_local_file_content ( & filepath ). await {
2024-09-23 16:51:50 +02:00
Ok ( content ) => Some ( content ),
2024-05-10 12:01:48 +02:00
Err ( err ) => {
2025-05-13 21:50:08 +02:00
return create_tuliprox_error_result! ( TuliproxErrorKind ::Notify , "Failed : {}" , err );
2024-05-10 12:01:48 +02:00
}
2021-09-29 20:48:32 +02:00
}
2024-05-10 12:01:48 +02:00
} else {
None
2021-10-15 16:59:20 +02:00
}
2021-09-29 20:48:32 +02:00
}
2024-05-10 12:01:48 +02:00
None => None
};
2024-11-04 18:46:56 +01:00
result . map_or_else ( || {
2025-08-13 13:40:41 +02:00
let msg = format! ( "cant read input url: {} " , sanitize_sensitive_info ( & input . url ));
2025-04-10 00:06:48 +02:00
error! ( "{msg}" );
2025-05-13 21:50:08 +02:00
create_tuliprox_error_result! ( TuliproxErrorKind ::Notify , "{msg}" )
2024-11-04 18:46:56 +01:00
}, Ok )
2021-09-29 20:48:32 +02:00
}
}
2025-12-12 14:35:00 +01:00
pub async fn get_input_text_content_as_stream ( client : & reqwest ::Client , input : & InputSource , working_dir : & str , persist_filepath : Option < PathBuf > ) -> Result < DynReader , TuliproxError > {
debug_if_enabled! ( "getting input text content working_dir: {}, url: {}" , working_dir , sanitize_sensitive_info ( & input . url ));
if input . url . parse ::< url ::Url > (). is_ok () {
match download_text_content_as_stream ( client , None , input , None , persist_filepath ). await {
Ok (( content , _response_url )) => Ok ( content ),
Err ( e ) => {
error! ( "Failed to download input '{}': {}" , & input . name , sanitize_sensitive_info ( e . to_string (). as_str ()));
create_tuliprox_error_result! ( TuliproxErrorKind ::Notify , "Failed to download" )
}
}
} else {
let result = match get_file_path ( working_dir , Some ( PathBuf ::from ( & input . url ))) {
Some ( filepath ) => {
if filepath . exists () {
match get_local_file_content_as_stream ( & filepath ). await {
Ok ( content ) => {
if persist_filepath . is_some () {
let tee_reader : DynReader = if let Some ( path ) = persist_filepath {
let tee = tee_dyn_reader ( content , & path , Some ( Arc ::new ( | size | {
debug_if_enabled! ( "Persisted {} bytes" , human_readable_byte_size ( size as u64 ));
}))). await ;
Box ::pin ( tee )
} else {
content
};
Some ( tee_reader )
} else {
Some ( content )
}
},
Err ( err ) => {
return create_tuliprox_error_result! ( TuliproxErrorKind ::Notify , "Failed : {}" , err );
}
}
} else {
None
}
}
None => None
};
result . map_or_else ( || {
let msg = format! ( "cant read input url: {} " , sanitize_sensitive_info ( & input . url ));
error! ( "{msg}" );
create_tuliprox_error_result! ( TuliproxErrorKind ::Notify , "{msg}" )
}, Ok )
}
}
2025-09-30 10:54:30 +02:00
pub fn get_client_request < S : ::std ::hash ::BuildHasher + Default >
2025-12-12 14:35:00 +01:00
( client : & reqwest ::Client ,
method : InputFetchMethod ,
headers : Option <& HashMap < String , String , S >> ,
url : & Url ,
custom_headers : Option <& HashMap < String , Vec < u8 > , S >> ,
disabled_headers : Option <& ReverseProxyDisabledHeaderConfig > ) -> reqwest ::RequestBuilder {
2025-04-14 18:19:01 +02:00
let request = match method {
InputFetchMethod ::GET => client . get ( url . clone ()),
InputFetchMethod ::POST => {
// let base_url = url[..url::Position::BeforePath].to_string() + url.path();
2025-05-04 10:59:08 +02:00
let mut params : HashMap < String , String , S > = HashMap ::default ();
2025-04-14 18:19:01 +02:00
for ( key , value ) in url . query_pairs () {
params . insert ( key . to_string (), value . to_string ());
}
// we could cut the params but we leave them as query and add them as form.
client . post ( url . clone ()). form ( & params )
2025-05-04 10:59:08 +02:00
}
2025-04-14 18:19:01 +02:00
};
2025-10-31 17:04:35 +01:00
let headers = get_request_headers ( headers , custom_headers , disabled_headers );
2024-11-05 15:58:50 +01:00
request . headers ( headers )
2023-10-27 19:03:29 +02:00
}
2025-10-31 17:04:35 +01:00
pub fn get_request_headers < S : ::std ::hash ::BuildHasher + Default > ( request_headers : Option <& HashMap < String , String , S >> , custom_headers : Option <& HashMap < String , Vec < u8 > , S >> , disabled_headers : Option <& ReverseProxyDisabledHeaderConfig > ) -> HeaderMap {
2025-05-04 10:59:08 +02:00
let mut headers = HeaderMap ::default ();
2025-05-09 22:25:44 +02:00
if let Some ( req_headers ) = request_headers {
for ( key , value ) in req_headers {
2025-05-04 10:59:08 +02:00
if let ( Ok ( key ), Ok ( value )) = ( HeaderName ::from_bytes ( key . as_bytes ()), HeaderValue ::from_bytes ( value . as_bytes ())) {
2025-05-09 17:13:37 +02:00
if filter_request_header ( key . as_str ()) {
2025-10-31 17:04:35 +01:00
if disabled_headers . as_ref (). is_some_and ( | d | d . should_remove ( key . as_str ())) {
continue ;
}
2025-05-09 17:13:37 +02:00
headers . insert ( key , value );
}
2025-05-04 10:59:08 +02:00
}
2024-11-05 15:58:50 +01:00
}
2023-12-01 21:38:23 +01:00
}
if let Some ( custom ) = custom_headers {
2023-12-02 20:47:26 +01:00
let header_keys : HashSet < String > = headers . keys (). map ( | k | k . as_str (). to_lowercase ()). collect ();
2023-12-01 21:38:23 +01:00
for ( key , value ) in custom {
2023-12-02 20:47:26 +01:00
let key_lc = key . to_lowercase ();
2025-05-09 22:25:44 +02:00
if filter_request_header ( key_lc . as_str ()) {
2025-10-31 17:04:35 +01:00
if disabled_headers . as_ref (). is_some_and ( | d | d . should_remove ( key )) {
continue ;
}
2025-05-09 22:25:44 +02:00
if header_keys . contains ( key_lc . as_str ()) {
// debug_if_enabled!("Ignoring request header '{}={}'", key_lc, String::from_utf8_lossy(value));
} else if let ( Ok ( key ), Ok ( value )) = ( HeaderName ::from_bytes ( key . as_bytes ()), HeaderValue ::from_bytes ( value )) {
headers . insert ( key , value );
}
2023-12-01 21:38:23 +01:00
}
}
2023-11-06 21:33:48 +01:00
}
2025-01-10 13:07:35 +01:00
if log_enabled! ( Level ::Trace ) {
2023-12-05 17:54:42 +01:00
let he : HashMap < String , String > = headers . iter (). map ( | ( k , v ) | ( k . to_string (), String ::from_utf8_lossy ( v . as_bytes ()). to_string ())). collect ();
2025-01-01 10:59:51 +01:00
if ! he . is_empty () {
2025-04-10 00:06:48 +02:00
trace! ( "Request headers {he:?}" );
2025-01-01 10:59:51 +01:00
}
2023-12-05 17:54:42 +01:00
}
2025-07-09 17:30:07 +02:00
if ! headers . contains_key ( axum ::http ::header ::USER_AGENT ) {
headers . insert ( axum ::http ::header ::USER_AGENT , HeaderValue ::from_static ( DEFAULT_USER_AGENT ));
}
2023-11-06 21:33:48 +01:00
headers
}
2025-11-20 13:00:01 +01:00
pub async fn get_local_file_content ( file_path : & Path ) -> Result < String , std ::io ::Error > {
// Datei öffnen
2025-11-20 13:40:04 +01:00
let file = File ::open ( file_path ). await . map_err ( | err | {
std ::io ::Error ::new ( ErrorKind ::NotFound , format! ( "Failed to open file: {} , {err:?} " , file_path . display ()))
2025-11-20 13:00:01 +01:00
}) ? ;
2025-12-11 15:27:48 +01:00
let mut buf_reader = async_file_reader ( file );
2025-11-20 13:00:01 +01:00
// Peek die ersten 2 Bytes, um gzip zu erkennen
let buffer = buf_reader . fill_buf (). await ? ;
let is_gzipped = buffer . len () >= 2 && is_gzip ( & buffer [ 0 .. 2 ]);
let mut decoded = String ::new ();
if is_gzipped {
// Async Gzip Decoder verwenden
let mut gzip_decoder = async_compression ::tokio ::bufread ::GzipDecoder ::new ( buf_reader );
gzip_decoder . read_to_string ( & mut decoded ). await . map_err ( | e | {
std ::io ::Error ::other ( format! ( "Failed to decode gzip content: {e} " ))
}) ? ;
2025-11-14 08:58:11 -06:00
} else {
2025-11-20 13:00:01 +01:00
// Plaintext lesen
buf_reader . read_to_string ( & mut decoded ). await . map_err ( | e | {
std ::io ::Error ::other ( format! ( "Failed to read file: {e} " ))
}) ? ;
2024-09-23 16:51:50 +02:00
}
2025-11-14 08:58:11 -06:00
2025-11-20 13:00:01 +01:00
Ok ( decoded )
2025-11-14 08:58:11 -06:00
}
2025-12-12 14:35:00 +01:00
pub async fn get_local_file_content_as_stream ( file_path : & Path ) -> Result < DynReader , std ::io ::Error > {
// open file
let file = File ::open ( file_path ). await . map_err ( | err | {
std ::io ::Error ::new ( ErrorKind ::NotFound , format! ( "Failed to open file: {} , {err:?} " , file_path . display ()))
}) ? ;
let mut buf_reader = async_file_reader ( file );
// Peek first 2 Bytes, for gzip detection
let buffer = buf_reader . fill_buf (). await ? ;
let is_gzipped = buffer . len () >= 2 && is_gzip ( & buffer [ 0 .. 2 ]);
if is_gzipped {
// use Async Gzip Decoder
Ok ( Box ::pin ( async_compression ::tokio ::bufread ::GzipDecoder ::new ( buf_reader )))
} else {
Ok ( Box ::pin ( buf_reader ))
}
}
2025-11-14 16:32:18 +01:00
// pub fn get_local_file_content_blocking(file_path: &PathBuf) -> Result<String, Error> {
// match fs::read(file_path) {
// Ok(content) => decode_local_file_bytes(content).await,
// Err(_) => Err(local_file_not_found(file_path)),
// }
// }
2024-09-23 16:51:50 +02:00
2024-11-02 16:44:38 +01:00
2025-12-11 09:53:49 +01:00
async fn get_remote_content_as_file ( client : & reqwest ::Client , input : & ConfigInput , url : & Url , file_path : & Path ) -> Result < PathBuf , std ::io ::Error > {
2024-11-03 14:52:56 +01:00
let start_time = Instant ::now ();
2025-12-11 09:53:49 +01:00
let request = get_client_request ( client , input . method , Some ( & input . headers ), url , None , None );
2024-11-02 16:44:38 +01:00
match request . send (). await {
Ok ( response ) => {
if response . status (). is_success () {
// Open a file in write mode
2025-12-11 15:27:48 +01:00
let mut writer = async_file_writer ( File ::create ( file_path ). await ? );
let mut write_counter = 0 ;
2024-11-02 16:44:38 +01:00
// Stream the response body in chunks
let mut stream = response . bytes_stream ();
while let Some ( chunk ) = stream . next (). await {
match chunk {
Ok ( bytes ) => {
2025-12-11 15:27:48 +01:00
write_counter += bytes . len ();
writer . write_all ( & bytes ). await ? ;
2025-12-11 19:50:31 +01:00
if write_counter >= IO_BUFFER_SIZE {
2025-12-11 15:27:48 +01:00
writer . flush (). await ? ;
write_counter = 0 ;
}
2024-11-02 16:44:38 +01:00
}
Err ( err ) => {
2025-12-11 20:02:20 +01:00
let _ = writer . flush (). await ;
let _ = writer . shutdown (). await ;
2025-01-17 18:32:17 +01:00
return Err ( str_to_io_error ( & format! ( "Failed to read chunk: {err} " )));
2024-11-02 16:44:38 +01:00
}
}
}
2025-12-11 15:27:48 +01:00
writer . flush (). await ? ;
writer . shutdown (). await ? ;
2024-11-03 14:52:56 +01:00
let elapsed = start_time . elapsed (). as_secs ();
2025-05-16 14:01:36 +02:00
debug! ( "File downloaded successfully to {}, took:{}" , file_path . display (), format_elapsed_time ( elapsed ));
2024-11-02 16:44:38 +01:00
Ok ( file_path . to_path_buf ())
} else {
2025-01-23 11:50:27 +01:00
Err ( str_to_io_error ( & format! ( "Request failed with status {} {} " , response . status (), sanitize_sensitive_info ( url . as_str ()))))
2024-11-02 16:44:38 +01:00
}
}
2025-01-23 11:50:27 +01:00
Err ( err ) => Err ( str_to_io_error ( & format! ( "Request failed: {} {err} " , sanitize_sensitive_info ( url . as_str ())))),
2024-11-02 16:44:38 +01:00
}
}
2025-12-12 14:35:00 +01:00
pub type DynReader = Pin < Box < dyn AsyncRead + Send >> ;
2024-11-03 14:52:56 +01:00
2025-09-30 10:54:30 +02:00
#[allow(clippy::implicit_hasher)]
pub async fn get_remote_content_as_stream (
2025-12-11 09:53:49 +01:00
client : & reqwest ::Client ,
2025-12-12 14:35:00 +01:00
input : & InputSource ,
headers : Option <& HeaderMap > ,
2025-09-30 10:54:30 +02:00
url : & Url ,
2025-12-12 14:35:00 +01:00
disabled_headers : Option <& ReverseProxyDisabledHeaderConfig > ,
2025-09-30 10:54:30 +02:00
) -> Result < ( DynReader , String ), Error > {
2025-12-12 14:35:00 +01:00
let custom_headers = headers . map ( | h | {
h . iter (). map ( | ( k , v ) | ( k . as_str (). to_string (), v . as_bytes (). to_vec ())). collect ::< HashMap < _ , _ >> ()
});
let merged = get_request_headers ( Some ( & input . headers ), custom_headers . as_ref (), disabled_headers );
let headers : HashMap < String , String > = merged . iter (). map ( | ( k , v ) | ( k . as_str (). to_string (), String ::from_utf8_lossy ( v . as_bytes ()). to_string ())). collect ();
let request = get_client_request ( client , input . method , Some ( & headers ), url , None , None );
2025-09-30 10:54:30 +02:00
let response = request . send (). await . map_err ( std ::io ::Error ::other ) ? ;
2024-09-23 16:51:50 +02:00
2025-09-30 10:54:30 +02:00
if ! response . status (). is_success () {
return Err ( str_to_io_error ( & format! ( "Request failed with status {} {} " , response . status (), sanitize_sensitive_info ( url . as_str ()))));
}
let response_url = response . url (). to_string ();
let headers = response . headers ();
let header_value = headers . get ( CONTENT_ENCODING );
let mut encoding = header_value . and_then ( | encoding_header | encoding_header . to_str (). map_or ( None , | value | Some ( value . to_string ())));
let stream_reader = StreamReader ::new (
response . bytes_stream (). map_err ( std ::io ::Error ::other ),
);
2025-12-11 15:27:48 +01:00
let mut buf_reader = async_file_reader ( stream_reader );
2025-09-30 10:54:30 +02:00
let peek = buf_reader . fill_buf (). await ? ;
if peek . len () >= 2 {
if is_gzip ( & peek [ 0 .. 2 ]) {
encoding = Some ( ENCODING_GZIP . to_string ());
} else if is_deflate ( & peek [ 0 .. 2 ]) {
encoding = Some ( ENCODING_DEFLATE . to_string ());
2024-09-23 16:51:50 +02:00
}
}
2025-09-30 10:54:30 +02:00
let reader : DynReader = if encoding . as_ref (). is_some_and ( | e | e . eq_ignore_ascii_case ( ENCODING_GZIP )) {
Box ::pin ( async_compression ::tokio ::bufread ::GzipDecoder ::new ( buf_reader ))
} else if encoding . as_ref (). is_some_and ( | e | e . eq_ignore_ascii_case ( ENCODING_DEFLATE )) {
Box ::pin ( async_compression ::tokio ::bufread ::ZlibDecoder ::new ( buf_reader ))
} else {
Box ::pin ( buf_reader )
};
Ok (( reader , response_url ))
}
2025-12-11 09:53:49 +01:00
async fn get_remote_content ( client : & reqwest ::Client , input : & InputSource , headers : Option <& HeaderMap > , url : & Url , disabled_headers : Option <& ReverseProxyDisabledHeaderConfig > ) -> Result < ( String , String ), Error > {
2025-09-30 10:54:30 +02:00
let start_time = Instant ::now ();
2025-10-14 19:04:32 +02:00
2025-12-12 14:35:00 +01:00
let ( mut stream , response_url ) = get_remote_content_as_stream ( client , input , headers , url , disabled_headers ). await . map_err ( | e | str_to_io_error ( & format! ( "Failed to read content: {e} " ))) ? ;
2025-09-30 10:54:30 +02:00
let mut content = String ::new ();
stream . read_to_string ( & mut content ). await . map_err ( | e | str_to_io_error ( & format! ( "Failed to read content: {e} " ))) ? ;
2025-11-21 01:35:04 +01:00
debug_if_enabled! ( "Request took: {} {}" , format_elapsed_time ( start_time . elapsed (). as_secs ()), sanitize_sensitive_info ( url . as_str ()));
2025-09-30 10:54:30 +02:00
Ok (( content , response_url ))
2024-09-23 16:51:50 +02:00
}
2025-12-11 09:53:49 +01:00
async fn download_epg_content_as_file ( client : & reqwest ::Client , input : & ConfigInput , url_str : & str , working_dir : & str , persist_filepath : Option < PathBuf > ) -> Result < PathBuf , Error > {
2024-11-02 16:44:38 +01:00
if let Ok ( url ) = url_str . parse ::< url ::Url > () {
if url . scheme () == "file" {
2025-01-23 11:50:27 +01:00
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 () {
2024-11-05 15:58:50 +01:00
Ok ( file_path )
} else {
2025-05-16 14:01:36 +02:00
Err ( Error ::new ( ErrorKind ::NotFound , format! ( "Unknown file {} " , file_path . display ())))
2024-11-05 15:58:50 +01:00
})
2024-11-02 16:44:38 +01:00
} else {
2025-03-19 09:54:59 +01:00
let file_path = persist_filepath . map_or_else ( || match get_input_storage_path ( & input . name , working_dir ) {
2024-11-04 18:46:56 +01:00
Ok ( download_path ) => {
2025-04-17 12:11:51 +02:00
Ok ( download_path . join ( format! ( " {} _ {} " , short_hash ( url_str ), storage_const ::FILE_EPG )))
2024-11-02 16:44:38 +01:00
}
2024-11-04 18:46:56 +01:00
Err ( err ) => Err ( err )
}, Ok );
2024-11-02 16:44:38 +01:00
match file_path {
2025-01-09 15:58:38 +01:00
Ok ( persist_path ) => get_remote_content_as_file ( client , input , & url , & persist_path ). await ,
2024-11-02 16:44:38 +01:00
Err ( err ) => Err ( err )
}
}
} else {
2025-01-23 11:50:27 +01:00
Err ( std ::io ::Error ::new ( ErrorKind ::Unsupported , format! ( "Malformed URL {} " , sanitize_sensitive_info ( url_str ))))
2024-11-02 16:44:38 +01:00
}
}
2025-11-14 08:58:11 -06:00
pub async fn download_text_content (
2025-12-11 09:53:49 +01:00
client : & reqwest ::Client ,
2025-11-14 08:58:11 -06:00
disabled_headers : Option <& ReverseProxyDisabledHeaderConfig > ,
input : & InputSource ,
headers : Option <& HeaderMap > ,
persist_filepath : Option < PathBuf > ,
) -> Result < ( String , String ), Error > {
2025-08-13 13:40:41 +02:00
if let Ok ( url ) = input . url . parse ::< url ::Url > () {
2024-09-23 16:51:50 +02:00
let result = if url . scheme () == "file" {
2025-11-14 08:58:11 -06:00
match url . to_file_path () {
Ok ( file_path ) => get_local_file_content ( & file_path ). await . map ( | c | ( c , url . to_string ())),
Err (()) => Err ( str_to_io_error ( & format! (
"Unknown file {} " ,
sanitize_sensitive_info ( & input . url )
))),
}
2024-09-23 16:51:50 +02:00
} else {
2025-10-31 17:04:35 +01:00
get_remote_content ( client , input , headers , & url , disabled_headers ). await
2024-05-27 09:51:35 +02:00
};
match result {
2025-04-15 20:16:47 +02:00
Ok (( content , response_url )) => {
2024-05-27 09:51:35 +02:00
if persist_filepath . is_some () {
2025-11-14 08:58:11 -06:00
persist_file ( persist_filepath , & content ). await ;
2024-05-27 09:51:35 +02:00
}
2025-04-15 20:16:47 +02:00
Ok (( content , response_url ))
2024-05-27 09:51:35 +02:00
}
2025-11-14 08:58:11 -06:00
Err ( err ) => Err ( err ),
2023-10-27 19:03:29 +02:00
}
2024-05-27 09:51:35 +02:00
} else {
2025-12-12 14:35:00 +01:00
Err ( str_to_io_error ( & format! ( "Malformed URL {} " , sanitize_sensitive_info ( & input . url ))))
}
}
pub async fn download_text_content_as_stream (
client : & reqwest ::Client ,
disabled_headers : Option <& ReverseProxyDisabledHeaderConfig > ,
input : & InputSource ,
headers : Option <& HeaderMap > ,
persist_filepath : Option < PathBuf > ,
) -> Result < ( DynReader , String ), Error > {
if let Ok ( url ) = input . url . parse ::< url ::Url > () {
let result = if url . scheme () == "file" {
match url . to_file_path () {
Ok ( file_path ) => get_local_file_content_as_stream ( & file_path ). await . map ( | c | ( c , url . to_string ())),
Err (()) => Err ( str_to_io_error ( & format! (
"Unknown file {} " ,
sanitize_sensitive_info ( & input . url )
))),
}
} else {
get_remote_content_as_stream ( client , input , headers , & url , disabled_headers ). await
};
match result {
Ok (( content , response_url )) => {
if persist_filepath . is_some () {
let tee_reader : DynReader = if let Some ( path ) = persist_filepath {
let tee = tee_dyn_reader ( content , & path , Some ( Arc ::new ( | size | {
debug! ( "Persisted {size} bytes" );
}))). await ;
Box ::pin ( tee )
} else {
content
};
Ok (( tee_reader , response_url ))
} else {
Ok (( content , response_url ))
}
}
Err ( err ) => Err ( err ),
}
} else {
Err ( str_to_io_error ( & format! ( "Malformed URL {} " , sanitize_sensitive_info ( & input . url ))))
2024-05-27 09:51:35 +02:00
}
}
2025-12-11 09:53:49 +01:00
async fn download_json_content ( client : & reqwest ::Client , disabled_headers : Option <& ReverseProxyDisabledHeaderConfig > , input : & InputSource , persist_filepath : Option < PathBuf > ) -> Result < serde_json ::Value , Error > {
2025-08-13 13:40:41 +02:00
debug_if_enabled! ( "downloading json content from {}" , sanitize_sensitive_info ( & input . url ));
2025-10-31 17:04:35 +01:00
match download_text_content ( client , disabled_headers , input , None , persist_filepath ). await {
2025-04-15 20:16:47 +02:00
Ok (( content , _response_url )) => {
2024-05-27 09:51:35 +02:00
match serde_json ::from_str ::< serde_json ::Value > ( & content ) {
Ok ( value ) => Ok ( value ),
2025-01-17 18:32:17 +01:00
Err ( err ) => Err ( str_to_io_error ( & format! ( "Failed to parse json {err} " )))
2024-05-25 18:55:28 +02:00
}
}
Err ( err ) => Err ( err )
2023-10-27 19:03:29 +02:00
}
2024-05-27 09:51:35 +02:00
}
2025-12-11 09:53:49 +01:00
pub async fn get_input_json_content ( client : & reqwest ::Client , disabled_headers : Option <& ReverseProxyDisabledHeaderConfig > , input : & InputSource , persist_filepath : Option < PathBuf > ) -> Result < serde_json ::Value , TuliproxError > {
2025-10-31 17:04:35 +01:00
match download_json_content ( client , disabled_headers , input , persist_filepath ). await {
2024-05-27 09:51:35 +02:00
Ok ( content ) => Ok ( content ),
2025-11-20 13:08:37 +01:00
Err ( e ) => create_tuliprox_error_result! ( TuliproxErrorKind ::Notify , "cant download input {}, url: {} => {}" , input . name , sanitize_sensitive_info ( & input . url ), sanitize_sensitive_info ( e . to_string (). as_str ()))
2024-05-27 09:51:35 +02:00
}
}
2025-01-23 11:50:27 +01:00
2025-12-12 14:35:00 +01:00
async fn download_json_content_as_stream ( client : & reqwest ::Client , disabled_headers : Option <& ReverseProxyDisabledHeaderConfig > , input : & InputSource , persist_filepath : Option < PathBuf > ) -> Result < DynReader , Error > {
debug_if_enabled! ( "downloading json content from {}" , sanitize_sensitive_info ( & input . url ));
match download_text_content_as_stream ( client , disabled_headers , input , None , persist_filepath ). await {
Ok (( reader , _response_url )) => Ok ( reader ),
Err ( err ) => Err ( err )
}
}
pub async fn get_input_json_content_as_stream ( client : & reqwest ::Client , disabled_headers : Option <& ReverseProxyDisabledHeaderConfig > , input : & InputSource , persist_filepath : Option < PathBuf > ) -> Result < DynReader , TuliproxError > {
match download_json_content_as_stream ( client , disabled_headers , input , persist_filepath ). await {
Ok ( stream ) => Ok ( stream ),
Err ( e ) => create_tuliprox_error_result! ( TuliproxErrorKind ::Notify , "cant download input {}, url: {} => {}" , input . name , sanitize_sensitive_info ( & input . url ), sanitize_sensitive_info ( e . to_string (). as_str ()))
}
}
2025-04-17 20:09:29 +02:00
2025-06-24 01:06:25 +02:00
pub fn create_client ( cfg : & AppConfig ) -> reqwest ::ClientBuilder {
2025-09-19 12:15:24 +02:00
let config = cfg . config . load ();
2025-06-13 14:03:18 +02:00
let mut client = reqwest ::Client ::builder ()
. redirect ( reqwest ::redirect ::Policy ::limited ( 10 ))
. pool_idle_timeout ( Duration ::from_secs ( 30 ))
2025-09-19 12:15:24 +02:00
. pool_max_idle_per_host ( 10 )
2025-10-01 17:06:41 +02:00
. danger_accept_invalid_certs ( config . accept_insecure_ssl_certificates );
2025-05-21 23:32:27 +02:00
2025-06-24 01:06:25 +02:00
if let Some ( proxy_cfg ) = config . proxy . as_ref () {
2025-08-09 16:41:29 +02:00
match Url ::parse ( & proxy_cfg . url ) {
2025-08-10 12:03:46 +02:00
Ok ( mut url ) => {
let scheme = url . scheme (). to_ascii_lowercase ();
2025-08-09 16:41:29 +02:00
2025-08-10 12:03:46 +02:00
match scheme . as_str () {
"socks5" | "socks5h" => {
if let Some ( user ) = & proxy_cfg . username {
let _ = url . set_username ( user );
}
if let Some ( pass ) = & proxy_cfg . password {
let _ = url . set_password ( Some ( pass ));
}
match reqwest ::Proxy ::all ( url . as_str ()) {
Ok ( p ) => { client = client . proxy ( p ); }
Err ( err ) => error! ( "Failed to create SOCKS proxy {url}: {err}" ),
}
2025-12-12 14:35:00 +01:00
}
2025-08-10 12:03:46 +02:00
"http" | "https" => {
match reqwest ::Proxy ::all ( url . as_str ()) {
Ok ( p ) => {
if let ( Some ( username ), Some ( password )) =
( & proxy_cfg . username , & proxy_cfg . password )
{
client = client . proxy ( p . basic_auth ( username , password ));
} else {
client = client . proxy ( p );
}
2025-08-09 16:41:29 +02:00
}
2025-08-10 12:03:46 +02:00
Err ( err ) => error! ( "Failed to create HTTP proxy {url}: {err}" ),
2025-08-09 16:41:29 +02:00
}
}
2025-08-10 12:03:46 +02:00
_ => {
error! ( "Unsupported proxy scheme '{scheme}' in URL: {url}" );
}
2025-04-30 13:32:55 +02:00
}
}
2025-08-09 16:41:29 +02:00
Err ( e ) => {
2025-08-10 12:03:46 +02:00
error! ( "Invalid proxy URL '{}': {e}" , & proxy_cfg . url );
2025-04-30 13:32:55 +02:00
}
2025-08-09 16:41:29 +02:00
}
2025-04-30 13:32:55 +02:00
}
2025-05-21 23:32:27 +02:00
2025-06-24 01:06:25 +02:00
if let Some ( rp_config ) = config . reverse_proxy . as_ref () {
2025-10-31 17:04:35 +01:00
if rp_config
. disabled_header
. as_ref ()
. is_some_and ( | d | d . referer_header )
{
2025-05-21 21:38:23 +02:00
client = client . referer ( false );
}
}
2025-05-21 23:32:27 +02:00
2025-04-30 13:32:55 +02:00
client
}
2025-01-07 17:52:12 +01:00
#[cfg(test)]
mod tests {
2025-06-24 01:06:25 +02:00
use shared ::utils ::{ get_base_url_from_str , replace_url_extension , sanitize_sensitive_info };
2025-01-07 17:52:12 +01:00
#[test]
fn test_url_mask () {
// Replace with "***"
2025-01-28 13:04:58 +01:00
let query = "https://bubblegum.tv/live/username/password/2344" ;
2025-04-24 20:59:07 +02:00
let masked = sanitize_sensitive_info ( query );
println! ( " {masked} " );
2025-01-07 17:52:12 +01:00
}
2025-01-24 19:01:53 +01:00
2025-01-31 23:08:23 +01:00
#[test]
fn test_replace_ext () {
let tests = [
2025-03-16 17:08:00 +01:00
( "http://hello.world.com" , "http://hello.world.com" ),
( "http://hello.world.com/123" , "http://hello.world.com/123.mp4" ),
( "http://hello.world.com/123.ts?hello=world" , "http://hello.world.com/123.mp4?hello=world" ),
( "http://hello.world.com/123?hello=world" , "http://hello.world.com/123.mp4?hello=world" ),
( "http://hello.world.com/123#hello=world" , "http://hello.world.com/123.mp4#hello=world" )
2025-01-31 23:08:23 +01:00
];
2025-03-16 17:08:00 +01:00
for ( test , expect ) in & tests {
2025-03-21 17:15:28 +01:00
assert_eq! ( replace_url_extension ( test , ".mp4" ), * expect );
2025-01-31 23:08:23 +01:00
}
}
2025-04-17 20:09:29 +02:00
#[test]
fn tes_base_url () {
let url = "http://my.provider.com:8080/xmltv?username=hello" ;
let expected = "http://my.provider.com:8080" ;
assert_eq! ( get_base_url_from_str ( url ). unwrap (), expected );
}
2025-01-30 12:30:04 +01:00
}