Skip to content

Commit c9bdb57

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 ae5a89d commit c9bdb57

File tree

3 files changed

+63
-14
lines changed

3 files changed

+63
-14
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ __pycache__/
3030
docs/_build
3131
*,cover
3232
.coverage
33-
/.pytest_cache/
33+
/.pytest_cache
34+
/.mypy_cache
3435
/.eggs
3536
/.vscode

asyncpg/connect_utils.py

Lines changed: 26 additions & 13 deletions
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(',')
@@ -180,10 +180,14 @@ def _parse_hostlist(hostlist, port):
180180

181181
for i, hostspec in enumerate(hostspecs):
182182
addr, _, hostspec_port = hostspec.partition(':')
183+
if unquote:
184+
addr = urllib.parse.unquote(addr)
183185
hosts.append(addr)
184186

185187
if not port:
186188
if hostspec_port:
189+
if unquote:
190+
hostspec_port = urllib.parse.unquote(hostspec_port)
187191
hostlist_ports.append(int(hostspec_port))
188192
else:
189193
hostlist_ports.append(default_port[i])
@@ -209,25 +213,34 @@ def _parse_connect_dsn_and_args(*, dsn, host, port, user,
209213
'invalid DSN: scheme is expected to be either '
210214
'"postgresql" or "postgres", got {!r}'.format(parsed.scheme))
211215

212-
if not host and parsed.netloc:
216+
if parsed.netloc:
213217
if '@' in parsed.netloc:
214-
auth, _, hostspec = parsed.netloc.partition('@')
218+
dsn_auth, _, dsn_hostspec = parsed.netloc.partition('@')
215219
else:
216-
hostspec = parsed.netloc
220+
dsn_hostspec = parsed.netloc
221+
dsn_auth = ''
222+
else:
223+
dsn_auth = dsn_hostspec = ''
224+
225+
if dsn_auth:
226+
dsn_user, _, dsn_password = dsn_auth.partition(':')
227+
else:
228+
dsn_user = dsn_password = ''
217229

218-
if hostspec:
219-
host, port = _parse_hostlist(hostspec, port)
230+
if not host and dsn_hostspec:
231+
host, port = _parse_hostlist(dsn_hostspec, port, unquote=True)
220232

221233
if parsed.path and database is None:
222-
database = parsed.path
223-
if database.startswith('/'):
224-
database = database[1:]
234+
dsn_database = parsed.path
235+
if dsn_database.startswith('/'):
236+
dsn_database = dsn_database[1:]
237+
database = urllib.parse.unquote(dsn_database)
225238

226-
if parsed.username and user is None:
227-
user = parsed.username
239+
if user is None and dsn_user:
240+
user = urllib.parse.unquote(dsn_user)
228241

229-
if parsed.password and password is None:
230-
password = parsed.password
242+
if password is None and dsn_password:
243+
password = urllib.parse.unquote(dsn_password)
231244

232245
if parsed.query:
233246
query = urllib.parse.parse_qs(parsed.query, strict_parsing=True)

tests/test_connect.py

Lines changed: 35 additions & 0 deletions
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)