diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a193fd..28361b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- CLI args now feature-gated: `--server` and related options hidden when built without `server` feature; `--client-*` options hidden when built without `client` feature. Run `--help` only shows relevant options. +- `server` Cargo feature now includes TLS support by default (`axum-server`); `tls` feature removed +- Clap `conflicts_with_all` removed from all mode args — exclusivity now handled by implicit `group("mode")` - Filter plugins check size before loading content into memory (prevents OOM on large inputs) - Status page pre-allocates collections with known capacities (meta plugins, compression info) - `#[inline]` on HTML escape helper functions (`esc`, `esc_attr`) for hot path performance diff --git a/Cargo.toml b/Cargo.toml index 778b60d..8381419 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -96,8 +96,8 @@ default = [ "zstd" ] -# Server feature (includes axum and related dependencies) -server = ["dep:axum", "dep:tower", "dep:tower-http", "dep:utoipa", "dep:jsonwebtoken"] +# Server feature (includes axum and TLS/HTTPS via axum-server; rustls already available via client/ureq) +server = ["dep:axum", "dep:tower", "dep:tower-http", "dep:utoipa", "dep:jsonwebtoken", "dep:axum-server"] # Compression features gzip = ["flate2"] @@ -121,9 +121,6 @@ swagger = ["dep:utoipa-swagger-ui"] # Client feature (HTTP client for remote server) client = ["dep:ureq", "dep:os_pipe"] -# TLS feature (HTTPS server support) -tls = ["dep:axum-server"] - # Token counting feature (LLM token support via tiktoken) tokens = ["dep:tiktoken-rs"] diff --git a/src/args.rs b/src/args.rs index 2da294f..4ee8399 100644 --- a/src/args.rs +++ b/src/args.rs @@ -24,77 +24,80 @@ pub struct Args { /// Struct for mode-specific arguments, defining CLI flags for different operations. #[derive(Parser, Debug, Clone)] pub struct ModeArgs { - #[arg(group("mode"), help_heading("Mode Options"), short, long, conflicts_with_all(["get", "diff", "list", "delete", "info", "update", "status", "export", "import"]))] + #[arg(group("mode"), help_heading("Mode Options"), short, long)] #[arg(help("Save an item using any tags or metadata provided"))] pub save: bool, - #[arg(group("mode"), help_heading("Mode Options"), short, long, conflicts_with_all(["save", "diff", "list", "delete", "info", "update", "status", "export", "import"]))] + #[arg(group("mode"), help_heading("Mode Options"), short, long)] #[arg(help("Get an item either by its ID or by a combination of matching tags and metadata"))] pub get: bool, - #[arg(group("mode"), help_heading("Mode Options"), long, conflicts_with_all(["save", "get", "list", "delete", "info", "update", "status", "export", "import"]))] + #[arg(group("mode"), help_heading("Mode Options"), long)] #[arg(help("Show a diff between two items by ID"))] pub diff: bool, - #[arg(group("mode"), help_heading("Mode Options"), short, long, conflicts_with_all(["save", "get", "diff", "delete", "info", "update", "status", "export", "import"]))] + #[arg(group("mode"), help_heading("Mode Options"), short, long)] #[arg(help("List items, filtering on tags or metadata if given"))] pub list: bool, - #[arg(group("mode"), help_heading("Mode Options"), short, long, conflicts_with_all(["save", "get", "diff", "list", "info", "update", "status", "export", "import"]))] + #[arg(group("mode"), help_heading("Mode Options"), short, long)] #[arg(help("Delete items either by ID or by matching tags"))] #[arg(requires = "ids_or_tags")] pub delete: bool, - #[arg(group("mode"), help_heading("Mode Options"), short, long, conflicts_with_all(["save", "get", "diff", "list", "delete", "update", "status", "export", "import"]))] + #[arg(group("mode"), help_heading("Mode Options"), short, long)] #[arg(help("Get an item either by its ID or by a combination of matching tags and metadata"))] pub info: bool, - #[arg(group("mode"), help_heading("Mode Options"), short('u'), long, conflicts_with_all(["save", "get", "diff", "list", "delete", "info", "status", "export", "import"]))] + #[arg(group("mode"), help_heading("Mode Options"), short('u'), long)] #[arg(help("Update an item's tags and metadata by ID"))] pub update: bool, - #[arg(group("mode"), help_heading("Mode Options"), short('S'), long, conflicts_with_all(["save", "get", "diff", "list", "delete", "info", "update", "server", "status_plugins", "export", "import"]))] + #[arg(group("mode"), help_heading("Mode Options"), short('S'), long)] #[arg(help("Show status of directories and supported compression algorithms"))] pub status: bool, - #[arg(group("mode"), help_heading("Mode Options"), long, conflicts_with_all(["save", "get", "diff", "list", "delete", "info", "update", "status", "server", "export", "import"]))] + #[arg(group("mode"), help_heading("Mode Options"), long)] #[arg(help("Show available plugins and their configurations"))] pub status_plugins: bool, - #[arg(group("mode"), help_heading("Mode Options"), long, conflicts_with_all(["save", "get", "diff", "list", "delete", "info", "update", "status", "import"]))] + #[arg(group("mode"), help_heading("Mode Options"), long)] #[arg(help("Export items to a .keep.tar archive (requires IDs or tags)"))] pub export: bool, - #[arg(group("mode"), help_heading("Mode Options"), long, value_name("FILE"), conflicts_with_all(["save", "get", "diff", "list", "delete", "info", "update", "status", "export"]))] + #[arg(group("mode"), help_heading("Mode Options"), long, value_name("FILE"))] #[arg(help("Import items from a .keep.tar archive or legacy .meta.yml file"))] pub import: Option, - #[arg(group("mode"), help_heading("Mode Options"), long, conflicts_with_all(["save", "get", "diff", "list", "delete", "info", "update", "status"]))] + #[cfg(feature = "server")] + #[arg(group("mode"), help_heading("Mode Options"), long)] #[arg(help("Start REST HTTP server"))] pub server: bool, - #[arg(group("mode"), help_heading("Mode Options"), long, conflicts_with_all(["save", "get", "diff", "list", "delete", "info", "update", "status", "server", "export", "import"]))] + #[arg(group("mode"), help_heading("Mode Options"), long)] #[arg(help("Generate default configuration and output to stdout"))] pub generate_config: bool, - #[arg(help_heading("Mode Options"), long, conflicts_with_all(["save", "get", "diff", "list", "delete", "info", "update", "status", "server", "generate_config", "export", "import"]))] + #[arg(help_heading("Mode Options"), long)] #[arg(help("Generate shell completion script"))] pub generate_completion: Option, + #[cfg(feature = "server")] #[arg(help_heading("Server Options"), long, env("KEEP_SERVER_ADDRESS"))] #[arg(help("Server address to bind to"))] pub server_address: Option, + #[cfg(feature = "server")] #[arg(help_heading("Server Options"), long, env("KEEP_SERVER_PORT"))] #[arg(help("Server port to bind to"))] pub server_port: Option, - #[cfg(feature = "tls")] + #[cfg(feature = "server")] #[arg(help_heading("Server Options"), long, env("KEEP_SERVER_CERT"))] #[arg(help("Path to TLS certificate file (PEM) for HTTPS"))] pub server_cert: Option, - #[cfg(feature = "tls")] + #[cfg(feature = "server")] #[arg(help_heading("Server Options"), long, env("KEEP_SERVER_KEY"))] #[arg(help("Path to TLS private key file (PEM) for HTTPS"))] pub server_key: Option, @@ -249,24 +252,29 @@ pub struct OptionsArgs { #[arg(help("Output format (only works with --info, --status, --list)"))] pub output_format: Option, + #[cfg(feature = "server")] #[arg(help_heading("Server Options"), long, env("KEEP_SERVER_PASSWORD"))] #[arg(help("Password for server authentication (requires --server)"))] pub server_password: Option, + #[cfg(feature = "server")] #[arg(help_heading("Server Options"), long, env("KEEP_SERVER_PASSWORD_HASH"))] #[arg(help("Password hash for server authentication (requires --server)"))] pub server_password_hash: Option, + #[cfg(feature = "server")] #[arg(help_heading("Server Options"), long, env("KEEP_SERVER_USERNAME"))] #[arg(help( "Username for server Basic authentication (requires --server, defaults to 'keep')" ))] pub server_username: Option, + #[cfg(feature = "server")] #[arg(help_heading("Server Options"), long, env("KEEP_SERVER_JWT_SECRET"))] #[arg(help("JWT secret for token-based authentication (requires --server)"))] pub server_jwt_secret: Option, + #[cfg(feature = "server")] #[arg( help_heading("Server Options"), long, @@ -275,6 +283,7 @@ pub struct OptionsArgs { #[arg(help("Path to file containing JWT secret (requires --server)"))] pub server_jwt_secret_file: Option, + #[cfg(feature = "server")] #[arg(help_heading("Server Options"), long, env("KEEP_SERVER_MAX_BODY_SIZE"))] #[arg(help("Maximum request body size in bytes (requires --server, default: unlimited)"))] pub server_max_body_size: Option, diff --git a/src/config.rs b/src/config.rs index 7d45f1a..2ca29c1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -301,42 +301,48 @@ impl Settings { config_builder = config_builder.set_override("force", true)?; } + #[cfg(feature = "server")] if let Some(server_password) = &args.options.server_password { config_builder = config_builder.set_override("server.password", server_password.as_str())?; } + #[cfg(feature = "server")] if let Some(server_password_hash) = &args.options.server_password_hash { config_builder = config_builder .set_override("server.password_hash", server_password_hash.as_str())?; } + #[cfg(feature = "server")] if let Some(server_username) = &args.options.server_username { config_builder = config_builder.set_override("server.username", server_username.as_str())?; } + #[cfg(feature = "server")] if let Some(server_address) = &args.mode.server_address { config_builder = config_builder.set_override("server.address", server_address.as_str())?; } + #[cfg(feature = "server")] if let Some(server_port) = args.mode.server_port { config_builder = config_builder.set_override("server.port", server_port)?; } - #[cfg(feature = "tls")] + #[cfg(feature = "server")] if let Some(server_cert) = &args.mode.server_cert { config_builder = config_builder .set_override("server.cert_file", server_cert.to_string_lossy().as_ref())?; } - #[cfg(feature = "tls")] + #[cfg(feature = "server")] if let Some(server_key) = &args.mode.server_key { config_builder = config_builder .set_override("server.key_file", server_key.to_string_lossy().as_ref())?; } + #[cfg(feature = "server")] if let Some(max_body_size) = args.options.server_max_body_size { config_builder = config_builder.set_override("server.max_body_size", max_body_size)?; } diff --git a/src/main.rs b/src/main.rs index 84742e1..dd87de6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -122,6 +122,7 @@ fn main() -> Result<(), Error> { Import, Status, StatusPlugins, + #[cfg(feature = "server")] Server, GenerateConfig, } @@ -150,9 +151,14 @@ fn main() -> Result<(), Error> { mode = KeepModes::Status; } else if args.mode.status_plugins { mode = KeepModes::StatusPlugins; - } else if args.mode.server { - mode = KeepModes::Server; - } else if args.mode.generate_config { + } + #[cfg(feature = "server")] + { + if args.mode.server { + mode = KeepModes::Server; + } + } + if args.mode.generate_config { mode = KeepModes::GenerateConfig; } @@ -188,6 +194,7 @@ fn main() -> Result<(), Error> { } // Validate server password usage + #[cfg(feature = "server")] if settings.server_password().is_some() && mode != KeepModes::Server { cmd.error( ErrorKind::InvalidValue, @@ -355,19 +362,8 @@ fn main() -> Result<(), Error> { KeepModes::StatusPlugins => { modes::status_plugins::mode_status_plugins(&mut cmd, &settings, data_path, db_path) } - KeepModes::Server => { - #[cfg(feature = "server")] - { - modes::server::mode_server(&mut cmd, &settings, &mut conn, data_path) - } - #[cfg(not(feature = "server"))] - { - cmd.error( - ErrorKind::MissingRequiredArgument, - "This binary was not compiled with server support. Recompile with --features server" - ).exit(); - } - } + #[cfg(feature = "server")] + KeepModes::Server => modes::server::mode_server(&mut cmd, &settings, &mut conn, data_path), KeepModes::GenerateConfig => { modes::generate_config::mode_generate_config(&mut cmd, &settings) } diff --git a/src/modes/common.rs b/src/modes/common.rs index 16d6db0..9501398 100644 --- a/src/modes/common.rs +++ b/src/modes/common.rs @@ -641,6 +641,7 @@ pub struct ImportMeta { /// /// Returns the first ID if provided, the newest item matching tags, /// or the newest item overall if neither is specified. +#[cfg(feature = "client")] pub fn resolve_item_id( client: &crate::client::KeepClient, ids: &[i64], @@ -664,6 +665,7 @@ pub fn resolve_item_id( } /// Resolve item IDs from explicit IDs or tags (multi-item variant). +#[cfg(feature = "client")] pub fn resolve_item_ids( client: &crate::client::KeepClient, ids: &[i64], diff --git a/src/modes/server/mod.rs b/src/modes/server/mod.rs index be274af..e130f23 100644 --- a/src/modes/server/mod.rs +++ b/src/modes/server/mod.rs @@ -180,11 +180,6 @@ async fn run_server( // Warn if authentication is enabled without TLS if config.password.is_some() || config.password_hash.is_some() || config.jwt_secret.is_some() { - #[cfg(not(feature = "tls"))] - log::warn!( - "SECURITY: Authentication enabled but TLS support is not compiled in. Credentials will be transmitted in plain text!" - ); - #[cfg(feature = "tls")] if config.cert_file.is_none() || config.key_file.is_none() { log::warn!( "SECURITY: Authentication enabled but TLS is not configured. Credentials will be transmitted in plain text!" @@ -196,7 +191,6 @@ async fn run_server( let service = app.into_make_service_with_connect_info::(); // Use TLS if both cert and key files are provided - #[cfg(feature = "tls")] if let (Some(cert_file), Some(key_file)) = (&config.cert_file, &config.key_file) { info!("SERVER: HTTPS server listening on {addr}");