603 lines
24 KiB
Python
603 lines
24 KiB
Python
# Copyright (c) 2012-2013 Mitch Garnaat http://garnaat.org/
|
|
# Copyright 2012-2014 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"). You
|
|
# may not use this file except in compliance with the License. A copy of
|
|
# the License is located at
|
|
#
|
|
# http://aws.amazon.com/apache2.0/
|
|
#
|
|
# or in the "license" file accompanying this file. This file is
|
|
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
|
|
# ANY KIND, either express or implied. See the License for the specific
|
|
# language governing permissions and limitations under the License.
|
|
|
|
import base64
|
|
import datetime
|
|
from hashlib import sha256
|
|
from hashlib import sha1
|
|
import hmac
|
|
import logging
|
|
from email.utils import formatdate
|
|
from operator import itemgetter
|
|
import functools
|
|
import time
|
|
import calendar
|
|
|
|
from botocore.exceptions import NoCredentialsError
|
|
from botocore.utils import normalize_url_path, percent_encode_sequence
|
|
from botocore.compat import HTTPHeaders
|
|
from botocore.compat import quote, unquote, urlsplit, parse_qs
|
|
from botocore.compat import urlunsplit
|
|
from botocore.compat import encodebytes
|
|
from botocore.compat import six
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
EMPTY_SHA256_HASH = (
|
|
'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855')
|
|
# This is the buffer size used when calculating sha256 checksums.
|
|
# Experimenting with various buffer sizes showed that this value generally
|
|
# gave the best result (in terms of performance).
|
|
PAYLOAD_BUFFER = 1024 * 1024
|
|
ISO8601 = '%Y-%m-%dT%H:%M:%SZ'
|
|
|
|
|
|
class BaseSigner(object):
|
|
REQUIRES_REGION = False
|
|
|
|
def add_auth(self, request):
|
|
raise NotImplementedError("add_auth")
|
|
|
|
|
|
class SigV2Auth(BaseSigner):
|
|
"""
|
|
Sign a request with Signature V2.
|
|
"""
|
|
|
|
def __init__(self, credentials):
|
|
self.credentials = credentials
|
|
|
|
def calc_signature(self, request, params):
|
|
logger.debug("Calculating signature using v2 auth.")
|
|
split = urlsplit(request.url)
|
|
path = split.path
|
|
if len(path) == 0:
|
|
path = '/'
|
|
string_to_sign = '%s\n%s\n%s\n' % (request.method,
|
|
split.netloc,
|
|
path)
|
|
lhmac = hmac.new(self.credentials.secret_key.encode('utf-8'),
|
|
digestmod=sha256)
|
|
pairs = []
|
|
for key in sorted(params):
|
|
value = six.text_type(params[key])
|
|
pairs.append(quote(key.encode('utf-8'), safe='') + '=' +
|
|
quote(value.encode('utf-8'), safe='-_~'))
|
|
qs = '&'.join(pairs)
|
|
string_to_sign += qs
|
|
logger.debug('String to sign: %s', string_to_sign)
|
|
lhmac.update(string_to_sign.encode('utf-8'))
|
|
b64 = base64.b64encode(lhmac.digest()).strip().decode('utf-8')
|
|
return (qs, b64)
|
|
|
|
def add_auth(self, request):
|
|
# The auth handler is the last thing called in the
|
|
# preparation phase of a prepared request.
|
|
# Because of this we have to parse the query params
|
|
# from the request body so we can update them with
|
|
# the sigv2 auth params.
|
|
if self.credentials is None:
|
|
raise NoCredentialsError
|
|
if request.data:
|
|
# POST
|
|
params = request.data
|
|
else:
|
|
# GET
|
|
params = request.param
|
|
params['AWSAccessKeyId'] = self.credentials.access_key
|
|
params['SignatureVersion'] = '2'
|
|
params['SignatureMethod'] = 'HmacSHA256'
|
|
params['Timestamp'] = time.strftime(ISO8601, time.gmtime())
|
|
if self.credentials.token:
|
|
params['SecurityToken'] = self.credentials.token
|
|
qs, signature = self.calc_signature(request, params)
|
|
params['Signature'] = signature
|
|
return request
|
|
|
|
|
|
class SigV3Auth(BaseSigner):
|
|
def __init__(self, credentials):
|
|
self.credentials = credentials
|
|
|
|
def add_auth(self, request):
|
|
if self.credentials is None:
|
|
raise NoCredentialsError
|
|
if 'Date' in request.headers:
|
|
del request.headers['Date']
|
|
request.headers['Date'] = formatdate(usegmt=True)
|
|
if self.credentials.token:
|
|
if 'X-Amz-Security-Token' in request.headers:
|
|
del request.headers['X-Amz-Security-Token']
|
|
request.headers['X-Amz-Security-Token'] = self.credentials.token
|
|
new_hmac = hmac.new(self.credentials.secret_key.encode('utf-8'),
|
|
digestmod=sha256)
|
|
new_hmac.update(request.headers['Date'].encode('utf-8'))
|
|
encoded_signature = encodebytes(new_hmac.digest()).strip()
|
|
signature = ('AWS3-HTTPS AWSAccessKeyId=%s,Algorithm=%s,Signature=%s' %
|
|
(self.credentials.access_key, 'HmacSHA256',
|
|
encoded_signature.decode('utf-8')))
|
|
if 'X-Amzn-Authorization' in request.headers:
|
|
del request.headers['X-Amzn-Authorization']
|
|
request.headers['X-Amzn-Authorization'] = signature
|
|
|
|
|
|
class SigV4Auth(BaseSigner):
|
|
"""
|
|
Sign a request with Signature V4.
|
|
"""
|
|
REQUIRES_REGION = True
|
|
|
|
def __init__(self, credentials, service_name, region_name):
|
|
self.credentials = credentials
|
|
# We initialize these value here so the unit tests can have
|
|
# valid values. But these will get overriden in ``add_auth``
|
|
# later for real requests.
|
|
self._now = datetime.datetime.utcnow()
|
|
self.timestamp = self._now.strftime('%Y%m%dT%H%M%SZ')
|
|
self._region_name = region_name
|
|
self._service_name = service_name
|
|
|
|
def _sign(self, key, msg, hex=False):
|
|
if hex:
|
|
sig = hmac.new(key, msg.encode('utf-8'), sha256).hexdigest()
|
|
else:
|
|
sig = hmac.new(key, msg.encode('utf-8'), sha256).digest()
|
|
return sig
|
|
|
|
def headers_to_sign(self, request):
|
|
"""
|
|
Select the headers from the request that need to be included
|
|
in the StringToSign.
|
|
"""
|
|
header_map = HTTPHeaders()
|
|
split = urlsplit(request.url)
|
|
for name, value in request.headers.items():
|
|
lname = name.lower()
|
|
header_map[lname] = value
|
|
if 'host' not in header_map:
|
|
header_map['host'] = split.netloc
|
|
return header_map
|
|
|
|
def canonical_query_string(self, request):
|
|
cqs = ''
|
|
# The query string can come from two parts. One is the
|
|
# params attribute of the request. The other is from the request
|
|
# url (in which case we have to re-split the url into its components
|
|
# and parse out the query string component).
|
|
if request.params:
|
|
return self._canonical_query_string_params(request.params)
|
|
else:
|
|
return self._canonical_query_string_url(urlsplit(request.url))
|
|
return cqs
|
|
|
|
def _canonical_query_string_params(self, params):
|
|
l = []
|
|
for param in params:
|
|
value = str(params[param])
|
|
l.append('%s=%s' % (quote(param, safe='-_.~'),
|
|
quote(value, safe='-_.~')))
|
|
l = sorted(l)
|
|
cqs = '&'.join(l)
|
|
return cqs
|
|
|
|
def _canonical_query_string_url(self, parts):
|
|
buf = ''
|
|
if parts.query:
|
|
qsa = parts.query.split('&')
|
|
qsa = [a.split('=', 1) for a in qsa]
|
|
split_qsa = []
|
|
for q in qsa:
|
|
if len(q) == 2:
|
|
split_qsa.append('%s=%s' % (q[0], q[1]))
|
|
elif len(q) == 1:
|
|
split_qsa.append('%s=' % q[0])
|
|
if len(split_qsa) > 0:
|
|
split_qsa.sort()
|
|
buf += '&'.join(split_qsa)
|
|
return buf
|
|
|
|
def canonical_headers(self, headers_to_sign):
|
|
"""
|
|
Return the headers that need to be included in the StringToSign
|
|
in their canonical form by converting all header keys to lower
|
|
case, sorting them in alphabetical order and then joining
|
|
them into a string, separated by newlines.
|
|
"""
|
|
headers = []
|
|
sorted_header_names = sorted(set(headers_to_sign))
|
|
for key in sorted_header_names:
|
|
value = ','.join(v.strip() for v in
|
|
sorted(headers_to_sign.get_all(key)))
|
|
headers.append('%s:%s' % (key, value))
|
|
return '\n'.join(headers)
|
|
|
|
def signed_headers(self, headers_to_sign):
|
|
l = ['%s' % n.lower().strip() for n in set(headers_to_sign)]
|
|
l = sorted(l)
|
|
return ';'.join(l)
|
|
|
|
def payload(self, request):
|
|
if request.body and hasattr(request.body, 'seek'):
|
|
position = request.body.tell()
|
|
read_chunksize = functools.partial(request.body.read,
|
|
PAYLOAD_BUFFER)
|
|
checksum = sha256()
|
|
for chunk in iter(read_chunksize, b''):
|
|
checksum.update(chunk)
|
|
hex_checksum = checksum.hexdigest()
|
|
request.body.seek(position)
|
|
return hex_checksum
|
|
elif request.body:
|
|
return sha256(request.body.encode('utf-8')).hexdigest()
|
|
else:
|
|
return EMPTY_SHA256_HASH
|
|
|
|
def canonical_request(self, request):
|
|
cr = [request.method.upper()]
|
|
path = self._normalize_url_path(urlsplit(request.url).path)
|
|
cr.append(path)
|
|
cr.append(self.canonical_query_string(request))
|
|
headers_to_sign = self.headers_to_sign(request)
|
|
cr.append(self.canonical_headers(headers_to_sign) + '\n')
|
|
cr.append(self.signed_headers(headers_to_sign))
|
|
if 'X-Amz-Content-SHA256' in request.headers:
|
|
body_checksum = request.headers['X-Amz-Content-SHA256']
|
|
else:
|
|
body_checksum = self.payload(request)
|
|
cr.append(body_checksum)
|
|
return '\n'.join(cr)
|
|
|
|
def _normalize_url_path(self, path):
|
|
normalized_path = quote(normalize_url_path(path), safe='/~')
|
|
return normalized_path
|
|
|
|
def scope(self, args):
|
|
scope = [self.credentials.access_key]
|
|
scope.append(self.timestamp[0:8])
|
|
scope.append(self._region_name)
|
|
scope.append(self._service_name)
|
|
scope.append('aws4_request')
|
|
return '/'.join(scope)
|
|
|
|
def credential_scope(self, args):
|
|
scope = []
|
|
scope.append(self.timestamp[0:8])
|
|
scope.append(self._region_name)
|
|
scope.append(self._service_name)
|
|
scope.append('aws4_request')
|
|
return '/'.join(scope)
|
|
|
|
def string_to_sign(self, request, canonical_request):
|
|
"""
|
|
Return the canonical StringToSign as well as a dict
|
|
containing the original version of all headers that
|
|
were included in the StringToSign.
|
|
"""
|
|
sts = ['AWS4-HMAC-SHA256']
|
|
sts.append(self.timestamp)
|
|
sts.append(self.credential_scope(request))
|
|
sts.append(sha256(canonical_request.encode('utf-8')).hexdigest())
|
|
return '\n'.join(sts)
|
|
|
|
def signature(self, string_to_sign):
|
|
key = self.credentials.secret_key
|
|
k_date = self._sign(('AWS4' + key).encode('utf-8'),
|
|
self.timestamp[0:8])
|
|
k_region = self._sign(k_date, self._region_name)
|
|
k_service = self._sign(k_region, self._service_name)
|
|
k_signing = self._sign(k_service, 'aws4_request')
|
|
return self._sign(k_signing, string_to_sign, hex=True)
|
|
|
|
def add_auth(self, request):
|
|
if self.credentials is None:
|
|
raise NoCredentialsError
|
|
# Create a new timestamp for each signing event
|
|
self._now = datetime.datetime.utcnow()
|
|
self.timestamp = self._now.strftime('%Y%m%dT%H%M%SZ')
|
|
# This could be a retry. Make sure the previous
|
|
# authorization header is removed first.
|
|
self._modify_request_before_signing(request)
|
|
canonical_request = self.canonical_request(request)
|
|
logger.debug("Calculating signature using v4 auth.")
|
|
logger.debug('CanonicalRequest:\n%s', canonical_request)
|
|
string_to_sign = self.string_to_sign(request, canonical_request)
|
|
logger.debug('StringToSign:\n%s', string_to_sign)
|
|
signature = self.signature(string_to_sign)
|
|
logger.debug('Signature:\n%s', signature)
|
|
|
|
self._inject_signature_to_request(request, signature)
|
|
|
|
def _inject_signature_to_request(self, request, signature):
|
|
l = ['AWS4-HMAC-SHA256 Credential=%s' % self.scope(request)]
|
|
headers_to_sign = self.headers_to_sign(request)
|
|
l.append('SignedHeaders=%s' % self.signed_headers(headers_to_sign))
|
|
l.append('Signature=%s' % signature)
|
|
request.headers['Authorization'] = ', '.join(l)
|
|
return request
|
|
|
|
def _modify_request_before_signing(self, request):
|
|
if 'Authorization' in request.headers:
|
|
del request.headers['Authorization']
|
|
self._set_necessary_date_headers(request)
|
|
if self.credentials.token:
|
|
if 'X-Amz-Security-Token' in request.headers:
|
|
del request.headers['X-Amz-Security-Token']
|
|
request.headers['X-Amz-Security-Token'] = self.credentials.token
|
|
|
|
def _set_necessary_date_headers(self, request):
|
|
# The spec allows for either the Date _or_ the X-Amz-Date value to be
|
|
# used so we check both. If there's a Date header, we use the date
|
|
# header. Otherwise we use the X-Amz-Date header.
|
|
if 'Date' in request.headers:
|
|
del request.headers['Date']
|
|
request.headers['Date'] = formatdate(
|
|
int(calendar.timegm(self._now.timetuple())))
|
|
if 'X-Amz-Date' in request.headers:
|
|
del request.headers['X-Amz-Date']
|
|
else:
|
|
if 'X-Amz-Date' in request.headers:
|
|
del request.headers['X-Amz-Date']
|
|
request.headers['X-Amz-Date'] = self.timestamp
|
|
|
|
|
|
class S3SigV4Auth(SigV4Auth):
|
|
|
|
def _modify_request_before_signing(self, request):
|
|
super(S3SigV4Auth, self)._modify_request_before_signing(request)
|
|
if 'X-Amz-Content-SHA256' in request.headers:
|
|
del request.headers['X-Amz-Content-SHA256']
|
|
request.headers['X-Amz-Content-SHA256'] = self.payload(request)
|
|
|
|
def _normalize_url_path(self, path):
|
|
# For S3, we do not normalize the path.
|
|
return path
|
|
|
|
|
|
class SigV4QueryAuth(SigV4Auth):
|
|
DEFAULT_EXPIRES = 3600
|
|
|
|
def __init__(self, credentials, service_name, region_name,
|
|
expires=DEFAULT_EXPIRES):
|
|
super(SigV4QueryAuth, self).__init__(credentials, service_name,
|
|
region_name)
|
|
self._expires = expires
|
|
|
|
def _modify_request_before_signing(self, request):
|
|
# This is our chance to add additional query params we need
|
|
# before we go about calculating the signature.
|
|
request.headers = {}
|
|
request.method = 'GET'
|
|
# Note that we're not including X-Amz-Signature.
|
|
# From the docs: "The Canonical Query String must include all the query
|
|
# parameters from the preceding table except for X-Amz-Signature.
|
|
auth_params = {
|
|
'X-Amz-Algorithm': 'AWS4-HMAC-SHA256',
|
|
'X-Amz-Credential': self.scope(request),
|
|
'X-Amz-Date': self.timestamp,
|
|
'X-Amz-Expires': self._expires,
|
|
'X-Amz-SignedHeaders': 'host',
|
|
}
|
|
if self.credentials.token is not None:
|
|
auth_params['X-Amz-Security-Token'] = self.credentials.token
|
|
# Now parse the original query string to a dict, inject our new query
|
|
# params, and serialize back to a query string.
|
|
url_parts = urlsplit(request.url)
|
|
# parse_qs makes each value a list, but in our case we know we won't
|
|
# have repeated keys so we know we have single element lists which we
|
|
# can convert back to scalar values.
|
|
query_dict = dict(
|
|
[(k, v[0]) for k, v in parse_qs(url_parts.query).items()])
|
|
# The spec is particular about this. It *has* to be:
|
|
# https://<endpoint>?<operation params>&<auth params>
|
|
# You can't mix the two types of params together, i.e just keep doing
|
|
# new_query_params.update(op_params)
|
|
# new_query_params.update(auth_params)
|
|
# percent_encode_sequence(new_query_params)
|
|
operation_params = ''
|
|
if request.data:
|
|
# We also need to move the body params into the query string.
|
|
# request.data will be populated, for example, with query services
|
|
# which normally form encode the params into the body.
|
|
# This means that request.data is a dict() of the operation params.
|
|
query_dict.update(request.data)
|
|
request.data = ''
|
|
if query_dict:
|
|
operation_params = percent_encode_sequence(query_dict) + '&'
|
|
new_query_string = (operation_params +
|
|
percent_encode_sequence(auth_params))
|
|
# url_parts is a tuple (and therefore immutable) so we need to create
|
|
# a new url_parts with the new query string.
|
|
# <part> - <index>
|
|
# scheme - 0
|
|
# netloc - 1
|
|
# path - 2
|
|
# query - 3 <-- we're replacing this.
|
|
# fragment - 4
|
|
p = url_parts
|
|
new_url_parts = (p[0], p[1], p[2], new_query_string, p[4])
|
|
request.url = urlunsplit(new_url_parts)
|
|
|
|
def _inject_signature_to_request(self, request, signature):
|
|
# Rather than calculating an "Authorization" header, for the query
|
|
# param quth, we just append an 'X-Amz-Signature' param to the end
|
|
# of the query string.
|
|
request.url += '&X-Amz-Signature=%s' % signature
|
|
|
|
|
|
class S3SigV4QueryAuth(SigV4QueryAuth):
|
|
"""S3 SigV4 auth using query parameters.
|
|
|
|
This signer will sign a request using query parameters and signature
|
|
version 4, i.e a "presigned url" signer.
|
|
|
|
Based off of:
|
|
|
|
http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html
|
|
|
|
"""
|
|
def _normalize_url_path(self, path):
|
|
# For S3, we do not normalize the path.
|
|
return path
|
|
|
|
def payload(self, request):
|
|
# From the doc link above:
|
|
# "You don't include a payload hash in the Canonical Request, because
|
|
# when you create a presigned URL, you don't know anything about the
|
|
# payload. Instead, you use a constant string "UNSIGNED-PAYLOAD".
|
|
return "UNSIGNED-PAYLOAD"
|
|
|
|
|
|
class HmacV1Auth(BaseSigner):
|
|
|
|
# List of Query String Arguments of Interest
|
|
QSAOfInterest = ['acl', 'cors', 'defaultObjectAcl', 'location', 'logging',
|
|
'partNumber', 'policy', 'requestPayment', 'torrent',
|
|
'versioning', 'versionId', 'versions', 'website',
|
|
'uploads', 'uploadId', 'response-content-type',
|
|
'response-content-language', 'response-expires',
|
|
'response-cache-control', 'response-content-disposition',
|
|
'response-content-encoding', 'delete', 'lifecycle',
|
|
'tagging', 'restore', 'storageClass', 'notification']
|
|
|
|
def __init__(self, credentials, service_name=None, region_name=None):
|
|
self.credentials = credentials
|
|
|
|
def sign_string(self, string_to_sign):
|
|
new_hmac = hmac.new(self.credentials.secret_key.encode('utf-8'),
|
|
digestmod=sha1)
|
|
new_hmac.update(string_to_sign.encode('utf-8'))
|
|
return encodebytes(new_hmac.digest()).strip().decode('utf-8')
|
|
|
|
def canonical_standard_headers(self, headers):
|
|
interesting_headers = ['content-md5', 'content-type', 'date']
|
|
hoi = []
|
|
if 'Date' in headers:
|
|
del headers['Date']
|
|
headers['Date'] = formatdate(usegmt=True)
|
|
for ih in interesting_headers:
|
|
found = False
|
|
for key in headers:
|
|
lk = key.lower()
|
|
if headers[key] is not None and lk == ih:
|
|
hoi.append(headers[key].strip())
|
|
found = True
|
|
if not found:
|
|
hoi.append('')
|
|
return '\n'.join(hoi)
|
|
|
|
def canonical_custom_headers(self, headers):
|
|
custom_headers = {}
|
|
for key in headers:
|
|
lk = key.lower()
|
|
if headers[key] is not None:
|
|
if lk.startswith('x-amz-'):
|
|
custom_headers[lk] = ','.join(v.strip() for v in
|
|
headers.get_all(key))
|
|
sorted_header_keys = sorted(custom_headers.keys())
|
|
hoi = []
|
|
for key in sorted_header_keys:
|
|
hoi.append("%s:%s" % (key, custom_headers[key]))
|
|
return '\n'.join(hoi)
|
|
|
|
def unquote_v(self, nv):
|
|
"""
|
|
TODO: Do we need this?
|
|
"""
|
|
if len(nv) == 1:
|
|
return nv
|
|
else:
|
|
return (nv[0], unquote(nv[1]))
|
|
|
|
def canonical_resource(self, split, auth_path=None):
|
|
# don't include anything after the first ? in the resource...
|
|
# unless it is one of the QSA of interest, defined above
|
|
# NOTE:
|
|
# The path in the canonical resource should always be the
|
|
# full path including the bucket name, even for virtual-hosting
|
|
# style addressing. The ``auth_path`` keeps track of the full
|
|
# path for the canonical resource and would be passed in if
|
|
# the client was using virtual-hosting style.
|
|
if auth_path is not None:
|
|
buf = auth_path
|
|
else:
|
|
buf = split.path
|
|
if split.query:
|
|
qsa = split.query.split('&')
|
|
qsa = [a.split('=', 1) for a in qsa]
|
|
qsa = [self.unquote_v(a) for a in qsa
|
|
if a[0] in self.QSAOfInterest]
|
|
if len(qsa) > 0:
|
|
qsa.sort(key=itemgetter(0))
|
|
qsa = ['='.join(a) for a in qsa]
|
|
buf += '?'
|
|
buf += '&'.join(qsa)
|
|
return buf
|
|
|
|
def canonical_string(self, method, split, headers, expires=None,
|
|
auth_path=None):
|
|
cs = method.upper() + '\n'
|
|
cs += self.canonical_standard_headers(headers) + '\n'
|
|
custom_headers = self.canonical_custom_headers(headers)
|
|
if custom_headers:
|
|
cs += custom_headers + '\n'
|
|
cs += self.canonical_resource(split, auth_path=auth_path)
|
|
return cs
|
|
|
|
def get_signature(self, method, split, headers, expires=None,
|
|
auth_path=None):
|
|
if self.credentials.token:
|
|
del headers['x-amz-security-token']
|
|
headers['x-amz-security-token'] = self.credentials.token
|
|
string_to_sign = self.canonical_string(method,
|
|
split,
|
|
headers,
|
|
auth_path=auth_path)
|
|
logger.debug('StringToSign:\n%s', string_to_sign)
|
|
return self.sign_string(string_to_sign)
|
|
|
|
def add_auth(self, request):
|
|
if self.credentials is None:
|
|
raise NoCredentialsError
|
|
logger.debug("Calculating signature using hmacv1 auth.")
|
|
split = urlsplit(request.url)
|
|
logger.debug('HTTP request method: %s', request.method)
|
|
signature = self.get_signature(request.method, split,
|
|
request.headers,
|
|
auth_path=request.auth_path)
|
|
if 'Authorization' in request.headers:
|
|
# We have to do this because request.headers is not
|
|
# normal dictionary. It has the (unintuitive) behavior
|
|
# of aggregating repeated setattr calls for the same
|
|
# key value. For example:
|
|
# headers['foo'] = 'a'; headers['foo'] = 'b'
|
|
# list(headers) will print ['foo', 'foo'].
|
|
del request.headers['Authorization']
|
|
request.headers['Authorization'] = (
|
|
"AWS %s:%s" % (self.credentials.access_key, signature))
|
|
|
|
|
|
# Defined at the bottom instead of the top of the module because the Auth
|
|
# classes weren't defined yet.
|
|
AUTH_TYPE_MAPS = {
|
|
'v2': SigV2Auth,
|
|
'v4': SigV4Auth,
|
|
'v3': SigV3Auth,
|
|
'v3https': SigV3Auth,
|
|
's3': HmacV1Auth,
|
|
's3v4': S3SigV4Auth,
|
|
's3v4-query': S3SigV4QueryAuth,
|
|
'v4-query': SigV4QueryAuth,
|
|
}
|