Repository navigation
feat(signalr): opt-in SignalR/WebSocket passthrough for proxied arrs (#91) - #111
Closed
bakerboy448 wants to merge 5 commits into
Closed
bakerboy448 wants to merge 5 commits into
bakerboy448 wants to merge 5 commits into
Conversation
…otifiarr#91) Adds an nginx /signalr location that WebSocket-proxies /<base>/signalr/* to the backend arr, using an internal auth_request to signalr/auth.php which resolves the scoped key via getAppFromProxiedKey, gates on a new per-app 'signalr' opt-in flag (migration 009), validates the backend URL, and swaps the scoped access_token for the backend's real key. PHP cURL cannot proxy WebSockets, so this stays at the nginx layer. Bazarr templates opt in; admin UI adds the toggle. Closes Notifiarr#91
…ring The auth_request subrequest has no query string, so keys passed as ?apikey= or ?access_token= (browser SignalR clients cannot set headers) were never seen by auth.php. Pass the original request URI to PHP and parse its query string.
…keep the real key off the client Replace the per-app signalr flag with allow-list entries (/signalr/messages get, /signalr/messages/negotiate post) checked by the same isAllowedEndpoint and method logic as REST, with the same logging and usage accounting. The auth script moves out of the web root so a client cannot call it, and the upstream path comes from the matched entry. The real key is injected by nginx as X-Api-Key and is no longer placed in the query string.
…rict Upgrade and X-Accel-Redirect Apps with redactions set get 401 on SignalR (the stream is unredacted). Only websocket Upgrade is forwarded. SignalR auth is logged as auth-granted instead of 200. SignalR location moved after /feed/ and /backup/. X-Accel-Redirect ignored on fastcgi and SignalR proxy. Concurrent SignalR connections capped at 10 per client address (429).
…ment SignalR limits
bakerboy448
force-pushed
the
claude/signalr-passthrough
branch
from
October 21, 2026 23:34
4292868 to
9b5cd89
Compare
Collaborator
Author
|
Rebased onto Tested live on that exact head, image built from a clean tree, against 3 real Sonarr instances:
Not tested: Bazarr client, Radarr, UrlBase, TLS upstreams. Separate from this PR: |
Collaborator
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements #91: pass
/<base>/signalr/*through to the backend arr so consumers of the arr SignalR channel (Bazarr first) keep working when repointed at starrproxy with a scoped key.How it works
Starr.php(addSignalrEndpoints) adds/signalr/messages(get) and/signalr/messages/negotiate(post) to the endpoint list, because SignalR is not in the arr OpenAPI specs. An app can use SignalR only if its endpoint list or template includes those entries. The Radarr and Sonarr Bazarr templates include them. There is no per-app flag and no migration.location ~ ^(.*)/signalr/(.*)$usesauth_requestto an internal-onlylocation = /signalr-auth, which runsroot/app/www/signalr-auth.php(outside the web root, so a client cannot call it). The script resolves the scoped key, applies the sameisAllowedEndpointand method checks as REST, and returns the backend URL, the allow-listed upstream path, and the real key as response headers. The upstream path comes from the matched entry, never from the client path.X-Api-Keyto the real key and drops the clientAuthorizationheader. The scoped key is removed from the proxied query string, and the real key never reaches the client.Upgrade: websocketis forwarded.X-Accel-Redirectfrom PHP and from the arr is ignored on everyfastcgi_passandproxy_passlocation involved.proxyCode:auth-granted(nginx contacts the arr after the check, so the upstream status is unknown) and count asallowedin usage./api,/feed,/backup,/health, PHP, and static routes are otherwise unchanged.Limitations (also in the README)
127.0.0.11). An arr URL that resolves only through/etc/hosts,localhost,extra_hosts, or host networking works for REST but not SignalR. Use an IP or a Docker-resolvable name.Testing
Head 9b5cd89 (rebased onto develop 8bc0d68), image built from a clean tree at that commit with a fresh
/config, run on a throwaway docker network with generated keys. No production containers or keys. Real Sonarr (linuxserver latest) x3, plus a recording echo upstream registered as a Sonarr backend to prove what reaches the arr. Full plan and output: run4 (PR plan 60 / 60 PASS, upstream smoke and route-order checks 29 / 29 PASS).php -lonsignalr-auth.phpandStarr.phpandnginx -ton the rendered config pass in the image.X-Api-Key), websocket upgrade with SignalR handshake (101) against real Sonarr%2fvariants,HEAD/OPTIONS, uppercase pathAuthorization: Beareralone/signalr-auth,/signalr-auth.php,/signalr/auth.phpX-Starr-*header or real key in any client-visible response (all captured responses checked)ApiKey, encoded param name,ACCESS_TOKEN, duplicate params, withAuthorizationX-Api-Key, noAuthorizationUpgrade: h2c,websocket, h2cUpgradeforwarded;WebSocketforwardswebsocketauth-granted, not200X-Accel-Redirectreached through?endpoint=on/feed/,/backup/,/api//feed/: nginx follows the redirect (401)nginx -T: everyfastcgi_passlocation (5) hasfastcgi_ignore_headers X-Accel-Redirect/<base>/signalr/.../{api,feed,backup}differs, denied either way (JSON 404 now, 401 if moved), arr never reached. Kept after the catch-all/api,/feed,/backup,/sonarr/api,/x/feed,/x/backup,/a/b/api;/still the UIaddstarron a new (4th) backend: app gets that backend'sstarr_idtagGET and POST,importlistexclusionand/pagedagainst real Sonarr;config/hostdeniedsession.upload_progress.enabledOff,memory_limit-1,post_max_size8M,fatal_error_backtracesOff; nginxclient_max_body_size 8M(9 MiB body: 413)Not covered on this head: the Bazarr client itself (an earlier revision was tested with Bazarr 1.6.2 against Sonarr and Radarr), Radarr, UrlBase, TLS upstreams, legacy SignalR, and the admin UI.
Closes #91
🤖 Generated with Claude Code