OLD | NEW |
1 # Copyright (c) 2006-2010 Mitch Garnaat http://garnaat.org/ | 1 # Copyright (c) 2006-2010 Mitch Garnaat http://garnaat.org/ |
2 # Copyright (c) 2010, Eucalyptus Systems, Inc. | 2 # Copyright (c) 2010, Eucalyptus Systems, Inc. |
3 # All rights reserved. | 3 # All rights reserved. |
4 # | 4 # |
5 # Permission is hereby granted, free of charge, to any person obtaining a | 5 # Permission is hereby granted, free of charge, to any person obtaining a |
6 # copy of this software and associated documentation files (the | 6 # copy of this software and associated documentation files (the |
7 # "Software"), to deal in the Software without restriction, including | 7 # "Software"), to deal in the Software without restriction, including |
8 # without limitation the rights to use, copy, modify, merge, publish, dis- | 8 # without limitation the rights to use, copy, modify, merge, publish, dis- |
9 # tribute, sublicense, and/or sell copies of the Software, and to permit | 9 # tribute, sublicense, and/or sell copies of the Software, and to permit |
10 # persons to whom the Software is furnished to do so, subject to the fol- | 10 # persons to whom the Software is furnished to do so, subject to the fol- |
11 # lowing conditions: | 11 # lowing conditions: |
12 # | 12 # |
13 # The above copyright notice and this permission notice shall be included | 13 # The above copyright notice and this permission notice shall be included |
14 # in all copies or substantial portions of the Software. | 14 # in all copies or substantial portions of the Software. |
15 # | 15 # |
16 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS | 16 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS |
17 # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL- | 17 # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL- |
18 # ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT | 18 # ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT |
19 # SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, | 19 # SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, |
20 # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | 20 # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
21 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS | 21 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS |
22 # IN THE SOFTWARE. | 22 # IN THE SOFTWARE. |
23 | 23 |
24 import xml.sax | 24 import xml.sax |
25 import urllib, base64 | 25 import urllib, base64 |
26 import time | 26 import time |
27 import boto.utils | 27 import boto.utils |
28 from boto.connection import AWSAuthConnection | 28 from boto.connection import AWSAuthConnection |
29 from boto import handler | 29 from boto import handler |
30 from boto.provider import Provider | |
31 from boto.s3.bucket import Bucket | 30 from boto.s3.bucket import Bucket |
32 from boto.s3.key import Key | 31 from boto.s3.key import Key |
33 from boto.resultset import ResultSet | 32 from boto.resultset import ResultSet |
34 from boto.exception import BotoClientError | 33 from boto.exception import BotoClientError |
35 | 34 |
36 def check_lowercase_bucketname(n): | 35 def check_lowercase_bucketname(n): |
37 """ | 36 """ |
38 Bucket names must not contain uppercase characters. We check for | 37 Bucket names must not contain uppercase characters. We check for |
39 this by appending a lowercase character and testing with islower(). | 38 this by appending a lowercase character and testing with islower(). |
40 Note this also covers cases like numeric bucket names with dashes. | 39 Note this also covers cases like numeric bucket names with dashes. |
(...skipping 16 matching lines...) Expand all Loading... |
57 "hosting calling format.") | 56 "hosting calling format.") |
58 return True | 57 return True |
59 | 58 |
60 def assert_case_insensitive(f): | 59 def assert_case_insensitive(f): |
61 def wrapper(*args, **kwargs): | 60 def wrapper(*args, **kwargs): |
62 if len(args) == 3 and check_lowercase_bucketname(args[2]): | 61 if len(args) == 3 and check_lowercase_bucketname(args[2]): |
63 pass | 62 pass |
64 return f(*args, **kwargs) | 63 return f(*args, **kwargs) |
65 return wrapper | 64 return wrapper |
66 | 65 |
67 class _CallingFormat: | 66 class _CallingFormat(object): |
| 67 |
| 68 def get_bucket_server(self, server, bucket): |
| 69 return '' |
68 | 70 |
69 def build_url_base(self, connection, protocol, server, bucket, key=''): | 71 def build_url_base(self, connection, protocol, server, bucket, key=''): |
70 url_base = '%s://' % protocol | 72 url_base = '%s://' % protocol |
71 url_base += self.build_host(server, bucket) | 73 url_base += self.build_host(server, bucket) |
72 url_base += connection.get_path(self.build_path_base(bucket, key)) | 74 url_base += connection.get_path(self.build_path_base(bucket, key)) |
73 return url_base | 75 return url_base |
74 | 76 |
75 def build_host(self, server, bucket): | 77 def build_host(self, server, bucket): |
76 if bucket == '': | 78 if bucket == '': |
77 return server | 79 return server |
78 else: | 80 else: |
79 return self.get_bucket_server(server, bucket) | 81 return self.get_bucket_server(server, bucket) |
80 | 82 |
81 def build_auth_path(self, bucket, key=''): | 83 def build_auth_path(self, bucket, key=''): |
| 84 key = boto.utils.get_utf8_value(key) |
82 path = '' | 85 path = '' |
83 if bucket != '': | 86 if bucket != '': |
84 path = '/' + bucket | 87 path = '/' + bucket |
85 return path + '/%s' % urllib.quote(key) | 88 return path + '/%s' % urllib.quote(key) |
86 | 89 |
87 def build_path_base(self, bucket, key=''): | 90 def build_path_base(self, bucket, key=''): |
| 91 key = boto.utils.get_utf8_value(key) |
88 return '/%s' % urllib.quote(key) | 92 return '/%s' % urllib.quote(key) |
89 | 93 |
90 class SubdomainCallingFormat(_CallingFormat): | 94 class SubdomainCallingFormat(_CallingFormat): |
91 | 95 |
92 @assert_case_insensitive | 96 @assert_case_insensitive |
93 def get_bucket_server(self, server, bucket): | 97 def get_bucket_server(self, server, bucket): |
94 return '%s.%s' % (bucket, server) | 98 return '%s.%s' % (bucket, server) |
95 | 99 |
96 class VHostCallingFormat(_CallingFormat): | 100 class VHostCallingFormat(_CallingFormat): |
97 | 101 |
98 @assert_case_insensitive | 102 @assert_case_insensitive |
99 def get_bucket_server(self, server, bucket): | 103 def get_bucket_server(self, server, bucket): |
100 return bucket | 104 return bucket |
101 | 105 |
102 class OrdinaryCallingFormat(_CallingFormat): | 106 class OrdinaryCallingFormat(_CallingFormat): |
103 | 107 |
104 def get_bucket_server(self, server, bucket): | 108 def get_bucket_server(self, server, bucket): |
105 return server | 109 return server |
106 | 110 |
107 def build_path_base(self, bucket, key=''): | 111 def build_path_base(self, bucket, key=''): |
| 112 key = boto.utils.get_utf8_value(key) |
108 path_base = '/' | 113 path_base = '/' |
109 if bucket: | 114 if bucket: |
110 path_base += "%s/" % bucket | 115 path_base += "%s/" % bucket |
111 return path_base + urllib.quote(key) | 116 return path_base + urllib.quote(key) |
112 | 117 |
| 118 class ProtocolIndependentOrdinaryCallingFormat(OrdinaryCallingFormat): |
| 119 |
| 120 def build_url_base(self, connection, protocol, server, bucket, key=''): |
| 121 url_base = '//' |
| 122 url_base += self.build_host(server, bucket) |
| 123 url_base += connection.get_path(self.build_path_base(bucket, key)) |
| 124 return url_base |
| 125 |
113 class Location: | 126 class Location: |
114 DEFAULT = '' # US Classic Region | 127 DEFAULT = '' # US Classic Region |
115 EU = 'EU' | 128 EU = 'EU' |
116 USWest = 'us-west-1' | 129 USWest = 'us-west-1' |
| 130 APNortheast = 'ap-northeast-1' |
117 APSoutheast = 'ap-southeast-1' | 131 APSoutheast = 'ap-southeast-1' |
118 | 132 |
119 class S3Connection(AWSAuthConnection): | 133 class S3Connection(AWSAuthConnection): |
120 | 134 |
121 DefaultHost = 's3.amazonaws.com' | 135 DefaultHost = 's3.amazonaws.com' |
122 QueryString = 'Signature=%s&Expires=%d&AWSAccessKeyId=%s' | 136 QueryString = 'Signature=%s&Expires=%d&AWSAccessKeyId=%s' |
123 | 137 |
124 def __init__(self, aws_access_key_id=None, aws_secret_access_key=None, | 138 def __init__(self, aws_access_key_id=None, aws_secret_access_key=None, |
125 is_secure=True, port=None, proxy=None, proxy_port=None, | 139 is_secure=True, port=None, proxy=None, proxy_port=None, |
126 proxy_user=None, proxy_pass=None, | 140 proxy_user=None, proxy_pass=None, |
127 host=DefaultHost, debug=0, https_connection_factory=None, | 141 host=DefaultHost, debug=0, https_connection_factory=None, |
128 calling_format=SubdomainCallingFormat(), path='/', provider='aw
s', | 142 calling_format=SubdomainCallingFormat(), path='/', |
129 bucket_class=Bucket): | 143 provider='aws', bucket_class=Bucket, security_token=None): |
130 self.calling_format = calling_format | 144 self.calling_format = calling_format |
131 self.bucket_class = bucket_class | 145 self.bucket_class = bucket_class |
132 AWSAuthConnection.__init__(self, host, | 146 AWSAuthConnection.__init__(self, host, |
133 aws_access_key_id, aws_secret_access_key, | 147 aws_access_key_id, aws_secret_access_key, |
134 is_secure, port, proxy, proxy_port, proxy_user, proxy_pass, | 148 is_secure, port, proxy, proxy_port, proxy_user, proxy_pass, |
135 debug=debug, https_connection_factory=https_connection_factory, | 149 debug=debug, https_connection_factory=https_connection_factory, |
136 path=path, provider=provider) | 150 path=path, provider=provider, security_token=security_token) |
137 | 151 |
138 def _required_auth_capability(self): | 152 def _required_auth_capability(self): |
139 return ['s3'] | 153 return ['s3'] |
140 | 154 |
141 def __iter__(self): | 155 def __iter__(self): |
142 for bucket in self.get_all_buckets(): | 156 for bucket in self.get_all_buckets(): |
143 yield bucket | 157 yield bucket |
144 | 158 |
145 def __contains__(self, bucket_name): | 159 def __contains__(self, bucket_name): |
146 return not (self.lookup(bucket_name) is None) | 160 return not (self.lookup(bucket_name) is None) |
147 | 161 |
148 def set_bucket_class(self, bucket_class): | 162 def set_bucket_class(self, bucket_class): |
149 """ | 163 """ |
150 Set the Bucket class associated with this bucket. By default, this | 164 Set the Bucket class associated with this bucket. By default, this |
151 would be the boto.s3.key.Bucket class but if you want to subclass that | 165 would be the boto.s3.key.Bucket class but if you want to subclass that |
152 for some reason this allows you to associate your new class. | 166 for some reason this allows you to associate your new class. |
153 | 167 |
154 :type bucket_class: class | 168 :type bucket_class: class |
155 :param bucket_class: A subclass of Bucket that can be more specific | 169 :param bucket_class: A subclass of Bucket that can be more specific |
156 """ | 170 """ |
157 self.bucket_class = bucket_class | 171 self.bucket_class = bucket_class |
158 | 172 |
159 def build_post_policy(self, expiration_time, conditions): | 173 def build_post_policy(self, expiration_time, conditions): |
160 """ | 174 """ |
161 Taken from the AWS book Python examples and modified for use with boto | 175 Taken from the AWS book Python examples and modified for use with boto |
162 """ | 176 """ |
163 assert type(expiration_time) == time.struct_time, \ | 177 assert type(expiration_time) == time.struct_time, \ |
164 'Policy document must include a valid expiration Time object' | 178 'Policy document must include a valid expiration Time object' |
165 | 179 |
166 # Convert conditions object mappings to condition statements | 180 # Convert conditions object mappings to condition statements |
167 | 181 |
168 return '{"expiration": "%s",\n"conditions": [%s]}' % \ | 182 return '{"expiration": "%s",\n"conditions": [%s]}' % \ |
169 (time.strftime(boto.utils.ISO8601, expiration_time), ",".join(condit
ions)) | 183 (time.strftime(boto.utils.ISO8601, expiration_time), ",".join(condit
ions)) |
170 | 184 |
171 | 185 |
172 def build_post_form_args(self, bucket_name, key, expires_in = 6000, | 186 def build_post_form_args(self, bucket_name, key, expires_in = 6000, |
173 acl = None, success_action_redirect = None, max_content_
length = None, | 187 acl = None, success_action_redirect = None, |
174 http_method = "http", fields=None, conditions=None): | 188 max_content_length = None, |
| 189 http_method = "http", fields=None, |
| 190 conditions=None): |
175 """ | 191 """ |
176 Taken from the AWS book Python examples and modified for use with boto | 192 Taken from the AWS book Python examples and modified for use with boto |
177 This only returns the arguments required for the post form, not the actu
al form | 193 This only returns the arguments required for the post form, not the actu
al form |
178 This does not return the file input field which also needs to be added | 194 This does not return the file input field which also needs to be added |
179 | 195 |
180 :param bucket_name: Bucket to submit to | 196 :param bucket_name: Bucket to submit to |
181 :type bucket_name: string | 197 :type bucket_name: string |
182 | 198 |
183 :param key: Key name, optionally add ${filename} to the end to attach t
he submitted filename | 199 :param key: Key name, optionally add ${filename} to the end to attach t
he submitted filename |
184 :type key: string | 200 :type key: string |
(...skipping 69 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
254 signature = self._auth_handler.sign_string(policy_b64) | 270 signature = self._auth_handler.sign_string(policy_b64) |
255 fields.append({"name": "signature", "value": signature}) | 271 fields.append({"name": "signature", "value": signature}) |
256 fields.append({"name": "key", "value": key}) | 272 fields.append({"name": "key", "value": key}) |
257 | 273 |
258 # HTTPS protocol will be used if the secure HTTP option is enabled. | 274 # HTTPS protocol will be used if the secure HTTP option is enabled. |
259 url = '%s://%s/' % (http_method, self.calling_format.build_host(self.ser
ver_name(), bucket_name)) | 275 url = '%s://%s/' % (http_method, self.calling_format.build_host(self.ser
ver_name(), bucket_name)) |
260 | 276 |
261 return {"action": url, "fields": fields} | 277 return {"action": url, "fields": fields} |
262 | 278 |
263 | 279 |
264 def generate_url(self, expires_in, method, bucket='', key='', | 280 def generate_url(self, expires_in, method, bucket='', key='', headers=None, |
265 headers=None, query_auth=True, force_http=False): | 281 query_auth=True, force_http=False, response_headers=None): |
266 if not headers: | 282 if not headers: |
267 headers = {} | 283 headers = {} |
268 expires = int(time.time() + expires_in) | 284 expires = int(time.time() + expires_in) |
269 auth_path = self.calling_format.build_auth_path(bucket, key) | 285 auth_path = self.calling_format.build_auth_path(bucket, key) |
270 auth_path = self.get_path(auth_path) | 286 auth_path = self.get_path(auth_path) |
| 287 # Arguments to override response headers become part of the canonical |
| 288 # string to be signed. |
| 289 if response_headers: |
| 290 response_hdrs = ["%s=%s" % (k, v) for k, v in |
| 291 response_headers.items()] |
| 292 delimiter = '?' if '?' not in auth_path else '&' |
| 293 auth_path = "%s%s%s" % (auth_path, delimiter, '&'.join(response_hdrs
)) |
| 294 else: |
| 295 response_headers = {} |
271 c_string = boto.utils.canonical_string(method, auth_path, headers, | 296 c_string = boto.utils.canonical_string(method, auth_path, headers, |
272 expires, self.provider) | 297 expires, self.provider) |
273 b64_hmac = self._auth_handler.sign_string(c_string) | 298 b64_hmac = self._auth_handler.sign_string(c_string) |
274 encoded_canonical = urllib.quote_plus(b64_hmac) | 299 encoded_canonical = urllib.quote_plus(b64_hmac) |
275 self.calling_format.build_path_base(bucket, key) | 300 self.calling_format.build_path_base(bucket, key) |
276 if query_auth: | 301 if query_auth: |
277 query_part = '?' + self.QueryString % (encoded_canonical, expires, | 302 query_part = '?' + self.QueryString % (encoded_canonical, expires, |
278 self.aws_access_key_id) | 303 self.aws_access_key_id) |
279 sec_hdr = self.provider.security_token_header | 304 # The response headers must also be GET parameters in the URL. |
280 if sec_hdr in headers: | 305 headers.update(response_headers) |
281 query_part += ('&%s=%s' % (sec_hdr, | 306 hdrs = [ '%s=%s'%(name, urllib.quote(val)) for name,val in headers.i
tems() ] |
282 urllib.quote(headers[sec_hdr]))); | 307 q_str = '&'.join(hdrs) |
| 308 if q_str: |
| 309 query_part += '&' + q_str |
283 else: | 310 else: |
284 query_part = '' | 311 query_part = '' |
285 if force_http: | 312 if force_http: |
286 protocol = 'http' | 313 protocol = 'http' |
287 port = 80 | 314 port = 80 |
288 else: | 315 else: |
289 protocol = self.protocol | 316 protocol = self.protocol |
290 port = self.port | 317 port = self.port |
291 return self.calling_format.build_url_base(self, protocol, self.server_na
me(port), | 318 return self.calling_format.build_url_base(self, protocol, self.server_na
me(port), |
292 bucket, key) + query_part | 319 bucket, key) + query_part |
293 | 320 |
294 def get_all_buckets(self, headers=None): | 321 def get_all_buckets(self, headers=None): |
295 response = self.make_request('GET', headers=headers) | 322 response = self.make_request('GET', headers=headers) |
296 body = response.read() | 323 body = response.read() |
297 if response.status > 300: | 324 if response.status > 300: |
298 raise self.provider.storage_response_error( | 325 raise self.provider.storage_response_error( |
299 response.status, response.reason, body) | 326 response.status, response.reason, body) |
300 rs = ResultSet([('Bucket', self.bucket_class)]) | 327 rs = ResultSet([('Bucket', self.bucket_class)]) |
301 h = handler.XmlHandler(rs, self) | 328 h = handler.XmlHandler(rs, self) |
302 xml.sax.parseString(body, h) | 329 xml.sax.parseString(body, h) |
303 return rs | 330 return rs |
304 | 331 |
305 def get_canonical_user_id(self, headers=None): | 332 def get_canonical_user_id(self, headers=None): |
306 """ | 333 """ |
307 Convenience method that returns the "CanonicalUserID" of the user who's
credentials | 334 Convenience method that returns the "CanonicalUserID" of the |
308 are associated with the connection. The only way to get this value is t
o do a GET | 335 user who's credentials are associated with the connection. |
309 request on the service which returns all buckets associated with the acc
ount. As part | 336 The only way to get this value is to do a GET request on the |
310 of that response, the canonical userid is returned. This method simply
does all of | 337 service which returns all buckets associated with the account. |
311 that and then returns just the user id. | 338 As part of that response, the canonical userid is returned. |
| 339 This method simply does all of that and then returns just the |
| 340 user id. |
312 | 341 |
313 :rtype: string | 342 :rtype: string |
314 :return: A string containing the canonical user id. | 343 :return: A string containing the canonical user id. |
315 """ | 344 """ |
316 rs = self.get_all_buckets(headers=headers) | 345 rs = self.get_all_buckets(headers=headers) |
317 return rs.ID | 346 return rs.ID |
318 | 347 |
319 def get_bucket(self, bucket_name, validate=True, headers=None): | 348 def get_bucket(self, bucket_name, validate=True, headers=None): |
320 bucket = self.bucket_class(self, bucket_name) | 349 bucket = self.bucket_class(self, bucket_name) |
321 if validate: | 350 if validate: |
(...skipping 70 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
392 host = self.calling_format.build_host(self.server_name(), bucket) | 421 host = self.calling_format.build_host(self.server_name(), bucket) |
393 if query_args: | 422 if query_args: |
394 path += '?' + query_args | 423 path += '?' + query_args |
395 boto.log.debug('path=%s' % path) | 424 boto.log.debug('path=%s' % path) |
396 auth_path += '?' + query_args | 425 auth_path += '?' + query_args |
397 boto.log.debug('auth_path=%s' % auth_path) | 426 boto.log.debug('auth_path=%s' % auth_path) |
398 return AWSAuthConnection.make_request(self, method, path, headers, | 427 return AWSAuthConnection.make_request(self, method, path, headers, |
399 data, host, auth_path, sender, | 428 data, host, auth_path, sender, |
400 override_num_retries=override_num_retries) | 429 override_num_retries=override_num_retries) |
401 | 430 |
OLD | NEW |