feat(stream): authenticate /stream and /hls with signed tokens
/stream and /hls were served with no auth (only CORS + rate limit), so a funnel- or UPnP-exposed daemon leaked active downloads to anyone with the URL. Bind a short-lived HMAC token (scope + 6h expiry) to every stream URL the daemon hands out and verify it on each request: - /stream + VLC playlist: ?t= query, agent-minted, scope "stream" - /hls: path segment /hls/<session>/<token>/<resource>, web-minted with the agent's reported secret, scope "hls:<session>" — relative playlist URIs inherit it with no rewriting - NO loopback exemption: cloudflared relays public funnel traffic over localhost, so a loopback source address is not a trust signal - the agent reports its per-run signing key on register only when enforcing - require_stream_token config (default true); secret fails hard if rand fails - /playlist.m3u no longer self-mints a token (was an open token oracle) Roadmap: Docs/plans/unarr-agent-roadmap.md (hueco #1). Deploy the web HLS-minting change BEFORE shipping this agent release.
This commit is contained in:
parent
ea00130d08
commit
444d7e63fd
8 changed files with 622 additions and 36 deletions
|
|
@ -22,6 +22,7 @@ type DaemonConfig struct {
|
|||
Version string
|
||||
DownloadDir string
|
||||
StreamPort int // port for the HTTP stream server
|
||||
StreamSecret string // hex HMAC key for stream tokens (reported so the web can mint HLS tokens)
|
||||
LanIP string // LAN IP (reported in sync for stream URL resolution)
|
||||
TailscaleIP string // Tailscale IP (reported in sync for stream URL resolution)
|
||||
CanDelete bool // library.allow_delete is enabled
|
||||
|
|
@ -109,6 +110,13 @@ func (d *Daemon) SetFunnelURL(url string) {
|
|||
WriteState(&d.State)
|
||||
}
|
||||
|
||||
// UpdateStreamSecret sets the hex HMAC key reported on register so the web can
|
||||
// mint HLS stream tokens the agent will accept.
|
||||
func (d *Daemon) UpdateStreamSecret(secretHex string) {
|
||||
d.cfg.StreamSecret = secretHex
|
||||
d.sync.cfg.StreamSecret = secretHex
|
||||
}
|
||||
|
||||
// UpdateStreamPort updates the stream port reported in sync requests.
|
||||
func (d *Daemon) UpdateStreamPort(port int) {
|
||||
d.cfg.StreamPort = port
|
||||
|
|
@ -126,6 +134,7 @@ func (d *Daemon) Register(ctx context.Context) error {
|
|||
Version: d.cfg.Version,
|
||||
DownloadDir: d.cfg.DownloadDir,
|
||||
StreamPort: d.cfg.StreamPort,
|
||||
StreamSecret: d.cfg.StreamSecret,
|
||||
LanIP: d.cfg.LanIP,
|
||||
TailscaleIP: d.cfg.TailscaleIP,
|
||||
HWAccel: d.cfg.HWAccel,
|
||||
|
|
|
|||
|
|
@ -18,6 +18,11 @@ type RegisterRequest struct {
|
|||
StreamPort int `json:"streamPort,omitempty"`
|
||||
LanIP string `json:"lanIp,omitempty"`
|
||||
TailscaleIP string `json:"tailscaleIp,omitempty"`
|
||||
// StreamSecret is the daemon's per-run HMAC key (hex) for stream tokens. The
|
||||
// web mints the HLS path token with it (the agent mints /stream tokens on its
|
||||
// own URLs); the agent verifies both. In memory, regenerated each start, so a
|
||||
// fresh register after restart re-syncs it.
|
||||
StreamSecret string `json:"streamSecret,omitempty"`
|
||||
// Transcode capabilities — let the web side suggest a smarter quality
|
||||
// before the player even starts. HWAccel is the picked backend
|
||||
// ("nvenc"/"qsv"/"vaapi"/"videotoolbox"/"none"). MaxTranscodeHeight is
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue