refactor(server)!: cleanup authorization boilerplate (#199)
* refactor!: use `actix-web-grants` to protect endpoints * fix: filter out blank strings * doc: add documentation for a function * fix: don't return body for not exposed endpoints * test: add fixtures * test: fix naming * test: remove extra step in teardown
This commit is contained in:
parent
b74e9ceeaf
commit
e21f99ac4a
|
@ -269,6 +269,16 @@ dependencies = [
|
||||||
"syn 2.0.39",
|
"syn 2.0.39",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "actix-web-grants"
|
||||||
|
version = "4.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cf5941a5bdf4cc022ca7721dae70d9818d7b13f93040b0543cb901410c8d3172"
|
||||||
|
dependencies = [
|
||||||
|
"actix-web",
|
||||||
|
"protect-endpoints-proc-macro",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "addr2line"
|
name = "addr2line"
|
||||||
version = "0.21.0"
|
version = "0.21.0"
|
||||||
|
@ -2014,6 +2024,18 @@ dependencies = [
|
||||||
"prost 0.12.3",
|
"prost 0.12.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "protect-endpoints-proc-macro"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dfff647917bb00f5e9c57a5c15d95db74db5387139ac03052358e38462c86a76"
|
||||||
|
dependencies = [
|
||||||
|
"darling",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.39",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.33"
|
version = "1.0.33"
|
||||||
|
@ -2226,6 +2248,7 @@ dependencies = [
|
||||||
"actix-multipart",
|
"actix-multipart",
|
||||||
"actix-rt",
|
"actix-rt",
|
||||||
"actix-web",
|
"actix-web",
|
||||||
|
"actix-web-grants",
|
||||||
"awc",
|
"awc",
|
||||||
"byte-unit",
|
"byte-unit",
|
||||||
"config",
|
"config",
|
||||||
|
|
|
@ -20,6 +20,7 @@ shuttle = ["dep:shuttle-actix-web", "dep:shuttle-runtime", "dep:tokio"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-web = { version = "4.4.0" }
|
actix-web = { version = "4.4.0" }
|
||||||
|
actix-web-grants = { version = "4.0.3" }
|
||||||
actix-multipart = "0.6.1"
|
actix-multipart = "0.6.1"
|
||||||
actix-files = "0.6.2"
|
actix-files = "0.6.2"
|
||||||
shuttle-actix-web = { version = "0.35.0", optional = true }
|
shuttle-actix-web = { version = "0.35.0", optional = true }
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
[server]
|
||||||
|
address = "127.0.0.1:8000"
|
||||||
|
max_content_length = "10MB"
|
||||||
|
upload_path = "./upload"
|
||||||
|
expose_list = false
|
||||||
|
|
||||||
|
[paste]
|
||||||
|
random_url = { type = "petname", words = 2, separator = "-" }
|
||||||
|
default_extension = "txt"
|
||||||
|
duplicate_files = true
|
|
@ -0,0 +1,14 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
:;
|
||||||
|
}
|
||||||
|
|
||||||
|
run_test() {
|
||||||
|
result=$(curl -s --write-out "%{http_code}" http://localhost:8000/list)
|
||||||
|
test "404" = "$result"
|
||||||
|
}
|
||||||
|
|
||||||
|
teardown() {
|
||||||
|
rm -r upload
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
[server]
|
||||||
|
address = "127.0.0.1:8000"
|
||||||
|
max_content_length = "10MB"
|
||||||
|
upload_path = "./upload"
|
||||||
|
|
||||||
|
[paste]
|
||||||
|
default_extension = "txt"
|
||||||
|
duplicate_files = false
|
|
@ -0,0 +1,19 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
content="topsecret"
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
echo "$content" > file
|
||||||
|
}
|
||||||
|
|
||||||
|
run_test() {
|
||||||
|
result=$(curl -s -F "file=@file" localhost:8000)
|
||||||
|
test "unauthorized" != "$result"
|
||||||
|
test "$content" = "$(cat upload/file.txt)"
|
||||||
|
test "$content" = "$(curl -s $result)"
|
||||||
|
}
|
||||||
|
|
||||||
|
teardown() {
|
||||||
|
rm file
|
||||||
|
rm -r upload
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
[server]
|
||||||
|
address = "127.0.0.1:8000"
|
||||||
|
max_content_length = "10MB"
|
||||||
|
upload_path = "./upload"
|
||||||
|
|
||||||
|
[paste]
|
||||||
|
default_extension = "txt"
|
||||||
|
duplicate_files = false
|
|
@ -0,0 +1,20 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
content="test data"
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
echo "$content" > file
|
||||||
|
}
|
||||||
|
|
||||||
|
run_test() {
|
||||||
|
file_url=$(curl -s -F "file=@file" localhost:8000)
|
||||||
|
test "$file_url" = "http://localhost:8000/file.txt"
|
||||||
|
|
||||||
|
result=$(curl -s --write-out "%{http_code}" -X DELETE http://localhost:8000/file.txt)
|
||||||
|
test "404" = "$result"
|
||||||
|
}
|
||||||
|
|
||||||
|
teardown() {
|
||||||
|
rm file
|
||||||
|
rm -r upload
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
[server]
|
||||||
|
address = "127.0.0.1:8000"
|
||||||
|
max_content_length = "10MB"
|
||||||
|
upload_path = "./upload"
|
||||||
|
expose_version = false
|
||||||
|
|
||||||
|
[paste]
|
||||||
|
random_url = { type = "petname", words = 2, separator = "-" }
|
||||||
|
default_extension = "txt"
|
||||||
|
duplicate_files = true
|
|
@ -0,0 +1,15 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
echo "$content" > file
|
||||||
|
}
|
||||||
|
|
||||||
|
run_test() {
|
||||||
|
result=$(curl -s --write-out "%{http_code}" http://localhost:8000/version)
|
||||||
|
test "404" = "$result"
|
||||||
|
}
|
||||||
|
|
||||||
|
teardown() {
|
||||||
|
rm file
|
||||||
|
rm -r upload
|
||||||
|
}
|
188
src/auth.rs
188
src/auth.rs
|
@ -1,55 +1,163 @@
|
||||||
use actix_web::http::header::{HeaderMap, AUTHORIZATION};
|
use crate::config::{Config, TokenType};
|
||||||
use actix_web::{error, Error};
|
use actix_web::dev::{ServiceRequest, ServiceResponse};
|
||||||
|
use actix_web::http::header::AUTHORIZATION;
|
||||||
|
use actix_web::http::Method;
|
||||||
|
use actix_web::middleware::ErrorHandlerResponse;
|
||||||
|
use actix_web::{error, web, Error};
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::sync::RwLock;
|
||||||
|
|
||||||
/// Checks the authorization header for the specified token.
|
/// Extracts the tokens from the authorization header by token type.
|
||||||
///
|
///
|
||||||
/// `Authorization: (type) <token>`
|
/// `Authorization: (type) <token>`
|
||||||
pub fn check(host: &str, headers: &HeaderMap, tokens: Option<Vec<String>>) -> Result<(), Error> {
|
pub(crate) async fn extract_tokens(req: &ServiceRequest) -> Result<HashSet<TokenType>, Error> {
|
||||||
if let Some(tokens) = tokens {
|
let config = req
|
||||||
let auth_header = headers
|
.app_data::<web::Data<RwLock<Config>>>()
|
||||||
.get(AUTHORIZATION)
|
.map(|cfg| cfg.read())
|
||||||
.map(|v| v.to_str().unwrap_or_default())
|
.and_then(Result::ok)
|
||||||
.map(|v| v.split_whitespace().last().unwrap_or_default());
|
.ok_or_else(|| error::ErrorInternalServerError("cannot acquire config"))?;
|
||||||
if !tokens.iter().any(|v| v == auth_header.unwrap_or_default()) {
|
|
||||||
#[cfg(debug_assertions)]
|
let mut user_tokens = HashSet::with_capacity(2);
|
||||||
warn!(
|
|
||||||
"authorization failure for {host} (token: {})",
|
let auth_header = req
|
||||||
auth_header.unwrap_or("none"),
|
.headers()
|
||||||
);
|
.get(AUTHORIZATION)
|
||||||
#[cfg(not(debug_assertions))]
|
.map(|v| v.to_str().unwrap_or_default())
|
||||||
warn!("authorization failure for {host}");
|
.map(|v| v.split_whitespace().last().unwrap_or_default());
|
||||||
return Err(error::ErrorUnauthorized("unauthorized\n"));
|
|
||||||
|
for token_type in [TokenType::Auth, TokenType::Delete] {
|
||||||
|
let maybe_tokens = config.get_tokens(token_type);
|
||||||
|
if let Some(configured_tokens) = maybe_tokens {
|
||||||
|
if configured_tokens.contains(auth_header.unwrap_or_default()) {
|
||||||
|
user_tokens.insert(token_type);
|
||||||
|
}
|
||||||
|
} else if token_type == TokenType::Auth {
|
||||||
|
// not configured `auth_tokens` means that the user is allowed to access the endpoints
|
||||||
|
warn!("auth_tokens not configured, allowing the request without auth header");
|
||||||
|
user_tokens.insert(token_type);
|
||||||
|
} else if token_type == TokenType::Delete && req.method() == Method::DELETE {
|
||||||
|
// explicitly disable `DELETE` methods if no `delete_tokens` are set
|
||||||
|
warn!("delete endpoints is not served because there are no delete_tokens set");
|
||||||
|
Err(error::ErrorNotFound(""))?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
|
||||||
|
Ok(user_tokens)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `HttpResponse` with unauthorized (`401`) error and `unauthorized\n` as body.
|
||||||
|
pub(crate) fn unauthorized_error() -> actix_web::HttpResponse {
|
||||||
|
error::ErrorUnauthorized("unauthorized\n").into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Log all unauthorized requests.
|
||||||
|
pub(crate) fn handle_unauthorized_error<B>(
|
||||||
|
res: ServiceResponse<B>,
|
||||||
|
) -> actix_web::Result<ErrorHandlerResponse<B>> {
|
||||||
|
let connection = res.request().connection_info().clone();
|
||||||
|
let host = connection.realip_remote_addr().unwrap_or("unknown host");
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
{
|
||||||
|
let auth_header = res
|
||||||
|
.request()
|
||||||
|
.headers()
|
||||||
|
.get(AUTHORIZATION)
|
||||||
|
.and_then(|v| v.to_str().ok())
|
||||||
|
.unwrap_or("none");
|
||||||
|
|
||||||
|
warn!("authorization failure for {host} (token: {auth_header})",);
|
||||||
|
}
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
warn!("authorization failure for {host}");
|
||||||
|
|
||||||
|
Ok(ErrorHandlerResponse::Response(res.map_into_left_body()))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use actix_web::http::header::HeaderValue;
|
use actix_web::http::header::HeaderValue;
|
||||||
|
use actix_web::test::TestRequest;
|
||||||
|
use actix_web::web::Data;
|
||||||
|
use actix_web::HttpResponse;
|
||||||
|
use awc::http::StatusCode;
|
||||||
|
|
||||||
|
#[actix_web::test]
|
||||||
|
async fn test_extract_tokens() -> Result<(), Error> {
|
||||||
|
let mut config = Config::default();
|
||||||
|
|
||||||
|
// request without configured auth-tokens
|
||||||
|
let request = TestRequest::default()
|
||||||
|
.app_data(Data::new(RwLock::new(config.clone())))
|
||||||
|
.insert_header((AUTHORIZATION, HeaderValue::from_static("basic test_token")))
|
||||||
|
.to_srv_request();
|
||||||
|
let tokens = extract_tokens(&request).await?;
|
||||||
|
assert_eq!(HashSet::from([TokenType::Auth]), tokens);
|
||||||
|
|
||||||
|
// request with configured auth-tokens
|
||||||
|
config.server.auth_tokens = Some(["test_token".to_string()].into());
|
||||||
|
let request = TestRequest::default()
|
||||||
|
.app_data(Data::new(RwLock::new(config.clone())))
|
||||||
|
.insert_header((AUTHORIZATION, HeaderValue::from_static("basic test_token")))
|
||||||
|
.to_srv_request();
|
||||||
|
let tokens = extract_tokens(&request).await?;
|
||||||
|
assert_eq!(HashSet::from([TokenType::Auth]), tokens);
|
||||||
|
|
||||||
|
// request with configured auth-tokens but wrong token in request
|
||||||
|
config.server.auth_tokens = Some(["test_token".to_string()].into());
|
||||||
|
let request = TestRequest::default()
|
||||||
|
.app_data(Data::new(RwLock::new(config.clone())))
|
||||||
|
.insert_header((
|
||||||
|
AUTHORIZATION,
|
||||||
|
HeaderValue::from_static("basic invalid_token"),
|
||||||
|
))
|
||||||
|
.to_srv_request();
|
||||||
|
let tokens = extract_tokens(&request).await?;
|
||||||
|
assert_eq!(HashSet::new(), tokens);
|
||||||
|
|
||||||
|
// DELETE request without configured delete-tokens
|
||||||
|
let request = TestRequest::default()
|
||||||
|
.method(Method::DELETE)
|
||||||
|
.app_data(Data::new(RwLock::new(config.clone())))
|
||||||
|
.insert_header((AUTHORIZATION, HeaderValue::from_static("basic test_token")))
|
||||||
|
.to_srv_request();
|
||||||
|
let res = extract_tokens(&request).await;
|
||||||
|
assert!(res.is_err());
|
||||||
|
assert_eq!(
|
||||||
|
Some(StatusCode::NOT_FOUND),
|
||||||
|
res.err()
|
||||||
|
.as_ref()
|
||||||
|
.map(Error::error_response)
|
||||||
|
.as_ref()
|
||||||
|
.map(HttpResponse::status)
|
||||||
|
);
|
||||||
|
|
||||||
|
// DELETE request with configured delete-tokens
|
||||||
|
config.server.delete_tokens = Some(["delete_token".to_string()].into());
|
||||||
|
let request = TestRequest::default()
|
||||||
|
.method(Method::DELETE)
|
||||||
|
.app_data(Data::new(RwLock::new(config.clone())))
|
||||||
|
.insert_header((
|
||||||
|
AUTHORIZATION,
|
||||||
|
HeaderValue::from_static("basic delete_token"),
|
||||||
|
))
|
||||||
|
.to_srv_request();
|
||||||
|
let tokens = extract_tokens(&request).await?;
|
||||||
|
assert_eq!(HashSet::from([TokenType::Delete]), tokens);
|
||||||
|
|
||||||
|
// DELETE request with configured delete-tokens but wrong token in request
|
||||||
|
let request = TestRequest::default()
|
||||||
|
.method(Method::DELETE)
|
||||||
|
.app_data(Data::new(RwLock::new(config.clone())))
|
||||||
|
.insert_header((
|
||||||
|
AUTHORIZATION,
|
||||||
|
HeaderValue::from_static("basic invalid_token"),
|
||||||
|
))
|
||||||
|
.to_srv_request();
|
||||||
|
let tokens = extract_tokens(&request).await?;
|
||||||
|
assert_eq!(HashSet::new(), tokens);
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_check_auth() -> Result<(), Error> {
|
|
||||||
let mut headers = HeaderMap::new();
|
|
||||||
headers.insert(AUTHORIZATION, HeaderValue::from_static("basic test_token"));
|
|
||||||
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(vec!["token".to_string()])).is_err());
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ use crate::random::RandomURLConfig;
|
||||||
use crate::{AUTH_TOKEN_ENV, DELETE_TOKEN_ENV};
|
use crate::{AUTH_TOKEN_ENV, DELETE_TOKEN_ENV};
|
||||||
use byte_unit::Byte;
|
use byte_unit::Byte;
|
||||||
use config::{self, ConfigError};
|
use config::{self, ConfigError};
|
||||||
|
use std::collections::HashSet;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
@ -49,7 +50,7 @@ pub struct ServerConfig {
|
||||||
#[deprecated(note = "use [server].auth_tokens instead")]
|
#[deprecated(note = "use [server].auth_tokens instead")]
|
||||||
pub auth_token: Option<String>,
|
pub auth_token: Option<String>,
|
||||||
/// Authentication tokens.
|
/// Authentication tokens.
|
||||||
pub auth_tokens: Option<Vec<String>>,
|
pub auth_tokens: Option<HashSet<String>>,
|
||||||
/// Expose version.
|
/// Expose version.
|
||||||
pub expose_version: Option<bool>,
|
pub expose_version: Option<bool>,
|
||||||
/// Landing page text.
|
/// Landing page text.
|
||||||
|
@ -63,7 +64,7 @@ pub struct ServerConfig {
|
||||||
/// Path of the JSON index.
|
/// Path of the JSON index.
|
||||||
pub expose_list: Option<bool>,
|
pub expose_list: Option<bool>,
|
||||||
/// Authentication tokens for deleting.
|
/// Authentication tokens for deleting.
|
||||||
pub delete_tokens: Option<Vec<String>>,
|
pub delete_tokens: Option<HashSet<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Enum representing different strategies for handling spaces in filenames.
|
/// Enum representing different strategies for handling spaces in filenames.
|
||||||
|
@ -130,6 +131,7 @@ pub struct CleanupConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Type of access token.
|
/// Type of access token.
|
||||||
|
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
|
||||||
pub enum TokenType {
|
pub enum TokenType {
|
||||||
/// Token for authentication.
|
/// Token for authentication.
|
||||||
Auth,
|
Auth,
|
||||||
|
@ -148,29 +150,33 @@ impl Config {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieves all configured auth/delete tokens.
|
/// Retrieves all configured auth/delete tokens.
|
||||||
pub fn get_tokens(&self, token_type: TokenType) -> Option<Vec<String>> {
|
pub fn get_tokens(&self, token_type: TokenType) -> Option<HashSet<String>> {
|
||||||
let mut tokens = match token_type {
|
let mut tokens = match token_type {
|
||||||
TokenType::Auth => {
|
TokenType::Auth => {
|
||||||
let mut tokens = self.server.auth_tokens.clone().unwrap_or_default();
|
let mut tokens: HashSet<_> = self.server.auth_tokens.clone().unwrap_or_default();
|
||||||
|
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
if let Some(token) = &self.server.auth_token {
|
if let Some(token) = &self.server.auth_token {
|
||||||
tokens.insert(0, token.to_string());
|
tokens.insert(token.to_string());
|
||||||
}
|
}
|
||||||
if let Ok(env_token) = env::var(AUTH_TOKEN_ENV) {
|
if let Ok(env_token) = env::var(AUTH_TOKEN_ENV) {
|
||||||
tokens.insert(0, env_token);
|
tokens.insert(env_token);
|
||||||
}
|
}
|
||||||
tokens
|
tokens
|
||||||
}
|
}
|
||||||
TokenType::Delete => {
|
TokenType::Delete => {
|
||||||
let mut tokens = self.server.delete_tokens.clone().unwrap_or_default();
|
let mut tokens: HashSet<_> = self.server.delete_tokens.clone().unwrap_or_default();
|
||||||
|
|
||||||
if let Ok(env_token) = env::var(DELETE_TOKEN_ENV) {
|
if let Ok(env_token) = env::var(DELETE_TOKEN_ENV) {
|
||||||
tokens.insert(0, env_token);
|
tokens.insert(env_token);
|
||||||
}
|
}
|
||||||
tokens
|
tokens
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
tokens.retain(|v| !v.is_empty());
|
|
||||||
(!tokens.is_empty()).then_some(tokens)
|
// filter out blank tokens
|
||||||
|
tokens.retain(|v| !v.trim().is_empty());
|
||||||
|
Some(tokens).filter(|v| !v.is_empty())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Print deprecation warnings.
|
/// Print deprecation warnings.
|
||||||
|
@ -241,24 +247,33 @@ mod tests {
|
||||||
env::set_var("AUTH_TOKEN", "env_auth");
|
env::set_var("AUTH_TOKEN", "env_auth");
|
||||||
env::set_var("DELETE_TOKEN", "env_delete");
|
env::set_var("DELETE_TOKEN", "env_delete");
|
||||||
let mut config = Config::parse(&config_path)?;
|
let mut config = Config::parse(&config_path)?;
|
||||||
config.server.auth_tokens = Some(vec!["may_the_force_be_with_you".to_string()]);
|
// empty tokens will be filtered
|
||||||
config.server.delete_tokens = Some(vec!["i_am_your_father".to_string()]);
|
config.server.auth_tokens =
|
||||||
|
Some(["may_the_force_be_with_you".to_string(), "".to_string()].into());
|
||||||
|
config.server.delete_tokens = Some(["i_am_your_father".to_string(), "".to_string()].into());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Some(vec![
|
Some(HashSet::from([
|
||||||
"env_auth".to_string(),
|
"env_auth".to_string(),
|
||||||
"may_the_force_be_with_you".to_string()
|
"may_the_force_be_with_you".to_string()
|
||||||
]),
|
])),
|
||||||
config.get_tokens(TokenType::Auth)
|
config.get_tokens(TokenType::Auth)
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Some(vec![
|
Some(HashSet::from([
|
||||||
"env_delete".to_string(),
|
"env_delete".to_string(),
|
||||||
"i_am_your_father".to_string()
|
"i_am_your_father".to_string()
|
||||||
]),
|
])),
|
||||||
config.get_tokens(TokenType::Delete)
|
config.get_tokens(TokenType::Delete)
|
||||||
);
|
);
|
||||||
env::remove_var("AUTH_TOKEN");
|
env::remove_var("AUTH_TOKEN");
|
||||||
env::remove_var("DELETE_TOKEN");
|
env::remove_var("DELETE_TOKEN");
|
||||||
|
|
||||||
|
// `get_tokens` returns `None` if no tokens are configured
|
||||||
|
config.server.auth_tokens = Some([" ".to_string()].into());
|
||||||
|
config.server.delete_tokens = Some(HashSet::new());
|
||||||
|
assert_eq!(None, config.get_tokens(TokenType::Auth));
|
||||||
|
assert_eq!(None, config.get_tokens(TokenType::Delete));
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::auth;
|
use crate::auth::{extract_tokens, handle_unauthorized_error, unauthorized_error};
|
||||||
use crate::config::{Config, LandingPageConfig, TokenType};
|
use crate::config::{Config, LandingPageConfig, TokenType};
|
||||||
use crate::file::Directory;
|
use crate::file::Directory;
|
||||||
use crate::header::{self, ContentDisposition};
|
use crate::header::{self, ContentDisposition};
|
||||||
|
@ -7,7 +7,10 @@ use crate::paste::{Paste, PasteType};
|
||||||
use crate::util;
|
use crate::util;
|
||||||
use actix_files::NamedFile;
|
use actix_files::NamedFile;
|
||||||
use actix_multipart::Multipart;
|
use actix_multipart::Multipart;
|
||||||
|
use actix_web::http::StatusCode;
|
||||||
|
use actix_web::middleware::ErrorHandlers;
|
||||||
use actix_web::{delete, error, get, post, web, Error, HttpRequest, HttpResponse};
|
use actix_web::{delete, error, get, post, web, Error, HttpRequest, HttpResponse};
|
||||||
|
use actix_web_grants::GrantsMiddleware;
|
||||||
use awc::Client;
|
use awc::Client;
|
||||||
use byte_unit::{Byte, UnitType};
|
use byte_unit::{Byte, UnitType};
|
||||||
use futures_util::stream::StreamExt;
|
use futures_util::stream::StreamExt;
|
||||||
|
@ -148,8 +151,8 @@ async fn serve(
|
||||||
|
|
||||||
/// Remove a file from the upload directory.
|
/// Remove a file from the upload directory.
|
||||||
#[delete("/{file}")]
|
#[delete("/{file}")]
|
||||||
|
#[actix_web_grants::protect("TokenType::Delete", ty = TokenType, error = unauthorized_error)]
|
||||||
async fn delete(
|
async fn delete(
|
||||||
request: HttpRequest,
|
|
||||||
file: web::Path<String>,
|
file: web::Path<String>,
|
||||||
config: web::Data<RwLock<Config>>,
|
config: web::Data<RwLock<Config>>,
|
||||||
) -> Result<HttpResponse, Error> {
|
) -> Result<HttpResponse, Error> {
|
||||||
|
@ -158,14 +161,6 @@ async fn delete(
|
||||||
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?;
|
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?;
|
||||||
let path = config.server.upload_path.join(&*file);
|
let path = config.server.upload_path.join(&*file);
|
||||||
let path = util::glob_match_file(path)?;
|
let path = util::glob_match_file(path)?;
|
||||||
let connection = request.connection_info().clone();
|
|
||||||
let host = connection.realip_remote_addr().unwrap_or("unknown host");
|
|
||||||
let tokens = config.get_tokens(TokenType::Delete);
|
|
||||||
if tokens.is_none() {
|
|
||||||
warn!("delete endpoint is not served because there are no delete_tokens set");
|
|
||||||
return Err(error::ErrorForbidden("endpoint is not exposed\n"));
|
|
||||||
}
|
|
||||||
auth::check(host, request.headers(), tokens)?;
|
|
||||||
if !path.is_file() || !path.exists() {
|
if !path.is_file() || !path.exists() {
|
||||||
return Err(error::ErrorNotFound("file is not found or expired :(\n"));
|
return Err(error::ErrorNotFound("file is not found or expired :(\n"));
|
||||||
}
|
}
|
||||||
|
@ -181,27 +176,23 @@ async fn delete(
|
||||||
|
|
||||||
/// Expose version endpoint
|
/// Expose version endpoint
|
||||||
#[get("/version")]
|
#[get("/version")]
|
||||||
async fn version(
|
#[actix_web_grants::protect("TokenType::Auth", ty = TokenType, error = unauthorized_error)]
|
||||||
request: HttpRequest,
|
async fn version(config: web::Data<RwLock<Config>>) -> Result<HttpResponse, Error> {
|
||||||
config: web::Data<RwLock<Config>>,
|
|
||||||
) -> Result<HttpResponse, Error> {
|
|
||||||
let config = config
|
let config = config
|
||||||
.read()
|
.read()
|
||||||
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?;
|
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?;
|
||||||
let connection = request.connection_info().clone();
|
|
||||||
let host = connection.realip_remote_addr().unwrap_or("unknown host");
|
|
||||||
let tokens = config.get_tokens(TokenType::Auth);
|
|
||||||
auth::check(host, request.headers(), tokens)?;
|
|
||||||
if !config.server.expose_version.unwrap_or(false) {
|
if !config.server.expose_version.unwrap_or(false) {
|
||||||
warn!("server is not configured to expose version endpoint");
|
warn!("server is not configured to expose version endpoint");
|
||||||
Err(error::ErrorForbidden("endpoint is not exposed\n"))?;
|
Err(error::ErrorNotFound(""))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let version = env!("CARGO_PKG_VERSION");
|
let version = env!("CARGO_PKG_VERSION");
|
||||||
Ok(HttpResponse::Ok().body(version.to_owned() + "\n"))
|
Ok(HttpResponse::Ok().body(version.to_owned() + "\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handles file upload by processing `multipart/form-data`.
|
/// Handles file upload by processing `multipart/form-data`.
|
||||||
#[post("/")]
|
#[post("/")]
|
||||||
|
#[actix_web_grants::protect("TokenType::Auth", ty = TokenType, error = unauthorized_error)]
|
||||||
async fn upload(
|
async fn upload(
|
||||||
request: HttpRequest,
|
request: HttpRequest,
|
||||||
mut payload: Multipart,
|
mut payload: Multipart,
|
||||||
|
@ -210,13 +201,6 @@ async fn upload(
|
||||||
) -> Result<HttpResponse, Error> {
|
) -> Result<HttpResponse, Error> {
|
||||||
let connection = request.connection_info().clone();
|
let connection = request.connection_info().clone();
|
||||||
let host = connection.realip_remote_addr().unwrap_or("unknown host");
|
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(TokenType::Auth);
|
|
||||||
auth::check(host, request.headers(), tokens)?;
|
|
||||||
}
|
|
||||||
let server_url = match config
|
let server_url = match config
|
||||||
.read()
|
.read()
|
||||||
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?
|
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?
|
||||||
|
@ -340,21 +324,15 @@ pub struct ListItem {
|
||||||
|
|
||||||
/// Returns the list of files.
|
/// Returns the list of files.
|
||||||
#[get("/list")]
|
#[get("/list")]
|
||||||
async fn list(
|
#[actix_web_grants::protect("TokenType::Auth", ty = TokenType, error = unauthorized_error)]
|
||||||
request: HttpRequest,
|
async fn list(config: web::Data<RwLock<Config>>) -> Result<HttpResponse, Error> {
|
||||||
config: web::Data<RwLock<Config>>,
|
|
||||||
) -> Result<HttpResponse, Error> {
|
|
||||||
let config = config
|
let config = config
|
||||||
.read()
|
.read()
|
||||||
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?
|
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?
|
||||||
.clone();
|
.clone();
|
||||||
let connection = request.connection_info().clone();
|
|
||||||
let host = connection.realip_remote_addr().unwrap_or("unknown host");
|
|
||||||
let tokens = config.get_tokens(TokenType::Auth);
|
|
||||||
auth::check(host, request.headers(), tokens)?;
|
|
||||||
if !config.server.expose_list.unwrap_or(false) {
|
if !config.server.expose_list.unwrap_or(false) {
|
||||||
warn!("server is not configured to expose list endpoint");
|
warn!("server is not configured to expose list endpoint");
|
||||||
Err(error::ErrorForbidden("endpoint is not exposed\n"))?;
|
Err(error::ErrorNotFound(""))?;
|
||||||
}
|
}
|
||||||
let entries: Vec<ListItem> = fs::read_dir(config.server.upload_path)?
|
let entries: Vec<ListItem> = fs::read_dir(config.server.upload_path)?
|
||||||
.filter_map(|entry| {
|
.filter_map(|entry| {
|
||||||
|
@ -400,13 +378,20 @@ async fn list(
|
||||||
|
|
||||||
/// Configures the server routes.
|
/// Configures the server routes.
|
||||||
pub fn configure_routes(cfg: &mut web::ServiceConfig) {
|
pub fn configure_routes(cfg: &mut web::ServiceConfig) {
|
||||||
cfg.service(index)
|
cfg.service(
|
||||||
.service(version)
|
web::scope("")
|
||||||
.service(list)
|
.service(index)
|
||||||
.service(serve)
|
.service(version)
|
||||||
.service(upload)
|
.service(list)
|
||||||
.service(delete)
|
.service(serve)
|
||||||
.route("", web::head().to(HttpResponse::MethodNotAllowed));
|
.service(upload)
|
||||||
|
.service(delete)
|
||||||
|
.route("", web::head().to(HttpResponse::MethodNotAllowed))
|
||||||
|
.wrap(GrantsMiddleware::with_extractor(extract_tokens))
|
||||||
|
.wrap(
|
||||||
|
ErrorHandlers::new().handler(StatusCode::UNAUTHORIZED, handle_unauthorized_error),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -566,7 +551,7 @@ mod tests {
|
||||||
#[actix_web::test]
|
#[actix_web::test]
|
||||||
async fn test_version_without_auth() -> Result<(), Error> {
|
async fn test_version_without_auth() -> Result<(), Error> {
|
||||||
let mut config = Config::default();
|
let mut config = Config::default();
|
||||||
config.server.auth_tokens = Some(vec!["test".to_string()]);
|
config.server.auth_tokens = Some(["test".to_string()].into());
|
||||||
let app = test::init_service(
|
let app = test::init_service(
|
||||||
App::new()
|
App::new()
|
||||||
.app_data(Data::new(RwLock::new(config)))
|
.app_data(Data::new(RwLock::new(config)))
|
||||||
|
@ -600,8 +585,8 @@ mod tests {
|
||||||
.uri("/version")
|
.uri("/version")
|
||||||
.to_request();
|
.to_request();
|
||||||
let response = test::call_service(&app, request).await;
|
let response = test::call_service(&app, request).await;
|
||||||
assert_eq!(StatusCode::FORBIDDEN, response.status());
|
assert_eq!(StatusCode::NOT_FOUND, response.status());
|
||||||
assert_body(response.into_body(), "endpoint is not exposed\n").await?;
|
assert_body(response.into_body(), "").await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -721,7 +706,7 @@ mod tests {
|
||||||
#[actix_web::test]
|
#[actix_web::test]
|
||||||
async fn test_auth() -> Result<(), Error> {
|
async fn test_auth() -> Result<(), Error> {
|
||||||
let mut config = Config::default();
|
let mut config = Config::default();
|
||||||
config.server.auth_tokens = Some(vec!["test".to_string()]);
|
config.server.auth_tokens = Some(["test".to_string()].into());
|
||||||
|
|
||||||
let app = test::init_service(
|
let app = test::init_service(
|
||||||
App::new()
|
App::new()
|
||||||
|
@ -764,7 +749,7 @@ mod tests {
|
||||||
#[actix_web::test]
|
#[actix_web::test]
|
||||||
async fn test_delete_file() -> Result<(), Error> {
|
async fn test_delete_file() -> Result<(), Error> {
|
||||||
let mut config = Config::default();
|
let mut config = Config::default();
|
||||||
config.server.delete_tokens = Some(vec!["test".to_string()]);
|
config.server.delete_tokens = Some(["test".to_string()].into());
|
||||||
config.server.upload_path = env::current_dir()?;
|
config.server.upload_path = env::current_dir()?;
|
||||||
|
|
||||||
let app = test::init_service(
|
let app = test::init_service(
|
||||||
|
@ -818,8 +803,8 @@ mod tests {
|
||||||
.to_request();
|
.to_request();
|
||||||
let response = test::call_service(&app, request).await;
|
let response = test::call_service(&app, request).await;
|
||||||
|
|
||||||
assert_eq!(StatusCode::FORBIDDEN, response.status());
|
assert_eq!(StatusCode::NOT_FOUND, response.status());
|
||||||
assert_body(response.into_body(), "endpoint is not exposed\n").await?;
|
assert_body(response.into_body(), "").await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue