Releases: csd113/RustChan
Release v1.1.4
[1.1.4]
Added
- Full banner management in the admin panel: operators can upload, preview, reorder, edit, and delete global board banners, per-board banner overrides, and a separate home-page MOTD/news banner.
- Global board-banner rotation with two modes: rotate on each refresh by default, or enforce a site-wide time-based rotation interval in minutes.
- Per-board banner behavior modes that mirror the favicon-style override model: each board can inherit the global banner pool, disable banners entirely, or use one fixed board-specific override.
- Clickable banner destinations for internal boards and internal paths, plus optional external banner links guarded by an on-site warning/interstitial page before redirecting users away from RustChan.
- The admin quick-create board form now includes an audio-upload toggle, so new boards can be created with audio enabled directly from the UI instead of only through later edits or the CLI.
Improved
- Board-page presentation is more intentional: centered banners now render under the board title/description, above the board nav on index pages, and above catalog controls on catalog pages.
- Home page announcement tooling is stronger through a dedicated banner box that is separate from board-header banners and suitable for MOTD, maintenance, or news updates.
- Banner uploads now follow RustChan's media pipeline expectations by validating the exact
468x60aspect ratio, documenting a minimum468x60/ recommended936x120workflow, and normalizing uploads to WebP. - Full-site and board-level restore compatibility now covers the new banner metadata and asset layout so banner configuration survives backup workflows.
Documentation
README.mdandSETUP.mdnow document the new banner system, placement rules, link behavior, and the exact artwork requirements for banner uploads.
Release v1.1.3
[1.1.3]
Added
- Automated saved full-site backups with admin-configurable cadence and retention: the full backup panel and
settings.tomlnow expose how many hours to wait between runs and how many saved full backups to keep, with automated runs pruning the oldest saved full backups after each new server-side full backup completes. - Per-board password protection with two modes: boards can now require a password to view the board at all or stay publicly readable while requiring the board password for posting, with admin controls for saving/clearing board passwords, unlock flows for users, and server-side enforcement across board pages, thread views, replies, edits, votes, media, and post-preview endpoints.
Improved
- Homepage board cards and board catalog thread cards now keep a more consistent square visual rhythm: the main content rail is wider on desktop, homepage NSFW badges sit beside board IDs for faster scanning, and catalog size toggles once again distinguish compact and large thread cards while preserving more uniform tile heights.
- HTTP timeout handling is now more robust across the full request pipeline:
GETandHEADrequests keep the fast 30-second cutoff, while slower write paths such as uploads, restores, and adminPOSTs are now covered by a longer request timeout instead of bypassing timeout protection entirely. - Proxy-aware HTTPS detection is now stricter and operator-configurable:
X-Forwarded-*headers are trusted only from explicitly allowed proxy CIDRs, with loopback remaining the safe default. - Admin session cookie issuance is now wired through real connection metadata on login and restore flows, eliminating header-only protocol trust and keeping direct-access and proxied deployments aligned.
- HTTP to HTTPS redirects are now more robust on manual-certificate deployments bound to wildcard addresses, with explicit public-host configuration for production domains that are not discoverable from the local bind address.
- The shared site footer now stays pinned to the bottom of the viewport through a dedicated fixed-footer layout, while preserving the original homepage card grid and overall 1.1.2-style page flow.
- Theme CSS internals are cleaner and safer to maintain: the fixed footer now uses one shared height variable with safe-area-aware body padding, Frutiger Aero and NeonCubicle now share one glass-pill navigation implementation, and the Forest theme now centralizes repeated surface, link, button, and input colors behind theme-scoped variables.
- Mobile header polish is tighter on board pages: the search bar now stretches to the same visual rails as the Home and Boards controls instead of ending short on narrow screens.
- The theme picker now lives in a footer-docked control bar on both desktop and mobile, giving theme switching one consistent home and keeping it from floating over page content.
- Backup and media-processing observability are stronger: posts now expose pending and failed async media state,
/readyzand/metricsreport media backlog, backup freshness, and maintenance activity, and the admin panel surfaces backup verification health instead of assuming saved ZIPs are restorable. - Heavy admin maintenance now coordinates through a shared maintenance gate and less aggressive background scheduling, so backups, restores, integrity checks, repair, and scheduled
VACUUM/WAL work are less likely to pile onto live request traffic or each other. - Full backup recovery is now more flexible without adding scheduler clutter: new full backups record the boards they contain, and the admin panel can derive a single-board restore or downloadable board backup directly from a saved full-site archive.
- Long media filenames now keep post layouts tidier without hiding the real upload name: thread and reply views truncate only the displayed stem, preserve the extension in the visible link text, and still expose the full original filename through the link tooltip.
- Upload-backed posting and admin restore flows now use explicit XHR redirect/error responses instead of scraping returned HTML, so media uploads fail in-place with clearer feedback and restore uploads stay inside the existing progress modal without fragile document replacement.
- Thread pages now separate board-level navigation from thread-specific actions more cleanly: board links live in the shared board-nav strip, reply/update controls stay in the thread nav, and the admin toolbar sits under the board context instead of leading the page.
- Admin board management is now organized around distinct tasks instead of one dense block: each board card separates basic setup, access controls, post features, appearance, backups, and destructive actions, while the full-site and board-backup areas now split scheduling, immediate restore/create actions, and saved archives into clearer sections.
- Handled XHR validation and restore failures are now transported without browser-level network noise: inline upload and restore errors return structured JSON that preserves the original semantic status in
X-Rustchan-Error-Status, letting RustChan keep the same in-place error UX without Chromium surfacing expected invalid-request checks as consoleFailed to load resourceerrors. - The admin panel now better preserves operator context during repeated maintenance work: backup/archive dropdowns remember their open state, board/settings forms restore more of their previous inputs after validation failures, and moderation copy/actions are more compact and easier to scan.
- The terminal dashboard now surfaces active FFmpeg video jobs directly in the TUI, making it easier to spot live transcode backlog without leaving the server console.
- VP9 transcode settings are now auto-tuned per host architecture and CPU capability: RustChan picks more appropriate
libvpx-vp9threading, tiling, andcpu-usedsettings on AVX512, AVX2, AVX, SSE4.1, ARM, and generic targets instead of using one static profile everywhere. - Release engineering is more automated and portable: tagged builds now publish GitHub Releases through Actions, attach per-platform ZIP archives with bundled
README/LICENSE, and generate verifiedSHA256SUMSmanifests for release downloads. - CI and release automation now track newer dependency and action versions, including the move to
reqwest 0.13, newerrustls-acme, refreshed Windows support crates, and updated GitHub Actions checkout/artifact/release steps.
Fixed
- Per-board password protection now fails closed more reliably: invalid or partial access-mode data from backups is rejected or forced into a locked state instead of silently becoming public, password-gated pages now return consistent
403/429responses with no-cache headers, and repeated board-unlock failures are temporarily throttled to make online guessing harder. - Requests coming directly from untrusted public peers can no longer spoof
X-Forwarded-Prototo make the app believe they arrived over HTTPS. - Built-in self-signed TLS recovery is now resilient to partially missing or corrupted dev-cert files: if the stored cert/key pair cannot be reused, RustChan regenerates a fresh pair instead of failing startup outright.
- Timeout coverage no longer leaves upload-heavy and admin mutation endpoints outside the request-timeout middleware.
- Mobile layout resilience is stronger across the updated style system: the header board menu now follows the real wrapped header height instead of a fixed offset, admin board-settings forms collapse cleanly to one column on narrow screens, and wide admin tables stay usable on phones through horizontal scrolling.
- The admin panel is now substantially more mobile-friendly: dropdown headings wrap instead of running offscreen, board action controls stack cleanly on narrow screens, create-board and moderation forms fit the viewport, and the heaviest admin tables no longer force excessive horizontal overflow.
- Admin login is now more robust on plain
http://deployments and local-network mobile access: insecure login redirects can recover through a short-lived bootstrap handoff instead of failing when the browser drops the freshly issued admin session cookie before/admin/panelloads. - Admin login no longer fails with a
403after the CSS refactor on plainhttp://deployments: the login page now reissues its CSRF cookie using the real request scheme so browsers do not drop the cookie before/admin/loginis processed. - Mobile media expansion behaves more predictably: tapping a video thumbnail now keeps playback inline on the page instead of collapsing back or jumping toward fullscreen, the filename remains the explicit open-in-new-tab path for fullscreen viewing, and image/video close buttons now use a smaller control footprint.
- Mobile image and video viewing now matches desktop more closely: the old floating media viewer has been removed, images and videos expand inline on the page with the same close-button flow as desktop, and the blue double-arrow/expand overlay is no longer shown over media on touch layouts.
- Desktop and mobile audio MiniPlayers now use the attached post image as album art for image+audio combo posts, while audio-only posts continue falling back to the current favicon artwork.
- Duplicate threads and replies are now prevented on unstable connections: post forms carry a per-render submission token, successful submissions are recorded server-side, and a retried POST now redirects back to the already-created post instead of inserting a second copy when the first response was lost in transit.
- Board search no longer fails when the FTS join exposes duplicate column names, and search queries are now normalized consistently so lowercase searches such as
aialso match uppercase post text likeAI. - Background media processing now degrades more honestly under pressure: queue-capacity drops and permanent worker failures are persist...
v1.1.2
[1.1.2]
Added
- Shared board ordering controls, backed by a persistent
display_orderfield, so admins can reorder boards once and see the same order reflected across the homepage, top header board list, and admin panel. - Live upload progress bars for post media uploads and admin restore uploads, covering image/video/audio post forms plus full-site and per-board backup restore uploads from local files.
- Modular theme infrastructure backed by a runtime theme registry, database-managed theme records, dynamic
/theme-css/{theme}delivery, and board-level default theme support so built-in and custom themes can be managed through one system. - Admin theme management for enabling and disabling built-in themes, creating custom themes, editing custom theme metadata and CSS, deleting custom themes, and choosing both site-wide and per-board default themes.
Improved
- Runtime data layout is now tidier under
rustchan-data/, with backups grouped intobackups/fullandbackups/boards, and generated operational state grouped underruntime/for Tor, TLS, favicon assets, and temporary admin files. - Homepage admin board reordering is now available through a subtler per-card toggle instead of always-visible controls, keeping the feature accessible without cluttering the board list.
- Board navigation and admin ordering now split SFW and NSFW boards into separate groups, with independent per-group move controls and safer reordering when a board is retagged between normal and NSFW.
- Post headers now render subjects inline ahead of poster names, with theme-appropriate subject colors and separators so titles remain distinct from usernames across Terminal, DORFic, ChanClassic, Frutiger Aero, FluoroGrid, and NeonCubicle.
- Theme presentation is more polished through reordered theme-picker menus, softer ChanClassic header link contrast, and rounder shared controls in Frutiger Aero and NeonCubicle so top-level navigation matches those themes' bubbly styling better.
- Theme resolution, rendering, and picker behavior are now centralized around the live theme registry, so normal pages, admin pages, ban pages, JS bootstrap, no-JS fallbacks, startup seeding, and runtime cache refreshes all follow the same precedence rules.
- Theme picker and admin theme controls are now fully data-driven, so adding, renaming, disabling, or reordering themes no longer requires parallel hardcoded edits across Rust templates, handlers, and client JavaScript.
- Theme-related admin and test internals are leaner through one shared admin dashboard snapshot loader, one shared live-theme synchronization path, a unified CSS response path for built-in and custom themes, shared CSRF jar-check handling, and a reusable
Boardtest fixture. - Admin theme management is cleaner and easier to use through a redesigned themes panel layout, separate built-in and custom theme sections, clearer built-in/custom editing affordances, and a documented custom-theme starter scaffold that explains RustChan's scoped theme variables and common override selectors.
- Catalog page presentation is cleaner through centered sort/display selectors and larger board-description text on both board headers and homepage board cards.
- The admin site-settings layout is tidier, with the save button aligned into the form action row instead of floating awkwardly above the global favicon controls.
- Database maintenance is more user-friendly through a clearer integrity/repair results page and deeper admin repair tooling that now rebuilds SQLite indexes plus the
posts_ftssearch table and triggers instead of only reporting a bare integrity status.
Fixed
- Existing installs now migrate old runtime folders automatically at startup, so prior
full-backups,board-backups,arti_state,arti_cache,tls,favicon, and temp backup-download directories continue working under the new layout without manual moves. - Backup, Tor, TLS, favicon, admin UI, and documentation paths now consistently point at the reorganized filesystem structure instead of the older scattered folder names.
- Admin-panel live access addresses now wrap correctly on mobile instead of overflowing offscreen, and the console live-log renderer now avoids panic-prone slicing flagged by strict Clippy.
- The long-greentext collapse toggle now works as a true per-board setting instead of a global site-wide flag, with migration/backfill support for existing installs and backup/restore compatibility for the new board field.
- Client-side auto-compress is safer for oversized media: animated images are no longer silently flattened, transparent images avoid destructive JPEG fallback when the browser cannot preserve alpha, and video re-encoding now has stronger cleanup and timeout handling so the modal is less likely to get stuck.
- Board search no longer crashes on punctuation-heavy input such as
',", or>>1; the search layer now normalizes free-form input into FTS-safe terms and returns ordinary empty results when nothing usable remains. - Spoilers on legacy posts now keep working under the stricter CSP by upgrading older inline-click spoiler markup to the shared delegated
data-actionhandler at runtime. - Board backup restore now preserves archived-thread state, so threads that were already in a board archive stay archived after restore instead of being pulled back onto the live board index.
- Admin board delete and board restore now surface SQLite corruption failures more clearly, and the new integrity/repair tools are wired into the admin maintenance flow to help diagnose FTS/index corruption before destructive operations.
- Theme validation drift is eliminated: duplicated hardcoded theme lists, mismatched validators, and stale per-layer defaults were replaced with registry-backed validation and one canonical fallback path.
- Renaming or deleting custom themes now updates dependent site and board defaults safely instead of leaving stale references behind, and saved cookie or localStorage themes now fall back cleanly when a theme is disabled or removed.
v1.1.1 Polish and Usability Enhancement
[1.1.1]
Added
- Mobile-only board picker in the header, homepage NSFW consent overlay flow, and a no-JS theme fallback for slower or restricted browsers.
- Server-backed theme switching with explicit
return_torouting and better backup/restore diagnostics across admin upload paths. - Restore route request logging, board backup manifest inspection logs, and larger multipart restore coverage in the route test harness.
- Per-board archived-thread retention limit in the admin panel, with a default cap of
150archived threads per board. - Automatic favicon generation from a single
512x512upload, with global site icons plus optional per-board favicon overrides.
Improved
- Mobile interaction quality for reply, media expansion, archive rows, catalog controls, board descriptions, and header layout without changing the desktop interface.
- Poster ID chips on boards with IDs enabled now use stronger per-ID color separation so different posters are easier to tell apart without breaking theme compatibility.
- NSFW disclaimer copy and action-button styling now read more clearly across themes, including light-theme contrast improvements for the consent button.
- Audio posting UX now leads with an audio-first upload flow, clearer field labels, and an explicit optional cover-image slot instead of the previous mixed primary/secondary upload wording.
- Tor and mobile resilience through safer identity bucketing, less brittle theme persistence, JS-degraded fallbacks, and better cache revalidation for board, catalog, and thread pages.
- Generated
settings.tomlreadability by regrouping settings into clearer related sections, and log organization by moving runtime logs intorustchan-data/logs/. - Backup and restore internals by deduplicating board restore into one shared core and full-site restore into one shared execution path with rollback-aware filesystem swaps.
- Automatic archive trimming now deletes media only after the last remaining post reference is gone, so deduplicated uploads shared across multiple threads are preserved safely until truly unused.
- Admin favicon controls now use a compact inline layout with live previews and clearer replace/clear actions for both global and board-specific icons.
Fixed
- Mobile photo uploads now preserve correct orientation for both stored images and generated thumbnails.
- Admin archive, pin, thread deletion, board restore, and full restore flows now refresh more reliably without requiring manual cookie or cache clearing.
- Firefox and localhost admin restore uploads no longer fail on
Origin: nullor loopback host alias mismatches when valid session and CSRF state are present. - Linked image+audio posts now render as one combined media block, use the uploaded image as the audio thumbnail, autoplay the attached song when the image is expanded, keep playing when the image is collapsed, and size the audio seek bar to the same width as the linked image.
- Secondary combo-audio uploads now preserve FLAC bytes as-is without FFmpeg re-encoding, while still reusing the linked image thumbnail for the post presentation.
- Theme picker, board menu, catalog sort controls, and top-bar alignment no longer overflow or misplace themselves on mobile and Tor Browser.
- Thumbnail hover and click hitboxes no longer stretch left of the visible image after closing expanded media.
- OP quotelinks now render the
(OP)marker with tighter spacing so they display as>>123 (OP)instead of looking over-separated. - Backup/restore logging now respects the app’s actual tracing targets instead of being silently filtered out.
- Board index, catalog, and thread tab titles now use clearer board-aware formatting, and full-site restore no longer wipes the current global favicon when restoring an older backup that did not include favicon data.
v1.1.0
[1.1.0]
Added
- ChanNet API for federation and RustWave gateway commands on port
7070. - Full-screen operator dashboard with live stats, logs, boards, shortcuts, and setup flows.
- Native HTTPS support with self-signed and Let's Encrypt options, plus optional HTTP to HTTPS redirects and HSTS.
- Stronger Tor support with per-stream isolation, Tor-only mode, better startup and shutdown handling, and
Onion-Location. - Optional arbitrary file uploads with safe download-only handling for non-media files.
Improved
- Faster board search, batched thread previews, cached thread updates, and lower job-queue overhead.
- Safer posting, polling, replies, restores, uploads, and ChanNet imports through better transactions and rollback handling.
- Cleaner internals across server, admin, backup, middleware, media, and schema code, with a new in-memory route test harness.
- Better operator tooling with
/healthz,/readyz,/metrics,X-Request-ID, cleaner logs, and more reliable FFmpeg and bind-address handling.
Fixed
- Proxy-aware IP handling now blocks spoofed
X-Real-IPandX-Forwarded-Forvalues from untrusted clients. - Rate limiting now covers more write and preview paths, closing easy abuse and DoS gaps.
- HTTPS deployments now enforce secure cookies, safer redirects, and more consistent HSTS behavior.
- Restore, upload, temp-file, and background-job edge cases were cleaned up to avoid partial state, stuck jobs, and unsafe paths.
- Admin feedback, upload-disabled UI, error messages, and login logging are now more consistent and safer.
Security
- Restore validation, upload serving, backup handling, and appeal flows were tightened to reduce traversal, duplication, and data-leak risks.
Onion-Location, CAPTCHA wording, and HTTPS documentation now match real runtime behavior.
Breaking Changes
- HTTP to HTTPS redirects now use configured and trusted hosts instead of echoing arbitrary
Hostheaders.
Validation
cargo fmt --allcargo clippy --all-targets --all-features -- -D warnings -W clippy::all -W clippy::pedantic -W clippy::nurserycargo test
v1.1.0 Alpha 3: HTTPS addition and interface redesign
[1.1.0 alpha 3]
Full-Screen TUI Console
The operator-facing terminal console has been rewritten from a scrolling line-input shell into a full-screen static TUI, matching the dashboard style introduced in RustHost.
Architecture
src/server/console.rs is deleted and replaced by a four-file module at src/server/console/:
| File | Responsibility |
|---|---|
mod.rs |
Alternate screen lifecycle, RAW_MODE_ACTIVE atomic, ConsoleMode / WizardKind enums, start(), cleanup(), render() loop |
dashboard.rs |
Pure render functions — no I/O, no DB calls; takes a &ChanStats snapshot and returns a formatted String |
input.rs |
Crossterm key reader (50 ms poll), KeyEvent enum, spawn() |
wizard.rs |
Interactive admin wizards; exits raw mode for read_line, re-enters it on completion |
A new crossterm = "0.27" dependency is added to Cargo.toml.
Dashboard Layout
On startup the terminal switches to the alternate screen and displays a live dashboard refreshed every 3 seconds (or immediately on [R]):
────────────────────────────────
RustChan
────────────────────────────────
Status
Server : RUNNING (0.0.0.0:8080)
Uptime : 2h 14m 33s
Memory : 42.1 MiB
Activity
Requests : 18 402 1.2/s in-flight 3
Online : 7
Content
Boards : 4
Threads : 831 (+2)
Posts : 12 047 (+8)
Storage
Database : 94.3 MiB
Uploads : 1.22 GiB
/g/ 204t 3021p /tech/ 91t 1204p
⠹ 2 file(s) uploading
────────────────────────────────
[H] Help [B] Boards [C] Create board [A] Admin [D] Del thread [L] Logs [Q] Quit
────────────────────────────────
Delta counts (+N) are coloured yellow. The upload spinner uses a Braille frame array. All ANSI helpers (green, yellow, red, dim, bold, cyan) are pure functions with no side effects.
Keyboard Shortcuts
| Key | Action |
|---|---|
H |
Help screen — full key reference |
R |
Force immediate stats refresh |
L |
Toggle log view (40 most recent lines) |
B |
Board list (ID / slug / name / NSFW / thread count / post count) |
C |
Create board wizard |
A |
Create admin wizard |
D |
Delete thread wizard |
Q / Esc |
Confirm-quit prompt |
Y |
Confirm quit |
N |
Cancel — return to dashboard |
Ctrl-C |
Force quit (no confirmation) |
ConsoleMode State Machine
Dashboard ──[L]──▶ LogView ──[L]──▶ Dashboard
──[H]──▶ Help
──[B]──▶ BoardList
──[Q]──▶ ConfirmQuit ──[Y]──▶ shutdown
──[N]──▶ Dashboard
──[C/A/D]──▶ Wizard(_) ──(done)──▶ Dashboard
While any Wizard(_) mode is active, the render task skips frame output entirely — the wizard thread owns the terminal.
Wizard Flows
kb_create_board, kb_create_admin, and kb_delete_thread move from the old console.rs to wizard.rs unchanged. run_wizard() handles the terminal hand-off:
- Disable raw mode, leave alternate screen
- Run the wizard (blocking,
spawn_blocking) - Re-enable raw mode, re-enter alternate screen, clear for a clean frame
- Reset
ConsoleModetoDashboard
Stats Refresh
A background task in server.rs polls the database every 3 seconds and writes into SharedStats (Arc<RwLock<ChanStats>>). KeyEvent::Reload triggers an immediate refresh outside the timer. render() takes a read lock on SharedStats — no DB calls ever happen on the render path.
Terminal Safety
RAW_MODE_ACTIVEatomic prevents double-cleanupcleanup()is called from both thestd::panichook (registered inmain.rs) and the graceful shutdown path- The terminal is always restored even on unexpected exits
🔄 Changed
src/server/mod.rs—pub mod consolenow resolves to the sub-directory;pub use console::cleanupadded for the panic hook path inmain.rssrc/server/server.rs—spawn_keyboard_handler()replaced byconsole::start();match cmd.as_str()loop replaced by typedmatch key_eventloop; stats refresh background task addedsrc/main.rs— panic hook registerscrate::server::cleanup(); explicitconsole::cleanup()call added after server future resolves
Native HTTPS / TLS Support
RustChan can now serve itself directly over HTTPS without needing a reverse proxy in front of it. Two modes are available:
Self-signed certificate — enabled with two lines in settings.toml. A certificate is generated automatically on first run and saved to disk. Your browser will show a security warning (normal for self-signed), which you can accept. Good for local development and private installs.
Let's Encrypt (ACME) — for public servers with a real domain name. RustChan contacts Let's Encrypt automatically, proves it owns the domain, and gets a trusted certificate. No browser warning. Renews itself before it expires.
Both modes run alongside the existing HTTP server — adding HTTPS does not remove or break anything. Installs that do not add a [tls] section to settings.toml are completely unaffected.
HTTP → HTTPS Redirect
When HTTPS is enabled, an optional redirect listener can be turned on. Any visitor who arrives on the plain HTTP port is automatically sent to the HTTPS address. Enable with redirect_http = true in the [tls] section.
HSTS (automatic)
Once a visitor connects over HTTPS, their browser is instructed to always use HTTPS for future visits. No configuration needed — this activates automatically when TLS is running.
Fixes
- IP banning and rate limiting now work correctly over HTTPS — the security features that track visitor IPs continue to work on HTTPS connections the same way they do on HTTP. No bans or limits are bypassed by switching to HTTPS.
- Secure cookies enforced when TLS is active — session and auth cookies are automatically marked
Securewhen HTTPS is enabled, preventing them from being sent over plain HTTP.
origin/indev-1.1.0-alpha-4
Auto-Terminal Launch Support
RustChan now automatically opens in a terminal window when double-clicked, instead of silently failing. If already running in a terminal, it behaves as normal. Works on Windows, Linux, and macOS.
fixes:
- Files left behind on DB errors: Disk fills forever. Fix: Show errors clearly, handle deletions properly.
- Stuck tasks after crashes: Jobs never restart. Fix: Auto-reset at startup, limit retries.
- Huge text uploads crash memory: Attackers overload server. Fix: Cap text fields at 64KB.
- Multiple backups corrupt progress: Overlapping runs mess up display. Fix: Add lock flag.
- ZIP files write to wrong folders: Hackers escape safe areas. Fix: Strict path checks.
- Temp folder tricks break SQL: Env vars inject bad chars. Fix: Use safe folder near DB.
- Restore uploads fill disk unchecked: No per-file limits. Fix: Cap at 4GB per file.
- ZIP bombs explode RAM: Bad peers unpack gigabytes. Fix: Limit entries to 8MB each.
- FFmpeg hangs block everything: No timeouts tie up workers. Fix: Add 2-min timeout, kill if stuck.
- Leftover backup files after crashes: Disk clogs on restart. Fix: Startup cleanup, use safe temp folder.
v1.1.0 Alpha 2: Replace Tor with Arti
[1.1.0 alpha 2]
The headline change in this release is a deep security and correctness audit of the Arti/Tor implementation introduced in alpha 1, resulting in six critical fixes, nine high-priority fixes, and a set of new operator-facing configuration options. Alongside that, this release includes reliability improvements to shutdown coordination, backup handling, multipart parsing, and the database layer.
🔒 Tor / Arti — Security & Correctness Audit
Architecture
The hidden service implementation from alpha 1 has been audited and corrected. The core architecture — bootstrapping Arti in-process, deriving a .onion address from a persistent Ed25519 keypair, and proxying inbound onion streams to the local HTTP port — is unchanged. What changed is correctness, isolation, and operational safety.
🔴 Critical fixes
Per-stream IP isolation for Tor users
Previously every Tor user resolved to 127.0.0.1 as their client IP. The Arti proxy was a raw TCP passthrough (copy_bidirectional) with no HTTP awareness, so no header injection was possible. This meant all Tor users shared a single rate-limit bucket, ban entry, and post cooldown: banning one Tor user banned everyone on Tor simultaneously.
Fixed by introducing TOR_STREAM_TOKENS, a DashMap<u16, Arc<str>> in detect.rs keyed by the ephemeral local port of each proxy connection. When proxy_tor_stream connects to the local axum socket, the OS assigns an ephemeral source port; axum's ConnectInfo sees this as the peer port on the accepted socket. A random tor:<hex> token is inserted into the map under that port, and a TokenGuard RAII struct removes it when the task ends. Both ClientIp::from_request_parts and extract_ip now look up the peer port in TOR_STREAM_TOKENS when the connection is from loopback with enable_tor_support=true, returning the per-stream token instead of 127.0.0.1. Every Tor stream now has its own isolated bucket for rate limiting, bans, and post cooldowns.
Files: src/detect.rs, src/middleware/mod.rs
Tor-only mode (tor_only setting)
With enable_tor_support = true and the default bind_addr = 0.0.0.0:8080, the HTTP server was reachable directly over clearnet simultaneously with the hidden service. An operator expecting a private Tor-only site had no way to enforce that without manually overriding bind_addr.
Added a new tor_only setting to settings.toml. When tor_only = true and enable_tor_support = true, bind_addr is silently overridden to 127.0.0.1:{port} during config loading — the port is preserved, only the host changes. The override is logged at startup. Default remains false (dual-stack: clearnet and Tor both active), which is the correct default for an imageboard that wants to be reachable both ways.
# Restrict to Tor-only (hidden service). Clearnet access blocked.
# tor_only = falseFiles: src/config.rs
Graceful shutdown for the Tor task
The Tor retry loop had no CancellationToken. During shutdown, worker_cancel.cancel() signaled every other background task but the Tor task continued running — sleeping through a backoff of up to 480 seconds. The shutdown code then hit a hard 10-second timeout and abandoned the task, leaving Tor circuits open without sending RELAY_END cells.
Fixed by adding a cancel: CancellationToken parameter to detect_tor(). Both the run_arti(...) call and the backoff sleep now use tokio::select! against the token, so the task exits promptly when shutdown is signaled. The worker_cancel variable in run_server() is moved to before the detect_tor call so it is available to pass in. The shutdown timeout is extended from 10s to 15s as a safety net for any in-flight copy_bidirectional draining — in practice the task exits in milliseconds once the token fires.
Files: src/detect.rs, src/server/server.rs
tor_client and onion_service explicit keepalive
tor_client is last used on the line that calls launch_onion_service. onion_service is last used inside the HsId retry block. Both have side-effectful Drop implementations: dropping tor_client closes all Tor circuits; dropping onion_service deregisters the hidden service from the Tor network. Both variables must stay alive through the entire stream loop.
Rust named let bindings drop at end of their enclosing scope (the function body), not at last-use, so this was not a live bug — but it was invisible and fragile. Added explicit let _ = &tor_client; let _ = &onion_service; keepalive borrows at the end of run_arti, after the stream loop exits, making the intent unambiguous and guarding against any future tooling that might warn about "unused" bindings.
Files: src/detect.rs
🟠 High-priority fixes
Onion address encoder: fixed checksum computation
In hsid_to_onion_address, the two checksum bytes were extracted from the Sha3_256 digest using an iterator with .unwrap_or(0) fallbacks. Sha3_256 always produces 32 bytes so the fallback was dead code, but it masked the logic and would silently produce a wrong checksum if the digest size ever changed. Replaced with direct array indexing: let hash: [u8; 32] = hasher.finalize().into(); let checksum = [hash[0], hash[1]];.
Added a Python-verified cryptographic test vector for the all-zeros Ed25519 key:
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaam2dqd.onion
Verified with:
import hashlib, base64
pub = bytes(32); ver = bytes([3])
chk = hashlib.sha3_256(b'.onion checksum' + pub + ver).digest()[:2]
print(base64.b32encode(pub+chk+ver).decode().lower().rstrip('=')+'.onion')Files: src/detect.rs
Onion-Location response header for Tor Browser
Tor Browser reads the Onion-Location response header and automatically prompts the user to switch to the .onion address when browsing the clearnet version of a site. The header was never set anywhere in the codebase.
Added onion_location_middleware — an async middleware function that reads state.onion_address, and when the address is known and the response Content-Type is text/html, inserts Onion-Location: http://<addr> into the response headers. Wired into build_router via axum_middleware::from_fn_with_state at the outermost position so it fires on every HTML response. Non-HTML responses (static assets, JSON, media) are skipped.
Files: src/server/server.rs
Configurable bootstrap timeout
The Tor bootstrap timeout was hardcoded at 120 seconds. On censored networks using bridges or pluggable transports, directory fetch is slow and 120 seconds is insufficient — the task would time out, wait through exponential backoff, and retry indefinitely without ever succeeding.
Added tor_bootstrap_timeout_secs to settings.toml (default 120). The timeout error message now includes a hint to increase this value.
# Increase for censored networks or when using bridges.
# tor_bootstrap_timeout_secs = 120Files: src/config.rs, src/detect.rs
Configurable maximum concurrent Tor streams
MAX_CONCURRENT_TOR_STREAMS was a hardcoded compile-time constant (512). Operators on resource-constrained hosts (low FD limits, limited RAM) had no way to reduce it without recompiling.
Added tor_max_concurrent_streams to settings.toml (default 512). When the limit is reached, stream_req is dropped explicitly — Arti sends a RELAY_END cell automatically on drop.
# Reduce if the process hits file descriptor limits.
# tor_max_concurrent_streams = 512Files: src/config.rs, src/detect.rs
Infrastructure errors distinguished from normal stream closure
All errors from proxy_tor_stream were logged at DEBUG with the message "Tor: stream closed". This made it impossible to distinguish a normal client disconnect (expected, routine) from "local TCP connect failed" (axum has crashed or is unrestarted — requires operator attention).
Split error handling: connection failures to the local HTTP server now log at ERROR with a clear message ("Tor: cannot reach local HTTP server — is axum running?"). Normal stream closures (EOF, client disconnect, keep-alive expiry) continue to log at DEBUG.
Files: src/detect.rs
Attempt counter reset after healthy session
The exponential backoff retry counter (attempt) incremented on both crash exits and clean exits. After 4 clean reconnect cycles, the service was waiting 480 seconds between restart attempts — identical behavior to a crash loop. A clean exit after ≥60 seconds of healthy operation now resets attempt = 0.
Files: src/detect.rs
Arc<str> for the local address string
local_addr was a String cloned into every spawned proxy task — one heap allocation per Tor connection. Replaced with Arc<str>, making each clone an atomic reference count increment with no heap allocation.
Files: src/detect.rs
Configurable service nickname
The Arti onion service nickname was hardcoded to "rustchan". When multiple instances share the same arti_state/ directory (e.g. Docker volume mounts, CI), identical nicknames cause key collisions and one instance fails to start its onion service.
Added tor_service_nickname to settings.toml (default "rustchan").
# Change when running multiple instances sharing the same arti_state/ directory.
# tor_service_nickname = "rustchan"Files: src/config.rs, src/detect.rs
Onion address omitted from structured INFO log
The onion address was logged as a structured field at INFO level, causing it to appear in plaintext in the JSON log file (rustchan.log) and any log aggregator or forwarding pipeline it feeds into. For operators running a sensitive hidden se...
v1.1.0 Alpha 1
[1.1.0]
🌐 New: ChanNet API (Port 7070)
RustChan can now talk to other RustChans. Introducing the ChanNet API — a two-layer federation and gateway system living entirely on port 7070. Disabled by default, enable with ./rustchan-cli --chan-net
Layer 1 — Federation (/chan/export, /chan/import, /chan/refresh, /chan/poll): nodes sync with each other via ZIP snapshots. Push your posts out, pull theirs in, keep your mirror fresh.
Layer 2 — RustWave Gateway (/chan/command): the RustWave audio transport client gets its own command interface. Send a typed JSON command, get a ZIP back. Supported commands: full_export, board_export, thread_export, archive_export, force_refresh, and reply_push (the only one that actually writes anything).
Text only — no images, no media, no binary data cross this interface by design. Full schema docs in channet_api_reference.docx.
Architecture Refactor
This release restructures the codebase for maintainability. No user-facing
behavior has changed. Every route, every feature, every pixel is identical.
The only difference is where the code lives.
The problem
main.rs had grown to 1,757 lines and owned everything from the HTTP router
to the ASCII startup banner. handlers/admin.rs hit 4,576 lines with 33
handler functions covering auth, backups, bans, reports, settings, and more.
Both files were becoming difficult to navigate and risky to modify.
What changed
Phase 1 — Cleanup
- Removed unused
src/theme-init.js(dead duplicate ofstatic/theme-init.js) - Moved
validate_password()frommain.rstoutils/crypto.rsalongside
the other credential helpers - Moved
first_run_check()andget_per_board_stats()frommain.rsinto
thedbmodule, eliminating the only raw SQL that lived outsidedb/
Phase 2 — Background work
- Moved
evict_thumb_cache()frommain.rstoworkers/mod.rswhere it
belongs alongside the other background maintenance operations
Phase 3 — Console extraction
- Created
src/server/directory for server infrastructure - Extracted terminal stats, keyboard console, startup banner, and all
kb_*
helpers toserver/console.rs(~350 lines)
Phase 4 — CLI extraction
- Moved
Cli,Command,AdminActionclap types andrun_admin()to
server/cli.rs(~250 lines)
Phase 5 — Server extraction
- Moved
run_server(),build_router(), all 7 background task spawns,
static asset handlers, HSTS middleware, request tracking,ScopedDecrement,
and global atomics toserver/server.rs(~800 lines) main.rsis now ~50 lines: runtime construction, CLI parsing, dispatch
Phase 6 — Admin handler decomposition
- Converted
handlers/admin.rsto a module folder (handlers/admin/) - Extracted
backup.rs— all backup and restore handlers (~2,500 lines) - Extracted
auth.rs— login, logout, session management - Extracted
moderation.rs— bans, reports, appeals, word filters, mod log - Extracted
content.rs— post/thread actions, board management - Extracted
settings.rs— site settings, VACUUM, admin panel admin/mod.rsnow contains only shared session helpers and re-exports
By the numbers
File Before After
main.rs 1,757 lines ~50 lines
handlers/admin.rs 4,576 lines split across 6 files
server/ (new) — ~1,400 lines total
db/ unchanged + 2 functions from main.rs
workers/ unchanged + evict_thumb_cache
utils/crypto.rs unchanged + validate_password
What was not changed
db/, templates/, utils/, media/, config.rs, error.rs, models.rs,
detect.rs, handlers/board.rs, handlers/thread.rs, and middleware/ are
all untouched. They were already well-structured.
## New Module: src/media/
### media/ffmpeg.rs — FFmpeg detection and subprocess execution
- Added detect_ffmpeg() for checking FFmpeg availability (synchronous, suitable for spawn_blocking)
- Added run_ffmpeg() shared executor used by all FFmpeg calls
- Added ffmpeg_image_to_webp() with quality 85 and metadata stripping
- Added ffmpeg_gif_to_webm() using VP9 codec, CRF 30, zero bitrate target, metadata stripped
- Added ffmpeg_thumbnail() extracting first frame as WebP at quality 80 with aspect-preserving scale
- Added probe_video_codec() via ffprobe subprocess (moved from utils/files.rs)
- Added ffmpeg_transcode_to_webm() using path-based API (replaces old bytes-in/bytes-out version)
- Added ffmpeg_audio_waveform() using path-based API (same refactor as above)
### media/convert.rs — Per-format conversion logic
- Added ConversionAction enum: ToWebp, ToWebm, ToWebpIfSmaller, KeepAsIs
- Added conversion_action() mapping each MIME type to the correct action
- Added convert_file() as the main entry point for all conversions
- PNG to WebP is attempted but original PNG is kept if WebP is larger
- All conversions use atomic temp-then-rename strategy
- FFmpeg failures fall back to original file with a warning (never panics, never returns 500)
### media/thumbnail.rs — WebP thumbnail generation
- All thumbnails output as .webp
- SVG placeholders used for video without FFmpeg, audio, and SVG sources
- Added generate_thumbnail() as unified entry point
- Added image crate fallback path for when FFmpeg is unavailable (decode, resize, save as WebP)
- Added thumbnail_output_path() for determining correct output path and extension
- Added write_placeholder() for generating static SVG placeholders by kind
### media/exif.rs — EXIF orientation handling (new file)
- Moved read_exif_orientation and apply_exif_orientation from utils/files.rs
### media/mod.rs — Public API
- Added ProcessedMedia struct with file_path, thumbnail_path, mime_type, was_converted, original_size, final_size
- Added MediaProcessor::new() with FFmpeg detection and warning log if not found
- Added MediaProcessor::new_with_ffmpeg() as lightweight constructor for request handlers
- Added MediaProcessor::process_upload() for conversion and thumbnail generation (never propagates FFmpeg errors)
- Added MediaProcessor::generate_thumbnail() for standalone thumbnail regeneration
- Registered submodules: convert, ffmpeg, thumbnail, exif
---
## Modified Files
### src/utils/files.rs
- Extended detect_mime_type with BMP, TIFF (LE and BE), and SVG detection including BOM stripping
- Rewrote save_upload to delegate conversion and thumbnailing to MediaProcessor
- GIF to WebM conversions now set processing_pending = false (converted inline, no background job)
- MP4 and WebM uploads still set processing_pending = true as before
- Removed dead functions: generate_video_thumb, ffmpeg_first_frame, generate_video_placeholder, generate_audio_placeholder, generate_image_thumb
- Removed relocated functions: ffprobe_video_codec, probe_video_codec, ffmpeg_transcode_webm, transcode_to_webm, ffmpeg_audio_waveform, gen_waveform_png
- EXIF functions kept as thin private delegates to crate::media::exif for backward compatibility
- Added mime_to_ext_pub() public wrapper for use by media/convert.rs
- Added apply_thumb_exif_orientation() for post-hoc EXIF correction on image crate thumbnails
- Added tests for BMP, TIFF LE, TIFF BE, SVG detection and new mime_to_ext mappings
### src/models.rs
- Updated from_ext to include bmp, tiff, tif, and svg
### src/lib.rs and src/main.rs
- Registered new media module
### src/workers/mod.rs
- Updated probe_video_codec call to use crate::media::ffmpeg::probe_video_codec
- Replaced in-memory transcode_to_webm with path-based ffmpeg_transcode_to_webm using temp file persist
- Replaced in-memory gen_waveform_png with path-based ffmpeg_audio_waveform using temp file persist
- File bytes now read from disk only for SHA-256 dedup step
### Cargo.toml
- Added bmp and tiff features to the image crate dependency
v1.0.13 Mostly bugfixes and optimizations
[1.0.13] — 2026-03-08
WAL Mode + Connection Tuning
db/mod.rs
cache_size bumped from -4096 (4 MiB) to -32000 (32 MiB) in the pool's with_init pragma block. The journal_mode=WAL and synchronous=NORMAL pragmas were already present.
Missing Indexes
db/mod.rs
Two new migrations added at the end of the migration table:
- Migration 23:
CREATE INDEX IF NOT EXISTS idx_posts_thread_id ON posts(thread_id)— supplements the existing composite index for queries that filter onthread_idalone. - Migration 24:
CREATE INDEX IF NOT EXISTS idx_posts_ip_hash ON posts(ip_hash)— eliminates the full-table scan on the admin IP history page and per-IP cooldown checks.
Prepared Statement Caching Audit
db/threads.rs · db/boards.rs · db/posts.rs
All remaining bare conn.prepare(...) calls on hot or repeated queries replaced with conn.prepare_cached(...): delete_thread, archive_old_threads, prune_old_threads (outer SELECT) in threads.rs; delete_board in boards.rs; search_posts in posts.rs. Every query path is now consistently cached.
Transaction Batching for Thread Prune
Already implemented in the codebase. Both prune_old_threads and archive_old_threads already use unchecked_transaction() / tx.commit() to batch all deletes/updates into a single atomic transaction. No changes needed.
RETURNING Clause for Inserts
db/threads.rs · db/posts.rs
create_thread_with_op and create_post_inner now use INSERT … RETURNING id via query_row, replacing the execute() + last_insert_rowid() pattern. The new ID is returned atomically in the same statement, eliminating the implicit coupling to connection-local state.
Scheduled VACUUM
config.rs · main.rs
Added auto_vacuum_interval_hours = 24 to config. A background Tokio task now sleeps for the configured interval (staggered from startup), then calls db::run_vacuum() via spawn_blocking and logs the bytes reclaimed.
Expired Poll Cleanup
config.rs · main.rs · db/posts.rs
Added poll_cleanup_interval_hours = 72. A new cleanup_expired_poll_votes() DB function deletes vote rows for polls whose expires_at is older than the retention window. A background task runs it on the configured interval, preserving poll questions and options.
DB Size Warning
config.rs · handlers/admin.rs · templates/admin.rs
Added db_warn_threshold_mb = 2048. The admin panel handler reads the actual file size via std::fs::metadata, computes a boolean flag, and passes it to the template. The template renders a red warning banner in the database maintenance section when the threshold is exceeded.
Job Queue Back-Pressure
config.rs · workers/mod.rs
Added job_queue_capacity = 1000. The enqueue() method now checks pending_job_count() before inserting — if the queue is at or over capacity, the job is dropped with a warn! log and a sentinel -1 is returned, avoiding OOM under post floods.
Coalesce Duplicate Media Jobs
workers/mod.rs
Added an Arc<DashMap<String, bool>> (in_progress) to JobQueue. Before dispatching a VideoTranscode or AudioWaveform job, handle_job checks if the file_path is already in the map — if so it skips and logs. The entry is removed on both success and failure.
FFmpeg Timeout
config.rs · workers/mod.rs
Replaced hardcoded FFMPEG_TRANSCODE_TIMEOUT / FFMPEG_WAVEFORM_TIMEOUT constants with CONFIG.ffmpeg_timeout_secs (default: 120). Both transcode_video and generate_waveform now read this value at runtime so operators can tune it in settings.toml.
Auto-Archive Before Prune
workers/mod.rs · config.rs
prune_threads now evaluates allow_archive || CONFIG.archive_before_prune. The new global flag (default true) means no thread is ever silently hard-deleted on a board that has archiving enabled at the global level, even if the individual board didn't opt in.
Waveform Cache Eviction
main.rs · config.rs
A background task runs every hour (after a 30-min startup stagger). It walks every {board}/thumbs/ directory, sorts files oldest-first by mtime, and deletes until total size is under waveform_cache_max_mb (default 200 MiB). A new evict_thumb_cache function handles the scan-and-prune logic; originals are never touched.
Streaming Multipart
handlers/mod.rs
The old .bytes().await (full in-memory buffering) is replaced by read_field_bytes, which streams via .chunk() and returns a 413 UploadTooLarge the moment the running total exceeds the configured limit — before memory is exhausted.
ETag / Conditional GET
handlers/board.rs · handlers/thread.rs
Both handlers now accept HeaderMap, derive an ETag (board index: "{max_bump_ts}-{page}"; thread: "{bumped_at}"), check If-None-Match, and return 304 Not Modified on a hit. The ETag is included on all 200 responses too.
Gzip / Brotli Compression
main.rs · Cargo.toml
tower-http features updated to compression-full. CompressionLayer::new() added to the middleware stack — it negotiates gzip, Brotli, or zstd based on the client's Accept-Encoding header.
Blocking Pool Sizing
main.rs · config.rs
#[tokio::main] replaced with a manual tokio::runtime::Builder that calls .max_blocking_threads(CONFIG.blocking_threads). Default is logical_cpus × 4 (auto-detected); configurable via blocking_threads in settings.toml or CHAN_BLOCKING_THREADS.
EXIF Orientation Correction
utils/files.rs · Cargo.toml
kamadak-exif = "0.5" added. generate_image_thumb now calls read_exif_orientation for JPEGs and passes the result to apply_exif_orientation, which dispatches to imageops::rotate90/180/270 and flip_horizontal/vertical as needed. Non-JPEG formats skip the EXIF path entirely.
✨ Added
- Backup system rewritten to stream instead of buffering in RAM — all backup operations previously loaded entire zip files into memory, risking OOM on large instances. Downloads now stream from disk in 64 KiB chunks (browsers also get a proper progress bar). Backup creation now writes directly to disk via temp files with atomic rename on success, so partial backups never appear in the saved list. Individual file archiving now streams through an 8 KiB buffer instead of reading each file fully into memory. Peak RAM usage dropped from "entire backup size" to roughly 64 KiB regardless of instance size.
- ChanClassic theme — a new theme that mimics the classic 4chan aesthetic: light tan/beige background, maroon/red accents, blue post-number links, and the iconic post block styling. Available in the theme picker alongside existing themes.
- Default theme in settings.toml — the generated
settings.tomlnow includes adefault_themefield so the server-side default theme can be set before first startup, without requiring admin panel access. - Home page subtitle in settings.toml —
site_subtitleis now present in the generatedsettings.tomldirectly belowforum_name, allowing the home page subtitle to be configured at install time. - Default theme selector in admin panel — the Site Settings section now includes a dropdown to set the site-wide default theme served to new visitors.
🔄 Changed
- Admin panel reorganized — sections are now ordered: Site Settings → Boards → Moderation Log → Report Inbox → Moderation (ban appeals, active bans, word filters consolidated) → Full Site Backup & Restore → Board Backup & Restore → Database Maintenance → Active Onion Address. Code order matches page order for easier future editing.
- "Backup & Restore" renamed to "Full Site Backup & Restore" to clearly distinguish it from the board-level backup section.
- Ban appeals, active bans, and word filters condensed into a single Moderation panel with clearly labelled subsections.
v1.0.12 Per board post cooldowns and more
[1.0.12] — 2026-03-07
🔄 Changed
- Database module fixes —
threads.rs: added explicitROLLBACKon failedCOMMITto prevent dirty transaction state.mod.rs: addedsort_unstable+deduptopaths_safe_to_deleteto eliminate duplicate path entries.mod.rs: addedmedia_typeandedited_atcolumns to the baseCREATE TABLE postsschema to match the final migrated state.admin.rs: replaced inlined Post row mapper with sharedsuper::posts::map_postto eliminate duplication.admin.rs: clarifiedrun_wal_checkpointdoc comment on return tuple order. - Template module fixes —
board.rs: fixed archive thumbnail path prefix from/static/to/boards/.board.rs: movedfmt_tsto the top-level import, removed redundant localuseinsidearchive_page.thread.rs: corrected misleading comment about embed and draft script loading.thread.rs: added doc comment documenting thebody_htmltrust precondition onrender_post.forms.rs: removed deadcaptcha_jsvariable and no-op string concatenation. - CSS cleanup — removed 11 dead rules for classes never emitted by templates or JS (
.greentext,.quote-link,.admin-thread-del-btn, duplicate.media-expanded,.media-rotate-btn,.thread-id-badge,.quote-block,.quote-toggle,.archive-heading,.autoupdate-bar,.video-player). Fixed two undefined CSS variable references (--font-mono→--font,--bg-body→--bg). Merged duplicate.file-containerblock into a single declaration. - Database module split — the 2,264-line monolithic
db.rshas been reorganized into five focused modules with zero call-site changes (all existingdb::references compile unchanged):mod.rs(466 lines) — connection pool, shared types (NewPost,CachedFile), schema initialization, shared helpersboards.rs(293 lines) — site settings, board CRUD, statsthreads.rs(333 lines) — thread listing, creation, mutation, archiving, pruningposts.rs(642 lines) — post CRUD, file deduplication, polls, job queue, worker helpersadmin.rs(558 lines) — admin sessions, bans, word filters, reports, mod log, ban appeals, IP history, maintenance
- Template module split — the 2,736-line monolithic template file has been reorganized into five focused modules with no changes to the public API (all existing handler code works without modification):
mod.rs(392 lines) — shared infrastructure: site name/subtitle statics, base layout, pagination, timestamp formatting, utility helpersboard.rs(697 lines) — home page, board index, catalog, search, and archive renderingthread.rs(738 lines) — thread view, post rendering, polls, and post edit formadmin.rs(760 lines) — login page, admin panel, mod log, VACUUM results, IP historyforms.rs(198 lines) — new thread and reply forms, shared across board and thread pages
🔒 Security Fixes
Critical
- PoW bypass on replies — proof-of-work verification was only enforced on new threads but not on replies. Replies now require a valid PoW nonce when the board has CAPTCHA enabled.
- PoW nonce replay — the same proof-of-work solution could be submitted repeatedly. Used nonces are now tracked in memory and rejected within their 5-minute validity window. Stale entries are automatically pruned.
High
- Removed inline JavaScript — all inline
<script>blocks andonclick/onchange/onsubmitattributes have been extracted into external.jsfiles. The Content Security Policy now usesscript-src 'self'with nounsafe-inline, closing a major XSS surface. - Backup upload size cap — the restore endpoints previously accepted uploads of unlimited size, risking out-of-memory crashes. Both full and board restore routes are now capped at 512 MiB.
🐛 Fixes
- Post rate limiting simplified — removed the global
check_post_rate_limitfunction that was silently overriding per-board cooldown settings. A board withpost_cooldown_secs = 0now correctly means zero cooldown. The per-board setting is the sole post rate control. - API endpoints excluded from GET rate limit — hover-preview requests (
/api/post/*) were being counted against the navigational rate limit, causing false throttling on threads with many quote links. All/api/routes are now excluded alongside/static/,/boards/, and/admin/. The GET limiter now only covers page loads that a scraper would target (board index, catalog, archive, threads, search, home). - Trailing slash 404s — several routes returned 404 when accessed with or without a trailing slash (board index, catalog, archive, thread pages, post editing). Added middleware to normalize trailing slashes so all URL variations resolve correctly. Bookmarks and manually typed URLs now work as expected.