mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-16 21:02:28 +02:00
Network access restriction for api user (#728)
* Network access restriction for api user * Fixed strm provider:// url bug
This commit is contained in:
@@ -122,7 +122,9 @@ impl ApiProxyConfigDto {
|
||||
let mut tokens = HashSet::new();
|
||||
for target_user in &mut self.user {
|
||||
for user in &mut target_user.credentials {
|
||||
user.prepare();
|
||||
if let Err(err) = user.prepare() {
|
||||
errors.push(err.to_string());
|
||||
}
|
||||
if usernames.contains(&user.username) {
|
||||
errors.push(format!("Non unique username found {}", &user.username));
|
||||
} else {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
error::TuliproxError,
|
||||
model::{ClusterFlags, ProxyType, ProxyUserStatus, XtreamCluster},
|
||||
model::{ClusterFlags, NetworkAccessDto, ProxyType, ProxyUserStatus, XtreamCluster},
|
||||
utils::{
|
||||
default_as_true, default_user_priority, deserialize_timestamp, is_blank_optional_string, is_cluster_optional,
|
||||
is_default_user_priority, is_true,
|
||||
@@ -49,10 +49,21 @@ pub struct ProxyUserCredentialsDto {
|
||||
pub soft_connections: u16,
|
||||
#[serde(default = "default_user_priority", skip_serializing_if = "is_default_user_priority")]
|
||||
pub soft_priority: i8,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub network_access: Option<NetworkAccessDto>,
|
||||
}
|
||||
|
||||
impl ProxyUserCredentialsDto {
|
||||
pub fn prepare(&mut self) { self.trim(); }
|
||||
pub fn prepare(&mut self) -> Result<(), TuliproxError> {
|
||||
self.trim();
|
||||
if let Some(na) = &mut self.network_access {
|
||||
na.prepare()?;
|
||||
if na.is_empty() {
|
||||
self.network_access = None;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn trim(&mut self) {
|
||||
self.username = self.username.trim().to_string();
|
||||
@@ -125,6 +136,7 @@ impl Default for ProxyUserCredentialsDto {
|
||||
priority: default_user_priority(),
|
||||
soft_connections: 0,
|
||||
soft_priority: default_user_priority(),
|
||||
network_access: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -176,4 +188,73 @@ mod tests {
|
||||
|
||||
assert_eq!(deserialized.output_clusters, Some(ClusterFlags::empty()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepare_rejects_invalid_country_in_network_access() {
|
||||
let mut user = ProxyUserCredentialsDto {
|
||||
username: "alice".to_string(),
|
||||
password: "secret".to_string(),
|
||||
network_access: Some(NetworkAccessDto {
|
||||
allowed_countries: Some(vec!["INVALID".to_string()]),
|
||||
allowed_networks: None,
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(user.prepare().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepare_rejects_invalid_cidr_in_network_access() {
|
||||
let mut user = ProxyUserCredentialsDto {
|
||||
username: "alice".to_string(),
|
||||
password: "secret".to_string(),
|
||||
network_access: Some(NetworkAccessDto {
|
||||
allowed_countries: None,
|
||||
allowed_networks: Some(vec!["not-a-cidr".to_string()]),
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(user.prepare().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepare_normalizes_empty_network_access_to_none() {
|
||||
let mut user = ProxyUserCredentialsDto {
|
||||
username: "alice".to_string(),
|
||||
password: "secret".to_string(),
|
||||
network_access: Some(NetworkAccessDto { allowed_countries: Some(vec![]), allowed_networks: Some(vec![]) }),
|
||||
..Default::default()
|
||||
};
|
||||
user.prepare().unwrap();
|
||||
assert_eq!(user.network_access, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepare_normalizes_and_deduplicates_network_access() {
|
||||
let mut user = ProxyUserCredentialsDto {
|
||||
username: "alice".to_string(),
|
||||
password: "secret".to_string(),
|
||||
network_access: Some(NetworkAccessDto {
|
||||
allowed_countries: Some(vec!["de".to_string(), "DE".to_string(), "at".to_string()]),
|
||||
allowed_networks: Some(vec![
|
||||
"10.0.0.1/8".to_string(), // non-canonical, normalized to 10.0.0.0/8
|
||||
"10.0.0.0/8".to_string(), // duplicate after normalization
|
||||
"192.168.1.0/24".to_string(),
|
||||
]),
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
user.prepare().unwrap();
|
||||
let na = user.network_access.as_ref();
|
||||
assert!(na.is_some());
|
||||
if let Some(na) = na {
|
||||
assert_eq!(na.allowed_countries, Some(vec!["DE".to_string(), "AT".to_string()]));
|
||||
assert_eq!(na.allowed_networks.as_ref().map(|n| n.len()), Some(2));
|
||||
assert!(na.allowed_networks.as_ref().is_some_and(|networks| networks.iter().any(|n| n == "10.0.0.0/8")));
|
||||
assert!(na
|
||||
.allowed_networks
|
||||
.as_ref()
|
||||
.is_some_and(|networks| networks.iter().any(|n| n == "192.168.1.0/24")));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,45 @@
|
||||
use crate::error::TuliproxError;
|
||||
use enum_iterator::Sequence;
|
||||
use std::fmt;
|
||||
|
||||
pub fn default_geoip_url() -> String {
|
||||
String::from(
|
||||
"https://raw.githubusercontent.com/sapics/ip-location-db/refs/heads/main/asn-country/asn-country-ipv4.csv",
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq, Default, Sequence)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum GeoIpUnavailablePolicy {
|
||||
#[default]
|
||||
Deny,
|
||||
Allow,
|
||||
}
|
||||
|
||||
impl fmt::Display for GeoIpUnavailablePolicy {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
GeoIpUnavailablePolicy::Deny => write!(f, "deny"),
|
||||
GeoIpUnavailablePolicy::Allow => write!(f, "allow"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::str::FromStr for GeoIpUnavailablePolicy {
|
||||
type Err = TuliproxError;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"deny" => Ok(GeoIpUnavailablePolicy::Deny),
|
||||
"allow" => Ok(GeoIpUnavailablePolicy::Allow),
|
||||
_ => Err(TuliproxError::Config(format!("Unknown GeoIpUnavailablePolicy {s}"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn is_default_unavailable_policy(policy: &GeoIpUnavailablePolicy) -> bool {
|
||||
matches!(policy, GeoIpUnavailablePolicy::Deny)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default, PartialEq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct GeoIpConfigDto {
|
||||
@@ -11,8 +47,63 @@ pub struct GeoIpConfigDto {
|
||||
pub enabled: bool,
|
||||
#[serde(default = "default_geoip_url")]
|
||||
pub url: String,
|
||||
#[serde(default, skip_serializing_if = "is_default_unavailable_policy")]
|
||||
pub unavailable_policy: GeoIpUnavailablePolicy,
|
||||
}
|
||||
|
||||
impl GeoIpConfigDto {
|
||||
pub fn is_empty(&self) -> bool { !self.enabled && self.url.trim().is_empty() }
|
||||
pub fn is_empty(&self) -> bool {
|
||||
!self.enabled && self.url.trim().is_empty() && self.unavailable_policy == GeoIpUnavailablePolicy::Deny
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn geoip_unavailable_policy_defaults_to_deny() {
|
||||
let dto: GeoIpConfigDto = serde_json::from_str(r#"{"enabled": false}"#).unwrap();
|
||||
assert_eq!(dto.unavailable_policy, GeoIpUnavailablePolicy::Deny);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn geoip_unavailable_policy_accepts_allow() {
|
||||
let dto: GeoIpConfigDto = serde_json::from_str(r#"{"enabled": false, "unavailable_policy": "allow"}"#).unwrap();
|
||||
assert_eq!(dto.unavailable_policy, GeoIpUnavailablePolicy::Allow);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn geoip_unavailable_policy_accepts_deny() {
|
||||
let dto: GeoIpConfigDto = serde_json::from_str(r#"{"enabled": true, "unavailable_policy": "deny"}"#).unwrap();
|
||||
assert_eq!(dto.unavailable_policy, GeoIpUnavailablePolicy::Deny);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn geoip_unavailable_policy_unknown_fails() {
|
||||
let result: Result<GeoIpConfigDto, _> =
|
||||
serde_json::from_str(r#"{"enabled": false, "unavailable_policy": "invalid"}"#);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn geoip_config_default_has_unavailable_policy_deny() {
|
||||
let dto = GeoIpConfigDto::default();
|
||||
assert_eq!(dto.unavailable_policy, GeoIpUnavailablePolicy::Deny);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn geoip_config_with_allow_is_not_empty() {
|
||||
let dto: GeoIpConfigDto =
|
||||
serde_json::from_str(r#"{"enabled": false, "unavailable_policy": "allow", "url": ""}"#).unwrap();
|
||||
assert!(!dto.is_empty(), "is_empty() should return false for explicit allow policy");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn geoip_config_is_empty_respects_policy() {
|
||||
// URL default means we need an explicit empty string to test is_empty with deny policy
|
||||
let dto: GeoIpConfigDto =
|
||||
serde_json::from_str(r#"{"enabled": false, "url": "", "unavailable_policy": "deny"}"#).unwrap();
|
||||
assert!(dto.is_empty(), "disabled config with deny policy and empty url should be empty");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ mod cache;
|
||||
mod config_type;
|
||||
mod epg;
|
||||
mod epg_smart_match;
|
||||
mod geoip;
|
||||
mod hdhomerun;
|
||||
mod input;
|
||||
mod ipcheck;
|
||||
@@ -36,8 +37,8 @@ mod web_auth;
|
||||
mod web_ui;
|
||||
|
||||
mod favourites;
|
||||
mod geoip;
|
||||
mod library;
|
||||
mod network_access;
|
||||
mod proxy_user_status;
|
||||
mod qos_aggregation;
|
||||
|
||||
@@ -60,6 +61,7 @@ pub use library::*;
|
||||
pub use log::*;
|
||||
pub use messaging::*;
|
||||
pub use metadata_update::*;
|
||||
pub use network_access::*;
|
||||
pub use panel_api::*;
|
||||
pub use paths::*;
|
||||
pub use pattern_template::*;
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
use crate::error::TuliproxError;
|
||||
|
||||
fn is_empty_vec<T>(v: &Option<Vec<T>>) -> bool { v.as_ref().is_none_or(|v| v.is_empty()) }
|
||||
|
||||
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, PartialEq)]
|
||||
pub struct NetworkAccessDto {
|
||||
#[serde(default, skip_serializing_if = "is_empty_vec")]
|
||||
pub allowed_countries: Option<Vec<String>>,
|
||||
#[serde(default, skip_serializing_if = "is_empty_vec")]
|
||||
pub allowed_networks: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl NetworkAccessDto {
|
||||
pub fn is_empty(&self) -> bool { is_empty_vec(&self.allowed_countries) && is_empty_vec(&self.allowed_networks) }
|
||||
|
||||
/// Prepares the DTO for storage/comparison: trim whitespace, uppercase country codes,
|
||||
/// deduplicate country codes, validate CIDRs, normalize empty lists to None.
|
||||
pub fn prepare(&mut self) -> Result<(), TuliproxError> {
|
||||
if let Some(countries) = &mut self.allowed_countries {
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
let mut deduped = Vec::new();
|
||||
for country in countries.drain(..) {
|
||||
let upper = country.trim().to_uppercase();
|
||||
if upper.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if upper.len() != 2 || !upper.chars().all(|ch| ch.is_ascii_alphabetic()) {
|
||||
return Err(TuliproxError::ProxyUser(format!(
|
||||
"Invalid network_access.allowed_countries entry '{upper}', expected 2-letter ISO code"
|
||||
)));
|
||||
}
|
||||
if seen.insert(upper.clone()) {
|
||||
deduped.push(upper);
|
||||
}
|
||||
}
|
||||
*countries = deduped;
|
||||
}
|
||||
if let Some(networks) = &mut self.allowed_networks {
|
||||
let mut deduped = Vec::new();
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
for network in networks.drain(..) {
|
||||
let trimmed = network.trim();
|
||||
if trimmed.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let network_str = trimmed.to_string();
|
||||
let ip: std::net::IpAddr = match network_str.split('/').next() {
|
||||
Some(addr) => addr.trim().parse().map_err(|_| {
|
||||
TuliproxError::ProxyUser(format!(
|
||||
"Invalid network_access.allowed_networks entry '{network_str}', expected CIDR"
|
||||
))
|
||||
})?,
|
||||
None => {
|
||||
return Err(TuliproxError::ProxyUser(format!(
|
||||
"Invalid network_access.allowed_networks entry '{network_str}', expected CIDR"
|
||||
)));
|
||||
}
|
||||
};
|
||||
let prefix: u8 = match network_str.split('/').nth(1) {
|
||||
Some(p) => p.trim().parse().map_err(|_| {
|
||||
TuliproxError::ProxyUser(format!(
|
||||
"Invalid network_access.allowed_networks entry '{network_str}', expected CIDR"
|
||||
))
|
||||
})?,
|
||||
None => {
|
||||
return Err(TuliproxError::ProxyUser(format!(
|
||||
"Invalid network_access.allowed_networks entry '{network_str}', expected CIDR"
|
||||
)));
|
||||
}
|
||||
};
|
||||
let prefix_valid = match ip {
|
||||
std::net::IpAddr::V4(_) => prefix <= 32,
|
||||
std::net::IpAddr::V6(_) => prefix <= 128,
|
||||
};
|
||||
if !prefix_valid {
|
||||
return Err(TuliproxError::ProxyUser(format!(
|
||||
"Invalid network_access.allowed_networks entry '{network_str}', expected CIDR"
|
||||
)));
|
||||
}
|
||||
// Normalize to canonical network representation (network address / prefix)
|
||||
let canonical_str = canonicalize_cidr(ip, prefix);
|
||||
if seen.insert(canonical_str.clone()) {
|
||||
deduped.push(canonical_str);
|
||||
}
|
||||
}
|
||||
*networks = deduped;
|
||||
}
|
||||
if self.allowed_countries.as_ref().is_some_and(|c| c.is_empty()) {
|
||||
self.allowed_countries = None;
|
||||
}
|
||||
if self.allowed_networks.as_ref().is_some_and(|n| n.is_empty()) {
|
||||
self.allowed_networks = None;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn canonicalize_cidr(ip: std::net::IpAddr, prefix: u8) -> String {
|
||||
match ip {
|
||||
std::net::IpAddr::V4(ipv4) => {
|
||||
let mask = if prefix == 0 { 0 } else { u32::MAX << (32 - prefix) };
|
||||
let network_addr = u32::from(ipv4) & mask;
|
||||
let network_ip = std::net::Ipv4Addr::from(network_addr);
|
||||
format!("{network_ip}/{prefix}")
|
||||
}
|
||||
std::net::IpAddr::V6(ipv6) => {
|
||||
let mask = if prefix == 0 { 0 } else { u128::MAX << (128 - prefix) };
|
||||
let network_addr = u128::from(ipv6) & mask;
|
||||
let network_ip = std::net::Ipv6Addr::from(network_addr);
|
||||
format!("{network_ip}/{prefix}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn default_is_empty() {
|
||||
let dto = NetworkAccessDto::default();
|
||||
assert_eq!(dto.allowed_countries, None);
|
||||
assert_eq!(dto.allowed_networks, None);
|
||||
assert!(dto.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_vecs_is_empty() {
|
||||
let dto = NetworkAccessDto { allowed_countries: Some(vec![]), allowed_networks: Some(vec![]) };
|
||||
assert!(dto.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_empty_is_not_empty() {
|
||||
let dto = NetworkAccessDto { allowed_countries: Some(vec!["DE".to_string()]), allowed_networks: None };
|
||||
assert!(!dto.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepare_uppercases_and_deduplicates_countries() {
|
||||
let mut dto = NetworkAccessDto {
|
||||
allowed_countries: Some(vec!["de".to_string(), "DE".to_string(), "At".to_string()]),
|
||||
allowed_networks: None,
|
||||
};
|
||||
dto.prepare().unwrap();
|
||||
let countries = dto.allowed_countries.unwrap();
|
||||
assert_eq!(countries, vec!["DE", "AT"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepare_normalizes_empty_to_none() {
|
||||
let mut dto = NetworkAccessDto { allowed_countries: Some(vec![]), allowed_networks: Some(vec![]) };
|
||||
dto.prepare().unwrap();
|
||||
assert_eq!(dto.allowed_countries, None);
|
||||
assert_eq!(dto.allowed_networks, None);
|
||||
assert!(dto.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_from_yaml_fragment() {
|
||||
let value = serde_json::json!({
|
||||
"allowed_countries": ["DE", "AT", "CH"],
|
||||
"allowed_networks": ["192.168.1.0/24", "10.0.0.0/8"]
|
||||
});
|
||||
let dto: NetworkAccessDto = serde_json::from_value(value).unwrap();
|
||||
assert_eq!(dto.allowed_countries, Some(vec!["DE".to_string(), "AT".to_string(), "CH".to_string()]));
|
||||
assert_eq!(dto.allowed_networks, Some(vec!["192.168.1.0/24".to_string(), "10.0.0.0/8".to_string()]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_omits_none_fields() {
|
||||
let dto = NetworkAccessDto::default();
|
||||
let serialized = serde_json::to_value(&dto).unwrap();
|
||||
assert!(!serialized.as_object().unwrap().contains_key("allowed_countries"));
|
||||
assert!(!serialized.as_object().unwrap().contains_key("allowed_networks"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_omits_empty_vec_fields() {
|
||||
let dto = NetworkAccessDto { allowed_countries: Some(vec![]), allowed_networks: Some(vec![]) };
|
||||
let serialized = serde_json::to_value(&dto).unwrap();
|
||||
assert!(!serialized.as_object().unwrap().contains_key("allowed_countries"));
|
||||
assert!(!serialized.as_object().unwrap().contains_key("allowed_networks"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roundtrip_preserves_content() {
|
||||
let dto = NetworkAccessDto {
|
||||
allowed_countries: Some(vec!["DE".to_string()]),
|
||||
allowed_networks: Some(vec!["10.0.0.0/8".to_string()]),
|
||||
};
|
||||
let serialized = serde_json::to_value(&dto).unwrap();
|
||||
let deserialized: NetworkAccessDto = serde_json::from_value(serialized).unwrap();
|
||||
assert_eq!(dto, deserialized);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepare_trims_whitespace() {
|
||||
let mut dto = NetworkAccessDto {
|
||||
allowed_countries: Some(vec![" de ".to_string()]),
|
||||
allowed_networks: Some(vec![" 10.0.0.0/8 ".to_string()]),
|
||||
};
|
||||
dto.prepare().unwrap();
|
||||
assert_eq!(dto.allowed_countries, Some(vec!["DE".to_string()]));
|
||||
assert_eq!(dto.allowed_networks, Some(vec!["10.0.0.0/8".to_string()]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepare_rejects_invalid_country_code() {
|
||||
let mut dto = NetworkAccessDto { allowed_countries: Some(vec!["DEU".to_string()]), allowed_networks: None };
|
||||
assert!(dto.prepare().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepare_rejects_invalid_cidr() {
|
||||
let mut dto =
|
||||
NetworkAccessDto { allowed_countries: None, allowed_networks: Some(vec!["not-a-cidr".to_string()]) };
|
||||
assert!(dto.prepare().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepare_normalizes_and_deduplicates_cidrs() {
|
||||
// "10.0.0.1/8" should be normalized to "10.0.0.0/8" (canonical network form)
|
||||
// and deduplicated against an explicit "10.0.0.0/8"
|
||||
let mut dto = NetworkAccessDto {
|
||||
allowed_networks: Some(vec![
|
||||
"10.0.0.1/8".to_string(), // non-canonical, equivalent to 10.0.0.0/8
|
||||
"10.0.0.0/8".to_string(), // canonical, duplicate after normalization
|
||||
"192.168.1.42/24".to_string(), // canonical
|
||||
" 192.168.1.0/24 ".to_string(), // needs trimming
|
||||
]),
|
||||
..Default::default()
|
||||
};
|
||||
dto.prepare().unwrap();
|
||||
let networks = dto.allowed_networks.unwrap();
|
||||
assert_eq!(networks.len(), 2, "expected 2 unique networks, got {networks:?}");
|
||||
assert!(networks.iter().any(|n| n == "10.0.0.0/8"), "missing 10.0.0.0/8 in {networks:?}");
|
||||
assert!(networks.iter().any(|n| n == "192.168.1.0/24"), "missing 192.168.1.0/24 in {networks:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepare_rejects_invalid_prefix() {
|
||||
let mut dto =
|
||||
NetworkAccessDto { allowed_networks: Some(vec!["192.168.1.0/33".to_string()]), ..Default::default() };
|
||||
assert!(dto.prepare().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepare_accepts_and_canonicalizes_zero_prefix_networks() {
|
||||
let mut dto = NetworkAccessDto {
|
||||
allowed_networks: Some(vec!["10.1.2.3/0".to_string(), "2001:db8::1/0".to_string()]),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(dto.prepare().is_ok());
|
||||
assert_eq!(dto.allowed_networks, Some(vec!["0.0.0.0/0".to_string(), "::/0".to_string()]));
|
||||
}
|
||||
}
|
||||
@@ -111,14 +111,14 @@ impl fmt::Display for PanelApiProvisioningMethod {
|
||||
}
|
||||
|
||||
impl FromStr for PanelApiProvisioningMethod {
|
||||
type Err = String;
|
||||
type Err = TuliproxError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.trim().to_ascii_uppercase().as_str() {
|
||||
"HEAD" => Ok(Self::Head),
|
||||
"GET" => Ok(Self::Get),
|
||||
"POST" => Ok(Self::Post),
|
||||
_ => Err("Unknown provisioning method: {s}".to_string()),
|
||||
_ => Err(TuliproxError::Config(format!("Unknown provisioning method: {s}"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,10 @@ use crate::{
|
||||
is_true, parse_to_kbps,
|
||||
},
|
||||
};
|
||||
use std::{
|
||||
fmt::{Display, Formatter},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
const STREAM_QUEUE_SIZE: usize = 1024; // mpsc channel holding messages. with 8192byte chunks and 2Mbit/s approx 8MB
|
||||
const MIN_SHARED_BURST_BUFFER_MB: u64 = 1;
|
||||
@@ -34,6 +38,39 @@ impl AdmissionStrategy {
|
||||
pub fn is_grace_hold(&self) -> bool { matches!(self, Self::GraceHoldStream) }
|
||||
}
|
||||
|
||||
impl Display for AdmissionStrategy {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
AdmissionStrategy::EvictUserSameIpOldest => "evict_user_same_ip_oldest",
|
||||
AdmissionStrategy::EvictUserSameIpLatest => "evict_user_same_ip_latest",
|
||||
AdmissionStrategy::EvictUserOldest => "evict_user_oldest",
|
||||
AdmissionStrategy::EvictUserLatest => "evict_user_latest",
|
||||
AdmissionStrategy::GraceInstantStream => "grace_instant_stream",
|
||||
AdmissionStrategy::GraceHoldStream => "grace_hold_stream",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for AdmissionStrategy {
|
||||
type Err = TuliproxError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.trim() {
|
||||
"evict_user_same_ip_oldest" => Ok(AdmissionStrategy::EvictUserSameIpOldest),
|
||||
"evict_user_same_ip_latest" => Ok(AdmissionStrategy::EvictUserSameIpLatest),
|
||||
"evict_user_oldest" => Ok(AdmissionStrategy::EvictUserOldest),
|
||||
"evict_user_latest" => Ok(AdmissionStrategy::EvictUserLatest),
|
||||
"grace_instant_stream" => Ok(AdmissionStrategy::GraceInstantStream),
|
||||
"grace_hold_stream" => Ok(AdmissionStrategy::GraceHoldStream),
|
||||
_ => Err(TuliproxError::Config(format!("Unknown admission strategy: {s}"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default, PartialEq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct StreamBufferConfigDto {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use crate::utils::{
|
||||
default_as_true, default_trakt_fuzzy_threshold, is_true, DEFAULT_USER_AGENT, TRAKT_API_KEY, TRAKT_API_URL,
|
||||
TRAKT_API_VERSION,
|
||||
use crate::{
|
||||
error::TuliproxError,
|
||||
utils::{
|
||||
default_as_true, default_trakt_fuzzy_threshold, is_true, DEFAULT_USER_AGENT, TRAKT_API_KEY, TRAKT_API_URL,
|
||||
TRAKT_API_VERSION,
|
||||
},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{fmt, str::FromStr};
|
||||
@@ -29,14 +32,14 @@ impl fmt::Display for TraktContentType {
|
||||
}
|
||||
|
||||
impl FromStr for TraktContentType {
|
||||
type Err = String;
|
||||
type Err = TuliproxError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"vod" => Ok(TraktContentType::Vod),
|
||||
"series" => Ok(TraktContentType::Series),
|
||||
"both" => Ok(TraktContentType::Both),
|
||||
_ => Err(format!("Invalid TraktContentType: {}", s)),
|
||||
_ => Err(TuliproxError::Config(format!("Invalid TraktContentType: {}", s))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::{
|
||||
create_bitset,
|
||||
error::TuliproxError,
|
||||
model::{
|
||||
xtream_const, ClusterFlags, CommonPlaylistItem, ConfigTargetOptions, EpisodeStreamProperties,
|
||||
SeriesStreamProperties, StreamProperties, UUIDType, VideoStreamProperties, XtreamInfoDocument,
|
||||
@@ -48,14 +49,14 @@ impl XtreamCluster {
|
||||
}
|
||||
|
||||
impl FromStr for XtreamCluster {
|
||||
type Err = String;
|
||||
type Err = TuliproxError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"live" => Ok(XtreamCluster::Live),
|
||||
"video" | "vod" | "movie" => Ok(XtreamCluster::Video),
|
||||
"series" => Ok(XtreamCluster::Series),
|
||||
_ => Err(format!("Invalid XtreamCluster: {s}")),
|
||||
_ => Err(TuliproxError::Config(format!("Invalid XtreamCluster: {s}"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -120,7 +121,7 @@ impl From<XtreamCluster> for PlaylistItemType {
|
||||
}
|
||||
|
||||
impl FromStr for PlaylistItemType {
|
||||
type Err = String;
|
||||
type Err = TuliproxError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
@@ -135,7 +136,7 @@ impl FromStr for PlaylistItemType {
|
||||
"LiveUnknown" => Ok(PlaylistItemType::LiveUnknown),
|
||||
"LiveHls" => Ok(PlaylistItemType::LiveHls),
|
||||
"LiveDash" => Ok(PlaylistItemType::LiveDash),
|
||||
_ => Err(format!("Invalid PlaylistItemType: {s}")),
|
||||
_ => Err(TuliproxError::Config(format!("Invalid PlaylistItemType: {s}"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::error::TuliproxError;
|
||||
use enum_iterator::Sequence;
|
||||
use std::{fmt::Display, str::FromStr};
|
||||
|
||||
@@ -45,7 +46,7 @@ impl Display for ProcessingOrder {
|
||||
}
|
||||
|
||||
impl FromStr for ProcessingOrder {
|
||||
type Err = &'static str;
|
||||
type Err = TuliproxError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let normalized = s.trim().to_lowercase();
|
||||
@@ -67,7 +68,7 @@ impl FromStr for ProcessingOrder {
|
||||
x if x == Self::MFR => Ok(Self::Mfr),
|
||||
x if x == Self::MRF => Ok(Self::Mrf),
|
||||
|
||||
_ => Err("invalid processing order"),
|
||||
_ => Err(TuliproxError::Config(format!("Invalid processing order {s}"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::error::TuliproxError;
|
||||
use enum_iterator::Sequence;
|
||||
use std::{fmt::Display, str::FromStr};
|
||||
|
||||
@@ -37,7 +38,7 @@ impl Display for StrmExportStyle {
|
||||
}
|
||||
|
||||
impl FromStr for StrmExportStyle {
|
||||
type Err = String;
|
||||
type Err = TuliproxError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
@@ -45,7 +46,7 @@ impl FromStr for StrmExportStyle {
|
||||
Self::PLEX => Ok(Self::Plex),
|
||||
Self::EMBY => Ok(Self::Emby),
|
||||
Self::JELLYFIN => Ok(Self::Jellyfin),
|
||||
_ => Err(format!("Unknown StrmExportStyle: {}", s)),
|
||||
_ => Err(TuliproxError::Config(format!("Unknown StrmExportStyle: {s}"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::error::TuliproxError;
|
||||
use enum_iterator::Sequence;
|
||||
use std::{fmt::Display, str::FromStr};
|
||||
|
||||
@@ -36,7 +37,7 @@ impl Display for TargetType {
|
||||
}
|
||||
|
||||
impl FromStr for TargetType {
|
||||
type Err = String;
|
||||
type Err = TuliproxError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
@@ -44,7 +45,7 @@ impl FromStr for TargetType {
|
||||
Self::XTREAM => Ok(Self::Xtream),
|
||||
Self::STRM => Ok(Self::Strm),
|
||||
Self::HDHOMERUN => Ok(Self::HdHomeRun),
|
||||
_ => Err(format!("Unknown TargetType: {}", s)),
|
||||
_ => Err(TuliproxError::Config(format!("Unknown TargetType: {s}"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user