Skip to content

Instantly share code, notes, and snippets.

@shiroamada
Forked from mrbar42/README.md
Created February 18, 2023 02:46
Show Gist options
  • Save shiroamada/e61fe9d532808c9731d6d920896b1ce4 to your computer and use it in GitHub Desktop.
Save shiroamada/e61fe9d532808c9731d6d920896b1ce4 to your computer and use it in GitHub Desktop.
Secured HLS setup with Nginx as media server

Secured HLS setup with Nginx as media server

This example is part of this article.

This is an example for an HLS delivery with basic security. Nginx compiled with nginx-rtmp-module & secure-link is used as media server. Features:

  • Domain filtering
  • Referrer filtering
  • Embed buster
  • Session token for playlist, segments and AES keys
  • AES encryption
  • HTTPS only

Throughout this example the host is assumed to be example.com. if you want to use this configurations, be sure to replace all instances of example.com with your domain.

Compiling Nginx

# install deps (Ubuntu)
sudo apt-get install -y build-essential libpcre3 libpcre3-dev libssl-dev

wget http://nginx.org/download/nginx-1.10.1.tar.gz
tar -xf nginx-1.10.1.tar.gz
cd nginx-1.10.1

./configure --with-http_ssl_module --add-module=../nginx-rtmp-module --with-http_secure_link_module

make -j
sudo make install
# nginx is now installed in /usr/local/nginx

Pushing video to Nginx

In order to push video to nginx i'm going to use ffmpeg which well supports RTMP as its output. I'm going to create an I frame roughly every 2 seconds which will allow nginx to achieve the 4s segment target. For simplicity i'll be using a static mp4 file and ingest it in infinite loop.

ffmpeg -hide_banner \
-stream_loop -1 \
-re -i test-video.mp4 \
-c:a aac -c:v h264 -g 48 \
-f flv rtmp://localhost:1935/show/live

I'm using live as the stream name, the output hls will carry that same name - e.g. live.m3u8.

Generating Session token

The session token is based on this format (note the spaces): MD5("EXPIREY_DATE_IN_SECONDS CLIENT_IP_ADDRESS SECRET")

here are several examples of generating the token:

BASH

get_customer_url() {
  local IP=${1:-127.0.0.1}
  local SECRET=${2:-VERY_COOL_SECRET}
  local EXPIRES="$(date -d "today + 30 minutes" +%s)";
  local token="$(echo -n "${EXPIRES} ${IP} ${SECRET}" | openssl md5 -binary | openssl base64 | tr +/ -_ | tr -d =)"
  echo "https://example.com/video/hls/${token}/${EXPIRES}/live.m3u8"
}

get_customer_url 10.20.1.55 "uigfp(@#tfpIUDGPFiouGDF"

Node.JS (Javascript)

var crypto = require('crypto');

function generateSecurePathHash(expires, client_ip, secret) {
    if (!expires || !client_ip || !secret) throw new Error('Must provide all token components');

    var input = expires + ' ' + client_ip + ' ' + secret;
    var binaryHash = crypto.createHash('md5').update(input).digest();
    var base64Value = new Buffer(binaryHash).toString('base64');
    return base64Value.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
}

function getStreamUrl(ip, secret) {
    const expiresTimestamp = new Date(Date.now() + (1000 * 60 * 30)).getTime();
    const expires = String(Math.round(expiresTimestamp / 1000));
    
    const token = generateSecurePathHash(expires, ip, secret);

    return `https://example.com/video/hls/${token}/${expires}/live.m3u8`;
}

getStreamUrl('127.0.0.1', 'uigfp(@#tfpIUDGPFiouGDF');
// https://example.com/video/hls/LdS-kcC-JGVHGNTFlX-6Sw/1526373776/live.m3u8
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>example.com</title>
<script>
if (top != self) {
top.location.replace(self.location.href);
}
</script>
<script src="//cdn.jsdelivr.net/clappr/latest/clappr.min.js"></script>
</head>
<body>
<div id="player" style="width:640px"></div>
<script>
var player = new window.Clappr.Player({
// this is an example url - for this to work you'll need to generate fresh token
source: 'https://example.com/video/hls/CIfZTIu8ygWzhXBSoQfPIQ/1526373226/live.m3u8',
parentId: '#player'
});
</script>
</body>
</html>
worker_processes auto;
events {
worker_connections 4096;
}
# RTMP configuration
rtmp {
server {
listen 1935; # Listen on standard RTMP port
chunk_size 4000;
application show {
live on;
# Turn on HLS
hls on;
hls_path /mnt/hls/;
hls_fragment 4;
hls_playlist_length 60;
# Setup AES encryption
hls_keys on;
hls_key_path /mnt/hls/keys;
hls_key_url keys/;
hls_fragments_per_key 10;
# disable consuming the stream from nginx as rtmp
deny play all;
}
}
}
http {
sendfile off;
tcp_nopush on;
directio 512;
default_type application/octet-stream;
access_log off;
error_log off;
# HTTPS certificate and key
ssl_certificate ssl/example.com.cert;
ssl_certificate_key ssl/example.com.key;
server {
listen 443 ssl;
server_name example.com;
root /mnt/;
# Disable cache
add_header 'Cache-Control' 'no-cache';
index index.html;
default_type "text/html";
types {
application/dash+xml mpd;
application/vnd.apple.mpegurl m3u8;
video/mp2t ts;
plain/text key;
}
location =/ {
# CORS setup
add_header 'Access-Control-Allow-Origin' 'https://example.com' always;
add_header 'Access-Control-Expose-Headers' 'Content-Length' always;
add_header 'X-Frame-Options' 'DENY' always;
}
location /video {
rewrite /hls/([a-zA-Z0-9_\-]*)/([0-9]*)/(.*)\.(ts|m3u8|key)$ /hls/$3.$4?token=$1&expires=$2;
root /mnt/not-exist;
}
location /hls {
internal;
# The secure link is base on the folowing format
# MD5("EXPIREY_DATE_IN_SECONDS CLIENT_IP_ADDRESS SECRET")
# here is a BASH function that generates a secure link
# get_token() {
# local expires="$(date -d "today + 30 minutes" +%s)";
# local token="$(echo -n "${expires} 127.0.0.1 VERY_COOL_SECRET" | openssl md5 -binary | openssl base64 | tr +/ -_ | tr -d =)"
# echo "${token}/${expires}"
# }
# echo "https://example.com/video/hls/$(get_token)/live.m3u8"
secure_link $arg_token,$arg_expires;
secure_link_md5 "$secure_link_expires $remote_addr VERY_COOL_SECRET";
if ($secure_link = "") { return 403; }
if ($secure_link = "0") { return 410; }
# Referrer protection
valid_referers server_names;
if ($invalid_referer) {
return 403;
}
# allow CORS preflight requests
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' 'https://example.com';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain charset=UTF-8';
add_header 'Content-Length' 0;
return 204;
}
}
}
server {
listen 443 ssl default;
server_name _;
return 444;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment