Browse code

fixup trivial merge conflict

Matt Domsch authored on 2012/12/07 13:26:40
Showing 3 changed files
... ...
@@ -14,6 +14,7 @@ import rfc822
14 14
 import hmac
15 15
 import base64
16 16
 import errno
17
+import urllib
17 18
 
18 19
 from logging import debug, info, warning, error
19 20
 
... ...
@@ -343,12 +344,73 @@ def replace_nonprintables(string):
343 343
 __all__.append("replace_nonprintables")
344 344
 
345 345
 def sign_string(string_to_sign):
346
-    #debug("string_to_sign: %s" % string_to_sign)
346
+    """Sign a string with the secret key, returning base64 encoded results.
347
+    By default the configured secret key is used, but may be overridden as
348
+    an argument.
349
+
350
+    Useful for REST authentication. See http://s3.amazonaws.com/doc/s3-developer-guide/RESTAuthentication.html
351
+    """
347 352
     signature = base64.encodestring(hmac.new(Config.Config().secret_key, string_to_sign, sha1).digest()).strip()
348
-    #debug("signature: %s" % signature)
349 353
     return signature
350 354
 __all__.append("sign_string")
351 355
 
356
+def sign_url(url_to_sign, expiry):
357
+    """Sign a URL in s3://bucket/object form with the given expiry
358
+    time. The object will be accessible via the signed URL until the
359
+    AWS key and secret are revoked or the expiry time is reached, even
360
+    if the object is otherwise private.
361
+
362
+    See: http://s3.amazonaws.com/doc/s3-developer-guide/RESTAuthentication.html
363
+    """
364
+    return sign_url_base(
365
+        bucket = url_to_sign.bucket(),
366
+        object = url_to_sign.object(),
367
+        expiry = expiry
368
+    )
369
+__all__.append("sign_url")
370
+
371
+def sign_url_base(**parms):
372
+    """Shared implementation of sign_url methods. Takes a hash of 'bucket', 'object' and 'expiry' as args."""
373
+    parms['expiry']=time_to_epoch(parms['expiry'])
374
+    parms['access_key']=Config.Config().access_key
375
+    debug("Expiry interpreted as epoch time %s", parms['expiry'])
376
+    signtext = 'GET\n\n\n%(expiry)d\n/%(bucket)s/%(object)s' % parms
377
+    debug("Signing plaintext: %r", signtext)
378
+    parms['sig'] = urllib.quote_plus(sign_string(signtext))
379
+    debug("Urlencoded signature: %s", parms['sig'])
380
+    return "http://%(bucket)s.s3.amazonaws.com/%(object)s?AWSAccessKeyId=%(access_key)s&Expires=%(expiry)d&Signature=%(sig)s" % parms
381
+
382
+def time_to_epoch(t):
383
+    """Convert time specified in a variety of forms into UNIX epoch time.
384
+    Accepts datetime.datetime, int, anything that has a strftime() method, and standard time 9-tuples
385
+    """
386
+    if isinstance(t, int):
387
+        # Already an int
388
+        return t
389
+    elif isinstance(t, tuple) or isinstance(t, time.struct_time):
390
+        # Assume it's a time 9-tuple
391
+        return int(time.mktime(t))
392
+    elif hasattr(t, 'timetuple'):
393
+        # Looks like a datetime object or compatible
394
+        return int(time.mktime(ex.timetuple()))
395
+    elif hasattr(t, 'strftime'):
396
+        # Looks like the object supports standard srftime()
397
+        return int(t.strftime('%s'))
398
+    elif isinstance(t, str) or isinstance(t, unicode):
399
+        # See if it's a string representation of an epoch
400
+        try:
401
+            return int(t)
402
+        except ValueError:
403
+            # Try to parse it as a timestamp string
404
+            try:
405
+                return time.strptime(t)
406
+            except ValueError as ex:
407
+                # Will fall through
408
+                debug("Failed to parse date with strptime: %s", ex)
409
+                pass
410
+    raise Exceptions.ParameterError('Unable to convert %r to an epoch time. Pass an epoch time. Try `date -d \'now + 1 year\' +%%s` (shell) or time.mktime (Python).' % t)
411
+
412
+
352 413
 def check_bucket_name(bucket, dns_strict = True):
353 414
     if dns_strict:
354 415
         invalid = re.search("([^a-z0-9\.-])", bucket)
... ...
@@ -25,6 +25,7 @@ import htmlentitydefs
25 25
 import socket
26 26
 import shutil
27 27
 import tempfile
28
+import S3.Exceptions
28 29
 
29 30
 from copy import copy
30 31
 from optparse import OptionParser, Option, OptionValueError, IndentedHelpFormatter
... ...
@@ -1224,6 +1225,15 @@ def cmd_sign(args):
1224 1224
     signature = Utils.sign_string(string_to_sign)
1225 1225
     output("Signature: %s" % signature)
1226 1226
 
1227
+def cmd_signurl(args):
1228
+	expiry = args.pop()
1229
+	url_to_sign = S3Uri(args.pop())
1230
+	if url_to_sign.type != 's3':
1231
+		raise ParameterError("Must be S3Uri. Got: %s" % url_to_sign)
1232
+	debug("url to sign: %r" % url_to_sign)
1233
+	signed_url = Utils.sign_url(url_to_sign, expiry)
1234
+	output(signed_url)
1235
+
1227 1236
 def cmd_fixbucket(args):
1228 1237
     def _unescape(text):
1229 1238
         ##
... ...
@@ -1541,6 +1551,7 @@ def get_commands_list():
1541 1541
     {"cmd":"setpolicy", "label":"Set an access policy for a bucket", "param":"s3://BUCKET POLICY_STRING", "func":cmd_setpolicy, "argc":2},
1542 1542
     {"cmd":"accesslog", "label":"Enable/disable bucket access logging", "param":"s3://BUCKET", "func":cmd_accesslog, "argc":1},
1543 1543
     {"cmd":"sign", "label":"Sign arbitrary string using the secret key", "param":"STRING-TO-SIGN", "func":cmd_sign, "argc":1},
1544
+    {"cmd":"signurl", "label":"Sign an S3 URL to provide limited public access with expiry", "param":"s3://BUCKET/OBJECT expiry_epoch", "func":cmd_signurl, "argc":2},
1544 1545
     {"cmd":"fixbucket", "label":"Fix invalid file names in a bucket", "param":"s3://BUCKET[/PREFIX]", "func":cmd_fixbucket, "argc":1},
1545 1546
 
1546 1547
     ## Website commands
... ...
@@ -63,6 +63,23 @@ Enable/disable bucket access logging
63 63
 s3cmd \fBsign\fR \fISTRING-TO-SIGN\fR
64 64
 Sign arbitrary string using the secret key
65 65
 .TP
66
+s3cmd \fBsignurl\fR \fIs3://BUCKET[/OBJECT]\fR \fIexpiry-in-epoch-seconds\fR
67
+Sign an S3 URL with the secret key, producing a URL that allows access to
68
+the named object using the credentials used to sign the URL until the date of
69
+expiry specified in epoch-seconds has passed. This is most useful for publishing
70
+time- or distribution-limited URLs to otherwise-private S3 objects.
71
+.br
72
+This is a purely offline operation. Your API key and secret are not sent on
73
+the wire, though your public API key is included in the generated URL. Because
74
+it's offline, no validation is done to ensure that the bucket and object actually
75
+exist, or that this API key has permission to access them.
76
+.br
77
+The URL generated is http:// but you can simply change to https:// if you want.
78
+.br
79
+See
80
+.B http://s3.amazonaws.com/doc/s3-developer-guide/RESTAuthentication.html
81
+for more information on signed URLs, and the examples section below.
82
+.TP
66 83
 s3cmd \fBfixbucket\fR \fIs3://BUCKET[/PREFIX]\fR
67 84
 Fix invalid file names in a bucket
68 85
 
... ...
@@ -427,6 +444,12 @@ about matching file names against exclude and include rules.
427 427
 For example to exclude all files with ".jpg" extension except those beginning with a number use:
428 428
 .PP
429 429
 	\-\-exclude '*.jpg' \-\-rinclude '[0-9].*\.jpg'
430
+.PP
431
+To produce a signed HTTP URL that allows access to the normally private s3 object
432
+s3://mybucket/someobj (which you must have permission to access) to anybody
433
+with the URL for one week from today, use:
434
+.PP
435
+        s3cmd signurl s3://mybucket/someobj `date -d 'today + 1 week' +%s` 
430 436
 .SH SEE ALSO
431 437
 For the most up to date list of options run 
432 438
 .B s3cmd \-\-help