Signed URL support
Requires a Enterprise license
Dave’s Dog Depot is a menu board rendered to a looping menu-board.webm for a screen. The animation loops on a 16-second cycle, so a 16-second capture plays seamlessly on repeat.
A sign-player on the wall should hold neither the render parameters nor the signing secret. It fetches one stable URL, and a small service mints a short-lived signed render and redirects to it. With --server-signed-url set, the zshot server rejects an unsigned GET / with 401 and a tampered or expired one with 403; only a URL signed with the shared secret renders.
zshot signs the query string by percent-encoding each value, sorting the pairs by key, joining them with &, and taking the HMAC-SHA256 with the secret. sign-url.py builds the same string, so the server accepts it, and 302s the player to a freshly signed render:
#!/usr/bin/env python3
"""Hands a sign-player a freshly signed zshot URL for the menu board."""
import hashlib
import hmac
import time
import urllib.parse
from http.server import BaseHTTPRequestHandler, HTTPServer
ZSHOT = "http://127.0.0.1:3000/" # the signed zshot server
SECRET = b"s3cr3t" # matches --server-signed-url
BOARD = "https://zshot-cli.com/example_assets/daves-dog-depot/"
TTL = 120 # signed URL lifetime, seconds
def signed_render_url():
params = {
"url": BOARD,
"output_type": "webm",
"video_duration": "16",
"video_framerate": "24",
"browser_width": "1920",
"browser_height": "1128",
"video_width": "1280",
"video_height": "752",
"wait_for": "js:menu:ready",
"expires": str(int(time.time()) + TTL),
}
canonical = "&".join(
"%s=%s" % (k, urllib.parse.quote(v, safe=""))
for k, v in sorted(params.items())
)
signature = hmac.new(SECRET, canonical.encode(), hashlib.sha256).hexdigest()
return ZSHOT + "?" + canonical + "&signature=" + signature
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(302)
self.send_header("Location", signed_render_url())
self.send_header("Cache-Control", "no-store")
self.end_headers()
if __name__ == "__main__":
HTTPServer(("127.0.0.1", 8080), Handler).serve_forever()Start the render server with the signing secret:
zshot --server --server-bind-port 3000 \
--server-signed-url hmac-sha256:s3cr3t --server-signed-url-max-age 300Then run the signer beside it:
python3 sign-url.py # listens on :8080Visit http://localhost:8080: the signer 302s to a freshly signed render and the zshot server returns the looping board. --server-signed-url-max-age caps the lifetime, so a leaked URL stops rendering once it expires. See the API for the server endpoints and the other server flags.