From 89e7cf9100f88b1ecf5c10ad342b176c951cdf22 Mon Sep 17 00:00:00 2001 From: Russell Martin Date: Sun, 15 Jan 2023 15:08:54 -0500 Subject: [PATCH] Remove dependence on qBittorrent session cookie being named `SID` --- qbittorrentapi/auth.py | 41 ++++++++++++++++++++++++------------ qbittorrentapi/auth.pyi | 1 + qbittorrentapi/decorators.py | 11 ++++++---- qbittorrentapi/request.py | 4 +++- 4 files changed, 39 insertions(+), 18 deletions(-) diff --git a/qbittorrentapi/auth.py b/qbittorrentapi/auth.py index d8fdf120c..3a9e54b02 100644 --- a/qbittorrentapi/auth.py +++ b/qbittorrentapi/auth.py @@ -4,6 +4,7 @@ from qbittorrentapi.decorators import login_required from qbittorrentapi.definitions import APINames from qbittorrentapi.definitions import ClientCache +from qbittorrentapi.exceptions import HTTP403Error from qbittorrentapi.exceptions import LoginFailed from qbittorrentapi.exceptions import UnsupportedQbittorrentVersion from qbittorrentapi.request import Request @@ -65,16 +66,19 @@ def auth(self): @property def is_logged_in(self): """ - Returns True/False for whether a log-in attempt was ever successfully - completed. + Returns True if low-overhead API call succeeds; False otherwise. - It isn't possible to know if qBittorrent will accept whatever SID is locally - cached...however, any request that is rejected because of the SID will be - automatically retried after a new SID is requested. + There isn't a reliable way to know if an existing session is still valid + without attempting to use it. qBittorrent invalidates cookies when they expire. - :returns: True/False for whether a log-in attempt was previously completed + :returns: True/False if current auth cookie is accepted by qBittorrent. """ - return bool(self._SID) + try: + self._post(_name=APINames.Application, _method="version") + except HTTP403Error: + return False + else: + return True def auth_log_in(self, username=None, password=None, **kwargs): """ @@ -93,13 +97,14 @@ def auth_log_in(self, username=None, password=None, **kwargs): self._initialize_context() creds = {"username": self.username, "password": self._password} - self._post(_name=APINames.Authorization, _method="login", data=creds, **kwargs) + auth_response = self._post( + _name=APINames.Authorization, _method="login", data=creds, **kwargs + ) - if not self.is_logged_in: + if auth_response.text != "Ok.": logger.debug("Login failed") raise LoginFailed() logger.debug("Login successful") - logger.debug("SID: %s", self._SID) # check if the connected qBittorrent is fully supported by this Client yet if self._RAISE_UNSUPPORTEDVERSIONERROR: @@ -117,12 +122,22 @@ def auth_log_in(self, username=None, password=None, **kwargs): @property def _SID(self): """ - Authorization cookie from qBittorrent. + Authorization session cookie from qBittorrent using default cookie name + `SID`. Backwards compatible for :meth:`~AuthAPIMixIn._session_cookie`. - :return: SID auth cookie from qBittorrent or None if one isn't already acquired + :return: Auth cookie value from qBittorrent or None if one isn't already acquired + """ + return self._session_cookie() + + def _session_cookie(self, cookie_name="SID"): + """ + Authorization session cookie from qBittorrent. + + :param cookie_name: Name of the authorization cookie; configurable after v4.5.0. + :return: Auth cookie value from qBittorrent or None if one isn't already acquired """ if self._http_session: - return self._http_session.cookies.get("SID", None) + return self._http_session.cookies.get(cookie_name, None) return None @login_required diff --git a/qbittorrentapi/auth.pyi b/qbittorrentapi/auth.pyi index 46bc0de37..bf832f255 100644 --- a/qbittorrentapi/auth.pyi +++ b/qbittorrentapi/auth.pyi @@ -31,4 +31,5 @@ class AuthAPIMixIn(Request): ) -> None: ... @property def _SID(self) -> Optional[Text]: ... + def _session_cookie(self, cookie_name: Text = "SID") -> Optional[Text]: ... def auth_log_out(self, **kwargs: KwargsT) -> None: ... diff --git a/qbittorrentapi/decorators.py b/qbittorrentapi/decorators.py index 3f62b97d1..bbb64fe05 100644 --- a/qbittorrentapi/decorators.py +++ b/qbittorrentapi/decorators.py @@ -70,7 +70,7 @@ def boring_method(): def login_required(func): - """Ensure client is logged in before calling API methods.""" + """Ensure client is logged in when calling API methods.""" def get_requests_kwargs(**kwargs): """Extract kwargs for performing transparent qBittorrent login.""" @@ -82,9 +82,12 @@ def get_requests_kwargs(**kwargs): @wraps(func) def wrapper(client, *args, **kwargs): - if not client.is_logged_in: - logger.debug("Not logged in...attempting login") - client.auth_log_in(**get_requests_kwargs(**kwargs)) + """ + Attempt API call; 403 is returned if the login is expired or the user + is banned. + + Attempt a login and if successful try the API call a final time. + """ try: return func(client, *args, **kwargs) except HTTP403Error: diff --git a/qbittorrentapi/request.py b/qbittorrentapi/request.py index 8bb7c00f3..e4ef52b32 100644 --- a/qbittorrentapi/request.py +++ b/qbittorrentapi/request.py @@ -729,8 +729,10 @@ def _trigger_session_initialization(self): During the next request, a new session will be created. """ - if hasattr(self, "_http_session") and isinstance(self._http_session, Session): + try: self._http_session.close() + except AttributeError: + pass self._http_session = None @staticmethod