Skip to content

Commit 5513b9d

Browse files
committed
Unquote connection string components properly
When a connection string component contains characters that have a special meaning in the URI (e.g. '@' or '='), percent-encoding must be used. asyncpg must take care to unquote the parsed components correctly, and it doesn't currently. Additionally, this makes asyncpg follow the libpq's behavior of parsing the authentication part of netloc, i.e. split on the first '@' and not the last. Fixes: #418 Fixes: #471
1 parent e8e238f commit 5513b9d

File tree

2 files changed

+62
-13
lines changed

2 files changed

+62
-13
lines changed

Diff for: asyncpg/connect_utils.py

+27-13
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ def _validate_port_spec(hosts, port):
153153
return port
154154

155155

156-
def _parse_hostlist(hostlist, port):
156+
def _parse_hostlist(hostlist, port, *, unquote=False):
157157
if ',' in hostlist:
158158
# A comma-separated list of host addresses.
159159
hostspecs = hostlist.split(',')
@@ -185,9 +185,14 @@ def _parse_hostlist(hostlist, port):
185185
addr = hostspec
186186
hostspec_port = ''
187187

188+
if unquote:
189+
addr = urllib.parse.unquote(addr)
190+
188191
hosts.append(addr)
189192
if not port:
190193
if hostspec_port:
194+
if unquote:
195+
hostspec_port = urllib.parse.unquote(hostspec_port)
191196
hostlist_ports.append(int(hostspec_port))
192197
else:
193198
hostlist_ports.append(default_port[i])
@@ -213,25 +218,34 @@ def _parse_connect_dsn_and_args(*, dsn, host, port, user,
213218
'invalid DSN: scheme is expected to be either '
214219
'"postgresql" or "postgres", got {!r}'.format(parsed.scheme))
215220

216-
if not host and parsed.netloc:
221+
if parsed.netloc:
217222
if '@' in parsed.netloc:
218-
auth, _, hostspec = parsed.netloc.partition('@')
223+
dsn_auth, _, dsn_hostspec = parsed.netloc.partition('@')
219224
else:
220-
hostspec = parsed.netloc
225+
dsn_hostspec = parsed.netloc
226+
dsn_auth = ''
227+
else:
228+
dsn_auth = dsn_hostspec = ''
229+
230+
if dsn_auth:
231+
dsn_user, _, dsn_password = dsn_auth.partition(':')
232+
else:
233+
dsn_user = dsn_password = ''
221234

222-
if hostspec:
223-
host, port = _parse_hostlist(hostspec, port)
235+
if not host and dsn_hostspec:
236+
host, port = _parse_hostlist(dsn_hostspec, port, unquote=True)
224237

225238
if parsed.path and database is None:
226-
database = parsed.path
227-
if database.startswith('/'):
228-
database = database[1:]
239+
dsn_database = parsed.path
240+
if dsn_database.startswith('/'):
241+
dsn_database = dsn_database[1:]
242+
database = urllib.parse.unquote(dsn_database)
229243

230-
if parsed.username and user is None:
231-
user = parsed.username
244+
if user is None and dsn_user:
245+
user = urllib.parse.unquote(dsn_user)
232246

233-
if parsed.password and password is None:
234-
password = parsed.password
247+
if password is None and dsn_password:
248+
password = urllib.parse.unquote(dsn_password)
235249

236250
if parsed.query:
237251
query = urllib.parse.parse_qs(parsed.query, strict_parsing=True)

Diff for: tests/test_connect.py

+35
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,41 @@ class TestConnectParams(tb.TestCase):
453453
'database': 'dbname'})
454454
},
455455

456+
{
457+
'dsn': 'postgresql://us%40r:p%40ss@h%40st1,h%40st2:543%33/d%62',
458+
'result': (
459+
[('h@st1', 5432), ('h@st2', 5433)],
460+
{
461+
'user': 'us@r',
462+
'password': 'p@ss',
463+
'database': 'db',
464+
}
465+
)
466+
},
467+
468+
{
469+
'dsn': 'postgresql://user:p@ss@host/db',
470+
'result': (
471+
[('ss@host', 5432)],
472+
{
473+
'user': 'user',
474+
'password': 'p',
475+
'database': 'db',
476+
}
477+
)
478+
},
479+
480+
{
481+
'dsn': 'postgresql:///d%62?user=us%40r&host=h%40st&port=543%33',
482+
'result': (
483+
[('h@st', 5433)],
484+
{
485+
'user': 'us@r',
486+
'database': 'db',
487+
}
488+
)
489+
},
490+
456491
{
457492
'dsn': 'pq:///dbname?host=/unix_sock/test&user=spam',
458493
'error': (ValueError, 'invalid DSN')

0 commit comments

Comments
 (0)