summaryrefslogtreecommitdiff
path: root/swiftclient/client.py
diff options
context:
space:
mode:
Diffstat (limited to 'swiftclient/client.py')
-rw-r--r--swiftclient/client.py196
1 files changed, 112 insertions, 84 deletions
diff --git a/swiftclient/client.py b/swiftclient/client.py
index 6990ea3..807cb3d 100644
--- a/swiftclient/client.py
+++ b/swiftclient/client.py
@@ -72,6 +72,69 @@ if StrictVersion(requests.__version__) < StrictVersion('2.0.0'):
logger = logging.getLogger("swiftclient")
logger.addHandler(NullHandler())
+#: Default behaviour is to redact header values known to contain secrets,
+#: such as ``X-Auth-Key`` and ``X-Auth-Token``. Up to the first 16 chars
+#: may be revealed.
+#:
+#: To disable, set the value of ``redact_sensitive_headers`` to ``False``.
+#:
+#: When header redaction is enabled, ``reveal_sensitive_prefix`` configures the
+#: maximum length of any sensitive header data sent to the logs. If the header
+#: is less than twice this length, only ``int(len(value)/2)`` chars will be
+#: logged; if it is less than 15 chars long, even less will be logged.
+logger_settings = {
+ 'redact_sensitive_headers': True,
+ 'reveal_sensitive_prefix': 16
+}
+#: A list of sensitive headers to redact in logs. Note that when extending this
+#: list, the header names must be added in all lower case.
+LOGGER_SENSITIVE_HEADERS = [
+ 'x-auth-token', 'x-auth-key', 'x-service-token', 'x-storage-token',
+ 'x-account-meta-temp-url-key', 'x-account-meta-temp-url-key-2',
+ 'x-container-meta-temp-url-key', 'x-container-meta-temp-url-key-2',
+ 'set-cookie'
+]
+
+
+def safe_value(name, value):
+ """
+ Only show up to logger_settings['reveal_sensitive_prefix'] characters
+ from a sensitive header.
+
+ :param name: Header name
+ :param value: Header value
+ :return: Safe header value
+ """
+ if name.lower() in LOGGER_SENSITIVE_HEADERS:
+ prefix_length = logger_settings.get('reveal_sensitive_prefix', 16)
+ prefix_length = int(
+ min(prefix_length, (len(value) ** 2) / 32, len(value) / 2)
+ )
+ redacted_value = value[0:prefix_length]
+ return redacted_value + '...'
+ return value
+
+
+def scrub_headers(headers):
+ """
+ Redact header values that can contain sensitive information that
+ should not be logged.
+
+ :param headers: Either a dict or an iterable of two-element tuples
+ :return: Safe dictionary of headers with sensitive information removed
+ """
+ if isinstance(headers, dict):
+ headers = headers.items()
+ headers = [
+ (parse_header_string(key), parse_header_string(val))
+ for (key, val) in headers
+ ]
+ if not logger_settings.get('redact_sensitive_headers', True):
+ return dict(headers)
+ if logger_settings.get('reveal_sensitive_prefix', 16) < 0:
+ logger_settings['reveal_sensitive_prefix'] = 16
+ return {key: safe_value(key, val) for (key, val) in headers}
+
def http_log(args, kwargs, resp, body):
if not logger.isEnabledFor(logging.INFO):
@@ -87,8 +150,9 @@ def http_log(args, kwargs, resp, body):
else:
string_parts.append(' %s' % element)
if 'headers' in kwargs:
- for element in kwargs['headers']:
- header = ' -H "%s: %s"' % (element, kwargs['headers'][element])
+ headers = scrub_headers(kwargs['headers'])
+ for element in headers:
+ header = ' -H "%s: %s"' % (element, headers[element])
string_parts.append(header)
# log response as debug if good, or info if error
@@ -99,12 +163,14 @@ def http_log(args, kwargs, resp, body):
log_method("REQ: %s", "".join(string_parts))
log_method("RESP STATUS: %s %s", resp.status, resp.reason)
- log_method("RESP HEADERS: %s", resp.getheaders())
+ log_method("RESP HEADERS: %s", scrub_headers(resp.getheaders()))
if body:
log_method("RESP BODY: %s", body)
def parse_header_string(data):
+ if not isinstance(data, (six.text_type, six.binary_type)):
+ data = str(data)
if six.PY2:
if isinstance(data, six.text_type):
# Under Python2 requests only returns binary_type, but if we get
@@ -403,20 +469,18 @@ def get_auth_1_0(url, user, key, snet, **kwargs):
parsed, conn = http_connection(url, cacert=cacert, insecure=insecure,
timeout=timeout)
method = 'GET'
- conn.request(method, parsed.path, '',
- {'X-Auth-User': user, 'X-Auth-Key': key})
+ headers = {'X-Auth-User': user, 'X-Auth-Key': key}
+ conn.request(method, parsed.path, '', headers)
resp = conn.getresponse()
body = resp.read()
- http_log((url, method,), {}, resp, body)
+ http_log((url, method,), headers, resp, body)
url = resp.getheader('x-storage-url')
# There is a side-effect on current Rackspace 1.0 server where a
# bad URL would get you that document page and a 200. We error out
# if we don't have a x-storage-url header and if we get a body.
if resp.status < 200 or resp.status >= 300 or (body and not url):
- raise ClientException('Auth GET failed', http_scheme=parsed.scheme,
- http_host=conn.host, http_path=parsed.path,
- http_status=resp.status, http_reason=resp.reason)
+ raise ClientException.from_response(resp, 'Auth GET failed', body)
if snet:
parsed = list(urlparse(url))
# Second item in the list is the netloc
@@ -471,6 +535,7 @@ def get_auth_keystone(auth_url, user, key, os_options, **kwargs):
_ksclient = ksclient.Client(
username=user,
password=key,
+ token=os_options.get('auth_token'),
tenant_name=os_options.get('tenant_name'),
tenant_id=os_options.get('tenant_id'),
user_id=os_options.get('user_id'),
@@ -655,11 +720,7 @@ def get_account(url, token, marker=None, limit=None, prefix=None,
resp_headers = resp_header_dict(resp)
if resp.status < 200 or resp.status >= 300:
- raise ClientException('Account GET failed', http_scheme=parsed.scheme,
- http_host=conn.host, http_path=parsed.path,
- http_query=qs, http_status=resp.status,
- http_reason=resp.reason,
- http_response_content=body)
+ raise ClientException.from_response(resp, 'Account GET failed', body)
if resp.status == 204:
return resp_headers, []
return resp_headers, parse_api_response(resp_headers, body)
@@ -691,16 +752,13 @@ def head_account(url, token, http_conn=None, service_token=None):
body = resp.read()
http_log((url, method,), {'headers': headers}, resp, body)
if resp.status < 200 or resp.status >= 300:
- raise ClientException('Account HEAD failed', http_scheme=parsed.scheme,
- http_host=conn.host, http_path=parsed.path,
- http_status=resp.status, http_reason=resp.reason,
- http_response_content=body)
+ raise ClientException.from_response(resp, 'Account HEAD failed', body)
resp_headers = resp_header_dict(resp)
return resp_headers
def post_account(url, token, headers, http_conn=None, response_dict=None,
- service_token=None):
+ service_token=None, query_string=None, data=None):
"""
Update an account's metadata.
@@ -712,17 +770,23 @@ def post_account(url, token, headers, http_conn=None, response_dict=None,
:param response_dict: an optional dictionary into which to place
the response - status, reason and headers
:param service_token: service auth token
+ :param query_string: if set will be appended with '?' to generated path
+ :param data: an optional message body for the request
:raises ClientException: HTTP POST request failed
+ :returns: resp_headers, body
"""
if http_conn:
parsed, conn = http_conn
else:
parsed, conn = http_connection(url)
method = 'POST'
+ path = parsed.path
+ if query_string:
+ path += '?' + query_string
headers['X-Auth-Token'] = token
if service_token:
headers['X-Service-Token'] = service_token
- conn.request(method, parsed.path, '', headers)
+ conn.request(method, path, data, headers)
resp = conn.getresponse()
body = resp.read()
http_log((url, method,), {'headers': headers}, resp, body)
@@ -730,13 +794,11 @@ def post_account(url, token, headers, http_conn=None, response_dict=None,
store_response(resp, response_dict)
if resp.status < 200 or resp.status >= 300:
- raise ClientException('Account POST failed',
- http_scheme=parsed.scheme,
- http_host=conn.host,
- http_path=parsed.path,
- http_status=resp.status,
- http_reason=resp.reason,
- http_response_content=body)
+ raise ClientException.from_response(resp, 'Account POST failed', body)
+ resp_headers = {}
+ for header, value in resp.getheaders():
+ resp_headers[header.lower()] = value
+ return resp_headers, body
def get_container(url, token, container, marker=None, limit=None,
@@ -775,7 +837,7 @@ def get_container(url, token, container, marker=None, limit=None,
if full_listing:
rv = get_container(url, token, container, marker, limit, prefix,
delimiter, end_marker, path, http_conn,
- service_token, headers=headers)
+ service_token=service_token, headers=headers)
listing = rv[1]
while listing:
if not delimiter:
@@ -784,7 +846,7 @@ def get_container(url, token, container, marker=None, limit=None,
marker = listing[-1].get('name', listing[-1].get('subdir'))
listing = get_container(url, token, container, marker, limit,
prefix, delimiter, end_marker, path,
- http_conn, service_token,
+ http_conn, service_token=service_token,
headers=headers)[1]
if listing:
rv[1].extend(listing)
@@ -817,11 +879,7 @@ def get_container(url, token, container, marker=None, limit=None,
{'headers': headers}, resp, body)
if resp.status < 200 or resp.status >= 300:
- raise ClientException('Container GET failed',
- http_scheme=parsed.scheme, http_host=conn.host,
- http_path=cont_path, http_query=qs,
- http_status=resp.status, http_reason=resp.reason,
- http_response_content=body)
+ raise ClientException.from_response(resp, 'Container GET failed', body)
resp_headers = resp_header_dict(resp)
if resp.status == 204:
return resp_headers, []
@@ -862,11 +920,8 @@ def head_container(url, token, container, http_conn=None, headers=None,
{'headers': req_headers}, resp, body)
if resp.status < 200 or resp.status >= 300:
- raise ClientException('Container HEAD failed',
- http_scheme=parsed.scheme, http_host=conn.host,
- http_path=path, http_status=resp.status,
- http_reason=resp.reason,
- http_response_content=body)
+ raise ClientException.from_response(
+ resp, 'Container HEAD failed', body)
resp_headers = resp_header_dict(resp)
return resp_headers
@@ -909,11 +964,7 @@ def put_container(url, token, container, headers=None, http_conn=None,
http_log(('%s%s' % (url.replace(parsed.path, ''), path), method,),
{'headers': headers}, resp, body)
if resp.status < 200 or resp.status >= 300:
- raise ClientException('Container PUT failed',
- http_scheme=parsed.scheme, http_host=conn.host,
- http_path=path, http_status=resp.status,
- http_reason=resp.reason,
- http_response_content=body)
+ raise ClientException.from_response(resp, 'Container PUT failed', body)
def post_container(url, token, container, headers, http_conn=None,
@@ -952,11 +1003,8 @@ def post_container(url, token, container, headers, http_conn=None,
store_response(resp, response_dict)
if resp.status < 200 or resp.status >= 300:
- raise ClientException('Container POST failed',
- http_scheme=parsed.scheme, http_host=conn.host,
- http_path=path, http_status=resp.status,
- http_reason=resp.reason,
- http_response_content=body)
+ raise ClientException.from_response(
+ resp, 'Container POST failed', body)
def delete_container(url, token, container, http_conn=None,
@@ -992,11 +1040,8 @@ def delete_container(url, token, container, http_conn=None,
store_response(resp, response_dict)
if resp.status < 200 or resp.status >= 300:
- raise ClientException('Container DELETE failed',
- http_scheme=parsed.scheme, http_host=conn.host,
- http_path=path, http_status=resp.status,
- http_reason=resp.reason,
- http_response_content=body)
+ raise ClientException.from_response(
+ resp, 'Container DELETE failed', body)
def get_object(url, token, container, name, http_conn=None,
@@ -1049,11 +1094,7 @@ def get_object(url, token, container, name, http_conn=None,
body = resp.read()
http_log(('%s%s' % (url.replace(parsed.path, ''), path), method,),
{'headers': headers}, resp, body)
- raise ClientException('Object GET failed', http_scheme=parsed.scheme,
- http_host=conn.host, http_path=path,
- http_status=resp.status,
- http_reason=resp.reason,
- http_response_content=body)
+ raise ClientException.from_response(resp, 'Object GET failed', body)
if resp_chunk_size:
object_body = _ObjectBody(resp, resp_chunk_size)
else:
@@ -1100,10 +1141,7 @@ def head_object(url, token, container, name, http_conn=None,
http_log(('%s%s' % (url.replace(parsed.path, ''), path), method,),
{'headers': headers}, resp, body)
if resp.status < 200 or resp.status >= 300:
- raise ClientException('Object HEAD failed', http_scheme=parsed.scheme,
- http_host=conn.host, http_path=path,
- http_status=resp.status, http_reason=resp.reason,
- http_response_content=body)
+ raise ClientException.from_response(resp, 'Object HEAD failed', body)
resp_headers = resp_header_dict(resp)
return resp_headers
@@ -1209,17 +1247,13 @@ def put_object(url, token=None, container=None, name=None, contents=None,
resp = conn.getresponse()
body = resp.read()
- headers = {'X-Auth-Token': token}
http_log(('%s%s' % (url.replace(parsed.path, ''), path), 'PUT',),
{'headers': headers}, resp, body)
store_response(resp, response_dict)
if resp.status < 200 or resp.status >= 300:
- raise ClientException('Object PUT failed', http_scheme=parsed.scheme,
- http_host=conn.host, http_path=path,
- http_status=resp.status, http_reason=resp.reason,
- http_response_content=body)
+ raise ClientException.from_response(resp, 'Object PUT failed', body)
etag = resp.getheader('etag', '').strip('"')
return etag
@@ -1259,10 +1293,7 @@ def post_object(url, token, container, name, headers, http_conn=None,
store_response(resp, response_dict)
if resp.status < 200 or resp.status >= 300:
- raise ClientException('Object POST failed', http_scheme=parsed.scheme,
- http_host=conn.host, http_path=path,
- http_status=resp.status, http_reason=resp.reason,
- http_response_content=body)
+ raise ClientException.from_response(resp, 'Object POST failed', body)
def delete_object(url, token=None, container=None, name=None, http_conn=None,
@@ -1316,11 +1347,7 @@ def delete_object(url, token=None, container=None, name=None, http_conn=None,
store_response(resp, response_dict)
if resp.status < 200 or resp.status >= 300:
- raise ClientException('Object DELETE failed',
- http_scheme=parsed.scheme, http_host=conn.host,
- http_path=path, http_status=resp.status,
- http_reason=resp.reason,
- http_response_content=body)
+ raise ClientException.from_response(resp, 'Object DELETE failed', body)
def get_capabilities(http_conn):
@@ -1337,11 +1364,8 @@ def get_capabilities(http_conn):
body = resp.read()
http_log((parsed.geturl(), 'GET',), {'headers': {}}, resp, body)
if resp.status < 200 or resp.status >= 300:
- raise ClientException('Capabilities GET failed',
- http_scheme=parsed.scheme,
- http_host=conn.host, http_path=parsed.path,
- http_status=resp.status, http_reason=resp.reason,
- http_response_content=body)
+ raise ClientException.from_response(
+ resp, 'Capabilities GET failed', body)
resp_headers = resp_header_dict(resp)
return parse_api_response(resp_headers, body)
@@ -1555,9 +1579,11 @@ class Connection(object):
prefix=prefix, end_marker=end_marker,
full_listing=full_listing)
- def post_account(self, headers, response_dict=None):
+ def post_account(self, headers, response_dict=None,
+ query_string=None, data=None):
"""Wrapper for :func:`post_account`"""
return self._retry(None, post_account, headers,
+ query_string=query_string, data=data,
response_dict=response_dict)
def head_container(self, container, headers=None):
@@ -1635,10 +1661,12 @@ class Connection(object):
if self.retries > 0:
tell = getattr(contents, 'tell', None)
seek = getattr(contents, 'seek', None)
+ reset = getattr(contents, 'reset', None)
if tell and seek:
orig_pos = tell()
reset_func = lambda *a, **k: seek(orig_pos)
-
+ elif reset:
+ reset_func = reset
return self._retry(reset_func, put_object, container, obj, contents,
content_length=content_length, etag=etag,
chunk_size=chunk_size, content_type=content_type,