mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-09 17:32:16 +02:00
some ui refactorings,
use_user_db option for storing api user inside db file
This commit is contained in:
@@ -50,6 +50,7 @@ async fn save_config_api_proxy_user(
|
||||
) -> HttpResponse {
|
||||
let mut users = req.0;
|
||||
let mut usernames = HashSet::new();
|
||||
let mut tokens = HashSet::new();
|
||||
for target_user in &mut users {
|
||||
for credential in &mut target_user.credentials {
|
||||
credential.trim();
|
||||
@@ -60,6 +61,12 @@ async fn save_config_api_proxy_user(
|
||||
return HttpResponse::BadRequest().json(json!({"error": format!("Duplicate username {}", &credential.username)}));
|
||||
}
|
||||
usernames.insert(&credential.username);
|
||||
if let Some(token) = &credential.token {
|
||||
if tokens.contains(token) {
|
||||
return HttpResponse::BadRequest().json(json!({"error": format!("Duplicate token {token}")}));
|
||||
}
|
||||
tokens.insert(token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -387,56 +387,56 @@ pub async fn create_provider_stream(cfg: &Config,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::model::streams::provider_stream_factory::PlaylistItemType;
|
||||
use crate::api::model::streams::provider_stream_factory::{create_provider_stream, BufferStreamOptions};
|
||||
use actix_web::test;
|
||||
use actix_web::test::TestRequest;
|
||||
use actix_web::web;
|
||||
use actix_web::App;
|
||||
use actix_web::{HttpRequest, HttpResponse};
|
||||
use futures::StreamExt;
|
||||
use std::sync::Arc;
|
||||
use crate::model::config::Config;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_stream() {
|
||||
let app = App::new().route("/test", web::get().to(test_stream_handler));
|
||||
let server = test::init_service(app).await;
|
||||
let req = TestRequest::get().uri("/test").to_request();
|
||||
let _response = test::call_service(&server, req).await;
|
||||
}
|
||||
async fn test_stream_handler(req: HttpRequest) -> HttpResponse {
|
||||
let cfg = Config::default();
|
||||
let mut counter = 5;
|
||||
let client = Arc::new(reqwest::Client::new());
|
||||
let url = url::Url::parse("https://info.cern.ch/hypertext/WWW/TheProject.html").unwrap();
|
||||
let input = None;
|
||||
|
||||
let options = BufferStreamOptions::new(PlaylistItemType::Live, true, true, 0, false);
|
||||
let value = create_provider_stream(&cfg, Arc::clone(&client), &url, &req, input, options);
|
||||
let mut values = value.await;
|
||||
'outer: while let Some((ref mut stream, info)) = values.as_mut() {
|
||||
if info.is_some() {
|
||||
println!("{:?}", info.as_ref().unwrap());
|
||||
}
|
||||
while let Some(result) = stream.next().await {
|
||||
match result {
|
||||
Ok(bytes) => {
|
||||
println!("Received {} bytes {bytes:?}", bytes.len());
|
||||
counter -= 1;
|
||||
if counter < 0 {
|
||||
break 'outer;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("Error occurred: {}", err);
|
||||
break 'outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
HttpResponse::Ok().finish()
|
||||
}
|
||||
}
|
||||
// #[cfg(test)]
|
||||
// mod tests {
|
||||
// use crate::api::model::streams::provider_stream_factory::PlaylistItemType;
|
||||
// use crate::api::model::streams::provider_stream_factory::{create_provider_stream, BufferStreamOptions};
|
||||
// use actix_web::test;
|
||||
// use actix_web::test::TestRequest;
|
||||
// use actix_web::web;
|
||||
// use actix_web::App;
|
||||
// use actix_web::{HttpRequest, HttpResponse};
|
||||
// use futures::StreamExt;
|
||||
// use std::sync::Arc;
|
||||
// use crate::model::config::Config;
|
||||
//
|
||||
// #[actix_rt::test]
|
||||
// async fn test_stream() {
|
||||
// let app = App::new().route("/test", web::get().to(test_stream_handler));
|
||||
// let server = test::init_service(app).await;
|
||||
// let req = TestRequest::get().uri("/test").to_request();
|
||||
// let _response = test::call_service(&server, req).await;
|
||||
// }
|
||||
// async fn test_stream_handler(req: HttpRequest) -> HttpResponse {
|
||||
// let cfg = Config::default();
|
||||
// let mut counter = 5;
|
||||
// let client = Arc::new(reqwest::Client::new());
|
||||
// let url = url::Url::parse("https://info.cern.ch/hypertext/WWW/TheProject.html").unwrap();
|
||||
// let input = None;
|
||||
//
|
||||
// let options = BufferStreamOptions::new(PlaylistItemType::Live, true, true, 0, false);
|
||||
// let value = create_provider_stream(&cfg, Arc::clone(&client), &url, &req, input, options);
|
||||
// let mut values = value.await;
|
||||
// 'outer: while let Some((ref mut stream, info)) = values.as_mut() {
|
||||
// if info.is_some() {
|
||||
// println!("{:?}", info.as_ref().unwrap());
|
||||
// }
|
||||
// while let Some(result) = stream.next().await {
|
||||
// match result {
|
||||
// Ok(bytes) => {
|
||||
// println!("Received {} bytes {bytes:?}", bytes.len());
|
||||
// counter -= 1;
|
||||
// if counter < 0 {
|
||||
// break 'outer;
|
||||
// }
|
||||
// }
|
||||
// Err(err) => {
|
||||
// eprintln!("Error occurred: {}", err);
|
||||
// break 'outer;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// HttpResponse::Ok().finish()
|
||||
// }
|
||||
// }
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
#![warn(clippy::pedantic)]
|
||||
#![warn(clippy::all,clippy::pedantic)]
|
||||
#![allow(clippy::module_name_repetitions)]
|
||||
#![allow(clippy::must_use_candidate)]
|
||||
#![allow(clippy::return_self_not_must_use)]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::api::model::app_state::AppState;
|
||||
use crate::m3u_filter_error::{create_m3u_filter_error_result, info_err, M3uFilterError, M3uFilterErrorKind};
|
||||
use crate::model::config::Config;
|
||||
use crate::repository::user_repository::{get_api_user_db_path, load_api_user, merge_api_user};
|
||||
use crate::repository::user_repository::{backup_api_user_db_file, get_api_user_db_path, load_api_user, merge_api_user};
|
||||
use crate::utils::file::config_reader;
|
||||
use chrono::Local;
|
||||
use enum_iterator::Sequence;
|
||||
@@ -304,7 +304,7 @@ impl ApiProxyConfig {
|
||||
let api_proxy_file = cfg.t_api_proxy_file_path.as_str();
|
||||
let backup_dir = cfg.backup_dir.as_ref().unwrap().as_str();
|
||||
self.user = vec![];
|
||||
if let Err(err) = config_reader::save_api_proxy(api_proxy_file, backup_dir, &self) {
|
||||
if let Err(err) = config_reader::save_api_proxy(api_proxy_file, backup_dir, self) {
|
||||
errors.push(format!("Error saving api proxy file: {err}"));
|
||||
}
|
||||
}
|
||||
@@ -315,7 +315,7 @@ impl ApiProxyConfig {
|
||||
}
|
||||
Err(err) => {
|
||||
println!("{err}");
|
||||
errors.push(err.to_string())
|
||||
errors.push(err.to_string());
|
||||
},
|
||||
};
|
||||
} else {
|
||||
@@ -326,8 +326,8 @@ impl ApiProxyConfig {
|
||||
if let Ok(stored_users) = load_api_user(cfg) {
|
||||
for stored_user in stored_users {
|
||||
if let Some(target_user) = self.user.iter_mut().find(|t| t.target == stored_user.target) {
|
||||
for stored_credential in stored_user.credentials.iter() {
|
||||
if target_user.credentials.iter().find(|&c| c.username == stored_credential.username).is_none() {
|
||||
for stored_credential in &stored_user.credentials {
|
||||
if !target_user.credentials.iter().any(|c| c.username == stored_credential.username) {
|
||||
target_user.credentials.push(stored_credential.clone());
|
||||
};
|
||||
}
|
||||
@@ -338,9 +338,10 @@ impl ApiProxyConfig {
|
||||
}
|
||||
let api_proxy_file = cfg.t_api_proxy_file_path.as_str();
|
||||
let backup_dir = cfg.backup_dir.as_ref().unwrap().as_str();
|
||||
if let Err(err) = config_reader::save_api_proxy(api_proxy_file, backup_dir, &self) {
|
||||
if let Err(err) = config_reader::save_api_proxy(api_proxy_file, backup_dir, self) {
|
||||
errors.push(format!("Error saving api proxy file: {err}"));
|
||||
} else {
|
||||
backup_api_user_db_file(cfg, &user_db_path);
|
||||
let _ = fs::remove_file(&user_db_path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,79 +1,137 @@
|
||||
use crate::model::api_proxy::{ProxyUserCredentials, TargetUser};
|
||||
use crate::model::api_proxy::{ProxyType, ProxyUserCredentials, ProxyUserStatus, TargetUser};
|
||||
use crate::model::config::Config;
|
||||
use crate::model::playlist::XtreamCluster;
|
||||
use crate::model::playlist_categories::{PlaylistCategoriesDto, PlaylistCategoryDto};
|
||||
use crate::repository::bplustree::BPlusTree;
|
||||
use crate::utils::file::file_utils;
|
||||
use crate::utils::json_utils::json_write_documents_to_file;
|
||||
use log::error;
|
||||
use log::{error};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::{Path, PathBuf};
|
||||
use chrono::Local;
|
||||
|
||||
const USER_LIVE_BOUQUET: &str = "live_bouquet.json";
|
||||
const USER_VOD_BOUQUET: &str = "vod_bouquet.json";
|
||||
const USER_SERIES_BOUQUET: &str = "series_bouquet.json";
|
||||
|
||||
const API_USER_DB_FILE: &str = "api_user.db";
|
||||
|
||||
// This is a Helper class to store all user into one Database file.
|
||||
// For the Config files we keep the old structure where a user is assigned to a target.
|
||||
// But for storing inside one db file it is easier to store the target next to the user.
|
||||
// due to known issue with bincode and skip_serialization_if we have to list all fields and cant use ProxyUserCredentials
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
struct StoredProxyUserCredentials {
|
||||
pub target: String,
|
||||
pub user: ProxyUserCredentials,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
pub token: Option<String>,
|
||||
pub proxy: ProxyType,
|
||||
pub server: Option<String>,
|
||||
pub epg_timeshift: Option<String>,
|
||||
pub created_at: Option<i64>,
|
||||
pub exp_date: Option<i64>,
|
||||
pub max_connections: Option<u32>,
|
||||
pub status: Option<ProxyUserStatus>,
|
||||
}
|
||||
|
||||
impl StoredProxyUserCredentials {
|
||||
fn from(proxy: &ProxyUserCredentials, target_name: &str) -> Self {
|
||||
Self {
|
||||
target: String::from(target_name),
|
||||
username: proxy.username.clone(),
|
||||
password: proxy.password.clone(),
|
||||
token: proxy.token.clone(),
|
||||
proxy: proxy.proxy.clone(),
|
||||
server: proxy.server.clone(),
|
||||
epg_timeshift: proxy.epg_timeshift.clone(),
|
||||
created_at: proxy.created_at,
|
||||
exp_date: proxy.exp_date,
|
||||
max_connections: proxy.max_connections,
|
||||
status: proxy.status.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn to(stored: &StoredProxyUserCredentials) -> ProxyUserCredentials {
|
||||
ProxyUserCredentials {
|
||||
username: stored.username.clone(),
|
||||
password: stored.password.clone(),
|
||||
token: stored.token.clone(),
|
||||
proxy: stored.proxy.clone(),
|
||||
server: stored.server.clone(),
|
||||
epg_timeshift: stored.epg_timeshift.clone(),
|
||||
created_at: stored.created_at,
|
||||
exp_date: stored.exp_date,
|
||||
max_connections: stored.max_connections,
|
||||
status: stored.status.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn get_api_user_db_path(cfg: &Config) -> PathBuf {
|
||||
PathBuf::from(&cfg.t_config_path).join("api_user.db")
|
||||
PathBuf::from(&cfg.t_config_path).join(API_USER_DB_FILE)
|
||||
}
|
||||
|
||||
|
||||
fn add_target_user_to_user_tree(target_users: &[TargetUser], user_tree: &mut BPlusTree<String, StoredProxyUserCredentials>) {
|
||||
for target_user in target_users {
|
||||
for user in &target_user.credentials {
|
||||
let store_user = StoredProxyUserCredentials {
|
||||
target: target_user.target.clone(),
|
||||
user: user.clone(),
|
||||
};
|
||||
let store_user: StoredProxyUserCredentials = StoredProxyUserCredentials::from(user, &target_user.target);
|
||||
user_tree.insert(user.username.clone(), store_user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn merge_api_user(cfg: &Config, target_users: &[TargetUser]) -> Result<u64, std::io::Error> {
|
||||
let path = get_api_user_db_path(&cfg);
|
||||
let _lock = cfg.file_locks.read_lock(&path);
|
||||
let path = get_api_user_db_path(cfg);
|
||||
let lock = cfg.file_locks.read_lock(&path);
|
||||
let mut user_tree: BPlusTree<String, StoredProxyUserCredentials> = BPlusTree::load(&path).unwrap_or_else(|_| BPlusTree::new());
|
||||
drop(_lock);
|
||||
drop(lock);
|
||||
add_target_user_to_user_tree(target_users, &mut user_tree);
|
||||
let _lock = cfg.file_locks.write_lock(&path);
|
||||
user_tree.store(&path)
|
||||
}
|
||||
|
||||
/// # Panics
|
||||
///
|
||||
/// Will panic if `backup_dir` is not given
|
||||
pub fn backup_api_user_db_file(cfg: &Config, path: &Path) {
|
||||
let backup_dir = cfg.backup_dir.as_ref().unwrap().as_str();
|
||||
let backup_path = PathBuf::from(backup_dir).join(format!("{API_USER_DB_FILE}_{}", Local::now().format("%Y%m%d_%H%M%S")));
|
||||
let _lock = cfg.file_locks.read_lock(path);
|
||||
match std::fs::copy(path, &backup_path) {
|
||||
Ok(_) => {}
|
||||
Err(err) => { error!("Could not backup file {}:{}", &backup_path.to_str().unwrap_or("?"), err) }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn store_api_user(cfg: &Config, target_users: &[TargetUser]) -> Result<u64, std::io::Error> {
|
||||
let mut user_tree = BPlusTree::<String, StoredProxyUserCredentials>::new();
|
||||
add_target_user_to_user_tree(target_users, &mut user_tree);
|
||||
let path = get_api_user_db_path(&cfg);
|
||||
let path = get_api_user_db_path(cfg);
|
||||
backup_api_user_db_file(cfg, &path);
|
||||
let _lock = cfg.file_locks.write_lock(&path);
|
||||
user_tree.store(&path)
|
||||
}
|
||||
|
||||
pub fn load_api_user(cfg: &Config) -> Result<Vec<TargetUser>, std::io::Error> {
|
||||
let path = get_api_user_db_path(&cfg);
|
||||
let _lock = cfg.file_locks.read_lock(&path);
|
||||
let path = get_api_user_db_path(cfg);
|
||||
let lock = cfg.file_locks.read_lock(&path);
|
||||
let user_tree = BPlusTree::<String, StoredProxyUserCredentials>::load(&path)?;
|
||||
drop(_lock);
|
||||
drop(lock);
|
||||
let mut target_users: HashMap<String, TargetUser> = HashMap::new();
|
||||
for (_uname, user_wrapper) in user_tree.iter() {
|
||||
match target_users.entry(user_wrapper.target.clone()) {
|
||||
for (_uname, stored_user) in &user_tree {
|
||||
let proxy_user: ProxyUserCredentials = StoredProxyUserCredentials::to(stored_user);
|
||||
let target_name = stored_user.target.clone();
|
||||
match target_users.entry(target_name) {
|
||||
std::collections::hash_map::Entry::Occupied(mut entry) => {
|
||||
let target = entry.get_mut();
|
||||
target.credentials.push(user_wrapper.user.clone());
|
||||
target.credentials.push(proxy_user);
|
||||
}
|
||||
std::collections::hash_map::Entry::Vacant(entry) => {
|
||||
entry.insert(TargetUser {
|
||||
target: user_wrapper.target.clone(),
|
||||
credentials: vec![user_wrapper.user.clone()],
|
||||
target: stored_user.target.clone(),
|
||||
credentials: vec![proxy_user],
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -189,4 +247,81 @@ pub async fn user_get_bouquet_filter(config: &Config, username: &str, category_i
|
||||
} else {
|
||||
Some(filter)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::env::temp_dir;
|
||||
use crate::model::api_proxy::{ProxyType, ProxyUserStatus};
|
||||
use super::*;
|
||||
|
||||
|
||||
#[test]
|
||||
pub fn save_target_user() {
|
||||
let user =
|
||||
TargetUser {
|
||||
target: "test".to_string(),
|
||||
credentials: vec![
|
||||
ProxyUserCredentials {
|
||||
username: "Test".to_string(),
|
||||
password: "Test".to_string(),
|
||||
token: Some("Test".to_string()),
|
||||
proxy: ProxyType::Reverse,
|
||||
server: Some("default".to_string()),
|
||||
epg_timeshift: None,
|
||||
created_at: None,
|
||||
exp_date: Some(1672705545),
|
||||
max_connections: Some(1),
|
||||
status: Some(ProxyUserStatus::Active),
|
||||
},
|
||||
ProxyUserCredentials {
|
||||
username: "Test2".to_string(),
|
||||
password: "Test".to_string(),
|
||||
token: Some("Test".to_string()),
|
||||
proxy: ProxyType::Reverse,
|
||||
server: Some("default".to_string()),
|
||||
epg_timeshift: None,
|
||||
created_at: None,
|
||||
exp_date: Some(1672705545),
|
||||
max_connections: Some(1),
|
||||
status: Some(ProxyUserStatus::Expired),
|
||||
},
|
||||
ProxyUserCredentials {
|
||||
username: "Test3".to_string(),
|
||||
password: "Test".to_string(),
|
||||
token: Some("Test".to_string()),
|
||||
proxy: ProxyType::Reverse,
|
||||
server: Some("default".to_string()),
|
||||
epg_timeshift: None,
|
||||
created_at: None,
|
||||
exp_date: Some(1672705545),
|
||||
max_connections: Some(1),
|
||||
status: Some(ProxyUserStatus::Expired),
|
||||
},
|
||||
ProxyUserCredentials {
|
||||
username: "Test4".to_string(),
|
||||
password: "Test".to_string(),
|
||||
token: Some("Test".to_string()),
|
||||
proxy: ProxyType::Reverse,
|
||||
server: Some("default".to_string()),
|
||||
epg_timeshift: None,
|
||||
created_at: None,
|
||||
exp_date: Some(1672705545),
|
||||
max_connections: Some(1),
|
||||
status: Some(ProxyUserStatus::Expired),
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
let mut cfg = Config::default();
|
||||
let target_user = vec![user];
|
||||
cfg.t_config_path = temp_dir().to_string_lossy().to_string();
|
||||
let _ = store_api_user(&cfg, &target_user);
|
||||
|
||||
let user_list = load_api_user(&cfg);
|
||||
assert!(user_list.is_ok());
|
||||
assert_eq!(user_list.as_ref().unwrap().len(), 1);
|
||||
assert_eq!(user_list.as_ref().unwrap().get(0).unwrap().credentials.len(), 4);
|
||||
}
|
||||
}
|
||||
@@ -117,7 +117,7 @@ fn write_config_file<T>(file_path: &str, backup_dir: &str, config: &T, default_n
|
||||
T: ?Sized + Serialize {
|
||||
let path = PathBuf::from(file_path);
|
||||
let filename = path.file_name().map_or(default_name.to_string(), |f| f.to_string_lossy().to_string());
|
||||
let backup_path = PathBuf::from(backup_dir).join(format!("{}_{}", filename, Local::now().format("%Y%m%d_%H%M%S")));
|
||||
let backup_path = PathBuf::from(backup_dir).join(format!("{filename}_{}", Local::now().format("%Y%m%d_%H%M%S")));
|
||||
|
||||
|
||||
match std::fs::copy(&path, &backup_path) {
|
||||
|
||||
Reference in New Issue
Block a user