Skip to content

Commit

Permalink
Initial commit: Set up project structure and added basic functionalit…
Browse files Browse the repository at this point in the history
…y for HLS file encryption in AWS Lambda.
  • Loading branch information
meanii committed Feb 29, 2024
0 parents commit b78afe7
Show file tree
Hide file tree
Showing 11 changed files with 395 additions and 0 deletions.
26 changes: 26 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
ZIP_FILE_NAME="hls-enc-lambda"
BIN="bootstrap"

GOOS="linux"
GOARCH="arm64"
CGO_ENABLED=0

LAMBDA_FUNCTION_NAME="hls-enc"

compile:
@echo "Compiling..."
GOOS=$(GOOS) GOARCH=$(GOARCH) go build -tags lambda.norpc -o $(BIN) main.go

bundle:
@echo "Bundling..."
zip $(ZIP_FILE_NAME).zip $(BIN)

push:
@echo "Pushing..."
aws lambda update-function-code --function-name $(LAMBDA_FUNCTION_NAME) --zip-file fileb://$(ZIP_FILE_NAME).zip

clean:
@echo "Cleaning..."
rm -f $(BIN) $(ZIP_FILE_NAME).zip

deploy: clean compile bundle push
34 changes: 34 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package config

import (
"net/url"
"os"
"time"
)

type config struct {
Origin string
KeyID string
PrivateKeyBase64 string
ExpireTime time.Duration
}

var ConfigEnv *config

// InitConfig initialize the configuration
func InitConfig() {
ConfigEnv = load()
}

// load configuration from environment variables
func load() (c *config) {
origin, _ := url.Parse(os.Getenv("CLOUDFRONT_ORIGIN"))
expireTime, _ := time.ParseDuration(os.Getenv("EXPIRE_TIME"))
c = &config{
Origin: origin.Host,
KeyID: os.Getenv("CLOUDFRONT_ACCESS_KEY_ID"),
PrivateKeyBase64: os.Getenv("CLOUDFRONT_PRIVATE_KEY_BASE64"),
ExpireTime: expireTime,
}
return
}
13 changes: 13 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module github.com/meanii/aws-lambda-hls-enc

go 1.22.0

require (
github.com/aws/aws-lambda-go v1.46.0
github.com/aws/aws-sdk-go v1.50.27
)

require (
github.com/awslabs/aws-lambda-go-api-proxy v0.16.1 // indirect
github.com/grafov/m3u8 v0.12.0 // indirect
)
18 changes: 18 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
github.com/aws/aws-lambda-go v1.46.0 h1:UWVnvh2h2gecOlFhHQfIPQcD8pL/f7pVCutmFl+oXU8=
github.com/aws/aws-lambda-go v1.46.0/go.mod h1:dpMpZgvWx5vuQJfBt0zqBha60q7Dd7RfgJv23DymV8A=
github.com/aws/aws-sdk-go v1.50.27 h1:96ifhrSuja+AzdP3W/T2337igqVQ2FcSIJYkk+0rCeA=
github.com/aws/aws-sdk-go v1.50.27/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
github.com/awslabs/aws-lambda-go-api-proxy v0.16.1 h1:x4F/VbWYt/f5K9+n3TAqbjFljDP52KWbYz/fNBvQdi8=
github.com/awslabs/aws-lambda-go-api-proxy v0.16.1/go.mod h1:31WDgvTzVyra022CWzO6uEZFel9/y7QKaZpUQEqYLr0=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/grafov/m3u8 v0.12.0 h1:T6iTwTsSEtMcwkayef+FJO8kj+Sglr4Lh81Zj8Ked/4=
github.com/grafov/m3u8 v0.12.0/go.mod h1:nqzOkfBiZJENr52zTVd/Dcl03yzphIMbJqkXGu+u080=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
32 changes: 32 additions & 0 deletions handler/binary_chunks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package handler

import (
"fmt"
"net/http"
"net/url"

"github.com/meanii/aws-lambda-hls-enc/config"
"github.com/meanii/aws-lambda-hls-enc/helper"
)

// BinaryChunks is the handler for binary chunks
// It will sign the URL and redirect to the signed URL
func BinaryChunksRedirection(cfUrl url.URL, w http.ResponseWriter, r *http.Request) {
signedUrlSchema := helper.URLSigner{
KeyID: config.ConfigEnv.KeyID,
PrivKeyBase64: config.ConfigEnv.PrivateKeyBase64,
Hour: config.ConfigEnv.ExpireTime,
}

signedChunkedUrl, err := signedUrlSchema.Signer(cfUrl.String())
if err != nil {
fmt.Printf("Error signing URL: %s\n", err)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(fmt.Sprintf("Error signing URL: %s\n", err)))
return
}

w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Location", signedChunkedUrl)
http.Redirect(w, r, signedChunkedUrl, http.StatusFound)
}
21 changes: 21 additions & 0 deletions handler/http_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package handler

import (
"net/http"
)

// HandlerMethodOptions is the handler for OPTIONS method
func HandlerMethodOptions(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodOptions {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
w.WriteHeader(http.StatusOK)
}
}

// HandlerMethodHead is the handler for HEAD method
func HandlerMethodHead(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodHead {
w.WriteHeader(http.StatusOK)
}
}
71 changes: 71 additions & 0 deletions handler/m3u8_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package handler

import (
"fmt"
"net/http"
"net/url"
"path"
"strings"

"github.com/grafov/m3u8"

"github.com/meanii/aws-lambda-hls-enc/config"
"github.com/meanii/aws-lambda-hls-enc/helper"
)

// M3u8Media is the handler for m3u8 media playlist
// It will return the media playlist
func M3u8Media(mediapl *m3u8.MediaPlaylist, w http.ResponseWriter, _ *http.Request) {
mediaPlaylistString := mediapl.String()
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Write([]byte(mediaPlaylistString))
}

// M3u8Master is the handler for m3u8 master playlist
// It will return the master playlist
func M3u8Master(
clurl url.URL,
masterpl *m3u8.MasterPlaylist,
w http.ResponseWriter,
_ *http.Request,
) {
masterPlaylistString := masterpl.String()
for _, variant := range masterpl.Variants {

baseName := path.Base(clurl.String())
variantUrl := strings.Replace(clurl.String(), baseName, variant.URI, 1)

// This is the URLSigner struct, it is used to sign the URL
signedUrlSchema := helper.URLSigner{
KeyID: config.ConfigEnv.KeyID,
PrivKeyBase64: config.ConfigEnv.PrivateKeyBase64,
Hour: config.ConfigEnv.ExpireTime,
}
signedVariantUrl, err := signedUrlSchema.Signer(variantUrl)
if err != nil {
fmt.Printf("Error signing URL: %s\n", err)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(fmt.Sprintf("Error signing URL: %s\n", err)))
return
}

// Replace the original variant URL with the signed variant URL
signedVariantURL, _ := url.Parse(signedVariantUrl)
signedChunkUrl := variant.URI + "?" + signedVariantURL.RawQuery // Add the query string
masterPlaylistString = strings.Replace(
masterPlaylistString,
variant.URI,
signedChunkUrl,
-1,
)

}

// Set the headers and write the response
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/x-mpegURL")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Write([]byte(masterPlaylistString))
}
14 changes: 14 additions & 0 deletions helper/fetch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package helper

import (
"net/http"
)

// Fetch fetches the content of the given URL
func Fetch(url string) (*http.Response, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
return resp, nil
}
52 changes: 52 additions & 0 deletions helper/signer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package helper

import (
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"time"

"github.com/aws/aws-sdk-go/service/cloudfront/sign"
)

// URLSigner is a struct that holds the key ID, private key, raw URL, and hour.
type URLSigner struct {
KeyID string // Credential Key Pair key ID
PrivKeyBase64 string // private key in base64
Hour time.Duration // hour duration
}

// Signer signs a URL to be valid, using the private key and credential pair key ID.
func (us *URLSigner) Signer(url string) (string, error) {
// Load the private key, and return an error if it fails
privKeyBytes, err := loadPrivateKey(us.PrivKeyBase64)
if err != nil {
return "", err
}

// Create a new URLSigner with the key ID and private key
signer := sign.NewURLSigner(us.KeyID, privKeyBytes)
signedURL, err := signer.Sign(url, time.Now().Add(us.Hour))
return signedURL, nil
}

// loadPrivateKey loads a RSA private key from the file path.
func loadPrivateKey(privKeyBase64 string) (*rsa.PrivateKey, error) {
// Decode the base64 string to bytes
privKeyBytes, err := base64.StdEncoding.DecodeString(privKeyBase64)
if err != nil {
return nil, err
}

// Decode the PEM block to get the private key
block, _ := pem.Decode(privKeyBytes)

// Parse the private key
// #TODO: handle fallback to PKCS1 if PKCS8 fails to parse
privKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return nil, err
}
return privKey.(*rsa.PrivateKey), nil
}
19 changes: 19 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package main

import (
"net/http"

"github.com/aws/aws-lambda-go/lambda"
"github.com/awslabs/aws-lambda-go-api-proxy/httpadapter"

"github.com/meanii/aws-lambda-hls-enc/config"
"github.com/meanii/aws-lambda-hls-enc/server"
)

func main() {
config.InitConfig()
http.HandleFunc("/", server.HandleRequest)

// Start the Lambda proxy
lambda.Start(httpadapter.New(http.DefaultServeMux).ProxyWithContext)
}
Loading

0 comments on commit b78afe7

Please sign in to comment.