From 70b5fa8e31e0afe3af414cb5c2e136be41e278a1 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 10 Aug 2020 16:41:51 +0100 Subject: [PATCH 1/2] Handle URL quoting username and password components --- httpx/_models.py | 14 +++++++++----- tests/models/test_url.py | 12 ++++++++++++ 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/httpx/_models.py b/httpx/_models.py index d388ea9b78..a334711fb6 100644 --- a/httpx/_models.py +++ b/httpx/_models.py @@ -7,7 +7,7 @@ import warnings from collections.abc import MutableMapping from http.cookiejar import Cookie, CookieJar -from urllib.parse import parse_qsl, urlencode +from urllib.parse import parse_qsl, quote, unquote, urlencode import chardet import rfc3986 @@ -54,6 +54,10 @@ ) +def _quote(component: typing.Optional[str]) -> typing.Optional[str]: + return None if component is None else quote(component) + + class URL: def __init__(self, url: URLTypes = "", params: QueryParamTypes = None) -> None: if isinstance(url, str): @@ -94,12 +98,12 @@ def userinfo(self) -> str: @property def username(self) -> str: userinfo = self._uri_reference.userinfo or "" - return userinfo.partition(":")[0] + return unquote(userinfo.partition(":")[0]) @property def password(self) -> str: userinfo = self._uri_reference.userinfo or "" - return userinfo.partition(":")[2] + return unquote(userinfo.partition(":")[2]) @property def host(self) -> str: @@ -169,8 +173,8 @@ def copy_with(self, **kwargs: typing.Any) -> "URL": ): host = kwargs.pop("host", self.host) port = kwargs.pop("port", self.port) - username = kwargs.pop("username", self.username) - password = kwargs.pop("password", self.password) + username = _quote(kwargs.pop("username", self.username)) + password = _quote(kwargs.pop("password", self.password)) authority = host if port is not None: diff --git a/tests/models/test_url.py b/tests/models/test_url.py index 6c0fdac82b..a5dee97aad 100644 --- a/tests/models/test_url.py +++ b/tests/models/test_url.py @@ -185,3 +185,15 @@ def test_url_copywith_for_authority(): for k, v in copy_with_kwargs.items(): assert getattr(new, k) == v assert str(new) == "https://username:password@example.net:444" + + +def test_url_copywith_for_userinfo(): + copy_with_kwargs = { + "username": "tom@example.org", + "password": "abc123@ %", + } + url = URL("https://example.org") + new = url.copy_with(**copy_with_kwargs) + assert str(new) == "https://tom%40example.org:abc123%40%20%25@example.org" + assert new.username == "tom@example.org" + assert new.password == "abc123@ %" From 71e9576a6277a644017e8326437d50a7b1f1256a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 11 Aug 2020 10:17:42 +0100 Subject: [PATCH 2/2] Tweak userinfo quoting --- httpx/_models.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/httpx/_models.py b/httpx/_models.py index 4e47893ef2..44554e29eb 100644 --- a/httpx/_models.py +++ b/httpx/_models.py @@ -56,10 +56,6 @@ ) -def _quote(component: typing.Optional[str]) -> typing.Optional[str]: - return None if component is None else quote(component) - - class URL: def __init__(self, url: URLTypes = "", params: QueryParamTypes = None) -> None: if isinstance(url, str): @@ -179,8 +175,8 @@ def copy_with(self, **kwargs: typing.Any) -> "URL": ): host = kwargs.pop("host", self.host) port = kwargs.pop("port", self.port) - username = _quote(kwargs.pop("username", self.username)) - password = _quote(kwargs.pop("password", self.password)) + username = quote(kwargs.pop("username", self.username) or "") + password = quote(kwargs.pop("password", self.password) or "") authority = host if port is not None: @@ -197,7 +193,7 @@ def copy_with(self, **kwargs: typing.Any) -> "URL": def join(self, url: URLTypes) -> "URL": """ - Return an absolute URL, using given this URL as the base. + Return an absolute URL, using this URL as the base. """ if self.is_relative_url: return URL(url)