diff --git a/Cargo.lock b/Cargo.lock index 7a2d82d8..f20e0e4b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1292,7 +1292,7 @@ checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" [[package]] name = "ffplayout" -version = "0.22.3" +version = "0.23.0" dependencies = [ "chrono", "clap", @@ -1314,7 +1314,7 @@ dependencies = [ [[package]] name = "ffplayout-api" -version = "0.22.3" +version = "0.23.0" dependencies = [ "actix-files", "actix-multipart", @@ -1330,6 +1330,7 @@ dependencies = [ "faccess", "ffplayout-lib", "futures-util", + "home", "jsonwebtoken", "lazy_static", "lexical-sort", @@ -1357,13 +1358,14 @@ dependencies = [ [[package]] name = "ffplayout-lib" -version = "0.22.3" +version = "0.23.0" dependencies = [ "chrono", "crossbeam-channel", "derive_more", "ffprobe", "file-rotate", + "home", "lazy_static", "lettre", "lexical-sort", @@ -3587,7 +3589,7 @@ dependencies = [ [[package]] name = "tests" -version = "0.22.3" +version = "0.23.0" dependencies = [ "chrono", "crossbeam-channel", diff --git a/Cargo.toml b/Cargo.toml index df1ca2fa..c619d299 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ default-members = ["ffplayout-api", "ffplayout-engine", "tests"] resolver = "2" [workspace.package] -version = "0.22.3" +version = "0.23.0" license = "GPL-3.0" repository = "https://github.com/ffplayout/ffplayout" authors = ["Jonathan Baecker "] diff --git a/ffplayout-api/Cargo.toml b/ffplayout-api/Cargo.toml index 96d1b1ab..ef2c9855 100644 --- a/ffplayout-api/Cargo.toml +++ b/ffplayout-api/Cargo.toml @@ -27,6 +27,7 @@ clap = { version = "4.3", features = ["derive"] } derive_more = "0.99" faccess = "0.2" futures-util = { version = "0.3", default-features = false, features = ["std"] } +home = "0.5" jsonwebtoken = "9" lazy_static = "1.4" lexical-sort = "0.3" diff --git a/ffplayout-api/src/api/routes.rs b/ffplayout-api/src/api/routes.rs index 514c5a62..3eab3073 100644 --- a/ffplayout-api/src/api/routes.rs +++ b/ffplayout-api/src/api/routes.rs @@ -855,7 +855,7 @@ pub async fn gen_playlist( let mut path_list = vec![]; for path in paths { - let (p, _, _) = norm_abs_path(&config.storage.path, path); + let (p, _, _) = norm_abs_path(&config.storage.path, path)?; path_list.push(p); } @@ -1024,7 +1024,7 @@ async fn get_file( let (config, _) = playout_config(&pool.into_inner(), &id).await?; let storage_path = config.storage.path; let file_path = req.match_info().query("filename"); - let (path, _, _) = norm_abs_path(&storage_path, file_path); + let (path, _, _) = norm_abs_path(&storage_path, file_path)?; let file = actix_files::NamedFile::open(path)?; Ok(file diff --git a/ffplayout-api/src/utils/errors.rs b/ffplayout-api/src/utils/errors.rs index 3ca1b79e..dc5d418d 100644 --- a/ffplayout-api/src/utils/errors.rs +++ b/ffplayout-api/src/utils/errors.rs @@ -12,6 +12,9 @@ pub enum ServiceError { #[display(fmt = "Conflict: {_0}")] Conflict(String), + #[display(fmt = "Forbidden: {_0}")] + Forbidden(String), + #[display(fmt = "Unauthorized: {_0}")] Unauthorized(String), @@ -31,6 +34,7 @@ impl ResponseError for ServiceError { } ServiceError::BadRequest(ref message) => HttpResponse::BadRequest().json(message), ServiceError::Conflict(ref message) => HttpResponse::Conflict().json(message), + ServiceError::Forbidden(ref message) => HttpResponse::Forbidden().json(message), ServiceError::Unauthorized(ref message) => HttpResponse::Unauthorized().json(message), ServiceError::NoContent(ref message) => HttpResponse::NoContent().json(message), ServiceError::ServiceUnavailable(ref message) => { diff --git a/ffplayout-api/src/utils/files.rs b/ffplayout-api/src/utils/files.rs index 14903cd9..4dca58e3 100644 --- a/ffplayout-api/src/utils/files.rs +++ b/ffplayout-api/src/utils/files.rs @@ -6,6 +6,7 @@ use std::{ use actix_multipart::Multipart; use actix_web::{web, HttpResponse}; use futures_util::TryStreamExt as _; +use lazy_static::lazy_static; use lexical_sort::{natural_lexical_cmp, PathSort}; use rand::{distributions::Alphanumeric, Rng}; use relative_path::RelativePath; @@ -54,11 +55,30 @@ pub struct VideoFile { duration: f64, } +lazy_static! { + pub static ref HOME_DIR: String = home::home_dir() + .unwrap_or("/home/h1wl3n2og".into()) // any random not existing folder + .as_os_str() + .to_string_lossy() + .to_string(); +} + +const FOLDER_WHITELIST: &[&str; 6] = &[ + "/media", + "/mnt", + "/playlists", + "/tv-media", + "/usr/share/ffplayout", + "/var/lib/ffplayout", +]; + /// Normalize absolut path /// /// This function takes care, that it is not possible to break out from root_path. -/// It also gives always a relative path back. -pub fn norm_abs_path(root_path: &Path, input_path: &str) -> (PathBuf, String, String) { +pub fn norm_abs_path( + root_path: &Path, + input_path: &str, +) -> Result<(PathBuf, String, String), ServiceError> { let path_relative = RelativePath::new(&root_path.to_string_lossy()) .normalize() .to_string() @@ -91,7 +111,15 @@ pub fn norm_abs_path(root_path: &Path, input_path: &str) -> (PathBuf, String, St let path = &root_path.join(&source_relative); - (path.to_path_buf(), path_suffix, source_relative) + if !FOLDER_WHITELIST.iter().any(|f| path.starts_with(f)) + && !path.starts_with(HOME_DIR.to_string()) + { + return Err(ServiceError::Forbidden( + "Access forbidden: Folder cannot be opened.".to_string(), + )); + } + + Ok((path.to_path_buf(), path_suffix, source_relative)) } /// File Browser @@ -114,7 +142,7 @@ pub async fn browser( let mut extensions = config.storage.extensions; extensions.append(&mut channel_extensions); - let (path, parent, path_component) = norm_abs_path(&config.storage.path, &path_obj.source); + let (path, parent, path_component) = norm_abs_path(&config.storage.path, &path_obj.source)?; let parent_path = if !path_component.is_empty() { path.parent().unwrap() @@ -212,7 +240,7 @@ pub async fn create_directory( path_obj: &PathObject, ) -> Result { let (config, _) = playout_config(conn, &id).await?; - let (path, _, _) = norm_abs_path(&config.storage.path, &path_obj.source); + let (path, _, _) = norm_abs_path(&config.storage.path, &path_obj.source)?; if let Err(e) = fs::create_dir_all(&path).await { return Err(ServiceError::BadRequest(e.to_string())); @@ -283,8 +311,8 @@ pub async fn rename_file( move_object: &MoveObject, ) -> Result { let (config, _) = playout_config(conn, &id).await?; - let (source_path, _, _) = norm_abs_path(&config.storage.path, &move_object.source); - let (mut target_path, _, _) = norm_abs_path(&config.storage.path, &move_object.target); + let (source_path, _, _) = norm_abs_path(&config.storage.path, &move_object.source)?; + let (mut target_path, _, _) = norm_abs_path(&config.storage.path, &move_object.target)?; if !source_path.exists() { return Err(ServiceError::BadRequest("Source file not exist!".into())); @@ -318,7 +346,7 @@ pub async fn remove_file_or_folder( source_path: &str, ) -> Result<(), ServiceError> { let (config, _) = playout_config(conn, &id).await?; - let (source, _, _) = norm_abs_path(&config.storage.path, source_path); + let (source, _, _) = norm_abs_path(&config.storage.path, source_path)?; if !source.exists() { return Err(ServiceError::BadRequest("Source does not exists!".into())); @@ -351,7 +379,7 @@ pub async fn remove_file_or_folder( async fn valid_path(conn: &Pool, id: i32, path: &str) -> Result { let (config, _) = playout_config(conn, &id).await?; - let (test_path, _, _) = norm_abs_path(&config.storage.path, path); + let (test_path, _, _) = norm_abs_path(&config.storage.path, path)?; if !test_path.is_dir() { return Err(ServiceError::BadRequest("Target folder not exists!".into())); diff --git a/ffplayout-api/src/utils/playlist.rs b/ffplayout-api/src/utils/playlist.rs index bb5390e7..7feee196 100644 --- a/ffplayout-api/src/utils/playlist.rs +++ b/ffplayout-api/src/utils/playlist.rs @@ -14,7 +14,8 @@ pub async fn read_playlist( date: String, ) -> Result { let (config, _) = playout_config(conn, &id).await?; - let mut playlist_path = PathBuf::from(&config.playlist.path); + let (path, _, _) = norm_abs_path(&config.playlist.path, "")?; + let mut playlist_path = path; let d: Vec<&str> = date.split('-').collect(); playlist_path = playlist_path .join(d[0]) @@ -96,7 +97,7 @@ pub async fn generate_playlist( for path in &source.paths { let (safe_path, _, _) = - norm_abs_path(&config.storage.path, &path.to_string_lossy()); + norm_abs_path(&config.storage.path, &path.to_string_lossy())?; paths.push(safe_path); } diff --git a/ffplayout-frontend b/ffplayout-frontend index 390544ae..8d63cc4f 160000 --- a/ffplayout-frontend +++ b/ffplayout-frontend @@ -1 +1 @@ -Subproject commit 390544ae3ad4494667a2c6c2f3385412e1e7eac0 +Subproject commit 8d63cc4f85f3cbd530d509d74494b6fefbb9bf2c diff --git a/lib/Cargo.toml b/lib/Cargo.toml index e862fd82..48770847 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -14,6 +14,7 @@ crossbeam-channel = "0.5" derive_more = "0.99" ffprobe = "0.4" file-rotate = "0.7" +home = "0.5" lazy_static = "1.4" lettre = { version = "0.11", features = ["builder", "rustls-tls", "smtp-transport"], default-features = false } lexical-sort = "0.3" diff --git a/lib/src/utils/config.rs b/lib/src/utils/config.rs index 1e62a96d..c28aa029 100644 --- a/lib/src/utils/config.rs +++ b/lib/src/utils/config.rs @@ -15,7 +15,7 @@ use shlex::split; use crate::AdvancedConfig; use super::vec_strings; -use crate::utils::{free_tcp_socket, home_dir, time_to_sec, OutputMode::*}; +use crate::utils::{free_tcp_socket, time_to_sec, OutputMode::*}; pub const DUMMY_LEN: f64 = 60.0; pub const IMAGE_FORMAT: [&str; 21] = [ @@ -406,7 +406,7 @@ impl PlayoutConfig { config.general.config_path = config_path.to_string_lossy().to_string(); - config.general.stat_file = home_dir() + config.general.stat_file = home::home_dir() .unwrap_or_else(env::temp_dir) .join(if config.general.stat_file.is_empty() { ".ffp_status" diff --git a/lib/src/utils/mod.rs b/lib/src/utils/mod.rs index 99dfbcb8..cdae30d9 100644 --- a/lib/src/utils/mod.rs +++ b/lib/src/utils/mod.rs @@ -10,9 +10,6 @@ use std::{ sync::{Arc, Mutex}, }; -#[cfg(not(windows))] -use std::env; - use chrono::{prelude::*, TimeDelta}; use ffprobe::{ffprobe, Stream as FFStream}; use rand::prelude::*; @@ -33,9 +30,6 @@ pub mod json_serializer; mod json_validate; mod logging; -#[cfg(windows)] -mod windows; - pub use config::{ self as playout_config, OutputMode::{self, *}, @@ -988,19 +982,6 @@ pub fn custom_format(template: &str, args: &[T]) -> String { filled_template } -pub fn home_dir() -> Option { - home_dir_inner() -} - -#[cfg(windows)] -use windows::home_dir_inner; - -#[cfg(any(unix, target_os = "redox"))] -fn home_dir_inner() -> Option { - #[allow(deprecated)] - env::home_dir() -} - /// Get system time, in non test/debug case. #[cfg(not(any(test, debug_assertions)))] pub fn time_now() -> DateTime { diff --git a/lib/src/utils/windows.rs b/lib/src/utils/windows.rs deleted file mode 100644 index 58aae005..00000000 --- a/lib/src/utils/windows.rs +++ /dev/null @@ -1,43 +0,0 @@ -use std::{env, ffi::OsString, os::windows::ffi::OsStringExt, path::PathBuf, ptr}; - -use winapi::shared::minwindef::MAX_PATH; -use winapi::shared::winerror::S_OK; -use winapi::um::shlobj::{SHGetFolderPathW, CSIDL_PROFILE}; - -pub fn home_dir_inner() -> Option { - env::var_os("USERPROFILE") - .filter(|s| !s.is_empty()) - .map(PathBuf::from) - .or_else(home_dir_crt) -} - -#[cfg(not(target_vendor = "uwp"))] -fn home_dir_crt() -> Option { - unsafe { - let mut path: Vec = Vec::with_capacity(MAX_PATH); - match SHGetFolderPathW( - ptr::null_mut(), - CSIDL_PROFILE, - ptr::null_mut(), - 0, - path.as_mut_ptr(), - ) { - S_OK => { - let len = wcslen(path.as_ptr()); - path.set_len(len); - let s = OsString::from_wide(&path); - Some(PathBuf::from(s)) - } - _ => None, - } - } -} - -#[cfg(target_vendor = "uwp")] -fn home_dir_crt() -> Option { - None -} - -extern "C" { - fn wcslen(buf: *const u16) -> usize; -}