Skip to content

Commit

Permalink
Merge pull request #1668 from rayluo/cloudfront-sign
Browse files Browse the repository at this point in the history
Cloudfront sign
  • Loading branch information
rayluo committed Dec 8, 2015
2 parents a5f6b3e + c56d859 commit 6d61a91
Show file tree
Hide file tree
Showing 4 changed files with 173 additions and 3 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ Next Release (TBD)
(`issue 1664 <https://github.com/aws/aws-cli/pull/1664>`__)
* feature:``aws cloudfront create-invalidation``: Add a new --paths option.
(`issue 1662 <https://github.com/aws/aws-cli/pull/1662>`__)
* feature:``aws cloudfront sign``: Add a new command to create a signed url.
(`issue 1668 <https://github.com/aws/aws-cli/pull/1668>`__)


1.9.11
Expand Down
86 changes: 85 additions & 1 deletion awscli/customizations/cloudfront.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,23 @@
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
import sys
import time
import random

import rsa
from botocore.utils import parse_to_aware_datetime
from botocore.signers import CloudFrontSigner

from awscli.arguments import CustomArgument
from awscli.customizations.utils import validate_mutually_exclusive_handler
from awscli.customizations.commands import BasicCommand


def register(event_handler):
"""Provides a simpler --paths for ``aws cloudfront create-invalidation``"""
event_handler.register('building-command-table.cloudfront', _add_sign)

# Provides a simpler --paths for ``aws cloudfront create-invalidation``
event_handler.register(
'building-argument-table.cloudfront.create-invalidation', _add_paths)
event_handler.register(
Expand Down Expand Up @@ -49,3 +56,80 @@ def add_to_params(self, parameters, value):
"CallerReference": caller_reference,
"Paths": {"Quantity": len(value), "Items": value},
}


def _add_sign(command_table, session, **kwargs):
command_table['sign'] = SignCommand(session)


class SignCommand(BasicCommand):
NAME = 'sign'
DESCRIPTION = 'Sign a given url.'
DATE_FORMAT = """Supported formats include:
YYYY-MM-DD (which means 0AM UTC of that day),
YYYY-MM-DDThh:mm:ss (with default timezone as UTC),
YYYY-MM-DDThh:mm:ss+hh:mm or YYYY-MM-DDThh:mm:ss-hh:mm (with offset),
or EpochTime (which always means UTC).
Do NOT use YYYYMMDD, because it will be treated as EpochTime."""
ARG_TABLE = [
{
'name': 'url',
'no_paramfile': True, # To disable the default paramfile behavior
'required': True,
'help_text': 'The URL to be signed',
},
{
'name': 'key-pair-id',
'required': True,
'help_text': (
"The active CloudFront key pair Id for the key pair "
"that you're using to generate the signature."),
},
{
'name': 'private-key',
'required': True,
'help_text': 'file://path/to/your/private-key.pem',
},
{
'name': 'date-less-than', 'required': True,
'help_text':
'The expiration date and time for the URL. ' + DATE_FORMAT,
},
{
'name': 'date-greater-than',
'help_text':
'An optional start date and time for the URL. ' + DATE_FORMAT,
},
{
'name': 'ip-address',
'help_text': (
'An optional IP address or IP address range to allow client '
'making the GET request from. Format: x.x.x.x/x or x.x.x.x'),
},
]

def _run_main(self, args, parsed_globals):
signer = CloudFrontSigner(
args.key_pair_id, RSASigner(args.private_key).sign)
date_less_than = parse_to_aware_datetime(args.date_less_than)
date_greater_than = args.date_greater_than
if date_greater_than is not None:
date_greater_than = parse_to_aware_datetime(date_greater_than)
if date_greater_than is not None or args.ip_address is not None:
policy = signer.build_policy(
args.url, date_less_than, date_greater_than=date_greater_than,
ip_address=args.ip_address)
sys.stdout.write(signer.generate_presigned_url(
args.url, policy=policy))
else:
sys.stdout.write(signer.generate_presigned_url(
args.url, date_less_than=date_less_than))
return 0


class RSASigner(object):
def __init__(self, private_key):
self.priv_key = rsa.PrivateKey.load_pkcs1(private_key.encode('utf8'))

def sign(self, message):
return rsa.sign(message, self.priv_key, 'SHA-1')
4 changes: 2 additions & 2 deletions awscli/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
from awscli.customizations.rds import register_rds_modify_split
from awscli.customizations.putmetricdata import register_put_metric_data
from awscli.customizations.sessendemail import register_ses_send_email
from awscli.customizations.cloudfront import register as cloudfront_register
from awscli.customizations.cloudfront import register as register_cloudfront
from awscli.customizations.iamvirtmfa import IAMVMFAWrapper
from awscli.customizations.argrename import register_arg_renames
from awscli.customizations.configure import register_configure_cmd
Expand Down Expand Up @@ -109,7 +109,6 @@ def awscli_initialize(event_handlers):
register_rds_modify_split(event_handlers)
register_put_metric_data(event_handlers)
register_ses_send_email(event_handlers)
cloudfront_register(event_handlers)
IAMVMFAWrapper(event_handlers)
register_arg_renames(event_handlers)
register_configure_cmd(event_handlers)
Expand Down Expand Up @@ -142,3 +141,4 @@ def awscli_initialize(event_handlers):
event_handlers.register(
'building-argument-table.iot.create-certificate-from-csr',
register_create_keys_from_csr_arguments)
register_cloudfront(event_handlers)
84 changes: 84 additions & 0 deletions tests/functional/cloudfront/test_sign.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
import mock
from botocore.compat import urlparse, parse_qs

from awscli.testutils import FileCreator
from awscli.testutils import BaseAWSPreviewCommandParamsTest as \
BaseAWSCommandParamsTest


class TestSign(BaseAWSCommandParamsTest):
# A private key only for testing purpose.
private_key = '''
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAu6o2+Jc8UINw2P/w2l7A1xXu3emQEZQ9diA3bmog8r9Dg+65
fZgAqmuNWPqBivv7j3DGnLUdt8uCIr7PYUbK7wDa6n7U3ryOWtO2ZTc3StiJVcqT
sokZ0qxGFtDRafjBuydXtcxh52vVTcHqH33nubyyZIzuhTwfmrIOnUXnLwbMrBBP
bg/8mlgQooyo1XbrN1eO4XMs+UgQ9Mqc7KRJRinUJ+KYuCnM8f/nN4RjYdjTcghk
xCPEHCeSt2luywWyYmfguWCBS2Mu1q0250wKyNazlgiiTJtAuuSeweb4NKPOJL9X
hR6Ce6UuU4WYlli8gvQh3FAV3N3C1Rxo20k28QIDAQABAoIBAQCUEkP5dWrzpCJg
NeHWizjg/L9SfT1dgXfVQqo6BqckoeElsjDNdifgT6hhcpbQEO52SWeMsiNWp85w
l9mNSYxJdIVGzPgtHt27sJyT1DNebOg/tu0+y4qCfcd3rR/u24YQo4RDP5ZoQN82
0TBn1LIIDWk8iS6SFdRh/OgnE8bLhNbK9IfZQFEEJrFkArrn/le/ro2mfJkC/imo
QvqKmM0dGBXt5SCDSbUQAzKtEcR/4gf/qSjFe2YAwAvSA05WXMH6szdtx6/H/VbK
Uck/WwTHvGObQDFEWmICxPK9AWT0qaFNjlUsi3bjQRdIlYYrXe+6nVMB/Jp1awq7
tGBqIcWBAoGBAPtXCNuoQhKXqkjJgteQpB+wFav12XRZgpOciYdeviJrgWydpOOu
O9wkiRUctUijRJbUuWCJF7SgYGoT2xTTp/COiOReqs7qXLMuuXCZcPKkMRJj5wmo
Uc2AwUV/o3+PNz1NFK+2RgciXplac7qugIyuxIvBKuVFTBlCg0+if/0pAoGBAL8k
845wKqOeiawwle/o9lKLGPy1T11GrE6l1A5jRuE1WTVM77jRrb0Hmo0mdfHaf5A0
EjXGIX/fjcmQzBrEd78eCUsvI2Bgn6xXwhd4TTyWHGZfoQjFqAGkixuLN1oo2h1g
bRreFKfAubFP8MC93z23vnH6tdY2VIA4h5ehUFyJAoGAJqxJrKLDJ+E2TmTTQR/8
YPPTIdZ+UyzCrrvTXYTydJFeJLxM9suEYmcswJbePgMBNsQckgIGJ8DVlPzhJN88
ZANKhPkcByKAiQGTfwPdITiqZE4C6rV/gMNi+bKeEa6TrVcC69Z8B/T94VLNo9fd
58esbmSWmRiEkQ5u7f3u+6ECgYA8+6ANCLJB43nPCu07TpsP+LrvHTWF799XdEa0
lG3vuiKNA8/TqmoAziU79VJZ6Dkcm9BXga/8aSmGboD/5UDDI+UZLJ/fxtQKmzEc
ZdBWjRnge5AYCV+xrnqHPiJZzIDSMIp+sO3sG2vjKzsHc0x/F1lWagOLpWfORLrV
4KyP6QKBgAafeSrfK3LM7idiCBuxckLCgFoHa7uXLUNJRS5iIU+bbZLPj2ozu/tk
U0jp7sNk1CyMWI36lR3sujkSyH3lPIXVgrXMuGY3PJRGntN8WlWEsw4VUMGRj3h4
5rB+y/UOS+nlEwQ6eOS09GByJDEXOXpcwjFcTr/f7V8mi0jH+gY/
-----END RSA PRIVATE KEY-----
'''
prefix = 'cloudfront sign --key-pair-id my_id --url http://example.com/hi '

def setUp(self):
files = FileCreator()
self.private_key_file = files.create_file('foo.pem', self.private_key)
self.addCleanup(files.remove_all)
super(TestSign, self).setUp()

def assertDesiredUrl(self, url, base, params):
self.assertEqual(len(url.splitlines()), 1, "Expects only 1 line")
self.assertTrue(url.startswith(base), "URL mismatch")
url = url.strip() # Otherwise the last param contains a trailing CRLF
self.assertEqual(parse_qs(urlparse(url).query), params)

def test_canned_policy(self):
cmdline = (
self.prefix + '--private-key file://' + self.private_key_file +
' --date-less-than 2016-1-1')
expected_params = {
'Key-Pair-Id': ['my_id'],
'Expires': ['1451606400'], 'Signature': [mock.ANY]}
self.assertDesiredUrl(
self.run_cmd(cmdline)[0], 'http://example.com/hi', expected_params)

def test_custom_policy(self):
cmdline = (
self.prefix + '--private-key file://' + self.private_key_file +
' --date-less-than 2016-1-1 --ip-address 12.34.56.78')
expected_params = {
'Key-Pair-Id': ['my_id'],
'Policy': [mock.ANY], 'Signature': [mock.ANY]}
self.assertDesiredUrl(
self.run_cmd(cmdline)[0], 'http://example.com/hi', expected_params)

0 comments on commit 6d61a91

Please sign in to comment.