Skip to content

feat(providers): add qBittorrent integration with username/password auth - #62

Merged
itz4blitz merged 12 commits into
itz4blitz:masterfrom
vbrzezina:feat/qbittorrent-integration
Oct 16, 2026
Merged

itz4blitz merged 12 commits into
itz4blitz:masterfrom
vbrzezina:feat/qbittorrent-integration

Conversation

@vbrzezina

@vbrzezina vbrzezina commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds a qBittorrent integration (@logarr/provider-qbittorrent). It also adds the first non-API-key credential support to Logarr: an optional, nullable username on servers.

Credentials (shared, reusable)

  • New nullable servers.username column. Migration 0010_add-server-username is a single ALTER TABLE … ADD COLUMN "username" text.
  • username is optional everywhere:
    • core ProviderConfig and the server types;
    • DTOs and ServersService (trimmed; null or empty clears it);
    • env seeding;
    • frontend API types;
    • the add-source modal and the edit dialog, through a small, tested server-payload.ts helper.
  • The qBittorrent password is stored in the existing secret (apiKey) field, which the frontend already maps password into.
  • apiKey may be empty only for qBittorrent, for its auth-bypass mode. Every other provider still requires it, and create now returns a 400 instead of hitting the DB constraint.

Capability flags (no hard-coded provider id)

  • ProviderCapabilities gets two optional flags, supportsUsername and apiKeyOptional. 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.
  • A username is only stored for providers that declare supportsUsername.

Provider

  • Three auth modes:
    • username and password: cookie session via POST /api/v2/auth/login;
    • no credentials: qBittorrent's localhost or whitelist bypass;
    • API key with no username: qBittorrent ≥ 5.3, sent as Authorization: Bearer.
  • Client, built on fetch with form login:
    • a generic cookie jar that handles QBT_SID_<port>, multiple Set-Cookie headers, and Max-Age/Expires deletion;
    • Referer/Origin headers for qBittorrent's CSRF check;
    • re-login once on 401/403;
    • redirect: 'manual', with a clear error when the URL redirects;
    • clear messages for an IP ban (403 on login), a reverse proxy or CSRF failure, and a non-qBittorrent page.
  • Login lockout guard: after a failed login, the client backs off for 5, then 15, then 60 minutes. Without this, 30 s polling would trigger qBittorrent's IP ban, which also locks the user's browser out.
    • Only a literal Fails. response counts as bad credentials.
    • Session and cooldown state is keyed by a SHA-256 of URL, username and secret, so different servers never share cookies and no plaintext secret is used as a map key.
  • Activity:
    • warning and critical entries from log/main, using the last_known_id cursor with restart and id-regression handling (skipped when a log path is configured, since file tailing then covers them);
    • errored torrents, with ids bucketed per day so recurring errors re-fire;
    • connection status, emitted on a change and at most daily while unhealthy.
  • Privacy:
    • Tracker URLs are reduced to the host, and passkey, authkey, torrent_pass and pk values and hex passkey path segments are redacted from log messages. Private-tracker passkeys therefore never reach the DB, the UI or the AI analysis.
    • Passwords and cookies never appear in errors or logs.
  • Naming and state:
    • Activity types are snake_case (log_warning, log_critical, torrent_error, connection_status), the same as the other providers.
    • Session, cooldown, cursor and connection state is kept in bounded LRU caches with TTLs.
  • Log files: tails qbittorrent.log. The parser handles (N|I|W|C) <ts> - message.

Wiring

  • Backend registration, plus env seeding with QBITTORRENT_URL, QBITTORRENT_USERNAME, QBITTORRENT_PASSWORD and QBITTORRENT_LOGS_PATH. The URL alone is enough to seed.
  • /qbittorrent-logs mount, both Dockerfiles, the compose files and turbo.json env.
  • Proxy: requests are rejected for any server that uses username/password login, because a cookie session can't go through the proxy. API-key servers work, since qBittorrent 5.3 accepts the X-API-Key header (checked in webapplication.cpp). X-Api-Key is omitted when the key is empty.
  • Frontend: qBittorrent flipped to available, with username and "Password / API key" fields, both optional.
  • README: a "qBittorrent authentication" section.

Notes

  • GET /servers still returns apiKey unmasked. That is existing behaviour for every provider, but it now also covers a qBittorrent password, so masking it should be a follow-up.
  • The Docker log paths for the linuxserver and hotio images are not verified. They are documented as such and are configurable.
  • API-key mode is based on qBittorrent master (5.3) source.
  • Not yet tested against a live qBittorrent instance.

Testing

  • @logarr/provider-qbittorrent has 163 unit tests covering:
    • the client: auth modes, cookie handling and its fallback path, re-login, ban, cooldown with fake timers, redirects, timeouts, and secrets never leaking;
    • activity: cursor reset, the resync timer, id stability, day buckets and redaction;
    • the parser, including adversarial-input timing.
  • Backend: servers.service, the new server-seed.service.spec.ts, the proxy, and a LogFileProcessor end-to-end test (881 backend tests in total).
  • Frontend: edit-dialog validation for each provider, and the payload helper (406 frontend tests in total).
  • pnpm build, lint, typecheck and test all pass. pnpm install --frozen-lockfile is clean.

pnpm format:check already fails on master (pre-existing files, including api.ts and server-seed.service.ts, which this PR touches but does not reformat).

@vbrzezina

Copy link
Copy Markdown
Contributor Author

Part of a set of integration PRs. Overview, merge order and design notes: #64

@vbrzezina
vbrzezina marked this pull request as ready for review October 12, 2026 16:43

@itz4blitz itz4blitz left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

  1. The request proxy authenticates an API-key qBittorrent server with X-Api-Key only (apps/backend/src/modules/proxy/proxy.service.ts). Released qBittorrent (v5.2.0 / WebAPI v2.14.1) accepts Authorization: Bearer <key>. X-API-Key landed 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. Send Authorization: Bearer for providerId === 'qbittorrent' when the key is non-empty. You can keep X-Api-Key as well. Point the user-facing copy at >= 5.2, not >= 5.3.

  2. GET/POST/PUT /servers return the Drizzle row, including api_key (servers.service.ts). ServerResponseDto omits apiKey, 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 move lastActivitySync forward. 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.
  • redactSecrets misses 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_PASSWORD can 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.
@vbrzezina
vbrzezina force-pushed the feat/qbittorrent-integration branch from ea08133 to 411e5fe Compare October 13, 2026 16:54
@vbrzezina

Copy link
Copy Markdown
Contributor Author

Thanks for the review. I've rebased onto current master and pushed the changes.

  • Login: accepts the 204 No Content response from qBittorrent 5.2 and requires a fresh session cookie.
  • Auth headers: API keys are sent as Authorization: Bearer, and the proxy also sends X-Api-Key.
  • Secrets: server responses no longer return the stored API key or password (they expose hasApiKey). Seed errors are sanitised, secrets are redacted in provider logs, and pasted keys are trimmed. A secret can be cleared explicitly.
  • Synthetic rows: health and queue rows no longer advance the sync cursor (builds on fix(ingestion): keep synthetic activity timestamps out of the sync cursor #67).

I did a focused security pass on secret handling and found no leaks. One open point: the proxy follows redirects and would forward X-Api-Key to a redirect target. That behaviour predates this PR, so I left it alone.

@itz4blitz
itz4blitz merged commit ea34ac7 into itz4blitz:master Oct 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants