# ushort `ushort` is a compact URL shortener written in Rust and backed by SQLite. It is a behavior-compatible replacement for the original stdlib-only Python service: the routes, base-path handling, response shapes, database schema, retention, API-key checks, throttling, redirects, and frontend are preserved. The frontend under `static/` is embedded into the executable at compile time. The final Linux image contains only one statically linked executable and runs without Python, a shell, a package manager, or external static files. ## Quick start ```bash cp config.example.toml config.toml # Edit base_url and api_key, then: docker compose up --build -d ``` The service listens on `127.0.0.1:18082` on the host. Its SQLite database is stored at `./data/urlshort.db` and survives container replacement. The container runs as UID/GID `1001:1001` by default. Override `USHORT_UID`/`USHORT_GID` if needed, and ensure `./data` is writable by that identity. ## Configuration TOML is the preferred format; see `config.example.toml`. Existing JSON files remain supported, including the old `short_length` alias for `min_short_length`. ```bash ushort config.toml ushort legacy-config.json ``` | Key | Required | Default | Description | |---|---:|---:|---| | `base_url` | yes | — | Public URL. Its path becomes the routing prefix. | | `api_key` | yes | — | Secret used by list and delete operations. | | `host` | no | `0.0.0.0` | Bind address. | | `port` | no | `8080` | Bind port. | | `db_path` | no | `data/urlshort.db` | SQLite database path. | | `retention_days` | no | `0` | Default lifetime for new URLs; zero never expires. | | `min_short_length` | no | `6` | Initial generated code length. | | `max_short_length` | no | `32` | Maximum generated and accepted code length. | | `max_url_length` | no | `2048` | Maximum original URL length. | | `max_retention_days` | no | `3650` | Maximum per-URL retention value. | | `rate_limit_requests` | no | `60` | Requests allowed per client/window. | | `rate_limit_window` | no | `60` | Sliding-window length in seconds. | | `production` | no | `false` | When true, disable embedded frontend routes for compatibility with deployments that serve static files separately. | For example, `base_url = "https://example.com/go/links"` limits routing to `/go/links` and produces URLs such as `https://example.com/go/links/aB3xYz`. Requests outside that prefix return 404. Real API keys belong only in the deployment's ignored `config.toml`, never in the repository or container image. ## API All paths below are relative to the path in `base_url`. Trailing slashes are accepted. Every response includes the legacy CORS and security headers. ### Health and frontend - `GET /` serves the byte-identical embedded frontend when `production=false`. - `GET /api/health` returns HTTP 200: ```json {"status":"ok","service":"url-shortener"} ``` When the configured base path is non-empty, a GET of the bare path (for example `/s`) redirects to `/s/`. ### Create a short URL `POST /api/shorten` and `GET /api/shorten` are both supported and do not require an API key. Fields may be supplied as query parameters or in a JSON body; query parameters take precedence. | Field | Required | Description | |---|---:|---| | `url` | yes | Absolute `http://` or `https://` URL. | | `retention_days` | no | Per-URL lifetime, or zero to keep forever. | Success is HTTP 201 with only the short URL as UTF-8 plain text: ```text https://example.com/s/aB3xYz ``` Codes begin at `min_short_length`. After ten collisions at one length the service tries the next length, through `max_short_length`. ### List URLs `GET /api/urls?api_key=` returns all rows newest first. A missing or invalid key returns HTTP 403 with an empty body. ```json { "count": 1, "urls": [ { "short_code": "aB3xYz", "short_url": "https://example.com/s/aB3xYz", "original_url": "https://example.org", "created_at": 1710000000, "visit_count": 5, "retention_days": 30 } ] } ``` ### Metadata and lookup - `GET /api/urls/` returns one metadata object without authentication. - `GET /api/lookup?url=` returns the newest matching metadata object without authentication. - A missing record returns HTTP 404 with an empty body. ### Delete `DELETE /api/urls/?api_key=` returns: - HTTP 204 with an empty body on success; - HTTP 404 with an empty body when the code does not exist; or - HTTP 403 with an empty body when authentication fails. ### Redirect `GET /` atomically increments `visit_count` and responds with HTTP 302 to the stored original URL. ## Data compatibility The existing database can be mounted directly; no export/import is required. The schema remains: ```sql CREATE TABLE urls ( short_code TEXT PRIMARY KEY, original_url TEXT NOT NULL, created_at INTEGER NOT NULL, visit_count INTEGER NOT NULL DEFAULT 0, retention_days INTEGER NOT NULL DEFAULT 0 ); ``` Older databases missing `retention_days` are upgraded in place with a default of zero. Expired records are removed on startup and before each GET, POST, or DELETE request, using the original strict expiry boundary. Before a production cutover, make a filesystem-consistent backup of the database (or use SQLite's backup command) and test the copy with the new image. ## Frontend The existing `index.html`, JavaScript, CSS, locally hosted fonts, sorting, pagination, theme selection, URL lookup, copy/delete actions, and admin flow are unchanged. Compile-time embedding makes those files part of the standalone artifact; no bind mount or separate web root is needed. For the existing `/s` deployment, place the location blocks from `nginx.conf` in the public vhost. They proxy all traffic to the binary while retaining the current `no-store` HTML and `no-cache` asset policies. ## Build and test The repository intentionally does not require a host Rust installation. ```bash docker build -t ushort:local . docker compose up -d ``` Run the Rust test suite in an isolated build stage: ```bash docker build --target tester . ``` The Dockerfile builds against musl and uses `scratch` for the runtime image. Docker Buildx can publish both required architectures from the same source: ```bash docker buildx build \ --platform linux/amd64,linux/arm64 \ -t sodium/ushort:latest \ --push . ``` ## Security model - SQL statements use bound parameters. - Short codes are restricted to ASCII alphanumerics. - URL, retention, code, and request-body sizes are bounded. - API-key comparison is constant-time; protected operations return no body on authentication failure. - Per-IP sliding-window throttling applies to API calls and redirects; embedded frontend files do not consume API quota. The app uses `X-Real-IP`, then the first `X-Forwarded-For` value, then the TCP peer. Keep the container port bound to loopback and let the trusted reverse proxy overwrite those headers. - CORS preflight uses `OPTIONS`; it does not consume rate-limit quota. - The runtime image is read-only, drops Linux capabilities, and runs as a numeric non-root user through Compose. - `SIGINT`/`SIGTERM` cleanly unblock the server for prompt container shutdown.