Skip to content

Instantly share code, notes, and snippets.

@mrbar42
Last active October 25, 2024 19:35
Show Gist options
  • Save mrbar42/09c149059f72da2f09e652d4c5079919 to your computer and use it in GitHub Desktop.
Save mrbar42/09c149059f72da2f09e652d4c5079919 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;
}
}
@michaeltoohig
Copy link

Why did you choose to have the HLS URL point to /video then rewrite it to /hls with the token and expire appended to the query string? Why not directly write the token and expire timestamp directly such as https://example.com/hls/live.m3u8?token=foo&expire=1234.

I noticed the /hls location is internal so perhaps that has a part to do with it or were you just following the pattern as described in the secure link documentation?

@iamandi
Copy link

iamandi commented Oct 10, 2020

./configure: error: no ../nginx-rtmp-module/config was found

You first need to install nginx-rtmp module. https://github.com/arut/nginx-rtmp-module

@josiaslg
Copy link

josiaslg commented Mar 10, 2021

Works ok on CentOS7.
The way work on CentOS and bash script to generate link:
On Script bash:

#!/bin/bash
get_customer_url() {
local IP=${1:-127.0.0.1}
local SECRET=${2:-PASSWORD123}
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 PASSWORD123
------------------------------------- END BASH FILE -------------------------------------------------------
10.20.1.55 you change to your IP on internet. Whatsmyip will tell you the value. Is not the IP server or where index.html will be hosted, is the IP from your computer test (will browser to the index.html page).
Probably you don't need to change local SECRET=${2:-VERY_COOL_SECRET} because is a parameter, but to not make visual confusion, change.

On nginx.conf,
secure_link_md5 "$secure_link_expires $remote_addr VERY_COOL_SECRET";
change to
secure_link_md5 "$secure_link_expires $remote_addr PASSWORD123";
------------------------------------- END OF NGINX.CONF ------------------------------------------

Now you have the index.html on /mnt. Remember, the CORS protec your page from be used from other domain/ip.
Don't use VLC and others to test, will always break.
On Index.html change the line of src video to one generated on bash script from begin.

-------------------------------------- END index.html --------------------------------------------------

Notes: If you use curl or firefox console and get 403 error, is because URL, IP or Secret (password) is wrong. The password insert against IP on bash and nginx.conf always must be equal, is the verification text to verify if is authentic.

@IkiaeM
Copy link

IkiaeM commented Aug 27, 2021

I have trouble understanding what to do with the "bash" code at the beginning, I think I understand the rest but I'm completely stuck on how to generate the token in bash because at no time there is a link. Can you enlighten me?

@josiaslg
Copy link

I have trouble understanding what to do with the "bash" code at the beginning, I think I understand the rest but I'm completely stuck on how to generate the token in bash because at no time there is a link. Can you enlighten me?

No problem.
The bash on beginning is a separate file. I'm specifying the shell bash. After you save the file, do chmod + x on that file and will be mark to be execute on their permission. Nginx interact with this script ( then why must be chmod +x and bash in the beginning).

@IkiaeM
Copy link

IkiaeM commented Aug 27, 2021 via email

@sdkao
Copy link

sdkao commented Sep 12, 2022

Hello who has got the equivalent angular code for method get_customer_url?

@anon-user-com
Copy link

anon-user-com commented Dec 6, 2022

Works ok on CentOS7. The way work on CentOS and bash script to generate link: On Script bash:

#!/bin/bash get_customer_url() { local IP=${1:-127.0.0.1} local SECRET=${2:-PASSWORD123} 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 PASSWORD123 ------------------------------------- END BASH FILE ------------------------------------------------------- 10.20.1.55 you change to your IP on internet. Whatsmyip will tell you the value. Is not the IP server or where index.html will be hosted, is the IP from your computer test (will browser to the index.html page). Probably you don't need to change local SECRET=${2:-VERY_COOL_SECRET} because is a parameter, but to not make visual confusion, change.

On nginx.conf, secure_link_md5 "$secure_link_expires $remote_addr VERY_COOL_SECRET"; change to secure_link_md5 "$secure_link_expires $remote_addr PASSWORD123"; ------------------------------------- END OF NGINX.CONF ------------------------------------------

Now you have the index.html on /mnt. Remember, the CORS protec your page from be used from other domain/ip. Don't use VLC and others to test, will always break. On Index.html change the line of src video to one generated on bash script from begin.

-------------------------------------- END index.html --------------------------------------------------

Notes: If you use curl or firefox console and get 403 error, is because URL, IP or Secret (password) is wrong. The password insert against IP on bash and nginx.conf always must be equal, is the verification text to verify if is authentic.

Thank you very much. bash command I have to uncomment from nginx or I have to create a new file and then call from nginx. Also Im not understanding this line "get_customer_url 10.20.1.55 PASSWORD123" I want to share a video that can be view by my audience I dont have their Ip. I hope that you can help me a bit

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment