# DDC (Direct Download Client) Self-hosted direct download client that fetches files onto the server, extracts archives, and moves the results into media folders. ## Stack - Backend: Go standard library only (no third-party dependencies), single static binary - Frontend: plain HTML, CSS, and JavaScript modules embedded in the binary - Container: distroless static image, non-root, read-only root filesystem ## Project Layout | Path | Purpose | | --- | --- | | `main.go` | Startup and graceful shutdown | | `internal/config` | Environment configuration and library root parsing | | `internal/preferences` | Settings editable from the web page, saved to `.settings.json` | | `internal/libraries` | Discovers and validates first level library folders | | `internal/auth` | Sessions, credential check, login rate limiting | | `internal/guard` | URL validation and SSRF-safe HTTP client | | `internal/fetch` | Streaming download with progress, size and stall limits | | `internal/extract` | Zip and tar extraction with Zip Slip and bomb protection | | `internal/files` | Safe path resolution, file tree, cross-device moves | | `internal/diskspace` | Free space checks | | `internal/jobs` | Download job lifecycle and workflows | | `internal/server` | HTTP routes, middleware, handlers | | `web/static` | Single page frontend | | `deploy/nginx` | Reverse proxy example | ## Destinations Every first level folder inside these roots is a move destination: - `/media/blob/TeaNet/Plex` - `/media/blob/ChaiNet/Plex` ## Security - Session login with HttpOnly, Secure, SameSite=Strict cookie - Login lockout after 5 failed attempts per client for 15 minutes - Same-origin check and JSON-only bodies on state changing requests - SSRF protection: http/https only, allowed port list, every resolved IP checked at connect time (blocks loopback, private, link-local, CGNAT, metadata, ULA, NAT64, multicast), redirects revalidated, proxies ignored - Zip Slip protection: absolute paths and `..` entries rejected, symlinks, hardlinks and devices skipped - Extraction limited by free disk space and entry count - Move targets restricted to first level folders of the destination roots (hidden folders and symlinks excluded), subfolders validated component by component, existing files never overwritten - Disk space checked before download, during download, before extraction, and before cross-device copies - Strict Content-Security-Policy, no inline scripts, all untrusted names rendered as text ## Supported Archives `.zip`, `.tar`, `.tar.gz`, `.tgz`, `.tar.bz2`, `.tbz2`, `.tbz` ## Configuration | Variable | Default | Description | | --- | --- | --- | | `AUTH_USERNAME` | required | Login username | | `AUTH_PASSWORD` | required | Login password, minimum 12 characters | | `COOKIE_SECURE` | `true` | Set `false` only for plain HTTP testing | | `SESSION_TTL_HOURS` | `12` | Session lifetime | | `TRUST_PROXY_HEADERS` | `false` | Use `X-Real-IP` for login rate limiting (enable behind nginx) | | `LISTEN_ADDR` | `:8080` | Listen address inside the container | | `DOWNLOAD_DIR` | `/data/downloads` | Temporary download directory | | `MIN_FREE_GB` | `10` | Initial value: free space that must always remain | | `MAX_CONCURRENT_DOWNLOADS` | `2` | Initial value: parallel downloads, others wait in queue | | `ALLOWED_PORTS` | `80,443` | Initial value: allowed remote ports, `*` for any | ## Web Settings The Settings dialog edits parallel downloads, free space to keep, allowed ports, default extract and cleanup options, and the theme. Server settings are saved to `.settings.json` in the download folder and override the initial values above. The theme is stored per browser. ## Local Testing 1. Create the download folder first, so Docker does not create it as root: ``` mkdir -p data/downloads ``` 2. `.env` is preset for testing: downloads go to `./data/downloads`, cookies work over plain HTTP. Both destination drives must be mounted before starting. 3. Start and open `http://127.0.0.1:8080` (user `admin`, password from `.env`): ``` docker compose up -d --build ``` ## Deployment 1. Make sure `PUID`/`PGID` can write to the download folder and to the first level folders of both destination roots. 2. Copy and edit the environment file: ``` cp .env.example .env ``` 3. Start the service: ``` docker compose up -d --build ``` 4. Install the nginx site: ``` sudo cp deploy/nginx/ddc.conf /etc/nginx/sites-available/ddc.conf sudo ln -s /etc/nginx/sites-available/ddc.conf /etc/nginx/sites-enabled/ sudo nginx -t && sudo systemctl reload nginx ``` ## Notes - Moves are instant renames when the download directory and destination share a mount, otherwise files are copied and the source removed. - Downloads left in the download directory after a restart are listed again as ready.