diff --git a/.gitignore b/.gitignore index ea8c4bf..4875c65 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +/images \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index e8c01c2..bea2407 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -911,6 +911,7 @@ dependencies = [ "rocket_codegen", "rocket_http", "serde", + "serde_json", "state", "tempfile", "time", @@ -1020,6 +1021,12 @@ dependencies = [ "serde", ] +[[package]] +name = "sha1_smol" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" + [[package]] name = "sha2" version = "0.10.5" @@ -1342,6 +1349,29 @@ dependencies = [ "subtle", ] +[[package]] +name = "uuid" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd6469f4314d5f1ffec476e05f17cc9a78bc7a27a6a857842170bdf8d6f98d2f" +dependencies = [ + "getrandom", + "rand", + "sha1_smol", + "uuid-macro-internal", +] + +[[package]] +name = "uuid-macro-internal" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "548f7181a5990efa50237abb7ebca410828b57a8955993334679f8b50b35c97d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "valuable" version = "0.1.0" @@ -1359,6 +1389,8 @@ name = "violetta" version = "0.1.0" dependencies = [ "rocket", + "serde", + "uuid", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index b602dd1..be923d2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,10 +3,16 @@ name = "violetta" version = "0.1.0" edition = "2021" -[toolchain] -channel = "nightly" - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -rocket = { version = "0.5.0-rc.2", features = ["secrets"] } \ No newline at end of file +rocket = { version = "0.5.0-rc.2", features = ["serde_json", "json"] } +serde = "1.0.144" + +[dependencies.uuid] +version = "1.1.2" +features = [ + "v5", # Lets you generate random UUIDs + "fast-rng", # Use a faster (but still sufficiently random) RNG + "macro-diagnostics", # Enable better diagnostics for compile-time UUIDs +] diff --git a/rocket.yml b/rocket.yml index 5305991..da609b9 100644 --- a/rocket.yml +++ b/rocket.yml @@ -1,10 +1,8 @@ [default] address = "0.0.0.0" -port = +port = 8080 + -[default.tls] -certs = "path/to/cert-chain.pem" -key = "path/to/key.pem" [default.limits] form = "64 kB" @@ -22,5 +20,7 @@ mercy = 5 port = 3000 limits = { json = "10MiB" } + [release] -port = 8443 \ No newline at end of file +port = 8443 +secret_key = "THIS IS A PLACEHOLDER, GENERATE YOUR OWN KEYS." \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 405a358..7aa6f34 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,24 +1,208 @@ -mod test; +use rocket::{ + data::ToByteUnit, + fairing::AdHoc, + figment::{ + providers::{Env, Format, Serialized, Toml}, + Figment, Profile, + }, + fs::{relative, NamedFile}, + http::{ContentType, Status}, + request::{FromRequest, Outcome}, + serde::json::Json, + serde::*, + tokio::{fs::File, io::AsyncWriteExt}, + *, +}; +use uuid::Uuid; -use rocket::*; +#[derive(Default)] +struct RequiresAuthentication; -#[get("/hello/")] -fn greet_user( name: String ) -> String { - format!("Hello, {name}!") +#[rocket::async_trait] +impl<'a> FromRequest<'a> for RequiresAuthentication { + type Error = UploadResponse; + + async fn from_request(req: &'a Request<'_>) -> Outcome { + let expected_key = &req + .guard::<&State>() + .await + .unwrap() + .inner() + .auth_key; + match expected_key { + Some(actual_key @ _) => { + if let Some(extern_key) = req.headers().get_one("Authorization") { + if actual_key == extern_key { + Outcome::Success(RequiresAuthentication::default()) + } else { + Outcome::Failure((Status::Unauthorized, UploadResponse::error())) + } + } else { + Outcome::Failure((Status::Unauthorized, UploadResponse::error())) + } + } + None => Outcome::Success(RequiresAuthentication::default()), + } + } +} + +#[derive(Deserialize)] +struct UploaderConfiguration { + pub max_size: u32, + pub auth_key: Option, + pub allowed_extensions: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +struct UploadResponse { + successful: bool, + file_id: Option, +} + +impl UploadResponse { + fn new(successful: bool, file_id: Option) -> Self { + Self { + successful, + file_id, + } + } + + fn error() -> Self { + Self { + successful: false, + file_id: None, + } + } +} + +#[put("/", data = "")] +async fn upload_media( + file: Data<'_>, + config: &State, + _a: RequiresAuthentication, + content_type: &ContentType, +) -> std::io::Result> { + match content_type.extension().map(|ext| ext.as_str()) { + ext @ Some("png") | ext @ Some("mp4") => { + let ext = ext.unwrap(); + let current_dir = std::env::current_dir().unwrap(); + let raw_data = file + .open(config.max_size.mebibytes()) + .into_bytes() + .await? + .into_inner(); + let file_id = Uuid::new_v5(&Uuid::NAMESPACE_OID, &raw_data); + let mut file = File::create(current_dir.join(format!( + "{}/{}.{ext}", + relative!("images"), + file_id + ))) + .await?; + file.write_all(&raw_data.as_slice()).await?; + Ok(Json(UploadResponse::new(true, Some(file_id.to_string())))) + } + _ => Ok(Json(UploadResponse::error())), + } +} + +#[get("/")] +async fn retrieve_media(file_id: &str, config: &State) -> Option { + let current_dir = std::env::current_dir().unwrap(); + let ext: Option = { + let mut corresponding_ext: Option = None; + for ext in config.allowed_extensions.iter() { + let corresponding_path = + current_dir.join(format!("{}/{}.{ext}", relative!("images"), file_id)); + + if corresponding_path.is_file() { + corresponding_ext = Some(ext.clone()); + } + } + corresponding_ext + }; + if let Some(ext) = ext { + Some( + NamedFile::open(current_dir.join(format!("{}/{file_id}.{ext}", relative!("images")))) + .await + .unwrap(), + ) + } else { + None + } } #[get("/")] -fn index( ) -> &'static str { +fn index() -> &'static str { "USAGE - GET /hello/:name - simply, greets you. + GET / + what are you reading right now + + PUT / + upload a file, send as raw body + + GET /:id + gets the file <:id> " } +#[catch(401)] +fn unauthorized() -> Json { + Json(UploadResponse::error()) +} + +#[catch(500)] +fn internal_server_err() -> Json { + Json(UploadResponse::error()) +} + +#[catch(404)] +fn not_found() -> Json { + Json(UploadResponse::error()) +} + #[launch] pub fn rocket() -> _ { - rocket::build() - .mount( - "/", routes![ index, greet_user ] - ) + rocket::custom( + Figment::from(rocket::Config::default()) + .merge(Serialized::defaults(Config::default())) + .merge(Toml::file("violetta.toml").nested()) + .merge(Env::prefixed("VIOLETTA_").global()) + .select(Profile::from_env_or("VIOLETTA_PROFILE", "default")), + ) + .mount("/", routes![index, upload_media, retrieve_media]) + .register("/", catchers![not_found, unauthorized, internal_server_err]) + .attach(AdHoc::config::()) +} + + +#[cfg(test)] +mod test { + use crate::*; + use rocket::local::blocking::Client; + + macro_rules! client { + () => { + Client::tracked(rocket()).expect("valid rocket instance") + }; + } + + macro_rules! request { + ( $client:ident, $method:ident, $path:expr ) => { + $client.$method($path) + }; + } + + #[test] + fn upload_file() { + let client = client!(); + let response = request!(client, put, "/") + .header(ContentType::PNG) + .body(&[0u8; 1024]) + .dispatch(); + let response_body = response.into_json::().unwrap(); + assert_eq!(response_body.successful, true); + assert!(response_body.file_id.is_some()); + let response = request!(client, get, format!( "/{}", response_body.file_id.unwrap() )).dispatch(); + assert_eq!(response.into_bytes().unwrap().as_slice(), [0u8; 1024]); + } } diff --git a/src/test.rs b/src/test.rs deleted file mode 100644 index 4987ba8..0000000 --- a/src/test.rs +++ /dev/null @@ -1,14 +0,0 @@ -#[cfg(test)] -mod test { - use rocket::local::blocking::Client; - use crate::*; - - #[test] - fn does_it_greet_us( ) { - let client = Client::tracked( rocket() ) - .expect( "invalid rocket instance" ); - - let response = client.get( "/hello/World" ).dispatch( ); - assert_eq!( response.into_string( ).unwrap( ), "Hello, World!" ); - } -} \ No newline at end of file diff --git a/violetta.toml b/violetta.toml new file mode 100644 index 0000000..9698c42 --- /dev/null +++ b/violetta.toml @@ -0,0 +1,3 @@ +[default] +max_size = 16 # in mebibytes [MiB] +allowed_extensions = [ "png", "jpg", "mp4" ] \ No newline at end of file