From 42324c69cf1028d4530c7ea3b9f20e723baae713 Mon Sep 17 00:00:00 2001 From: Julius Park Date: Fri, 29 Oct 2021 16:30:55 -0700 Subject: [PATCH] PYTHON-2973 Revert back to using quote_plus/unquote_plus (#767) --- .evergreen/config.yml | 4 ++-- doc/changelog.rst | 7 ------- doc/examples/authentication.rst | 4 ++-- doc/migrate-to-pymongo4.rst | 10 ---------- pymongo/auth.py | 3 +++ pymongo/uri_parser.py | 12 +++++------- test/test_ssl.py | 8 ++++---- test/test_uri_parser.py | 11 ++++++++++- test/test_uri_spec.py | 3 +++ 9 files changed, 29 insertions(+), 33 deletions(-) diff --git a/.evergreen/config.yml b/.evergreen/config.yml index 363b15ff5..f10652818 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -523,7 +523,7 @@ functions: silent: true script: | cat <<'EOF' > "${PROJECT_DIRECTORY}/prepare_mongodb_aws.sh" - alias urlencode='${python3_binary} -c "import sys, urllib.parse as ulp; sys.stdout.write(ulp.quote(sys.argv[1]))"' + alias urlencode='${python3_binary} -c "import sys, urllib.parse as ulp; sys.stdout.write(ulp.quote_plus(sys.argv[1]))"' USER=$(urlencode ${iam_auth_ecs_account}) PASS=$(urlencode ${iam_auth_ecs_secret_access_key}) MONGODB_URI="mongodb://$USER:$PASS@localhost" @@ -554,7 +554,7 @@ functions: script: | # DO NOT ECHO WITH XTRACE (which PREPARE_SHELL does) cat <<'EOF' > "${PROJECT_DIRECTORY}/prepare_mongodb_aws.sh" - alias urlencode='${python3_binary} -c "import sys, urllib.parse as ulp; sys.stdout.write(ulp.quote(sys.argv[1]))"' + alias urlencode='${python3_binary} -c "import sys, urllib.parse as ulp; sys.stdout.write(ulp.quote_plus(sys.argv[1]))"' alias jsonkey='${python3_binary} -c "import json,sys;sys.stdout.write(json.load(sys.stdin)[sys.argv[1]])" < ${DRIVERS_TOOLS}/.evergreen/auth_aws/creds.json' USER=$(jsonkey AccessKeyId) USER=$(urlencode $USER) diff --git a/doc/changelog.rst b/doc/changelog.rst index 3b3b664e9..acb0949d8 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -166,13 +166,6 @@ Breaking Changes in 4.0 :exc:`~pymongo.errors.InvalidURI` exception when it encounters unescaped percent signs in username and password when parsing MongoDB URIs. -- :class:`~pymongo.mongo_client.MongoClient` now uses - :py::func:`urllib.parse.unquote` rather than - :py:func:`urllib.parse.unquote_plus`, - meaning that plus signs ("+") are no longer converted to spaces (" "). This - means that if you were previously quoting your login information using - quote_plus, you must now switch to quote. Additionally, be aware that this - change only occurs when parsing login information from the URI. Notable improvements .................... diff --git a/doc/examples/authentication.rst b/doc/examples/authentication.rst index bd6036e46..1e0f133a5 100644 --- a/doc/examples/authentication.rst +++ b/doc/examples/authentication.rst @@ -15,10 +15,10 @@ Username and password must be percent-escaped with >>> from pymongo import MongoClient >>> import urllib.parse - >>> username = urllib.parse.quote('user') + >>> username = urllib.parse.quote_plus('user') >>> username 'user' - >>> password = urllib.parse.quote('pass/word') + >>> password = urllib.parse.quote_plus('pass/word') >>> password 'pass%2Fword' >>> MongoClient('mongodb://%s:%s@127.0.0.1' % (username, password)) diff --git a/doc/migrate-to-pymongo4.rst b/doc/migrate-to-pymongo4.rst index 994f8450a..8a0734670 100644 --- a/doc/migrate-to-pymongo4.rst +++ b/doc/migrate-to-pymongo4.rst @@ -200,16 +200,6 @@ MongoClient raises exception when given unescaped percent sign in login info :exc:`~pymongo.errors.InvalidURI` exception when it encounters unescaped percent signs in username and password. -MongoClient uses `unquote` rather than `unquote_plus` for login info -.................................................................... - -:class:`~pymongo.mongo_client.MongoClient` now uses -:py:func:`urllib.parse.unquote` rather than -:py:func:`urllib.parse.unquote_plus`, meaning that space characters are no -longer converted to plus signs. This means that if you were previously -quoting your login information using :py:func:`urllib.parse.quote_plus`, you -must now switch to :py:func:`urllib.parse.quote`. - Database -------- diff --git a/pymongo/auth.py b/pymongo/auth.py index 0fec2c775..b94698086 100644 --- a/pymongo/auth.py +++ b/pymongo/auth.py @@ -319,6 +319,9 @@ def _authenticate_gssapi(credentials, sock_info): if password is not None: if _USE_PRINCIPAL: + # Note that, though we use unquote_plus for unquoting URI + # options, we use quote here. Microsoft's UrlUnescape (used + # by WinKerberos) doesn't support +. principal = ":".join((quote(username), quote(password))) result, ctx = kerberos.authGSSClientInit( service, principal, gssflags=kerberos.GSS_C_MUTUAL_FLAG) diff --git a/pymongo/uri_parser.py b/pymongo/uri_parser.py index 5d97eb483..23db48bf4 100644 --- a/pymongo/uri_parser.py +++ b/pymongo/uri_parser.py @@ -18,7 +18,7 @@ import re import warnings import sys -from urllib.parse import unquote, unquote_plus +from urllib.parse import unquote_plus from pymongo.common import ( SRV_SERVICE_NAME, @@ -47,7 +47,7 @@ def _unquoted_percent(s): sub = s[i:i+3] # If unquoting yields the same string this means there was an # unquoted %. - if unquote(sub) == sub: + if unquote_plus(sub) == sub: return True return False @@ -65,14 +65,14 @@ def parse_userinfo(userinfo): if ('@' in userinfo or userinfo.count(':') > 1 or _unquoted_percent(userinfo)): raise InvalidURI("Username and password must be escaped according to " - "RFC 3986, use urllib.parse.quote") + "RFC 3986, use urllib.parse.quote_plus") user, _, passwd = userinfo.partition(":") # No password is expected with GSSAPI authentication. if not user: raise InvalidURI("The empty string is not valid username.") - return unquote(user), unquote(passwd) + return unquote_plus(user), unquote_plus(passwd) def parse_ipv6_literal_host(entity, default_port): @@ -430,9 +430,7 @@ def parse_uri(uri, default_port=DEFAULT_PORT, validate=True, warn=False, .. versionchanged:: 4.0 To better follow RFC 3986, unquoted percent signs ("%") are no longer - supported and plus signs ("+") are no longer decoded into spaces (" ") - when decoding username and password. To avoid these issues, use - :py:func:`urllib.parse.quote` when building the URI. + supported. .. versionchanged:: 3.9 Added the ``normalize`` parameter. diff --git a/test/test_ssl.py b/test/test_ssl.py index df44c31dc..0162eb3a0 100644 --- a/test/test_ssl.py +++ b/test/test_ssl.py @@ -20,7 +20,7 @@ import sys sys.path[0:0] = [""] -from urllib.parse import quote +from urllib.parse import quote_plus from pymongo import MongoClient, ssl_support from pymongo.errors import (ConfigurationError, @@ -526,7 +526,7 @@ class TestSSL(IntegrationTest): uri = ('mongodb://%s@%s:%d/?authMechanism=' 'MONGODB-X509' % ( - quote(MONGODB_X509_USERNAME), host, port)) + quote_plus(MONGODB_X509_USERNAME), host, port)) client = MongoClient(uri, ssl=True, tlsAllowInvalidCertificates=True, @@ -546,7 +546,7 @@ class TestSSL(IntegrationTest): # Auth should fail if username and certificate do not match uri = ('mongodb://%s@%s:%d/?authMechanism=' 'MONGODB-X509' % ( - quote("not the username"), host, port)) + quote_plus("not the username"), host, port)) bad_client = MongoClient( uri, ssl=True, tlsAllowInvalidCertificates=True, @@ -571,7 +571,7 @@ class TestSSL(IntegrationTest): # Invalid certificate (using CA certificate as client certificate) uri = ('mongodb://%s@%s:%d/?authMechanism=' 'MONGODB-X509' % ( - quote(MONGODB_X509_USERNAME), host, port)) + quote_plus(MONGODB_X509_USERNAME), host, port)) try: connected(MongoClient(uri, ssl=True, diff --git a/test/test_uri_parser.py b/test/test_uri_parser.py index 33c727604..7e00bd976 100644 --- a/test/test_uri_parser.py +++ b/test/test_uri_parser.py @@ -17,6 +17,7 @@ import copy import sys import warnings +from urllib.parse import quote_plus sys.path[0:0] = [""] @@ -43,7 +44,7 @@ class TestURI(unittest.TestCase): self.assertTrue(parse_userinfo('user:password')) self.assertEqual(('us:r', 'p@ssword'), parse_userinfo('us%3Ar:p%40ssword')) - self.assertEqual(('us+er', 'p+ssword'), + self.assertEqual(('us er', 'p ssword'), parse_userinfo('us+er:p+ssword')) self.assertEqual(('us er', 'p ssword'), parse_userinfo('us%20er:p%20ssword')) @@ -512,6 +513,14 @@ class TestURI(unittest.TestCase): 'quote_plus?'): parse_uri(uri) + def test_special_chars(self): + user = "user@ /9+:?~!$&'()*+,;=" + pwd = "pwd@ /9+:?~!$&'()*+,;=" + uri = 'mongodb://%s:%s@localhost' % (quote_plus(user), quote_plus(pwd)) + res = parse_uri(uri) + self.assertEqual(user, res['username']) + self.assertEqual(pwd, res['password']) + if __name__ == "__main__": unittest.main() diff --git a/test/test_uri_spec.py b/test/test_uri_spec.py index 1bb5fd2f5..59457b57a 100644 --- a/test/test_uri_spec.py +++ b/test/test_uri_spec.py @@ -143,6 +143,9 @@ def create_test(test, test_workdir): options['database'] += "." + options['collection'] for elm in auth: if auth[elm] is not None: + # We have to do this because while the spec requires + # "+"->"+", unquote_plus does "+"->" " + options[elm] = options[elm].replace(" ", "+") self.assertEqual(auth[elm], options[elm], "Expected %s but got %s" % (auth[elm], options[elm]))