Skip to content

Commit b182112

Browse files
author
Tristan Cacqueray
committed
Port to python-requests
Currently, httplib implementation does not support SSL certificate verification. This patch fixes this. Note that ssl compression parameter and 100-continue thing is still missing from requests, though those are lower priority. Requests now takes care of: * proxy configuration (get_environ_proxies), * chunked encoding (with data generator), * bulk uploading (with files dictionary), * SSL certificate verification (with 'insecure' and 'cacert' parameter). This patch have been tested with requests 1.1.0 (CentOS 6) and requests 2.2.1 (current version). Change-Id: Ib5de962f4102d57c71ad85fd81a615362ef175dc Closes-Bug: #1199783 DocImpact SecurityImpact
1 parent 9b73547 commit b182112

File tree

8 files changed

+150
-339
lines changed

8 files changed

+150
-339
lines changed

bin/swift

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ try:
3232
except ImportError:
3333
import json
3434

35-
from swiftclient import Connection, HTTPException
35+
from swiftclient import Connection, RequestException
3636
from swiftclient import command_helpers
3737
from swiftclient.utils import config_true_value, prt_bytes
3838
from swiftclient.multithreading import MultiThreadingManager
@@ -1388,16 +1388,16 @@ Examples:
13881388
parser.add_option('--insecure',
13891389
action="store_true", dest="insecure",
13901390
default=default_val,
1391-
help='Allow swiftclient to access insecure keystone '
1392-
'server. The keystone\'s certificate will not '
1393-
'be verified. '
1391+
help='Allow swiftclient to access servers without '
1392+
'having to verify the SSL certificate. '
13941393
'Defaults to env[SWIFTCLIENT_INSECURE] '
13951394
'(set to \'true\' to enable).')
13961395
parser.add_option('--no-ssl-compression',
13971396
action='store_false', dest='ssl_compression',
13981397
default=True,
1399-
help='Disable SSL compression when using https. '
1400-
'This may increase performance.')
1398+
help='This option is deprecated and not used anymore. '
1399+
'SSL compression should be disabled by default '
1400+
'by the system SSL library')
14011401
parser.disable_interspersed_args()
14021402
(options, args) = parse_args(parser, argv[1:], enforce_requires=False)
14031403
parser.enable_interspersed_args()
@@ -1425,7 +1425,7 @@ Examples:
14251425
parser.usage = globals()['st_%s_help' % args[0]]
14261426
try:
14271427
globals()['st_%s' % args[0]](parser, argv[1:], thread_manager)
1428-
except (ClientException, HTTPException, socket.error) as err:
1428+
except (ClientException, RequestException, socket.error) as err:
14291429
thread_manager.error(str(err))
14301430

14311431
had_error = thread_manager.error_count

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
requests>=1.1
12
simplejson>=2.0.9

swiftclient/client.py

Lines changed: 114 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -18,24 +18,18 @@
1818
"""
1919

2020
import socket
21+
import requests
2122
import sys
2223
import logging
2324
import warnings
24-
from functools import wraps
2525

26+
from distutils.version import StrictVersion
27+
from requests.exceptions import RequestException, SSLError
2628
from urllib import quote as _quote
2729
from urlparse import urlparse, urlunparse
28-
from httplib import HTTPException, HTTPConnection, HTTPSConnection
2930
from time import sleep, time
3031

3132
from swiftclient.exceptions import ClientException, InvalidHeadersException
32-
from swiftclient.utils import get_environ_proxies
33-
34-
try:
35-
from swiftclient.https_connection import HTTPSConnectionNoSSLComp
36-
except ImportError:
37-
HTTPSConnectionNoSSLComp = HTTPSConnection
38-
3933

4034
try:
4135
from logging import NullHandler
@@ -50,6 +44,18 @@ def emit(self, record):
5044
def createLock(self):
5145
self.lock = None
5246

47+
# requests version 1.2.3 try to encode headers in ascii, preventing
48+
# utf-8 encoded header to be 'prepared'
49+
if StrictVersion(requests.__version__) < StrictVersion('2.0.0'):
50+
from requests.structures import CaseInsensitiveDict
51+
52+
def prepare_unicode_headers(self, headers):
53+
if headers:
54+
self.headers = CaseInsensitiveDict(headers)
55+
else:
56+
self.headers = CaseInsensitiveDict()
57+
requests.models.PreparedRequest.prepare_headers = prepare_unicode_headers
58+
5359
logger = logging.getLogger("swiftclient")
5460
logger.addHandler(NullHandler())
5561

@@ -124,68 +130,93 @@ def encode_utf8(value):
124130
from json import loads as json_loads
125131

126132

127-
def http_connection(url, proxy=None, ssl_compression=True):
128-
"""
129-
Make an HTTPConnection or HTTPSConnection
133+
class HTTPConnection:
134+
def __init__(self, url, proxy=None, cacert=None, insecure=False,
135+
ssl_compression=False):
136+
"""
137+
Make an HTTPConnection or HTTPSConnection
138+
139+
:param url: url to connect to
140+
:param proxy: proxy to connect through, if any; None by default; str
141+
of the format 'http://127.0.0.1:8888' to set one
142+
:param cacert: A CA bundle file to use in verifying a TLS server
143+
certificate.
144+
:param insecure: Allow to access servers without checking SSL certs.
145+
The server's certificate will not be verified.
146+
:param ssl_compression: SSL compression should be disabled by default
147+
and this setting is not usable as of now. The
148+
parameter is kept for backward compatibility.
149+
:raises ClientException: Unable to handle protocol scheme
150+
"""
151+
self.url = url
152+
self.parsed_url = urlparse(url)
153+
self.host = self.parsed_url.netloc
154+
self.port = self.parsed_url.port
155+
self.requests_args = {}
156+
if self.parsed_url.scheme not in ('http', 'https'):
157+
raise ClientException("Unsupported scheme")
158+
self.requests_args['verify'] = not insecure
159+
if cacert:
160+
# verify requests parameter is used to pass the CA_BUNDLE file
161+
# see: http://docs.python-requests.org/en/latest/user/advanced/
162+
self.requests_args['verify'] = cacert
163+
if proxy:
164+
proxy_parsed = urlparse(proxy)
165+
if not proxy_parsed.scheme:
166+
raise ClientException("Proxy's missing scheme")
167+
self.requests_args['proxies'] = {
168+
proxy_parsed.scheme: '%s://%s' % (
169+
proxy_parsed.scheme, proxy_parsed.netloc
170+
)
171+
}
172+
self.requests_args['stream'] = True
173+
174+
def _request(self, *arg, **kwarg):
175+
""" Final wrapper before requests call, to be patched in tests """
176+
return requests.request(*arg, **kwarg)
177+
178+
def request(self, method, full_path, data=None, headers={}, files=None):
179+
""" Encode url and header, then call requests.request """
180+
headers = dict((encode_utf8(x), encode_utf8(y)) for x, y in
181+
headers.iteritems())
182+
url = encode_utf8("%s://%s%s" % (
183+
self.parsed_url.scheme,
184+
self.parsed_url.netloc,
185+
full_path))
186+
self.resp = self._request(method, url, headers=headers, data=data,
187+
files=files, **self.requests_args)
188+
return self.resp
189+
190+
def putrequest(self, full_path, data=None, headers={}, files=None):
191+
"""
192+
Use python-requests files upload
130193
131-
:param url: url to connect to
132-
:param proxy: proxy to connect through, if any; None by default; str of the
133-
format 'http://127.0.0.1:8888' to set one
134-
:param ssl_compression: Whether to enable compression at the SSL layer.
135-
If set to 'False' and the pyOpenSSL library is
136-
present an attempt to disable SSL compression
137-
will be made. This may provide a performance
138-
increase for https upload/download operations.
139-
:returns: tuple of (parsed url, connection object)
140-
:raises ClientException: Unable to handle protocol scheme
141-
"""
142-
url = encode_utf8(url)
143-
parsed = urlparse(url)
144-
if proxy:
145-
proxy_parsed = urlparse(proxy)
146-
else:
147-
proxies = get_environ_proxies(parsed.netloc)
148-
proxy = proxies.get(parsed.scheme, None)
149-
proxy_parsed = urlparse(proxy) if proxy else None
150-
host = proxy_parsed.netloc if proxy else parsed.netloc
151-
if parsed.scheme == 'http':
152-
conn = HTTPConnection(host)
153-
elif parsed.scheme == 'https':
154-
if ssl_compression is True:
155-
conn = HTTPSConnection(host)
156-
else:
157-
conn = HTTPSConnectionNoSSLComp(host)
158-
else:
159-
raise ClientException('Cannot handle protocol scheme %s for url %s' %
160-
(parsed.scheme, repr(url)))
161-
162-
def putheader_wrapper(func):
163-
164-
@wraps(func)
165-
def putheader_escaped(key, value):
166-
func(encode_utf8(key), encode_utf8(value))
167-
return putheader_escaped
168-
conn.putheader = putheader_wrapper(conn.putheader)
169-
170-
def request_wrapper(func):
171-
172-
@wraps(func)
173-
def request_escaped(method, url, body=None, headers=None):
174-
validate_headers(headers)
175-
url = encode_utf8(url)
176-
if body:
177-
body = encode_utf8(body)
178-
func(method, url, body=body, headers=headers or {})
179-
return request_escaped
180-
conn.request = request_wrapper(conn.request)
181-
if proxy:
182-
try:
183-
# python 2.6 method
184-
conn._set_tunnel(parsed.hostname, parsed.port)
185-
except AttributeError:
186-
# python 2.7 method
187-
conn.set_tunnel(parsed.hostname, parsed.port)
188-
return parsed, conn
194+
:param data: Use data generator for chunked-transfer
195+
:param files: Use files for default transfer
196+
"""
197+
return self.request('PUT', full_path, data, headers, files)
198+
199+
def getresponse(self):
200+
""" Adapt requests response to httplib interface """
201+
self.resp.status = self.resp.status_code
202+
old_getheader = self.resp.raw.getheader
203+
204+
def getheaders():
205+
return self.resp.headers.items()
206+
207+
def getheader(k, v=None):
208+
return old_getheader(k.lower(), v)
209+
210+
self.resp.getheaders = getheaders
211+
self.resp.getheader = getheader
212+
self.resp.read = self.resp.raw.read
213+
return self.resp
214+
215+
216+
def http_connection(*arg, **kwarg):
217+
""" :returns: tuple of (parsed url, connection object) """
218+
conn = HTTPConnection(*arg, **kwarg)
219+
return conn.parsed_url, conn
189220

190221

191222
def get_auth_1_0(url, user, key, snet):
@@ -890,27 +921,16 @@ def put_object(url, token=None, container=None, name=None, contents=None,
890921
if hasattr(contents, 'read'):
891922
if chunk_size is None:
892923
chunk_size = 65536
893-
conn.putrequest('PUT', path)
894-
for header, value in headers.iteritems():
895-
conn.putheader(header, value)
896924
if content_length is None:
897-
conn.putheader('Transfer-Encoding', 'chunked')
898-
conn.endheaders()
899-
chunk = contents.read(chunk_size)
900-
while chunk:
901-
conn.send('%x\r\n%s\r\n' % (len(chunk), chunk))
902-
chunk = contents.read(chunk_size)
903-
conn.send('0\r\n\r\n')
925+
def chunk_reader():
926+
while True:
927+
data = contents.read(chunk_size)
928+
if not data:
929+
break
930+
yield data
931+
conn.putrequest(path, headers=headers, data=chunk_reader())
904932
else:
905-
conn.endheaders()
906-
left = content_length
907-
while left > 0:
908-
size = chunk_size
909-
if size > left:
910-
size = left
911-
chunk = contents.read(size)
912-
conn.send(chunk)
913-
left -= len(chunk)
933+
conn.putrequest(path, headers=headers, files={"file": contents})
914934
else:
915935
if chunk_size is not None:
916936
warn_msg = '%s object has no \"read\" method, ignoring chunk_size'\
@@ -1129,6 +1149,8 @@ def get_auth(self):
11291149

11301150
def http_connection(self):
11311151
return http_connection(self.url,
1152+
cacert=self.cacert,
1153+
insecure=self.insecure,
11321154
ssl_compression=self.ssl_compression)
11331155

11341156
def _add_response_dict(self, target_dict, kwargs):
@@ -1160,7 +1182,9 @@ def _retry(self, reset_func, func, *args, **kwargs):
11601182
rv = func(self.url, self.token, *args, **kwargs)
11611183
self._add_response_dict(caller_response_dict, kwargs)
11621184
return rv
1163-
except (socket.error, HTTPException) as e:
1185+
except SSLError:
1186+
raise
1187+
except (socket.error, RequestException) as e:
11641188
self._add_response_dict(caller_response_dict, kwargs)
11651189
if self.attempts > self.retries:
11661190
logger.exception(e)

swiftclient/https_connection.py

Lines changed: 0 additions & 95 deletions
This file was deleted.

0 commit comments

Comments
 (0)