784 lines
24 KiB
Rust
784 lines
24 KiB
Rust
/// Common utilities and types for the server module.
|
|
///
|
|
/// This module provides shared structures, functions, and middleware used across
|
|
/// different parts of the server implementation, including configuration, state
|
|
/// management, API responses, authentication, and logging.
|
|
///
|
|
/// # Usage
|
|
///
|
|
/// ```rust
|
|
/// use keep::modes::server::common::{ServerConfig, AppState};
|
|
/// let config = ServerConfig { address: "127.0.0.1".to_string(), ..Default::default() };
|
|
/// let state = AppState { /* ... */ };
|
|
/// ```
|
|
use anyhow::Result;
|
|
use axum::{
|
|
extract::{Request, ConnectInfo},
|
|
http::{HeaderMap, StatusCode},
|
|
middleware::Next,
|
|
response::Response,
|
|
};
|
|
use base64::Engine;
|
|
use log::{info, warn};
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::HashMap;
|
|
use std::net::SocketAddr;
|
|
use std::path::PathBuf;
|
|
use std::sync::Arc;
|
|
use std::time::Instant;
|
|
use tokio::sync::Mutex;
|
|
use utoipa::ToSchema;
|
|
use crate::services::item_service::ItemService;
|
|
|
|
/// Server configuration structure.
|
|
///
|
|
/// This struct holds the configuration parameters for the HTTP server, including
|
|
/// binding address, port, and authentication settings.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```
|
|
/// let config = ServerConfig {
|
|
/// address: "127.0.0.1".to_string(),
|
|
/// port: Some(8080),
|
|
/// password: None,
|
|
/// password_hash: None,
|
|
/// };
|
|
/// ```
|
|
#[derive(Debug, Clone)]
|
|
pub struct ServerConfig {
|
|
/// Server bind address.
|
|
///
|
|
/// The IP address or hostname the server should bind to. Defaults to "127.0.0.1"
|
|
/// for local-only access.
|
|
pub address: String,
|
|
/// Optional server port.
|
|
///
|
|
/// The TCP port number to listen on. If not specified, a default port (typically
|
|
/// 8080 or 21080) will be used.
|
|
pub port: Option<u16>,
|
|
/// Optional authentication password.
|
|
///
|
|
/// Plain text password for basic or bearer token authentication. This should be
|
|
/// used only for testing or low-security environments.
|
|
pub password: Option<String>,
|
|
/// Optional hashed authentication password.
|
|
///
|
|
/// Pre-hashed password (Unix crypt format) for secure authentication. Preferred
|
|
/// over plain text password for production use.
|
|
pub password_hash: Option<String>,
|
|
}
|
|
|
|
/// Application state shared across all routes.
|
|
///
|
|
/// This struct encapsulates the shared state that is accessible to all request handlers,
|
|
/// including database connections, file paths, services, and configuration.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```rust
|
|
/// use keep::modes::server::common::AppState;
|
|
/// use std::sync::Arc;
|
|
/// use tokio::sync::Mutex;
|
|
/// let state = AppState {
|
|
/// db: Arc::new(Mutex::new(conn)),
|
|
/// data_dir: PathBuf::from("/data"),
|
|
/// item_service: Arc::new(ItemService::new(data_dir.clone())),
|
|
/// cmd: Arc::new(Mutex::new(Command::new("keep"))),
|
|
/// settings: Arc::new(settings),
|
|
/// };
|
|
/// ```
|
|
#[derive(Clone)]
|
|
pub struct AppState {
|
|
/// Database connection wrapped in Arc<Mutex>.
|
|
///
|
|
/// A thread-safe reference to the SQLite database connection, protected by a mutex
|
|
/// for concurrent access safety.
|
|
pub db: Arc<Mutex<rusqlite::Connection>>,
|
|
/// Data directory path.
|
|
///
|
|
/// The root directory where item files are stored.
|
|
pub data_dir: PathBuf,
|
|
/// Item service instance.
|
|
///
|
|
/// Shared reference to the service handling item storage and retrieval operations.
|
|
pub item_service: Arc<ItemService>,
|
|
/// Command line argument parser.
|
|
///
|
|
/// Thread-safe reference to the Clap command builder for configuration access.
|
|
pub cmd: Arc<Mutex<clap::Command>>,
|
|
/// Application settings.
|
|
///
|
|
/// Shared reference to the application's configuration settings.
|
|
pub settings: Arc<crate::config::Settings>,
|
|
}
|
|
|
|
/// Standard API response wrapper containing success status, data payload, and error information.
|
|
///
|
|
/// This generic type is used for all API responses to provide a consistent structure across
|
|
/// different endpoints.
|
|
///
|
|
/// # Type Parameters
|
|
///
|
|
/// * `T` - The type of the data payload.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```rust
|
|
/// use keep::modes::server::common::ApiResponse;
|
|
/// let response: ApiResponse<Vec<ItemInfo>> = ApiResponse {
|
|
/// success: true,
|
|
/// data: Some(items),
|
|
/// error: None,
|
|
/// };
|
|
/// ```
|
|
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
|
#[schema(description = "Standard API response wrapper containing success status, data payload, and error information")]
|
|
pub struct ApiResponse<T> {
|
|
/// Success indicator.
|
|
///
|
|
/// Boolean flag indicating whether the request was successful.
|
|
pub success: bool,
|
|
/// Optional data payload.
|
|
///
|
|
/// The actual response data, present only if the request was successful.
|
|
pub data: Option<T>,
|
|
/// Optional error message.
|
|
///
|
|
/// Error description, present only if the request failed.
|
|
pub error: Option<String>,
|
|
}
|
|
|
|
/// Response type for list of item information.
|
|
///
|
|
/// Specialized response for endpoints that return multiple items.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```rust
|
|
/// use keep::modes::server::common::ItemInfoListResponse;
|
|
/// let response = ItemInfoListResponse {
|
|
/// success: true,
|
|
/// data: Some(vec![item_info]),
|
|
/// error: None,
|
|
/// };
|
|
/// ```
|
|
#[derive(Serialize, Deserialize, ToSchema)]
|
|
pub struct ItemInfoListResponse {
|
|
/// Success indicator.
|
|
///
|
|
/// Boolean flag indicating whether the request was successful.
|
|
pub success: bool,
|
|
/// Optional list of item information.
|
|
///
|
|
/// Vector of `ItemInfo` structures containing details about each item.
|
|
pub data: Option<Vec<ItemInfo>>,
|
|
/// Optional error message.
|
|
///
|
|
/// Error description if the request failed.
|
|
pub error: Option<String>,
|
|
}
|
|
|
|
/// Response type for single item information.
|
|
///
|
|
/// Specialized response for endpoints that return a single item's details.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```rust
|
|
/// use keep::modes::server::common::ItemInfoResponse;
|
|
/// let response = ItemInfoResponse {
|
|
/// success: true,
|
|
/// data: Some(item_info),
|
|
/// error: None,
|
|
/// };
|
|
/// ```
|
|
#[derive(Serialize, Deserialize, ToSchema)]
|
|
pub struct ItemInfoResponse {
|
|
/// Success indicator.
|
|
///
|
|
/// Boolean flag indicating whether the request was successful.
|
|
pub success: bool,
|
|
/// Optional item information.
|
|
///
|
|
/// The `ItemInfo` structure containing details about the item.
|
|
pub data: Option<ItemInfo>,
|
|
/// Optional error message.
|
|
///
|
|
/// Error description if the request failed.
|
|
pub error: Option<String>,
|
|
}
|
|
|
|
/// Response type for item content information.
|
|
///
|
|
/// Specialized response for endpoints that return item content and related metadata.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```rust
|
|
/// use keep::modes::server::common::ItemContentInfoResponse;
|
|
/// let response = ItemContentInfoResponse {
|
|
/// success: true,
|
|
/// data: Some(content_info),
|
|
/// error: None,
|
|
/// };
|
|
/// ```
|
|
#[derive(Serialize, Deserialize, ToSchema)]
|
|
pub struct ItemContentInfoResponse {
|
|
/// Success indicator.
|
|
///
|
|
/// Boolean flag indicating whether the request was successful.
|
|
pub success: bool,
|
|
/// Optional item content information.
|
|
///
|
|
/// The `ItemContentInfo` structure containing content and metadata.
|
|
pub data: Option<ItemContentInfo>,
|
|
/// Optional error message.
|
|
///
|
|
/// Error description if the request failed.
|
|
pub error: Option<String>,
|
|
}
|
|
|
|
/// Response type for metadata.
|
|
///
|
|
/// Specialized response for metadata-only endpoints.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```rust
|
|
/// use keep::modes::server::common::MetadataResponse;
|
|
/// let response = MetadataResponse {
|
|
/// success: true,
|
|
/// data: Some(meta_map),
|
|
/// error: None,
|
|
/// };
|
|
/// ```
|
|
#[derive(Serialize, Deserialize, ToSchema)]
|
|
pub struct MetadataResponse {
|
|
/// Success indicator.
|
|
///
|
|
/// Boolean flag indicating whether the request was successful.
|
|
pub success: bool,
|
|
/// Optional metadata hashmap.
|
|
///
|
|
/// HashMap containing key-value pairs of metadata.
|
|
pub data: Option<HashMap<String, String>>,
|
|
/// Optional error message.
|
|
///
|
|
/// Error description if the request failed.
|
|
pub error: Option<String>,
|
|
}
|
|
|
|
/// Response type for status information.
|
|
///
|
|
/// Specialized response for system status endpoints.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```rust
|
|
/// use keep::modes::server::common::StatusInfoResponse;
|
|
/// let response = StatusInfoResponse {
|
|
/// success: true,
|
|
/// data: Some(status_info),
|
|
/// error: None,
|
|
/// };
|
|
/// ```
|
|
#[derive(Serialize, Deserialize, ToSchema)]
|
|
pub struct StatusInfoResponse {
|
|
/// Success indicator.
|
|
///
|
|
/// Boolean flag indicating whether the request was successful.
|
|
pub success: bool,
|
|
/// Optional status information.
|
|
///
|
|
/// The `StatusInfo` structure containing system status details.
|
|
pub data: Option<crate::common::status::StatusInfo>,
|
|
/// Optional error message.
|
|
///
|
|
/// Error description if the request failed.
|
|
pub error: Option<String>,
|
|
}
|
|
|
|
/// Complete information about a stored item including metadata and tags.
|
|
///
|
|
/// This structure represents the full details of an item, combining basic item
|
|
/// properties with associated tags and metadata.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```rust
|
|
/// use keep::modes::server::common::ItemInfo;
|
|
/// use std::collections::HashMap;
|
|
/// let item_info = ItemInfo {
|
|
/// id: 42,
|
|
/// ts: "2023-12-01T15:30:45Z".to_string(),
|
|
/// size: Some(1024),
|
|
/// compression: "gzip".to_string(),
|
|
/// tags: vec!["important".to_string()],
|
|
/// metadata: HashMap::from([("mime_type".to_string(), "text/plain".to_string())]),
|
|
/// };
|
|
/// ```
|
|
#[derive(Serialize, Deserialize, ToSchema)]
|
|
#[schema(description = "Complete information about a stored item including metadata and tags")]
|
|
pub struct ItemInfo {
|
|
/// Item ID.
|
|
///
|
|
/// The unique identifier for the item in the database.
|
|
#[schema(example = 42)]
|
|
pub id: i64,
|
|
/// Timestamp.
|
|
///
|
|
/// The creation timestamp of the item in ISO 8601 format.
|
|
#[schema(example = "2023-12-01T15:30:45Z")]
|
|
pub ts: String,
|
|
/// Size in bytes.
|
|
///
|
|
/// The size of the item's content in bytes, may be None if not set.
|
|
#[schema(example = 1024)]
|
|
pub size: Option<i64>,
|
|
/// Compression type.
|
|
///
|
|
/// The compression algorithm used for the item's content.
|
|
#[schema(example = "gzip")]
|
|
pub compression: String,
|
|
/// List of tags.
|
|
///
|
|
/// Vector of strings representing the tags associated with the item.
|
|
#[schema(example = json!(["important", "work", "document"]))]
|
|
pub tags: Vec<String>,
|
|
/// Metadata hashmap.
|
|
///
|
|
/// Key-value pairs containing additional metadata about the item.
|
|
#[schema(example = json!({"mime_type": "text/plain", "mime_encoding": "utf-8", "line_count": "42"}))]
|
|
pub metadata: HashMap<String, String>,
|
|
}
|
|
|
|
/// Item information including content and metadata, with binary detection.
|
|
///
|
|
/// This structure provides item details along with its content, handling binary
|
|
/// content detection and safe string representation.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```rust
|
|
/// use keep::modes::server::common::ItemContentInfo;
|
|
/// use std::collections::HashMap;
|
|
/// let content_info = ItemContentInfo {
|
|
/// metadata: HashMap::from([("mime_type".to_string(), "text/plain".to_string())]),
|
|
/// content: Some("Hello, world!".to_string()),
|
|
/// binary: false,
|
|
/// };
|
|
/// ```
|
|
#[derive(Serialize, Deserialize, ToSchema)]
|
|
#[schema(description = "Item information including content and metadata, with binary detection")]
|
|
pub struct ItemContentInfo {
|
|
/// Metadata hashmap.
|
|
///
|
|
/// Key-value pairs of metadata, flattened into the structure.
|
|
#[serde(flatten)]
|
|
#[schema(example = json!({"mime_type": "text/plain", "mime_encoding": "utf-8", "line_count": "42"}))]
|
|
pub metadata: HashMap<String, String>,
|
|
/// Optional content as string.
|
|
///
|
|
/// The item's content as a string, only present if the content is text.
|
|
#[schema(example = "Hello, world!\nThis is the content of the file.")]
|
|
pub content: Option<String>,
|
|
/// Binary content indicator.
|
|
///
|
|
/// Boolean flag indicating whether the content is binary (true) or text (false).
|
|
#[schema(example = false)]
|
|
pub binary: bool,
|
|
}
|
|
|
|
/// Query parameters for tags.
|
|
///
|
|
/// Structure for handling tag-based query parameters in API requests.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```rust
|
|
/// use keep::modes::server::common::TagsQuery;
|
|
/// let query = TagsQuery { tags: Some("tag1,tag2".to_string()) };
|
|
/// ```
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct TagsQuery {
|
|
/// Optional comma-separated tags.
|
|
///
|
|
/// String containing comma-separated tag names for filtering.
|
|
pub tags: Option<String>,
|
|
}
|
|
|
|
/// Query parameters for listing items.
|
|
///
|
|
/// Structure for pagination and sorting parameters in item listing endpoints.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```rust
|
|
/// use keep::modes::server::common::ListItemsQuery;
|
|
/// let query = ListItemsQuery {
|
|
/// tags: Some("important".to_string()),
|
|
/// order: Some("newest".to_string()),
|
|
/// start: Some(0),
|
|
/// count: Some(10),
|
|
/// };
|
|
/// ```
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct ListItemsQuery {
|
|
/// Optional comma-separated tags for filtering.
|
|
///
|
|
/// String containing tags to filter the item list.
|
|
pub tags: Option<String>,
|
|
/// Optional sort order.
|
|
///
|
|
/// String specifying sort direction: "newest" or "oldest".
|
|
pub order: Option<String>,
|
|
/// Optional pagination start index.
|
|
///
|
|
/// Unsigned integer indicating the starting index for pagination.
|
|
pub start: Option<u32>,
|
|
/// Optional number of items to return.
|
|
///
|
|
/// Unsigned integer limiting the number of items returned.
|
|
pub count: Option<u32>,
|
|
}
|
|
|
|
/// Query parameters for item retrieval.
|
|
///
|
|
/// Structure for content retrieval parameters, including binary handling and streaming options.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```rust
|
|
/// use keep::modes::server::common::ItemQuery;
|
|
/// let query = ItemQuery {
|
|
/// allow_binary: true,
|
|
/// offset: 0,
|
|
/// length: 1024,
|
|
/// stream: false,
|
|
/// as_meta: false,
|
|
/// };
|
|
/// ```
|
|
#[derive(Debug, Deserialize, utoipa::ToSchema)]
|
|
pub struct ItemQuery {
|
|
/// Allow binary content (default: true).
|
|
///
|
|
/// Boolean flag to allow or deny binary content in responses.
|
|
#[serde(default = "default_allow_binary")]
|
|
pub allow_binary: bool,
|
|
/// Byte offset (default: 0).
|
|
///
|
|
/// Unsigned integer specifying the starting byte position for content retrieval.
|
|
#[serde(default)]
|
|
pub offset: u64,
|
|
/// Byte length (default: 0, meaning all).
|
|
///
|
|
/// Unsigned integer specifying the maximum number of bytes to retrieve (0 = all remaining).
|
|
#[serde(default)]
|
|
pub length: u64,
|
|
/// Stream response (default: false).
|
|
///
|
|
/// Boolean flag to enable streaming responses for large content.
|
|
#[serde(default = "default_stream")]
|
|
pub stream: bool,
|
|
/// Return as metadata JSON (default: false).
|
|
///
|
|
/// Boolean flag to return content and metadata in a structured JSON format.
|
|
#[serde(default = "default_as_meta")]
|
|
pub as_meta: bool,
|
|
}
|
|
|
|
/// Query parameters for item content retrieval.
|
|
///
|
|
/// Extended query parameters for content-specific operations, including tag filtering.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```rust
|
|
/// use keep::modes::server::common::ItemContentQuery;
|
|
/// let query = ItemContentQuery {
|
|
/// tags: Some("important".to_string()),
|
|
/// allow_binary: true,
|
|
/// offset: 0,
|
|
/// length: 1024,
|
|
/// stream: false,
|
|
/// as_meta: false,
|
|
/// };
|
|
/// ```
|
|
#[derive(Debug, Deserialize, utoipa::ToSchema)]
|
|
pub struct ItemContentQuery {
|
|
/// Optional comma-separated tags for filtering.
|
|
///
|
|
/// String containing tags to filter the item selection.
|
|
pub tags: Option<String>,
|
|
/// Allow binary content (default: true).
|
|
///
|
|
/// Boolean flag to allow or deny binary content in responses.
|
|
#[serde(default = "default_allow_binary")]
|
|
pub allow_binary: bool,
|
|
/// Byte offset (default: 0).
|
|
///
|
|
/// Unsigned integer specifying the starting byte position for content retrieval.
|
|
#[serde(default)]
|
|
pub offset: u64,
|
|
/// Byte length (default: 0, meaning all).
|
|
///
|
|
/// Unsigned integer specifying the maximum number of bytes to retrieve (0 = all remaining).
|
|
#[serde(default)]
|
|
pub length: u64,
|
|
/// Stream response (default: false).
|
|
///
|
|
/// Boolean flag to enable streaming responses for large content.
|
|
#[serde(default = "default_stream")]
|
|
pub stream: bool,
|
|
/// Return as metadata JSON (default: false).
|
|
///
|
|
/// Boolean flag to return content and metadata in a structured JSON format.
|
|
#[serde(default = "default_as_meta")]
|
|
pub as_meta: bool,
|
|
}
|
|
|
|
/// Default function for allow_binary parameter.
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// `true` as the default value for allowing binary content.
|
|
fn default_allow_binary() -> bool {
|
|
true
|
|
}
|
|
|
|
/// Default function for stream parameter.
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// `false` as the default value for streaming responses.
|
|
fn default_stream() -> bool {
|
|
false
|
|
}
|
|
|
|
/// Default function for as_meta parameter.
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// `false` as the default value for metadata JSON responses.
|
|
fn default_as_meta() -> bool {
|
|
false
|
|
}
|
|
|
|
/// Validates bearer authentication token.
|
|
///
|
|
/// This function checks if the provided authorization string is a valid Bearer token
|
|
/// matching the expected password or hash.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `auth_str` - The authorization string from the header.
|
|
/// * `expected_password` - The expected plain text password.
|
|
/// * `expected_hash` - Optional expected password hash.
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// * `true` - If authentication succeeds.
|
|
/// * `false` - Otherwise.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// None; returns false on failure.
|
|
fn check_bearer_auth(auth_str: &str, expected_password: &str, expected_hash: &Option<String>) -> bool {
|
|
if !auth_str.starts_with("Bearer ") {
|
|
return false;
|
|
}
|
|
|
|
let provided_password = &auth_str[7..];
|
|
|
|
// If we have a password hash, verify against it
|
|
if let Some(hash) = expected_hash {
|
|
return pwhash::unix::verify(provided_password, hash);
|
|
}
|
|
|
|
// Otherwise, do direct comparison
|
|
provided_password == expected_password
|
|
}
|
|
|
|
/// Validates basic authentication credentials.
|
|
///
|
|
/// This function decodes and validates Basic Auth credentials from the authorization
|
|
/// header against the expected password or hash.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `auth_str` - The authorization string from the header.
|
|
/// * `expected_password` - The expected plain text password.
|
|
/// * `expected_hash` - Optional expected password hash.
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// * `true` - If authentication succeeds.
|
|
/// * `false` - Otherwise.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns false on decode or validation failure.
|
|
fn check_basic_auth(auth_str: &str, expected_password: &str, expected_hash: &Option<String>) -> bool {
|
|
if !auth_str.starts_with("Basic ") {
|
|
return false;
|
|
}
|
|
|
|
let encoded = &auth_str[6..];
|
|
if let Ok(decoded_bytes) = base64::engine::general_purpose::STANDARD.decode(encoded) {
|
|
if let Ok(decoded_str) = String::from_utf8(decoded_bytes) {
|
|
if let Some(colon_pos) = decoded_str.find(':') {
|
|
let provided_password = &decoded_str[colon_pos + 1..];
|
|
|
|
// If we have a password hash, verify against it
|
|
if let Some(hash) = expected_hash {
|
|
return pwhash::unix::verify(provided_password, hash);
|
|
}
|
|
|
|
// Otherwise, do direct comparison
|
|
let expected_credentials = format!("keep:{}", expected_password);
|
|
return decoded_str == expected_credentials;
|
|
}
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
/// Checks authorization header for valid credentials.
|
|
///
|
|
/// This function inspects the HTTP Authorization header for valid Bearer or Basic
|
|
/// authentication credentials against the provided password or hash.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `headers` - HTTP headers from the request.
|
|
/// * `password` - Optional expected password.
|
|
/// * `password_hash` - Optional expected password hash.
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// * `true` - If authorized (or no auth required).
|
|
/// * `false` - If unauthorized.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```
|
|
/// if check_auth(&headers, &Some("pass".to_string()), &None) {
|
|
/// // Proceed
|
|
/// }
|
|
/// ```
|
|
pub fn check_auth(headers: &HeaderMap, password: &Option<String>, password_hash: &Option<String>) -> bool {
|
|
// If neither password nor hash is set, no authentication required
|
|
if password.is_none() && password_hash.is_none() {
|
|
return true;
|
|
}
|
|
|
|
if let Some(auth_header) = headers.get("authorization") {
|
|
if let Ok(auth_str) = auth_header.to_str() {
|
|
return check_bearer_auth(auth_str, password.as_deref().unwrap_or(""), password_hash) ||
|
|
check_basic_auth(auth_str, password.as_deref().unwrap_or(""), password_hash);
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
/// Middleware for logging requests and responses.
|
|
///
|
|
/// This middleware logs incoming requests and outgoing responses, including method,
|
|
/// URI, status code, response size, duration, and Accept header.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `ConnectInfo(addr)` - Connection info with client address.
|
|
/// * `request` - Incoming request.
|
|
/// * `next` - Next middleware.
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// The processed response with logging.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// Used in Axum router: `.layer_handler(logging_middleware)`.
|
|
pub async fn logging_middleware(
|
|
ConnectInfo(addr): ConnectInfo<SocketAddr>,
|
|
request: Request,
|
|
next: Next,
|
|
) -> Response {
|
|
let method = request.method().clone();
|
|
let uri = request.uri().clone();
|
|
|
|
// Log the Accept header - extract before moving the request
|
|
let accept_header = request.headers()
|
|
.get("accept")
|
|
.and_then(|v| v.to_str().ok())
|
|
.unwrap_or("-")
|
|
.to_string();
|
|
|
|
let start = Instant::now();
|
|
let response = next.run(request).await;
|
|
let duration = start.elapsed();
|
|
|
|
// Try to get response body size from content-length header, or default to 0
|
|
let response_content_length = response.headers()
|
|
.get("content-length")
|
|
.and_then(|v| v.to_str().ok())
|
|
.and_then(|s| s.parse::<u64>().ok())
|
|
.unwrap_or(0);
|
|
|
|
info!("{} {} {} {} {} bytes - {:?} - Accept: {}",
|
|
addr, method, uri, response.status(), response_content_length, duration, accept_header);
|
|
|
|
response
|
|
}
|
|
|
|
/// Creates authentication middleware for the application.
|
|
///
|
|
/// This function returns a middleware that enforces authentication on protected routes
|
|
/// using Bearer token or Basic Auth, challenging unauthorized requests with appropriate
|
|
/// headers.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `password` - Optional plain text password.
|
|
/// * `password_hash` - Optional hashed password.
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// A clonable async middleware function for Axum.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```
|
|
/// let auth_middleware = create_auth_middleware(Some("pass".to_string()), None);
|
|
/// router.layer(auth_middleware);
|
|
/// ```
|
|
pub fn create_auth_middleware(
|
|
password: Option<String>,
|
|
password_hash: Option<String>,
|
|
) -> impl Fn(ConnectInfo<SocketAddr>, Request, Next) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Response, StatusCode>> + Send>> + Clone + Send {
|
|
move |ConnectInfo(addr): ConnectInfo<SocketAddr>, request: Request, next: Next| {
|
|
let password = password.clone();
|
|
let password_hash = password_hash.clone();
|
|
Box::pin(async move {
|
|
let headers = request.headers().clone();
|
|
let uri = request.uri().clone();
|
|
|
|
if !check_auth(&headers, &password, &password_hash) {
|
|
warn!("Unauthorized request to {} from {}", uri, addr);
|
|
// Add WWW-Authenticate header to trigger basic auth in browsers
|
|
let mut response = Response::new(axum::body::Body::from("Unauthorized"));
|
|
*response.status_mut() = StatusCode::UNAUTHORIZED;
|
|
response.headers_mut().insert(
|
|
"www-authenticate",
|
|
"Basic realm=\"Keep Server\", charset=\"UTF-8\"".parse().unwrap(),
|
|
);
|
|
return Ok(response);
|
|
}
|
|
|
|
let response = next.run(request).await;
|
|
Ok(response)
|
|
})
|
|
}
|
|
}
|