Skip to content

Commit 0ca7e98

Browse files
author
Harshavardhana
committed
Merge pull request #258 from harshavardhana/pr_out_add_new_presignurl_api
Add new presignURL api, tests and examples, overall cleanup
2 parents 3927f0d + b65fb57 commit 0ca7e98

File tree

5 files changed

+224
-61
lines changed

5 files changed

+224
-61
lines changed

examples/presigned_get_object.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# -*- coding: utf-8 -*-
2+
# Minio Python Library for Amazon S3 Compatible Cloud Storage, (C) 2015 Minio, Inc.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
import hashlib
16+
17+
from minio import Minio
18+
19+
__author__ = 'minio'
20+
21+
# find out your s3 end point here:
22+
# http://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region
23+
24+
client = Minio('https://<your-s3-endpoint>',
25+
access_key='YOUR-ACCESSKEYID',
26+
secret_key='YOUR-SECRETACCESSKEY')
27+
28+
print client.presigned_get_object('mybucket', 'myobject')

minio/minio.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
parse_new_multipart_upload)
3838
from .error import ResponseError
3939
from .definitions import Object
40-
from .signer import sign_v4
40+
from .signer import sign_v4, presign_v4
4141
from .xml_requests import bucket_constraint, get_complete_multipart_upload
4242

4343
class Minio(object):
@@ -329,6 +329,40 @@ def drop_all_incomplete_uploads(self, bucket):
329329
for upload in uploads:
330330
self._drop_incomplete_upload(bucket, upload.key, upload.upload_id)
331331

332+
def presigned_get_object(self, bucket, key, expires=None):
333+
"""
334+
Presigns a get object request and provides a url
335+
"""
336+
return self.presigned_get_partial_object(bucket, key, expires)
337+
338+
def presigned_get_partial_object(self, bucket, key, expires=None, offset=0, length=0):
339+
"""
340+
"""
341+
is_valid_bucket_name(bucket)
342+
is_non_empty_string(key)
343+
344+
request_range = ''
345+
if offset is not 0 and length is not 0:
346+
request_range = str(offset) + "-" + str(offset + length - 1)
347+
if offset is not 0 and length is 0:
348+
request_range = str(offset) + "-"
349+
if offset is 0 and length is not 0:
350+
request_range = "0-" + str(length - 1)
351+
352+
method = 'GET'
353+
url = get_target_url(self._endpoint_url, bucket=bucket, key=key)
354+
headers = {}
355+
356+
if request_range:
357+
headers['Range'] = 'bytes=' + request_range
358+
359+
method = 'GET'
360+
presign_url = presign_v4(method=method, url=url, headers=headers,
361+
access_key=self._access_key,
362+
secret_key=self._secret_key)
363+
364+
return presign_url
365+
332366
def get_object(self, bucket, key):
333367
"""
334368
Retrieves an object from a bucket.

minio/signer.py

Lines changed: 121 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,106 @@
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
1515

16+
import collections
1617
import hashlib
1718
import hmac
1819
import binascii
1920

2021
from datetime import datetime
21-
from .compat import urlsplit, strtype
22+
from .error import InvalidArgumentError
23+
from .compat import urlsplit, strtype, urlencode
2224
from .helpers import get_region
2325

26+
def presign_v4(method, url, headers=None, access_key=None, secret_key=None, expires=None):
27+
if not access_key or not secret_key:
28+
raise InvalidArgumentError('invalid access/secret id')
29+
30+
# verify only if 'None' not on expires with 0 value which should
31+
# be an InvalidArgument is handled later below
32+
if expires is None:
33+
expires = 604800
34+
35+
if expires < 1 or expires > 604800:
36+
raise InvalidArgumentError('expires param valid values are between 1 secs to 604800 secs')
37+
38+
if headers is None:
39+
headers = {}
40+
41+
parsed_url = urlsplit(url)
42+
content_hash_hex = 'UNSIGNED-PAYLOAD'
43+
host = parsed_url.netloc
44+
headers['host'] = host
45+
date = datetime.utcnow()
46+
iso8601Date = date.strftime("%Y%m%dT%H%M%SZ")
47+
region = get_region(parsed_url.hostname)
48+
49+
headers_to_sign = dict(headers)
50+
ignored_headers = ['Authorization', 'Content-Length', 'Content-Type',
51+
'User-Agent']
52+
53+
for ignored_header in ignored_headers:
54+
if ignored_header in headers_to_sign:
55+
del headers_to_sign[ignored_header]
56+
57+
query = {}
58+
query['X-Amz-Algorithm'] = 'AWS4-HMAC-SHA256'
59+
query['X-Amz-Credential'] = generate_credential_string(access_key, date, region)
60+
query['X-Amz-Date'] = iso8601Date
61+
query['X-Amz-Expires'] = expires
62+
query['X-Amz-SignedHeaders'] = ';'.join(get_signed_headers(headers_to_sign))
63+
64+
url_components = [parsed_url.geturl()]
65+
if query is not None:
66+
ordered_query = collections.OrderedDict(sorted(query.items()))
67+
query_components = []
68+
for component_key in ordered_query:
69+
single_component = [component_key]
70+
if ordered_query[component_key] is not None:
71+
single_component.append('=')
72+
single_component.append(
73+
urlencode(str(ordered_query[component_key])).replace('/', '%2F'))
74+
query_components.append(''.join(single_component))
75+
76+
query_string = '&'.join(query_components)
77+
if query_string:
78+
url_components.append('?')
79+
url_components.append(query_string)
80+
new_url = ''.join(url_components)
81+
new_parsed_url = urlsplit(new_url)
82+
canonical_request = generate_canonical_request(method,
83+
new_parsed_url,
84+
headers_to_sign,
85+
content_hash_hex)
86+
87+
canonical_request_hasher = hashlib.sha256()
88+
canonical_request_hasher.update(canonical_request.encode('utf-8'))
89+
canonical_request_sha256 = canonical_request_hasher.hexdigest()
90+
91+
string_to_sign = generate_string_to_sign(date, region,
92+
canonical_request_sha256)
93+
signing_key = generate_signing_key(date, region, secret_key)
94+
signature = hmac.new(signing_key, string_to_sign.encode('utf-8'),
95+
hashlib.sha256).hexdigest()
96+
97+
new_parsed_url = urlsplit(new_url + "&X-Amz-Signature="+signature)
98+
return new_parsed_url.geturl()
99+
100+
def get_signed_headers(headers):
101+
headers_to_sign = dict(headers)
102+
ignored_headers = ['Authorization', 'Content-Length', 'Content-Type',
103+
'User-Agent']
104+
105+
for ignored_header in ignored_headers:
106+
if ignored_header in headers_to_sign:
107+
del headers_to_sign[ignored_header]
108+
109+
signed_headers = []
110+
for header in headers:
111+
signed_headers.append(header)
112+
signed_headers.sort()
113+
114+
return signed_headers
115+
24116
def sign_v4(method, url, headers=None, access_key=None, secret_key=None,
25117
content_hash=None):
26118
if not access_key or not secret_key:
@@ -36,7 +128,10 @@ def sign_v4(method, url, headers=None, access_key=None, secret_key=None,
36128

37129
host = parsed_url.netloc
38130
headers['host'] = host
39-
headers['x-amz-date'] = datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")
131+
region = get_region(parsed_url.hostname)
132+
133+
date = datetime.utcnow()
134+
headers['x-amz-date'] = date.strftime("%Y%m%dT%H%M%SZ")
40135
headers['x-amz-content-sha256'] = content_hash_hex
41136

42137
headers_to_sign = dict(headers)
@@ -75,14 +170,11 @@ def sign_v4(method, url, headers=None, access_key=None, secret_key=None,
75170
if ignored_header in headers_to_sign:
76171
del headers_to_sign[ignored_header]
77172

78-
canonical_request, signed_headers = generate_canonical_request(method,
79-
parsed_url,
80-
headers_to_sign,
81-
content_hash_hex)
82-
83-
region = get_region(parsed_url.hostname)
84-
85-
date = datetime.utcnow()
173+
signed_headers = get_signed_headers(headers_to_sign)
174+
canonical_request = generate_canonical_request(method,
175+
parsed_url,
176+
headers_to_sign,
177+
content_hash_hex)
86178

87179
canonical_request_hasher = hashlib.sha256()
88180
canonical_request_hasher.update(canonical_request.encode('utf-8'))
@@ -91,12 +183,12 @@ def sign_v4(method, url, headers=None, access_key=None, secret_key=None,
91183
string_to_sign = generate_string_to_sign(date, region,
92184
canonical_request_sha256)
93185
signing_key = generate_signing_key(date, region, secret_key)
94-
signed_request = hmac.new(signing_key, string_to_sign.encode('utf-8'),
95-
hashlib.sha256).hexdigest()
186+
signature = hmac.new(signing_key, string_to_sign.encode('utf-8'),
187+
hashlib.sha256).hexdigest()
96188

97189
authorization_header = generate_authorization_header(access_key, date, region,
98190
signed_headers,
99-
signed_request)
191+
signature)
100192

101193
headers['authorization'] = authorization_header
102194

@@ -134,21 +226,15 @@ def generate_canonical_request(method, parsed_url, headers, content_hash_hex):
134226
lines.append(';'.join(signed_headers))
135227
lines.append(str(content_hash_hex))
136228

137-
return '\n'.join(lines), signed_headers
229+
return '\n'.join(lines)
138230

139231

140232
def generate_string_to_sign(date, region, request_hash):
141233
formatted_date_time = date.strftime("%Y%m%dT%H%M%SZ")
142-
formatted_date = date.strftime("%Y%m%d")
143-
144-
scope = '/'.join([formatted_date,
145-
region,
146-
's3',
147-
'aws4_request'])
148234

149235
return '\n'.join(['AWS4-HMAC-SHA256',
150236
formatted_date_time,
151-
scope,
237+
generate_scope_string(date, region),
152238
request_hash])
153239

154240

@@ -165,15 +251,21 @@ def generate_signing_key(date, region, secret):
165251
return hmac.new(key4, 'aws4_request'.encode('utf-8'),
166252
hashlib.sha256).digest()
167253

254+
def generate_scope_string(date, region):
255+
formatted_date = date.strftime("%Y%m%d")
256+
scope = '/'.join([formatted_date,
257+
region,
258+
's3',
259+
'aws4_request'])
260+
return scope
261+
262+
def generate_credential_string(access_key, date, region):
263+
return access_key + '/' + generate_scope_string(date, region)
168264

169265
def generate_authorization_header(access_key, date, region, signed_headers,
170-
signed_request):
171-
formatted_date = date.strftime("%Y%m%d")
266+
signature):
172267
signed_headers_string = ';'.join(signed_headers)
173-
auth_header = "AWS4-HMAC-SHA256 Credential=" + access_key + "/" + \
174-
formatted_date + "/" + region + \
175-
"/s3/aws4_request, SignedHeaders=" + \
176-
signed_headers_string + \
177-
", Signature=" + \
178-
signed_request
268+
credential = generate_credential_string(access_key, date, region)
269+
auth_header = "AWS4-HMAC-SHA256 Credential=" + credential + ", SignedHeaders=" + \
270+
signed_headers_string + ", Signature=" + signature
179271
return auth_header

tests/unit/compat.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@
1616
import sys
1717

1818
try:
19-
from urllib.parse import urlparse as compat_urllib_parse
19+
from urllib.parse import urlparse as urlsplit
2020
except ImportError: # python 2
21-
from urlparse import urlparse as compat_urllib_parse
21+
from urlparse import urlparse as urlsplit
2222

2323
strtype = None
2424
if sys.version_info < (3, 0):

0 commit comments

Comments
 (0)