Can upload ISO files

This commit is contained in:
2023-09-05 13:19:25 +02:00
parent 83ccd3a4b9
commit 036595fb24
24 changed files with 671 additions and 36 deletions

View File

@ -1,4 +1,5 @@
use clap::Parser;
use std::path::{Path, PathBuf};
/// VirtWeb backend API
#[derive(Parser, Debug, Clone)]
@ -64,6 +65,10 @@ pub struct AppConfig {
#[arg(long, env, default_value = "APP_ORIGIN/oidc_cb")]
oidc_redirect_url: String,
/// Storage directory
#[arg(long, env, default_value = "storage")]
pub storage: String,
/// Directory where temporary files are stored
#[arg(long, env, default_value = "/tmp")]
pub temp_dir: String,
@ -131,6 +136,16 @@ impl AppConfig {
self.oidc_redirect_url
.replace("APP_ORIGIN", &self.website_origin)
}
/// Get root storage directory
pub fn storage_path(&self) -> PathBuf {
Path::new(&self.storage).canonicalize().unwrap()
}
/// Get iso storage directory
pub fn iso_storage_path(&self) -> PathBuf {
self.storage_path().join("iso")
}
}
#[derive(Debug, Clone, serde::Serialize)]

View File

@ -15,3 +15,9 @@ pub const ROUTES_WITHOUT_AUTH: [&str; 5] = [
"/api/auth/start_oidc",
"/api/auth/finish_oidc",
];
/// Allowed ISO mimetypes
pub const ALLOWED_ISO_MIME_TYPES: [&str; 1] = ["application/x-cd-image"];
/// ISO max size
pub const ISO_MAX_SIZE: usize = 10 * 1000 * 1000 * 1000;

View File

@ -0,0 +1,60 @@
use crate::app_config::AppConfig;
use crate::constants;
use crate::controllers::HttpResult;
use crate::utils::files_utils;
use actix_multipart::form::tempfile::TempFile;
use actix_multipart::form::MultipartForm;
use actix_web::HttpResponse;
#[derive(Debug, MultipartForm)]
pub struct UploadIsoForm {
#[multipart(rename = "file")]
files: Vec<TempFile>,
}
/// Upload iso file
pub async fn upload_file(MultipartForm(mut form): MultipartForm<UploadIsoForm>) -> HttpResult {
if form.files.is_empty() {
log::error!("Missing uploaded ISO file!");
return Ok(HttpResponse::BadRequest().json("Missing file!"));
}
let file = form.files.remove(0);
if file.size > constants::ISO_MAX_SIZE {
log::error!("Uploaded ISO file is too large!");
return Ok(HttpResponse::BadRequest().json("File is too large!"));
}
if let Some(m) = &file.content_type {
if !constants::ALLOWED_ISO_MIME_TYPES.contains(&m.to_string().as_str()) {
log::error!("Uploaded ISO file has an invalid mimetype!");
return Ok(HttpResponse::BadRequest().json("Invalid mimetype!"));
}
}
let file_name = match &file.file_name {
None => {
log::error!("Uploaded ISO file does not have a name!");
return Ok(HttpResponse::BadRequest().json("Missing file name!"));
}
Some(f) => f,
};
if !files_utils::check_file_name(file_name) {
log::error!("Bad file name for uploaded iso!");
return Ok(HttpResponse::BadRequest().json("Bad file name!"));
}
let dest_file = AppConfig::get().iso_storage_path().join(file_name);
log::info!("Will save ISO file {:?}", dest_file);
if dest_file.exists() {
log::error!("Conflict with uploaded iso file name!");
return Ok(HttpResponse::Conflict().json("The file already exists!"));
}
file.file.persist(dest_file)?;
Ok(HttpResponse::Accepted().finish())
}

View File

@ -5,6 +5,7 @@ use std::fmt::{Display, Formatter};
use std::io::ErrorKind;
pub mod auth_controller;
pub mod iso_controller;
pub mod server_controller;
/// Custom error to ease controller writing
@ -58,4 +59,10 @@ impl From<std::num::ParseIntError> for HttpErr {
}
}
impl From<tempfile::PersistError> for HttpErr {
fn from(value: tempfile::PersistError) -> Self {
HttpErr { err: value.into() }
}
}
pub type HttpResult = Result<HttpResponse, HttpErr>;

View File

@ -1,4 +1,5 @@
use crate::app_config::AppConfig;
use crate::constants;
use crate::extractors::local_auth_extractor::LocalAuthEnabled;
use actix_web::{HttpResponse, Responder};
@ -10,11 +11,15 @@ pub async fn root_index() -> impl Responder {
struct StaticConfig {
local_auth_enabled: bool,
oidc_auth_enabled: bool,
iso_mimetypes: &'static [&'static str],
iso_max_size: usize,
}
pub async fn static_config(local_auth: LocalAuthEnabled) -> impl Responder {
HttpResponse::Ok().json(StaticConfig {
local_auth_enabled: *local_auth,
oidc_auth_enabled: !AppConfig::get().disable_oidc,
iso_mimetypes: &constants::ALLOWED_ISO_MIME_TYPES,
iso_max_size: constants::ISO_MAX_SIZE,
})
}

View File

@ -3,3 +3,4 @@ pub mod constants;
pub mod controllers;
pub mod extractors;
pub mod middlewares;
pub mod utils;

View File

@ -1,6 +1,8 @@
use actix_cors::Cors;
use actix_identity::config::LogoutBehaviour;
use actix_identity::IdentityMiddleware;
use actix_multipart::form::tempfile::TempFileConfig;
use actix_multipart::form::MultipartFormConfig;
use actix_remote_ip::RemoteIPConfig;
use actix_session::storage::CookieSessionStore;
use actix_session::SessionMiddleware;
@ -12,16 +14,21 @@ use actix_web::{web, App, HttpServer};
use light_openid::basic_state_manager::BasicStateManager;
use std::time::Duration;
use virtweb_backend::app_config::AppConfig;
use virtweb_backend::constants;
use virtweb_backend::constants::{
MAX_INACTIVITY_DURATION, MAX_SESSION_DURATION, SESSION_COOKIE_NAME,
};
use virtweb_backend::controllers::{auth_controller, server_controller};
use virtweb_backend::controllers::{auth_controller, iso_controller, server_controller};
use virtweb_backend::middlewares::auth_middleware::AuthChecker;
use virtweb_backend::utils::files_utils;
#[actix_web::main]
async fn main() -> std::io::Result<()> {
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
log::debug!("Create required directory, if missing");
files_utils::create_directory_if_missing(&AppConfig::get().iso_storage_path()).unwrap();
log::info!("Start to listen on {}", AppConfig::get().listen_address);
let state_manager = Data::new(BasicStateManager::new());
@ -62,6 +69,10 @@ async fn main() -> std::io::Result<()> {
.app_data(Data::new(RemoteIPConfig {
proxy: AppConfig::get().proxy_ip.clone(),
}))
// Uploaded files
.app_data(web::PayloadConfig::new(constants::ISO_MAX_SIZE))
.app_data(MultipartFormConfig::default().total_limit(constants::ISO_MAX_SIZE))
.app_data(TempFileConfig::default().directory(&AppConfig::get().temp_dir))
// Server controller
.route("/", web::get().to(server_controller::root_index))
.route(
@ -89,6 +100,11 @@ async fn main() -> std::io::Result<()> {
"/api/auth/sign_out",
web::get().to(auth_controller::sign_out),
)
// ISO controller
.route(
"/api/iso/upload",
web::post().to(iso_controller::upload_file),
)
})
.bind(&AppConfig::get().listen_address)?
.run()

View File

@ -0,0 +1,49 @@
use std::path::PathBuf;
const INVALID_CHARS: [&str; 19] = [
"@", "\\", "/", ":", ",", "<", ">", "%", "'", "\"", "?", "{", "}", "$", "*", "|", ";", "=",
"\t",
];
/// Check out whether a file name is valid or not
pub fn check_file_name(name: &str) -> bool {
!name.is_empty() && !INVALID_CHARS.iter().any(|c| name.contains(c))
}
/// Create directory if missing
pub fn create_directory_if_missing(path: &PathBuf) -> anyhow::Result<()> {
if !path.exists() {
std::fs::create_dir_all(path)?;
}
Ok(())
}
#[cfg(test)]
mod test {
use crate::utils::files_utils::check_file_name;
#[test]
fn empty_file_name() {
assert!(!check_file_name(""));
}
#[test]
fn parent_dir_file_name() {
assert!(!check_file_name("../file.test"));
}
#[test]
fn windows_parent_dir_file_name() {
assert!(!check_file_name("..\\test.fr"));
}
#[test]
fn special_char_file_name() {
assert!(!check_file_name("test:test.@"));
}
#[test]
fn valid_file_name() {
assert!(check_file_name("test.iso"));
}
}

View File

@ -0,0 +1 @@
pub mod files_utils;