Merge pull request #666 from jb-alvarado/master
allow only whitelisted folders to be used in ffpapi
This commit is contained in:
commit
dc88d541f4
10
Cargo.lock
generated
10
Cargo.lock
generated
@ -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",
|
||||
|
@ -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 <jonbae77@gmail.com>"]
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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) => {
|
||||
|
@ -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<HttpResponse, ServiceError> {
|
||||
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<MoveObject, ServiceError> {
|
||||
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<Sqlite>, id: i32, path: &str) -> Result<PathBuf, ServiceError> {
|
||||
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()));
|
||||
|
@ -14,7 +14,8 @@ pub async fn read_playlist(
|
||||
date: String,
|
||||
) -> Result<JsonPlaylist, ServiceError> {
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit 390544ae3ad4494667a2c6c2f3385412e1e7eac0
|
||||
Subproject commit 8d63cc4f85f3cbd530d509d74494b6fefbb9bf2c
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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<T: fmt::Display>(template: &str, args: &[T]) -> String {
|
||||
filled_template
|
||||
}
|
||||
|
||||
pub fn home_dir() -> Option<PathBuf> {
|
||||
home_dir_inner()
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
use windows::home_dir_inner;
|
||||
|
||||
#[cfg(any(unix, target_os = "redox"))]
|
||||
fn home_dir_inner() -> Option<PathBuf> {
|
||||
#[allow(deprecated)]
|
||||
env::home_dir()
|
||||
}
|
||||
|
||||
/// Get system time, in non test/debug case.
|
||||
#[cfg(not(any(test, debug_assertions)))]
|
||||
pub fn time_now() -> DateTime<Local> {
|
||||
|
@ -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<PathBuf> {
|
||||
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<PathBuf> {
|
||||
unsafe {
|
||||
let mut path: Vec<u16> = 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<PathBuf> {
|
||||
None
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
fn wcslen(buf: *const u16) -> usize;
|
||||
}
|
Loading…
Reference in New Issue
Block a user