From bee980605f9cef32d95fc1081e23dbc9aa721504 Mon Sep 17 00:00:00 2001 From: Andrew Phillips Date: Thu, 12 Mar 2026 22:18:42 -0300 Subject: [PATCH] feat: add HTTPS/TLS server support via rustls Add optional TLS support for the server using axum-server with the tls-rustls feature. When --server-cert and --server-key are provided (and tls feature is enabled), the server binds with TLS instead of plain HTTP. Changes: - Add axum-server dependency with optional tls-rustls feature - New 'tls' feature flag (independent of 'server') - --server-cert/--server-key CLI args gated behind tls feature - ServerConfig extended with cert_file/key_file fields - Conditional TLS/HTTP binding in server mod.rs - Fix PathBuf::to_str().unwrap() panic risk -> to_string_lossy() - Update README.md and DESIGN.md with TLS documentation --- Cargo.lock | 113 +++++++++- Cargo.toml | 4 + DESIGN.md | 59 ++++- README.md | 448 ++++++++++++++++++++++++++++--------- src/args.rs | 10 + src/config.rs | 22 ++ src/modes/server/common.rs | 8 + src/modes/server/mod.rs | 29 ++- 8 files changed, 569 insertions(+), 124 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4c25343..1f90f25 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -124,6 +124,15 @@ dependencies = [ "derive_arbitrary", ] +[[package]] +name = "arc-swap" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f3647c145568cec02c42054e07bdf9a5a698e15b466fb2341bfc393cd24aa5" +dependencies = [ + "rustversion", +] + [[package]] name = "arraydeque" version = "0.5.1" @@ -175,6 +184,28 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "aws-lc-rs" +version = "1.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94bffc006df10ac2a68c83692d734a465f8ee6c5b384d8545a636f81d858f4bf" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4321e568ed89bb5a7d291a7f37997c2c0df89809d7b6d12062c81ddb54aa782e" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + [[package]] name = "axum" version = "0.8.4" @@ -229,6 +260,28 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-server" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1df331683d982a0b9492b38127151e6453639cd34926eb9c07d4cd8c6d22bfc" +dependencies = [ + "arc-swap", + "bytes", + "either", + "fs-err", + "http", + "http-body", + "hyper", + "hyper-util", + "pin-project-lite", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + [[package]] name = "backtrace" version = "0.3.75" @@ -324,6 +377,8 @@ version = "1.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2352e5597e9c544d5e6d9c95190d5d27738ade584fa8db0a16e130e5c2b5296e" dependencies = [ + "jobserver", + "libc", "shlex", ] @@ -771,6 +826,12 @@ dependencies = [ "litrs", ] +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "dyn-clone" version = "1.0.20" @@ -906,6 +967,22 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs-err" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73fde052dbfc920003cfd2c8e2c6e6d4cc7c1091538c3a24226cec0665ab08c0" +dependencies = [ + "autocfg", + "tokio", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "futures" version = "0.3.31" @@ -1185,13 +1262,14 @@ dependencies = [ [[package]] name = "hyper" -version = "1.6.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ + "atomic-waker", "bytes", "futures-channel", - "futures-util", + "futures-core", "h2", "http", "http-body", @@ -1199,6 +1277,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", + "pin-utils", "smallvec", "tokio", "want", @@ -1206,12 +1285,11 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.16" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ "bytes", - "futures-core", "http", "http-body", "hyper", @@ -1411,6 +1489,16 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.3", + "libc", +] + [[package]] name = "js-sys" version = "0.3.77" @@ -1439,6 +1527,7 @@ dependencies = [ "anyhow", "async-stream", "axum", + "axum-server", "base64 0.22.1", "chrono", "clap", @@ -2235,6 +2324,7 @@ version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ + "aws-lc-rs", "log", "once_cell", "ring", @@ -2259,6 +2349,7 @@ version = "0.103.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" dependencies = [ + "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -2761,6 +2852,16 @@ dependencies = [ "syn 2.0.105", ] +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.17" diff --git a/Cargo.toml b/Cargo.toml index 8541261..7763ecb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,6 +72,7 @@ dirs = "6.0.0" similar = { version = "2.7.0", default-features = false, features = ["text"] } ureq = { version = "3", features = ["json"], optional = true } os_pipe = { version = "1", optional = true } +axum-server = { version = "0.8", features = ["tls-rustls"], optional = true } [features] # Default features include core compression engines and swagger UI @@ -107,6 +108,9 @@ 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"] + [dev-dependencies] tempfile = "3.3.0" rand = "0.8.5" diff --git a/DESIGN.md b/DESIGN.md index 2b64afc..7fe5b44 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -31,7 +31,8 @@ - `modes/info.rs` - Show detailed item information - `modes/diff.rs` - Compare two items - `modes/status.rs` - Show system status and capabilities -- `modes/server.rs` - REST HTTP server mode with OpenAPI documentation +- `modes/server.rs` - REST HTTP/HTTPS server mode with OpenAPI documentation +- `modes/client.rs` - Client mode for remote server (streaming save, local decompression) - `modes/common.rs` - Shared utilities for all modes ### Database Module @@ -57,6 +58,16 @@ - `common/is_binary.rs` - Binary file detection utilities - `common/status.rs` - Status information generation +### Client Module +- `client.rs` - HTTP client wrapper (ureq-based, supports streaming POST) +- `modes/client/save.rs` - 3-thread streaming save (stdin → tee → compress → pipe → HTTP POST) +- `modes/client/get.rs` - Get with server-side raw fetch + local decompression +- `modes/client/list.rs` - List delegation to server +- `modes/client/info.rs` - Info delegation to server +- `modes/client/delete.rs` - Delete delegation to server +- `modes/client/diff.rs` - Diff delegation to server +- `modes/client/status.rs` - Status delegation to server + ### Utility Modules - `plugins.rs` - Shared plugin utilities - `args.rs` - CLI argument definitions @@ -88,8 +99,14 @@ - `--quiet` - Do not show any messages - `--output-format ` - Output format for info, status, and list modes - `--server-password ` - Password for server authentication +- `--server-cert ` - TLS certificate file (PEM) for HTTPS server +- `--server-key ` - TLS private key file (PEM) for HTTPS server - `--force` - Force output even when binary data would be sent to a TTY +### Client Options (requires `client` feature) +- `--client-url ` - Remote keep server URL +- `--client-password ` - Remote server password + ## Data Storage ### Database Schema @@ -107,17 +124,31 @@ ### Status Operations - `GET /api/status` - Get system status information +- `GET /api/plugins/status` - Get plugin status information ### Item Operations -- `GET /api/item/` - Get a list of items as JSON. Optional params: `order=newest|oldest`, `start=0`, `count=100`, `tags[]=tag1&tags[]=tag2` -- `POST /api/item/` - Add a new item +- `GET /api/item/` - Get a list of items as JSON. Optional params: `order=newest|oldest`, `start=0`, `count=100`, `tags=tag1,tag2` +- `POST /api/item/` - Add a new item (body: raw content). Query params: `tags`, `metadata` (JSON), `compress=true|false`, `meta=true|false` +- `POST /api/item/<#>/meta` - Add metadata to an existing item (body: JSON object) - `DELETE /api/item/<#>` - Delete an item -- `GET /api/item/latest` - Return the latest item as JSON. Optional params: `tags[]=tag1&tags[]=tag2`, `allow_binary=true|false` -- `GET /api/item/latest/meta` - Return the latest item metadata as JSON. Optional params: `tags[]=tag1&tags[]=tag2` -- `GET /api/item/latest/content` - Return the raw content of the latest item. Optional params: `tags[]=tag1&tags[]=tag2` +- `GET /api/item/latest` - Return the latest item as JSON. Optional params: `tags=tag1,tag2`, `allow_binary=true|false` +- `GET /api/item/latest/meta` - Return the latest item metadata as JSON. Optional params: `tags=tag1,tag2` +- `GET /api/item/latest/content` - Return the raw content of the latest item. Optional params: `tags=tag1,tag2`, `decompress=true|false` - `GET /api/item/<#>` - Return the item as JSON. Optional params: `allow_binary=true|false` - `GET /api/item/<#>/meta` - Return the item metadata as JSON -- `GET /api/item/<#>/content` - Return the raw content of the item +- `GET /api/item/<#>/content` - Return the raw content of the item. Optional params: `decompress=true|false` +- `GET /api/diff` - Diff two items. Params: `id_a`, `id_b` + +### Server Modes +- **Plain HTTP** (default): `tokio::net::TcpListener` + `axum::serve()` +- **HTTPS** (with `tls` feature): `axum_server::bind_rustls()` with rustls when `--server-cert` and `--server-key` are provided +- Conditional selection at startup: cert+key present → HTTPS, otherwise → HTTP + +### Client/Server Protocol +- Smart clients (keep CLI) set `compress=false` and `meta=false` on POST, handling compression/metadata locally +- Dumb clients (curl) use defaults (`compress=true`, `meta=true`), server handles everything +- GET responses include `X-Keep-Compression` header when `decompress=false` +- Streaming save uses chunked transfer encoding for constant memory usage ### Authentication - Bearer token authentication: `Authorization: Bearer ` @@ -173,5 +204,19 @@ - File permissions are restricted to user only (umask 077) - Input validation for item IDs to prevent path traversal - Authentication for server mode with bearer or basic auth +- TLS/HTTPS support via rustls when certificate and key are provided - Proper resource cleanup using RAII patterns - Safe handling of external processes with proper stdin/stdout management + +## Feature Flags +- `server` - HTTP REST API server (axum-based) +- `tls` - HTTPS/TLS support for server (axum-server + rustls) +- `client` - HTTP client for remote server (ureq-based, includes streaming save) +- `mcp` - Model Context Protocol for AI assistant integration +- `swagger` - OpenAPI/Swagger UI documentation +- `magic` - File type detection via libmagic +- `lz4` - LZ4 compression (internal) +- `gzip` - GZip compression (internal) +- `bzip2` - BZip2 compression (external) +- `xz` - XZ compression (external) +- `zstd` - ZStd compression (external) diff --git a/README.md b/README.md index dcf1a20..48508d5 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,53 @@ # Keep -A command-line utility for managing temporary files with automatic compression, metadata generation, and querying. Instead of redirecting output to temporary files, pipe it into `keep` for organized storage and retrieval. +A command-line utility for storing and retrieving temporary data with automatic compression, metadata extraction, and querying. Pipe any output into `keep` for organized storage — no more losing data in `/tmp` files with cryptic names. ```sh # Instead of this: curl -s https://api.example.com/data > /tmp/api-data.json -cat /tmp/api-data.json # Do this: -curl -s https://api.example.com/data | keep api-data +curl -s https://api.example.com/data | keep --save api-data keep --get api-data ``` +## Table of Contents + +- [Features](#features) +- [Installation](#installation) +- [Quick Start](#quick-start) +- [Usage](#usage) + - [Save Mode](#save-mode) + - [Get Mode](#get-mode) + - [List Mode](#list-mode) + - [Info Mode](#info-mode) + - [Update Mode](#update-mode) + - [Delete Mode](#delete-mode) + - [Diff Mode](#diff-mode) + - [Status Mode](#status-mode) +- [Filters](#filters) +- [Compression](#compression) +- [Meta Plugins](#meta-plugins) +- [Configuration](#configuration) +- [Client/Server Mode](#clientserver-mode) + - [Server Mode](#server-mode) + - [Client Mode](#client-mode) + - [API Endpoints](#api-endpoints) +- [MCP (Model Context Protocol)](#mcp-model-context-protocol) +- [Shell Integration](#shell-integration) +- [Feature Flags](#feature-flags) +- [License](#license) + ## Features -- **Store and Retrieve** - Save content with tags, retrieve by ID or tag -- **Automatic Compression** - LZ4, GZip, BZip2, XZ, ZStd support -- **Metadata Plugins** - Auto-extract file type, digests, hostname, user info, and more -- **Filters** - Apply transformations (head, tail, grep, strip ANSI) on retrieval -- **Querying** - List, search, diff items with flexible formatting -- **REST API Server** - Optional HTTP server for programmatic access -- **MCP Support** - Model Context Protocol integration for AI assistants -- **Modular Design** - Extensible plugin system for compression, metadata, and filtering +- **Store and retrieve** — Save content with tags, retrieve by ID or tag +- **Automatic compression** — LZ4, GZip, BZip2, XZ, ZStd support +- **Metadata plugins** — Auto-extract file type, digests, hostname, user info, and more +- **Filters** — Apply transformations (head, tail, grep, strip ANSI) on retrieval +- **Querying** — List, search, diff items with flexible formatting +- **Client/server architecture** — Optional HTTP server with streaming support +- **MCP support** — Model Context Protocol integration for AI assistants +- **Modular design** — Extensible plugin system for compression, metadata, and filtering ## Installation @@ -31,6 +57,11 @@ Requires Rust and Cargo. ```sh cargo build --release +``` + +### Install via Cargo + +```sh cargo install --path . ``` @@ -41,22 +72,29 @@ cargo install --path . # Binary at bin/keep ``` -### Using Cargo +### Build with Server/Client Features ```sh -cargo install --path . +# Server only +cargo build --release --features server + +# Client only (for connecting to a remote keep server) +cargo build --release --features client + +# Server + client + all optional features +cargo build --release --features server,tls,client,swagger,mcp ``` ## Quick Start ```sh -# Save content with tags +# Save content with a tag echo "Hello, world!" | keep --save greeting # Retrieve by tag keep --get greeting -# List all items +# List all stored items keep --list # Get item details @@ -66,6 +104,28 @@ keep --info greeting keep --delete greeting ``` +### Real-World Examples + +```sh +# Save API response +curl -s https://api.github.com/repos/user/repo | keep --save repo-info + +# Save test output with metadata +npm test 2>&1 | keep --save test-results --meta project=myapp --meta env=staging + +# Chain commands: process and store +cat data.csv | sort | uniq | keep --save cleaned-data + +# Diff two versions +keep --diff 1 5 + +# Get first 20 lines of an item +keep --get 1 --filters "head_lines(20)" + +# List items from a specific project +keep --list --meta project=myapp +``` + ## Usage ### Save Mode @@ -73,22 +133,24 @@ keep --delete greeting Save stdin content with tags and metadata. ```sh -# Basic save (auto-assigned ID) +# Save (auto-assigned ID, no tag) echo "data" | keep --save -# Save with tags +# Save with a tag echo "data" | keep --save my-tag # Save with multiple tags and metadata cat report.pdf | keep --save report --meta project=alpha --meta env=prod -# Specify compression and digest +# Specify compression and digest algorithm echo "data" | keep --save my-tag --compression gzip --digest sha256 ``` +Tags and metadata make items easy to find later. Tags are simple identifiers; metadata is key-value pairs. + ### Get Mode -Retrieve items by ID or tags. Default mode when IDs are provided. +Retrieve items by ID or tags. This is the default mode when IDs are provided. ```sh # Get by ID @@ -99,13 +161,13 @@ keep 1 keep --get my-tag keep my-tag -# Get with filters +# Get with filters applied keep --get 1 --filters "head_lines(10)" -# Get with metadata filter +# Get by metadata filter keep --get --meta project=alpha -# Force binary output to TTY +# Force binary output to TTY (override safety check) keep --get 1 --force ``` @@ -120,16 +182,16 @@ keep --list # List by tag keep --list my-tag -# List with metadata filter +# Filter by metadata keep --list --meta env=prod # Custom column format keep --list --list-format "id,time,size,tags" -# JSON output +# JSON output for scripting keep --list --output-format json -# Human-readable sizes +# Human-readable file sizes keep --list --human-readable ``` @@ -154,7 +216,7 @@ keep --update 1 new-tag # Update metadata keep --update 1 --meta key=newvalue -# Remove metadata (no value) +# Remove a metadata key keep --update 1 --meta key ``` @@ -193,7 +255,7 @@ Apply transformations to item content during retrieval. Filters are chained with # First 10 lines keep --get 1 --filters "head_lines(10)" -# Skip first 5 lines, then grep +# Skip first 5 lines, then grep for errors keep --get 1 --filters "skip_lines(5)|grep(pattern=error)" # Strip ANSI escape codes @@ -201,6 +263,9 @@ keep --get 1 --filters "strip_ansi" # Last 100 bytes keep --get 1 --filters "tail_bytes(100)" + +# Complex chain +keep --get 1 --filters "skip_lines(10)|grep(pattern=TODO)|head_lines(5)" ``` ### Available Filters @@ -216,7 +281,61 @@ keep --get 1 --filters "tail_bytes(100)" | `grep(pattern)` | Filter matching lines | `pattern` (regex) | | `strip_ansi` | Remove ANSI escape codes | none | -Set the `KEEP_FILTERS` environment variable to apply filters by default. +Set `KEEP_FILTERS` to apply a default filter chain to all retrievals. + +## Compression + +Items are compressed automatically on save. Default: LZ4. + +| Algorithm | Type | Speed | Ratio | +|-----------|------|-------|-------| +| `lz4` | Internal | Fastest | Lower | +| `gzip` | Internal | Fast | Good | +| `bzip2` | External | Slow | Better | +| `xz` | External | Slowest | Best | +| `zstd` | External | Fast | Good | +| `none` | Internal | N/A | N/A | + +```sh +# Specify compression per item +echo "data" | keep --save my-tag --compression zstd + +# Set default via environment +export KEEP_COMPRESSION=gzip +``` + +External compression programs (`bzip2`, `xz`, `zstd`) must be installed on the system. + +## Meta Plugins + +Metadata is automatically extracted when saving items. + +| Plugin | Key | Description | +|--------|-----|-------------| +| `env` | `*` | Capture `KEEP_META_*` environment variables | +| `magic_file` | `file_type` | File type detection (requires `magic` feature) | +| `text` | `text_line_count`, `text_word_count` | Line and word counts | +| `user` | `uid`, `user`, `gid`, `group` | Current user info | +| `shell` | `shell` | Current shell path | +| `shell_pid` | `shell_pid` | Shell process ID | +| `keep_pid` | `keep_pid` | Keep process ID | +| `digest` | `digest_sha256`, `digest_md5` | Content digests | +| `read_time` | `read_time` | Time to read content | +| `read_rate` | `read_rate` | Data read rate | +| `hostname` | `hostname`, `hostname_short` | System hostname | +| `exec` | Custom | Run external commands for metadata | +| `cwd` | `cwd` | Current working directory | + +```sh +# Use specific plugins +echo "data" | keep --save tag --meta-plugins "digest,text,user" + +# Capture custom metadata via environment +KEEP_META_project=alpha echo "data" | keep --save tag + +# Combine environment and CLI metadata +KEEP_META_build=1234 echo "data" | keep --save tag --meta env=staging +``` ## Configuration @@ -234,11 +353,13 @@ Set the `KEEP_FILTERS` environment variable to apply filters by default. | `KEEP_SERVER_PORT` | Server port | `21080` | | `KEEP_SERVER_PASSWORD` | Server password | none | | `KEEP_SERVER_PASSWORD_HASH` | Server password hash | none | +| `KEEP_SERVER_CERT` | TLS certificate file path (PEM) | none | +| `KEEP_SERVER_KEY` | TLS private key file path (PEM) | none | +| `KEEP_CLIENT_URL` | Remote keep server URL | none | +| `KEEP_CLIENT_PASSWORD` | Remote server password | none | Any config setting can be overridden with `KEEP__` environment variables (double underscore separator). -Capture shell commands automatically by setting `KEEP_META_command` in your shell profile (see Shell Integration). - ### Configuration File Default location: `~/.config/keep/config.yml` @@ -289,18 +410,35 @@ server: address: "127.0.0.1" port: 21080 password: "secret" + # TLS (requires tls feature) + # cert_file: /path/to/cert.pem + # key_file: /path/to/key.pem + +# Client settings +client: + url: "http://localhost:21080" + password: "secret" human_readable: true quiet: false force: false ``` -## Server Mode +## Client/Server Mode -Start an HTTP REST API server for programmatic access. +Keep supports a client/server architecture where one machine runs a keep server and other machines connect as clients. This is useful for: + +- Centralizing stored data across multiple machines +- Sharing items between team members +- Offloading storage to a dedicated server +- Piping data from long-running processes without local storage + +### Server Mode + +Start an HTTP REST API server: ```sh -# Start server on default address (127.0.0.1:21080) +# Default: 127.0.0.1:21080 keep --server # Custom address and port @@ -310,24 +448,165 @@ keep --server --server-address 0.0.0.0 --server-port 8080 keep --server --server-password mypassword ``` +#### HTTPS / TLS + +Build with the `tls` feature to enable HTTPS: + +```sh +cargo build --release --features server,tls +``` + +Provide a TLS certificate and private key (both PEM format): + +```sh +# Via CLI flags +keep --server \ + --server-cert /path/to/cert.pem \ + --server-key /path/to/key.pem + +# Via environment variables +export KEEP_SERVER_CERT=/path/to/cert.pem +export KEEP_SERVER_KEY=/path/to/key.pem +keep --server + +# Via config file (config.yml) +server: + cert_file: /path/to/cert.pem + key_file: /path/to/key.pem +``` + +When cert and key are provided, the server listens with HTTPS. Without them, it falls back to plain HTTP. The port is controlled by `--server-port` (default: 21080). + +**Self-signed certificates** (for development): + +```sh +# Generate a self-signed cert +openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem \ + -days 365 -nodes -subj "/CN=localhost" + +# Start server with self-signed cert +keep --server --server-cert cert.pem --server-key key.pem + +# Connect client with HTTPS +keep --client-url https://localhost:21080 --save my-tag +``` + +The server accepts data from both dumb clients (raw HTTP/curl) and smart clients (the keep CLI). + +#### Server Query Parameters + +The server supports query parameters that control processing: + +| Parameter | Default | Description | +|-----------|---------|-------------| +| `tags` | none | Comma-separated tags | +| `metadata` | none | JSON-encoded metadata | +| `compress` | `true` | `false` = client already compressed, store as-is | +| `meta` | `true` | `false` = client handles metadata, skip server-side plugins | +| `decompress` | `true` | `false` = return raw compressed bytes on GET | + +When using a smart client, these are set automatically. For curl, the server handles everything by default. + +#### Example: Curl as a Dumb Client + +```sh +# Save (server handles compression and metadata) +curl -X POST -d "my data" http://localhost:21080/api/item/?tags=my-tag + +# Retrieve (server decompresses) +curl http://localhost:21080/api/item/1/content + +# Save compressed (client handles compression, server skips) +gzip -c data.txt | curl -X POST -d @- "http://localhost:21080/api/item/?compress=false&tags=my-tag" +``` + +### Client Mode + +The keep CLI can connect to a remote server as a smart client. Build with the `client` feature: + +```sh +cargo build --release --features client +``` + +```sh +# Set server URL via flag or environment +keep --client-url http://server:21080 --save my-tag +export KEEP_CLIENT_URL=http://server:21080 + +# With authentication +keep --client-url http://server:21080 --client-password mypassword --save my-tag +export KEEP_CLIENT_PASSWORD=mypassword +``` + +#### How Client Mode Works + +Client mode uses **local plugins** and **remote storage**: + +1. **Save**: Local compression and metadata plugins run on the client; compressed data streams to the server +2. **Get**: Server sends raw compressed data; client decompresses locally and applies filters +3. **Other operations** (list, info, delete, diff): Delegated directly to the server + +This means client behavior is consistent with local mode — the same compression settings and filters apply. + +#### Streaming Architecture + +Client save uses a 3-thread streaming pipeline for constant memory usage regardless of data size: + +``` +┌──────────────┐ OS pipe ┌────────────────┐ +│ Reader thread ├──────────────────┤ Streamer thread│ +│ │ (compressed │ │ +│ stdin → tee │ bytes) │ pipe → POST │ +│ → hash │ │ (chunked) │ +│ → compress│ │ │ +└──────────────┘ └────────────────┘ + │ │ + ▼ ▼ + stdout + Server stores blob + SHA-256 digest +``` + +- **Reader thread**: Reads stdin, tees output to stdout, computes SHA-256, compresses data, writes to OS pipe +- **Streamer thread**: Reads compressed bytes from pipe, streams to server via chunked HTTP POST +- **Main thread**: After streaming completes, sends computed metadata (digest, hostname, size) to server + +Memory usage is O(PIPESIZE) — typically 64KB — regardless of how much data is being stored. + +#### Example: Remote Pipeline + +```sh +# On a build server, pipe logs to a central keep server +make build 2>&1 | keep --client-url http://logserver:21080 \ + --save build-logs \ + --meta project=myapp \ + --meta branch=$(git branch --show-current) + +# Retrieve from any machine +keep --client-url http://logserver:21080 --get build-logs + +# List recent builds from a specific project +keep --client-url http://logserver:21080 --list --meta project=myapp +``` + ### API Endpoints | Method | Path | Description | |--------|------|-------------| | `GET` | `/api/status` | System status | -| `GET` | `/api/plugins/status` | Plugins status | -| `GET` | `/api/item/` | List items (supports `tags`, `order`, `start`, `count` params) | -| `POST` | `/api/item/` | Create item (body: raw content, query: `tags`, `metadata`) | +| `GET` | `/api/plugins/status` | Plugin status | +| `GET` | `/api/item/` | List items (`tags`, `order`, `start`, `count` params) | +| `POST` | `/api/item/` | Create item (body: raw content, params: `tags`, `metadata`, `compress`, `meta`) | | `GET` | `/api/item/latest/content` | Latest item content | | `GET` | `/api/item/latest/meta` | Latest item metadata | | `GET` | `/api/item/{id}` | Item info by ID | | `GET` | `/api/item/{id}/content` | Item content by ID | | `GET` | `/api/item/{id}/meta` | Item metadata by ID | | `GET` | `/api/item/{id}/info` | Item info by ID | +| `POST` | `/api/item/{id}/meta` | Add metadata to existing item (body: JSON object) | | `DELETE` | `/api/item/{id}` | Delete item by ID | | `GET` | `/api/diff` | Diff two items (`id_a`, `id_b` params) | -### Authentication +#### Authentication ```sh # Bearer token @@ -339,7 +618,7 @@ curl -u keep:mypassword http://localhost:21080/api/status When no password is configured, authentication is disabled. -### Swagger UI +#### Swagger UI Build with the `swagger` feature to enable OpenAPI documentation: @@ -369,28 +648,6 @@ MCP endpoint available at `/mcp/sse` when the server is running. | `list_items` | List items | `tags[]`, `limit`, `offset` | | `search_items` | Search items | `tags[]`, `metadata{}` | -## Feature Flags - -| Feature | Default | Description | -|---------|---------|-------------| -| `magic` | Yes | File type detection via libmagic | -| `lz4` | Yes | LZ4 compression (internal) | -| `gzip` | Yes | GZip compression (internal) | -| `server` | No | HTTP REST API server | -| `mcp` | No | Model Context Protocol support | -| `swagger` | No | Swagger UI for API docs | -| `bzip2` | No | BZip2 compression (external program) | -| `xz` | No | XZ compression (external program) | -| `zstd` | No | ZStd compression (external program) | - -```sh -# Build with server and swagger -cargo build --features server,swagger - -# Build with all features -cargo build --features server,mcp,swagger,magic -``` - ## Shell Integration Source `profile.bash` to enable shell integration: @@ -401,9 +658,9 @@ source /path/to/keep/profile.bash This provides: -- **`keep` function** - Captures the current command in metadata automatically -- **`@` alias** - Shorthand for `keep --save` -- **`@@` alias** - Shorthand for `keep --get` +- **`keep` function** — Captures the current command in metadata automatically +- **`@` alias** — Shorthand for `keep --save` +- **`@@` alias** — Shorthand for `keep --get` ```sh # Save with automatic command capture @@ -413,55 +670,34 @@ curl -s api.example.com | @ api-response @@ api-response ``` -## Compression +## Feature Flags -Items are compressed automatically on save. Default: LZ4. - -| Algorithm | Type | Speed | Ratio | -|-----------|------|-------|-------| -| `lz4` | Internal | Fastest | Lower | -| `gzip` | Internal | Fast | Good | -| `bzip2` | External | Slow | Better | -| `xz` | External | Slowest | Best | -| `zstd` | External | Fast | Good | -| `none` | Internal | N/A | N/A | +| Feature | Default | Description | +|---------|---------|-------------| +| `magic` | Yes | File type detection via libmagic | +| `lz4` | Yes | LZ4 compression (internal) | +| `gzip` | Yes | GZip compression (internal) | +| `server` | No | HTTP REST API server | +| `tls` | No | HTTPS/TLS server support (requires `server`) | +| `client` | No | HTTP client for remote server | +| `mcp` | No | Model Context Protocol support | +| `swagger` | No | Swagger UI for API docs | +| `bzip2` | No | BZip2 compression (external program) | +| `xz` | No | XZ compression (external program) | +| `zstd` | No | ZStd compression (external program) | ```sh -# Specify compression per item -echo "data" | keep --save my-tag --compression zstd +# Server with Swagger UI +cargo build --features server,swagger -# Set default via environment -export KEEP_COMPRESSION=gzip -``` +# Server with HTTPS +cargo build --features server,tls -External compression programs (`bzip2`, `xz`, `zstd`) must be installed on the system. +# Client only +cargo build --features client -## Meta Plugins - -Metadata is automatically extracted when saving items. - -| Plugin | Key | Description | -|--------|-----|-------------| -| `env` | `*` | Capture `KEEP_META_*` environment variables | -| `magic_file` | `file_type` | File type detection (requires `magic` feature) | -| `text` | `text_line_count`, `text_word_count` | Line and word counts | -| `user` | `uid`, `user`, `gid`, `group` | Current user info | -| `shell` | `shell` | Current shell path | -| `shell_pid` | `shell_pid` | Shell process ID | -| `keep_pid` | `keep_pid` | Keep process ID | -| `digest` | `digest_sha256`, `digest_md5` | Content digests | -| `read_time` | `read_time` | Time to read content | -| `read_rate` | `read_rate` | Data read rate | -| `hostname` | `hostname`, `hostname_short` | System hostname | -| `exec` | Custom | Run external commands for metadata | -| `cwd` | `cwd` | Current working directory | - -```sh -# Use specific plugins -echo "data" | keep --save tag --meta-plugins "digest,text,user" - -# Capture custom metadata via environment -KEEP_META_project=alpha echo "data" | keep --save tag +# Everything +cargo build --features server,tls,client,mcp,swagger,magic ``` ## License diff --git a/src/args.rs b/src/args.rs index d994a4b..b53679a 100644 --- a/src/args.rs +++ b/src/args.rs @@ -75,6 +75,16 @@ pub struct ModeArgs { #[arg(help_heading("Server Options"), long, env("KEEP_SERVER_PORT"))] #[arg(help("Server port to bind to"))] pub server_port: Option, + + #[cfg(feature = "tls")] + #[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")] + #[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, } /// Struct for item-specific arguments, such as compression and plugins. diff --git a/src/config.rs b/src/config.rs index 7faee68..6ec7c52 100644 --- a/src/config.rs +++ b/src/config.rs @@ -146,6 +146,8 @@ pub struct ServerConfig { pub password_file: Option, pub password: Option, pub password_hash: Option, + pub cert_file: Option, + pub key_file: Option, } #[derive(Debug, Clone, Deserialize, Serialize)] @@ -287,6 +289,18 @@ impl Settings { config_builder = config_builder.set_override("server.port", server_port)?; } + #[cfg(feature = "tls")] + 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")] + 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())?; + } + if let Some(compression) = &args.item.compression { config_builder = config_builder.set_override("compression_plugin.name", compression.as_str())?; @@ -480,6 +494,14 @@ impl Settings { self.server.as_ref().and_then(|s| s.port) } + pub fn server_cert_file(&self) -> Option { + self.server.as_ref().and_then(|s| s.cert_file.clone()) + } + + pub fn server_key_file(&self) -> Option { + self.server.as_ref().and_then(|s| s.key_file.clone()) + } + pub fn compression(&self) -> Option { self.compression_plugin.as_ref().map(|c| c.name.clone()) } diff --git a/src/modes/server/common.rs b/src/modes/server/common.rs index 1551f50..77d0c84 100644 --- a/src/modes/server/common.rs +++ b/src/modes/server/common.rs @@ -67,6 +67,14 @@ pub struct ServerConfig { /// Pre-hashed password (Unix crypt format) for secure authentication. Preferred /// over plain text password for production use. pub password_hash: Option, + /// Optional path to TLS certificate file (PEM). + /// + /// When both cert_file and key_file are set, the server uses HTTPS. + pub cert_file: Option, + /// Optional path to TLS private key file (PEM). + /// + /// When both cert_file and key_file are set, the server uses HTTPS. + pub key_file: Option, } /// Application state shared across all routes. diff --git a/src/modes/server/mod.rs b/src/modes/server/mod.rs index a693165..416a25b 100644 --- a/src/modes/server/mod.rs +++ b/src/modes/server/mod.rs @@ -52,6 +52,8 @@ pub fn mode_server( port: Some(server_port), password: settings.server_password(), password_hash: settings.server_password_hash(), + cert_file: settings.server_cert_file(), + key_file: settings.server_key_file(), }; // Create ItemService once @@ -138,14 +140,31 @@ async fn run_server( let addr: SocketAddr = bind_address.parse()?; + // Build the app into a service + 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}"); + + use axum_server::tls_rustls::RustlsConfig; + + let tls_config = RustlsConfig::from_pem_file(cert_file, key_file) + .await + .map_err(|e| anyhow::anyhow!("Failed to load TLS config: {e}"))?; + + axum_server::bind_rustls(addr, tls_config) + .serve(service) + .await?; + + return Ok(()); + } + info!("SERVER: HTTP server listening on {addr}"); let listener = tokio::net::TcpListener::bind(addr).await?; - axum::serve( - listener, - app.into_make_service_with_connect_info::(), - ) - .await?; + axum::serve(listener, service).await?; Ok(()) }