Files
tuliprox/backend/app/src/api/api_utils/mod.rs
T
euzuandGitHub 69040f022b Feature/maintenance (#916)
* Maintenance mode
* fix(hls): release origin slot before next origin I/O
2026-10-13 22:30:21 +02:00

345 lines
12 KiB
Rust

pub use crate::repository::{
evaluate_network_access, log_network_access_allowed_geoip_unavailable, log_network_access_denied,
NetworkAccessDecision, NetworkAccessDenyReason,
};
use crate::{
api::{
endpoints::xtream_api::ApiStreamContext,
model::{AppState, PlaybackLeaseRef},
},
auth::Fingerprint,
model::{ConfigInput, ConfigTarget, ProxyUserCredentials},
};
use axum::http::HeaderMap;
use shared::model::{PlaylistEntry, StreamChannel, TargetType, XtreamCluster};
use std::{
net::SocketAddr,
sync::{Arc, LazyLock},
time::Duration,
};
pub(crate) struct ConnectFailedAttempt<'a> {
pub app_state: &'a Arc<AppState>,
pub fingerprint: &'a Fingerprint,
pub user: &'a ProxyUserCredentials,
pub stream_channel: StreamChannel,
pub provider_name: Arc<str>,
pub req_headers: &'a HeaderMap,
pub reason: ConnectFailureReason,
pub failure_stage: FailureStage,
}
#[macro_export]
macro_rules! try_option_bad_request {
($option:expr, $msg_is_error:expr, $msg:expr) => {
match $option {
Some(value) => value,
None => {
if $msg_is_error {
error!("{}", $msg);
} else {
debug!("{}", $msg);
}
return axum::http::StatusCode::BAD_REQUEST.into_response();
}
}
};
($option:expr) => {
match $option {
Some(value) => value,
None => return axum::http::StatusCode::BAD_REQUEST.into_response(),
}
};
}
#[macro_export]
macro_rules! try_option_forbidden {
($option:expr, $status:expr, $msg_is_error:expr, $msg:expr) => {
match $option {
Some(value) => value,
None => {
if $msg_is_error {
error!("{}", $msg);
} else {
debug!("{}", $msg);
}
return $status.into_response();
}
}
};
($option:expr, $msg_is_error:expr, $msg:expr) => {
match $option {
Some(value) => value,
None => {
if $msg_is_error {
error!("{}", $msg);
} else {
debug!("{}", $msg);
}
return axum::http::StatusCode::FORBIDDEN.into_response();
}
}
};
($option:expr) => {
match $option {
Some(value) => value,
None => return axum::http::StatusCode::FORBIDDEN.into_response(),
}
};
}
#[macro_export]
macro_rules! internal_server_error {
() => {
axum::http::StatusCode::INTERNAL_SERVER_ERROR.into_response()
};
}
#[macro_export]
macro_rules! try_result_or_status {
($option:expr, $status:expr, $msg_is_error:expr, $msg:expr) => {
match $option {
Ok(value) => value,
Err(_) => {
if $msg_is_error {
error!("{}", $msg);
} else {
debug!("{}", $msg);
}
return $status.into_response();
}
}
};
($option:expr, $status:expr) => {
match $option {
Ok(value) => value,
Err(_) => return $status.into_response(),
}
};
}
#[macro_export]
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)
};
}
pub use internal_server_error;
use shared::model::{ConnectFailureReason, FailureStage};
pub use try_option_bad_request;
pub use try_option_forbidden;
pub use try_result_bad_request;
pub use try_result_not_found;
pub use try_result_or_status;
// Moved to `tuliprox-core` so crates outside `api` can build responses too.
pub use tuliprox_core::try_unwrap_body;
// Admission moved to `tuliprox-session`, where the types it decides over
// already live. Re-exported so api call sites keep their names.
pub(crate) use tuliprox_core::utils::request_headers::{get_headers_from_request, HeaderFilter};
pub(crate) use tuliprox_session::{
admission::{
classify_playback_request, connection_priority_for_kind, resolve_admission_with_strategies,
resolve_playback_request_admission, AdmissionRequest, EvictionReentryGuard, PlaybackRequestClass,
PlaybackRequestFacts,
},
stream_options::{get_stream_options, StreamOptions, StreamResponseMode},
};
static PROCESS_START: LazyLock<std::time::Instant> = LazyLock::new(std::time::Instant::now);
// Response-compression opt-out moved to `tuliprox_core::utils`; re-exported so
// api call sites keep their names.
pub(crate) use tuliprox_core::utils::response_compression::{
mark_response_as_uncompressed, should_compress_response_extensions,
};
pub struct ForceStreamRequestContext<'a> {
pub req_headers: &'a HeaderMap,
pub input: &'a Arc<ConfigInput>,
pub user: &'a ProxyUserCredentials,
pub session_reservation_ttl_secs: u64,
pub(crate) content_representation: crate::api::model::ProviderContentRepresentationMode,
}
/// Upper bound a manifest refresh waits for its pinned account to free a slot. Parallel
/// playbacks on one account free their slots within seconds; a 503 would stall the player.
pub(crate) const HLS_MANIFEST_CAPACITY_WAIT: Duration = Duration::from_secs(5);
/// Upper bound a media object (segment, init, key) waits. It stays well below a typical
/// segment duration, so the player keeps buffer to retry after a 503.
const HLS_MEDIA_CAPACITY_WAIT: Duration = Duration::from_secs(2);
/// Lower bound between two acquisition attempts that only wait for a lease to expire.
const LEASE_EXPIRY_RECHECK_FLOOR: Duration = Duration::from_millis(10);
/// Acquisition of a slot on exactly one provider account for a playback lease.
pub(crate) struct ExactProviderAcquire<'a> {
pub provider: &'a Arc<str>,
pub addr: &'a SocketAddr,
pub allow_grace: bool,
pub priority: i8,
pub kind: crate::api::model::ConnectionKind,
pub lease: Option<PlaybackLeaseRef<'a>>,
}
pub struct RedirectParams<'a, P>
where
P: PlaylistEntry,
{
pub item: &'a P,
pub provider_id: Option<u32>,
pub cluster: XtreamCluster,
pub target_type: TargetType,
pub target: &'a ConfigTarget,
pub input: &'a ConfigInput,
pub user: &'a ProxyUserCredentials,
pub stream_ext: Option<&'a str>,
pub req_context: ApiStreamContext,
pub action_path: &'a str,
}
/// Upper bound of redirect hops a resource route follows. Every hop is classified, so the number of
/// requests a provider or EPG entry can trigger through one resource link stays bounded.
const RESOURCE_REDIRECT_LIMIT: u8 = 5;
/// How the client-visible answer of a resource route is shaped.
///
/// The variant mirrors the classification of the destination the route serves: a [`Public`] destination
/// may be handed to the client as it was fetched, a [`NonPublic`] one is relayed with a sanitized
/// answer. Which client performs a hop is decided per hop, not by this policy.
///
/// [`Public`]: ResourceFetchPolicy::Public
/// [`NonPublic`]: ResourceFetchPolicy::NonPublic
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ResourceFetchPolicy {
Public,
NonPublic,
}
const API_STREAM_CHUNK_SIZE: usize = 64 * 1024;
#[cfg(test)]
mod tests;
mod activation;
mod auth;
mod catchup;
mod clock;
mod local_files;
mod maintenance;
mod media_server;
mod metering;
mod provider_acquire;
mod provider_urls;
mod resources;
mod response;
mod serialization;
mod session_identity;
mod stalker;
mod strategy;
mod streaming;
#[cfg(test)]
use self::metering::resolve_stream_config_u64;
#[cfg(test)]
use self::provider_urls::find_input_account_by_signature;
#[cfg(test)]
use self::provider_urls::stream_url_matches_provider;
#[cfg(test)]
use self::response::admission_failure_video_type;
#[cfg(test)]
use self::stalker::needs_initial_stalker_resolution;
#[cfg(test)]
use self::strategy::resolve_streaming_strategy;
use self::{
activation::{
activate_session_before_stream_open, cleanup_forced_reopen_addrs, CurrentSessionGuard, SessionActivationRequest,
},
catchup::{
cleanup_failed_detected_catchup_hls, detected_catchup_hls_response, probe_catchup_payload, CatchupPayload,
DetectedCatchupHlsResponseParams,
},
media_server::{
is_media_server_playback_url, is_media_server_stream_ref_url, media_server_image_error_status,
open_media_server_image_resource, open_media_server_stream_for_input,
},
metering::{
get_stream_config_u64, get_stream_throttle, is_stream_metrics_enabled, is_throttled_stream,
prepare_stream_metering, StreamMeteringConfig,
},
provider_acquire::{
allows_provider_pool_failover, resolve_streaming_strategy_with_provider_handle, split_provider_stream_open,
},
provider_urls::{get_redirect_alternative_url, resolve_xtream_vod_provider_url},
resources::hls_resource_failure_status,
response::stream_admission_rejected_response,
session_identity::session_reacquire_cleanup_addrs,
stalker::{re_resolve_stalker_url_singleflight, should_refresh_stalker_playback, stalker_stream_kind},
strategy::{get_grace_period_millis, should_defer_provider_open_for_grace_hold},
streaming::{
create_stream_response_details, force_stream_response, is_hop_by_hop_response_header,
no_custom_video_fallback_status, prepare_body_stream, try_shared_stream_response_if_any,
StreamingAcquireOptions,
},
};
pub use self::{
auth::{
create_api_proxy_user, create_recording_proxy_user, get_user_target, get_user_target_by_credentials,
get_user_target_by_username, get_username_from_auth_header,
},
catchup::{create_catchup_session_key, create_m3u_catchup_session_key},
clock::{get_build_time, get_server_time, get_uptime_secs, init_uptime_clock},
local_files::serve_file,
provider_urls::get_stream_alternative_url,
resources::{resource_input_for_url, resource_proxy_response, resource_redirect_or_proxy, resource_response},
response::{
bin_response, empty_json_list_response, empty_json_response_as_array, empty_json_response_as_object,
json_or_bin_response, json_response, redirect, reentry_suppressed_response, separate_number_and_remainder,
},
serialization::{
stream_bin_array, stream_bin_array_stream, stream_json_array, stream_json_array_stream,
stream_json_or_bin_response, stream_json_or_bin_response_stream, stream_json_or_bin_response_try_stream,
},
session_identity::create_session_fingerprint,
strategy::redirect_response,
streaming::{
force_provider_stream_response, is_hls_stream_share_enabled, is_seek_request, is_seekable_media_request,
is_stream_share_enabled,
},
};
pub(crate) use self::{
auth::{input_for_user, with_recording_headers},
catchup::get_catchup_session_ttl_secs,
local_files::local_stream_response,
maintenance::{blocks_new_playback, is_running_playback, maintenance_rejection_response},
provider_acquire::{acquire_exact_provider_handle, stream_response_with_provider_handle},
provider_urls::select_provider_stream_url,
resources::force_hls_resource_response,
response::admission_failure_response,
serialization::coalesce_byte_stream,
session_identity::{
create_playback_session_fingerprint, get_hls_session_ttl_secs, get_session_reservation_ttl_secs,
is_session_based_playback, is_socket_bound_playback_session, should_pin_provider_for_session,
},
stalker::resolve_initial_stalker_playback_url,
strategy::resolve_redirect_location,
streaming::{
get_hls_playback_ttl_secs, record_connect_failed_attempt, resolve_request_url_for_logging,
resolve_stream_user_agent_index, should_allow_exhausted_shared_reconnect, stream_response,
},
};