]>
Commit | Line | Data |
---|---|---|
11fdf7f2 TL |
1 | # -*- coding: utf-8 -*- |
2 | # pylint: disable-all | |
3 | # | |
4 | # Copyright (c) 2012-2013 Paul Tax <paultax@gmail.com> All rights reserved. | |
5 | # | |
6 | # Redistribution and use in source and binary forms, with or without | |
7 | # modification, are permitted provided that the following conditions are | |
8 | # met: | |
9 | # | |
10 | # 1. Redistributions of source code must retain the above copyright | |
11 | # notice, this list of conditions and the following disclaimer. | |
12 | # | |
13 | # 2. Redistributions in binary form must reproduce the above copyright | |
14 | # notice, this list of conditions and the following disclaimer in | |
15 | # the documentation and/or other materials provided with the | |
16 | # distribution. | |
17 | # | |
18 | # 3. Neither the name of Infrae nor the names of its contributors may | |
19 | # be used to endorse or promote products derived from this software | |
20 | # without specific prior written permission. | |
21 | # | |
22 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
23 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
24 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
25 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL INFRAE OR | |
26 | # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, | |
27 | # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, | |
28 | # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR | |
29 | # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF | |
30 | # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING | |
31 | # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | |
32 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
33 | ||
34 | import hmac | |
35 | ||
36 | from hashlib import sha1 as sha | |
37 | ||
38 | py3k = False | |
39 | try: | |
40 | from urlparse import urlparse, unquote | |
41 | from base64 import encodestring | |
42 | except ImportError: | |
43 | py3k = True | |
44 | from urllib.parse import urlparse, unquote | |
45 | from base64 import encodebytes as encodestring | |
46 | ||
47 | from email.utils import formatdate | |
48 | ||
49 | from requests.auth import AuthBase | |
50 | ||
51 | ||
52 | class S3Auth(AuthBase): | |
53 | ||
54 | """Attaches AWS Authentication to the given Request object.""" | |
55 | ||
56 | service_base_url = 's3.amazonaws.com' | |
57 | # List of Query String Arguments of Interest | |
58 | special_params = [ | |
59 | 'acl', 'location', 'logging', 'partNumber', 'policy', 'requestPayment', | |
60 | 'torrent', 'versioning', 'versionId', 'versions', 'website', 'uploads', | |
61 | 'uploadId', 'response-content-type', 'response-content-language', | |
62 | 'response-expires', 'response-cache-control', 'delete', 'lifecycle', | |
63 | 'response-content-disposition', 'response-content-encoding', 'tagging', | |
64 | 'notification', 'cors' | |
65 | ] | |
66 | ||
67 | def __init__(self, access_key, secret_key, service_url=None): | |
68 | if service_url: | |
69 | self.service_base_url = service_url | |
70 | self.access_key = str(access_key) | |
71 | self.secret_key = str(secret_key) | |
72 | ||
73 | def __call__(self, r): | |
74 | # Create date header if it is not created yet. | |
75 | if 'date' not in r.headers and 'x-amz-date' not in r.headers: | |
76 | r.headers['date'] = formatdate( | |
77 | timeval=None, | |
78 | localtime=False, | |
79 | usegmt=True) | |
80 | signature = self.get_signature(r) | |
81 | if py3k: | |
82 | signature = signature.decode('utf-8') | |
83 | r.headers['Authorization'] = 'AWS %s:%s' % (self.access_key, signature) | |
84 | return r | |
85 | ||
86 | def get_signature(self, r): | |
87 | canonical_string = self.get_canonical_string( | |
88 | r.url, r.headers, r.method) | |
89 | if py3k: | |
90 | key = self.secret_key.encode('utf-8') | |
91 | msg = canonical_string.encode('utf-8') | |
92 | else: | |
9f95a23c | 93 | key = self.secret_key # type: ignore |
11fdf7f2 TL |
94 | msg = canonical_string |
95 | h = hmac.new(key, msg, digestmod=sha) | |
96 | return encodestring(h.digest()).strip() | |
97 | ||
98 | def get_canonical_string(self, url, headers, method): | |
99 | parsedurl = urlparse(url) | |
100 | objectkey = parsedurl.path[1:] | |
101 | query_args = sorted(parsedurl.query.split('&')) | |
102 | ||
103 | bucket = parsedurl.netloc[:-len(self.service_base_url)] | |
104 | if len(bucket) > 1: | |
105 | # remove last dot | |
106 | bucket = bucket[:-1] | |
107 | ||
108 | interesting_headers = { | |
109 | 'content-md5': '', | |
110 | 'content-type': '', | |
111 | 'date': ''} | |
112 | for key in headers: | |
113 | lk = key.lower() | |
114 | try: | |
115 | if isinstance(lk, bytes): | |
116 | lk = lk.decode('utf-8') | |
117 | except UnicodeDecodeError: | |
118 | pass | |
119 | if headers[key] and (lk in interesting_headers.keys() | |
120 | or lk.startswith('x-amz-')): | |
121 | interesting_headers[lk] = headers[key].strip() | |
122 | ||
123 | # If x-amz-date is used it supersedes the date header. | |
124 | if not py3k: | |
125 | if 'x-amz-date' in interesting_headers: | |
126 | interesting_headers['date'] = '' | |
127 | else: | |
128 | if 'x-amz-date' in interesting_headers: | |
129 | interesting_headers['date'] = '' | |
130 | ||
131 | buf = '%s\n' % method | |
132 | for key in sorted(interesting_headers.keys()): | |
133 | val = interesting_headers[key] | |
134 | if key.startswith('x-amz-'): | |
135 | buf += '%s:%s\n' % (key, val) | |
136 | else: | |
137 | buf += '%s\n' % val | |
138 | ||
139 | # append the bucket if it exists | |
140 | if bucket != '': | |
141 | buf += '/%s' % bucket | |
142 | ||
143 | # add the objectkey. even if it doesn't exist, add the slash | |
144 | buf += '/%s' % objectkey | |
145 | ||
146 | params_found = False | |
147 | ||
148 | # handle special query string arguments | |
149 | for q in query_args: | |
150 | k = q.split('=')[0] | |
151 | if k in self.special_params: | |
152 | buf += '&' if params_found else '?' | |
153 | params_found = True | |
154 | ||
155 | try: | |
156 | k, v = q.split('=', 1) | |
157 | ||
158 | except ValueError: | |
159 | buf += q | |
160 | ||
161 | else: | |
162 | # Riak CS multipart upload ids look like this, `TFDSheOgTxC2Tsh1qVK73A==`, | |
163 | # is should be escaped to be included as part of a query string. | |
164 | # | |
165 | # A requests mp upload part request may look like | |
166 | # resp = requests.put( | |
167 | # 'https://url_here', | |
168 | # params={ | |
169 | # 'partNumber': 1, | |
170 | # 'uploadId': 'TFDSheOgTxC2Tsh1qVK73A==' | |
171 | # }, | |
172 | # data='some data', | |
173 | # auth=S3Auth('access_key', 'secret_key') | |
174 | # ) | |
175 | # | |
176 | # Requests automatically escapes the values in the `params` dict, so now | |
177 | # our uploadId is `TFDSheOgTxC2Tsh1qVK73A%3D%3D`, | |
178 | # if we sign the request with the encoded value the signature will | |
179 | # not be valid, we'll get 403 Access Denied. | |
180 | # So we unquote, this is no-op if the value isn't encoded. | |
181 | buf += '{key}={value}'.format(key=k, value=unquote(v)) | |
182 | ||
183 | return buf |