diff --git a/README.md b/README.md index 7a2c488..0303874 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,9 @@ A lightweight, secure pastebin REST API built with Flask. - **Automatic expiry** - Pastes expire after configurable period of inactivity - **Size limits** - Configurable limits for anonymous and authenticated users - **Abuse prevention** - Content-hash deduplication throttles repeated identical submissions +- **Proof-of-work** - Configurable computational puzzle prevents automated spam - **Security headers** - HSTS, CSP, X-Frame-Options, Cache-Control, and more +- **CLI client** - Standalone `fpaste` command-line tool included - **Request tracing** - X-Request-ID support for log correlation - **Proxy trust validation** - Optional shared secret for defense-in-depth - **Minimal dependencies** - Flask only, SQLite built-in @@ -36,9 +38,12 @@ python run.py |--------|----------|-------------| | `GET /` | API information and usage | | `GET /health` | Health check (returns DB status) | +| `GET /challenge` | Get proof-of-work challenge | | `POST /` | Create a new paste | | `GET /` | Retrieve paste metadata | +| `HEAD /` | Retrieve paste metadata (headers only) | | `GET //raw` | Retrieve raw paste content | +| `HEAD //raw` | Retrieve paste headers (no body) | | `DELETE /` | Delete paste (requires auth) | ## Usage Examples @@ -77,6 +82,46 @@ curl -X DELETE \ http://localhost:5000/abc12345 ``` +## CLI Client + +A standalone command-line client `fpaste` is included (no external dependencies). + +### Basic Usage + +```bash +# Create paste from file +./fpaste create file.txt + +# Create paste from stdin +echo "Hello" | ./fpaste + +# Get paste content +./fpaste get abc12345 + +# Get paste metadata +./fpaste get -m abc12345 + +# Delete paste (requires auth) +./fpaste delete abc12345 + +# Show server info +./fpaste info +``` + +### Configuration + +Set server URL and authentication via environment or config file: + +```bash +# Environment variables +export FLASKPASTE_SERVER="https://paste.example.com" +export FLASKPASTE_CERT_SHA1="your-cert-fingerprint" + +# Or config file (~/.config/fpaste/config) +server = https://paste.example.com +cert_sha1 = your-cert-fingerprint +``` + ## Configuration Configuration via environment variables: @@ -92,6 +137,9 @@ Configuration via environment variables: | `FLASKPASTE_DEDUP_WINDOW` | `3600` (1 hour) | Dedup throttle window in seconds | | `FLASKPASTE_DEDUP_MAX` | `3` | Max identical submissions per window | | `FLASKPASTE_PROXY_SECRET` | (empty) | Shared secret for proxy trust validation | +| `FLASKPASTE_POW_DIFFICULTY` | `20` | PoW difficulty (leading zero bits, 0=disabled) | +| `FLASKPASTE_POW_TTL` | `300` (5 min) | PoW challenge validity period | +| `FLASKPASTE_POW_SECRET` | (auto) | Secret for signing PoW challenges | ## Authentication diff --git a/documentation/api.md b/documentation/api.md index 965cbbc..07ce694 100644 --- a/documentation/api.md +++ b/documentation/api.md @@ -28,6 +28,39 @@ The fingerprint must be exactly 40 lowercase hexadecimal characters (SHA1). ## Endpoints +### GET /challenge + +Get a proof-of-work challenge for paste creation. Required when PoW is enabled. + +**Request:** +```http +GET /challenge HTTP/1.1 +Host: localhost:5000 +``` + +**Response (PoW disabled):** +```json +{ + "enabled": false, + "difficulty": 0 +} +``` + +**Response (PoW enabled):** +```json +{ + "enabled": true, + "nonce": "a1b2c3d4e5f6a7b8a1b2c3d4e5f6a7b8", + "difficulty": 20, + "expires": 1700000300, + "token": "a1b2c3d4...:1700000300:20:signature" +} +``` + +The client must find a number `N` such that `SHA256(nonce + ":" + N)` has at least `difficulty` leading zero bits. + +--- + ### GET /health Health check endpoint for load balancers and monitoring. @@ -112,6 +145,20 @@ Content-Type: application/json {"content": "Hello, World!"} ``` +**Request (with Proof-of-Work):** + +When PoW is enabled, include the challenge token and solution: + +```http +POST / HTTP/1.1 +Host: localhost:5000 +Content-Type: text/plain +X-PoW-Token: a1b2c3d4...:1700000300:20:signature +X-PoW-Solution: 12345678 + +Hello, World! +``` + **Response (201 Created):** ```json { @@ -128,6 +175,8 @@ Content-Type: application/json | Code | Description | |------|-------------| | 400 | No content provided | +| 400 | Proof-of-work required (when PoW enabled) | +| 400 | Proof-of-work failed (invalid/expired challenge) | | 413 | Paste too large | | 429 | Duplicate content rate limit exceeded | @@ -139,7 +188,9 @@ Content-Type: application/json ### GET /{id} -Retrieve paste metadata. +### HEAD /{id} + +Retrieve paste metadata. HEAD returns headers only (no body). **Request:** ```http @@ -168,7 +219,9 @@ Host: localhost:5000 ### GET /{id}/raw -Retrieve raw paste content with correct MIME type. +### HEAD /{id}/raw + +Retrieve raw paste content with correct MIME type. HEAD returns headers only (useful for checking MIME type and size without downloading content). **Request:** ```http @@ -290,6 +343,58 @@ export FLASKPASTE_DEDUP_MAX=3 # Max duplicates per window (default: 3) --- +## Proof-of-Work + +FlaskPaste includes an optional proof-of-work system to prevent automated spam. + +**How it works:** +1. Client requests a challenge via `GET /challenge` +2. Server returns a nonce, difficulty, expiry time, and signed token +3. Client computes SHA256 hashes until finding one with enough leading zero bits +4. Client submits paste with `X-PoW-Token` and `X-PoW-Solution` headers + +**Algorithm:** +``` +For N = 0, 1, 2, ...: + hash = SHA256(nonce + ":" + N) + if leading_zero_bits(hash) >= difficulty: + return N as solution +``` + +**Configuration:** +```bash +export FLASKPASTE_POW_DIFFICULTY=20 # Leading zero bits required (0=disabled) +export FLASKPASTE_POW_TTL=300 # Challenge validity in seconds +export FLASKPASTE_POW_SECRET="..." # Optional signing key (auto-generated if empty) +``` + +**Headers:** +| Header | Description | +|--------|-------------| +| `X-PoW-Token` | The full token from `/challenge` response | +| `X-PoW-Solution` | The computed solution number | + +**Error responses:** +```json +{ + "error": "Proof-of-work required", + "hint": "GET /challenge for a new challenge" +} +``` + +```json +{ + "error": "Proof-of-work failed: Challenge expired" +} +``` + +**Notes:** +- Difficulty 20 requires approximately 1 million hash attempts on average +- Challenges are signed to prevent tampering +- Each challenge can only be used once (nonce uniqueness) + +--- + ## Error Response Format All errors return JSON: