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",
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "addr2line"
|
||||
version = "0.21.0"
|
||||
|
@ -2014,6 +2024,18 @@ dependencies = [
|
|||
"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]]
|
||||
name = "quote"
|
||||
version = "1.0.33"
|
||||
|
@ -2226,6 +2248,7 @@ dependencies = [
|
|||
"actix-multipart",
|
||||
"actix-rt",
|
||||
"actix-web",
|
||||
"actix-web-grants",
|
||||
"awc",
|
||||
"byte-unit",
|
||||
"config",
|
||||
|
|
|
@ -20,6 +20,7 @@ shuttle = ["dep:shuttle-actix-web", "dep:shuttle-runtime", "dep:tokio"]
|
|||
|
||||
[dependencies]
|
||||
actix-web = { version = "4.4.0" }
|
||||
actix-web-grants = { version = "4.0.3" }
|
||||
actix-multipart = "0.6.1"
|
||||
actix-files = "0.6.2"
|
||||
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 actix_web::{error, Error};
|
||||
use crate::config::{Config, TokenType};
|
||||
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>`
|
||||
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()) {
|
||||
#[cfg(debug_assertions)]
|
||||
warn!(
|
||||
"authorization failure for {host} (token: {})",
|
||||
auth_header.unwrap_or("none"),
|
||||
);
|
||||
#[cfg(not(debug_assertions))]
|
||||
warn!("authorization failure for {host}");
|
||||
return Err(error::ErrorUnauthorized("unauthorized\n"));
|
||||
pub(crate) async fn extract_tokens(req: &ServiceRequest) -> Result<HashSet<TokenType>, Error> {
|
||||
let config = req
|
||||
.app_data::<web::Data<RwLock<Config>>>()
|
||||
.map(|cfg| cfg.read())
|
||||
.and_then(Result::ok)
|
||||
.ok_or_else(|| error::ErrorInternalServerError("cannot acquire config"))?;
|
||||
|
||||
let mut user_tokens = HashSet::with_capacity(2);
|
||||
|
||||
let auth_header = req
|
||||
.headers()
|
||||
.get(AUTHORIZATION)
|
||||
.map(|v| v.to_str().unwrap_or_default())
|
||||
.map(|v| v.split_whitespace().last().unwrap_or_default());
|
||||
|
||||
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)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ use crate::random::RandomURLConfig;
|
|||
use crate::{AUTH_TOKEN_ENV, DELETE_TOKEN_ENV};
|
||||
use byte_unit::Byte;
|
||||
use config::{self, ConfigError};
|
||||
use std::collections::HashSet;
|
||||
use std::env;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
|
@ -49,7 +50,7 @@ pub struct ServerConfig {
|
|||
#[deprecated(note = "use [server].auth_tokens instead")]
|
||||
pub auth_token: Option<String>,
|
||||
/// Authentication tokens.
|
||||
pub auth_tokens: Option<Vec<String>>,
|
||||
pub auth_tokens: Option<HashSet<String>>,
|
||||
/// Expose version.
|
||||
pub expose_version: Option<bool>,
|
||||
/// Landing page text.
|
||||
|
@ -63,7 +64,7 @@ pub struct ServerConfig {
|
|||
/// Path of the JSON index.
|
||||
pub expose_list: Option<bool>,
|
||||
/// 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.
|
||||
|
@ -130,6 +131,7 @@ pub struct CleanupConfig {
|
|||
}
|
||||
|
||||
/// Type of access token.
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
|
||||
pub enum TokenType {
|
||||
/// Token for authentication.
|
||||
Auth,
|
||||
|
@ -148,29 +150,33 @@ impl Config {
|
|||
}
|
||||
|
||||
/// 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 {
|
||||
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)]
|
||||
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) {
|
||||
tokens.insert(0, env_token);
|
||||
tokens.insert(env_token);
|
||||
}
|
||||
tokens
|
||||
}
|
||||
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) {
|
||||
tokens.insert(0, env_token);
|
||||
tokens.insert(env_token);
|
||||
}
|
||||
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.
|
||||
|
@ -241,24 +247,33 @@ mod tests {
|
|||
env::set_var("AUTH_TOKEN", "env_auth");
|
||||
env::set_var("DELETE_TOKEN", "env_delete");
|
||||
let mut config = Config::parse(&config_path)?;
|
||||
config.server.auth_tokens = Some(vec!["may_the_force_be_with_you".to_string()]);
|
||||
config.server.delete_tokens = Some(vec!["i_am_your_father".to_string()]);
|
||||
// empty tokens will be filtered
|
||||
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!(
|
||||
Some(vec![
|
||||
Some(HashSet::from([
|
||||
"env_auth".to_string(),
|
||||
"may_the_force_be_with_you".to_string()
|
||||
]),
|
||||
])),
|
||||
config.get_tokens(TokenType::Auth)
|
||||
);
|
||||
assert_eq!(
|
||||
Some(vec![
|
||||
Some(HashSet::from([
|
||||
"env_delete".to_string(),
|
||||
"i_am_your_father".to_string()
|
||||
]),
|
||||
])),
|
||||
config.get_tokens(TokenType::Delete)
|
||||
);
|
||||
env::remove_var("AUTH_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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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::file::Directory;
|
||||
use crate::header::{self, ContentDisposition};
|
||||
|
@ -7,7 +7,10 @@ use crate::paste::{Paste, PasteType};
|
|||
use crate::util;
|
||||
use actix_files::NamedFile;
|
||||
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_grants::GrantsMiddleware;
|
||||
use awc::Client;
|
||||
use byte_unit::{Byte, UnitType};
|
||||
use futures_util::stream::StreamExt;
|
||||
|
@ -148,8 +151,8 @@ async fn serve(
|
|||
|
||||
/// Remove a file from the upload directory.
|
||||
#[delete("/{file}")]
|
||||
#[actix_web_grants::protect("TokenType::Delete", ty = TokenType, error = unauthorized_error)]
|
||||
async fn delete(
|
||||
request: HttpRequest,
|
||||
file: web::Path<String>,
|
||||
config: web::Data<RwLock<Config>>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
|
@ -158,14 +161,6 @@ async fn delete(
|
|||
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?;
|
||||
let path = config.server.upload_path.join(&*file);
|
||||
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() {
|
||||
return Err(error::ErrorNotFound("file is not found or expired :(\n"));
|
||||
}
|
||||
|
@ -181,27 +176,23 @@ async fn delete(
|
|||
|
||||
/// Expose version endpoint
|
||||
#[get("/version")]
|
||||
async fn version(
|
||||
request: HttpRequest,
|
||||
config: web::Data<RwLock<Config>>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
#[actix_web_grants::protect("TokenType::Auth", ty = TokenType, error = unauthorized_error)]
|
||||
async fn version(config: web::Data<RwLock<Config>>) -> Result<HttpResponse, Error> {
|
||||
let config = config
|
||||
.read()
|
||||
.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) {
|
||||
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");
|
||||
Ok(HttpResponse::Ok().body(version.to_owned() + "\n"))
|
||||
}
|
||||
|
||||
/// Handles file upload by processing `multipart/form-data`.
|
||||
#[post("/")]
|
||||
#[actix_web_grants::protect("TokenType::Auth", ty = TokenType, error = unauthorized_error)]
|
||||
async fn upload(
|
||||
request: HttpRequest,
|
||||
mut payload: Multipart,
|
||||
|
@ -210,13 +201,6 @@ 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(TokenType::Auth);
|
||||
auth::check(host, request.headers(), tokens)?;
|
||||
}
|
||||
let server_url = match config
|
||||
.read()
|
||||
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?
|
||||
|
@ -340,21 +324,15 @@ pub struct ListItem {
|
|||
|
||||
/// Returns the list of files.
|
||||
#[get("/list")]
|
||||
async fn list(
|
||||
request: HttpRequest,
|
||||
config: web::Data<RwLock<Config>>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
#[actix_web_grants::protect("TokenType::Auth", ty = TokenType, error = unauthorized_error)]
|
||||
async fn list(config: web::Data<RwLock<Config>>) -> Result<HttpResponse, Error> {
|
||||
let config = config
|
||||
.read()
|
||||
.map_err(|_| error::ErrorInternalServerError("cannot acquire config"))?
|
||||
.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) {
|
||||
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)?
|
||||
.filter_map(|entry| {
|
||||
|
@ -400,13 +378,20 @@ async fn list(
|
|||
|
||||
/// Configures the server routes.
|
||||
pub fn configure_routes(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(index)
|
||||
.service(version)
|
||||
.service(list)
|
||||
.service(serve)
|
||||
.service(upload)
|
||||
.service(delete)
|
||||
.route("", web::head().to(HttpResponse::MethodNotAllowed));
|
||||
cfg.service(
|
||||
web::scope("")
|
||||
.service(index)
|
||||
.service(version)
|
||||
.service(list)
|
||||
.service(serve)
|
||||
.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)]
|
||||
|
@ -566,7 +551,7 @@ mod tests {
|
|||
#[actix_web::test]
|
||||
async fn test_version_without_auth() -> Result<(), Error> {
|
||||
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(
|
||||
App::new()
|
||||
.app_data(Data::new(RwLock::new(config)))
|
||||
|
@ -600,8 +585,8 @@ mod tests {
|
|||
.uri("/version")
|
||||
.to_request();
|
||||
let response = test::call_service(&app, request).await;
|
||||
assert_eq!(StatusCode::FORBIDDEN, response.status());
|
||||
assert_body(response.into_body(), "endpoint is not exposed\n").await?;
|
||||
assert_eq!(StatusCode::NOT_FOUND, response.status());
|
||||
assert_body(response.into_body(), "").await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -721,7 +706,7 @@ mod tests {
|
|||
#[actix_web::test]
|
||||
async fn test_auth() -> Result<(), Error> {
|
||||
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(
|
||||
App::new()
|
||||
|
@ -764,7 +749,7 @@ mod tests {
|
|||
#[actix_web::test]
|
||||
async fn test_delete_file() -> Result<(), Error> {
|
||||
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()?;
|
||||
|
||||
let app = test::init_service(
|
||||
|
@ -818,8 +803,8 @@ mod tests {
|
|||
.to_request();
|
||||
let response = test::call_service(&app, request).await;
|
||||
|
||||
assert_eq!(StatusCode::FORBIDDEN, response.status());
|
||||
assert_body(response.into_body(), "endpoint is not exposed\n").await?;
|
||||
assert_eq!(StatusCode::NOT_FOUND, response.status());
|
||||
assert_body(response.into_body(), "").await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue