Add per-server custom HTTP headers (#228) - #229
Merged
Merged
Conversation
Supports tunnels and reverse proxies that gate access with their own
header-based token auth (Pangolin, Cloudflare Access, etc.), layered
independently of qBittorrent's own auth mode.
Header values are tokens, so the whole customHeaders array is treated as
a secret: stored in SecureStore under server_custom_headers_{id}, forced
to [] in AsyncStorage, stripped on export and forced empty on import.
Header names colliding with ones qRemote manages (Authorization, Cookie,
Referer, Origin, Content-Type, Host) are rejected at save time, so a
custom header can't silently break auth or cookie handling.
…iagnostics The auth-method row's value was laid out at its intrinsic width, and since SettingRow's label side is flex:1 it only received the leftover space — "Username & Password" starved the label until "Authentication Method" broke mid-word. Cap the value at 45% and let it scale its font down, which leaves the label enough room to wrap on a word boundary. Also carry custom headers through the diagnostics paths so a header-gated server doesn't fail there while working in the app: SuperDebugPanel now attaches them to every request (reach, login, API check, log export) and includes them in its config fingerprint. Debug mode reports header names only — that block is copied to the clipboard and pasted into public issue reports, and the values are auth tokens. Test Connection already exercised them via the ServerConfig it builds.
There is no migration system, so servers saved before #228 must keep working untouched. Covers the four surfaces a legacy record passes through: - storage: an AsyncStorage record with no useCustomHeaders/customHeaders keys and no SecureStore entry loads normally, with customHeaders defaulting to [] - storage: a corrupt stored secret degrades to [] instead of throwing, which would otherwise take down the whole Promise.all and hide every server - client: a config with neither field set sends exactly the headers it did before, and headers present without the flag are still not sent - export/import: legacy configs and legacy export files round-trip
Save-time UI validation was being relied on as the guarantee that a custom
header can never be reserved or malformed. It is not the only way data
reaches a ServerConfig, so that assumption did not hold:
- settings import (app/(tabs)/settings/advanced.tsx) spreads unvalidated JSON
and blanked password/basicAuthPassword/apiKey but not customHeaders, so a
hand-edited file could put header tokens into SecureStore
- custom headers are applied last in the request interceptor, so a header
named Authorization or Cookie would have clobbered the real auth
- the read path validated JSON syntax but not shape, so [{"key":123}] parsed
fine and then threw on .trim() when the edit screen rendered
Enforce at each point that actually matters rather than upstream of it:
sanitize on write in storage.saveServer (every write passes through it),
validate shape on read via parseStoredCustomHeaders, re-check reserved names
in the interceptor, and blank customHeaders on settings import.
The app's own exports were already safe — both server and settings export go
through toExportedServer, which forces customHeaders to [].
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.
Closes #228.
What
Adds an optional Custom Headers section to the add/edit server forms. When enabled, the configured name/value pairs are sent on every request to that server — for tunnels and reverse proxies that gate access with their own header-based token auth.
The reporter's setup is Pangolin fronting qBittorrent via a newt client; they need at least two header pairs. This is a proxy-layer concern, so no qBittorrent endpoint is involved and it works identically on 4.x and 5.x.
How
Follows the existing reverse-proxy Basic Auth pattern (
useBasicAuth, #118):useCustomHeadersflag +customHeadersarray onServerConfig.services/api/client.tsapplies them after theAuthorizationblock, independent of the server's auth mode — API key, password, and none all compose with custom headers.server_custom_headers_{id}, forced to[]before the record reaches AsyncStorage, deleted on server delete, stripped on export and forced empty on import.Authorization,Cookie,Referer,Origin,Content-Type,Host) are rejected at save time and flagged inline in the form — otherwise a custom header could silently break auth or cookie handling.New
utils/customHeaders.tsholds the sanitize/validate logic; newcomponents/CustomHeadersSection.tsxis shared by both server screens.Notes
colorskey, or stored field is renamed, so existing users are unaffected.Test plan
npx tsc --noEmit— exit 0npm test— 1045 passed / 74 suites, both projects (includes the locale parity check)npm run lint— 0 errors (37 pre-existing warnings)npm run formattests/utils/customHeaders.test.ts(reserved names, sanitize, validate),tests/services/client-custom-headers.test.ts(interceptor behavior incl. composing with an API key), plus secret-handling assertions added to the storage and server-export suites.Not run on device — the UI section itself is unverified in a simulator.
🤖 Generated with Claude Code