zshot/cliDownload

Authentication

The server authorizes a request by a credential header or by a signed URL.

MethodFlagClient sends
Basic--server-basic-authAuthorization: Basic <base64 of user:password>
Token--server-token-authAuthorization: Bearer <token>
Delegated--server-delegated-authA Bearer or Basic header the configured service or program accepts
Signed URL--server-signed-urlexpires and signature query parameters

A missing or wrong credential answers 401. With both Basic and token set, either authorizes. A delegate sees only the credentials neither accepts.

Precedence

A valid credential header authorizes outright, and a wrong one is rejected without checking a signature. A request with no credential header is checked for a signature when signed URLs are on.

Signatures apply to GET /, DELETE / and GET /assets. POST / and /mcp authorize by credential header only, so a server with signed URLs and no Basic or token auth accepts any POST and any MCP request.

Delegated Authentication

--server-delegated-auth names an https:// URL, an http:// URL on a loopback host, or a command. The server sends it each credential the static flags do not accept, as JSON with a fresh nonce:

{"type": "token", "token": "…", "nonce": "9f2c…"}
{"type": "basic", "username": "…", "password": "…", "nonce": "9f2c…"}

A URL receives it as a POST; a command receives it on stdin and nothing in its arguments or environment. The command is pinned at startup: the server refuses to run it once the file changes.

Delegate answersServer answers
{"authenticated": true, "nonce": <the same>}, with 200 or exit 0The request proceeds
{"authenticated": false}, 401, 403 or a non-zero exit401
Anything else, or nothing within --server-delegated-auth-timeout503

An approval without the request’s nonce is not one, so no generic page, cached reply or fixed file can approve a request. The server asks for every request; it remembers no answer.

A command that approves one token:

#!/bin/sh
payload=$(cat)
nonce=$(printf '%s' "$payload" | sed -n 's/.*"nonce":"\([0-9a-f]*\)".*/\1/p')
case "$payload" in
  *'"token":"s3cr3t"'*) printf '{"authenticated":true,"nonce":"%s"}' "$nonce" ;;
  *) exit 1 ;;
esac

Signed URLs

Sign the request’s query parameters, expires included:

  1. Set expires to a Unix time in seconds.
  2. Sort the parameters by key, then by value, and form-encode them.
  3. Compute HMAC-SHA256 of that string with the secret, as lowercase hex.
  4. Append it as signature.
import hashlib, hmac, time, urllib.parse

params = {
    "url": "https://zshot-cli.com/example_assets/zrm/",
    "output_type": "png",
    "expires": str(int(time.time()) + 300),
}
query = urllib.parse.urlencode(sorted(params.items()))
signature = hmac.new(b"SECRET", query.encode(), hashlib.sha256).hexdigest()
print("http://127.0.0.1:3000/?" + query + "&signature=" + signature)
ResponseReason
401no signature
403the signature does not match, expires is missing or past, or expires is beyond --server-signed-url-max-age

The server signs the asset links in a render response with the same secret; see Additional Assets.

Metrics Endpoint

--server-prometheus-auth sets a Basic credential that is the only one /metrics accepts. Without it, /metrics takes the main server’s credentials, and is open when the server has none.