Initial DDC release

This commit is contained in:
SirBlob committed 2026-10-25 20:24:50 -04:00
commit 434f6441da
57 files changed
+4606

No files matched your search

Executable
+105
View File
@@ -0,0 +1,105 @@
# 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.