Skip to content

Commit 42324c6

Browse files
authored
PYTHON-2973 Revert back to using quote_plus/unquote_plus (#767)
1 parent 3c3a85d commit 42324c6

File tree

9 files changed

+29
-33
lines changed

9 files changed

+29
-33
lines changed

.evergreen/config.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -523,7 +523,7 @@ functions:
523523
silent: true
524524
script: |
525525
cat <<'EOF' > "${PROJECT_DIRECTORY}/prepare_mongodb_aws.sh"
526-
alias urlencode='${python3_binary} -c "import sys, urllib.parse as ulp; sys.stdout.write(ulp.quote(sys.argv[1]))"'
526+
alias urlencode='${python3_binary} -c "import sys, urllib.parse as ulp; sys.stdout.write(ulp.quote_plus(sys.argv[1]))"'
527527
USER=$(urlencode ${iam_auth_ecs_account})
528528
PASS=$(urlencode ${iam_auth_ecs_secret_access_key})
529529
MONGODB_URI="mongodb://$USER:$PASS@localhost"
@@ -554,7 +554,7 @@ functions:
554554
script: |
555555
# DO NOT ECHO WITH XTRACE (which PREPARE_SHELL does)
556556
cat <<'EOF' > "${PROJECT_DIRECTORY}/prepare_mongodb_aws.sh"
557-
alias urlencode='${python3_binary} -c "import sys, urllib.parse as ulp; sys.stdout.write(ulp.quote(sys.argv[1]))"'
557+
alias urlencode='${python3_binary} -c "import sys, urllib.parse as ulp; sys.stdout.write(ulp.quote_plus(sys.argv[1]))"'
558558
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'
559559
USER=$(jsonkey AccessKeyId)
560560
USER=$(urlencode $USER)

doc/changelog.rst

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -166,13 +166,6 @@ Breaking Changes in 4.0
166166
:exc:`~pymongo.errors.InvalidURI` exception
167167
when it encounters unescaped percent signs in username and password when
168168
parsing MongoDB URIs.
169-
- :class:`~pymongo.mongo_client.MongoClient` now uses
170-
:py::func:`urllib.parse.unquote` rather than
171-
:py:func:`urllib.parse.unquote_plus`,
172-
meaning that plus signs ("+") are no longer converted to spaces (" "). This
173-
means that if you were previously quoting your login information using
174-
quote_plus, you must now switch to quote. Additionally, be aware that this
175-
change only occurs when parsing login information from the URI.
176169

177170
Notable improvements
178171
....................

doc/examples/authentication.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ Username and password must be percent-escaped with
1515

1616
>>> from pymongo import MongoClient
1717
>>> import urllib.parse
18-
>>> username = urllib.parse.quote('user')
18+
>>> username = urllib.parse.quote_plus('user')
1919
>>> username
2020
'user'
21-
>>> password = urllib.parse.quote('pass/word')
21+
>>> password = urllib.parse.quote_plus('pass/word')
2222
>>> password
2323
'pass%2Fword'
2424
>>> MongoClient('mongodb://%s:%[email protected]' % (username, password))

doc/migrate-to-pymongo4.rst

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -200,16 +200,6 @@ MongoClient raises exception when given unescaped percent sign in login info
200200
:exc:`~pymongo.errors.InvalidURI` exception
201201
when it encounters unescaped percent signs in username and password.
202202

203-
MongoClient uses `unquote` rather than `unquote_plus` for login info
204-
....................................................................
205-
206-
:class:`~pymongo.mongo_client.MongoClient` now uses
207-
:py:func:`urllib.parse.unquote` rather than
208-
:py:func:`urllib.parse.unquote_plus`, meaning that space characters are no
209-
longer converted to plus signs. This means that if you were previously
210-
quoting your login information using :py:func:`urllib.parse.quote_plus`, you
211-
must now switch to :py:func:`urllib.parse.quote`.
212-
213203
Database
214204
--------
215205

pymongo/auth.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,9 @@ def _authenticate_gssapi(credentials, sock_info):
319319

320320
if password is not None:
321321
if _USE_PRINCIPAL:
322+
# Note that, though we use unquote_plus for unquoting URI
323+
# options, we use quote here. Microsoft's UrlUnescape (used
324+
# by WinKerberos) doesn't support +.
322325
principal = ":".join((quote(username), quote(password)))
323326
result, ctx = kerberos.authGSSClientInit(
324327
service, principal, gssflags=kerberos.GSS_C_MUTUAL_FLAG)

pymongo/uri_parser.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import warnings
1919
import sys
2020

21-
from urllib.parse import unquote, unquote_plus
21+
from urllib.parse import unquote_plus
2222

2323
from pymongo.common import (
2424
SRV_SERVICE_NAME,
@@ -47,7 +47,7 @@ def _unquoted_percent(s):
4747
sub = s[i:i+3]
4848
# If unquoting yields the same string this means there was an
4949
# unquoted %.
50-
if unquote(sub) == sub:
50+
if unquote_plus(sub) == sub:
5151
return True
5252
return False
5353

@@ -65,14 +65,14 @@ def parse_userinfo(userinfo):
6565
if ('@' in userinfo or userinfo.count(':') > 1 or
6666
_unquoted_percent(userinfo)):
6767
raise InvalidURI("Username and password must be escaped according to "
68-
"RFC 3986, use urllib.parse.quote")
68+
"RFC 3986, use urllib.parse.quote_plus")
6969

7070
user, _, passwd = userinfo.partition(":")
7171
# No password is expected with GSSAPI authentication.
7272
if not user:
7373
raise InvalidURI("The empty string is not valid username.")
7474

75-
return unquote(user), unquote(passwd)
75+
return unquote_plus(user), unquote_plus(passwd)
7676

7777

7878
def parse_ipv6_literal_host(entity, default_port):
@@ -430,9 +430,7 @@ def parse_uri(uri, default_port=DEFAULT_PORT, validate=True, warn=False,
430430
431431
.. versionchanged:: 4.0
432432
To better follow RFC 3986, unquoted percent signs ("%") are no longer
433-
supported and plus signs ("+") are no longer decoded into spaces (" ")
434-
when decoding username and password. To avoid these issues, use
435-
:py:func:`urllib.parse.quote` when building the URI.
433+
supported.
436434
437435
.. versionchanged:: 3.9
438436
Added the ``normalize`` parameter.

test/test_ssl.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020

2121
sys.path[0:0] = [""]
2222

23-
from urllib.parse import quote
23+
from urllib.parse import quote_plus
2424

2525
from pymongo import MongoClient, ssl_support
2626
from pymongo.errors import (ConfigurationError,
@@ -526,7 +526,7 @@ def test_mongodb_x509_auth(self):
526526

527527
uri = ('mongodb://%s@%s:%d/?authMechanism='
528528
'MONGODB-X509' % (
529-
quote(MONGODB_X509_USERNAME), host, port))
529+
quote_plus(MONGODB_X509_USERNAME), host, port))
530530
client = MongoClient(uri,
531531
ssl=True,
532532
tlsAllowInvalidCertificates=True,
@@ -546,7 +546,7 @@ def test_mongodb_x509_auth(self):
546546
# Auth should fail if username and certificate do not match
547547
uri = ('mongodb://%s@%s:%d/?authMechanism='
548548
'MONGODB-X509' % (
549-
quote("not the username"), host, port))
549+
quote_plus("not the username"), host, port))
550550

551551
bad_client = MongoClient(
552552
uri, ssl=True, tlsAllowInvalidCertificates=True,
@@ -571,7 +571,7 @@ def test_mongodb_x509_auth(self):
571571
# Invalid certificate (using CA certificate as client certificate)
572572
uri = ('mongodb://%s@%s:%d/?authMechanism='
573573
'MONGODB-X509' % (
574-
quote(MONGODB_X509_USERNAME), host, port))
574+
quote_plus(MONGODB_X509_USERNAME), host, port))
575575
try:
576576
connected(MongoClient(uri,
577577
ssl=True,

test/test_uri_parser.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import copy
1818
import sys
1919
import warnings
20+
from urllib.parse import quote_plus
2021

2122
sys.path[0:0] = [""]
2223

@@ -43,7 +44,7 @@ def test_validate_userinfo(self):
4344
self.assertTrue(parse_userinfo('user:password'))
4445
self.assertEqual(('us:r', 'p@ssword'),
4546
parse_userinfo('us%3Ar:p%40ssword'))
46-
self.assertEqual(('us+er', 'p+ssword'),
47+
self.assertEqual(('us er', 'p ssword'),
4748
parse_userinfo('us+er:p+ssword'))
4849
self.assertEqual(('us er', 'p ssword'),
4950
parse_userinfo('us%20er:p%20ssword'))
@@ -512,6 +513,14 @@ def test_redact_AWS_SESSION_TOKEN(self):
512513
'quote_plus?'):
513514
parse_uri(uri)
514515

516+
def test_special_chars(self):
517+
user = "user@ /9+:?~!$&'()*+,;="
518+
pwd = "pwd@ /9+:?~!$&'()*+,;="
519+
uri = 'mongodb://%s:%s@localhost' % (quote_plus(user), quote_plus(pwd))
520+
res = parse_uri(uri)
521+
self.assertEqual(user, res['username'])
522+
self.assertEqual(pwd, res['password'])
523+
515524

516525
if __name__ == "__main__":
517526
unittest.main()

test/test_uri_spec.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,9 @@ def run_scenario(self):
143143
options['database'] += "." + options['collection']
144144
for elm in auth:
145145
if auth[elm] is not None:
146+
# We have to do this because while the spec requires
147+
# "+"->"+", unquote_plus does "+"->" "
148+
options[elm] = options[elm].replace(" ", "+")
146149
self.assertEqual(auth[elm], options[elm],
147150
"Expected %s but got %s"
148151
% (auth[elm], options[elm]))

0 commit comments

Comments
 (0)