Getting started with secure AWS CloudFront streaming with Python

后端 未结 2 487
北海茫月
北海茫月 2020-11-29 17:08

I have created a S3 bucket, uploaded a video, created a streaming distribution in CloudFront. Tested it with a static HTML player and it works. I have created a keypair thro

相关标签:
2条回答
  • 2020-11-29 17:46

    You're right, it takes a lot of API work to get this set up. I hope they get it in the AWS Console soon!

    UPDATE: I have submitted this code to boto - as of boto v2.1 (released 2011-10-27) this gets much easier. For boto < 2.1, use the instructions here. For boto 2.1 or greater, get the updated instructions on my blog: http://www.secretmike.com/2011/10/aws-cloudfront-secure-streaming.html Once boto v2.1 gets packaged by more distros I'll update the answer here.

    To accomplish what you want you need to perform the following steps which I will detail below:

    1. Create your s3 bucket and upload some objects (you've already done this)
    2. Create a Cloudfront "Origin Access Identity" (basically an AWS account to allow cloudfront to access your s3 bucket)
    3. Modify the ACLs on your objects so that only your Cloudfront Origin Access Identity is allowed to read them (this prevents people from bypassing Cloudfront and going direct to s3)
    4. Create a cloudfront distribution with basic URLs and one which requires signed URLs
    5. Test that you can download objects from basic cloudfront distribution but not from s3 or the signed cloudfront distribution
    6. Create a key pair for signing URLs
    7. Generate some URLs using Python
    8. Test that the signed URLs work

    1 - Create Bucket and upload object

    The easiest way to do this is through the AWS Console but for completeness I'll show how using boto. Boto code is shown here:

    import boto
    
    #credentials stored in environment AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY
    s3 = boto.connect_s3()
    
    #bucket name MUST follow dns guidelines
    new_bucket_name = "stream.example.com"
    bucket = s3.create_bucket(new_bucket_name)
    
    object_name = "video.mp4"
    key = bucket.new_key(object_name)
    key.set_contents_from_filename(object_name)
    

    2 - Create a Cloudfront "Origin Access Identity"

    For now, this step can only be performed using the API. Boto code is here:

    import boto
    
    #credentials stored in environment AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY
    cf = boto.connect_cloudfront()
    
    oai = cf.create_origin_access_identity(comment='New identity for secure videos')
    
    #We need the following two values for later steps:
    print("Origin Access Identity ID: %s" % oai.id)
    print("Origin Access Identity S3CanonicalUserId: %s" % oai.s3_user_id)
    

    3 - Modify the ACLs on your objects

    Now that we've got our special S3 user account (the S3CanonicalUserId we created above) we need to give it access to our s3 objects. We can do this easily using the AWS Console by opening the object's (not the bucket's!) Permissions tab, click the "Add more permissions" button, and pasting the very long S3CanonicalUserId we got above into the "Grantee" field of a new. Make sure you give the new permission "Open/Download" rights.

    You can also do this in code using the following boto script:

    import boto
    
    #credentials stored in environment AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY
    s3 = boto.connect_s3()
    
    bucket_name = "stream.example.com"
    bucket = s3.get_bucket(bucket_name)
    
    object_name = "video.mp4"
    key = bucket.get_key(object_name)
    
    #Now add read permission to our new s3 account
    s3_canonical_user_id = "<your S3CanonicalUserID from above>"
    key.add_user_grant("READ", s3_canonical_user_id)
    

    4 - Create a cloudfront distribution

    Note that custom origins and private distributions are not fully supported in boto until version 2.0 which has not been formally released at time of writing. The code below pulls out some code from the boto 2.0 branch and hacks it together to get it going but it's not pretty. The 2.0 branch handles this much more elegantly - definitely use that if possible!

    import boto
    from boto.cloudfront.distribution import DistributionConfig
    from boto.cloudfront.exception import CloudFrontServerError
    
    import re
    
    def get_domain_from_xml(xml):
        results = re.findall("<DomainName>([^<]+)</DomainName>", xml)
        return results[0]
    
    #custom class to hack this until boto v2.0 is released
    class HackedStreamingDistributionConfig(DistributionConfig):
    
        def __init__(self, connection=None, origin='', enabled=False,
                     caller_reference='', cnames=None, comment='',
                     trusted_signers=None):
            DistributionConfig.__init__(self, connection=connection,
                                        origin=origin, enabled=enabled,
                                        caller_reference=caller_reference,
                                        cnames=cnames, comment=comment,
                                        trusted_signers=trusted_signers)
    
        #override the to_xml() function
        def to_xml(self):
            s = '<?xml version="1.0" encoding="UTF-8"?>\n'
            s += '<StreamingDistributionConfig xmlns="http://cloudfront.amazonaws.com/doc/2010-07-15/">\n'
    
            s += '  <S3Origin>\n'
            s += '    <DNSName>%s</DNSName>\n' % self.origin
            if self.origin_access_identity:
                val = self.origin_access_identity
                s += '    <OriginAccessIdentity>origin-access-identity/cloudfront/%s</OriginAccessIdentity>\n' % val
            s += '  </S3Origin>\n'
    
    
            s += '  <CallerReference>%s</CallerReference>\n' % self.caller_reference
            for cname in self.cnames:
                s += '  <CNAME>%s</CNAME>\n' % cname
            if self.comment:
                s += '  <Comment>%s</Comment>\n' % self.comment
            s += '  <Enabled>'
            if self.enabled:
                s += 'true'
            else:
                s += 'false'
            s += '</Enabled>\n'
            if self.trusted_signers:
                s += '<TrustedSigners>\n'
                for signer in self.trusted_signers:
                    if signer == 'Self':
                        s += '  <Self/>\n'
                    else:
                        s += '  <AwsAccountNumber>%s</AwsAccountNumber>\n' % signer
                s += '</TrustedSigners>\n'
            if self.logging:
                s += '<Logging>\n'
                s += '  <Bucket>%s</Bucket>\n' % self.logging.bucket
                s += '  <Prefix>%s</Prefix>\n' % self.logging.prefix
                s += '</Logging>\n'
            s += '</StreamingDistributionConfig>\n'
    
            return s
    
        def create(self):
            response = self.connection.make_request('POST',
                '/%s/%s' % ("2010-11-01", "streaming-distribution"),
                {'Content-Type' : 'text/xml'},
                data=self.to_xml())
    
            body = response.read()
            if response.status == 201:
                return body
            else:
                raise CloudFrontServerError(response.status, response.reason, body)
    
    
    cf = boto.connect_cloudfront()
    
    s3_dns_name = "stream.example.com.s3.amazonaws.com"
    comment = "example streaming distribution"
    oai = "<OAI ID from step 2 above like E23KRHS6GDUF5L>"
    
    #Create a distribution that does NOT need signed URLS
    hsd = HackedStreamingDistributionConfig(connection=cf, origin=s3_dns_name, comment=comment, enabled=True)
    hsd.origin_access_identity = oai
    basic_dist = hsd.create()
    print("Distribution with basic URLs: %s" % get_domain_from_xml(basic_dist))
    
    #Create a distribution that DOES need signed URLS
    hsd = HackedStreamingDistributionConfig(connection=cf, origin=s3_dns_name, comment=comment, enabled=True)
    hsd.origin_access_identity = oai
    #Add some required signers (Self means your own account)
    hsd.trusted_signers = ['Self']
    signed_dist = hsd.create()
    print("Distribution with signed URLs: %s" % get_domain_from_xml(signed_dist))
    

    5 - Test that you can download objects from cloudfront but not from s3

    You should now be able to verify:

    • stream.example.com.s3.amazonaws.com/video.mp4 - should give AccessDenied
    • signed_distribution.cloudfront.net/video.mp4 - should give MissingKey (because the URL is not signed)
    • basic_distribution.cloudfront.net/video.mp4 - should work fine

    The tests will have to be adjusted to work with your stream player, but the basic idea is that only the basic cloudfront url should work.

    6 - Create a keypair for CloudFront

    I think the only way to do this is through Amazon's web site. Go into your AWS "Account" page and click on the "Security Credentials" link. Click on the "Key Pairs" tab then click "Create a New Key Pair". This will generate a new key pair for you and automatically download a private key file (pk-xxxxxxxxx.pem). Keep the key file safe and private. Also note down the "Key Pair ID" from amazon as we will need it in the next step.

    7 - Generate some URLs in Python

    As of boto version 2.0 there does not seem to be any support for generating signed CloudFront URLs. Python does not include RSA encryption routines in the standard library so we will have to use an additional library. I've used M2Crypto in this example.

    For a non-streaming distribution, you must use the full cloudfront URL as the resource, however for streaming we only use the object name of the video file. See the code below for a full example of generating a URL which only lasts for 5 minutes.

    This code is based loosely on the PHP example code provided by Amazon in the CloudFront documentation.

    from M2Crypto import EVP
    import base64
    import time
    
    def aws_url_base64_encode(msg):
        msg_base64 = base64.b64encode(msg)
        msg_base64 = msg_base64.replace('+', '-')
        msg_base64 = msg_base64.replace('=', '_')
        msg_base64 = msg_base64.replace('/', '~')
        return msg_base64
    
    def sign_string(message, priv_key_string):
        key = EVP.load_key_string(priv_key_string)
        key.reset_context(md='sha1')
        key.sign_init()
        key.sign_update(str(message))
        signature = key.sign_final()
        return signature
    
    def create_url(url, encoded_signature, key_pair_id, expires):
        signed_url = "%(url)s?Expires=%(expires)s&Signature=%(encoded_signature)s&Key-Pair-Id=%(key_pair_id)s" % {
                'url':url,
                'expires':expires,
                'encoded_signature':encoded_signature,
                'key_pair_id':key_pair_id,
                }
        return signed_url
    
    def get_canned_policy_url(url, priv_key_string, key_pair_id, expires):
        #we manually construct this policy string to ensure formatting matches signature
        canned_policy = '{"Statement":[{"Resource":"%(url)s","Condition":{"DateLessThan":{"AWS:EpochTime":%(expires)s}}}]}' % {'url':url, 'expires':expires}
    
        #now base64 encode it (must be URL safe)
        encoded_policy = aws_url_base64_encode(canned_policy)
        #sign the non-encoded policy
        signature = sign_string(canned_policy, priv_key_string)
        #now base64 encode the signature (URL safe as well)
        encoded_signature = aws_url_base64_encode(signature)
    
        #combine these into a full url
        signed_url = create_url(url, encoded_signature, key_pair_id, expires);
    
        return signed_url
    
    def encode_query_param(resource):
        enc = resource
        enc = enc.replace('?', '%3F')
        enc = enc.replace('=', '%3D')
        enc = enc.replace('&', '%26')
        return enc
    
    
    #Set parameters for URL
    key_pair_id = "APKAIAZCZRKVIO4BQ" #from the AWS accounts page
    priv_key_file = "cloudfront-pk.pem" #your private keypair file
    resource = 'video.mp4' #your resource (just object name for streaming videos)
    expires = int(time.time()) + 300 #5 min
    
    #Create the signed URL
    priv_key_string = open(priv_key_file).read()
    signed_url = get_canned_policy_url(resource, priv_key_string, key_pair_id, expires)
    
    #Flash player doesn't like query params so encode them
    enc_url = encode_query_param(signed_url)
    print(enc_url)
    

    8 - Try out the URLs

    Hopefully you should now have a working URL which looks something like this:

    video.mp4%3FExpires%3D1309979985%26Signature%3DMUNF7pw1689FhMeSN6JzQmWNVxcaIE9mk1x~KOudJky7anTuX0oAgL~1GW-ON6Zh5NFLBoocX3fUhmC9FusAHtJUzWyJVZLzYT9iLyoyfWMsm2ylCDBqpy5IynFbi8CUajd~CjYdxZBWpxTsPO3yIFNJI~R2AFpWx8qp3fs38Yw_%26Key-Pair-Id%3DAPKAIAZRKVIO4BQ
    

    Put this into your js and you should have something which looks like this (from the PHP example in Amazon's CloudFront documentation):

    var so_canned = new SWFObject('http://location.domname.com/~jvngkhow/player.swf','mpl','640','360','9');
        so_canned.addParam('allowfullscreen','true');
        so_canned.addParam('allowscriptaccess','always');
        so_canned.addParam('wmode','opaque');
        so_canned.addVariable('file','video.mp4%3FExpires%3D1309979985%26Signature%3DMUNF7pw1689FhMeSN6JzQmWNVxcaIE9mk1x~KOudJky7anTuX0oAgL~1GW-ON6Zh5NFLBoocX3fUhmC9FusAHtJUzWyJVZLzYT9iLyoyfWMsm2ylCDBqpy5IynFbi8CUajd~CjYdxZBWpxTsPO3yIFNJI~R2AFpWx8qp3fs38Yw_%26Key-Pair-Id%3DAPKAIAZRKVIO4BQ');
        so_canned.addVariable('streamer','rtmp://s3nzpoyjpct.cloudfront.net/cfx/st');
        so_canned.write('canned');
    

    Summary

    As you can see, not very easy! boto v2 will help a lot setting up the distribution. I will find out if it's possible to get some URL generation code in there as well to improve this great library!

    0 讨论(0)
  • 2020-11-29 18:01

    In Python, what's the easiest way of generating an expiring URL for a file. I have boto installed but I don't see how to get a file from a streaming distribution.

    You can generate a expiring signed-URL for the resource. Boto3 documentation has a nice example solution for that:

    import datetime
    
    from cryptography.hazmat.backends import default_backend
    from cryptography.hazmat.primitives import hashes
    from cryptography.hazmat.primitives import serialization
    from cryptography.hazmat.primitives.asymmetric import padding
    from botocore.signers import CloudFrontSigner
    
    
    def rsa_signer(message):
        with open('path/to/key.pem', 'rb') as key_file:
            private_key = serialization.load_pem_private_key(
                key_file.read(), 
                password=None,
                backend=default_backend()
            )
        signer = private_key.signer(padding.PKCS1v15(), hashes.SHA1())
        signer.update(message)
        return signer.finalize()
    
    key_id = 'AKIAIOSFODNN7EXAMPLE'
    url = 'http://d2949o5mkkp72v.cloudfront.net/hello.txt'
    expire_date = datetime.datetime(2017, 1, 1)
    
    cloudfront_signer = CloudFrontSigner(key_id, rsa_signer)
    
    # Create a signed url that will be valid until the specfic expiry date
    # provided using a canned policy.
    signed_url = cloudfront_signer.generate_presigned_url(
        url, date_less_than=expire_date)
    print(signed_url)
    
    0 讨论(0)
提交回复
热议问题