Skip to content

Commit

Permalink
Handle percent-encoded URLs in parsing code
Browse files Browse the repository at this point in the history
  • Loading branch information
paulkeene committed Feb 10, 2015
1 parent c4e2f56 commit 904dd00
Show file tree
Hide file tree
Showing 3 changed files with 71 additions and 9 deletions.
3 changes: 2 additions & 1 deletion redis/_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@


if sys.version_info[0] < 3:
from urllib import unquote
from urlparse import parse_qs, urlparse
from itertools import imap, izip
from string import letters as ascii_letters
Expand Down Expand Up @@ -38,7 +39,7 @@ def safe_unicode(obj, *args):
bytes = str
long = long
else:
from urllib.parse import parse_qs, urlparse
from urllib.parse import parse_qs, unquote, urlparse
from io import BytesIO
from string import ascii_letters
from queue import Queue
Expand Down
32 changes: 24 additions & 8 deletions redis/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@

from redis._compat import (b, xrange, imap, byte_to_chr, unicode, bytes, long,
BytesIO, nativestr, basestring, iteritems,
LifoQueue, Empty, Full, urlparse, parse_qs)
LifoQueue, Empty, Full, urlparse, parse_qs,
unquote)
from redis.exceptions import (
RedisError,
ConnectionError,
Expand Down Expand Up @@ -730,7 +731,7 @@ def _error_message(self, exception):
class ConnectionPool(object):
"Generic connection pool"
@classmethod
def from_url(cls, url, db=None, **kwargs):
def from_url(cls, url, db=None, decode_components=False, **kwargs):
"""
Return a connection pool configured from the given URL.
Expand All @@ -754,6 +755,12 @@ def from_url(cls, url, db=None, **kwargs):
If none of these options are specified, db=0 is used.
The ``decode_components`` argument allows this function to work with
percent-encoded URLs. If this argument is set to ``True`` all ``%xx``
escapes will be replaced by their single-character equivalents after
the URL has been parsed. This only applies to the ``hostname``,
``path``, and ``password`` components.
Any additional querystring arguments and keyword arguments will be
passed along to the ConnectionPool class's initializer. In the case
of conflicting arguments, querystring arguments always win.
Expand All @@ -778,26 +785,35 @@ def from_url(cls, url, db=None, **kwargs):
if value and len(value) > 0:
url_options[name] = value[0]

if decode_components:
password = unquote(url.password) if url.password else None
path = unquote(url.path) if url.path else None
hostname = unquote(url.hostname) if url.hostname else None
else:
password = url.password
path = url.path
hostname = url.hostname

# We only support redis:// and unix:// schemes.
if url.scheme == 'unix':
url_options.update({
'password': url.password,
'path': url.path,
'password': password,
'path': path,
'connection_class': UnixDomainSocketConnection,
})

else:
url_options.update({
'host': url.hostname,
'host': hostname,
'port': int(url.port or 6379),
'password': url.password,
'password': password,
})

# If there's a path argument, use it as the db argument if a
# querystring value wasn't specified
if 'db' not in url_options and url.path:
if 'db' not in url_options and path:
try:
url_options['db'] = int(url.path.replace('/', ''))
url_options['db'] = int(path.replace('/', ''))
except (AttributeError, ValueError):
pass

Expand Down
45 changes: 45 additions & 0 deletions tests/test_connection_pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,17 @@ def test_hostname(self):
'password': None,
}

def test_quoted_hostname(self):
pool = redis.ConnectionPool.from_url('redis://my %2F host %2B%3D+',
decode_components=True)
assert pool.connection_class == redis.Connection
assert pool.connection_kwargs == {
'host': 'my / host +=+',
'port': 6379,
'db': 0,
'password': None,
}

def test_port(self):
pool = redis.ConnectionPool.from_url('redis://localhost:6380')
assert pool.connection_class == redis.Connection
Expand All @@ -183,6 +194,18 @@ def test_password(self):
'password': 'mypassword',
}

def test_quoted_password(self):
pool = redis.ConnectionPool.from_url(
'redis://:%2Fmypass%2F%2B word%3D%24+@localhost',
decode_components=True)
assert pool.connection_class == redis.Connection
assert pool.connection_kwargs == {
'host': 'localhost',
'port': 6379,
'db': 0,
'password': '/mypass/+ word=$+',
}

def test_db_as_argument(self):
pool = redis.ConnectionPool.from_url('redis://localhost', db='1')
assert pool.connection_class == redis.Connection
Expand Down Expand Up @@ -260,6 +283,28 @@ def test_password(self):
'password': 'mypassword',
}

def test_quoted_password(self):
pool = redis.ConnectionPool.from_url(
'unix://:%2Fmypass%2F%2B word%3D%24+@/socket',
decode_components=True)
assert pool.connection_class == redis.UnixDomainSocketConnection
assert pool.connection_kwargs == {
'path': '/socket',
'db': 0,
'password': '/mypass/+ word=$+',
}

def test_quoted_path(self):
pool = redis.ConnectionPool.from_url(
'unix://:mypassword@/my%2Fpath%2Fto%2F..%2F+_%2B%3D%24ocket',
decode_components=True)
assert pool.connection_class == redis.UnixDomainSocketConnection
assert pool.connection_kwargs == {
'path': '/my/path/to/../+_+=$ocket',
'db': 0,
'password': 'mypassword',
}

def test_db_as_argument(self):
pool = redis.ConnectionPool.from_url('unix:///socket', db=1)
assert pool.connection_class == redis.UnixDomainSocketConnection
Expand Down

0 comments on commit 904dd00

Please sign in to comment.