diff --git a/Cargo.lock b/Cargo.lock index 566fe15..cd70b6f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3164,7 +3164,7 @@ dependencies = [ [[package]] name = "rustus" -version = "0.7.2" +version = "0.7.3" dependencies = [ "actix-cors", "actix-files", diff --git a/Cargo.toml b/Cargo.toml index c1bf68d..66546b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustus" -version = "0.7.2" +version = "0.7.3" edition = "2021" description = "TUS protocol implementation written in Rust." keywords = ["tus", "server", "actix-web"] diff --git a/docs/configuration.md b/docs/configuration.md index 05ee32f..df8d8cd 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -24,6 +24,9 @@ Also you can configure number of actix `workers` that handle connections. `--cors` is a list of allowed hosts with wildcards separated by commas. By default all hosts are allowed. You can define which hosts are allowed for your particular application. +`--allow-empty` is a parameter that allows users to upload empty files. Empty +file means that while creation 0 bytes was passed as an `Upload-Length`. + For example if you add `--cors "*.staging.domain,*.prod.domain"`, it allows all origins like `my.staging.domain` or `my.prod.domain`, but it will refuse to serve other origins. @@ -39,7 +42,8 @@ Also you can disable access log for `/health` endpoint, by using `--disable-heal --url "/files" \ --log-level "INFO" \ --cors "my.*.domain.com,your.*.domain.com" \ - --disable-health-access-log + --disable-health-access-log \ + --allow-empty ``` === "ENV" @@ -53,6 +57,7 @@ Also you can disable access log for `/health` endpoint, by using `--disable-heal export RUSTUS_LOG_LEVEL="INFO" export RUSTUS_CORS="my.*.domain.com,your.*.domain.com" export RUSTUS_DISABLE_HEALTH_ACCESS_LOG="true" + export RUSTUS_ALLOW_EMPTY="true" rustus ``` @@ -166,7 +171,9 @@ Parameters: * `--s3-bucket` - name of a bucket to use; * `--s3-region` - AWS region to use; * `--s3-access-key` - S3 access key; +* `--s3-access-key-path` - S3 access key path; * `--s3-secret-key` - S3 secret key; +* `--s3-secret-key-path` - S3 secret key path; * `--s3-security-token` - s3 secrity token; * `--s3-session-token` - S3 session token; * `--s3-profile` - Name of the section from `~/.aws/credentials` file; @@ -183,7 +190,9 @@ Required parameter are only `--s3-url` and `--s3-bucket`. --s3-bucket "bucket" \ --s3-region "eu-central1" \ --s3-access-key "fJljHcXo07rqIOzh" \ + --s3-access-key-path "/run/agenix/S3_ACCESS_KEY" \ --s3-secret-key "6BJfBUL18nLiGmF5zKW0NKrdxQVxNYWB" \ + --s3-secret-key-path "/run/agenix/S3_SECRET_KEY" \ --s3-profile "my_profile" \ --s3-security-token "token" \ --s3-session-token "token" \ @@ -202,7 +211,9 @@ Required parameter are only `--s3-url` and `--s3-bucket`. export RUSTUS_S3_BUCKET="bucket" export RUSTUS_S3_REGION="eu-central1" export RUSTUS_S3_ACCESS_KEY="fJljHcXo07rqIOzh" + export RUSTUS_S3_ACCESS_KEY_PATH="/run/agenix/S3_ACCESS_KEY" export RUSTUS_S3_SECRET_KEY="6BJfBUL18nLiGmF5zKW0NKrdxQVxNYWB" + export RUSTUS_S3_SECRET_KEY_PATH="/run/agenix/S3_SECCRET_KEY" export RUSTUS_S3_SECURITY_TOKEN="token" export RUSTUS_S3_SESSION_TOKEN="token" export RUSTUS_S3_PROFILE="my_profile" diff --git a/src/config.rs b/src/config.rs index 78a997f..3f37a4c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -62,12 +62,26 @@ pub struct StorageOptions { #[arg(long, env = "RUSTUS_S3_ACCESS_KEY")] pub s3_access_key: Option, + /// S3 access key path. + /// + /// This parameter is used fo s3-based storages. + /// path to file that has s3-access-key inside. + #[arg(long, env = "RUSTUS_S3_ACCESS_KEY_PATH")] + pub s3_access_key_path: Option, + /// S3 secret key. /// /// This parameter is required fo s3-based storages. #[arg(long, env = "RUSTUS_S3_SECRET_KEY")] pub s3_secret_key: Option, + /// S3 secret key path. + /// + /// This parameter is required fo s3-based storages. + /// path to file that has s3-secret-key inside. + #[arg(long, env = "RUSTUS_S3_SECRET_KEY_PATH")] + pub s3_secret_key_path: Option, + /// S3 URL. /// /// This parameter is required fo s3-based storages. @@ -366,6 +380,12 @@ pub struct RustusConf { )] pub tus_extensions: Vec, + /// Enabling this parameter + /// Will allow creation of empty files + /// when Upload-Length header equals to 0. + #[arg(long, env = "RUSTUS_ALLOW_EMPTY")] + pub allow_empty: bool, + /// Remove part files after concatenation is done. /// By default rustus does nothing with part files after concatenation. /// diff --git a/src/notifiers/http_notifier.rs b/src/notifiers/http_notifier.rs index 06aaed8..cc0fe1a 100644 --- a/src/notifiers/http_notifier.rs +++ b/src/notifiers/http_notifier.rs @@ -66,11 +66,15 @@ impl Notifier for HttpNotifier { .headers() .get("Content-Type") .and_then(|hval| hval.to_str().ok().map(String::from)); - return Err(RustusError::HTTPHookError( - real_resp.status().as_u16(), - real_resp.text().await.unwrap_or_default(), - content_type, - )); + let status = real_resp.status().as_u16(); + let text = real_resp.text().await.unwrap_or_default(); + log::warn!( + "Got wrong response for `{hook}`. Status code: `{status}`, body: `{body}`", + hook = hook, + status = status, + body = text, + ); + return Err(RustusError::HTTPHookError(status, text, content_type)); } } Ok(()) diff --git a/src/notifiers/models/notification_manager.rs b/src/notifiers/models/notification_manager.rs index 923adc3..f84f12e 100644 --- a/src/notifiers/models/notification_manager.rs +++ b/src/notifiers/models/notification_manager.rs @@ -74,6 +74,7 @@ impl NotificationManager { hook: Hook, header_map: &HeaderMap, ) -> RustusResult<()> { + log::debug!("Sending a `{}` hook with body `{}`", hook, message); for notifier in &self.notifiers { notifier .send_message(message.clone(), hook, header_map) diff --git a/src/protocol/creation/routes.rs b/src/protocol/creation/routes.rs index c191fe8..946a83b 100644 --- a/src/protocol/creation/routes.rs +++ b/src/protocol/creation/routes.rs @@ -80,6 +80,15 @@ pub async fn create_file( ) -> actix_web::Result { // Getting Upload-Length header value as usize. let length = parse_header(&request, "Upload-Length"); + + // With this option enabled, + // we have to check whether length is a non-zero number. + if !state.config.allow_empty { + if let Some(0) = length { + return Ok(HttpResponse::BadRequest().body("Upload-Length should be greater than zero")); + } + } + // Checking Upload-Defer-Length header. let defer_size = check_header(&request, "Upload-Defer-Length", |val| val == "1"); @@ -284,6 +293,31 @@ mod tests { assert_eq!(file_info.offset, 0); } + #[actix_rt::test] + async fn wrong_length() { + let state = State::test_new().await; + let mut rustus = get_service(state.clone()).await; + let request = TestRequest::post() + .uri(state.config.test_url().as_str()) + .insert_header(("Upload-Length", 0)) + .to_request(); + let resp = call_service(&mut rustus, request).await; + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); + } + + #[actix_rt::test] + async fn allow_empty() { + let mut state = State::test_new().await; + state.config.allow_empty = true; + let mut rustus = get_service(state.clone()).await; + let request = TestRequest::post() + .uri(state.config.test_url().as_str()) + .insert_header(("Upload-Length", 0)) + .to_request(); + let resp = call_service(&mut rustus, request).await; + assert_eq!(resp.status(), StatusCode::CREATED); + } + #[actix_rt::test] async fn success_with_bytes() { let state = State::test_new().await; diff --git a/src/storages/models/available_stores.rs b/src/storages/models/available_stores.rs index 508cc3c..b33d6ef 100644 --- a/src/storages/models/available_stores.rs +++ b/src/storages/models/available_stores.rs @@ -4,6 +4,11 @@ use crate::{ RustusConf, Storage, }; use derive_more::{Display, From}; +use std::{ + fs::File, + io::{BufReader, Read}, + path::PathBuf, +}; use strum::EnumIter; /// Enum of available Storage implementations. @@ -35,11 +40,19 @@ impl AvailableStores { )), Self::HybridS3 => { log::warn!("Hybrid S3 is an unstable feature. If you ecounter a problem, please raise an issue: https://github.com/s3rius/rustus/issues."); + let access_key = from_string_or_path( + &config.storage_opts.s3_access_key, + &config.storage_opts.s3_access_key_path, + ); + let secret_key = from_string_or_path( + &config.storage_opts.s3_secret_key, + &config.storage_opts.s3_secret_key_path, + ); Box::new(s3_hybrid_storage::S3HybridStorage::new( config.storage_opts.s3_url.clone().unwrap(), config.storage_opts.s3_region.clone().unwrap(), - &config.storage_opts.s3_access_key, - &config.storage_opts.s3_secret_key, + &Some(access_key), + &Some(secret_key), &config.storage_opts.s3_security_token, &config.storage_opts.s3_session_token, &config.storage_opts.s3_profile, @@ -54,3 +67,20 @@ impl AvailableStores { } } } + +// TODO this should probably be a COW +fn from_string_or_path(variable: &Option, path: &Option) -> String { + if let Some(variable) = variable { + variable.to_string() + } else if let Some(path) = path { + let file = File::open("path_to_your_file") + .unwrap_or_else(|_| panic!("failed to open path {}", path.display())); + let mut contents = String::new(); + BufReader::new(file) + .read_to_string(&mut contents) + .unwrap_or_else(|_| panic!("failed to read from path {}", path.display())); + contents + } else { + panic!("can't find {variable:?} or path {path:?}") + } +}