diff --git a/virtweb_backend/Cargo.lock b/virtweb_backend/Cargo.lock index 3cbff97..bf900f5 100644 --- a/virtweb_backend/Cargo.lock +++ b/virtweb_backend/Cargo.lock @@ -863,6 +863,12 @@ version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" +[[package]] +name = "futures-io" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" + [[package]] name = "futures-macro" version = "0.3.28" @@ -893,8 +899,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" dependencies = [ "futures-core", + "futures-io", "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", "slab", @@ -1528,10 +1537,12 @@ dependencies = [ "serde_urlencoded", "tokio", "tokio-native-tls", + "tokio-util", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", "winreg", ] @@ -2000,9 +2011,11 @@ dependencies = [ "lazy_static", "light-openid", "log", + "reqwest", "serde", "serde_json", "tempfile", + "url", ] [[package]] @@ -2086,6 +2099,19 @@ version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" +[[package]] +name = "wasm-streams" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4609d447824375f43e1ffbc051b50ad8f4b3ae8219680c94452ea05eb240ac7" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "web-sys" version = "0.3.64" diff --git a/virtweb_backend/Cargo.toml b/virtweb_backend/Cargo.toml index e596e98..192fc47 100644 --- a/virtweb_backend/Cargo.toml +++ b/virtweb_backend/Cargo.toml @@ -9,7 +9,7 @@ edition = "2021" log = "0.4.19" env_logger = "0.10.0" clap = { version = "4.3.19", features = ["derive", "env"] } -light-openid = { version = "1.0.1", features=["crypto-wrapper"] } +light-openid = { version = "1.0.1", features = ["crypto-wrapper"] } lazy_static = "1.4.0" actix-web = "4" actix-remote-ip = "0.1.0" @@ -21,4 +21,6 @@ serde_json = "1.0.105" futures-util = "0.3.28" anyhow = "1.0.75" actix-multipart = "0.6.1" -tempfile = "3.8.0" \ No newline at end of file +tempfile = "3.8.0" +reqwest = { version = "0.11.18", features = ["stream"] } +url = "2.4.0" \ No newline at end of file diff --git a/virtweb_backend/src/constants.rs b/virtweb_backend/src/constants.rs index 96af19f..ec19c29 100644 --- a/virtweb_backend/src/constants.rs +++ b/virtweb_backend/src/constants.rs @@ -17,7 +17,11 @@ pub const ROUTES_WITHOUT_AUTH: [&str; 5] = [ ]; /// Allowed ISO mimetypes -pub const ALLOWED_ISO_MIME_TYPES: [&str; 1] = ["application/x-cd-image"]; +pub const ALLOWED_ISO_MIME_TYPES: [&str; 3] = [ + "application/x-cd-image", + "application/x-iso9660-image", + "application/octet-stream", +]; /// ISO max size pub const ISO_MAX_SIZE: usize = 10 * 1000 * 1000 * 1000; diff --git a/virtweb_backend/src/controllers/iso_controller.rs b/virtweb_backend/src/controllers/iso_controller.rs index 6aed72d..bc038f3 100644 --- a/virtweb_backend/src/controllers/iso_controller.rs +++ b/virtweb_backend/src/controllers/iso_controller.rs @@ -4,7 +4,10 @@ use crate::controllers::HttpResult; use crate::utils::files_utils; use actix_multipart::form::tempfile::TempFile; use actix_multipart::form::MultipartForm; -use actix_web::HttpResponse; +use actix_web::{web, HttpResponse}; +use futures_util::StreamExt; +use std::fs::File; +use std::io::Write; #[derive(Debug, MultipartForm)] pub struct UploadIsoForm { @@ -58,3 +61,47 @@ pub async fn upload_file(MultipartForm(mut form): MultipartForm) Ok(HttpResponse::Accepted().finish()) } + +#[derive(serde::Deserialize)] +pub struct DownloadFromURLReq { + url: String, + filename: String, +} + +/// Upload ISO file from URL +pub async fn upload_from_url(req: web::Json) -> HttpResult { + if !files_utils::check_file_name(&req.filename) || !req.filename.ends_with(".iso") { + return Ok(HttpResponse::BadRequest().json("Invalid file name!")); + } + + let dest_file = AppConfig::get().iso_storage_path().join(&req.filename); + + if dest_file.exists() { + return Ok(HttpResponse::Conflict().json("A similar file already exists!")); + } + + let response = reqwest::get(&req.url).await?; + + if let Some(len) = response.content_length() { + if len > constants::ISO_MAX_SIZE as u64 { + return Ok(HttpResponse::BadRequest().json("File is too large!")); + } + } + + if let Some(ct) = response.headers().get("content-type") { + if !constants::ALLOWED_ISO_MIME_TYPES.contains(&ct.to_str()?) { + return Ok(HttpResponse::BadRequest().json("Invalid file mimetype!")); + } + } + + let mut stream = response.bytes_stream(); + + let mut file = File::create(dest_file)?; + + while let Some(item) = stream.next().await { + let bytes = item?; + file.write_all(&bytes)?; + } + + Ok(HttpResponse::Accepted().finish()) +} diff --git a/virtweb_backend/src/controllers/mod.rs b/virtweb_backend/src/controllers/mod.rs index 9b0e10a..635fb39 100644 --- a/virtweb_backend/src/controllers/mod.rs +++ b/virtweb_backend/src/controllers/mod.rs @@ -65,4 +65,16 @@ impl From for HttpErr { } } +impl From for HttpErr { + fn from(value: reqwest::Error) -> Self { + HttpErr { err: value.into() } + } +} + +impl From for HttpErr { + fn from(value: reqwest::header::ToStrError) -> Self { + HttpErr { err: value.into() } + } +} + pub type HttpResult = Result; diff --git a/virtweb_backend/src/main.rs b/virtweb_backend/src/main.rs index 8fc11c5..20afedb 100644 --- a/virtweb_backend/src/main.rs +++ b/virtweb_backend/src/main.rs @@ -104,6 +104,10 @@ async fn main() -> std::io::Result<()> { "/api/iso/upload", web::post().to(iso_controller::upload_file), ) + .route( + "/api/iso/upload_from_url", + web::post().to(iso_controller::upload_from_url), + ) }) .bind(&AppConfig::get().listen_address)? .run() diff --git a/virtweb_backend/src/utils/mod.rs b/virtweb_backend/src/utils/mod.rs index b68ecd0..bd18501 100644 --- a/virtweb_backend/src/utils/mod.rs +++ b/virtweb_backend/src/utils/mod.rs @@ -1 +1,2 @@ pub mod files_utils; +pub mod url_utils; diff --git a/virtweb_backend/src/utils/url_utils.rs b/virtweb_backend/src/utils/url_utils.rs new file mode 100644 index 0000000..3e468ad --- /dev/null +++ b/virtweb_backend/src/utils/url_utils.rs @@ -0,0 +1,58 @@ +use std::fmt::Display; +use url::Url; + +/// Check out whether a URL is valid or not +pub fn check_url(url: impl Display) -> bool { + match Url::parse(&url.to_string()) { + Ok(u) => { + if u.scheme() != "http" && u.scheme() != "https" { + log::debug!("URL is invalid, scheme is not http or https!"); + return false; + } + + if u.port_or_known_default() != Some(443) && u.port_or_known_default() != Some(80) { + log::debug!("URL is invalid, port is not 80 or 443!"); + return false; + } + + true + } + Err(e) => { + log::debug!("URL is invalid, could not be parsed! {e}"); + false + } + } +} + +#[cfg(test)] +mod test { + use crate::utils::url_utils::check_url; + + #[test] + fn valid_url_ubuntu() { + assert!(check_url( + "https://releases.ubuntu.com/22.04.3/ubuntu-22.04.3-desktop-amd64.iso" + )); + } + + #[test] + fn valid_url_with_port_ubuntu() { + assert!(check_url( + "https://releases.ubuntu.com:443/22.04.3/ubuntu-22.04.3-desktop-amd64.iso" + )); + } + + #[test] + fn invalid_valid_url_ftp() { + assert!(!check_url( + "ftp://releases.ubuntu.com/22.04.3/ubuntu-22.04.3-desktop-amd64.iso" + )); + } + + #[test] + fn invalid_valid_bad_port() { + assert!(!check_url( + "http://releases.ubuntu.com:81/22.04.3/ubuntu-22.04.3-desktop-amd64.iso" + )); + } +} diff --git a/virtweb_frontend/src/api/IsoFilesApi.ts b/virtweb_frontend/src/api/IsoFilesApi.ts index 3114b90..fb909f4 100644 --- a/virtweb_frontend/src/api/IsoFilesApi.ts +++ b/virtweb_frontend/src/api/IsoFilesApi.ts @@ -18,4 +18,15 @@ export class IsoFilesApi { progress: progress, }); } + + /** + * Upload iso from URL + */ + static async UploadFromURL(url: string, filename: string): Promise { + await APIClient.exec({ + method: "POST", + uri: "/iso/upload_from_url", + jsonData: { url: url, filename: filename }, + }); + } } diff --git a/virtweb_frontend/src/routes/IsoFilesRoute.tsx b/virtweb_frontend/src/routes/IsoFilesRoute.tsx index 3cc5e68..a056199 100644 --- a/virtweb_frontend/src/routes/IsoFilesRoute.tsx +++ b/virtweb_frontend/src/routes/IsoFilesRoute.tsx @@ -1,4 +1,4 @@ -import { Button, LinearProgress, Typography } from "@mui/material"; +import { Button, LinearProgress, TextField, Typography } from "@mui/material"; import { filesize } from "filesize"; import { MuiFileInput } from "mui-file-input"; import React from "react"; @@ -8,16 +8,20 @@ import { useAlert } from "../hooks/providers/AlertDialogProvider"; import { useSnackbar } from "../hooks/providers/SnackbarProvider"; import { VirtWebPaper } from "../widgets/VirtWebPaper"; import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer"; +import { useLoadingMessage } from "../hooks/providers/LoadingMessageProvider"; export function IsoFilesRoute(): React.ReactElement { return ( - alert("file uploaded!")} /> + alert("file uploaded!")} /> + alert("file uploaded!")} + /> ); } -function UploadIsoFileForm(p: { +function UploadIsoFileCard(p: { onFileUploaded: () => void; }): React.ReactElement { const alert = useAlert(); @@ -53,6 +57,8 @@ function UploadIsoFileForm(p: { setValue(null); snackbar("The file was successfully uploaded!"); + + p.onFileUploaded(); } catch (e) { console.error(e); await alert("Failed to perform file upload! " + e); @@ -75,7 +81,6 @@ function UploadIsoFileForm(p: { return (
- ); } + +function UploadIsoFileFromUrlCard(p: { + onFileUploaded: () => void; +}): React.ReactElement { + const alert = useAlert(); + const snackbar = useSnackbar(); + const loadingMessage = useLoadingMessage(); + + const [url, setURL] = React.useState(""); + const [filename, setFilename] = React.useState(null); + + const autoFileName = url.split("/").slice(-1)[0]; + const actualFileName = filename ?? autoFileName; + + const upload = async () => { + try { + loadingMessage.show("Downloading file from URL..."); + await IsoFilesApi.UploadFromURL(url, actualFileName); + + setURL(""); + setFilename(null); + snackbar("Successfully downloaded file!"); + } catch (e) { + console.error(e); + alert("Failed to download file!"); + } + loadingMessage.hide(); + }; + + return ( + +
+ setURL(e.target.value)} + /> + + setFilename(e.target.value)} + /> + {url !== "" && actualFileName !== "" && ( + + )} +
+
+ ); +} diff --git a/virtweb_frontend/src/widgets/VirtWebPaper.tsx b/virtweb_frontend/src/widgets/VirtWebPaper.tsx index 5991e74..88fd0bb 100644 --- a/virtweb_frontend/src/widgets/VirtWebPaper.tsx +++ b/virtweb_frontend/src/widgets/VirtWebPaper.tsx @@ -5,7 +5,7 @@ export function VirtWebPaper( p: { label: string } & PropsWithChildren ): React.ReactElement { return ( - +