summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--swift/common/bufferedhttp.py11
-rw-r--r--test/functional/swift_test_client.py88
-rw-r--r--test/unit/common/test_bufferedhttp.py5
3 files changed, 101 insertions, 3 deletions
diff --git a/swift/common/bufferedhttp.py b/swift/common/bufferedhttp.py
index 129460913..ecf9b155e 100644
--- a/swift/common/bufferedhttp.py
+++ b/swift/common/bufferedhttp.py
@@ -35,7 +35,7 @@ import socket
import eventlet
from eventlet.green.httplib import CONTINUE, HTTPConnection, HTTPMessage, \
HTTPResponse, HTTPSConnection, _UNKNOWN
-from six.moves.urllib.parse import quote
+from six.moves.urllib.parse import quote, parse_qsl, urlencode
import six
if six.PY2:
@@ -240,6 +240,15 @@ def http_connect_raw(ipaddr, port, method, path, headers=None,
else:
conn = BufferedHTTPConnection('%s:%s' % (ipaddr, port))
if query_string:
+ # Round trip to ensure proper quoting
+ if six.PY2:
+ query_string = urlencode(parse_qsl(
+ query_string, keep_blank_values=True))
+ else:
+ query_string = urlencode(
+ parse_qsl(query_string, keep_blank_values=True,
+ encoding='latin1'),
+ encoding='latin1')
path += '?' + query_string
conn.path = path
conn.putrequest(method, path, skip_host=(headers and 'Host' in headers))
diff --git a/test/functional/swift_test_client.py b/test/functional/swift_test_client.py
index 3b68d3a44..9ff8a22ba 100644
--- a/test/functional/swift_test_client.py
+++ b/test/functional/swift_test_client.py
@@ -104,6 +104,91 @@ def listing_items(method):
items = []
+def putrequest(self, method, url, skip_host=False, skip_accept_encoding=False):
+ '''Send a request to the server.
+
+ This is mostly a regurgitation of CPython's HTTPConnection.putrequest,
+ but fixed up so we can still send arbitrary bytes in the request line
+ on py3. See also: https://bugs.python.org/issue36274
+
+ To use, swap out a HTTP(S)Connection's putrequest with something like::
+
+ conn.putrequest = putrequest.__get__(conn)
+
+ :param method: specifies an HTTP request method, e.g. 'GET'.
+ :param url: specifies the object being requested, e.g. '/index.html'.
+ :param skip_host: if True does not add automatically a 'Host:' header
+ :param skip_accept_encoding: if True does not add automatically an
+ 'Accept-Encoding:' header
+ '''
+ # (Mostly) inline the HTTPConnection implementation; just fix it
+ # so we can send non-ascii request lines. For comparison, see
+ # https://github.com/python/cpython/blob/v2.7.16/Lib/httplib.py#L888-L1003
+ # and https://github.com/python/cpython/blob/v3.7.2/
+ # Lib/http/client.py#L1061-L1183
+ if self._HTTPConnection__response \
+ and self._HTTPConnection__response.isclosed():
+ self._HTTPConnection__response = None
+
+ if self._HTTPConnection__state == http_client._CS_IDLE:
+ self._HTTPConnection__state = http_client._CS_REQ_STARTED
+ else:
+ raise http_client.CannotSendRequest(self._HTTPConnection__state)
+
+ self._method = method
+ if not url:
+ url = '/'
+ self._path = url
+ request = '%s %s %s' % (method, url, self._http_vsn_str)
+ if not isinstance(request, bytes):
+ # This choice of encoding is the whole reason we copy/paste from
+ # cpython. When making backend requests, it should never be
+ # necessary; however, we have some functional tests that want
+ # to send non-ascii bytes.
+ # TODO: when https://bugs.python.org/issue36274 is resolved, make
+ # sure we fix up our API to match whatever upstream chooses to do
+ self._output(request.encode('latin1'))
+ else:
+ self._output(request)
+
+ if self._http_vsn == 11:
+ if not skip_host:
+ netloc = ''
+ if url.startswith('http'):
+ nil, netloc, nil, nil, nil = urllib.parse.urlsplit(url)
+
+ if netloc:
+ try:
+ netloc_enc = netloc.encode("ascii")
+ except UnicodeEncodeError:
+ netloc_enc = netloc.encode("idna")
+ self.putheader('Host', netloc_enc)
+ else:
+ if self._tunnel_host:
+ host = self._tunnel_host
+ port = self._tunnel_port
+ else:
+ host = self.host
+ port = self.port
+
+ try:
+ host_enc = host.encode("ascii")
+ except UnicodeEncodeError:
+ host_enc = host.encode("idna")
+
+ if host.find(':') >= 0:
+ host_enc = b'[' + host_enc + b']'
+
+ if port == self.default_port:
+ self.putheader('Host', host_enc)
+ else:
+ host_enc = host_enc.decode("ascii")
+ self.putheader('Host', "%s:%s" % (host_enc, port))
+
+ if not skip_accept_encoding:
+ self.putheader('Accept-Encoding', 'identity')
+
+
class Connection(object):
def __init__(self, config):
for key in 'auth_host auth_port auth_ssl username password'.split():
@@ -125,6 +210,7 @@ class Connection(object):
self.storage_host = None
self.storage_port = None
self.storage_url = None
+ self.connection = None # until you call .http_connect()/.put_start()
self.conn_class = None
@@ -209,6 +295,7 @@ class Connection(object):
self.connection = self.conn_class(self.storage_host,
port=self.storage_port)
# self.connection.set_debuglevel(3)
+ self.connection.putrequest = putrequest.__get__(self.connection)
def make_path(self, path=None, cfg=None):
if path is None:
@@ -338,6 +425,7 @@ class Connection(object):
self.connection = self.conn_class(self.storage_host,
port=self.storage_port)
# self.connection.set_debuglevel(3)
+ self.connection.putrequest = putrequest.__get__(self.connection)
self.connection.putrequest('PUT', path)
for key, value in headers.items():
self.connection.putheader(key, value)
diff --git a/test/unit/common/test_bufferedhttp.py b/test/unit/common/test_bufferedhttp.py
index 9ea07ec3b..319986dd1 100644
--- a/test/unit/common/test_bufferedhttp.py
+++ b/test/unit/common/test_bufferedhttp.py
@@ -59,7 +59,8 @@ class TestBufferedHTTP(unittest.TestCase):
fp.flush()
self.assertEqual(
fp.readline(),
- 'PUT /dev/%s/path/..%%25/?omg&no=%%7f HTTP/1.1\r\n' %
+ 'PUT /dev/%s/path/..%%25/?omg=&no=%%7F&%%FF=%%FF'
+ '&no=%%25ff HTTP/1.1\r\n' %
expected_par)
headers = {}
line = fp.readline()
@@ -82,7 +83,7 @@ class TestBufferedHTTP(unittest.TestCase):
'PUT', '/path/..%/', {
'content-length': 7,
'x-header': 'value'},
- query_string='omg&no=%7f')
+ query_string='omg&no=%7f&\xff=%ff&no=%25ff')
conn.send('REQUEST\r\n')
self.assertTrue(conn.sock.getsockopt(socket.IPPROTO_TCP,
socket.TCP_NODELAY))