# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 AWSTemplateFormatVersion: 2010-09-09 Transform: AWS::Serverless-2016-10-31 Description: A demo setup to update streaming manifest with signed URL params Parameters: Origin: Description: Domain name of content origin Type: String Default: origin.example.com OriginPath: Description: (optional) origin prefix URL Type: String AddCORSResponse: Description: (optional) add CORS header Access-Control-Allow-Origin:* Type: String Default: Yes AllowedValues: [Yes, No] Conditions: CORSResponse: !Equals [!Ref AddCORSResponse, Yes] Outputs: ViewerCloudFrontDomain: Value: !GetAtt ViewerDistribution.DomainName Description: Viewer CloudFront domain KeyPairId: Value: !Ref CFPKey Description: Key-Pair-Id for signed URL Resources: #Lambda@Edge Role for manifest rewriting manifest LambdaEdgeRoleUpdateManifest: Type: AWS::IAM::Role Properties: Path: / ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Sid: AllowLambdaServiceToAssumeRole Effect: Allow Action: - sts:AssumeRole Principal: Service: - lambda.amazonaws.com - edgelambda.amazonaws.com #Lambda@Edge Function manifest LambdaEdgeFunctionUpdateManifest: Type: AWS::Serverless::Function Properties: Description: update manifest file to add signed URL InlineCode: !Join - '' - - | 'use strict'; const https = require('https'); const AWS = require('aws-sdk'); const path = require('path'); exports.handler = (event, context, callback) => { const request = event.Records[0].cf.request; - " const domain = 'https://" - !GetAtt ManifestDistribution.DomainName - | '; const viewerDomain = event.Records[0].cf.config.distributionDomainName; let response = {}; //find dir for the m3u8 let dir = path.dirname(request.uri); let querystring = (request.querystring)?"?"+request.querystring:""; //request to 2nd CF to get cached m3u8 https.get(domain+request.uri+querystring, (resp) => { //use same status code response.status = resp.statusCode; //respond with a few headers let headers = { - !If [CORSResponse, ' "access-control-allow-origin": [{"key": "Access-Control-Allow-Origin", "value": "*"}],', ''] - | // "content-type": [{"key": "Content-Type", "value": resp.headers["content-type"]||"text/html"}], "server": [{"key": "Server", "value": resp.headers["server"]||"Server"}] }; response.headers = headers; //load response to let data = ""; resp.on('data', (chunk) => { data += chunk; }); //create signed URL only for lines without initial #, or URI component let body = []; resp.on('end', () => { data.split("\n").forEach((elem)=> { if (elem.startsWith("#") ) { if(elem.indexOf("URI") != -1) { //URI component inline var uriComponents = elem.substring(elem.indexOf("URI")).split("\""); uriComponents[1] = signed(viewerDomain, dir, uriComponents[1]); body.push(elem.substring(0,elem.indexOf("URI"))+uriComponents.join("\"")); } else { body.push(elem); } } else if(elem== "") { body.push(elem); } else { body.push(signed(viewerDomain, dir, elem)); } }); response.body = body.join('\n'); callback(null, response); }); }).on('error', (err)=>{ callback( null, { 'status': '500', 'statusDescrition': 'Server Error', 'headers': { 'content-type': [{'key': 'Content-Type', 'value': 'text/plain'}] }, 'body': 'Error reading content \n\n'+err } ); }); }; function signed(domain, dir, file) { - ' let keyPairId = "' - !Ref CFPKey - | "; let privateKey = ""; let duration = 300; let cf = new AWS.CloudFront.Signer(keyPairId, privateKey); let policy = JSON.stringify({ Statement: [{ Resource: 'http*://'+ domain+path.posix.join(dir,file), Condition: { DateLessThan: { 'AWS:EpochTime': Math.floor(new Date().getTime() / 1000) + duration } } }] }); let signedUrl= cf.getSignedUrl({'url': 'https://'+ domain+path.posix.join(dir,file), 'policy': policy}); let qry = signedUrl.split('?').pop(); return file+'?'+qry; } Role: !GetAtt LambdaEdgeRoleUpdateManifest.Arn Runtime: nodejs14.x Handler: index.handler Timeout: 5 AutoPublishAlias: live #Lambda@Edge Role for add CORS header to segments LambdaEdgeRoleAddCORS: Type: AWS::IAM::Role Properties: Path: / ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Sid: AllowLambdaServiceToAssumeRole Effect: Allow Action: - sts:AssumeRole Principal: Service: - lambda.amazonaws.com - edgelambda.amazonaws.com #Lambda@Edge Function manifest LambdaEdgeFunctionAddCORS: Type: AWS::Serverless::Function Properties: Description: add CORS header to the response InlineCode: | 'use strict'; exports.handler = (event, context, callback) => { const response = event.Records[0].cf.response; const headers = response.headers; headers['access-control-allow-origin'] = [{value: "*"}]; callback(null, response); }; Role: !GetAtt LambdaEdgeRoleAddCORS.Arn Runtime: nodejs14.x Handler: index.handler Timeout: 5 AutoPublishAlias: live #Public Key CFPKey: Type: 'AWS::CloudFront::PublicKey' Properties: PublicKeyConfig: CallerReference: !Sub '${AWS::StackName}-pubkey-cfn-template' Comment: Public Key for signed URL EncodedKey: | Name: !Sub '${AWS::StackName}-Public-Key' #Key Group CFKeyGroup: Type: 'AWS::CloudFront::KeyGroup' Properties: KeyGroupConfig: Comment: Key Group includes public key Items: - !Ref CFPKey Name: !Sub '${AWS::StackName}-Key-Group' #Viewer Distribution ViewerDistribution: Type: 'AWS::CloudFront::Distribution' Properties: DistributionConfig: Enabled: true Comment: !Sub '${AWS::StackName}-Viewer-Distribution' Origins: - Id: Content-Origin DomainName: !Ref Origin OriginPath: !Ref OriginPath CustomOriginConfig: HTTPPort: 80 HTTPSPort: 443 OriginProtocolPolicy: 'match-viewer' OriginSSLProtocols: ['TLSv1.2'] OriginReadTimeout: 30 OriginKeepaliveTimeout: 5 ConnectionAttempts: 3 ConnectionTimeout: 10 OriginShield: Enabled: false DefaultRootObject: '' HttpVersion: 'http1.1' DefaultCacheBehavior: TargetOriginId: Content-Origin ViewerProtocolPolicy: 'redirect-to-https' AllowedMethods: [GET, HEAD] CachedMethods: [GET, HEAD] CachePolicyId: "658327ea-f89d-4fab-a63d-7e88639e58f6" ##Cache Optimized LambdaFunctionAssociations: !If - CORSResponse - - EventType: 'origin-response' IncludeBody: false LambdaFunctionARN: !Ref LambdaEdgeFunctionAddCORS.Version - !Ref AWS::NoValue Compress: true TrustedKeyGroups: - !Ref CFKeyGroup CacheBehaviors: - AllowedMethods: [GET, HEAD] CachedMethods: [GET, HEAD] CachePolicyId: "4135ea2d-6df8-44a3-9df3-4b5a84be39ad" ##Cache Disabled OriginRequestPolicyId: "216adef6-5c7f-47e4-b989-5492eafa07d3" ## Forward all Compress: true LambdaFunctionAssociations: - EventType: 'origin-request' IncludeBody: false LambdaFunctionARN: !Ref LambdaEdgeFunctionUpdateManifest.Version PathPattern: '*.m3u8' TargetOriginId: Content-Origin TrustedKeyGroups: - !Ref CFKeyGroup ViewerProtocolPolicy: 'redirect-to-https' #Manifest Distribution ManifestDistribution: Type: 'AWS::CloudFront::Distribution' Properties: DistributionConfig: Enabled: true Comment: !Sub '${AWS::StackName}-Manifest-Distribution' Origins: - Id: Content-Origin DomainName: !Ref Origin OriginPath: !Ref OriginPath CustomOriginConfig: HTTPPort: 80 HTTPSPort: 443 OriginProtocolPolicy: 'match-viewer' OriginSSLProtocols: ['TLSv1.2'] OriginReadTimeout: 30 OriginKeepaliveTimeout: 5 ConnectionAttempts: 3 ConnectionTimeout: 10 OriginShield: Enabled: false DefaultRootObject: '' HttpVersion: 'http1.1' DefaultCacheBehavior: TargetOriginId: Content-Origin ViewerProtocolPolicy: 'redirect-to-https' AllowedMethods: [GET, HEAD] CachedMethods: [GET, HEAD] CachePolicyId: "658327ea-f89d-4fab-a63d-7e88639e58f6" ##Cache Optimized Compress: true TrustedKeyGroups: - !Ref CFKeyGroup CacheBehaviors: - AllowedMethods: [GET, HEAD] CachedMethods: [GET, HEAD] CachePolicyId: "658327ea-f89d-4fab-a63d-7e88639e58f6" ##Cache Optimized Compress: true PathPattern: '*.m3u8' TargetOriginId: Content-Origin ViewerProtocolPolicy: 'redirect-to-https'