/// 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, /// 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, /// 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, } /// 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. /// /// A thread-safe reference to the SQLite database connection, protected by a mutex /// for concurrent access safety. pub db: Arc>, /// 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, /// Command line argument parser. /// /// Thread-safe reference to the Clap command builder for configuration access. pub cmd: Arc>, /// Application settings. /// /// Shared reference to the application's configuration settings. pub settings: Arc, } /// 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> = 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 { /// 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, /// Optional error message. /// /// Error description, present only if the request failed. pub error: Option, } /// 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>, /// Optional error message. /// /// Error description if the request failed. pub error: Option, } /// 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, /// Optional error message. /// /// Error description if the request failed. pub error: Option, } /// 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, /// Optional error message. /// /// Error description if the request failed. pub error: Option, } /// 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>, /// Optional error message. /// /// Error description if the request failed. pub error: Option, } /// 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, /// Optional error message. /// /// Error description if the request failed. pub error: Option, } /// 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, /// 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, /// 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, } /// 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, /// 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, /// 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, } /// 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, /// Optional sort order. /// /// String specifying sort direction: "newest" or "oldest". pub order: Option, /// Optional pagination start index. /// /// Unsigned integer indicating the starting index for pagination. pub start: Option, /// Optional number of items to return. /// /// Unsigned integer limiting the number of items returned. pub count: Option, } /// 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, /// 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) -> 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) -> 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, password_hash: &Option) -> 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, 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::().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, password_hash: Option, ) -> impl Fn(ConnectInfo, Request, Next) -> std::pin::Pin> + Send>> + Clone + Send { move |ConnectInfo(addr): ConnectInfo, 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) }) } }