From 0d4808880fd05d206a4f0774b6aacfba500288c7 Mon Sep 17 00:00:00 2001 From: "Helmut K. C. Tessarek" Date: Fri, 21 Jul 2023 06:28:17 -0400 Subject: [PATCH] feat(server): support multiple auth tokens (#84) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(server): support multiple auth tokens Example: ```toml [server] auth_tokens = [ "super_secret_token1", "super_secret_token2", ] ``` The previously used `AUTH_TOKEN` environment variable can still be used and will be evaluated as well. * fixtures: add all tokens in array to the test * add deprecation warning for auth_token * also add deprecation warnings at server startup * fix formatting * fixed tests, so that we do not use deprecated config options * use bash array * refactor: use separate function * refactor: check auth tokens * Update fixtures/test-server-auth-multiple-tokens/test.sh Co-authored-by: Orhun Parmaksız * refactor: convert functions to methods * refactor: check function * refactor: get_tokens method * style(format): add newline between functions * refactor(server): print deprecation warnings once --------- Co-authored-by: Orhun Parmaksız --- README.md | 8 ++-- config.toml | 4 ++ .../config.toml | 10 ++++ .../test-server-auth-multiple-tokens/test.sh | 27 +++++++++++ src/auth.rs | 46 +++++++++++-------- src/config.rs | 34 ++++++++++++++ src/main.rs | 1 + src/server.rs | 36 +++++---------- 8 files changed, 120 insertions(+), 46 deletions(-) create mode 100644 fixtures/test-server-auth-multiple-tokens/config.toml create mode 100755 fixtures/test-server-auth-multiple-tokens/test.sh diff --git a/README.md b/README.md index d096d41..e96aa51 100644 --- a/README.md +++ b/README.md @@ -219,7 +219,7 @@ $ curl -F "remote=https://example.com/file.png" "" #### Cleaning up expired files -Configure `delete_expired_files` to set an interval for deleting the expired files automatically. +Configure `[paste].delete_expired_files` to set an interval for deleting the expired files automatically. On the other hand, following script can be used as [cron](https://en.wikipedia.org/wiki/Cron) for cleaning up the expired files manually: @@ -253,14 +253,16 @@ $ echo "AUTH_TOKEN=$(openssl rand -base64 16)" > .env $ rustypaste ``` +You can also set multiple auth tokens via the array field `[server].auth_tokens` in your `config.toml`. + See [config.toml](./config.toml) for configuration options. #### HTML Form It is possible to use an HTML form for uploading files. To do so, you need to update two fields in your `config.toml`: -- Set the `landing_page_content_type` to `text/html; charset=utf-8`. -- Update the `landing_page` field with your HTML form. +- Set the `[landing_page].content_type` to `text/html; charset=utf-8`. +- Update the `[landing_page].text` field with your HTML form or point `[landing_page].file` to your html file. For an example, see [examples/html_form.toml](./examples/html_form.toml) diff --git a/config.toml b/config.toml index 04cfbc3..970a501 100644 --- a/config.toml +++ b/config.toml @@ -9,6 +9,10 @@ max_content_length = "10MB" upload_path = "./upload" timeout = "30s" expose_version = false +#auth_tokens = [ +# "super_secret_token1", +# "super_secret_token2", +#] [landing_page] text = """ diff --git a/fixtures/test-server-auth-multiple-tokens/config.toml b/fixtures/test-server-auth-multiple-tokens/config.toml new file mode 100644 index 0000000..837bc46 --- /dev/null +++ b/fixtures/test-server-auth-multiple-tokens/config.toml @@ -0,0 +1,10 @@ +[server] +address="127.0.0.1:8000" +max_content_length="10MB" +upload_path="./upload" +auth_tokens=["token1", "token2", "rustypasteisawesome", "token4"] + +[paste] +random_url = { enabled = false, type = "petname", words = 2, separator = "-" } +default_extension = "txt" +duplicate_files = false diff --git a/fixtures/test-server-auth-multiple-tokens/test.sh b/fixtures/test-server-auth-multiple-tokens/test.sh new file mode 100755 index 0000000..bd0a97b --- /dev/null +++ b/fixtures/test-server-auth-multiple-tokens/test.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +auth_tokens=("rustypasteisawesome" "token1" "token2" "token4") + +content="topsecret" + +setup() { + echo "$content" > file +} + +run_test() { + result=$(curl -s -F "file=@file" localhost:8000) + test "unauthorized" = "$result" + + for auth_token in ${auth_tokens[@]} + do + result=$(curl -s -F "file=@file" -H "Authorization: $auth_token" localhost:8000) + test "unauthorized" != "$result" + test "$content" = "$(cat upload/file.txt)" + test "$content" = "$(curl -s $result)" + done +} + +teardown() { + rm file + rm -r upload +} diff --git a/src/auth.rs b/src/auth.rs index 553d737..22cced8 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -4,21 +4,19 @@ use actix_web::{error, Error}; /// Checks the authorization header for the specified token. /// /// `Authorization: (type) ` -pub fn check(host: &str, headers: &HeaderMap, token: Option) -> Result<(), Error> { - if let Some(token) = token { - if !token.is_empty() { - let auth_header = headers - .get(AUTHORIZATION) - .map(|v| v.to_str().unwrap_or_default()) - .map(|v| v.split_whitespace().last().unwrap_or_default()); - if auth_header.unwrap_or_default() != token { - log::warn!( - "authorization failure for {} (header: {})", - host, - auth_header.unwrap_or("none"), - ); - return Err(error::ErrorUnauthorized("unauthorized")); - } +pub fn check(host: &str, headers: &HeaderMap, tokens: Option>) -> Result<(), Error> { + if let Some(tokens) = tokens { + let auth_header = headers + .get(AUTHORIZATION) + .map(|v| v.to_str().unwrap_or_default()) + .map(|v| v.split_whitespace().last().unwrap_or_default()); + if !tokens.iter().any(|v| v == auth_header.unwrap_or_default()) { + log::warn!( + "authorization failure for {} (header: {})", + host, + auth_header.unwrap_or("none"), + ); + return Err(error::ErrorUnauthorized("unauthorized")); } } Ok(()) @@ -33,11 +31,23 @@ mod tests { fn test_check_auth() -> Result<(), Error> { let mut headers = HeaderMap::new(); headers.insert(AUTHORIZATION, HeaderValue::from_static("basic test_token")); - assert!(check("", &headers, Some(String::from("test_token"))).is_ok()); - assert!(check("", &headers, Some(String::from("invalid_token"))).is_err()); + assert!(check("", &headers, Some(vec!["test_token".to_string()])).is_ok()); + assert!(check("", &headers, Some(vec!["invalid_token".to_string()])).is_err()); + assert!(check( + "", + &headers, + Some(vec!["invalid1".to_string(), "test_token".to_string()]) + ) + .is_ok()); + assert!(check( + "", + &headers, + Some(vec!["invalid1".to_string(), "invalid2".to_string()]) + ) + .is_err()); assert!(check("", &headers, None).is_ok()); assert!(check("", &HeaderMap::new(), None).is_ok()); - assert!(check("", &HeaderMap::new(), Some(String::from("token"))).is_err()); + assert!(check("", &HeaderMap::new(), Some(vec!["token".to_string()])).is_err()); Ok(()) } } diff --git a/src/config.rs b/src/config.rs index 310cc8f..dc90e21 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,7 +1,9 @@ use crate::mime::MimeMatcher; use crate::random::RandomURLConfig; +use crate::AUTH_TOKEN_ENV; use byte_unit::Byte; use config::{self, ConfigError}; +use std::env; use std::path::{Path, PathBuf}; use std::time::Duration; @@ -44,7 +46,10 @@ pub struct ServerConfig { #[serde(default, with = "humantime_serde")] pub timeout: Option, /// Authentication token. + #[deprecated(note = "use [server].auth_tokens instead")] pub auth_token: Option, + /// Authentication tokens. + pub auth_tokens: Option>, /// Expose version. pub expose_version: Option, /// Landing page text. @@ -107,6 +112,35 @@ impl Config { .build()? .try_deserialize() } + + /// Retrieves all configured tokens. + #[allow(deprecated)] + pub fn get_tokens(&self) -> Option> { + let mut tokens = self.server.auth_tokens.clone().unwrap_or_default(); + if let Some(token) = &self.server.auth_token { + tokens.insert(0, token.to_string()); + } + if let Ok(env_token) = env::var(AUTH_TOKEN_ENV) { + tokens.insert(0, env_token); + } + (!tokens.is_empty()).then_some(tokens) + } + + /// Print deprecation warnings. + #[allow(deprecated)] + pub fn warn_deprecation(&self) { + if self.server.auth_token.is_some() { + log::warn!("[server].auth_token is deprecated, please use [server].auth_tokens"); + } + if self.server.landing_page.is_some() { + log::warn!("[server].landing_page is deprecated, please use [landing_page].text"); + } + if self.server.landing_page_content_type.is_some() { + log::warn!( + "[server].landing_page_content_type is deprecated, please use [landing_page].content_type" + ); + } + } } #[cfg(test)] diff --git a/src/main.rs b/src/main.rs index a6e5a3c..fd75ac4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -48,6 +48,7 @@ fn setup(config_folder: &Path) -> IoResult<(Data>, ServerConfig, }; let config = Config::parse(&config_path).expect("failed to parse config"); log::trace!("{:#?}", config); + config.warn_deprecation(); let server_config = config.server.clone(); let paste_config = RwLock::new(config.paste.clone()); let (config_sender, config_receiver) = mpsc::channel::(); diff --git a/src/server.rs b/src/server.rs index 08a27f8..1d69db3 100644 --- a/src/server.rs +++ b/src/server.rs @@ -5,7 +5,6 @@ use crate::header::{self, ContentDisposition}; use crate::mime as mime_util; use crate::paste::{Paste, PasteType}; use crate::util; -use crate::AUTH_TOKEN_ENV; use actix_files::NamedFile; use actix_multipart::Multipart; use actix_web::{error, get, post, web, Error, HttpRequest, HttpResponse}; @@ -37,7 +36,6 @@ async fn index(config: web::Data>) -> Result if let Some(ref mut landing_page) = config.landing_page { landing_page.text = config.server.landing_page; } - log::warn!("[server].landing_page is deprecated, please use [landing_page].text"); } if config.server.landing_page_content_type.is_some() { if config.landing_page.is_none() { @@ -46,9 +44,6 @@ async fn index(config: web::Data>) -> Result if let Some(ref mut landing_page) = config.landing_page { landing_page.content_type = config.server.landing_page_content_type; } - log::warn!( - "[server].landing_page_content_type is deprecated, please use [landing_page].content_type" - ); } if let Some(mut landing_page) = config.landing_page { if let Some(file) = landing_page.file { @@ -159,13 +154,8 @@ async fn version( .map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?; let connection = request.connection_info().clone(); let host = connection.realip_remote_addr().unwrap_or("unknown host"); - auth::check( - host, - request.headers(), - env::var(AUTH_TOKEN_ENV) - .ok() - .or_else(|| config.server.auth_token.as_ref().cloned()), - )?; + let tokens = config.get_tokens(); + auth::check(host, request.headers(), tokens)?; if !config.server.expose_version.unwrap_or(false) { log::warn!("server is not configured to expose version endpoint"); Err(error::ErrorForbidden("endpoint is not exposed"))?; @@ -184,6 +174,13 @@ async fn upload( ) -> Result { let connection = request.connection_info().clone(); let host = connection.realip_remote_addr().unwrap_or("unknown host"); + { + let config = config + .read() + .map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?; + let tokens = config.get_tokens(); + auth::check(host, request.headers(), tokens)?; + } let server_url = match config .read() .map_err(|_| error::ErrorInternalServerError("cannot acquire config"))? @@ -196,17 +193,6 @@ async fn upload( format!("{}://{}", connection.scheme(), connection.host(),) } }; - auth::check( - host, - request.headers(), - env::var(AUTH_TOKEN_ENV).ok().or(config - .read() - .map_err(|_| error::ErrorInternalServerError("cannot acquire config"))? - .server - .auth_token - .as_ref() - .cloned()), - )?; let time = util::get_system_time()?; let mut expiry_date = header::parse_expiry_date(request.headers(), time)?; if expiry_date.is_none() { @@ -462,7 +448,7 @@ mod tests { #[actix_web::test] async fn test_version_without_auth() -> Result<(), Error> { let mut config = Config::default(); - config.server.auth_token = Some(String::from("test")); + config.server.auth_tokens = Some(vec!["test".to_string()]); let app = test::init_service( App::new() .app_data(Data::new(RwLock::new(config))) @@ -526,7 +512,7 @@ mod tests { #[actix_web::test] async fn test_auth() -> Result<(), Error> { let mut config = Config::default(); - config.server.auth_token = Some(String::from("test")); + config.server.auth_tokens = Some(vec!["test".to_string()]); let app = test::init_service( App::new()