... | ... |
@@ -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 |