40 votes

Mise en route du streaming AWS CloudFront sécurisé avec Python

J'ai créé un seau S3, téléchargé une vidéo, créé une distribution de streaming dans CloudFront. Je l'ai testé avec un lecteur HTML statique et cela fonctionne. J'ai créé une paire de clés via les paramètres du compte. J'ai le fichier de la clé privée sur mon bureau en ce moment. C'est là que je me trouve.

Mon objectif est d'arriver à ce que mon site Django/Python crée des URL sécurisées et que les gens ne puissent pas accéder aux vidéos s'ils ne viennent pas de l'une de mes pages. Le problème, c'est que je suis allergique à la façon dont Amazon a organisé les choses et que je m'y perds de plus en plus.

Je me rends compte que ce ne sera pas la meilleure question sur StackOverflow, mais je suis certain que je ne dois pas être le seul imbécile ici qui ne sait pas comment mettre en place une situation CloudFront/S3 sécurisée. J'apprécierais vraiment votre aide et je suis prêt (une fois les deux jours passés) à donner une prime de 500pt à la meilleure réponse.

J'ai plusieurs questions qui, une fois répondues, devraient se résumer à une explication de la façon d'accomplir ce que je cherche :

  • Dans la documentation (il y a un exemple dans le point suivant), il y a beaucoup de XML qui traîne et qui me dit que je dois POST des choses à divers endroits. Existe-t-il une console en ligne pour faire cela ? Ou dois-je littéralement forcer le passage via cURL (et autres) ?

  • Comment créer une identité d'accès d'origine pour CloudFront et la lier à ma distribution ? J'ai lu ce document mais, pour le premier point, je ne sais pas quoi en faire. Quelle est la place de mon trousseau dans tout ça ?

  • Une fois que c'est fait, comment puis-je limiter le seau S3 pour permettre aux gens de télécharger des choses uniquement par le biais de cette identité ? S'il s'agit d'un autre jobby XML plutôt que de cliquer sur l'interface web, veuillez me dire où et comment je suis censé introduire ceci dans mon compte.

  • En Python, quel est le moyen le plus simple de générer une URL expirante pour un fichier. J'ai boto installé mais je ne vois pas comment obtenir un fichier à partir d'une distribution en continu.

  • Existe-t-il des applications ou des scripts qui peuvent prendre en charge la difficulté de la mise en place de ces vêtements ? J'utilise Ubuntu (Linux) mais j'ai XP dans une VM si c'est uniquement Windows. J'ai déjà regardé CloudBerry S3 Explorer Pro, mais cela n'a pas plus de sens que l'interface utilisateur en ligne.

53voto

secretmike Points 4517

Vous avez raison, il faut beaucoup de travail sur l'API pour mettre cela en place. J'espère qu'elle sera bientôt disponible dans la console AWS !

MISE À JOUR : J'ai soumis ce code à boto - à partir de boto v2.1 (publié le 2011-10-27), cela devient beaucoup plus facile. Pour boto < 2.1, utilisez les instructions ici. Pour boto 2.1 ou plus, utilisez les instructions mises à jour sur mon blog : http://www.secretmike.com/2011/10/aws-cloudfront-secure-streaming.html Une fois que boto v2.1 sera empaqueté par plus de distros, je mettrai à jour la réponse ici.

Pour réaliser ce que vous voulez, vous devez suivre les étapes suivantes que je vais détailler ci-dessous :

  1. Créez votre seau s3 et téléchargez des objets (vous l'avez déjà fait).
  2. Créez une "identité d'accès d'origine" Cloudfront (en fait, un compte AWS pour permettre à Cloudfront d'accéder à votre seau s3).
  3. Modifiez les ACL sur vos objets afin que seule votre identité d'accès d'origine Cloudfront soit autorisée à les lire (cela empêche les gens de contourner Cloudfront et d'aller directement sur s3).
  4. Créez une distribution cloudfront avec des URLs basiques et une autre qui requiert des URLs signées.
  5. Testez que vous pouvez télécharger des objets à partir de la distribution cloudfront de base mais pas à partir de s3 ou de la distribution cloudfront signée.
  6. Créer une paire de clés pour signer les URLs
  7. Générer des URL à l'aide de Python
  8. Testez que les URLs signés fonctionnent

1 - Créer un seau et télécharger un objet

La façon la plus simple de le faire est de passer par la console AWS, mais pour être complet, je vais vous montrer comment utiliser boto. Le code de boto est montré ici :

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 - Créer une "identité d'accès d'origine" Cloudfront

Pour l'instant, cette étape ne peut être réalisée qu'en utilisant l'API. Le code Boto est ici :

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 - Modifier les ACLs sur vos objets

Maintenant que nous avons notre compte utilisateur S3 spécial (le S3CanonicalUserId que nous avons créé ci-dessus), nous devons lui donner accès à nos objets s3. Nous pouvons le faire facilement à l'aide de la console AWS en ouvrant l'onglet Permissions de l'objet (pas du seau !), en cliquant sur le bouton "Add more permissions", et en collant le très long S3CanonicalUserId que nous avons obtenu ci-dessus dans le champ "Grantee" d'une nouvelle permission. Assurez-vous que vous donnez à la nouvelle permission les droits "Open/Download".

Vous pouvez également le faire en code en utilisant le boto suivant 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 - Créer une distribution cloudfront

Notez que les origines personnalisées et les distributions privées ne sont pas entièrement prises en charge par boto avant la version 2.0 qui n'a pas été officiellement publiée au moment de l'écriture. Le code ci-dessous extrait du code de la branche 2.0 de boto et le modifie pour le faire fonctionner, mais ce n'est pas joli. La branche 2.0 gère cela de manière beaucoup plus élégante - utilisez-la si 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 - Testez que vous pouvez télécharger des objets depuis cloudfront mais pas depuis s3

Vous devriez maintenant être en mesure de vérifier :

  • stream.example.com.s3.amazonaws.com/video.mp4 - devrait donner AccessDenied
  • signed_distribution.cloudfront.net/video.mp4 - devrait donner MissingKey (parce que l'URL n'est pas signé)
  • basic_distribution.cloudfront.net/video.mp4 - devrait fonctionner correctement

Les tests devront être adaptés pour fonctionner avec votre lecteur de flux, mais l'idée de base est que seule l'url de base de Cloudfront devrait fonctionner.

6 - Créer une paire de clés pour CloudFront

Je pense que le seul moyen de le faire est de passer par le site Web d'Amazon. Allez sur la page "Compte" de votre AWS et cliquez sur le lien "Security Credentials". Cliquez sur l'onglet "Key Pairs" puis sur "Create a New Key Pair". Cela va générer une nouvelle paire de clés pour vous et télécharger automatiquement un fichier de clé privée (pk-xxxxxxxxx.pem). Conservez le fichier de clé en lieu sûr et privé. Notez également l'"ID de la paire de clés" d'Amazon, car nous en aurons besoin à l'étape suivante.

7 - Générer quelques URLs en Python

À partir de la version 2.0 de boto, il ne semble pas y avoir de support pour générer des URL signées de CloudFront. Python n'inclut pas de routines de chiffrement RSA dans la bibliothèque standard, nous devrons donc utiliser une bibliothèque supplémentaire. J'ai utilisé M2Crypto dans cet exemple.

Pour une distribution non-streaming, vous devez utiliser l'URL complète de cloudfront comme ressource, cependant pour le streaming nous utilisons seulement le nom d'objet du fichier vidéo. Voir le code ci-dessous pour un exemple complet de génération d'une URL qui ne dure que 5 minutes.

Ce code est librement basé sur le code d'exemple PHP fourni par Amazon dans la documentation de CloudFront.

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 - Essayez les URLs

Avec un peu de chance, vous devriez maintenant avoir une URL fonctionnelle qui ressemble à quelque chose comme ceci :

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

Insérez ceci dans votre js et vous devriez avoir quelque chose qui ressemble à ceci (tiré de l'exemple PHP dans la documentation CloudFront d'Amazon) :

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');

Résumé

Comme vous pouvez le voir, pas très facile ! boto v2 aidera beaucoup à mettre en place la distribution. Je vais voir s'il est possible d'y inclure du code de génération d'URL pour améliorer cette grande bibliothèque !

3voto

kmonsoor Points 259

En Python, quel est le moyen le plus simple de générer une URL d'expiration pour un fichier. J'ai installé boto mais je ne vois pas comment obtenir un fichier à partir d'une distribution en continu.

Vous pouvez générer un signed-URL expirant pour la ressource. La documentation Boto3 a une bel exemple de solution pour ça :

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)

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X