2025-06-03 18:25:15 +02:00
|
|
|
use crate::api::endpoints::xtream_api::{get_xtream_player_api_stream_url, ApiStreamContext};
|
2025-12-10 17:19:57 +01:00
|
|
|
use crate::api::model::{tee_stream, UserSession};
|
2025-07-24 14:27:57 +02:00
|
|
|
use crate::api::model::{
|
|
|
|
|
create_channel_unavailable_stream, create_custom_video_stream_response,
|
|
|
|
|
create_provider_connections_exhausted_stream, create_provider_stream,
|
2025-11-04 18:01:44 +01:00
|
|
|
get_stream_response_with_headers, ActiveClientStream, AppState,
|
2025-12-10 17:19:57 +01:00
|
|
|
CustomVideoStreamType,
|
2025-11-04 18:01:44 +01:00
|
|
|
ProviderStreamFactoryOptions, SharedStreamManager,
|
2025-07-24 14:27:57 +02:00
|
|
|
StreamError, ThrottledStream, UserApiRequest,
|
|
|
|
|
};
|
2025-11-11 10:00:35 +01:00
|
|
|
use crate::api::model::{ProviderAllocation, ProviderConfig, ProviderStreamState, StreamDetails, StreamingStrategy};
|
2025-12-16 23:03:24 +01:00
|
|
|
use crate::api::panel_api::try_provision_account_on_exhausted;
|
2025-11-11 10:00:35 +01:00
|
|
|
use crate::model::{ConfigInput, ResourceRetryConfig};
|
2025-06-19 17:20:31 +02:00
|
|
|
use crate::model::{ConfigTarget, ProxyUserCredentials};
|
2025-02-05 20:01:49 +01:00
|
|
|
use crate::tools::lru_cache::LRUResourceCache;
|
2025-12-11 15:27:48 +01:00
|
|
|
use crate::utils::{async_file_reader, async_file_writer, create_new_file_for_write};
|
2025-05-04 10:59:08 +02:00
|
|
|
use crate::utils::request;
|
|
|
|
|
use crate::utils::{debug_if_enabled, trace_if_enabled};
|
2025-06-25 14:55:44 +02:00
|
|
|
use crate::BUILD_TIMESTAMP;
|
|
|
|
|
use arc_swap::ArcSwapOption;
|
2025-12-03 11:47:36 +01:00
|
|
|
use axum::http::{HeaderMap};
|
2025-03-20 11:57:32 +01:00
|
|
|
use axum::response::IntoResponse;
|
2025-04-22 17:08:31 +02:00
|
|
|
use chrono::{DateTime, Utc};
|
2025-03-11 14:56:10 +01:00
|
|
|
use futures::{StreamExt, TryStreamExt};
|
2025-03-20 11:57:32 +01:00
|
|
|
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
|
2025-12-03 11:47:36 +01:00
|
|
|
use log::{debug, error, log_enabled, trace, warn};
|
2025-09-08 13:16:57 +02:00
|
|
|
use reqwest::header::RETRY_AFTER;
|
2025-11-11 10:00:35 +01:00
|
|
|
use serde::Serialize;
|
2025-10-24 18:36:13 +02:00
|
|
|
use shared::model::{Claims, InputFetchMethod, PlaylistEntry, PlaylistItemType, StreamChannel, TargetType, UserConnectionPermission, XtreamCluster};
|
2025-11-29 15:37:39 +01:00
|
|
|
use shared::utils::{bin_serialize, default_grace_period_millis, trim_slash};
|
2025-07-24 14:27:57 +02:00
|
|
|
use shared::utils::{
|
|
|
|
|
extract_extension_from_url, replace_url_extension, sanitize_sensitive_info, DASH_EXT, HLS_EXT,
|
|
|
|
|
};
|
2025-04-22 17:08:31 +02:00
|
|
|
use std::borrow::Cow;
|
2024-12-05 19:54:34 +01:00
|
|
|
use std::collections::HashMap;
|
2025-01-27 15:58:57 +01:00
|
|
|
use std::path::Path;
|
2025-03-20 11:57:32 +01:00
|
|
|
use std::sync::Arc;
|
2025-09-08 13:16:57 +02:00
|
|
|
use std::time::Duration;
|
2025-03-11 14:56:10 +01:00
|
|
|
use tokio::sync::Mutex;
|
2024-12-05 19:54:34 +01:00
|
|
|
use url::Url;
|
2023-10-20 12:18:44 +02:00
|
|
|
|
2025-09-30 15:44:45 +02:00
|
|
|
const CONTENT_TYPE_BIN: &str = "application/cbor";
|
2025-09-30 10:54:30 +02:00
|
|
|
|
2025-01-31 23:08:23 +01:00
|
|
|
#[macro_export]
|
|
|
|
|
macro_rules! try_option_bad_request {
|
|
|
|
|
($option:expr, $msg_is_error:expr, $msg:expr) => {
|
|
|
|
|
match $option {
|
|
|
|
|
Some(value) => value,
|
|
|
|
|
None => {
|
2025-07-24 14:27:57 +02:00
|
|
|
if $msg_is_error {
|
|
|
|
|
error!("{}", $msg);
|
|
|
|
|
} else {
|
|
|
|
|
debug!("{}", $msg);
|
|
|
|
|
}
|
2025-03-11 14:56:10 +01:00
|
|
|
return axum::http::StatusCode::BAD_REQUEST.into_response();
|
2025-01-31 23:08:23 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
($option:expr) => {
|
|
|
|
|
match $option {
|
|
|
|
|
Some(value) => value,
|
2025-03-11 14:56:10 +01:00
|
|
|
None => return axum::http::StatusCode::BAD_REQUEST.into_response(),
|
2025-01-31 23:08:23 +01:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[macro_export]
|
2025-10-16 11:58:40 +02:00
|
|
|
macro_rules! try_unwrap_body {
|
|
|
|
|
($body:expr) => {
|
|
|
|
|
$body.map_or_else(
|
|
|
|
|
|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR.into_response(),
|
|
|
|
|
|resp| resp.into_response(),
|
|
|
|
|
)
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[macro_export]
|
|
|
|
|
macro_rules! try_result_or_status {
|
|
|
|
|
($option:expr, $status:expr, $msg_is_error:expr, $msg:expr) => {
|
2025-01-31 23:08:23 +01:00
|
|
|
match $option {
|
|
|
|
|
Ok(value) => value,
|
|
|
|
|
Err(_) => {
|
2025-07-24 14:27:57 +02:00
|
|
|
if $msg_is_error {
|
|
|
|
|
error!("{}", $msg);
|
|
|
|
|
} else {
|
|
|
|
|
debug!("{}", $msg);
|
|
|
|
|
}
|
2025-10-16 11:58:40 +02:00
|
|
|
return $status.into_response();
|
2025-01-31 23:08:23 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
2025-10-16 11:58:40 +02:00
|
|
|
($option:expr, $status:expr) => {
|
2025-01-31 23:08:23 +01:00
|
|
|
match $option {
|
|
|
|
|
Ok(value) => value,
|
2025-10-16 11:58:40 +02:00
|
|
|
Err(_) => return $status.into_response(),
|
2025-01-31 23:08:23 +01:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-22 11:32:46 +02:00
|
|
|
#[macro_export]
|
2025-10-16 11:58:40 +02:00
|
|
|
macro_rules! try_result_bad_request {
|
|
|
|
|
($option:expr, $msg_is_error:expr, $msg:expr) => {
|
|
|
|
|
$crate::api::api_utils::try_result_or_status!($option, axum::http::StatusCode::BAD_REQUEST, $msg_is_error, $msg)
|
|
|
|
|
};
|
|
|
|
|
($option:expr) => {
|
|
|
|
|
$crate::api::api_utils::try_result_or_status!($option, axum::http::StatusCode::BAD_REQUEST)
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[macro_export]
|
|
|
|
|
macro_rules! try_result_not_found {
|
|
|
|
|
($option:expr, $msg_is_error:expr, $msg:expr) => {
|
|
|
|
|
$crate::api::api_utils::try_result_or_status!($option, axum::http::StatusCode::NOT_FOUND, $msg_is_error, $msg)
|
|
|
|
|
};
|
|
|
|
|
($option:expr) => {
|
|
|
|
|
$crate::api::api_utils::try_result_or_status!($option, axum::http::StatusCode::NOT_FOUND)
|
2025-07-22 11:32:46 +02:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-16 11:58:40 +02:00
|
|
|
pub use try_result_or_status;
|
2025-02-05 20:01:49 +01:00
|
|
|
pub use try_option_bad_request;
|
|
|
|
|
pub use try_result_bad_request;
|
2025-10-16 11:58:40 +02:00
|
|
|
pub use try_result_not_found;
|
2025-07-22 11:32:46 +02:00
|
|
|
pub use try_unwrap_body;
|
2025-10-30 13:00:25 +01:00
|
|
|
use crate::auth::Fingerprint;
|
2025-04-22 00:21:45 +02:00
|
|
|
|
|
|
|
|
pub fn get_server_time() -> String {
|
2025-07-24 14:27:57 +02:00
|
|
|
chrono::offset::Local::now()
|
|
|
|
|
.with_timezone(&chrono::Local)
|
|
|
|
|
.format("%Y-%m-%d %H:%M:%S %Z")
|
|
|
|
|
.to_string()
|
2025-04-22 00:21:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn get_build_time() -> Option<String> {
|
2025-07-24 14:27:57 +02:00
|
|
|
BUILD_TIMESTAMP
|
|
|
|
|
.to_string()
|
|
|
|
|
.parse::<DateTime<Utc>>()
|
|
|
|
|
.ok()
|
|
|
|
|
.map(|datetime| datetime.format("%Y-%m-%d %H:%M:%S %Z").to_string())
|
2025-04-22 00:21:45 +02:00
|
|
|
}
|
|
|
|
|
|
2025-04-10 00:06:48 +02:00
|
|
|
#[allow(clippy::missing_panics_doc)]
|
2025-07-15 12:28:25 +02:00
|
|
|
pub async fn serve_file(file_path: &Path, mime_type: mime::Mime) -> impl IntoResponse + Send {
|
2025-11-20 14:22:26 +01:00
|
|
|
match tokio::fs::try_exists(file_path).await {
|
|
|
|
|
Ok(exists) => {
|
|
|
|
|
if ! exists {
|
|
|
|
|
return axum::http::StatusCode::NOT_FOUND.into_response();
|
2025-03-11 14:56:10 +01:00
|
|
|
}
|
2025-11-20 14:22:26 +01:00
|
|
|
}
|
|
|
|
|
Err(err) => {
|
|
|
|
|
error!("Failed to open egp file {}, {err:?}", file_path.display());
|
|
|
|
|
return axum::http::StatusCode::NOT_FOUND.into_response();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
match tokio::fs::File::open(file_path).await {
|
|
|
|
|
Ok(file) => {
|
2025-12-11 15:27:48 +01:00
|
|
|
let reader = async_file_reader(file);
|
2025-11-20 14:22:26 +01:00
|
|
|
let stream = tokio_util::io::ReaderStream::new(reader);
|
|
|
|
|
let body = axum::body::Body::from_stream(stream);
|
|
|
|
|
|
|
|
|
|
try_unwrap_body!(axum::response::Response::builder()
|
|
|
|
|
.status(axum::http::StatusCode::OK)
|
|
|
|
|
.header(axum::http::header::CONTENT_TYPE, mime_type.to_string())
|
|
|
|
|
.header(
|
|
|
|
|
axum::http::header::CACHE_CONTROL,
|
|
|
|
|
axum::http::header::HeaderValue::from_static("no-cache")
|
|
|
|
|
)
|
|
|
|
|
.body(body))
|
|
|
|
|
}
|
|
|
|
|
Err(_) => axum::http::StatusCode::INTERNAL_SERVER_ERROR.into_response(),
|
2023-10-20 12:18:44 +02:00
|
|
|
}
|
2023-10-23 09:58:36 +02:00
|
|
|
}
|
|
|
|
|
|
2025-07-24 14:27:57 +02:00
|
|
|
pub fn get_user_target_by_username(
|
|
|
|
|
username: &str,
|
|
|
|
|
app_state: &AppState,
|
|
|
|
|
) -> Option<(ProxyUserCredentials, Arc<ConfigTarget>)> {
|
2025-02-20 23:04:06 +01:00
|
|
|
if !username.is_empty() {
|
2025-06-24 12:31:38 +02:00
|
|
|
return app_state.app_config.get_target_for_username(username);
|
2025-02-20 23:04:06 +01:00
|
|
|
}
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-24 14:27:57 +02:00
|
|
|
pub fn get_user_target_by_credentials<'a>(
|
|
|
|
|
username: &str,
|
|
|
|
|
password: &str,
|
|
|
|
|
api_req: &'a UserApiRequest,
|
|
|
|
|
app_state: &'a AppState,
|
|
|
|
|
) -> Option<(ProxyUserCredentials, Arc<ConfigTarget>)> {
|
2023-10-23 09:58:36 +02:00
|
|
|
if !username.is_empty() && !password.is_empty() {
|
2025-06-24 12:31:38 +02:00
|
|
|
app_state.app_config.get_target_for_user(username, password)
|
2023-10-23 09:58:36 +02:00
|
|
|
} else {
|
2023-10-25 21:07:38 +02:00
|
|
|
let token = api_req.token.as_str().trim();
|
2024-05-10 12:01:48 +02:00
|
|
|
if token.is_empty() {
|
2023-10-23 09:58:36 +02:00
|
|
|
None
|
2024-05-10 12:01:48 +02:00
|
|
|
} else {
|
2025-06-24 12:31:38 +02:00
|
|
|
app_state.app_config.get_target_for_user_by_token(token)
|
2023-10-23 09:58:36 +02:00
|
|
|
}
|
|
|
|
|
}
|
2023-11-22 21:06:58 +01:00
|
|
|
}
|
|
|
|
|
|
2025-07-24 14:27:57 +02:00
|
|
|
pub fn get_user_target<'a>(
|
|
|
|
|
api_req: &'a UserApiRequest,
|
|
|
|
|
app_state: &'a AppState,
|
|
|
|
|
) -> Option<(ProxyUserCredentials, Arc<ConfigTarget>)> {
|
2023-11-22 21:06:58 +01:00
|
|
|
let username = api_req.username.as_str().trim();
|
|
|
|
|
let password = api_req.password.as_str().trim();
|
2025-05-14 09:44:56 +02:00
|
|
|
get_user_target_by_credentials(username, password, api_req, app_state)
|
2024-04-01 16:49:12 +02:00
|
|
|
}
|
|
|
|
|
|
2025-03-16 17:08:00 +01:00
|
|
|
pub struct StreamOptions {
|
|
|
|
|
pub stream_retry: bool,
|
|
|
|
|
pub buffer_enabled: bool,
|
|
|
|
|
pub buffer_size: usize,
|
2025-03-20 11:57:32 +01:00
|
|
|
pub pipe_provider_stream: bool,
|
2025-03-16 17:08:00 +01:00
|
|
|
}
|
|
|
|
|
|
2025-05-09 17:13:37 +02:00
|
|
|
/// Constructs a `StreamOptions` object based on the application's reverse proxy configuration.
|
|
|
|
|
///
|
|
|
|
|
/// This function retrieves streaming-related settings from the `AppState`:
|
|
|
|
|
/// - `stream_retry`: whether retrying the stream is enabled,
|
|
|
|
|
/// - `buffer_enabled`: whether stream buffering is enabled,
|
|
|
|
|
/// - `buffer_size`: the size of the stream buffer.
|
|
|
|
|
///
|
|
|
|
|
/// If the reverse proxy or stream settings are not defined, default values are used:
|
|
|
|
|
/// - retry: `false`
|
|
|
|
|
/// - buffering: `false`
|
|
|
|
|
/// - buffer size: `0`
|
|
|
|
|
///
|
|
|
|
|
/// Additionally, it computes `pipe_provider_stream`, which is `true` only if
|
|
|
|
|
/// both retry and buffering are disabled—indicating that the stream can be piped directly
|
|
|
|
|
/// from the provider without additional handling.
|
|
|
|
|
///
|
|
|
|
|
/// Returns a `StreamOptions` instance with the resolved configuration.
|
2025-03-16 17:08:00 +01:00
|
|
|
fn get_stream_options(app_state: &AppState) -> StreamOptions {
|
2025-12-13 13:54:01 +01:00
|
|
|
let (stream_retry, buffer_enabled, buffer_size) = app_state
|
2025-07-24 14:27:57 +02:00
|
|
|
.app_config
|
|
|
|
|
.config
|
|
|
|
|
.load()
|
2025-01-14 10:47:37 +01:00
|
|
|
.reverse_proxy
|
|
|
|
|
.as_ref()
|
|
|
|
|
.and_then(|reverse_proxy| reverse_proxy.stream.as_ref())
|
2025-12-13 13:54:01 +01:00
|
|
|
.map_or((false, false, 0), |stream| {
|
2025-01-14 10:47:37 +01:00
|
|
|
let (buffer_enabled, buffer_size) = stream
|
|
|
|
|
.buffer
|
|
|
|
|
.as_ref()
|
|
|
|
|
.map_or((false, 0), |buffer| (buffer.enabled, buffer.size));
|
2025-07-24 14:27:57 +02:00
|
|
|
(
|
|
|
|
|
stream.retry,
|
|
|
|
|
buffer_enabled,
|
|
|
|
|
buffer_size,
|
|
|
|
|
)
|
2025-01-14 10:47:37 +01:00
|
|
|
});
|
2025-01-27 15:58:57 +01:00
|
|
|
let pipe_provider_stream = !stream_retry && !buffer_enabled;
|
2025-07-24 14:27:57 +02:00
|
|
|
StreamOptions {
|
|
|
|
|
stream_retry,
|
|
|
|
|
buffer_enabled,
|
|
|
|
|
buffer_size,
|
|
|
|
|
pipe_provider_stream,
|
|
|
|
|
}
|
2025-01-27 15:58:57 +01:00
|
|
|
}
|
|
|
|
|
|
2025-03-11 14:56:10 +01:00
|
|
|
// fn get_stream_content_length(provider_response: Option<&(Vec<(String, String)>, StatusCode)>) -> u64 {
|
|
|
|
|
// let content_length = provider_response
|
|
|
|
|
// .as_ref()
|
|
|
|
|
// .and_then(|(headers, _)| headers.iter().find(|(h, _)| h.eq(axum::http::header::CONTENT_LENGTH.as_str())))
|
|
|
|
|
// .and_then(|(_, val)| val.parse::<u64>().ok())
|
|
|
|
|
// .unwrap_or(0);
|
|
|
|
|
// content_length
|
|
|
|
|
// }
|
2025-01-27 15:58:57 +01:00
|
|
|
|
2025-07-24 14:27:57 +02:00
|
|
|
pub fn get_stream_alternative_url(
|
|
|
|
|
stream_url: &str,
|
|
|
|
|
input: &ConfigInput,
|
|
|
|
|
alias_input: &Arc<ProviderConfig>,
|
|
|
|
|
) -> String {
|
|
|
|
|
let Some(input_user_info) = input.get_user_info() else {
|
|
|
|
|
return stream_url.to_owned();
|
|
|
|
|
};
|
|
|
|
|
let Some(alt_input_user_info) = alias_input.get_user_info() else {
|
|
|
|
|
return stream_url.to_owned();
|
|
|
|
|
};
|
2025-03-19 09:54:59 +01:00
|
|
|
|
2025-10-24 20:03:47 +02:00
|
|
|
let modified = stream_url.replacen(&input_user_info.base_url, &alt_input_user_info.base_url, 1);
|
|
|
|
|
let modified = modified.replacen(&input_user_info.username, &alt_input_user_info.username, 1);
|
|
|
|
|
modified.replacen(&input_user_info.password, &alt_input_user_info.password, 1)
|
2025-03-19 09:54:59 +01:00
|
|
|
}
|
|
|
|
|
|
2025-07-24 14:27:57 +02:00
|
|
|
async fn get_redirect_alternative_url<'a>(
|
|
|
|
|
app_state: &AppState,
|
|
|
|
|
redirect_url: &'a str,
|
|
|
|
|
input: &ConfigInput,
|
|
|
|
|
) -> Cow<'a, str> {
|
2025-04-20 10:17:49 +02:00
|
|
|
if let Some((base_url, username, password)) = input.get_matched_config_by_url(redirect_url) {
|
2025-07-24 14:27:57 +02:00
|
|
|
if let Some(provider_cfg) = app_state
|
|
|
|
|
.active_provider
|
|
|
|
|
.get_next_provider(&input.name)
|
|
|
|
|
.await
|
|
|
|
|
{
|
2025-04-20 10:17:49 +02:00
|
|
|
let mut new_url = redirect_url.replacen(base_url, provider_cfg.url.as_str(), 1);
|
|
|
|
|
if let (Some(old_username), Some(old_password)) = (username, password) {
|
2025-07-24 14:27:57 +02:00
|
|
|
if let (Some(new_username), Some(new_password)) = (
|
|
|
|
|
provider_cfg.username.as_ref(),
|
|
|
|
|
provider_cfg.password.as_ref(),
|
|
|
|
|
) {
|
2025-04-20 10:17:49 +02:00
|
|
|
new_url = new_url.replacen(old_username, new_username, 1);
|
|
|
|
|
new_url = new_url.replacen(old_password, new_password, 1);
|
|
|
|
|
return Cow::Owned(new_url);
|
|
|
|
|
}
|
|
|
|
|
// one has credentials the other not, something not right
|
|
|
|
|
return Cow::Borrowed(redirect_url);
|
|
|
|
|
}
|
|
|
|
|
return Cow::Owned(new_url);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Cow::Borrowed(redirect_url)
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-09 17:13:37 +02:00
|
|
|
|
|
|
|
|
/// Determines the appropriate streaming strategy for the given input and stream URL.
|
|
|
|
|
///
|
|
|
|
|
/// This function attempts to acquire a connection to a streaming provider, either using a forced provider
|
|
|
|
|
/// (if specified), or based on the input name. It then selects a corresponding `StreamingOption`:
|
|
|
|
|
///
|
|
|
|
|
/// - If no connections are available (`Exhausted`), it returns a custom stream indicating exhaustion.
|
|
|
|
|
/// - If a connection is available or in a grace period, it constructs a streaming URL accordingly:
|
|
|
|
|
/// - If the provider was forced or matches the input, the original URL is reused.
|
|
|
|
|
/// - Otherwise, an alternative URL is generated based on the provider and input.
|
|
|
|
|
///
|
|
|
|
|
/// The function returns:
|
|
|
|
|
/// - an optional `ProviderConnectionGuard` to manage the connection's lifecycle,
|
|
|
|
|
/// - a `ProviderStreamState` describing how the stream state is,
|
|
|
|
|
/// - and optional HTTP headers to include in the request.
|
|
|
|
|
///
|
|
|
|
|
/// This logic helps abstract the decision-making behind provider selection and stream URL resolution.
|
2025-07-24 14:27:57 +02:00
|
|
|
async fn resolve_streaming_strategy(
|
|
|
|
|
app_state: &AppState,
|
|
|
|
|
stream_url: &str,
|
2025-10-30 13:00:25 +01:00
|
|
|
fingerprint: &Fingerprint,
|
2025-07-24 14:27:57 +02:00
|
|
|
input: &ConfigInput,
|
|
|
|
|
force_provider: Option<&str>,
|
|
|
|
|
) -> StreamingStrategy {
|
2025-05-09 17:13:37 +02:00
|
|
|
// allocate a provider connection
|
2025-12-16 23:03:24 +01:00
|
|
|
let mut provider_connection_handle = match force_provider {
|
2025-11-04 18:01:44 +01:00
|
|
|
Some(provider) => app_state.active_provider.force_exact_acquire_connection(provider, &fingerprint.addr).await,
|
|
|
|
|
None => app_state.active_provider.acquire_connection(&input.name, &fingerprint.addr).await,
|
2025-04-24 18:12:29 +02:00
|
|
|
};
|
2025-07-21 22:17:57 +02:00
|
|
|
|
2025-12-16 23:03:24 +01:00
|
|
|
if provider_connection_handle.is_none() && force_provider.is_none() {
|
|
|
|
|
if try_provision_account_on_exhausted(app_state, input).await {
|
|
|
|
|
provider_connection_handle = app_state
|
|
|
|
|
.active_provider
|
|
|
|
|
.acquire_connection(&input.name, &fingerprint.addr)
|
|
|
|
|
.await;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-04 18:01:44 +01:00
|
|
|
let stream_response_params =
|
|
|
|
|
if let Some(allocation) = provider_connection_handle.as_ref().map(|ph| &ph.allocation) {
|
|
|
|
|
match allocation {
|
|
|
|
|
ProviderAllocation::Exhausted => {
|
2025-11-26 09:56:09 +01:00
|
|
|
debug!("Provider {} is exhausted. No connections allowed.", input.name);
|
2025-11-04 18:01:44 +01:00
|
|
|
let stream = create_provider_connections_exhausted_stream(&app_state.app_config, &[]);
|
|
|
|
|
ProviderStreamState::Custom(stream)
|
|
|
|
|
}
|
|
|
|
|
ProviderAllocation::Available(ref provider)
|
|
|
|
|
| ProviderAllocation::GracePeriod(ref provider) => {
|
|
|
|
|
// force_stream_provider means we keep the url and the provider.
|
|
|
|
|
// If force_stream_provider or the input is the same as the config we don't need to get new url
|
|
|
|
|
let (provider, url) = if force_provider.is_some() || provider.id == input.id {
|
|
|
|
|
(input.name.clone(), stream_url.to_string())
|
|
|
|
|
} else {
|
|
|
|
|
(
|
|
|
|
|
provider.name.clone(),
|
|
|
|
|
get_stream_alternative_url(stream_url, input, provider),
|
|
|
|
|
)
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-03 11:47:36 +01:00
|
|
|
match allocation {
|
|
|
|
|
ProviderAllocation::Exhausted => {
|
|
|
|
|
let stream = create_provider_connections_exhausted_stream(&app_state.app_config, &[]);
|
|
|
|
|
ProviderStreamState::Custom(stream)
|
|
|
|
|
},
|
|
|
|
|
ProviderAllocation::Available(_) => ProviderStreamState::Available(Some(provider), url),
|
|
|
|
|
ProviderAllocation::GracePeriod(_) => ProviderStreamState::GracePeriod(Some(provider), url),
|
2025-11-04 18:01:44 +01:00
|
|
|
}
|
|
|
|
|
}
|
2025-03-19 09:54:59 +01:00
|
|
|
}
|
2025-11-11 10:00:35 +01:00
|
|
|
} else {
|
2025-11-26 13:44:11 +01:00
|
|
|
debug!("Provider {} is exhausted. No connections allowed.", input.name);
|
2025-11-11 10:00:35 +01:00
|
|
|
let stream = create_provider_connections_exhausted_stream(&app_state.app_config, &[]);
|
|
|
|
|
ProviderStreamState::Custom(stream)
|
|
|
|
|
};
|
2025-11-04 18:01:44 +01:00
|
|
|
|
|
|
|
|
|
2025-05-09 17:13:37 +02:00
|
|
|
StreamingStrategy {
|
2025-11-04 18:01:44 +01:00
|
|
|
provider_handle: provider_connection_handle,
|
2025-05-09 17:13:37 +02:00
|
|
|
provider_stream_state: stream_response_params,
|
2025-06-25 14:55:44 +02:00
|
|
|
input_headers: Some(input.headers.clone()),
|
2025-05-09 17:13:37 +02:00
|
|
|
}
|
2025-03-19 09:54:59 +01:00
|
|
|
}
|
|
|
|
|
|
2025-07-24 14:27:57 +02:00
|
|
|
fn get_grace_period_millis(
|
|
|
|
|
connection_permission: UserConnectionPermission,
|
|
|
|
|
stream_response_params: &ProviderStreamState,
|
|
|
|
|
config_grace_period_millis: u64,
|
|
|
|
|
) -> u64 {
|
|
|
|
|
if config_grace_period_millis > 0
|
|
|
|
|
&& (
|
2025-11-11 10:00:35 +01:00
|
|
|
matches!(stream_response_params, ProviderStreamState::GracePeriod(_, _)) // provider grace period
|
2025-07-24 14:27:57 +02:00
|
|
|
|| connection_permission == UserConnectionPermission::GracePeriod
|
2025-11-11 10:00:35 +01:00
|
|
|
// user grace period
|
|
|
|
|
)
|
2025-07-24 14:27:57 +02:00
|
|
|
{
|
|
|
|
|
config_grace_period_millis
|
|
|
|
|
} else {
|
|
|
|
|
0
|
|
|
|
|
}
|
2025-04-17 12:11:51 +02:00
|
|
|
}
|
|
|
|
|
|
2025-12-03 11:47:36 +01:00
|
|
|
#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
|
2025-07-24 14:27:57 +02:00
|
|
|
async fn create_stream_response_details(
|
2025-11-10 01:04:13 +01:00
|
|
|
app_state: &Arc<AppState>,
|
2025-07-24 14:27:57 +02:00
|
|
|
stream_options: &StreamOptions,
|
|
|
|
|
stream_url: &str,
|
2025-10-30 13:00:25 +01:00
|
|
|
fingerprint: &Fingerprint,
|
2025-07-24 14:27:57 +02:00
|
|
|
req_headers: &HeaderMap,
|
|
|
|
|
input: &ConfigInput,
|
|
|
|
|
item_type: PlaylistItemType,
|
|
|
|
|
share_stream: bool,
|
|
|
|
|
connection_permission: UserConnectionPermission,
|
|
|
|
|
force_provider: Option<&str>,
|
|
|
|
|
) -> StreamDetails {
|
2025-10-30 13:00:25 +01:00
|
|
|
let mut streaming_strategy = resolve_streaming_strategy(app_state, stream_url, fingerprint, input, force_provider).await;
|
2025-07-24 14:27:57 +02:00
|
|
|
let config_grace_period_millis = app_state
|
|
|
|
|
.app_config
|
|
|
|
|
.config
|
|
|
|
|
.load()
|
|
|
|
|
.reverse_proxy
|
|
|
|
|
.as_ref()
|
|
|
|
|
.and_then(|r| r.stream.as_ref())
|
|
|
|
|
.map_or_else(default_grace_period_millis, |s| s.grace_period_millis);
|
|
|
|
|
let grace_period_millis = get_grace_period_millis(
|
|
|
|
|
connection_permission,
|
|
|
|
|
&streaming_strategy.provider_stream_state,
|
|
|
|
|
config_grace_period_millis,
|
|
|
|
|
);
|
2025-10-29 16:40:17 +01:00
|
|
|
let guard_provider_name = streaming_strategy
|
2025-11-04 18:01:44 +01:00
|
|
|
.provider_handle
|
2025-10-25 20:40:58 +02:00
|
|
|
.as_ref()
|
2025-11-04 18:01:44 +01:00
|
|
|
.and_then(|guard| guard.allocation.get_provider_name());
|
2025-10-25 20:40:58 +02:00
|
|
|
|
2025-12-03 11:47:36 +01:00
|
|
|
if let ProviderStreamState::GracePeriod(ref provider_grace_check, ref stream_url) = streaming_strategy.provider_stream_state {
|
|
|
|
|
tokio::time::sleep(tokio::time::Duration::from_millis(grace_period_millis)).await;
|
|
|
|
|
if let Some(provider_name) = provider_grace_check.as_ref() {
|
|
|
|
|
if app_state.active_provider.is_over_limit(provider_name).await {
|
|
|
|
|
app_state.connection_manager.update_stream_detail(&fingerprint.addr, CustomVideoStreamType::ProviderConnectionsExhausted).await;
|
|
|
|
|
debug!("Provider connections exhausted after grace period for provider: {provider_name}");
|
|
|
|
|
let stream = create_provider_connections_exhausted_stream(&app_state.app_config, &[]);
|
|
|
|
|
streaming_strategy.provider_stream_state = ProviderStreamState::Custom(stream);
|
|
|
|
|
let provider_handle = streaming_strategy.provider_handle.take();
|
|
|
|
|
app_state.connection_manager.release_provider_handle(provider_handle).await;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
streaming_strategy.provider_stream_state = ProviderStreamState::Available(provider_grace_check.clone(), stream_url.clone());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-09 17:13:37 +02:00
|
|
|
match streaming_strategy.provider_stream_state {
|
2025-07-17 09:15:41 +02:00
|
|
|
// custom stream means we display our own stream like connection exhausted, channel-unavailable...
|
2025-05-09 17:13:37 +02:00
|
|
|
ProviderStreamState::Custom(provider_stream) => {
|
2025-03-20 11:57:32 +01:00
|
|
|
let (stream, stream_info) = provider_stream;
|
|
|
|
|
StreamDetails {
|
|
|
|
|
stream,
|
|
|
|
|
stream_info,
|
2025-10-29 16:40:17 +01:00
|
|
|
provider_name: guard_provider_name.clone(),
|
2025-03-27 12:19:07 +01:00
|
|
|
grace_period_millis,
|
2025-03-20 11:57:32 +01:00
|
|
|
reconnect_flag: None,
|
2025-11-04 18:01:44 +01:00
|
|
|
provider_handle: streaming_strategy.provider_handle.clone(),
|
2025-03-20 11:57:32 +01:00
|
|
|
}
|
|
|
|
|
}
|
2025-10-29 16:40:17 +01:00
|
|
|
ProviderStreamState::Available(_provider_name, request_url)
|
|
|
|
|
| ProviderStreamState::GracePeriod(_provider_name, request_url) => {
|
2025-03-20 11:57:32 +01:00
|
|
|
let parsed_url = Url::parse(&request_url);
|
|
|
|
|
let ((stream, stream_info), reconnect_flag) = if let Ok(url) = parsed_url {
|
2025-12-12 14:35:00 +01:00
|
|
|
let disabled_headers = app_state.get_disabled_headers();
|
2025-07-24 14:27:57 +02:00
|
|
|
let provider_stream_factory_options = ProviderStreamFactoryOptions::new(
|
2025-11-10 01:04:13 +01:00
|
|
|
fingerprint.addr,
|
2025-07-24 14:27:57 +02:00
|
|
|
item_type,
|
|
|
|
|
share_stream,
|
|
|
|
|
stream_options,
|
|
|
|
|
&url,
|
|
|
|
|
req_headers,
|
|
|
|
|
streaming_strategy.input_headers.as_ref(),
|
2025-10-31 17:04:35 +01:00
|
|
|
disabled_headers.as_ref(),
|
2025-07-24 14:27:57 +02:00
|
|
|
);
|
2025-05-09 17:13:37 +02:00
|
|
|
let reconnect_flag = provider_stream_factory_options.get_reconnect_flag_clone();
|
2025-07-24 14:27:57 +02:00
|
|
|
let provider_stream = match create_provider_stream(
|
2025-11-10 01:04:13 +01:00
|
|
|
app_state,
|
|
|
|
|
&app_state.http_client.load(),
|
2025-07-24 14:27:57 +02:00
|
|
|
provider_stream_factory_options,
|
|
|
|
|
)
|
2025-11-11 10:00:35 +01:00
|
|
|
.await
|
2025-07-24 14:27:57 +02:00
|
|
|
{
|
2025-05-09 17:13:37 +02:00
|
|
|
None => (None, None),
|
2025-07-24 14:27:57 +02:00
|
|
|
Some((stream, info)) => (Some(stream), info),
|
2025-05-09 17:13:37 +02:00
|
|
|
};
|
|
|
|
|
(provider_stream, Some(reconnect_flag))
|
2025-03-20 11:57:32 +01:00
|
|
|
} else {
|
|
|
|
|
((None, None), None)
|
|
|
|
|
};
|
2025-03-19 09:54:59 +01:00
|
|
|
|
2025-03-20 11:57:32 +01:00
|
|
|
if log_enabled!(log::Level::Debug) {
|
2025-11-10 01:04:13 +01:00
|
|
|
if let Some((headers, status_code, response_url, _custom_video_type)) = stream_info.as_ref() {
|
2025-03-20 11:57:32 +01:00
|
|
|
debug!(
|
|
|
|
|
"Responding stream request {} with status {}, headers {:?}",
|
2025-07-24 14:27:57 +02:00
|
|
|
sanitize_sensitive_info(
|
|
|
|
|
response_url.as_ref().map_or(stream_url, |s| s.as_str())
|
|
|
|
|
),
|
2025-03-20 11:57:32 +01:00
|
|
|
status_code,
|
|
|
|
|
headers
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-03-19 09:54:59 +01:00
|
|
|
|
2025-10-25 20:40:58 +02:00
|
|
|
// if we have no stream, we should release the provider
|
2025-11-10 01:30:23 +01:00
|
|
|
let provider_handle = if stream.is_none() {
|
2025-11-10 01:04:13 +01:00
|
|
|
let provider_handle = streaming_strategy.provider_handle.take();
|
|
|
|
|
app_state.connection_manager.release_provider_handle(provider_handle).await;
|
2025-10-25 20:40:58 +02:00
|
|
|
error!("Cant open stream {}", sanitize_sensitive_info(&request_url));
|
2025-11-10 01:04:13 +01:00
|
|
|
None
|
|
|
|
|
} else {
|
|
|
|
|
streaming_strategy.provider_handle.take()
|
|
|
|
|
};
|
2025-10-25 20:40:58 +02:00
|
|
|
|
2025-03-20 11:57:32 +01:00
|
|
|
StreamDetails {
|
|
|
|
|
stream,
|
|
|
|
|
stream_info,
|
2025-10-29 16:40:17 +01:00
|
|
|
provider_name: guard_provider_name.clone(),
|
2025-03-27 12:19:07 +01:00
|
|
|
grace_period_millis,
|
2025-03-20 11:57:32 +01:00
|
|
|
reconnect_flag,
|
2025-11-10 01:30:23 +01:00
|
|
|
provider_handle,
|
2025-03-20 11:57:32 +01:00
|
|
|
}
|
2025-03-19 09:54:59 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-19 13:50:41 +02:00
|
|
|
pub struct RedirectParams<'a, P>
|
|
|
|
|
where
|
2025-04-19 15:01:23 +02:00
|
|
|
P: PlaylistEntry,
|
2025-04-19 13:50:41 +02:00
|
|
|
{
|
|
|
|
|
pub item: &'a P,
|
|
|
|
|
pub provider_id: Option<u32>,
|
|
|
|
|
pub cluster: XtreamCluster,
|
|
|
|
|
pub target_type: TargetType,
|
|
|
|
|
pub target: &'a ConfigTarget,
|
2025-04-24 18:12:29 +02:00
|
|
|
pub input: &'a ConfigInput,
|
2025-04-19 13:50:41 +02:00
|
|
|
pub user: &'a ProxyUserCredentials,
|
|
|
|
|
pub stream_ext: Option<&'a str>,
|
2025-06-03 18:25:15 +02:00
|
|
|
pub req_context: ApiStreamContext,
|
2025-04-19 13:50:41 +02:00
|
|
|
pub action_path: &'a str,
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-19 15:01:23 +02:00
|
|
|
impl<P> RedirectParams<'_, P>
|
2025-04-19 13:50:41 +02:00
|
|
|
where
|
2025-04-19 15:01:23 +02:00
|
|
|
P: PlaylistEntry,
|
2025-04-19 13:50:41 +02:00
|
|
|
{
|
2025-04-19 15:01:23 +02:00
|
|
|
pub fn get_query_path(&self, provider_id: u32, url: &str) -> String {
|
|
|
|
|
let extension = self.stream_ext.map_or_else(
|
2025-07-24 14:27:57 +02:00
|
|
|
|| {
|
|
|
|
|
extract_extension_from_url(url)
|
|
|
|
|
.map_or_else(String::new, std::string::ToString::to_string)
|
|
|
|
|
},
|
|
|
|
|
std::string::ToString::to_string,
|
|
|
|
|
);
|
2025-04-19 15:01:23 +02:00
|
|
|
|
2025-07-17 09:15:41 +02:00
|
|
|
// if there is an action_path (like for timeshift duration/start), it will be added in front of the stream_id
|
2025-04-19 15:01:23 +02:00
|
|
|
if self.action_path.is_empty() {
|
|
|
|
|
format!("{provider_id}{extension}")
|
|
|
|
|
} else {
|
2025-07-08 18:23:03 +02:00
|
|
|
format!("{}/{provider_id}{extension}", trim_slash(self.action_path))
|
2025-04-19 15:01:23 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-04-19 13:50:41 +02:00
|
|
|
|
2025-07-24 14:27:57 +02:00
|
|
|
pub async fn redirect_response<'a, P>(
|
|
|
|
|
app_state: &AppState,
|
|
|
|
|
params: &'a RedirectParams<'a, P>,
|
|
|
|
|
) -> Option<impl IntoResponse + Send>
|
2025-04-19 15:01:23 +02:00
|
|
|
where
|
|
|
|
|
P: PlaylistEntry,
|
|
|
|
|
{
|
2025-04-19 13:50:41 +02:00
|
|
|
let item_type = params.item.get_item_type();
|
|
|
|
|
let provider_url = ¶ms.item.get_provider_url();
|
|
|
|
|
|
2025-07-24 14:27:57 +02:00
|
|
|
let redirect_request =
|
|
|
|
|
params.user.proxy.is_redirect(item_type) || params.target.is_force_redirect(item_type);
|
|
|
|
|
let is_hls_request =
|
|
|
|
|
item_type == PlaylistItemType::LiveHls || params.stream_ext == Some(HLS_EXT);
|
2025-10-24 19:24:36 +02:00
|
|
|
let is_dash_request = (!is_hls_request && item_type == PlaylistItemType::LiveDash)
|
2025-07-24 14:27:57 +02:00
|
|
|
|| params.stream_ext == Some(DASH_EXT);
|
2025-04-19 13:50:41 +02:00
|
|
|
|
|
|
|
|
if params.target_type == TargetType::M3u {
|
|
|
|
|
if redirect_request || is_dash_request {
|
2025-07-24 14:27:57 +02:00
|
|
|
let redirect_url = if is_hls_request {
|
|
|
|
|
&replace_url_extension(provider_url, HLS_EXT)
|
|
|
|
|
} else {
|
|
|
|
|
provider_url
|
|
|
|
|
};
|
|
|
|
|
let redirect_url = if is_dash_request {
|
|
|
|
|
&replace_url_extension(redirect_url, DASH_EXT)
|
|
|
|
|
} else {
|
|
|
|
|
redirect_url
|
|
|
|
|
};
|
|
|
|
|
let redirect_url =
|
|
|
|
|
get_redirect_alternative_url(app_state, redirect_url, params.input).await;
|
|
|
|
|
debug_if_enabled!(
|
|
|
|
|
"Redirecting stream request to {}",
|
|
|
|
|
sanitize_sensitive_info(&redirect_url)
|
|
|
|
|
);
|
2025-04-24 18:12:29 +02:00
|
|
|
return Some(redirect(&redirect_url).into_response());
|
2025-04-19 13:50:41 +02:00
|
|
|
}
|
|
|
|
|
} else if params.target_type == TargetType::Xtream {
|
|
|
|
|
let Some(provider_id) = params.provider_id else {
|
2025-07-15 12:28:25 +02:00
|
|
|
return Some(axum::http::StatusCode::BAD_REQUEST.into_response());
|
2025-04-19 13:50:41 +02:00
|
|
|
};
|
|
|
|
|
|
2025-04-19 15:01:23 +02:00
|
|
|
if redirect_request {
|
2025-07-17 09:15:41 +02:00
|
|
|
// handle redirect for series but why?
|
2025-04-19 15:01:23 +02:00
|
|
|
if params.cluster == XtreamCluster::Series {
|
|
|
|
|
let ext = params.stream_ext.unwrap_or_default();
|
2025-04-24 18:12:29 +02:00
|
|
|
let url = params.input.url.as_str();
|
|
|
|
|
let username = params.input.username.as_ref().map_or("", |v| v);
|
|
|
|
|
let password = params.input.password.as_ref().map_or("", |v| v);
|
2025-04-19 15:01:23 +02:00
|
|
|
// TODO do i need action_path like for timeshift ?
|
|
|
|
|
let stream_url = format!("{url}/series/{username}/{password}/{provider_id}{ext}");
|
2025-07-24 14:27:57 +02:00
|
|
|
debug_if_enabled!(
|
|
|
|
|
"Redirecting stream request to {}",
|
|
|
|
|
sanitize_sensitive_info(&stream_url)
|
|
|
|
|
);
|
2025-04-19 15:01:23 +02:00
|
|
|
return Some(redirect(&stream_url).into_response());
|
|
|
|
|
}
|
2025-04-19 13:50:41 +02:00
|
|
|
|
2025-04-19 15:01:23 +02:00
|
|
|
let target_name = params.target.name.as_str();
|
|
|
|
|
let virtual_id = params.item.get_virtual_id();
|
2025-07-24 14:27:57 +02:00
|
|
|
let stream_url = match get_xtream_player_api_stream_url(
|
|
|
|
|
params.input,
|
|
|
|
|
params.req_context,
|
|
|
|
|
¶ms.get_query_path(provider_id, provider_url),
|
|
|
|
|
provider_url,
|
|
|
|
|
) {
|
2025-04-19 15:01:23 +02:00
|
|
|
None => {
|
|
|
|
|
error!("Cant find stream url for target {target_name}, context {}, stream_id {virtual_id}", params.req_context);
|
2025-07-15 12:28:25 +02:00
|
|
|
return Some(axum::http::StatusCode::BAD_REQUEST.into_response());
|
2025-04-19 15:01:23 +02:00
|
|
|
}
|
|
|
|
|
Some(url) => {
|
2025-07-24 14:27:57 +02:00
|
|
|
match app_state
|
|
|
|
|
.active_provider
|
|
|
|
|
.get_next_provider(¶ms.input.name)
|
|
|
|
|
.await
|
|
|
|
|
{
|
|
|
|
|
Some(provider_cfg) => {
|
|
|
|
|
get_stream_alternative_url(&url, params.input, &provider_cfg)
|
|
|
|
|
}
|
2025-04-19 15:01:23 +02:00
|
|
|
None => url,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
2025-04-19 13:50:41 +02:00
|
|
|
|
2025-04-19 15:01:23 +02:00
|
|
|
// hls or dash redirect
|
|
|
|
|
if is_dash_request {
|
2025-07-24 14:27:57 +02:00
|
|
|
let redirect_url = if is_hls_request {
|
|
|
|
|
&replace_url_extension(&stream_url, HLS_EXT)
|
|
|
|
|
} else {
|
|
|
|
|
&replace_url_extension(&stream_url, DASH_EXT)
|
|
|
|
|
};
|
|
|
|
|
debug_if_enabled!(
|
|
|
|
|
"Redirecting stream request to {}",
|
|
|
|
|
sanitize_sensitive_info(redirect_url)
|
|
|
|
|
);
|
2025-04-19 15:01:23 +02:00
|
|
|
return Some(redirect(redirect_url).into_response());
|
2025-04-19 13:50:41 +02:00
|
|
|
}
|
|
|
|
|
|
2025-07-24 14:27:57 +02:00
|
|
|
debug_if_enabled!(
|
|
|
|
|
"Redirecting stream request to {}",
|
|
|
|
|
sanitize_sensitive_info(&stream_url)
|
|
|
|
|
);
|
2025-04-19 13:50:41 +02:00
|
|
|
return Some(redirect(&stream_url).into_response());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-22 17:08:31 +02:00
|
|
|
fn is_throttled_stream(item_type: PlaylistItemType, throttle_kbps: usize) -> bool {
|
2025-07-24 14:27:57 +02:00
|
|
|
throttle_kbps > 0
|
|
|
|
|
&& matches!(
|
|
|
|
|
item_type,
|
|
|
|
|
PlaylistItemType::Video
|
|
|
|
|
| PlaylistItemType::Series
|
|
|
|
|
| PlaylistItemType::SeriesInfo
|
|
|
|
|
| PlaylistItemType::Catchup
|
|
|
|
|
)
|
2025-04-22 17:08:31 +02:00
|
|
|
}
|
|
|
|
|
|
2025-07-24 14:27:57 +02:00
|
|
|
fn prepare_body_stream(
|
|
|
|
|
app_state: &AppState,
|
|
|
|
|
item_type: PlaylistItemType,
|
|
|
|
|
stream: ActiveClientStream,
|
|
|
|
|
) -> axum::body::Body {
|
2025-04-24 18:12:29 +02:00
|
|
|
let throttle_kbps = usize::try_from(get_stream_throttle(app_state)).unwrap_or_default();
|
|
|
|
|
let body_stream = if is_throttled_stream(item_type, throttle_kbps) {
|
|
|
|
|
axum::body::Body::from_stream(ThrottledStream::new(stream.boxed(), throttle_kbps))
|
|
|
|
|
} else {
|
|
|
|
|
axum::body::Body::from_stream(stream)
|
|
|
|
|
};
|
|
|
|
|
body_stream
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-22 17:08:31 +02:00
|
|
|
/// # Panics
|
2025-07-24 14:27:57 +02:00
|
|
|
pub async fn force_provider_stream_response(
|
2025-10-30 13:00:25 +01:00
|
|
|
fingerprint: &Fingerprint,
|
2025-11-10 01:04:13 +01:00
|
|
|
app_state: &Arc<AppState>,
|
2025-07-24 14:27:57 +02:00
|
|
|
user_session: &UserSession,
|
2025-10-24 19:47:39 +02:00
|
|
|
mut stream_channel: StreamChannel,
|
2025-07-24 14:27:57 +02:00
|
|
|
req_headers: &HeaderMap,
|
|
|
|
|
input: &ConfigInput,
|
|
|
|
|
user: &ProxyUserCredentials,
|
|
|
|
|
) -> impl IntoResponse + Send {
|
2025-04-30 09:55:08 +02:00
|
|
|
let stream_options = get_stream_options(app_state);
|
|
|
|
|
let share_stream = false;
|
|
|
|
|
let connection_permission = UserConnectionPermission::Allowed;
|
2025-10-24 18:36:13 +02:00
|
|
|
let item_type = stream_channel.item_type;
|
2025-04-22 17:08:31 +02:00
|
|
|
|
2025-11-10 01:04:13 +01:00
|
|
|
let stream_details = create_stream_response_details(
|
2025-07-24 14:27:57 +02:00
|
|
|
app_state,
|
|
|
|
|
&stream_options,
|
|
|
|
|
&user_session.stream_url,
|
2025-10-30 13:00:25 +01:00
|
|
|
fingerprint,
|
2025-07-24 14:27:57 +02:00
|
|
|
req_headers,
|
|
|
|
|
input,
|
|
|
|
|
item_type,
|
|
|
|
|
share_stream,
|
|
|
|
|
connection_permission,
|
|
|
|
|
Some(&user_session.provider),
|
|
|
|
|
)
|
2025-11-11 10:00:35 +01:00
|
|
|
.await;
|
2025-04-22 17:08:31 +02:00
|
|
|
|
2025-04-30 09:55:08 +02:00
|
|
|
if stream_details.has_stream() {
|
2025-07-24 14:27:57 +02:00
|
|
|
let provider_response = stream_details
|
|
|
|
|
.stream_info
|
|
|
|
|
.as_ref()
|
2025-11-10 01:04:13 +01:00
|
|
|
.map(|(h, sc, url, cvt)| (h.clone(), *sc, url.clone(), *cvt));
|
2025-07-24 14:27:57 +02:00
|
|
|
app_state
|
|
|
|
|
.active_users
|
2025-10-30 13:00:25 +01:00
|
|
|
.update_session_addr(&user.username, &user_session.token, &fingerprint.addr)
|
2025-09-08 13:16:57 +02:00
|
|
|
.await;
|
2025-10-24 19:47:39 +02:00
|
|
|
stream_channel.shared = share_stream;
|
2025-07-24 14:27:57 +02:00
|
|
|
let stream =
|
2025-11-19 23:00:25 -06:00
|
|
|
ActiveClientStream::new(stream_details, app_state, user, connection_permission, fingerprint, stream_channel, Some(&user_session.token), req_headers)
|
2025-09-08 13:16:57 +02:00
|
|
|
.await;
|
2025-07-24 14:27:57 +02:00
|
|
|
|
|
|
|
|
let (status_code, header_map) =
|
2025-11-10 01:04:13 +01:00
|
|
|
get_stream_response_with_headers(provider_response.map(|(h, s, _, _)| (h, s)));
|
2025-05-09 17:13:37 +02:00
|
|
|
let mut response = axum::response::Response::builder().status(status_code);
|
2025-04-30 09:55:08 +02:00
|
|
|
for (key, value) in &header_map {
|
|
|
|
|
response = response.header(key, value);
|
2025-04-22 17:08:31 +02:00
|
|
|
}
|
2025-04-30 09:55:08 +02:00
|
|
|
|
|
|
|
|
let body_stream = prepare_body_stream(app_state, item_type, stream);
|
2025-07-24 14:27:57 +02:00
|
|
|
debug_if_enabled!(
|
|
|
|
|
"Streaming provider forced stream request from {}",
|
|
|
|
|
sanitize_sensitive_info(&user_session.stream_url)
|
|
|
|
|
);
|
2025-07-22 11:32:46 +02:00
|
|
|
return try_unwrap_body!(response.body(body_stream));
|
2025-04-22 17:08:31 +02:00
|
|
|
}
|
2025-11-10 01:04:13 +01:00
|
|
|
|
|
|
|
|
app_state.connection_manager.release_provider_handle(stream_details.provider_handle).await;
|
2025-07-24 14:27:57 +02:00
|
|
|
if let (Some(stream), _stream_info) = create_channel_unavailable_stream(
|
|
|
|
|
&app_state.app_config,
|
|
|
|
|
&[],
|
2025-11-06 15:02:22 +01:00
|
|
|
axum::http::StatusCode::SERVICE_UNAVAILABLE,
|
2025-07-24 14:27:57 +02:00
|
|
|
) {
|
2025-11-10 01:04:13 +01:00
|
|
|
app_state.connection_manager.update_stream_detail(&fingerprint.addr, CustomVideoStreamType::ChannelUnavailable).await;
|
2025-05-09 17:13:37 +02:00
|
|
|
debug!("Streaming custom stream");
|
2025-07-24 14:27:57 +02:00
|
|
|
try_unwrap_body!(axum::response::Response::builder()
|
|
|
|
|
.status(axum::http::StatusCode::OK)
|
|
|
|
|
.body(axum::body::Body::from_stream(stream)))
|
2025-05-09 17:13:37 +02:00
|
|
|
} else {
|
2025-07-15 12:28:25 +02:00
|
|
|
axum::http::StatusCode::BAD_REQUEST.into_response()
|
2025-05-09 17:13:37 +02:00
|
|
|
}
|
2025-04-22 17:08:31 +02:00
|
|
|
}
|
|
|
|
|
|
2025-04-10 10:43:14 +02:00
|
|
|
/// # Panics
|
2025-07-24 14:27:57 +02:00
|
|
|
#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
|
|
|
|
|
pub async fn stream_response(
|
2025-10-30 13:00:25 +01:00
|
|
|
fingerprint: &Fingerprint,
|
2025-11-10 01:04:13 +01:00
|
|
|
app_state: &Arc<AppState>,
|
2025-07-24 14:27:57 +02:00
|
|
|
session_token: &str,
|
2025-10-24 19:47:39 +02:00
|
|
|
mut stream_channel: StreamChannel,
|
2025-07-24 14:27:57 +02:00
|
|
|
stream_url: &str,
|
|
|
|
|
req_headers: &HeaderMap,
|
|
|
|
|
input: &ConfigInput,
|
|
|
|
|
target: &ConfigTarget,
|
|
|
|
|
user: &ProxyUserCredentials,
|
|
|
|
|
connection_permission: UserConnectionPermission,
|
|
|
|
|
) -> impl IntoResponse + Send {
|
|
|
|
|
if log_enabled!(log::Level::Trace) {
|
|
|
|
|
trace!("Try to open stream {}", sanitize_sensitive_info(stream_url));
|
|
|
|
|
}
|
2025-01-27 15:58:57 +01:00
|
|
|
|
2025-04-16 18:04:57 +02:00
|
|
|
if connection_permission == UserConnectionPermission::Exhausted {
|
2025-07-24 14:27:57 +02:00
|
|
|
return create_custom_video_stream_response(
|
2025-11-10 01:04:13 +01:00
|
|
|
app_state,
|
|
|
|
|
&fingerprint.addr,
|
2025-07-24 14:27:57 +02:00
|
|
|
CustomVideoStreamType::UserConnectionsExhausted,
|
2025-11-10 01:04:13 +01:00
|
|
|
).await
|
2025-11-11 10:00:35 +01:00
|
|
|
.into_response();
|
2025-04-16 18:04:57 +02:00
|
|
|
}
|
|
|
|
|
|
2025-10-24 18:36:13 +02:00
|
|
|
let virtual_id = stream_channel.virtual_id;
|
|
|
|
|
let item_type = stream_channel.item_type;
|
|
|
|
|
|
2025-01-27 15:58:57 +01:00
|
|
|
let share_stream = is_stream_share_enabled(item_type, target);
|
2025-11-30 11:51:45 +01:00
|
|
|
let _shared_lock = if share_stream {
|
|
|
|
|
let write_lock = app_state.app_config.file_locks.write_lock_str(stream_url).await;
|
|
|
|
|
|
2025-07-24 14:27:57 +02:00
|
|
|
if let Some(value) =
|
2025-11-20 13:00:01 +01:00
|
|
|
try_shared_stream_response_if_any(app_state, stream_url, fingerprint, user, connection_permission, stream_channel.clone(), session_token, req_headers).await
|
2025-07-24 14:27:57 +02:00
|
|
|
{
|
2025-03-11 14:56:10 +01:00
|
|
|
return value.into_response();
|
2025-01-27 15:58:57 +01:00
|
|
|
}
|
2025-11-30 11:51:45 +01:00
|
|
|
Some(write_lock)
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
};
|
2025-01-14 10:47:37 +01:00
|
|
|
|
2025-03-19 09:54:59 +01:00
|
|
|
let stream_options = get_stream_options(app_state);
|
2025-07-24 14:27:57 +02:00
|
|
|
let mut stream_details = create_stream_response_details(
|
|
|
|
|
app_state,
|
|
|
|
|
&stream_options,
|
|
|
|
|
stream_url,
|
2025-10-30 13:00:25 +01:00
|
|
|
fingerprint,
|
2025-07-24 14:27:57 +02:00
|
|
|
req_headers,
|
|
|
|
|
input,
|
|
|
|
|
item_type,
|
|
|
|
|
share_stream,
|
|
|
|
|
connection_permission,
|
|
|
|
|
None,
|
2025-12-03 11:47:36 +01:00
|
|
|
).await;
|
2025-11-06 15:02:22 +01:00
|
|
|
|
2025-03-20 11:57:32 +01:00
|
|
|
if stream_details.has_stream() {
|
2025-03-19 09:54:59 +01:00
|
|
|
// let content_length = get_stream_content_length(provider_response.as_ref());
|
2025-07-24 14:27:57 +02:00
|
|
|
let provider_response = stream_details
|
|
|
|
|
.stream_info
|
|
|
|
|
.as_ref()
|
2025-11-10 01:04:13 +01:00
|
|
|
.map(|(h, sc, response_url, cvt)| (h.clone(), *sc, response_url.clone(), *cvt));
|
2025-10-25 20:40:58 +02:00
|
|
|
let provider_name = stream_details.provider_name.clone();
|
2025-07-24 14:27:57 +02:00
|
|
|
|
2025-11-04 21:46:44 +01:00
|
|
|
let provider_handle = if share_stream {
|
2025-11-04 18:01:44 +01:00
|
|
|
stream_details.provider_handle.take()
|
2025-07-24 14:27:57 +02:00
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
};
|
2025-12-03 11:47:36 +01:00
|
|
|
|
|
|
|
|
let mut is_stream_shared = share_stream;
|
|
|
|
|
if let Some((_header, _status_code, _url, Some(_custom_video))) = stream_details.stream_info.as_ref() {
|
|
|
|
|
if stream_details.stream.is_some() {
|
|
|
|
|
is_stream_shared = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
stream_channel.shared = is_stream_shared;
|
2025-07-24 14:27:57 +02:00
|
|
|
let stream =
|
2025-11-20 13:00:01 +01:00
|
|
|
ActiveClientStream::new(stream_details, app_state, user, connection_permission, fingerprint, stream_channel, Some(session_token), req_headers)
|
2025-09-08 13:16:57 +02:00
|
|
|
.await;
|
2025-12-03 11:47:36 +01:00
|
|
|
let stream_resp = if is_stream_shared {
|
|
|
|
|
debug_if_enabled!("Streaming shared stream request from {}",sanitize_sensitive_info(stream_url));
|
2025-03-19 09:54:59 +01:00
|
|
|
// Shared Stream response
|
2025-07-24 14:27:57 +02:00
|
|
|
let shared_headers = provider_response
|
|
|
|
|
.as_ref()
|
2025-11-10 01:04:13 +01:00
|
|
|
.map_or_else(Vec::new, |(h, _, _, _)| h.clone());
|
2025-07-27 20:47:35 +02:00
|
|
|
|
2025-10-29 16:40:17 +01:00
|
|
|
if let Some((broadcast_stream, _shared_provider)) = SharedStreamManager::register_shared_stream(
|
2025-07-24 14:27:57 +02:00
|
|
|
app_state,
|
|
|
|
|
stream_url,
|
|
|
|
|
stream,
|
2025-11-01 00:26:30 +01:00
|
|
|
&fingerprint.addr,
|
2025-07-24 14:27:57 +02:00
|
|
|
shared_headers,
|
|
|
|
|
stream_options.buffer_size,
|
2025-11-04 21:46:44 +01:00
|
|
|
provider_handle,
|
2025-09-08 13:16:57 +02:00
|
|
|
)
|
2025-11-11 10:00:35 +01:00
|
|
|
.await
|
2025-09-08 13:16:57 +02:00
|
|
|
{
|
2025-07-24 14:27:57 +02:00
|
|
|
let (status_code, header_map) =
|
2025-11-10 01:04:13 +01:00
|
|
|
get_stream_response_with_headers(provider_response.map(|(h, s, _, _)| (h, s)));
|
2025-07-24 14:27:57 +02:00
|
|
|
let mut response = axum::response::Response::builder().status(status_code);
|
2025-03-11 14:56:10 +01:00
|
|
|
for (key, value) in &header_map {
|
|
|
|
|
response = response.header(key, value);
|
|
|
|
|
}
|
2025-07-22 11:32:46 +02:00
|
|
|
try_unwrap_body!(response.body(axum::body::Body::from_stream(broadcast_stream)))
|
2025-03-19 09:54:59 +01:00
|
|
|
} else {
|
|
|
|
|
axum::http::StatusCode::BAD_REQUEST.into_response()
|
|
|
|
|
}
|
|
|
|
|
} else {
|
2025-07-24 14:27:57 +02:00
|
|
|
let session_url = provider_response
|
|
|
|
|
.as_ref()
|
2025-11-10 01:04:13 +01:00
|
|
|
.and_then(|(_, _, u, _)| u.as_ref())
|
2025-07-24 14:27:57 +02:00
|
|
|
.map_or_else(
|
|
|
|
|
|| Cow::Borrowed(stream_url),
|
|
|
|
|
|url| Cow::Owned(url.to_string()),
|
|
|
|
|
);
|
2025-06-04 21:40:14 +02:00
|
|
|
if log_enabled!(log::Level::Debug) {
|
|
|
|
|
if session_url.eq(&stream_url) {
|
2025-12-03 11:47:36 +01:00
|
|
|
debug!("Streaming stream request from {}", sanitize_sensitive_info(stream_url)
|
2025-07-24 14:27:57 +02:00
|
|
|
);
|
2025-06-04 21:40:14 +02:00
|
|
|
} else {
|
2025-07-24 14:27:57 +02:00
|
|
|
debug!(
|
|
|
|
|
"Streaming stream request for {} from {}",
|
|
|
|
|
sanitize_sensitive_info(stream_url),
|
|
|
|
|
sanitize_sensitive_info(&session_url)
|
|
|
|
|
);
|
2025-06-04 21:40:14 +02:00
|
|
|
}
|
|
|
|
|
}
|
2025-07-24 14:27:57 +02:00
|
|
|
let (status_code, header_map) =
|
2025-11-10 01:04:13 +01:00
|
|
|
get_stream_response_with_headers(provider_response.map(|(h, s, _, _)| (h, s)));
|
2025-04-24 18:12:29 +02:00
|
|
|
let mut response = axum::response::Response::builder().status(status_code);
|
2025-03-19 09:54:59 +01:00
|
|
|
for (key, value) in &header_map {
|
|
|
|
|
response = response.header(key, value);
|
|
|
|
|
}
|
2025-03-26 15:06:05 +01:00
|
|
|
|
2025-04-22 17:08:31 +02:00
|
|
|
if let Some(provider) = provider_name {
|
2025-07-24 14:27:57 +02:00
|
|
|
if matches!(
|
|
|
|
|
item_type,
|
|
|
|
|
PlaylistItemType::LiveHls
|
|
|
|
|
| PlaylistItemType::LiveDash
|
|
|
|
|
| PlaylistItemType::Video
|
|
|
|
|
| PlaylistItemType::Series
|
|
|
|
|
| PlaylistItemType::Catchup
|
|
|
|
|
) {
|
2025-09-08 13:16:57 +02:00
|
|
|
let _ = app_state
|
|
|
|
|
.active_users
|
|
|
|
|
.create_user_session(
|
|
|
|
|
user,
|
|
|
|
|
session_token,
|
|
|
|
|
virtual_id,
|
|
|
|
|
&provider,
|
|
|
|
|
&session_url,
|
2025-10-30 13:00:25 +01:00
|
|
|
&fingerprint.addr,
|
2025-09-08 13:16:57 +02:00
|
|
|
connection_permission,
|
|
|
|
|
)
|
|
|
|
|
.await;
|
2025-04-22 17:08:31 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let body_stream = prepare_body_stream(app_state, item_type, stream);
|
2025-07-22 11:32:46 +02:00
|
|
|
try_unwrap_body!(response.body(body_stream))
|
2025-03-19 09:54:59 +01:00
|
|
|
};
|
2025-01-24 19:01:53 +01:00
|
|
|
|
2025-03-19 09:54:59 +01:00
|
|
|
return stream_resp.into_response();
|
2024-04-01 18:02:02 +02:00
|
|
|
}
|
2025-11-10 01:04:13 +01:00
|
|
|
app_state.connection_manager.release_provider_handle(stream_details.provider_handle).await;
|
2025-03-11 14:56:10 +01:00
|
|
|
axum::http::StatusCode::BAD_REQUEST.into_response()
|
2024-04-02 17:00:28 +02:00
|
|
|
}
|
2024-12-07 15:45:00 +01:00
|
|
|
|
2025-03-28 17:31:24 +01:00
|
|
|
fn get_stream_throttle(app_state: &AppState) -> u64 {
|
2025-07-24 14:27:57 +02:00
|
|
|
app_state
|
|
|
|
|
.app_config
|
|
|
|
|
.config
|
|
|
|
|
.load()
|
2025-03-26 15:06:05 +01:00
|
|
|
.reverse_proxy
|
|
|
|
|
.as_ref()
|
|
|
|
|
.and_then(|reverse_proxy| reverse_proxy.stream.as_ref())
|
2025-07-24 14:27:57 +02:00
|
|
|
.map(|stream| stream.throttle_kbps)
|
|
|
|
|
.unwrap_or_default()
|
2025-03-26 15:06:05 +01:00
|
|
|
}
|
|
|
|
|
|
2025-11-20 13:00:01 +01:00
|
|
|
#[allow(clippy::too_many_arguments)]
|
2025-11-04 21:46:44 +01:00
|
|
|
async fn try_shared_stream_response_if_any(
|
2025-12-12 20:58:55 +01:00
|
|
|
app_state: &Arc<AppState>,
|
2025-07-24 14:27:57 +02:00
|
|
|
stream_url: &str,
|
2025-10-30 13:00:25 +01:00
|
|
|
fingerprint: &Fingerprint,
|
2025-07-24 14:27:57 +02:00
|
|
|
user: &ProxyUserCredentials,
|
|
|
|
|
connect_permission: UserConnectionPermission,
|
2025-10-25 20:40:58 +02:00
|
|
|
mut stream_channel: StreamChannel,
|
2025-11-20 13:00:01 +01:00
|
|
|
session_token: &str,
|
2025-10-25 20:40:58 +02:00
|
|
|
req_headers: &HeaderMap,
|
2025-07-24 14:27:57 +02:00
|
|
|
) -> Option<impl IntoResponse> {
|
2025-10-27 08:27:35 +01:00
|
|
|
if let Some((stream, provider)) =
|
2025-11-01 00:26:30 +01:00
|
|
|
SharedStreamManager::subscribe_shared_stream(app_state, stream_url, &fingerprint.addr).await
|
2025-07-24 14:27:57 +02:00
|
|
|
{
|
2025-12-03 11:47:36 +01:00
|
|
|
debug_if_enabled!("Using shared stream {}", sanitize_sensitive_info(stream_url)
|
2025-07-24 14:27:57 +02:00
|
|
|
);
|
|
|
|
|
if let Some(headers) = app_state
|
|
|
|
|
.shared_stream_manager
|
2025-09-08 13:16:57 +02:00
|
|
|
.get_shared_state_headers(stream_url)
|
|
|
|
|
.await
|
2025-07-24 14:27:57 +02:00
|
|
|
{
|
|
|
|
|
let (status_code, header_map) = get_stream_response_with_headers(Some((
|
|
|
|
|
headers.clone(),
|
|
|
|
|
axum::http::StatusCode::OK,
|
|
|
|
|
)));
|
2025-10-27 08:27:35 +01:00
|
|
|
let mut stream_details = StreamDetails::from_stream(stream);
|
2025-12-03 11:47:36 +01:00
|
|
|
|
2025-10-27 08:27:35 +01:00
|
|
|
stream_details.provider_name = provider;
|
2025-10-24 19:47:39 +02:00
|
|
|
stream_channel.shared = true;
|
2025-07-24 14:27:57 +02:00
|
|
|
let stream =
|
2025-11-20 13:00:01 +01:00
|
|
|
ActiveClientStream::new(stream_details, app_state, user, connect_permission, fingerprint, stream_channel, Some(session_token), req_headers)
|
2025-09-08 13:16:57 +02:00
|
|
|
.await
|
|
|
|
|
.boxed();
|
2025-07-24 14:27:57 +02:00
|
|
|
let mut response = axum::response::Response::builder().status(status_code);
|
2025-03-11 14:56:10 +01:00
|
|
|
for (key, value) in &header_map {
|
|
|
|
|
response = response.header(key, value);
|
|
|
|
|
}
|
2025-07-22 11:32:46 +02:00
|
|
|
return response.body(axum::body::Body::from_stream(stream)).ok();
|
2024-12-07 15:45:00 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
None
|
|
|
|
|
}
|
2024-12-09 16:43:19 +01:00
|
|
|
|
|
|
|
|
pub fn is_stream_share_enabled(item_type: PlaylistItemType, target: &ConfigTarget) -> bool {
|
2025-07-24 14:27:57 +02:00
|
|
|
(item_type == PlaylistItemType::Live/* || item_type == PlaylistItemType::LiveHls */)
|
|
|
|
|
&& target
|
2025-11-11 10:00:35 +01:00
|
|
|
.options
|
|
|
|
|
.as_ref()
|
|
|
|
|
.is_some_and(|opt| opt.share_live_streams)
|
2025-01-03 13:08:46 +01:00
|
|
|
}
|
|
|
|
|
|
2025-03-11 14:56:10 +01:00
|
|
|
pub type HeaderFilter = Option<Box<dyn Fn(&str) -> bool + Send>>;
|
2025-07-24 14:27:57 +02:00
|
|
|
pub fn get_headers_from_request(
|
|
|
|
|
req_headers: &HeaderMap,
|
|
|
|
|
filter: &HeaderFilter,
|
|
|
|
|
) -> HashMap<String, Vec<u8>> {
|
2025-03-11 14:56:10 +01:00
|
|
|
req_headers
|
2025-01-08 21:17:57 +01:00
|
|
|
.iter()
|
2025-01-14 09:12:53 +01:00
|
|
|
.filter(|(k, _)| match &filter {
|
|
|
|
|
None => true,
|
2025-07-24 14:27:57 +02:00
|
|
|
Some(predicate) => predicate(k.as_str()),
|
2025-01-14 09:12:53 +01:00
|
|
|
})
|
2025-01-08 21:17:57 +01:00
|
|
|
.map(|(k, v)| (k.as_str().to_string(), v.as_bytes().to_vec()))
|
|
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-24 14:27:57 +02:00
|
|
|
fn get_add_cache_content(
|
|
|
|
|
res_url: &str,
|
|
|
|
|
cache: &Arc<ArcSwapOption<Mutex<LRUResourceCache>>>,
|
|
|
|
|
) -> Arc<dyn Fn(usize) + Send + Sync> {
|
2025-01-14 09:12:53 +01:00
|
|
|
let resource_url = String::from(res_url);
|
|
|
|
|
let cache = Arc::clone(cache);
|
2025-03-20 11:57:32 +01:00
|
|
|
let add_cache_content: Arc<dyn Fn(usize) + Send + Sync> = Arc::new(move |size| {
|
2025-01-14 09:12:53 +01:00
|
|
|
let res_url = resource_url.clone();
|
2025-10-27 13:58:55 +01:00
|
|
|
|
|
|
|
|
// todo spawn, replace with unboundchannel
|
2025-01-14 09:12:53 +01:00
|
|
|
let cache = Arc::clone(&cache);
|
2025-03-11 14:56:10 +01:00
|
|
|
tokio::spawn(async move {
|
2025-06-25 14:55:44 +02:00
|
|
|
if let Some(cache) = cache.load().as_ref() {
|
2025-03-11 14:56:10 +01:00
|
|
|
let _ = cache.lock().await.add_content(&res_url, size);
|
2025-01-14 09:12:53 +01:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
add_cache_content
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-08 13:16:57 +02:00
|
|
|
async fn build_stream_response(
|
|
|
|
|
app_state: &AppState,
|
|
|
|
|
resource_url: &str,
|
|
|
|
|
response: reqwest::Response,
|
|
|
|
|
) -> axum::response::Response {
|
2025-11-17 08:06:28 -06:00
|
|
|
let sanitized_resource_url = sanitize_sensitive_info(resource_url);
|
2025-09-19 13:51:39 +02:00
|
|
|
let status = response.status();
|
2025-09-08 13:16:57 +02:00
|
|
|
let mut response_builder =
|
2025-09-19 13:51:39 +02:00
|
|
|
axum::response::Response::builder().status(status);
|
2025-10-16 12:20:58 +02:00
|
|
|
let has_content_range = response.headers().contains_key(axum::http::header::CONTENT_RANGE);
|
2025-09-08 13:16:57 +02:00
|
|
|
for (key, value) in response.headers() {
|
2025-09-19 13:51:39 +02:00
|
|
|
let name = key.as_str();
|
|
|
|
|
let is_hop_by_hop = matches!(
|
|
|
|
|
name.to_ascii_lowercase().as_str(),
|
|
|
|
|
"connection"
|
|
|
|
|
| "keep-alive"
|
|
|
|
|
| "proxy-authenticate"
|
|
|
|
|
| "proxy-authorization"
|
|
|
|
|
| "te"
|
|
|
|
|
| "trailer"
|
|
|
|
|
| "transfer-encoding"
|
|
|
|
|
| "upgrade"
|
|
|
|
|
);
|
|
|
|
|
if !is_hop_by_hop {
|
|
|
|
|
response_builder = response_builder.header(key, value);
|
|
|
|
|
}
|
2025-09-08 13:16:57 +02:00
|
|
|
}
|
|
|
|
|
let byte_stream = response
|
|
|
|
|
.bytes_stream()
|
|
|
|
|
.map_err(|err| StreamError::reqwest(&err));
|
2025-09-19 13:51:39 +02:00
|
|
|
// Cache only complete responses (200 OK without Content-Range)
|
|
|
|
|
let can_cache = status == axum::http::StatusCode::OK && !has_content_range;
|
|
|
|
|
if can_cache {
|
2025-11-18 21:01:42 +01:00
|
|
|
debug!( "Caching eligible resource stream {sanitized_resource_url}");
|
2025-09-19 13:51:39 +02:00
|
|
|
let cache_resource_path = if let Some(cache) = app_state.cache.load().as_ref() {
|
|
|
|
|
Some(cache.lock().await.store_path(resource_url))
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
};
|
|
|
|
|
if let Some(resource_path) = cache_resource_path {
|
2025-11-17 08:06:28 -06:00
|
|
|
match create_new_file_for_write(&resource_path).await {
|
|
|
|
|
Ok(file) => {
|
2025-11-18 21:01:42 +01:00
|
|
|
debug!("Persisting resource stream {sanitized_resource_url} to {}", resource_path.display());
|
2025-12-11 15:27:48 +01:00
|
|
|
let writer = async_file_writer(file);
|
2025-11-17 08:06:28 -06:00
|
|
|
let add_cache_content = get_add_cache_content(resource_url, &app_state.cache);
|
2025-12-10 17:19:57 +01:00
|
|
|
let tee = tee_stream(byte_stream, writer, &resource_path, add_cache_content);
|
|
|
|
|
return try_unwrap_body!(response_builder.body(axum::body::Body::from_stream(tee)));
|
2025-11-17 08:06:28 -06:00
|
|
|
}
|
|
|
|
|
Err(err) => {
|
2025-11-18 21:01:42 +01:00
|
|
|
warn!("Failed to create cache file {} for {sanitized_resource_url}: {err}", resource_path.display());
|
2025-11-17 08:06:28 -06:00
|
|
|
}
|
2025-09-19 13:51:39 +02:00
|
|
|
}
|
2025-11-17 08:06:28 -06:00
|
|
|
} else {
|
2025-11-18 21:01:42 +01:00
|
|
|
debug!("Resource cache unavailable; streaming response for {sanitized_resource_url} without persistence");
|
2025-09-08 13:16:57 +02:00
|
|
|
}
|
|
|
|
|
}
|
2025-09-19 13:51:39 +02:00
|
|
|
|
2025-09-08 13:16:57 +02:00
|
|
|
try_unwrap_body!(response_builder.body(axum::body::Body::from_stream(byte_stream)))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn fetch_resource_with_retry(
|
|
|
|
|
app_state: &AppState,
|
|
|
|
|
url: &Url,
|
|
|
|
|
resource_url: &str,
|
|
|
|
|
req_headers: &HashMap<String, Vec<u8>>,
|
|
|
|
|
input: Option<&ConfigInput>,
|
|
|
|
|
) -> Option<axum::response::Response> {
|
2025-10-31 17:04:35 +01:00
|
|
|
let config = app_state.app_config.config.load();
|
2025-11-10 20:06:52 -06:00
|
|
|
let (max_attempts, backoff_ms, backoff_multiplier) = config
|
|
|
|
|
.reverse_proxy
|
|
|
|
|
.as_ref()
|
2025-11-11 10:00:35 +01:00
|
|
|
.map_or_else(ResourceRetryConfig::get_default_retry_values, |rp| rp.resource_retry.get_retry_values());
|
2025-12-12 14:35:00 +01:00
|
|
|
let disabled_headers = app_state.get_disabled_headers();
|
2025-09-19 13:16:48 +02:00
|
|
|
for attempt in 0..max_attempts {
|
2025-09-08 13:16:57 +02:00
|
|
|
let client = request::get_client_request(
|
|
|
|
|
&app_state.http_client.load(),
|
|
|
|
|
input.map_or(InputFetchMethod::GET, |i| i.method),
|
|
|
|
|
input.map(|i| &i.headers),
|
|
|
|
|
url,
|
|
|
|
|
Some(req_headers),
|
2025-10-31 17:04:35 +01:00
|
|
|
disabled_headers.as_ref(),
|
2025-09-08 13:16:57 +02:00
|
|
|
);
|
|
|
|
|
match client.send().await {
|
|
|
|
|
Ok(response) => {
|
|
|
|
|
let status = response.status();
|
|
|
|
|
if status.is_success() {
|
|
|
|
|
return Some(
|
|
|
|
|
build_stream_response(app_state, resource_url, response).await,
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-10-28 19:52:46 +01:00
|
|
|
// Retry only for 408, 425, 429 and all 5xx statuses
|
2025-09-08 18:06:19 +02:00
|
|
|
let should_retry = status.is_server_error()
|
|
|
|
|
|| matches!(
|
|
|
|
|
status,
|
2025-10-24 20:03:47 +02:00
|
|
|
// reqwest::StatusCode::BAD_REQUEST // 400 is typically client error; retrying likely won't help and adds load.
|
|
|
|
|
reqwest::StatusCode::REQUEST_TIMEOUT
|
2025-09-08 18:06:19 +02:00
|
|
|
| reqwest::StatusCode::TOO_EARLY
|
|
|
|
|
| reqwest::StatusCode::TOO_MANY_REQUESTS
|
|
|
|
|
);
|
|
|
|
|
|
2025-11-11 10:00:35 +01:00
|
|
|
if attempt < max_attempts - 1 && should_retry {
|
2025-09-08 13:16:57 +02:00
|
|
|
let wait_dur = response
|
|
|
|
|
.headers()
|
|
|
|
|
.get(RETRY_AFTER)
|
|
|
|
|
.and_then(|h| h.to_str().ok())
|
|
|
|
|
.and_then(|s| s.parse::<u64>().ok())
|
2025-11-10 20:06:52 -06:00
|
|
|
.map_or_else(
|
|
|
|
|
|| {
|
|
|
|
|
let delay = calculate_retry_backoff(backoff_ms, backoff_multiplier, attempt);
|
|
|
|
|
Duration::from_millis(delay)
|
|
|
|
|
},
|
|
|
|
|
Duration::from_secs,
|
|
|
|
|
);
|
2025-09-08 13:16:57 +02:00
|
|
|
tokio::time::sleep(wait_dur).await;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2025-09-08 18:06:19 +02:00
|
|
|
|
|
|
|
|
// For non-retriable statuses or when attempts are exhausted, return upstream response including body
|
2025-09-08 13:16:57 +02:00
|
|
|
debug_if_enabled!(
|
2025-09-19 13:16:48 +02:00
|
|
|
"Failed to open resource got status {status} for {}",
|
2025-09-08 13:16:57 +02:00
|
|
|
sanitize_sensitive_info(resource_url)
|
|
|
|
|
);
|
2025-09-08 18:06:19 +02:00
|
|
|
let mut response_builder = axum::response::Response::builder().status(status);
|
|
|
|
|
for (key, value) in response.headers() {
|
|
|
|
|
response_builder = response_builder.header(key, value);
|
|
|
|
|
}
|
|
|
|
|
let stream = response
|
|
|
|
|
.bytes_stream()
|
|
|
|
|
.map_err(|err| StreamError::reqwest(&err));
|
|
|
|
|
return Some(try_unwrap_body!(
|
|
|
|
|
response_builder.body(axum::body::Body::from_stream(stream))
|
|
|
|
|
));
|
2025-09-08 13:16:57 +02:00
|
|
|
}
|
|
|
|
|
Err(err) => {
|
2025-09-19 13:16:48 +02:00
|
|
|
if attempt < max_attempts - 1 {
|
2025-11-11 10:00:35 +01:00
|
|
|
let delay = calculate_retry_backoff(backoff_ms, backoff_multiplier, attempt);
|
|
|
|
|
tokio::time::sleep(Duration::from_millis(delay)).await;
|
|
|
|
|
continue;
|
2025-09-08 13:16:57 +02:00
|
|
|
}
|
2025-09-19 13:16:48 +02:00
|
|
|
error!("Received failure from server {}: {err}", sanitize_sensitive_info(resource_url));
|
2025-09-08 13:16:57 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-11 10:00:35 +01:00
|
|
|
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::cast_precision_loss)]
|
2025-11-10 20:06:52 -06:00
|
|
|
fn calculate_retry_backoff(base_delay_ms: u64, multiplier: f64, attempt: u32) -> u64 {
|
|
|
|
|
let base = base_delay_ms.max(1);
|
|
|
|
|
if multiplier <= 1.0 {
|
|
|
|
|
return base;
|
|
|
|
|
}
|
|
|
|
|
let delay = (base as f64) * multiplier.powi(i32::try_from(attempt).unwrap_or(i32::MAX));
|
|
|
|
|
if !delay.is_finite() || delay < 1.0 {
|
|
|
|
|
base
|
|
|
|
|
} else if delay >= u64::MAX as f64 {
|
|
|
|
|
u64::MAX
|
|
|
|
|
} else {
|
|
|
|
|
delay as u64
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-10 10:43:14 +02:00
|
|
|
/// # Panics
|
2025-07-24 14:27:57 +02:00
|
|
|
pub async fn resource_response(
|
|
|
|
|
app_state: &AppState,
|
|
|
|
|
resource_url: &str,
|
|
|
|
|
req_headers: &HeaderMap,
|
|
|
|
|
input: Option<&ConfigInput>,
|
|
|
|
|
) -> impl IntoResponse + Send {
|
2025-01-03 16:49:24 +01:00
|
|
|
if resource_url.is_empty() {
|
2025-03-11 14:56:10 +01:00
|
|
|
return axum::http::StatusCode::NO_CONTENT.into_response();
|
2025-01-03 16:49:24 +01:00
|
|
|
}
|
2025-07-24 14:27:57 +02:00
|
|
|
let filter: HeaderFilter = Some(Box::new(|key| {
|
|
|
|
|
key != "if-none-match" && key != "if-modified-since"
|
|
|
|
|
}));
|
2025-03-11 14:56:10 +01:00
|
|
|
let req_headers = get_headers_from_request(req_headers, &filter);
|
2025-06-25 14:55:44 +02:00
|
|
|
if let Some(cache) = app_state.cache.load().as_ref() {
|
2025-03-11 14:56:10 +01:00
|
|
|
let mut guard = cache.lock().await;
|
2025-02-06 10:49:34 +01:00
|
|
|
if let Some(resource_path) = guard.get_content(resource_url) {
|
2025-12-10 17:19:57 +01:00
|
|
|
trace_if_enabled!("Responding resource from cache {}", sanitize_sensitive_info(resource_url));
|
2025-07-24 14:27:57 +02:00
|
|
|
return serve_file(&resource_path, mime::APPLICATION_OCTET_STREAM)
|
|
|
|
|
.await
|
|
|
|
|
.into_response();
|
2025-01-14 00:44:43 +01:00
|
|
|
}
|
|
|
|
|
}
|
2025-07-24 14:27:57 +02:00
|
|
|
trace_if_enabled!(
|
|
|
|
|
"Try to fetch resource {}",
|
|
|
|
|
sanitize_sensitive_info(resource_url)
|
|
|
|
|
);
|
2025-01-03 13:08:46 +01:00
|
|
|
if let Ok(url) = Url::parse(resource_url) {
|
2025-09-08 13:16:57 +02:00
|
|
|
if let Some(resp) =
|
2025-09-19 13:51:39 +02:00
|
|
|
fetch_resource_with_retry(app_state, &url, resource_url, &req_headers, input).await {
|
2025-09-08 13:16:57 +02:00
|
|
|
return resp;
|
2025-01-03 13:08:46 +01:00
|
|
|
}
|
2025-09-19 13:51:39 +02:00
|
|
|
// Upstream failure after retries
|
|
|
|
|
return axum::http::StatusCode::BAD_GATEWAY.into_response();
|
2025-01-03 13:08:46 +01:00
|
|
|
}
|
2025-09-19 13:51:39 +02:00
|
|
|
error!("Url is malformed {}", sanitize_sensitive_info(resource_url));
|
2025-03-11 14:56:10 +01:00
|
|
|
axum::http::StatusCode::BAD_REQUEST.into_response()
|
2025-01-03 13:08:46 +01:00
|
|
|
}
|
2025-01-31 23:08:23 +01:00
|
|
|
|
|
|
|
|
pub fn separate_number_and_remainder(input: &str) -> (String, Option<String>) {
|
2025-07-24 14:27:57 +02:00
|
|
|
input.rfind('.').map_or_else(
|
|
|
|
|
|| (input.to_string(), None),
|
|
|
|
|
|dot_index| {
|
|
|
|
|
let number_part = input[..dot_index].to_string();
|
|
|
|
|
let rest = input[dot_index..].to_string();
|
|
|
|
|
(number_part, if rest.len() < 2 { None } else { Some(rest) })
|
|
|
|
|
},
|
|
|
|
|
)
|
2025-01-31 23:08:23 +01:00
|
|
|
}
|
2025-02-07 13:15:39 +01:00
|
|
|
|
2025-04-10 10:43:14 +02:00
|
|
|
/// # Panics
|
2025-11-20 13:00:01 +01:00
|
|
|
pub fn empty_json_list_response() -> axum::response::Response {
|
2025-07-22 11:32:46 +02:00
|
|
|
try_unwrap_body!(axum::response::Response::builder()
|
2025-07-15 12:28:25 +02:00
|
|
|
.status(axum::http::StatusCode::OK)
|
2025-10-16 11:58:40 +02:00
|
|
|
.header(axum::http::header::CONTENT_TYPE, mime::APPLICATION_JSON.to_string())
|
2025-07-28 14:46:13 +02:00
|
|
|
.body("[]".to_owned()))
|
2025-02-20 23:04:06 +01:00
|
|
|
}
|
|
|
|
|
|
2025-07-24 14:27:57 +02:00
|
|
|
pub fn get_username_from_auth_header(token: &str, app_state: &Arc<AppState>) -> Option<String> {
|
|
|
|
|
if let Some(web_auth_config) = &app_state
|
|
|
|
|
.app_config
|
|
|
|
|
.config
|
|
|
|
|
.load()
|
|
|
|
|
.web_ui
|
|
|
|
|
.as_ref()
|
|
|
|
|
.and_then(|c| c.auth.as_ref())
|
|
|
|
|
{
|
2025-07-21 22:17:57 +02:00
|
|
|
let secret_key: &[u8] = web_auth_config.secret.as_ref();
|
2025-03-11 14:56:10 +01:00
|
|
|
if let Ok(token_data) = decode::<Claims>(
|
|
|
|
|
token,
|
2025-07-21 22:17:57 +02:00
|
|
|
&DecodingKey::from_secret(secret_key),
|
2025-03-11 14:56:10 +01:00
|
|
|
&Validation::new(Algorithm::HS256),
|
|
|
|
|
) {
|
|
|
|
|
return Some(token_data.claims.username);
|
2025-02-20 23:04:06 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
None
|
|
|
|
|
}
|
2025-03-11 14:56:10 +01:00
|
|
|
|
2025-04-10 10:43:14 +02:00
|
|
|
/// # Panics
|
2025-03-11 14:56:10 +01:00
|
|
|
pub fn redirect(url: &str) -> impl IntoResponse {
|
2025-07-22 11:32:46 +02:00
|
|
|
try_unwrap_body!(axum::response::Response::builder()
|
2025-07-15 12:28:25 +02:00
|
|
|
.status(axum::http::StatusCode::FOUND)
|
2025-10-16 11:58:40 +02:00
|
|
|
.header(axum::http::header::LOCATION, url)
|
2025-07-22 11:32:46 +02:00
|
|
|
.body(axum::body::Body::empty()))
|
2025-03-16 17:08:00 +01:00
|
|
|
}
|
2025-04-22 17:08:31 +02:00
|
|
|
|
2025-07-24 14:27:57 +02:00
|
|
|
pub async fn is_seek_request(cluster: XtreamCluster, req_headers: &HeaderMap) -> bool {
|
2025-04-24 18:12:29 +02:00
|
|
|
// seek only for non-live streams
|
|
|
|
|
if cluster == XtreamCluster::Live {
|
2025-04-30 09:55:08 +02:00
|
|
|
return false;
|
2025-04-24 18:57:18 +02:00
|
|
|
}
|
|
|
|
|
|
2025-04-30 09:55:08 +02:00
|
|
|
// seek requests contains range header
|
|
|
|
|
let range = req_headers
|
|
|
|
|
.get("range")
|
|
|
|
|
.and_then(|h| h.to_str().ok())
|
|
|
|
|
.map(ToString::to_string);
|
2025-04-24 18:12:29 +02:00
|
|
|
|
2025-04-30 09:55:08 +02:00
|
|
|
if let Some(range) = range {
|
2025-11-06 12:29:21 +01:00
|
|
|
// if range.starts_with("bytes=0-") {
|
|
|
|
|
// return false;
|
|
|
|
|
// }
|
2025-04-30 09:55:08 +02:00
|
|
|
if range.starts_with("bytes=") {
|
|
|
|
|
return true;
|
2025-04-28 19:52:59 +02:00
|
|
|
}
|
2025-04-30 09:55:08 +02:00
|
|
|
}
|
2025-06-04 21:40:14 +02:00
|
|
|
false
|
2025-09-08 13:16:57 +02:00
|
|
|
}
|
2025-09-29 15:39:27 +02:00
|
|
|
|
2025-09-30 10:54:30 +02:00
|
|
|
pub fn bin_response<T: Serialize>(data: &T) -> impl IntoResponse + Send {
|
|
|
|
|
match bin_serialize(data) {
|
2025-09-29 15:39:27 +02:00
|
|
|
Ok(body) => (
|
2025-09-30 10:54:30 +02:00
|
|
|
[(axum::http::header::CONTENT_TYPE, CONTENT_TYPE_BIN)],
|
2025-09-29 15:39:27 +02:00
|
|
|
body,
|
2025-09-30 10:54:30 +02:00
|
|
|
).into_response(),
|
2025-09-29 15:39:27 +02:00
|
|
|
Err(_) => axum::http::StatusCode::INTERNAL_SERVER_ERROR.into_response(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn json_response<T: Serialize>(data: &T) -> impl IntoResponse + Send {
|
|
|
|
|
(axum::http::StatusCode::OK, axum::Json(data)).into_response()
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-30 10:54:30 +02:00
|
|
|
pub fn json_or_bin_response<T: Serialize>(accept: Option<&String>, data: &T) -> impl IntoResponse + Send {
|
|
|
|
|
if accept.is_some_and(|a| a.contains(CONTENT_TYPE_BIN)) {
|
|
|
|
|
return bin_response(data).into_response();
|
2025-09-29 15:39:27 +02:00
|
|
|
}
|
|
|
|
|
json_response(data).into_response()
|
2025-10-27 08:27:35 +01:00
|
|
|
}
|
|
|
|
|
|
2025-10-30 13:00:25 +01:00
|
|
|
pub fn create_session_fingerprint(fingerprint: &str, username: &str, virtual_id: u32) -> String {
|
2025-10-29 18:06:01 +01:00
|
|
|
format!("{fingerprint}|{username}|{virtual_id}")
|
2025-11-10 20:06:52 -06:00
|
|
|
}
|