Can upload ISO files
This commit is contained in:
		
							
								
								
									
										1
									
								
								virtweb_backend/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								virtweb_backend/.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -1,2 +1,3 @@
 | 
			
		||||
target/
 | 
			
		||||
.idea
 | 
			
		||||
storage
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										96
									
								
								virtweb_backend/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										96
									
								
								virtweb_backend/Cargo.lock
									
									
									
										generated
									
									
									
								
							@@ -99,6 +99,44 @@ dependencies = [
 | 
			
		||||
 "syn 2.0.29",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "actix-multipart"
 | 
			
		||||
version = "0.6.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "3b960e2aea75f49c8f069108063d12a48d329fc8b60b786dfc7552a9d5918d2d"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "actix-multipart-derive",
 | 
			
		||||
 "actix-utils",
 | 
			
		||||
 "actix-web",
 | 
			
		||||
 "bytes",
 | 
			
		||||
 "derive_more",
 | 
			
		||||
 "futures-core",
 | 
			
		||||
 "futures-util",
 | 
			
		||||
 "httparse",
 | 
			
		||||
 "local-waker",
 | 
			
		||||
 "log",
 | 
			
		||||
 "memchr",
 | 
			
		||||
 "mime",
 | 
			
		||||
 "serde",
 | 
			
		||||
 "serde_json",
 | 
			
		||||
 "serde_plain",
 | 
			
		||||
 "tempfile",
 | 
			
		||||
 "tokio",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "actix-multipart-derive"
 | 
			
		||||
version = "0.6.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "0a0a77f836d869f700e5b47ac7c3c8b9c8bc82e4aec861954c6198abee3ebd4d"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "darling",
 | 
			
		||||
 "parse-size",
 | 
			
		||||
 "proc-macro2",
 | 
			
		||||
 "quote",
 | 
			
		||||
 "syn 2.0.29",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "actix-remote-ip"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
@@ -656,6 +694,41 @@ dependencies = [
 | 
			
		||||
 "cipher",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "darling"
 | 
			
		||||
version = "0.20.3"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "darling_core",
 | 
			
		||||
 "darling_macro",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "darling_core"
 | 
			
		||||
version = "0.20.3"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "177e3443818124b357d8e76f53be906d60937f0d3a90773a664fa63fa253e621"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "fnv",
 | 
			
		||||
 "ident_case",
 | 
			
		||||
 "proc-macro2",
 | 
			
		||||
 "quote",
 | 
			
		||||
 "strsim",
 | 
			
		||||
 "syn 2.0.29",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "darling_macro"
 | 
			
		||||
version = "0.20.3"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "darling_core",
 | 
			
		||||
 "quote",
 | 
			
		||||
 "syn 2.0.29",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "deranged"
 | 
			
		||||
version = "0.3.8"
 | 
			
		||||
@@ -996,6 +1069,12 @@ dependencies = [
 | 
			
		||||
 "tokio-native-tls",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "ident_case"
 | 
			
		||||
version = "1.0.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "idna"
 | 
			
		||||
version = "0.4.0"
 | 
			
		||||
@@ -1280,6 +1359,12 @@ dependencies = [
 | 
			
		||||
 "windows-targets",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "parse-size"
 | 
			
		||||
version = "1.0.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "944553dd59c802559559161f9816429058b869003836120e262e8caec061b7ae"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "paste"
 | 
			
		||||
version = "1.0.14"
 | 
			
		||||
@@ -1560,6 +1645,15 @@ dependencies = [
 | 
			
		||||
 "serde",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "serde_plain"
 | 
			
		||||
version = "1.0.2"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "serde",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "serde_urlencoded"
 | 
			
		||||
version = "0.7.1"
 | 
			
		||||
@@ -1895,6 +1989,7 @@ version = "0.1.0"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "actix-cors",
 | 
			
		||||
 "actix-identity",
 | 
			
		||||
 "actix-multipart",
 | 
			
		||||
 "actix-remote-ip",
 | 
			
		||||
 "actix-session",
 | 
			
		||||
 "actix-web",
 | 
			
		||||
@@ -1907,6 +2002,7 @@ dependencies = [
 | 
			
		||||
 "log",
 | 
			
		||||
 "serde",
 | 
			
		||||
 "serde_json",
 | 
			
		||||
 "tempfile",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
 
 | 
			
		||||
@@ -19,4 +19,6 @@ actix-cors = "0.6.4"
 | 
			
		||||
serde = { version = "1.0.175", features = ["derive"] }
 | 
			
		||||
serde_json = "1.0.105"
 | 
			
		||||
futures-util = "0.3.28"
 | 
			
		||||
anyhow = "1.0.75"
 | 
			
		||||
anyhow = "1.0.75"
 | 
			
		||||
actix-multipart = "0.6.1"
 | 
			
		||||
tempfile = "3.8.0"
 | 
			
		||||
@@ -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)]
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										60
									
								
								virtweb_backend/src/controllers/iso_controller.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								virtweb_backend/src/controllers/iso_controller.rs
									
									
									
									
									
										Normal 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())
 | 
			
		||||
}
 | 
			
		||||
@@ -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>;
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
    })
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,3 +3,4 @@ pub mod constants;
 | 
			
		||||
pub mod controllers;
 | 
			
		||||
pub mod extractors;
 | 
			
		||||
pub mod middlewares;
 | 
			
		||||
pub mod utils;
 | 
			
		||||
 
 | 
			
		||||
@@ -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()
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										49
									
								
								virtweb_backend/src/utils/files_utils.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								virtweb_backend/src/utils/files_utils.rs
									
									
									
									
									
										Normal 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"));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1
									
								
								virtweb_backend/src/utils/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								virtweb_backend/src/utils/mod.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
pub mod files_utils;
 | 
			
		||||
		Reference in New Issue
	
	Block a user