Repository navigation
feat(providers): add qBittorrent integration with username/password auth - #62
Conversation
|
Part of a set of integration PRs. Overview, merge order and design notes: #64 |
itz4blitz
left a comment
There was a problem hiding this comment.
Requesting changes. The WebUI client, cookie jar, login cooldown, migration 0010, and the supportsUsername / apiKeyOptional gates are in good shape. Two holes should be fixed before merge.
-
The request proxy authenticates an API-key qBittorrent server with
X-Api-Keyonly (apps/backend/src/modules/proxy/proxy.service.ts). Released qBittorrent (v5.2.0 / WebAPI v2.14.1) acceptsAuthorization: Bearer <key>.X-API-Keylanded on qBittorrent master on 2026-10-05 and is not in a shipping release. The ingestion client already sends Bearer (qbittorrent.client.ts). The proxy's own error tells the user to use an API key, and that path 401s on every released version. SendAuthorization: BearerforproviderId === 'qbittorrent'when the key is non-empty. You can keepX-Api-Keyas well. Point the user-facing copy at>= 5.2, not>= 5.3. -
GET/POST/PUT /serversreturn the Drizzle row, includingapi_key(servers.service.ts).ServerResponseDtoomitsapiKey, but nothing strips the column. That was already true for API tokens. It is worse here because the qBittorrent WebUI password is stored in that column, and the edit dialog prefills the "Password / API key" field from it (edit-server-dialog.tsx). Please stop returning the secret. An empty password field on edit should mean "keep the stored value", not "clear it" and not "echo it back".
Non-blocking:
- The log id cursor is process memory. Torrent-error and connection-status rows use
new Date(), and once inserted they movelastActivitySyncforward. After a restart, a warning older than that cursor is dropped. Persist the log id per server, or do not let synthetic timestamps advance the cursor. redactSecretsmisses percent-encoded query keys (passkey%3D) and non-hex path tokens.- A failed env seed logs the Drizzle error, and that message includes bound parameters, so
QBITTORRENT_PASSWORDcan land in the logs. The success path already logs only a boolean.
This branch also conflicts with the other integration PRs on the shared wiring files. #57 and #60 are landing first; rebase onto master after the two fixes above.
Add @logarr/provider-qbittorrent and optional username credentials for servers. Credentials (generic, reusable by other providers): - nullable servers.username column (migration 0010) plumbed through core types, DTOs, ServersService, env seeding and the add/edit forms - the qBittorrent password is stored in the existing secret field; an empty secret is allowed only for qBittorrent (auth bypass), other providers still require an API key Provider: - fetch-based client supporting cookie login, auth bypass and the 5.3+ API key; generic cookie jar, Referer/Origin for CSRF, single re-login on 401/403, no redirect following, clear ban/proxy/CSRF errors - escalating login cooldown (5/15/60 min) so polling cannot trigger qBittorrent's IP ban; session state keyed by a hash of URL+credentials - activity from warning/critical log/main entries (cursor with restart handling, skipped when a log path is configured), errored torrents and connection status, with stable day-bucketed ids - tracker passkeys redacted and tracker URLs reduced to host; secrets never appear in errors or logs - qbittorrent.log file tailing parser - backend registration, env seeding (QBITTORRENT_URL/USERNAME/PASSWORD, /qbittorrent-logs), proxy guard, Dockerfiles, compose, turbo env, README
- credentials: supportsUsername/apiKeyOptional capability flags (core, provider, integration entry) replace hard-coded qbittorrent checks; proxy guard keys on stored username - proxy: qBittorrent >= 5.3 accepts X-API-Key (verified in webui source); message fixed - activity types snake_case (log_warning, log_critical, torrent_error, connection_status) - getActivity: 'Failed to get <source>' logging, rethrow when all sources fail, 'Not connected' - bounded session/cursor/status caches (LRU + TTL) with resetQBittorrentState - qbittorrent icon via dashboard-icons, realTimeLogs true, fetch stubbing per beforeEach/afterEach - add qBittorrent to maintainer agent provider list
- servers.service only stores/updates username for providers with supportsUsername - seed derives credentials-optional from the registered provider's apiKeyOptional capability - torrent-error id uses kebab-case state (missing-files) - provider-neutral proxy guard message; fix stale server-payload comment
…torrent 5.2) qBittorrent 5.2 answers a successful POST /auth/login with 204 and an empty body plus the session cookie. Success is now any 2xx with body 'Ok.' or an empty body with a cookie set; 'Fails.' still escalates the cooldown and other bodies stay an unexpected-response error.
Clear the cookie jar before POST /auth/login so an empty 2xx response only counts as success when this response set a new session cookie.
Released qBittorrent (5.2.x / WebAPI 2.14.1+) only accepts Authorization: Bearer <key>; X-API-Key exists on master only. Add a small per-provider auth header map in the proxy (Bearer plus X-Api-Key for qBittorrent, X-Api-Key for everything else) and point all user-facing copy at qBittorrent >= 5.2.
GET/POST/PUT /servers now map rows through toServerResponse, which drops api_key and adds hasApiKey. An omitted or empty apiKey on update keeps the stored value. The edit dialog no longer prefills the secret and omits an empty field from the update payload; internal callers (test connection, ingestion, proxy) still read the key from the database.
… and redact more secrets - torrent_error and connection_status activities set advancesSyncCursor: false - redactSecrets handles percent-encoded keys (passkey%3D...) and opaque tokens in announce/scrape paths (base32/base64url/alphanumeric >= 20 chars)
Seed failures log only the error name and code: Drizzle query errors embed the bound parameters, which include QBITTORRENT_PASSWORD. The startup failure is rethrown as a sanitized error. README documents that stored secrets are never returned.
PUT /servers/:id accepts apiKey: null to clear the stored password/API key (only for providers whose credentials are optional). The edit dialog offers a clear action when a secret is stored and makes the user choose between keeping it as an API key and clearing it when the username is removed, so there is no silent mode change.
ea08133 to
411e5fe
Compare
|
Thanks for the review. I've rebased onto current master and pushed the changes.
I did a focused security pass on secret handling and found no leaks. One open point: the proxy follows redirects and would forward |
Summary
Adds a qBittorrent integration (
@logarr/provider-qbittorrent). It also adds the first non-API-key credential support to Logarr: an optional, nullableusernameon servers.Credentials (shared, reusable)
servers.usernamecolumn. Migration0010_add-server-usernameis a singleALTER TABLE … ADD COLUMN "username" text.usernameis optional everywhere:ProviderConfigand the server types;ServersService(trimmed;nullor empty clears it);server-payload.tshelper.apiKey) field, which the frontend already mapspasswordinto.apiKeymay be empty only for qBittorrent, for its auth-bypass mode. Every other provider still requires it, andcreatenow returns a 400 instead of hitting the DB constraint.Capability flags (no hard-coded provider id)
ProviderCapabilitiesgets two optional flags,supportsUsernameandapiKeyOptional. The backend reads them from the registered provider: create and update, env seeding. The frontend reads the same-named flags from the integration entry: the payload helper and the add and edit forms.supportsUsername.Provider
POST /api/v2/auth/login;Authorization: Bearer.fetchwith form login:QBT_SID_<port>, multipleSet-Cookieheaders, andMax-Age/Expiresdeletion;Referer/Originheaders for qBittorrent's CSRF check;redirect: 'manual', with a clear error when the URL redirects;Fails.response counts as bad credentials.log/main, using thelast_known_idcursor with restart and id-regression handling (skipped when a log path is configured, since file tailing then covers them);passkey,authkey,torrent_passandpkvalues and hex passkey path segments are redacted from log messages. Private-tracker passkeys therefore never reach the DB, the UI or the AI analysis.log_warning,log_critical,torrent_error,connection_status), the same as the other providers.qbittorrent.log. The parser handles(N|I|W|C) <ts> - message.Wiring
QBITTORRENT_URL,QBITTORRENT_USERNAME,QBITTORRENT_PASSWORDandQBITTORRENT_LOGS_PATH. The URL alone is enough to seed./qbittorrent-logsmount, both Dockerfiles, the compose files andturbo.jsonenv.X-API-Keyheader (checked inwebapplication.cpp).X-Api-Keyis omitted when the key is empty.available, with username and "Password / API key" fields, both optional.Notes
GET /serversstill returnsapiKeyunmasked. That is existing behaviour for every provider, but it now also covers a qBittorrent password, so masking it should be a follow-up.Testing
@logarr/provider-qbittorrenthas 163 unit tests covering:servers.service, the newserver-seed.service.spec.ts, the proxy, and aLogFileProcessorend-to-end test (881 backend tests in total).pnpm build,lint,typecheckandtestall pass.pnpm install --frozen-lockfileis clean.pnpm format:checkalready fails onmaster(pre-existing files, includingapi.tsandserver-seed.service.ts, which this PR touches but does not reformat).