15
15
16
16
import urllib2
17
17
import urlparse
18
+ import datetime
18
19
import time
19
20
import hashlib
20
21
import hmac
21
22
import json
23
+ import re
22
24
23
25
import yum
24
26
import yum .config
31
33
__email__ = "julius@seporaitis.net"
32
34
__copyright__ = "Copyright 2012, Julius Seporaitis"
33
35
__license__ = "Apache 2.0"
34
- __version__ = "1.0.3 "
36
+ __version__ = "1.1.0 "
35
37
36
38
37
39
__all__ = ['requires_api_version' , 'plugin_type' , 'CONDUIT' ,
40
42
requires_api_version = '2.5'
41
43
plugin_type = yum .plugins .TYPE_CORE
42
44
CONDUIT = None
45
+ DEFAULT_DELAY = 3
46
+ DEFAULT_BACKOFF = 2
47
+ BUFFER_SIZE = 1024 * 1024
48
+ OPTIONAL_ATTRIBUTES = ['priority' , 'base_persistdir' , 'metadata_expire' ,
49
+ 'skip_if_unavailable' , 'keepcache' , 'priority' ]
50
+ UNSUPPORTED_ATTRIBUTES = ['mirrorlist' , 'proxy' ]
43
51
44
52
45
53
def config_hook (conduit ):
46
54
yum .config .RepoConf .s3_enabled = yum .config .BoolOption (False )
55
+ yum .config .RepoConf .region = yum .config .Option ()
47
56
yum .config .RepoConf .key_id = yum .config .Option ()
48
57
yum .config .RepoConf .secret_key = yum .config .Option ()
49
58
yum .config .RepoConf .delegated_role = yum .config .Option ()
59
+ yum .config .RepoConf .baseurl = yum .config .UrlListOption (
60
+ schemes = ('http' , 'https' , 's3' , 'ftp' , 'file' )
61
+ )
62
+ yum .config .RepoConf .backoff = yum .config .Option ()
63
+ yum .config .RepoConf .delay = yum .config .Option ()
64
+
65
+
66
+ def parse_url (url ):
67
+ # http://docs.aws.amazon.com/AmazonS3/latest/dev/UsingBucket.html
68
+ url = url [0 ] if isinstance (url , list ) else url
69
+
70
+ # http[s]://<bucket>.s3.amazonaws.com
71
+ m = re .match (r'(http|https|s3)://([a-z0-9][a-z0-9-.]{1,61}[a-z0-9])[.]s3[.]amazonaws[.]com(.*)$' , url )
72
+ if m :
73
+ return (m .group (2 ), None , m .group (3 ))
74
+
75
+ # http[s]://<bucket>.s3-<aws-region>.amazonaws.com
76
+ m = re .match (r'(http|https|s3)://([a-z0-9][a-z0-9-.]{1,61}[a-z0-9])[.]s3-([a-z0-9-]+)[.]amazonaws[.]com(.*)$' , url )
77
+ if m :
78
+ return (m .group (2 ), m .group (3 ), m .group (4 ))
79
+
80
+ # http[s]://s3.amazonaws.com/<bucket>
81
+ m = re .match (r'(http|https|s3)://s3[.]amazonaws[.]com/([a-z0-9][a-z0-9-.]{1,61}[a-z0-9])(.*)$' , url )
82
+ if m :
83
+ return (m .group (2 ), 'us-east-1' , m .group (3 ))
84
+
85
+ # http[s]://s3-<region>.amazonaws.com/<bucket>
86
+ m = re .match (r'(http|https|s3)://s3-([a-z0-9-]+)[.]amazonaws[.]com/([a-z0-9][a-z0-9-.]{1,61}[a-z0-9])(.*)$' , url )
87
+ if m :
88
+ return (m .group (3 ), m .group (2 ), m .group (4 ))
89
+
90
+ return (None , None , None )
91
+
92
+
93
+ def replace_repo (repos , repo ):
94
+ repos .delete (repo .id )
95
+ repos .add (S3Repository (repo .id , repo ))
50
96
51
97
52
98
def prereposetup_hook (conduit ):
53
99
"""Plugin initialization hook. Setup the S3 repositories."""
54
100
55
101
repos = conduit .getRepos ()
56
-
57
102
for repo in repos .listEnabled ():
103
+ url = repo .baseurl
104
+ if (isinstance (url , list )):
105
+ if len (url ) == 0 :
106
+ continue
107
+ url = url [0 ]
108
+ if re .match (r'^s3://' , url ):
109
+ repo .s3_enabled = 1
58
110
if isinstance (repo , YumRepository ) and repo .s3_enabled :
59
- new_repo = S3Repository (repo .id , repo .baseurl )
60
- new_repo .name = repo .name
61
- # new_repo.baseurl = repo.baseurl
62
- new_repo .mirrorlist = repo .mirrorlist
63
- new_repo .basecachedir = repo .basecachedir
64
- new_repo .gpgcheck = repo .gpgcheck
65
- new_repo .gpgkey = repo .gpgkey
66
- new_repo .key_id = repo .key_id
67
- new_repo .secret_key = repo .secret_key
68
- new_repo .proxy = repo .proxy
69
- new_repo .enablegroups = repo .enablegroups
70
- if hasattr (repo , 'priority' ):
71
- new_repo .priority = repo .priority
72
- if hasattr (repo , 'base_persistdir' ):
73
- new_repo .base_persistdir = repo .base_persistdir
74
- if hasattr (repo , 'metadata_expire' ):
75
- new_repo .metadata_expire = repo .metadata_expire
76
- if hasattr (repo , 'skip_if_unavailable' ):
77
- new_repo .skip_if_unavailable = repo .skip_if_unavailable
78
-
79
- repos .delete (repo .id )
80
- repos .add (new_repo )
111
+ replace_repo (repos , repo )
81
112
82
113
83
114
class S3Repository (YumRepository ):
84
115
"""Repository object for Amazon S3, using IAM Roles."""
85
116
86
- def __init__ (self , repoid , baseurl ):
117
+ def __init__ (self , repoid , repo ):
87
118
super (S3Repository , self ).__init__ (repoid )
119
+
120
+ bucket , region , path = parse_url (repo .baseurl )
121
+
122
+ if bucket is None :
123
+ msg = "s3iam: unable to parse url %s'" % repo .baseurl
124
+ raise yum .plugins .PluginYumExit (msg )
125
+
126
+ if region :
127
+ self .baseurl = "https://s3-%s.amazonaws.com/%s%s" % (region , bucket , path )
128
+ else :
129
+ self .baseurl = "https://%s.s3.amazonaws.com%s" % (bucket , path )
130
+
131
+ self .name = repo .name
132
+ self .region = repo .region if repo .region else region
133
+ self .basecachedir = repo .basecachedir
134
+ self .gpgcheck = repo .gpgcheck
135
+ self .gpgkey = repo .gpgkey
136
+ self .key_id = repo .key_id
137
+ self .secret_key = repo .secret_key
138
+ self .enablegroups = repo .enablegroups
139
+
140
+ self .retries = repo .retries
141
+ self .backoff = repo .backoff
142
+ self .delay = repo .delay
143
+
144
+ for attr in OPTIONAL_ATTRIBUTES :
145
+ if hasattr (repo , attr ):
146
+ setattr (self , attr , getattr (repo , attr ))
147
+
148
+ for attr in UNSUPPORTED_ATTRIBUTES :
149
+ if getattr (repo , attr ):
150
+ msg = "%s: Unsupported attribute: %s." % (__file__ , attr )
151
+ raise yum .plugins .PluginYumExit (msg )
152
+
88
153
self .iamrole = None
89
- self .baseurl = baseurl
90
154
self .grabber = None
91
155
self .enable ()
92
156
@@ -118,11 +182,17 @@ def __init__(self, repo):
118
182
"""
119
183
if isinstance (repo , basestring ):
120
184
self .baseurl = repo
185
+ self .region = None
186
+ self .retries = 0
121
187
else :
188
+ self .region = repo .region
189
+ self .retries = repo .retries
190
+ self .backoff = DEFAULT_BACKOFF if repo .backoff is None else repo .backoff
191
+ self .delay = DEFAULT_DELAY if repo .delay is None else repo .delay
122
192
if len (repo .baseurl ) != 1 :
123
- raise yum . plugins . PluginYumExit ( "s3iam : repository '%s' "
124
- "must have only one "
125
- "'baseurl' value" % repo . id )
193
+ msg = "%s : repository '%s' must" % ( __file__ , repo . id )
194
+ msg += ' have only one baseurl value'
195
+ raise yum . plugins . PluginYumExit ( msg )
126
196
else :
127
197
self .baseurl = repo .baseurl [0 ]
128
198
# Ensure urljoin doesn't ignore base path:
@@ -206,15 +276,13 @@ def get_instance_region(self):
206
276
response .close ()
207
277
self .region = data [:- 1 ]
208
278
209
- def _request (self , path ):
279
+ def _request (self , path , timeval = None ):
210
280
url = urlparse .urljoin (self .baseurl , urllib2 .quote (path ))
211
281
request = urllib2 .Request (url )
212
- if self .token :
213
- request .add_header ('x-amz-security-token' , self .token )
214
- signature = self .sign (request )
215
- request .add_header ('Authorization' , "AWS {0}:{1}" .format (
216
- self .access_key ,
217
- signature ))
282
+ if self .region :
283
+ self .signV4 (request , timeval )
284
+ else :
285
+ self .signV2 (request , timeval )
218
286
return request
219
287
220
288
def urlgrab (self , url , filename = None , ** kwargs ):
@@ -226,26 +294,38 @@ def urlgrab(self, url, filename=None, **kwargs):
226
294
filename = filename [1 :]
227
295
228
296
response = None
229
- try :
230
- out = open (filename , 'w+' )
231
- response = urllib2 .urlopen (request )
232
- buff = response .read (8192 )
233
- while buff :
234
- out .write (buff )
235
- buff = response .read (8192 )
236
- except urllib2 .HTTPError , e :
237
- # Wrap exception as URLGrabError so that YumRepository catches it
238
- from urlgrabber .grabber import URLGrabError
239
- new_e = URLGrabError (14 , '%s on %s' % (e , url ))
240
- new_e .code = e .code
241
- new_e .exception = e
242
- new_e .url = url
243
- raise new_e
244
- finally :
245
- if response :
246
- response .close ()
247
- out .close ()
248
-
297
+ retries = self .retries
298
+ delay = self .delay
299
+ out = open (filename , 'w+' )
300
+ while retries > 0 :
301
+ try :
302
+ response = urllib2 .urlopen (request )
303
+ buff = response .read (BUFFER_SIZE )
304
+ while buff :
305
+ out .write (buff )
306
+ buff = response .read (BUFFER_SIZE )
307
+ except urllib2 .HTTPError , e :
308
+ if retries > 0 :
309
+ time .sleep (delay )
310
+ delay *= self .backoff
311
+ else :
312
+ # Wrap exception as URLGrabError so that YumRepository catches it
313
+ from urlgrabber .grabber import URLGrabError
314
+ msg = '%s on %s tried' % (e , url )
315
+ if self .retries > 0 :
316
+ msg += ' tried %d time(s)' % (self .retries )
317
+ new_e = URLGrabError (14 , msg )
318
+ new_e .code = e .code
319
+ new_e .exception = e
320
+ new_e .url = url
321
+ raise new_e
322
+ finally :
323
+ retries -= 1
324
+ if response :
325
+ response .close ()
326
+ break
327
+
328
+ out .close ()
249
329
return filename
250
330
251
331
def urlopen (self , url , ** kwargs ):
@@ -256,30 +336,19 @@ def urlread(self, url, limit=None, **kwargs):
256
336
"""urlread(url) return the contents of the file as a string."""
257
337
return urllib2 .urlopen (self ._request (url )).read ()
258
338
259
- def sign (self , request , timeval = None ):
339
+ def signV2 (self , request , timeval = None ):
260
340
"""Attach a valid S3 signature to request.
261
341
request - instance of Request
262
342
"""
263
- date = time .strftime ("%a, %d %b %Y %H:%M:%S GMT" , timeval or time .gmtime ())
343
+ t = timeval or time .gmtime ()
344
+ date = time .strftime ("%a, %d %b %Y %H:%M:%S +0000" , t )
264
345
request .add_header ('Date' , date )
265
- host = request .get_host ()
266
346
267
- # TODO: bucket name finding is ugly, I should find a way to support
268
- # both naming conventions: http://bucket.s3.amazonaws.com/ and
269
- # http://s3.amazonaws.com/bucket/
270
- try :
271
- pos = host .find (".s3" )
272
- assert pos != - 1
273
- bucket = host [:pos ]
274
- except AssertionError :
275
- raise yum .plugins .PluginYumExit (
276
- "s3iam: baseurl hostname should be in format: "
277
- "'<bucket>.s3<aws-region>.amazonaws.com'; "
278
- "found '%s'" % host )
279
-
280
- resource = "/%s%s" % (bucket , request .get_selector (), )
347
+ (bucket , ignore , path ) = parse_url (request .get_full_url ())
348
+ resource = '/' + bucket + path
281
349
if self .token :
282
350
amz_headers = 'x-amz-security-token:%s\n ' % self .token
351
+ request .add_header ('x-amz-security-token' , self .token )
283
352
else :
284
353
amz_headers = ''
285
354
sigstring = ("%(method)s\n \n \n %(date)s\n "
@@ -292,5 +361,57 @@ def sign(self, request, timeval=None):
292
361
str (self .secret_key ),
293
362
str (sigstring ),
294
363
hashlib .sha1 ).digest ()
295
- signature = digest .encode ('base64' )
296
- return signature .strip ()
364
+ signature = digest .encode ('base64' ).rstrip ()
365
+
366
+ authorization = "AWS {0}:{1}" .format (self .access_key , signature )
367
+ request .add_header ('Authorization' , authorization )
368
+
369
+ def derive (self , key , msg ):
370
+ return hmac .new (key , msg .encode ('utf-8' ), hashlib .sha256 ).digest ()
371
+
372
+ def deriveKey (self , key , date , region , service ):
373
+ kDate = self .derive (('AWS4' + key ).encode ('utf-8' ), date )
374
+ kRegion = self .derive (kDate , region )
375
+ kService = self .derive (kRegion , service )
376
+ return self .derive (kService , 'aws4_request' )
377
+
378
+ def signV4 (self , request , timeval = None ):
379
+ algorithm = 'AWS4-HMAC-SHA256'
380
+ t = datetime .datetime .utcnow ()
381
+
382
+ amzdate = t .strftime ('%Y%m%dT%H%M%SZ' )
383
+ amz_headers = ('host:%s\n x-amz-date:%s\n ' %
384
+ (request .get_host (), amzdate ))
385
+ signed_headers = 'host;x-amz-date'
386
+ if self .token :
387
+ amz_headers += 'x-amz-security-token:%s\n ' % self .token
388
+ signed_headers += ';x-amz-security-token'
389
+ request .add_header ('x-amz-security-token' , self .token )
390
+
391
+ # Hash request
392
+ content_h = hashlib .sha256 ('' ).hexdigest () # Empty content
393
+ req = ('GET\n %s\n \n %s\n %s\n %s' %
394
+ (request .get_selector (), amz_headers , signed_headers , content_h ))
395
+ req_hash = hashlib .sha256 (req ).hexdigest ()
396
+
397
+ # Assemble content to be signed
398
+ datestamp = t .strftime ('%Y%m%d' )
399
+ scope = datestamp + '/' + self .region + '/s3/aws4_request'
400
+ sign_content = '%s\n %s\n %s\n %s' % (algorithm , amzdate , scope , req_hash )
401
+
402
+ # Get derived key
403
+ signing_key = self .deriveKey (self .secret_key , datestamp ,
404
+ self .region , 's3' )
405
+
406
+ # Compute signature
407
+ signature = hmac .new (signing_key , (sign_content ).encode ('utf-8' ),
408
+ hashlib .sha256 ).hexdigest ()
409
+
410
+ # Assemble 'Authorization' header value
411
+ credential = self .access_key + '/' + scope
412
+ auth = (('%s Credential=%s, SignedHeaders=%s, Signature=%s' ) %
413
+ (algorithm , credential , signed_headers , signature ))
414
+
415
+ request .add_header ('x-amz-content-sha256' , content_h )
416
+ request .add_header ('x-amz-date' , amzdate )
417
+ request .add_header ('Authorization' , auth )
0 commit comments