diff --git a/.gitignore b/.gitignore index ea8c4bf..0592392 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +.DS_Store diff --git a/Cargo.lock b/Cargo.lock index d6fa200..b793357 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -111,6 +111,21 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.28" @@ -118,6 +133,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -126,6 +142,34 @@ version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" +[[package]] +name = "futures-executor" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.13", +] + [[package]] name = "futures-sink" version = "0.3.28" @@ -144,10 +188,27 @@ version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" dependencies = [ + "futures-channel", "futures-core", + "futures-io", + "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" +dependencies = [ + "cfg-if", + "libc", + "wasi", ] [[package]] @@ -387,11 +448,15 @@ name = "nyazoom" version = "0.1.0" dependencies = [ "axum", + "futures", + "rand", "tokio", + "tokio-util", "tower", "tower-http", "tracing", "tracing-subscriber", + "urlencoding", ] [[package]] @@ -467,6 +532,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + [[package]] name = "proc-macro2" version = "1.0.56" @@ -485,6 +556,36 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "redox_syscall" version = "0.2.16" @@ -842,6 +943,12 @@ version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" +[[package]] +name = "urlencoding" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9" + [[package]] name = "valuable" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index f19be94..d5953b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,8 +7,12 @@ edition = "2021" [dependencies] axum = { version = "0.6.12", features = ["multipart", "http2"] } +futures = "0.3.28" +rand = { version = "0.8.5", features = ["small_rng"] } tokio = { version = "1.27.0", features = ["full"] } +tokio-util = { version = "0.7.7", features = ["io"] } tower = { version = "0.4.13", features = ["util"] } tower-http = { version = "0.4.0", features = ["fs", "trace", "limit"] } tracing = "0.1.37" tracing-subscriber = { version = "0.3.16", features = ["env-filter"] } +urlencoding = "2.1.2" diff --git a/src/main.rs b/src/main.rs index 9f1e503..b829ab0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,56 +1,161 @@ +use std::io; use std::net::SocketAddr; +use std::path::{Path, PathBuf}; +use axum::body::Bytes; +use axum::http::StatusCode; +use axum::routing::post; +use axum::BoxError; use axum::{ extract::{DefaultBodyLimit, Multipart}, - response::{IntoResponse, Redirect}, + response::Redirect, Router, }; -use axum::routing::post; +use futures::{Stream, TryStreamExt}; +use rand::distributions::{Alphanumeric, DistString}; +use rand::rngs::SmallRng; +use rand::SeedableRng; +use tokio::fs::File; +use tokio::io::BufWriter; +use tokio_util::io::StreamReader; use tower_http::{limit::RequestBodyLimitLayer, services::ServeDir, trace::TraceLayer}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; #[tokio::main] -async fn main() { +async fn main() -> io::Result<()> { tracing_subscriber::registry() .with( tracing_subscriber::EnvFilter::try_from_default_env() - .unwrap_or_else(|_| "nyazoo=debug,tower_http=debug".into()), + .unwrap_or_else(|_| "nyazoom=debug,tower_http=debug".into()), ) .with(tracing_subscriber::fmt::layer()) .init(); + // uses create_dir_all to create both .cache and .temp inside it in one go + make_dir(".cache/.temp").await?; + + // Router Setup let with_big_body = Router::new() .route("/upload", post(upload)) .layer(DefaultBodyLimit::disable()) .layer(RequestBodyLimitLayer::new( - 250 * 1024 * 1024, // 250Mb + 10 * 1024 * 1024 * 1024, // 10GiB )); - let base = Router::new().nest_service("/", ServeDir::new("dist")); + let base = Router::new() + .nest_service("/", ServeDir::new("dist")) + .nest_service("/download", ServeDir::new(".cache")); - let app = Router::new().merge(with_big_body).merge(base); + let app = Router::new() + .merge(with_big_body) + .merge(base) + .layer(TraceLayer::new_for_http()); + // Server creation let addr = SocketAddr::from(([0, 0, 0, 0], 3000)); tracing::debug!("listening on {}", addr); axum::Server::bind(&addr) - .serve(app.layer(TraceLayer::new_for_http()).into_make_service()) + .serve(app.into_make_service()) .await .unwrap(); + + Ok(()) } -async fn upload(mut body: Multipart) -> impl IntoResponse { - while let Some(field) = body.next_field().await.unwrap() { - let name = field.name().unwrap().to_string(); - let content_type = field.content_type().unwrap().to_string(); - let file_name = field.file_name().unwrap().to_string(); - let data = field.bytes().await.unwrap(); +async fn upload(mut body: Multipart) -> Result { + let cache_folder = Path::new(".cache/.temp").join(get_random_name(10)); - tracing::debug!( - "\n\nLength of {name} ({file_name}: {content_type}) is {} bytes\n", - data.len() - ) + make_dir(&cache_folder) + .await + .map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()))?; + + while let Some(field) = body.next_field().await.unwrap() { + let file_name = if let Some(file_name) = field.file_name() { + file_name.to_owned() + } else { + continue; + }; + + + if !path_is_valid(&file_name) { + return Err((StatusCode::BAD_REQUEST, "Invalid Filename >:(".to_owned())) + } + + let path = cache_folder.join(file_name); + + tracing::debug!("\n\nstuff written to {path:?}\n"); + stream_to_file(&path, field).await? } - Redirect::to("/") + tracing::debug!("{cache_folder:?}"); + + Ok(Redirect::to("/")) +} + +async fn stream_to_file(path: P, stream: S) -> Result<(), (StatusCode, String)> +where + P: AsRef, + S: Stream>, + E: Into, +{ + async { + // Convert the stream into an `AsyncRead`. + let body_with_io_error = stream.map_err(|err| io::Error::new(io::ErrorKind::Other, err)); + let body_reader = StreamReader::new(body_with_io_error); + futures::pin_mut!(body_reader); + + // Create the file. `File` implements `AsyncWrite`. + let mut file = BufWriter::new(File::create(&path).await?); + + // Copy the body into the file. + tokio::io::copy(&mut body_reader, &mut file).await?; + + io::Result::Ok(()) + } + .await + .map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string())) +} + +async fn remove_dir(folder: T) -> io::Result<()> +where + T: AsRef, +{ + tokio::fs::remove_dir_all(&folder).await?; + + Ok(()) +} + +#[inline] +fn path_is_valid(path: &str) -> bool { + let mut components = Path::new(path).components().peekable(); + + if let Some(first) = components.peek() { + tracing::debug!("{:?}", &first); + if !matches!(first, std::path::Component::Normal(_)) { + return false; + } + } + + components.count() == 1 +} + +#[inline] +async fn make_dir(name: T) -> io::Result<()> +where + T: AsRef, +{ + tokio::fs::create_dir_all(name) + .await + .or_else(|err| match err.kind() { + io::ErrorKind::AlreadyExists => Ok(()), + _ => Err(err), + }) +} + +#[inline] +fn get_random_name(len: usize) -> String { + let mut rng = SmallRng::from_entropy(); + + Alphanumeric.sample_string(&mut rng, len) }