Fix: qBittorrent 5.2.0+ login probe fails (HTTP 204 not handled)
qBittorrent 5.2.0 changed /api/v2/auth/login to return HTTP 204 (No Content) on success instead of HTTP 200 with body 'Ok.'. The adapter required the body to equal 'Ok.', so every login on 5.2.0+ failed with 'HTTP 204 body=' — the connection probe and all torrent actions were broken. Treat login as successful on the SID auth cookie and/or a success body: 'Ok.' (<=5.1) or an empty HTTP 204 (>=5.2.0). Still reject bad creds, which qBittorrent reports as HTTP 200 + 'Fails.' (not a 4xx). Tests: 204-empty -> success, SID-cookie+empty-body -> success, 'Fails.' (even with a stale cookie) -> failure.
This commit is contained in:
parent
807ac39570
commit
45f91fd318
2 changed files with 58 additions and 1 deletions
|
|
@ -117,7 +117,19 @@ class QBittorrentAdapter:
|
||||||
headers={'Referer': self._url},
|
headers={'Referer': self._url},
|
||||||
timeout=self.DEFAULT_TIMEOUT,
|
timeout=self.DEFAULT_TIMEOUT,
|
||||||
)
|
)
|
||||||
if not resp.ok or resp.text.strip() != 'Ok.':
|
body = resp.text.strip()
|
||||||
|
has_sid = bool(sess.cookies.get('SID'))
|
||||||
|
# qBittorrent reports BAD credentials as HTTP 200 + body "Fails."
|
||||||
|
# (it does NOT use a 4xx). SUCCESS is the SID auth cookie and/or a
|
||||||
|
# success body: "Ok." on <= 5.1, or an empty HTTP 204 on 5.2.0+,
|
||||||
|
# which changed /api/v2/auth/login to return 204 No Content.
|
||||||
|
# The old check required body == "Ok." and so rejected 5.2.0+.
|
||||||
|
login_ok = (
|
||||||
|
resp.ok
|
||||||
|
and body.lower() != 'fails.'
|
||||||
|
and (has_sid or resp.status_code == 204 or body in ('', 'Ok.'))
|
||||||
|
)
|
||||||
|
if not login_ok:
|
||||||
logger.error("qBittorrent login failed: HTTP %s body=%r", resp.status_code, resp.text[:200])
|
logger.error("qBittorrent login failed: HTTP %s body=%r", resp.status_code, resp.text[:200])
|
||||||
return None
|
return None
|
||||||
self._session = sess
|
self._session = sess
|
||||||
|
|
|
||||||
|
|
@ -150,6 +150,51 @@ def test_qbit_login_failure_returns_none() -> None:
|
||||||
assert sess is None
|
assert sess is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_qbit_login_accepts_204_no_content() -> None:
|
||||||
|
"""qBittorrent 5.2.0+ returns HTTP 204 with an empty body on a successful
|
||||||
|
login (was HTTP 200 + 'Ok.'). The adapter must treat that as success even
|
||||||
|
when no SID cookie is visible to us."""
|
||||||
|
adapter = _qbit_with_config()
|
||||||
|
fake_session = MagicMock()
|
||||||
|
fake_session.cookies.get.return_value = None # no SID surfaced
|
||||||
|
resp = _mock_response(204, text='')
|
||||||
|
resp.text = ''
|
||||||
|
fake_session.post.return_value = resp
|
||||||
|
with patch('core.torrent_clients.qbittorrent.http_requests.Session',
|
||||||
|
return_value=fake_session):
|
||||||
|
sess = adapter._ensure_session_sync()
|
||||||
|
assert sess is not None
|
||||||
|
|
||||||
|
|
||||||
|
def test_qbit_login_accepts_sid_cookie_with_empty_body() -> None:
|
||||||
|
"""A SID auth cookie is the authoritative success signal regardless of body."""
|
||||||
|
adapter = _qbit_with_config()
|
||||||
|
fake_session = MagicMock()
|
||||||
|
fake_session.cookies.get.return_value = 'SID-abc123'
|
||||||
|
resp = _mock_response(200, text='')
|
||||||
|
resp.text = ''
|
||||||
|
fake_session.post.return_value = resp
|
||||||
|
with patch('core.torrent_clients.qbittorrent.http_requests.Session',
|
||||||
|
return_value=fake_session):
|
||||||
|
sess = adapter._ensure_session_sync()
|
||||||
|
assert sess is not None
|
||||||
|
|
||||||
|
|
||||||
|
def test_qbit_login_rejects_fails_even_with_stale_cookie() -> None:
|
||||||
|
"""Bad creds: qBittorrent returns HTTP 200 'Fails.' (not a 4xx). Must fail
|
||||||
|
even if a stale SID cookie lingers on the session."""
|
||||||
|
adapter = _qbit_with_config()
|
||||||
|
fake_session = MagicMock()
|
||||||
|
fake_session.cookies.get.return_value = 'SID-stale'
|
||||||
|
resp = _mock_response(200, text='Fails.')
|
||||||
|
resp.text = 'Fails.'
|
||||||
|
fake_session.post.return_value = resp
|
||||||
|
with patch('core.torrent_clients.qbittorrent.http_requests.Session',
|
||||||
|
return_value=fake_session):
|
||||||
|
sess = adapter._ensure_session_sync()
|
||||||
|
assert sess is None
|
||||||
|
|
||||||
|
|
||||||
def test_qbit_parse_status_normalises_native_fields() -> None:
|
def test_qbit_parse_status_normalises_native_fields() -> None:
|
||||||
adapter = _qbit_with_config()
|
adapter = _qbit_with_config()
|
||||||
status = adapter._parse_status({
|
status = adapter._parse_status({
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue