feat(server): support multiple auth tokens (#84)
* 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 <orhunparmaksiz@gmail.com> * 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 <orhunparmaksiz@gmail.com>
This commit is contained in:
parent
b1bdc45767
commit
0d4808880f
|
@ -219,7 +219,7 @@ $ curl -F "remote=https://example.com/file.png" "<server_address>"
|
|||
|
||||
#### 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)
|
||||
|
||||
|
|
|
@ -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 = """
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
}
|
46
src/auth.rs
46
src/auth.rs
|
@ -4,21 +4,19 @@ use actix_web::{error, Error};
|
|||
/// Checks the authorization header for the specified token.
|
||||
///
|
||||
/// `Authorization: (type) <token>`
|
||||
pub fn check(host: &str, headers: &HeaderMap, token: Option<String>) -> 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<Vec<String>>) -> 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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Duration>,
|
||||
/// Authentication token.
|
||||
#[deprecated(note = "use [server].auth_tokens instead")]
|
||||
pub auth_token: Option<String>,
|
||||
/// Authentication tokens.
|
||||
pub auth_tokens: Option<Vec<String>>,
|
||||
/// Expose version.
|
||||
pub expose_version: Option<bool>,
|
||||
/// Landing page text.
|
||||
|
@ -107,6 +112,35 @@ impl Config {
|
|||
.build()?
|
||||
.try_deserialize()
|
||||
}
|
||||
|
||||
/// Retrieves all configured tokens.
|
||||
#[allow(deprecated)]
|
||||
pub fn get_tokens(&self) -> Option<Vec<String>> {
|
||||
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)]
|
||||
|
|
|
@ -48,6 +48,7 @@ fn setup(config_folder: &Path) -> IoResult<(Data<RwLock<Config>>, 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::<Config>();
|
||||
|
|
|
@ -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<RwLock<Config>>) -> Result<HttpResponse, Error>
|
|||
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<RwLock<Config>>) -> Result<HttpResponse, Error>
|
|||
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<HttpResponse, Error> {
|
||||
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()
|
||||
|
|
Loading…
Reference in New Issue