forked from username/flaskpaste
Features: - REST API for text/binary pastes with MIME detection - Client certificate auth via X-SSL-Client-SHA1 header - SQLite with WAL mode for concurrent access - Automatic paste expiry with LRU cleanup Security: - HSTS, CSP, X-Frame-Options, X-Content-Type-Options - Cache-Control: no-store for sensitive responses - X-Request-ID tracing for log correlation - X-Proxy-Secret validation for defense-in-depth - Parameterized queries, input validation - Size limits (3 MiB anon, 50 MiB auth) Includes /health endpoint, container support, and 70 tests.
334 lines
6.5 KiB
Markdown
334 lines
6.5 KiB
Markdown
# FlaskPaste API Reference
|
|
|
|
## Overview
|
|
|
|
FlaskPaste provides a RESTful API for creating, retrieving, and deleting text and binary pastes.
|
|
|
|
**Base URL:** `http://your-server:5000/`
|
|
|
|
**Content Types:**
|
|
- Requests: `application/json`, `text/plain`, `application/octet-stream`, or any binary type
|
|
- Responses: `application/json` for metadata, original MIME type for raw content
|
|
|
|
## Authentication
|
|
|
|
Authentication is optional and uses client certificate fingerprints passed via the `X-SSL-Client-SHA1` header.
|
|
|
|
```http
|
|
X-SSL-Client-SHA1: a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2
|
|
```
|
|
|
|
The fingerprint must be exactly 40 lowercase hexadecimal characters (SHA1).
|
|
|
|
**Benefits of authentication:**
|
|
- Larger upload limit (50 MiB vs 3 MiB)
|
|
- Ability to delete owned pastes
|
|
|
|
---
|
|
|
|
## Endpoints
|
|
|
|
### GET /health
|
|
|
|
Health check endpoint for load balancers and monitoring.
|
|
|
|
**Request:**
|
|
```http
|
|
GET /health HTTP/1.1
|
|
Host: localhost:5000
|
|
```
|
|
|
|
**Response (200 OK):**
|
|
```json
|
|
{
|
|
"status": "healthy",
|
|
"database": "ok"
|
|
}
|
|
```
|
|
|
|
**Response (503 Service Unavailable):**
|
|
```json
|
|
{
|
|
"status": "unhealthy",
|
|
"database": "error"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### GET /
|
|
|
|
Returns API information and usage instructions.
|
|
|
|
**Request:**
|
|
```http
|
|
GET / HTTP/1.1
|
|
Host: localhost:5000
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"name": "FlaskPaste",
|
|
"version": "1.0.0",
|
|
"endpoints": {
|
|
"GET /": "API information",
|
|
"GET /health": "Health check",
|
|
"POST /": "Create paste",
|
|
"GET /<id>": "Retrieve paste metadata",
|
|
"GET /<id>/raw": "Retrieve raw paste content",
|
|
"DELETE /<id>": "Delete paste"
|
|
},
|
|
"usage": {
|
|
"raw": "curl --data-binary @file.txt http://host/",
|
|
"pipe": "cat file.txt | curl --data-binary @- http://host/",
|
|
"json": "curl -H 'Content-Type: application/json' -d '{\"content\":\"...\"}' http://host/"
|
|
},
|
|
"note": "Use --data-binary (not -d) to preserve newlines"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### POST /
|
|
|
|
Create a new paste.
|
|
|
|
**Request (Raw Binary):**
|
|
```http
|
|
POST / HTTP/1.1
|
|
Host: localhost:5000
|
|
Content-Type: application/octet-stream
|
|
|
|
```
|
|
|
|
**Request (JSON):**
|
|
```http
|
|
POST / HTTP/1.1
|
|
Host: localhost:5000
|
|
Content-Type: application/json
|
|
|
|
```
|
|
|
|
**Response (201 Created):**
|
|
```json
|
|
{
|
|
"id": "abc12345",
|
|
"url": "/abc12345",
|
|
"raw": "/abc12345/raw",
|
|
"mime_type": "text/plain",
|
|
"created_at": 1700000000,
|
|
"owner": "a1b2c3..." // Only present if authenticated
|
|
}
|
|
```
|
|
|
|
**Errors:**
|
|
| Code | Description |
|
|
|------|-------------|
|
|
| 400 | No content provided |
|
|
| 413 | Paste too large |
|
|
|
|
**Size Limits:**
|
|
- Anonymous: 3 MiB (configurable via `FLASKPASTE_MAX_ANON`)
|
|
- Authenticated: 50 MiB (configurable via `FLASKPASTE_MAX_AUTH`)
|
|
|
|
---
|
|
|
|
### GET /{id}
|
|
|
|
Retrieve paste metadata.
|
|
|
|
**Request:**
|
|
```http
|
|
GET /abc12345 HTTP/1.1
|
|
Host: localhost:5000
|
|
```
|
|
|
|
**Response (200 OK):**
|
|
```json
|
|
{
|
|
"id": "abc12345",
|
|
"mime_type": "text/plain",
|
|
"size": 1234,
|
|
"created_at": 1700000000,
|
|
"raw": "/abc12345/raw"
|
|
}
|
|
```
|
|
|
|
**Errors:**
|
|
| Code | Description |
|
|
|------|-------------|
|
|
| 400 | Invalid paste ID format |
|
|
| 404 | Paste not found |
|
|
|
|
---
|
|
|
|
### GET /{id}/raw
|
|
|
|
Retrieve raw paste content with correct MIME type.
|
|
|
|
**Request:**
|
|
```http
|
|
GET /abc12345/raw HTTP/1.1
|
|
Host: localhost:5000
|
|
```
|
|
|
|
**Response (200 OK):**
|
|
```http
|
|
HTTP/1.1 200 OK
|
|
Content-Type: image/png
|
|
Content-Disposition: inline
|
|
|
|
<binary content>
|
|
```
|
|
|
|
- `Content-Disposition: inline` is set for `image/*` and `text/*` types
|
|
- Content-Type matches the detected/stored MIME type
|
|
|
|
**Errors:**
|
|
| Code | Description |
|
|
|------|-------------|
|
|
| 400 | Invalid paste ID format |
|
|
| 404 | Paste not found |
|
|
|
|
---
|
|
|
|
### DELETE /{id}
|
|
|
|
Delete a paste. Requires authentication and ownership.
|
|
|
|
**Request:**
|
|
```http
|
|
DELETE /abc12345 HTTP/1.1
|
|
Host: localhost:5000
|
|
X-SSL-Client-SHA1: a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2
|
|
```
|
|
|
|
**Response (200 OK):**
|
|
```json
|
|
{
|
|
"message": "Paste deleted"
|
|
}
|
|
```
|
|
|
|
**Errors:**
|
|
| Code | Description |
|
|
|------|-------------|
|
|
| 400 | Invalid paste ID format |
|
|
| 401 | Authentication required |
|
|
| 403 | Permission denied (not owner) |
|
|
| 404 | Paste not found |
|
|
|
|
---
|
|
|
|
## MIME Type Detection
|
|
|
|
FlaskPaste automatically detects MIME types using:
|
|
|
|
1. **Magic byte signatures** (highest priority)
|
|
- PNG: `\x89PNG\r\n\x1a\n`
|
|
- JPEG: `\xff\xd8\xff`
|
|
- GIF: `GIF87a` or `GIF89a`
|
|
- WebP: `RIFF....WEBP`
|
|
- ZIP: `PK\x03\x04`
|
|
- PDF: `%PDF`
|
|
- GZIP: `\x1f\x8b`
|
|
|
|
2. **Explicit Content-Type header** (if not generic)
|
|
|
|
3. **UTF-8 detection** (falls back to `text/plain`)
|
|
|
|
4. **Binary fallback** (`application/octet-stream`)
|
|
|
|
---
|
|
|
|
## Paste Expiry
|
|
|
|
Pastes expire based on last access time (default: 5 days).
|
|
|
|
- Every `GET /{id}` or `GET /{id}/raw` updates the last access timestamp
|
|
- Cleanup runs automatically (hourly, throttled)
|
|
- Configurable via `FLASKPASTE_EXPIRY` environment variable
|
|
|
|
---
|
|
|
|
## Error Response Format
|
|
|
|
All errors return JSON:
|
|
|
|
```json
|
|
{
|
|
"error": "Description of the error"
|
|
}
|
|
```
|
|
|
|
For size limit errors (413):
|
|
```json
|
|
{
|
|
"error": "Paste too large",
|
|
"size": 5000000,
|
|
"max_size": 3145728,
|
|
"authenticated": false
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Security Headers
|
|
|
|
All responses include the following security headers:
|
|
|
|
| Header | Value |
|
|
|--------|-------|
|
|
| `X-Content-Type-Options` | `nosniff` |
|
|
| `X-Frame-Options` | `DENY` |
|
|
| `X-XSS-Protection` | `1; mode=block` |
|
|
| `Referrer-Policy` | `strict-origin-when-cross-origin` |
|
|
| `Content-Security-Policy` | `default-src 'none'; frame-ancestors 'none'` |
|
|
| `Permissions-Policy` | `geolocation=(), microphone=(), camera=()` |
|
|
| `Strict-Transport-Security` | `max-age=31536000; includeSubDomains` |
|
|
| `Cache-Control` | `no-store, no-cache, must-revalidate, private` |
|
|
| `Pragma` | `no-cache` |
|
|
|
|
---
|
|
|
|
## Request Tracing
|
|
|
|
All requests include an `X-Request-ID` header for log correlation:
|
|
|
|
- If the client provides `X-Request-ID`, it is passed through
|
|
- If not provided, a UUID is generated
|
|
- The ID is echoed back in the response
|
|
- All log entries include `[rid=<request-id>]`
|
|
|
|
**Example:**
|
|
```bash
|
|
# Client-provided ID
|
|
curl -H "X-Request-ID: my-trace-123" https://paste.example.com/health
|
|
|
|
# Response includes:
|
|
# X-Request-ID: my-trace-123
|
|
```
|
|
|
|
---
|
|
|
|
## Proxy Trust Validation
|
|
|
|
When `FLASKPASTE_PROXY_SECRET` is configured, the application validates that requests come from a trusted reverse proxy by checking the `X-Proxy-Secret` header.
|
|
|
|
This provides defense-in-depth against header spoofing if an attacker bypasses the reverse proxy.
|
|
|
|
**Configuration:**
|
|
```bash
|
|
export FLASKPASTE_PROXY_SECRET="your-secret-value"
|
|
```
|
|
|
|
**Proxy Configuration (HAProxy):**
|
|
```
|
|
http-request set-header X-Proxy-Secret your-secret-value
|
|
```
|
|
|
|
If the secret doesn't match, authentication headers (`X-SSL-Client-SHA1`) are ignored and the request is treated as anonymous.
|