From a1bc3b1e032d53a0554ebd755a0389c52d633d83 Mon Sep 17 00:00:00 2001 From: csd113 Date: Tue, 10 Mar 2026 21:06:52 -0700 Subject: [PATCH] update readme for 1.0.13 --- README.md | 151 ++++++++++++++++++++++++++++-------- SETUP.md | 114 +++++++++++++++++++++++---- docs/build_all_rustchan.sh | 142 +++++++++++++++++++++++++++++++++ clippy.sh => docs/clippy.sh | 0 4 files changed, 356 insertions(+), 51 deletions(-) create mode 100755 docs/build_all_rustchan.sh rename clippy.sh => docs/clippy.sh (100%) diff --git a/README.md b/README.md index 3012720..933edc2 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ [![SQLite](https://img.shields.io/badge/SQLite-WAL_Mode-003B57?style=for-the-badge&logo=sqlite&logoColor=white)](https://www.sqlite.org/) [![Axum](https://img.shields.io/badge/Axum-0.8-7c3aed?style=for-the-badge)](https://github.com/tokio-rs/axum) [![License: MIT](https://img.shields.io/badge/License-MIT-22c55e?style=for-the-badge)](LICENSE) -[![Version](https://img.shields.io/badge/Version-1.0.11-0ea5e9?style=for-the-badge)](#changelog) +[![Version](https://img.shields.io/badge/Version-1.0.13-0ea5e9?style=for-the-badge)](#changelog)
@@ -44,7 +44,7 @@ Two external tools are supported as **optional enhancements**: [**ffmpeg**](#ffm ### ๐Ÿ“‹ Boards & Posting - Multiple boards with independent per-board configuration - Threaded replies with globally unique post numbers -- **Thread polls** โ€” OP-only, 2โ€“10 options, live percentage bar results, one vote per IP +- **Thread polls** โ€” OP-only, 2โ€“10 options, live percentage bar results, one vote per IP; expired vote rows cleaned up automatically - **Spoiler tags** โ€” `[spoiler]text[/spoiler]` with click-to-reveal - **Dice rolling** โ€” `[dice NdM]` resolved server-side at post time (e.g. `[dice 2d6]` โ†’ `๐ŸŽฒ 2d6 โ–ธ โš„ โš… = 11`) - **Emoji shortcodes** โ€” 25 built-in (`:fire:` โ†’ ๐Ÿ”ฅ, `:think:` โ†’ ๐Ÿค”, `:based:` โ†’ ๐Ÿ—ฟ) @@ -56,20 +56,23 @@ Two external tools are supported as **optional enhancements**: [**ffmpeg**](#ffm - Tripcodes and user-deletable posts via deletion tokens - Per-board NSFW tagging, bump limits, and thread caps - Board index, catalog grid, full-text search, and pagination +- Trailing slash normalization โ€” all URL variants resolve correctly ### ๐Ÿ–ผ๏ธ Media -- **Images:** JPEG *(EXIF-stripped on upload)*, PNG, GIF, WebP +- **Images:** JPEG *(EXIF-stripped and orientation-corrected on upload)*, PNG, GIF, WebP - **Video:** MP4, WebM โ€” auto-transcoded to VP9+Opus WebM via ffmpeg; AV1 streams re-encoded to VP9 - **Audio:** MP3, OGG, FLAC, WAV, M4A, AAC (up to 150 MB default) - **Image + audio combo posts** โ€” attach both an image and an audio file simultaneously - **Audio waveform thumbnails** โ€” generated via ffmpeg's `showwavespic` filter for standalone audio uploads +- **Waveform cache eviction** โ€” background task prunes oldest thumbnails when the cache exceeds `waveform_cache_max_mb` (default 200 MiB); originals never touched - **Video embed unfurling** โ€” per-board opt-in; YouTube, Invidious, and Streamable URLs render as thumbnail + click-to-play widgets - Auto-generated thumbnails with configurable dimensions - Resizable inline image expansion via drag-to-resize - **Client-side auto-compression** โ€” oversized files are compressed in-browser before upload with a live progress bar +- **Streaming multipart** โ€” uploads are validated against size limits in flight; never fully buffered in RAM - Two-layer file validation: Content-Type header + magic byte inspection (extensions are never trusted) @@ -83,12 +86,14 @@ Two external tools are supported as **optional enhancements**: [**ffmpeg**](#ffm - **Per-post ban + delete** โ€” single-click to ban an IP hash and remove the post simultaneously - **Ban appeal system** โ€” banned users can submit appeals; admins review from a dedicated queue with dismiss and accept+unban actions - **IP history view** โ€” paginated list of all posts from any IP hash across all boards -- **PoW CAPTCHA** โ€” per-board opt-in SHA-256 proof-of-work for new thread creation; replies are exempt +- **PoW CAPTCHA** โ€” per-board opt-in SHA-256 proof-of-work for all posts (threads and replies); nonce replay blocked within the 5-minute validity window - **Report system** โ€” users can report posts; admins see an inbox with resolve and resolve+ban actions - **Moderation log** โ€” append-only audit trail of all admin actions, viewable from the panel - Word filters (pattern โ†’ replacement, site-wide) -- **Full backup & restore** โ€” entirely web-based with no shell access required -- **SQLite VACUUM** โ€” one-click database compaction with before/after size reporting +- **Full Site Backup & Restore** โ€” entirely web-based, no shell access required; all operations stream from disk, never buffering the full backup in RAM +- **Scheduled VACUUM** โ€” automatic database compaction on a configurable interval; reclaimed bytes logged +- **DB size warning** โ€” admin panel shows a red banner when the database exceeds `db_warn_threshold_mb` +- **Expired poll cleanup** โ€” background task purges stale vote rows on a configurable schedule - Per-board controls: editing, edit window, archiving, video embeds, PoW CAPTCHA @@ -96,17 +101,21 @@ Two external tools are supported as **optional enhancements**: [**ffmpeg**](#ffm ### ๐Ÿ”’ Security - **Argon2id** password hashing (`t=2, m=65536, p=2`) -- **Security headers** โ€” CSP, HSTS (1 year + subdomains), and Permissions-Policy on all responses +- **Security headers** โ€” CSP (`script-src 'self'`, no `unsafe-inline`), HSTS (1 year + subdomains), and Permissions-Policy on all responses +- **Inline JS eliminated** โ€” all JavaScript extracted to external `.js` files; CSP fully enforced - **CSRF** โ€” double-submit cookie with constant-time token comparison (`subtle::ct_eq`) - `HttpOnly` + `SameSite=Strict` session cookies with configurable `Secure` flag and `Max-Age` - **Admin brute-force protection** โ€” progressive lockout after 5 consecutive failed login attempts +- **PoW nonce replay prevention** โ€” used nonces tracked in memory for the 5-minute validity window; stale entries auto-pruned - Raw IPs **never stored or logged** โ€” HMAC-keyed SHA-256 hash used everywhere -- Per-IP sliding-window rate limiting on both POST and GET endpoints -- **JPEG EXIF stripping** โ€” GPS, device IDs, and all metadata removed on upload +- Per-IP sliding-window rate limiting on POST endpoints (10/min) and page-load GET endpoints (60/min); `/api/` routes excluded from GET limiting +- **JPEG EXIF stripping + orientation correction** โ€” GPS, device IDs, and all metadata removed; rotation normalized on upload - All user input HTML-escaped before rendering; markup applied post-escape - **Zip-bomb protection** โ€” backup restore capped at 1 GiB per entry, 50,000 entries max +- **Backup upload size cap** โ€” full and board restore endpoints reject uploads over 512 MiB - **Redirect hardening** โ€” backslash and encoded variants blocked on redirect parameters - Path traversal prevention on all filesystem operations +- **Job queue back-pressure** โ€” queue capped at `job_queue_capacity` entries; excess jobs dropped with a log warning, never causing OOM @@ -115,8 +124,11 @@ Two external tools are supported as **optional enhancements**: [**ffmpeg**](#ffm ### ๐Ÿ—‚๏ธ Thread Lifecycle - **Thread archiving** โ€” overflow threads are archived (readable, locked, hidden from index) rather than deleted; configurable per board +- **Global `archive_before_prune` flag** โ€” ensures no thread is silently hard-deleted on any archiving-enabled instance, even if the individual board didn't opt in - **Archive page** โ€” `/{board}/archive` with thumbnails, reply counts, and pagination - **Thread auto-update** โ€” delta-compressed polling keeps reply counts, lock/sticky badges, and new posts in sync without full reloads +- **ETag / Conditional GET** โ€” board index and thread pages return `304 Not Modified` on cache hits; ETags included on all 200 responses +- **Response compression** โ€” gzip, Brotli, or zstd negotiated automatically via `Accept-Encoding` - **Floating new-reply pill** โ€” "+N new replies โ†“" notification; click to scroll, auto-dismisses after 30 seconds - **"(You)" tracking** โ€” posts you authored are marked with a `(You)` badge, persisted across refreshes - Per-board toggle between archive-on-overflow and permanent deletion @@ -127,9 +139,11 @@ Two external tools are supported as **optional enhancements**: [**ffmpeg**](#ffm ### ๐Ÿ“ฑ Mobile & UX - **Mobile reply drawer** โ€” floating action button slides up a full-width reply panel on small screens - **Cross-board hover previews** โ€” `>>>/board/123` links show a floating popup with client-side caching -- **Five built-in themes** โ€” user-selectable via a floating picker; persisted in `localStorage` with no flash +- **Six built-in themes** โ€” user-selectable via a floating picker; persisted in `localStorage` with no flash +- **Default theme** โ€” `default_theme` in `settings.toml` sets the server-side default for new visitors; also configurable from the admin panel +- **Site subtitle** โ€” `site_subtitle` in `settings.toml` customises the home page tagline at install time - **Live stats** โ€” total posts, uploads, and content size displayed on the home page -- **Background worker system** โ€” video transcoding, waveform generation, and thread cleanup run asynchronously without blocking requests +- **Background worker system** โ€” video transcoding, waveform generation, and thread cleanup run asynchronously; duplicate media jobs coalesced; configurable ffmpeg timeout; exponential backoff on retries - **Interactive keyboard console** โ€” `[s]` stats ยท `[l]` boards ยท `[c]` create ยท `[d]` delete ยท `[q]` quit @@ -157,7 +171,7 @@ When ffmpeg is available on `PATH`: - **Audio waveform thumbnails** via the `showwavespic` filter - **Video thumbnail extraction** from the first frame for catalog previews -Without ffmpeg, videos are served in their original format and audio posts use a generic icon. Set `require_ffmpeg = true` in `settings.toml` to enforce its presence at startup. +Without ffmpeg, videos are served in their original format and audio posts use a generic icon. Set `require_ffmpeg = true` in `settings.toml` to enforce its presence at startup. The ffmpeg execution timeout is configurable via `ffmpeg_timeout_secs` (default: 120). See **[SETUP.md โ€” Installing ffmpeg](SETUP.md#installing-ffmpeg)** for platform-specific instructions. @@ -214,7 +228,7 @@ rustchan-data/ โ”œโ”€โ”€ b/ โ”‚ โ”œโ”€โ”€ . โ† uploaded files โ”‚ โ””โ”€โ”€ thumbs/ - โ”‚ โ””โ”€โ”€ _thumb.jpg โ† auto-generated thumbnails + โ”‚ โ””โ”€โ”€ _thumb.jpg โ† auto-generated thumbnails & waveforms โ””โ”€โ”€ tech/ โ”œโ”€โ”€ . โ””โ”€โ”€ thumbs/ @@ -229,13 +243,18 @@ rustchan-data/ Auto-generated on first run. Edit and restart to apply. ```toml -# Site display name shown in the browser title and header. +# Site identity forum_name = "RustChan" +site_subtitle = "A self-hosted imageboard" + +# Default theme served to new visitors. Options: terminal, frutiger-aero, +# dorific-aero, fluorogrid, neoncubicle, chan-classic +default_theme = "terminal" # TCP port (binds to 0.0.0.0:). port = 8080 -# Upload size limits (MB). +# Upload size limits (MiB). max_image_size_mb = 8 max_video_size_mb = 50 max_audio_size_mb = 150 @@ -245,13 +264,43 @@ max_audio_size_mb = 150 cookie_secret = "" # Display .onion address if a Tor daemon is running. -enable_tor_support = true +enable_tor_support = false # Hard-exit if ffmpeg is not found (default: warn only). require_ffmpeg = false +# Maximum time (seconds) to allow a single ffmpeg job to run. +ffmpeg_timeout_secs = 120 + # WAL checkpoint interval in seconds (0 = disabled). wal_checkpoint_interval_secs = 3600 + +# Automatic VACUUM: compact the database this many hours after startup, +# then repeat on the same interval. Set to 0 to disable. +auto_vacuum_interval_hours = 24 + +# Expired poll vote cleanup interval (hours). Vote rows for expired polls +# are deleted; poll questions and options are preserved. +poll_cleanup_interval_hours = 72 + +# Show a red warning banner in the admin panel when the DB exceeds this size. +db_warn_threshold_mb = 2048 + +# Maximum number of pending background jobs. Excess jobs are dropped with +# a warning log rather than causing OOM under a post flood. +job_queue_capacity = 1000 + +# Maximum waveform/thumbnail cache size per board's thumbs/ directory (MiB). +# A background task evicts oldest files when the limit is exceeded. +waveform_cache_max_mb = 200 + +# Tokio blocking thread pool size. Defaults to logical_cpus ร— 4. +# Tune downward on memory-constrained hardware (e.g. Raspberry Pi). +# blocking_threads = 16 + +# Archive overflow threads globally before any hard-delete, even on boards +# where per-board archiving is disabled. +archive_before_prune = true ``` ### Environment Variables @@ -261,6 +310,8 @@ All settings can be overridden via environment variables, which take precedence | Variable | Default | Description | |---|---|---| | `CHAN_FORUM_NAME` | `RustChan` | Site display name | +| `CHAN_SITE_SUBTITLE` | *(from settings.toml)* | Home page subtitle | +| `CHAN_DEFAULT_THEME` | `terminal` | Default theme for new visitors | | `CHAN_PORT` | `8080` | TCP port | | `CHAN_BIND` | `0.0.0.0:8080` | Full bind address (overrides `CHAN_PORT`) | | `CHAN_DB` | `rustchan-data/chan.db` | SQLite database path | @@ -278,13 +329,22 @@ All settings can be overridden via environment variables, which take precedence | `CHAN_BEHIND_PROXY` | `false` | Trust `X-Forwarded-For` behind a reverse proxy | | `CHAN_HTTPS_COOKIES` | *(same as `CHAN_BEHIND_PROXY`)* | Set `Secure` flag on session cookies | | `CHAN_WAL_CHECKPOINT_SECS` | `3600` | WAL checkpoint interval; `0` to disable | +| `CHAN_AUTO_VACUUM_HOURS` | `24` | Scheduled VACUUM interval (hours); `0` to disable | +| `CHAN_POLL_CLEANUP_HOURS` | `72` | Expired poll vote cleanup interval (hours) | +| `CHAN_DB_WARN_MB` | `2048` | DB size warning threshold (MiB) | +| `CHAN_JOB_QUEUE_CAPACITY` | `1000` | Max pending background jobs | +| `CHAN_FFMPEG_TIMEOUT_SECS` | `120` | Max duration for a single ffmpeg job | +| `CHAN_WAVEFORM_CACHE_MB` | `200` | Max waveform thumbnail cache per board (MiB) | +| `CHAN_BLOCKING_THREADS` | `cpus ร— 4` | Tokio blocking thread pool size | +| `CHAN_ARCHIVE_BEFORE_PRUNE` | `true` | Archive globally before any hard-delete | +| `CHAN_TOR_HOSTNAME_FILE` | *(auto-detected)* | Override path to the Tor `hostname` file | | `RUST_LOG` | `rustchan-cli=info` | Log verbosity |
## ๐Ÿ’พ Backup & Restore -The entire backup system is accessible from the admin panel โ€” no shell access required. +The entire backup system is accessible from the admin panel โ€” no shell access required. All backup operations stream from disk in 64 KiB chunks; peak RAM overhead is roughly 64 KiB regardless of instance size. Backups are written to disk as temp files with an atomic rename on success, so partial backups never appear in the saved list. ### Full Site Backups @@ -295,7 +355,7 @@ A full backup is a `.zip` containing a consistent SQLite snapshot (via `VACUUM I | **๐Ÿ’พ Save** | Creates a backup and writes it to `rustchan-data/full-backups/` | | **โฌ‡ Download** | Streams a saved backup to your browser | | **โ†บ Restore (server)** | Restores from a file already on the server | -| **โ†บ Restore (upload)** | Restores from a `.zip` uploaded from your computer | +| **โ†บ Restore (upload)** | Restores from a `.zip` uploaded from your computer (max 512 MiB) | | **โœ• Delete** | Permanently removes the backup file | ### Per-Board Backups @@ -343,7 +403,7 @@ See **[SETUP.md](SETUP.md)** for a complete production guide covering: - **nginx** reverse proxy with TLS via Let's Encrypt - ffmpeg and Tor installation on Linux, macOS, and Windows - First-run configuration walkthrough -- Raspberry Pi SD card wear reduction +- Raspberry Pi SD card wear reduction and blocking thread tuning - Security hardening checklist ### Cross-Compilation @@ -370,14 +430,15 @@ RustChan is intentionally minimal โ€” no template engine, no ORM, no JavaScript | Layer | Technology | |---|---| | Web framework | [Axum](https://github.com/tokio-rs/axum) 0.8 | -| Async runtime | [Tokio](https://tokio.rs/) 1.x | -| Database | SQLite via [rusqlite](https://github.com/rusqlite/rusqlite) (bundled) | +| Async runtime | [Tokio](https://tokio.rs/) 1.x (manually sized blocking pool) | +| Database | SQLite via [rusqlite](https://github.com/rusqlite/rusqlite) (bundled, 32 MiB page cache) | | Connection pool | r2d2 + r2d2_sqlite (5-second acquisition timeout) | -| Image processing | [`image`](https://github.com/image-rs/image) crate | -| Video transcoding | ffmpeg (optional) | +| Image processing | [`image`](https://github.com/image-rs/image) crate + `kamadak-exif` for JPEG orientation | +| Video transcoding | ffmpeg (optional, configurable timeout) | | Audio waveforms | ffmpeg `showwavespic` filter (optional) | | Password hashing | `argon2` crate (Argon2id) | | Timing-safe comparison | `subtle` crate | +| Response compression | `tower-http` CompressionLayer (gzip, Brotli, zstd) | | HTML rendering | Plain Rust `format!` strings | | Configuration | `settings.toml` + env var overrides via `once_cell::Lazy` | | Logging | `tracing` + `tracing-subscriber` | @@ -388,18 +449,30 @@ RustChan is intentionally minimal โ€” no template engine, no ORM, no JavaScript src/ โ”œโ”€โ”€ main.rs โ€” entry point, router, background tasks, keyboard console โ”œโ”€โ”€ config.rs โ€” settings.toml + env var resolution -โ”œโ”€โ”€ db.rs โ€” all SQL queries (no ORM) โ”œโ”€โ”€ error.rs โ€” error handling and ban page rendering โ”œโ”€โ”€ models.rs โ€” database row structs โ”œโ”€โ”€ middleware/mod.rs โ€” rate limiting, CSRF, IP hashing, proxy trust +โ”œโ”€โ”€ workers/mod.rs โ€” background job queue, media transcoding, cache eviction โ”œโ”€โ”€ handlers/ โ”‚ โ”œโ”€โ”€ admin.rs โ€” admin panel, moderation, backup/restore, appeals โ”‚ โ”œโ”€โ”€ board.rs โ€” board index, catalog, archive, search, thread creation +โ”‚ โ”œโ”€โ”€ mod.rs โ€” streaming multipart, shared upload helpers โ”‚ โ””โ”€โ”€ thread.rs โ€” thread view, replies, polls, editing -โ”œโ”€โ”€ templates/mod.rs โ€” HTML generation (all themes, dynamic site name) +โ”œโ”€โ”€ db/ +โ”‚ โ”œโ”€โ”€ mod.rs โ€” connection pool, schema init, shared helpers +โ”‚ โ”œโ”€โ”€ boards.rs โ€” site settings, board CRUD, stats +โ”‚ โ”œโ”€โ”€ threads.rs โ€” thread listing, creation, mutation, archiving, pruning +โ”‚ โ”œโ”€โ”€ posts.rs โ€” post CRUD, file deduplication, polls, job queue +โ”‚ โ””โ”€โ”€ admin.rs โ€” sessions, bans, word filters, reports, mod log, appeals +โ”œโ”€โ”€ templates/ +โ”‚ โ”œโ”€โ”€ mod.rs โ€” base layout, pagination, timestamp formatting, utilities +โ”‚ โ”œโ”€โ”€ board.rs โ€” home page, board index, catalog, search, archive +โ”‚ โ”œโ”€โ”€ thread.rs โ€” thread view, post rendering, polls, edit form +โ”‚ โ”œโ”€โ”€ admin.rs โ€” login page, admin panel, mod log, VACUUM results, IP history +โ”‚ โ””โ”€โ”€ forms.rs โ€” new thread and reply forms โ””โ”€โ”€ utils/ โ”œโ”€โ”€ crypto.rs โ€” Argon2id, CSRF, sessions, IP hashing, PoW verification - โ”œโ”€โ”€ files.rs โ€” upload validation, thumbnails, EXIF stripping, waveforms + โ”œโ”€โ”€ files.rs โ€” upload validation, thumbnails, EXIF stripping + orientation โ”œโ”€โ”€ sanitize.rs โ€” HTML escaping, markup (greentext, spoilers, dice, embeds) โ””โ”€โ”€ tripcode.rs โ€” SHA-256 tripcode generation ``` @@ -414,19 +487,24 @@ src/ | **Brute-force** | Progressive lockout after 5 failed admin login attempts per IP | | **Sessions** | `HttpOnly`, `SameSite=Strict`, `Max-Age` aligned to server config | | **CSRF** | Double-submit cookie with constant-time token comparison (`subtle::ct_eq`) | -| **Security headers** | CSP (`self`-only scripts/styles/media), HSTS (1 year + subdomains), Permissions-Policy | +| **Security headers** | CSP (`script-src 'self'`, no `unsafe-inline`), HSTS (1 year + subdomains), Permissions-Policy | +| **Inline JavaScript** | Fully eliminated โ€” all JS in external files; CSP enforced with no `unsafe-inline` | | **IP privacy** | Raw IPs never stored or logged โ€” HMAC-keyed SHA-256 hash used everywhere | -| **Rate limiting** | Sliding-window per hashed IP on all POST endpoints (10/min) and GET endpoints (60/min) | +| **Rate limiting** | Sliding-window per hashed IP: POST endpoints (10/min), page-load GETs (60/min); `/api/` routes excluded | | **Proxy support** | All handlers use proxy-aware IP extraction when `CHAN_BEHIND_PROXY=true` | | **File safety** | Content-Type + magic byte validation; file extensions never trusted | -| **EXIF stripping** | All JPEG uploads re-encoded โ€” GPS, device IDs, and all metadata discarded | +| **EXIF stripping** | All JPEG uploads re-encoded โ€” GPS, device IDs, and all metadata discarded; EXIF orientation applied before strip | | **XSS** | All user input HTML-escaped before rendering; markup applied post-escape | | **Zip-bomb protection** | Backup restore capped at 1 GiB per entry, 50,000 entries max | +| **Backup upload cap** | Full and board restore endpoints reject uploads over 512 MiB | | **Redirect hardening** | Backslash and percent-encoded variants blocked on `return_to` parameters | | **Path traversal** | Backup filenames validated against `[a-zA-Z0-9._-]` before filesystem access | | **Body limits** | Per-route limits on small endpoints (64 KiB) to prevent memory exhaustion | | **Connection pool** | 5-second acquisition timeout prevents thread-pool exhaustion under load | -| **PoW CAPTCHA** | SHA-256 hashcash (20-bit difficulty), verified server-side with 5-minute grace window | +| **PoW CAPTCHA** | SHA-256 hashcash (20-bit difficulty), verified server-side with 5-minute grace window; covers threads and replies | +| **PoW nonce replay** | Used nonces tracked in memory; stale entries auto-pruned after the validity window expires | +| **Job queue** | Capped at `job_queue_capacity`; excess jobs logged and dropped, never causing OOM | +| **Streaming uploads** | Multipart fields validated against size limits in flight; memory use bounded regardless of payload |
@@ -448,7 +526,7 @@ __text__ italic ## ๐ŸŽจ Themes -Five built-in themes, selectable via the floating picker on every page. Persisted in `localStorage` with no flash on load. +Six built-in themes, selectable via the floating picker on every page. Persisted in `localStorage` with no flash on load. The site-wide default for new visitors is set via `default_theme` in `settings.toml` or from the admin panel. | Theme | Description | |---|---| @@ -457,6 +535,7 @@ Five built-in themes, selectable via the floating picker on every page. Persiste | **DORFic Aero** | Dark stone walls, torchlit amber/copper glass panels | | **FluoroGrid** | Pale sage, muted teal grid lines, dusty lavender panels | | **NeonCubicle** | Cool off-white, horizontal scanlines, steel-teal borders | +| **ChanClassic** | Light tan/beige background, maroon accents, blue post-number links โ€” classic imageboard styling |
@@ -464,8 +543,12 @@ Five built-in themes, selectable via the floating picker on every page. Persiste See **[CHANGELOG.md](CHANGELOG.md)** for the full version history. -**Latest โ€” v1.0.11:** -Security headers (CSP, HSTS, Permissions-Policy) ยท proxy-aware IP extraction on all handlers ยท GET rate limiting (60 req/min) ยท zip-bomb protection on restore ยท IP hashing everywhere ยท admin brute-force lockout ยท constant-time CSRF comparison ยท poll input caps ยท session cookie `Max-Age` ยท connection pool timeout ยท per-route body limits ยท open redirect hardening ยท worker exponential backoff ยท file dedup race fix ยท per-post ban+delete ยท ban appeal system ยท PoW CAPTCHA ยท video embeds ยท cross-board hover previews ยท new-reply pill ยท live thread metadata ยท "(You)" tracking +**Latest โ€” v1.0.13:** +Scheduled VACUUM ยท expired poll vote cleanup ยท DB size warning banner ยท job queue back-pressure ยท duplicate media job coalescing ยท configurable ffmpeg timeout ยท global `archive_before_prune` flag ยท waveform cache eviction ยท streaming multipart ยท ETag / Conditional GET (304) ยท gzip/Brotli/zstd response compression ยท manual Tokio blocking pool sizing ยท EXIF orientation correction ยท streaming backup I/O (peak RAM ~64 KiB) ยท **ChanClassic** theme ยท `default_theme` + `site_subtitle` in `settings.toml` ยท default theme selector in admin panel ยท admin panel reorganised ยท prepared statement caching audit ยท `RETURNING` clause for inserts ยท 32 MiB SQLite page cache ยท two new DB indexes (`idx_posts_thread_id`, `idx_posts_ip_hash`) + +**v1.0.12:** Database module split into 5 focused files ยท template module split into 5 focused files ยท PoW bypass on replies fixed (critical) ยท PoW nonce replay protection ยท inline JS fully eliminated (`script-src 'self'` CSP) ยท backup upload size cap (512 MiB) ยท post rate limiting simplified ยท `/api/` routes excluded from GET rate limit ยท trailing slash 404s fixed + +**v1.0.11:** Security headers (CSP, HSTS, Permissions-Policy) ยท proxy-aware IP extraction on all handlers ยท GET rate limiting (60 req/min) ยท zip-bomb protection on restore ยท IP hashing everywhere ยท admin brute-force lockout ยท constant-time CSRF comparison ยท poll input caps ยท session cookie `Max-Age` ยท connection pool timeout ยท per-route body limits ยท open redirect hardening ยท worker exponential backoff ยท file dedup race fix ยท per-post ban+delete ยท ban appeal system ยท PoW CAPTCHA ยท video embeds ยท cross-board hover previews ยท new-reply pill ยท live thread metadata ยท "(You)" tracking ยท spoiler text **v1.0.9:** Per-board editing toggle ยท configurable edit window ยท per-board archive toggle ยท AV1โ†’VP9 transcoding fix @@ -487,4 +570,4 @@ Built with ๐Ÿฆ€ Rust  ยท  Powered by SQLite  ยท  Optional in *Drop it anywhere. It just runs.* - \ No newline at end of file + diff --git a/SETUP.md b/SETUP.md index 0207ca2..167a0db 100644 --- a/SETUP.md +++ b/SETUP.md @@ -87,7 +87,7 @@ winget install --id Gyan.FFmpeg -e Verify: `ffmpeg -version` -Set `require_ffmpeg = true` in `settings.toml` if you want startup to fail when ffmpeg is missing. +Set `require_ffmpeg = true` in `settings.toml` if you want startup to fail when ffmpeg is missing. The maximum time any single ffmpeg job may run is controlled by `ffmpeg_timeout_secs` (default: 120); increase it for large video files on slow hardware. --- @@ -165,7 +165,7 @@ sudo chown -R chan:chan /var/lib/chan/static openssl rand -hex 32 ``` -Save this value โ€” it's used for CSRF tokens and IP hashing. **Do not change it after your instance has posts.** +Save this value โ€” it's used for CSRF tokens and IP hashing. **Do not change it after your instance has posts.** Changing it invalidates all existing bans, IP history, and session cookies. ### systemd (Linux) @@ -218,14 +218,40 @@ sudo -u chan nano /var/lib/chan/rustchan-data/settings.toml Key settings: ```toml +# Site identity forum_name = "My Chan" +site_subtitle = "A self-hosted imageboard" + +# Default theme for new visitors. +# Options: terminal, frutiger-aero, dorific-aero, fluorogrid, neoncubicle, chan-classic +default_theme = "terminal" + +# Network port = 8080 + +# Upload size limits (MiB) max_image_size_mb = 8 max_video_size_mb = 50 max_audio_size_mb = 150 + +# Optional integrations enable_tor_support = false require_ffmpeg = false +ffmpeg_timeout_secs = 120 + +# Database maintenance wal_checkpoint_interval_secs = 3600 +auto_vacuum_interval_hours = 24 +poll_cleanup_interval_hours = 72 +db_warn_threshold_mb = 2048 + +# Background worker tuning +job_queue_capacity = 1000 +waveform_cache_max_mb = 200 +archive_before_prune = true + +# Uncomment to tune the blocking thread pool (default: logical_cpus ร— 4) +# blocking_threads = 8 ``` Restart after editing: `sudo systemctl restart rustchan-cli` @@ -296,7 +322,7 @@ sudo systemctl edit rustchan-cli Environment=CHAN_BEHIND_PROXY=true ``` -This tells RustChan to trust `X-Forwarded-For` and automatically sets `Secure` on all cookies. Restart to apply. +This tells RustChan to trust `X-Forwarded-For` and automatically sets `Secure` on all cookies. Without this flag, bans and rate limits will not function correctly behind nginx. Restart to apply. ### Firewall @@ -376,6 +402,8 @@ All settings can be overridden via environment variables (take precedence over ` | Variable | Default | Description | |---|---|---| | `CHAN_FORUM_NAME` | `RustChan` | Site display name | +| `CHAN_SITE_SUBTITLE` | *(from settings.toml)* | Home page subtitle | +| `CHAN_DEFAULT_THEME` | `terminal` | Default theme for new visitors (`terminal`, `frutiger-aero`, `dorific-aero`, `fluorogrid`, `neoncubicle`, `chan-classic`) | | `CHAN_PORT` | `8080` | TCP port | | `CHAN_BIND` | `0.0.0.0:8080` | Full bind address (overrides port) | | `CHAN_DB` | `rustchan-data/chan.db` | Database path | @@ -393,25 +421,38 @@ All settings can be overridden via environment variables (take precedence over ` | `CHAN_BEHIND_PROXY` | `false` | Trust `X-Forwarded-For` | | `CHAN_HTTPS_COOKIES` | *(mirrors proxy setting)* | Force `Secure` cookies | | `CHAN_WAL_CHECKPOINT_SECS` | `3600` | WAL checkpoint interval; `0` to disable | +| `CHAN_AUTO_VACUUM_HOURS` | `24` | Scheduled VACUUM interval (hours); `0` to disable | +| `CHAN_POLL_CLEANUP_HOURS` | `72` | Expired poll vote cleanup interval (hours) | +| `CHAN_DB_WARN_MB` | `2048` | DB file size threshold for admin panel warning (MiB) | +| `CHAN_JOB_QUEUE_CAPACITY` | `1000` | Max pending background jobs; excess dropped with a warning | +| `CHAN_FFMPEG_TIMEOUT_SECS` | `120` | Max duration for a single ffmpeg job (seconds) | +| `CHAN_WAVEFORM_CACHE_MB` | `200` | Max waveform/thumbnail cache per board's `thumbs/` directory (MiB) | +| `CHAN_BLOCKING_THREADS` | `cpus ร— 4` | Tokio blocking thread pool size (tune down on RAM-constrained hardware) | +| `CHAN_ARCHIVE_BEFORE_PRUNE` | `true` | Archive globally before any hard-delete, even on boards without per-board archiving | +| `CHAN_TOR_HOSTNAME_FILE` | *(auto-detected)* | Override path to Tor `hostname` file | | `RUST_LOG` | `rustchan-cli=info` | Log verbosity | --- ## Admin Panel -Log in at `/admin`. All moderation is available from the web panel. +Log in at `/admin`. All moderation and configuration is available from the web panel. The panel is organised in the following order: + +**Site Settings** โ†’ **Boards** โ†’ **Moderation Log** โ†’ **Report Inbox** โ†’ **Moderation** (ban appeals, active bans, word filters) โ†’ **Full Site Backup & Restore** โ†’ **Board Backup & Restore** โ†’ **Database Maintenance** โ†’ **Active Onion Address** ### Key Features +- **Site Settings** โ€” update forum name, subtitle, and default theme without restarting - **Board settings** โ€” click any board to edit its name, limits, and feature toggles (video embeds, PoW CAPTCHA, editing, archiving) without restarting - **Ban + Delete** โ€” every post shows a โ›” button in admin view; one click to ban the IP hash and delete the post -- **Ban appeals** โ€” banned users can submit appeals; review them under the **ban appeals** section with dismiss or accept+unban +- **Ban appeals** โ€” banned users can submit appeals; review them under the **Moderation** section with dismiss or accept+unban - **IP history** โ€” click ๐Ÿ” on any post to see all posts from that IP hash across all boards - **Reports** โ€” user-submitted reports appear in the report inbox with resolve and resolve+ban actions - **Moderation log** โ€” append-only audit trail of all admin actions - **Word filters** โ€” plain-text substring match with optional replacement -- **Database maintenance** โ€” one-click VACUUM with before/after size display -- **Backups** โ€” full and per-board backup/restore directly from the panel +- **Database maintenance** โ€” one-click VACUUM with before/after size display; red warning banner when the database exceeds `db_warn_threshold_mb` +- **Full Site Backup & Restore** โ€” streaming backup creation and restore; no RAM buffering regardless of instance size +- **Board Backup & Restore** โ€” self-contained per-board backup and restore --- @@ -421,13 +462,15 @@ Log in at `/admin`. All moderation is available from the web panel. All backup operations are available in the admin panel โ€” no shell access needed. -**Full backups** include a consistent database snapshot and all uploaded files. +**Full Site Backups** include a consistent database snapshot and all uploaded files. All I/O streams in 64 KiB chunks so peak RAM overhead is roughly 64 KiB regardless of instance size. Backups are written as temp files with an atomic rename, so partial backups never appear in the saved list. + **Board backups** are self-contained (manifest + uploads) and can move a single board between instances. Restore behaviour: - Existing board โ†’ content wiped and replaced - Missing board โ†’ created from scratch - Row IDs are remapped to prevent collisions +- Restore uploads are capped at 512 MiB ### Automated Shell Backups @@ -454,6 +497,8 @@ sudo systemctl start rustchan-cli ### Move Database to USB Storage +Storing the database on a USB drive rather than the SD card extends card life significantly and improves write throughput. + ```bash sudo mkfs.ext4 /dev/sda1 sudo mkdir -p /mnt/rustchan-data @@ -480,6 +525,27 @@ Add to `/etc/fstab` for persistence. - Set journal storage to volatile: `Storage=volatile` in `/etc/systemd/journald.conf` - Add `noatime` to root partition mount options in `/etc/fstab` +### Tune the Blocking Thread Pool + +The default blocking thread pool (`logical_cpus ร— 4`) can exhaust RAM on a Raspberry Pi 4 under heavy transcoding load. Set a lower ceiling: + +```ini +[Service] +Environment=CHAN_BLOCKING_THREADS=8 +``` + +Adjust based on available RAM and expected concurrent uploads. 8 is a reasonable starting point for a Pi 4 with 4 GiB RAM. + +### Waveform Cache + +With limited storage on SD cards, consider lowering `waveform_cache_max_mb`: + +```toml +waveform_cache_max_mb = 50 +``` + +The background eviction task will keep the `thumbs/` directories under this limit automatically. + --- ## Security Checklist @@ -487,17 +553,19 @@ Add to `/etc/fstab` for persistence. Before exposing your instance to the internet: - [ ] `CHAN_COOKIE_SECRET` set to a unique value (`openssl rand -hex 32`) -- [ ] Default admin password changed +- [ ] Default admin password changed immediately after first login - [ ] Running as `chan` user, not root -- [ ] Port 8080 firewalled from external access -- [ ] nginx configured with HTTPS -- [ ] `CHAN_BEHIND_PROXY=true` set +- [ ] Port 8080 firewalled from external access (`ufw deny 8080/tcp`) +- [ ] nginx configured with HTTPS (Let's Encrypt) +- [ ] `CHAN_BEHIND_PROXY=true` set โ€” required for bans and rate limits to work with nginx - [ ] `client_max_body_size` in nginx matches your video size limit - [ ] Rate limits tuned for your audience - [ ] Automated backups scheduled and restore tested -- [ ] systemd hardening directives active +- [ ] systemd hardening directives active (`NoNewPrivileges`, `PrivateTmp`, `ProtectSystem=strict`) - [ ] Tor hidden service directory owned by `tor` user with mode `700` (if applicable) -- [ ] Log monitoring in place +- [ ] `db_warn_threshold_mb` set to a value appropriate for your disk (default: 2048 MiB) +- [ ] `auto_vacuum_interval_hours` enabled (default: 24) to prevent unbounded DB growth +- [ ] Log monitoring in place (`journalctl -u rustchan-cli -f`) --- @@ -522,7 +590,7 @@ sudo systemctl start rustchan-cli sudo journalctl -u rustchan-cli -n 20 ``` -Database migrations are automatic โ€” no manual SQL needed when upgrading. +Database migrations run automatically on startup โ€” no manual SQL is ever needed when upgrading. --- @@ -538,25 +606,34 @@ Common causes: path doesn't exist or wrong ownership, port already in use, wrong ```bash which ffmpeg && ffmpeg -version ``` -If installed to a custom path, add it to PATH in the systemd override. +If installed to a custom path, add it to PATH in the systemd override. If large videos time out during transcoding, increase `ffmpeg_timeout_secs` in `settings.toml`. **Tor address not showing:** 1. Verify Tor is running: `sudo systemctl status tor` 2. Check hostname file exists: `sudo cat /var/lib/tor/rustchan/hostname` 3. Verify `enable_tor_support = true` in `settings.toml` 4. Restart RustChan +5. If the hostname file is in a non-standard location, set `CHAN_TOR_HOSTNAME_FILE` **Uploads failing:** ```bash ls -la /var/lib/chan/rustchan-data/boards/ # check ownership sudo nginx -T | grep client_max_body_size # check nginx limit ``` +Large uploads rejected mid-stream indicate the nginx `client_max_body_size` is lower than RustChan's configured limit. **Admin login fails:** ```bash sudo -u chan CHAN_DB=/var/lib/chan/rustchan-data/chan.db \ /usr/local/bin/rustchan-cli admin reset-password admin "NewPassword!" ``` +If the login page shows a lockout message, wait for the progressive delay to expire (up to a few minutes) before retrying. + +**Bans and rate limits not working:** +Ensure `CHAN_BEHIND_PROXY=true` is set in the systemd override. Without it, RustChan sees all requests as coming from `127.0.0.1`. + +**Background jobs not processing:** +Check `RUST_LOG=rustchan-cli=debug` output for job queue warnings. If jobs are being dropped with "queue at capacity", increase `job_queue_capacity`. If ffmpeg jobs are timing out, increase `ffmpeg_timeout_secs`. **Database integrity:** ```bash @@ -564,4 +641,7 @@ sqlite3 /var/lib/chan/rustchan-data/chan.db "PRAGMA integrity_check;" # Expected: ok ``` -**Memory usage:** Typical idle footprint is 30โ€“60 MiB. Connection pool under load uses ~32 MiB. Image processing may spike to ~64 MiB temporarily. Well within Raspberry Pi 4 limits. \ No newline at end of file +**DB growing unboundedly:** +Verify `auto_vacuum_interval_hours` is non-zero and check the admin panel Database Maintenance section. If the DB exceeds `db_warn_threshold_mb` a red banner will appear. Run a manual VACUUM from the panel after bulk deletions. + +**Memory usage:** Typical idle footprint is 30โ€“60 MiB. Connection pool under load uses ~32 MiB. Image processing may spike to ~64 MiB temporarily. Backup I/O peaks at ~64 KiB regardless of backup size. Well within Raspberry Pi 4 limits when `blocking_threads` is tuned appropriately. diff --git a/docs/build_all_rustchan.sh b/docs/build_all_rustchan.sh new file mode 100755 index 0000000..6e442d8 --- /dev/null +++ b/docs/build_all_rustchan.sh @@ -0,0 +1,142 @@ +#!/bin/bash +set -e + +######################################## +# CONFIG +######################################## + +APP_NAME="rustchan-cli" +VERSION=$(grep '^version' Cargo.toml | head -1 | cut -d '"' -f2) + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +BUILD_DIR="$SCRIPT_DIR/target/release-bundles" +STAGING="$BUILD_DIR/staging" +DOWNLOADS="$HOME/Downloads" + +mkdir -p "$BUILD_DIR" +rm -rf "$STAGING" +mkdir -p "$STAGING" + +echo "========================================" +echo "Building $APP_NAME v$VERSION" +echo "========================================" + +######################################## +# Build macOS +######################################## + +build_macos() { + ARCH=$1 + TARGET="${ARCH}-apple-darwin" + BINNAME="${APP_NAME}-macos" + ARCHIVE="${APP_NAME}-macos-v${VERSION}-${ARCH}.tar.gz" + + echo "Building macOS $ARCH..." + + cargo build --release --target "$TARGET" -j 2 + + STAGE="$STAGING/macos-$ARCH" + mkdir -p "$STAGE" + + cp "target/${TARGET}/release/${APP_NAME}" "$STAGE/$BINNAME" + + strip "$STAGE/$BINNAME" 2>/dev/null || true + + tar -czf "$BUILD_DIR/$ARCHIVE" -C "$STAGE" "$BINNAME" +} + +######################################## +# Build Linux +######################################## + +build_linux() { + ARCH=$1 + TARGET="${ARCH}-unknown-linux-musl" + BINNAME="${APP_NAME}-linux" + ARCHIVE="${APP_NAME}-linux-v${VERSION}-${ARCH}.tar.gz" + + echo "Building Linux $ARCH..." + + cargo zigbuild --release --target "$TARGET" -j 2 + + STAGE="$STAGING/linux-$ARCH" + mkdir -p "$STAGE" + + cp "target/${TARGET}/release/${APP_NAME}" "$STAGE/$BINNAME" + + strip "$STAGE/$BINNAME" 2>/dev/null || true + + tar -czf "$BUILD_DIR/$ARCHIVE" -C "$STAGE" "$BINNAME" +} + +######################################## +# Build Windows +######################################## + +build_windows() { + + ARCH="x86_64" + TARGET="x86_64-pc-windows-gnu" + BINNAME="${APP_NAME}-windows.exe" + ARCHIVE="${APP_NAME}-windows-v${VERSION}-${ARCH}.zip" + + echo "Building Windows..." + + cargo build --release --target "$TARGET" -j 2 + + STAGE="$STAGING/windows-$ARCH" + mkdir -p "$STAGE" + + cp "target/${TARGET}/release/${APP_NAME}.exe" "$STAGE/$BINNAME" + + cd "$STAGE" + + zip "$BUILD_DIR/$ARCHIVE" "$BINNAME" + + cd "$SCRIPT_DIR" +} + +######################################## +# Run Builds +######################################## + +build_macos aarch64 +build_macos x86_64 +build_linux x86_64 +build_linux aarch64 +build_windows + +######################################## +# Checksums +######################################## + +echo "Generating checksums..." + +cd "$BUILD_DIR" + +FILES=$(ls *.tar.gz *.zip 2>/dev/null) + +if [ -z "$FILES" ]; then + echo "Error: No release files found" + exit 1 +fi + +shasum -a 256 *.tar.gz *.zip > "${APP_NAME}-v${VERSION}-SHA256SUMS.txt" + +cd "$SCRIPT_DIR" + +######################################## +# Copy to Downloads +######################################## + +echo "Copying artifacts to Downloads..." + +cp "$BUILD_DIR"/*.tar.gz "$BUILD_DIR"/*.zip "$BUILD_DIR"/*.txt "$DOWNLOADS/" 2>/dev/null || true + +echo "========================================" +echo "Build complete!" +echo "Artifacts available in:" +echo "$BUILD_DIR" +echo "and copied to:" +echo "$DOWNLOADS" +echo "========================================" \ No newline at end of file diff --git a/clippy.sh b/docs/clippy.sh similarity index 100% rename from clippy.sh rename to docs/clippy.sh