Skip to content

Commit

Permalink
Allow users to skip URL scheme detection with FORCE_SCHEME_FROM_HOST (f…
Browse files Browse the repository at this point in the history
…ixes #54) (#56)
  • Loading branch information
rmartin16 authored May 2, 2021
1 parent df20ca4 commit 7afa989
Show file tree
Hide file tree
Showing 5 changed files with 66 additions and 27 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
Version 2021.5.21 (1 may 2021)
- Allow users to force a specific communications scheme with FORCE_SCHEME_FROM_HOST (fixes #54)

Version 2021.4.20 (11 apr 2021)
- Add support for ratio limit and seeding time limit when adding torrents

Expand Down
4 changes: 4 additions & 0 deletions qbittorrentapi/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ class Client(
:param VERIFY_WEBUI_CERTIFICATE: Set to False to skip verify certificate for HTTPS connections;
for instance, if the connection is using a self-signed certificate. Not setting this to False for self-signed
certs will cause a APIConnectionError exception to be raised.
:param EXTRA_HEADERS: Dictionary of HTTP Headers to include in all requests made to qBittorrent.
:param FORCE_SCHEME_FROM_HOST: If a scheme (i.e. http or https) is specifed in host, it will be used regardless of
whether qBittorrent is configured for HTTP or HTTPS communication. Normally, this client will attempt to
determine which scheme qBittorrent is actually listening on...but this can cause problems in rare cases.
:param RAISE_NOTIMPLEMENTEDERROR_FOR_UNIMPLEMENTED_API_ENDPOINTS: Some Endpoints may not be implemented in older
versions of qBittorrent. Setting this to True will raise a NotImplementedError instead of just returning None.
:param DISABLE_LOGGING_DEBUG_OUTPUT: Turn off debug output from logging for this package as well as Requests & urllib3.
Expand Down
57 changes: 31 additions & 26 deletions qbittorrentapi/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ def _initialize_lesser(
self,
EXTRA_HEADERS=None,
VERIFY_WEBUI_CERTIFICATE=True,
FORCE_SCHEME_FROM_HOST=False,
RAISE_UNIMPLEMENTEDERROR_FOR_UNIMPLEMENTED_API_ENDPOINTS=False,
RAISE_NOTIMPLEMENTEDERROR_FOR_UNIMPLEMENTED_API_ENDPOINTS=False,
VERBOSE_RESPONSE_LOGGING=False,
Expand All @@ -160,6 +161,7 @@ def _initialize_lesser(
self._VERBOSE_RESPONSE_LOGGING = bool(VERBOSE_RESPONSE_LOGGING)
self._PRINT_STACK_FOR_EACH_REQUEST = bool(PRINT_STACK_FOR_EACH_REQUEST)
self._SIMPLE_RESPONSES = bool(SIMPLE_RESPONSES)
self._FORCE_SCHEME_FROM_HOST = bool(FORCE_SCHEME_FROM_HOST)
self._RAISE_UNIMPLEMENTEDERROR_FOR_UNIMPLEMENTED_API_ENDPOINTS = bool(
RAISE_UNIMPLEMENTEDERROR_FOR_UNIMPLEMENTED_API_ENDPOINTS
or RAISE_NOTIMPLEMENTEDERROR_FOR_UNIMPLEMENTED_API_ENDPOINTS
Expand Down Expand Up @@ -248,7 +250,7 @@ def retry_backoff(retry_count):
# then will sleep for 0s then .3s, then .6s, etc. between retries.
backoff_time = _retry_backoff_factor * (2 ** ((retry_count + 1) - 1))
sleep(backoff_time if backoff_time <= 10 else 10)
logger.debug("Retry attempt %d" % (retry_count + 1))
logger.debug("Retry attempt %d", (retry_count + 1))

max_retries = _retries if _retries > 1 else 2
for retry in range(0, (max_retries + 1)):
Expand All @@ -257,7 +259,7 @@ def retry_backoff(retry_count):
except HTTPError as e:
# retry the request for HTTP 500 statuses;
# raise immediately for other HTTP errors (e.g. 4XX statuses)
if not isinstance(e, HTTP5XXError) or retry >= max_retries:
if retry >= max_retries or not isinstance(e, HTTP5XXError):
raise
except Exception as e:
if retry >= max_retries:
Expand Down Expand Up @@ -316,6 +318,7 @@ def _build_url(self, api_namespace, api_method):
base_url=self._API_BASE_URL,
host=self.host,
port=self.port,
force_user_scheme=self._FORCE_SCHEME_FROM_HOST,
)
return self._build_url_path(
base_url=self._API_BASE_URL,
Expand All @@ -325,7 +328,7 @@ def _build_url(self, api_namespace, api_method):
)

@staticmethod
def _build_base_url(base_url=None, host="", port=None):
def _build_base_url(base_url=None, host="", port=None, force_user_scheme=False):
"""
Determine the Base URL for the Web API endpoints.
Expand Down Expand Up @@ -353,7 +356,7 @@ def _build_base_url(base_url=None, host="", port=None):
if not host.lower().startswith(("http:", "https:", "//")):
host = "//" + host
base_url = urlparse(url=host)
logger.debug("Parsed user URL: %s" % repr(base_url))
logger.debug("Parsed user URL: %r", base_url)
# default to HTTP if user didn't specify
user_scheme = base_url.scheme
base_url = base_url._replace(scheme="http") if not user_scheme else base_url
Expand All @@ -363,33 +366,35 @@ def _build_base_url(base_url=None, host="", port=None):
base_url = base_url._replace(netloc="%s:%s" % (base_url.netloc, port))

# detect whether Web API is configured for HTTP or HTTPS
logger.debug("Detecting scheme for URL...")
try:
# skip verification here...if there's a problem, we'll catch it during the actual API call
r = requests_head(base_url.geturl(), allow_redirects=True, verify=False)
# if WebUI eventually supports sending a redirect from HTTP to HTTPS then
# Requests will automatically provide a URL using HTTPS.
# For instance, the URL returned below will use the HTTPS scheme.
# >>> requests.head('http://grc.com', allow_redirects=True).url
scheme = urlparse(r.url).scheme
except requests_exceptions.RequestException:
# assume alternative scheme will work...we'll fail later if neither are working
scheme = alt_scheme

# use detected scheme
logger.debug("Using %s scheme" % scheme.upper())
base_url = base_url._replace(scheme=scheme)
if user_scheme and user_scheme != scheme:
logger.warning(
"Using '%s' instead of requested '%s' to communicate with qBittorrent"
% (scheme, user_scheme)
)
if not (user_scheme and force_user_scheme):
logger.debug("Detecting scheme for URL...")
try:
# skip verification here...if there's a problem, we'll catch it during the actual API call
r = requests_head(base_url.geturl(), allow_redirects=True, verify=False)
# if WebUI eventually supports sending a redirect from HTTP to HTTPS then
# Requests will automatically provide a URL using HTTPS.
# For instance, the URL returned below will use the HTTPS scheme.
# >>> requests.head('http://grc.com', allow_redirects=True).url
scheme = urlparse(r.url).scheme
except requests_exceptions.RequestException:
# assume alternative scheme will work...we'll fail later if neither are working
scheme = alt_scheme

# use detected scheme
logger.debug("Using %s scheme", scheme.upper())
base_url = base_url._replace(scheme=scheme)
if user_scheme and user_scheme != scheme:
logger.warning(
"Using '%s' instead of requested '%s' to communicate with qBittorrent",
scheme,
user_scheme,
)

# ensure URL always ends with a forward-slash
base_url = base_url.geturl()
if not base_url.endswith("/"):
base_url = base_url + "/"
logger.debug("Base URL: %s" % base_url)
logger.debug("Base URL: %s", base_url)

return base_url

Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

setup(
name="qbittorrent-api",
version="2021.4.20",
version="2021.5.21",
packages=find_packages(exclude=["*.tests", "*.tests.*", "tests.*", "tests"]),
include_package_data=True,
install_requires=[
Expand Down
27 changes: 27 additions & 0 deletions tests/test_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,33 @@ def test_port_from_host(app_version):
assert client.app.version == app_version


def test_force_user_scheme(app_version):
default_host = environ["PYTHON_QBITTORRENTAPI_HOST"]

client = Client(
host="http://" + default_host,
VERIFY_WEBUI_CERTIFICATE=False,
FORCE_SCHEME_FROM_HOST=True,
)
assert client.app.version == app_version
assert client._API_BASE_URL.startswith("http://")

client = Client(
host=default_host, VERIFY_WEBUI_CERTIFICATE=False, FORCE_SCHEME_FROM_HOST=True
)
assert client.app.version == app_version
assert client._API_BASE_URL.startswith("http://")

client = Client(
host="https://" + default_host,
VERIFY_WEBUI_CERTIFICATE=False,
FORCE_SCHEME_FROM_HOST=True,
)
with pytest.raises(APIConnectionError):
assert client.app.version == app_version
assert client._API_BASE_URL.startswith("https://")


def test_log_out(client):
client.auth_log_out()
with pytest.raises(Forbidden403Error):
Expand Down

0 comments on commit 7afa989

Please sign in to comment.