├── CSharp └── URLProcessor.cs ├── LICENSE ├── README.md ├── java ├── pom.xml └── src │ └── main │ └── java │ └── Main.java ├── javascript └── wmsauth-example.js ├── pay-per-view ├── ppv_handler.php ├── ppv_media_signature.php ├── ppv_sync.json ├── ppv_sync.xsd └── ppv_sync_response.json ├── php ├── basic-hls-rtmp-obfuscation.php ├── basic-hls-stream-based.php ├── basic-rtsp.php ├── jwpayer-rtmp-hls-with-proxy.php ├── jwpayer-rtmp-hls.php ├── ppv-rtsp-rtmp.php └── rtmp-flowplayer.php └── python ├── ppv_media_signature.py └── wmsauth.py /CSharp/URLProcessor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using System.Security.Cryptography; 4 | using System.Globalization; 5 | 6 | public class URLProcessor 7 | { 8 | 9 | public static string BuildProtectedURLWithValidity(string password, string media_url, string ip, int valid) 10 | { 11 | string result = null; 12 | DateTime cur_date = DateTime.Now; 13 | TimeZone localzone = TimeZone.CurrentTimeZone; 14 | 15 | DateTime localTime = localzone.ToUniversalTime(cur_date); 16 | 17 | string date_time = localTime.ToString(new CultureInfo("en-us")); 18 | 19 | Int32 Valid = valid; 20 | string to_be_hashed = ip + password + date_time + Valid.ToString(); 21 | 22 | byte[] to_be_hashed_byte_array = new byte[to_be_hashed.Length]; 23 | 24 | int i = 0; 25 | foreach (char cur_char in to_be_hashed) 26 | { 27 | to_be_hashed_byte_array[i++] = (byte)cur_char; 28 | } 29 | 30 | byte[] hash = (new MD5CryptoServiceProvider()).ComputeHash(to_be_hashed_byte_array); 31 | 32 | string md5_signature = Convert.ToBase64String(hash); 33 | 34 | string signature = "server_time=" + date_time + "&hash_value=" + md5_signature + "&validminutes=" + Valid.ToString(); 35 | string base64urlsignature = Convert.ToBase64String(Encoding.UTF8.GetBytes(signature)); 36 | result = media_url + "?wmsAuthSign=" + base64urlsignature; 37 | return (result); 38 | } 39 | 40 | public static void Main() 41 | { 42 | Console.WriteLine(BuildProtectedURLWithValidity("defaultpassword", "http://yourdomain.com:8081/vod/sample.mp4/playlist.m3u8", "127.0.0.1", 20)); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Softvelum team 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Code snippets for WMSAuth Paywall feature set 2 | ===================== 3 | 4 | WMSAuth Paywall is a feature set of WMSPanel which is the admin and reporting panel for media servers. This feature set can be applied to **[Nimble Streamer](https://softvelum.com/nimble/)** software media server. It allows restriction of your media access by: 5 | - hot-linking re-publishing protection; 6 | - geo-location and IP ranges; 7 | - connections count. 8 | 9 | Please read Paywall section of Softvelum website for details: https://softvelum.com/paywall/ 10 | 11 | "pay-per-view" directory has reference code, requests and responses for pay-per-view feature set described in respective articles for Nimble Streamer and Wowza. 12 | 13 | The following sample code snippets are provided for quick and seamless integration of hot-linking protection. The repository directories have the code as described below. 14 | 15 | "CSharp" contains C# snippet. 16 | 17 | "java" contains Java snippet. 18 | 19 | "javascript" contains NodeJS JavaScript snippet. 20 | 21 | "python" contains Python snippet. 22 | 23 | "php" contains snippets for PHP: 24 | - jwpayer-rtmp-hls.php - code sample for JWPlayer with RTMP and HLS 25 | - jwpayer-rtmp-hls-with-proxy.php - code sample for JWPlayer with RTMP and HLS which has IP address obtained from various headers - see https://blog.wmspanel.com/2015/02/using-paywall-cloudflare-proxies.html for details. 26 | - rtmp-flowplayer.php - Flowplayer sample with RTMP 27 | - basic-hls-rtmp-obfuscation.php - basic sample for HLS and RTMP with code obfuscation agains grabbers, see this article for details: https://blog.wmspanel.com/2015/07/protecting-media-links-from-web-scraping.html 28 | - basic-hls-stream-based.php - basic sample for HLS where signature includes streams name 29 | - basic-rtsp.php - basic sample for RTSP 30 | - ppv-rtsp-rtmp.php - sample of RTMP and RTSP signature for pay-per-view 31 | 32 | If you look for code samples for Android or iOS, notice that we do not recommend using link signature code in mobile apps. Please read Q19 in paywall FAQ: https://softvelum.com/paywall/faq/ 33 | 34 | You may also check publish control framework for managing streams publishers: https://blog.wmspanel.com/2015/10/rtsp-publish-control-framework-overview.html 35 | -------------------------------------------------------------------------------- /java/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | wmsauth-java 8 | wmsauth-java 9 | 1.0 10 | 11 | 12 | joda-time 13 | joda-time 14 | 1.6 15 | 16 | 17 | commons-codec 18 | commons-codec 19 | 1.5 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /java/src/main/java/Main.java: -------------------------------------------------------------------------------- 1 | import org.apache.commons.codec.binary.Base64; 2 | import org.apache.commons.codec.digest.DigestUtils; 3 | import org.joda.time.DateTime; 4 | import org.joda.time.DateTimeZone; 5 | import org.joda.time.format.DateTimeFormat; 6 | import org.joda.time.format.DateTimeFormatter; 7 | 8 | import java.io.UnsupportedEncodingException; 9 | import java.util.Locale; 10 | 11 | public class Main { 12 | 13 | public static void main(String[] args) throws UnsupportedEncodingException { 14 | DateTimeFormatter timeFormatter = DateTimeFormat.forPattern("M/d/y h:m:s a").withZone(DateTimeZone.UTC).withLocale(Locale.US); 15 | DateTime currentServerTime = new DateTime(DateTimeZone.UTC); // lets get localtime in UTC timezone 16 | String today = timeFormatter.print(currentServerTime); 17 | String video_url = "http://yourdomain.com:8081/live/Stream1"; 18 | String ip = "127.0.0.1"; 19 | String key = "defaultpassword"; 20 | String validminutes = "20"; 21 | 22 | String to_hash = ip + key + today + validminutes; 23 | byte[] ascii_to_hash = to_hash.getBytes("UTF-8"); 24 | String base64hash = Base64.encodeBase64String(DigestUtils.md5(ascii_to_hash)); 25 | String urlsignature = "server_time=" + today + "&hash_value=" + base64hash + "&validminutes=" + validminutes; 26 | String base64urlsignature = Base64.encodeBase64String(urlsignature.getBytes("UTF-8")); 27 | String signedurlwithvalidinterval = video_url + "?wmsAuthSign=" + base64urlsignature; 28 | 29 | return; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /javascript/wmsauth-example.js: -------------------------------------------------------------------------------- 1 | /* This server side example includes Date Format library and WMSAuth sample code */ 2 | 3 | /* 4 | * Date Format 1.2.3 5 | * (c) 2007-2009 Steven Levithan 6 | * MIT license 7 | * 8 | * Includes enhancements by Scott Trenda 9 | * and Kris Kowal 10 | * 11 | * Accepts a date, a mask, or a date and a mask. 12 | * Returns a formatted version of the given date. 13 | * The date defaults to the current date/time. 14 | * The mask defaults to dateFormat.masks.default. 15 | */ 16 | 17 | var dateFormat = function () { 18 | var token = /d{1,4}|m{1,4}|yy(?:yy)?|([HhMsTt])\1?|[LloSZ]|"[^"]*"|'[^']*'/g, 19 | timezone = /\b(?:[PMCEA][SDP]T|(?:Pacific|Mountain|Central|Eastern|Atlantic) (?:Standard|Daylight|Prevailing) Time|(?:GMT|UTC)(?:[-+]\d{4})?)\b/g, 20 | timezoneClip = /[^-+\dA-Z]/g, 21 | pad = function (val, len) { 22 | val = String(val); 23 | len = len || 2; 24 | while (val.length < len) val = "0" + val; 25 | return val; 26 | }; 27 | 28 | // Regexes and supporting functions are cached through closure 29 | return function (date, mask, utc) { 30 | var dF = dateFormat; 31 | 32 | // You can't provide utc if you skip other args (use the "UTC:" mask prefix) 33 | if (arguments.length == 1 && Object.prototype.toString.call(date) == "[object String]" && !/\d/.test(date)) { 34 | mask = date; 35 | date = undefined; 36 | } 37 | 38 | // Passing date through Date applies Date.parse, if necessary 39 | date = date ? new Date(date) : new Date; 40 | if (isNaN(date)) throw SyntaxError("invalid date"); 41 | 42 | mask = String(dF.masks[mask] || mask || dF.masks["default"]); 43 | 44 | // Allow setting the utc argument via the mask 45 | if (mask.slice(0, 4) == "UTC:") { 46 | mask = mask.slice(4); 47 | utc = true; 48 | } 49 | 50 | var _ = utc ? "getUTC" : "get", 51 | d = date[_ + "Date"](), 52 | D = date[_ + "Day"](), 53 | m = date[_ + "Month"](), 54 | y = date[_ + "FullYear"](), 55 | H = date[_ + "Hours"](), 56 | M = date[_ + "Minutes"](), 57 | s = date[_ + "Seconds"](), 58 | L = date[_ + "Milliseconds"](), 59 | o = utc ? 0 : date.getTimezoneOffset(), 60 | flags = { 61 | d: d, 62 | dd: pad(d), 63 | ddd: dF.i18n.dayNames[D], 64 | dddd: dF.i18n.dayNames[D + 7], 65 | m: m + 1, 66 | mm: pad(m + 1), 67 | mmm: dF.i18n.monthNames[m], 68 | mmmm: dF.i18n.monthNames[m + 12], 69 | yy: String(y).slice(2), 70 | yyyy: y, 71 | h: H % 12 || 12, 72 | hh: pad(H % 12 || 12), 73 | H: H, 74 | HH: pad(H), 75 | M: M, 76 | MM: pad(M), 77 | s: s, 78 | ss: pad(s), 79 | l: pad(L, 3), 80 | L: pad(L > 99 ? Math.round(L / 10) : L), 81 | t: H < 12 ? "a" : "p", 82 | tt: H < 12 ? "am" : "pm", 83 | T: H < 12 ? "A" : "P", 84 | TT: H < 12 ? "AM" : "PM", 85 | Z: utc ? "UTC" : (String(date).match(timezone) || [""]).pop().replace(timezoneClip, ""), 86 | o: (o > 0 ? "-" : "+") + pad(Math.floor(Math.abs(o) / 60) * 100 + Math.abs(o) % 60, 4), 87 | S: ["th", "st", "nd", "rd"][d % 10 > 3 ? 0 : (d % 100 - d % 10 != 10) * d % 10] 88 | }; 89 | 90 | return mask.replace(token, function ($0) { 91 | return $0 in flags ? flags[$0] : $0.slice(1, $0.length - 1); 92 | }); 93 | }; 94 | }(); 95 | 96 | // Some common format strings 97 | dateFormat.masks = { 98 | "default": "ddd mmm dd yyyy HH:MM:ss", 99 | shortDate: "m/d/yy", 100 | mediumDate: "mmm d, yyyy", 101 | longDate: "mmmm d, yyyy", 102 | fullDate: "dddd, mmmm d, yyyy", 103 | shortTime: "h:MM TT", 104 | mediumTime: "h:MM:ss TT", 105 | longTime: "h:MM:ss TT Z", 106 | isoDate: "yyyy-mm-dd", 107 | isoTime: "HH:MM:ss", 108 | isoDateTime: "yyyy-mm-dd'T'HH:MM:ss", 109 | isoUtcDateTime: "UTC:yyyy-mm-dd'T'HH:MM:ss'Z'" 110 | }; 111 | 112 | // Internationalization strings 113 | dateFormat.i18n = { 114 | dayNames: [ 115 | "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", 116 | "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" 117 | ], 118 | monthNames: [ 119 | "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", 120 | "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" 121 | ] 122 | }; 123 | 124 | // For convenience... 125 | Date.prototype.format = function (mask, utc) { 126 | return dateFormat(this, mask, utc); 127 | }; 128 | 129 | 130 | // WmsAuth related code 131 | 132 | var http = require('http'); 133 | http.createServer(function (req, res) { 134 | res.writeHead(200, {'Content-Type': 'text/plain'}); 135 | var today = (new Date()).format("UTC:m/d/yyyy h:MM:ss TT"); 136 | var initial_url = "http://yourdomain.com:8081/live/stream/playlist.m3u8"; 137 | var ip = req.connection.remoteAddress; 138 | var key = "defaultpassword"; 139 | var validminutes = 20; 140 | var str2hash = ip + key + today + validminutes; 141 | 142 | var crypto = require('crypto'); 143 | var md5sum = crypto.createHash('md5'); 144 | md5sum.update(str2hash, 'ascii'); 145 | var base64hash = md5sum.digest('base64'); 146 | 147 | var urlsignature = "server_time=" + today + "&hash_value=" + base64hash + "&validminutes=" + validminutes; 148 | 149 | var base64urlsignature = new Buffer(urlsignature).toString('base64'); 150 | 151 | var signedurlwithvalidinterval = initial_url + "?wmsAuthSign=" + base64urlsignature; 152 | 153 | res.end(signedurlwithvalidinterval); 154 | }).listen(80, '0.0.0.0'); 155 | console.log('Server running at http://0.0.0.0'); 156 | -------------------------------------------------------------------------------- /pay-per-view/ppv_handler.php: -------------------------------------------------------------------------------- 1 | array('ID' => array(1, 2))); 15 | echo json_encode($arr); 16 | ?> 17 | -------------------------------------------------------------------------------- /pay-per-view/ppv_media_signature.php: -------------------------------------------------------------------------------- 1 | 26 | -------------------------------------------------------------------------------- /pay-per-view/ppv_sync.json: -------------------------------------------------------------------------------- 1 | {"ID":"35465921-42f0-4a98-91b1-74305f329b4f","Signature":"flPsWZPshoBFwy/Ah464OQ==","Puzzle":"e34b51a3-4018-4707-8afe-890409202ec8","PayPerViewInfo":{"VHost":[{"name":"_defaultVHost_","Application":[{"name":"vod","Instance":[{"name":"_definst_","Stream":[{"name":"sample.mp4","Player":[{"id":"1","ip":"192.168.10.10","delta":286786},{"id":"2","ip":"192.168.10.11","delta":263791},{"id":"4","ip":"127.0.0.1","delta":30031},{"id":"5","ip":"127.0.0.1","delta":10563}]}]}]}]}]}} 2 | -------------------------------------------------------------------------------- /pay-per-view/ppv_sync.xsd: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /pay-per-view/ppv_sync_response.json: -------------------------------------------------------------------------------- 1 | {"DenyList":{"ID":["1","2"]}} 2 | -------------------------------------------------------------------------------- /php/basic-hls-rtmp-obfuscation.php: -------------------------------------------------------------------------------- 1 | 43 | 44 | " . $fakelastsigpart . ""); 49 | } 50 | echo(""); 51 | for($i = 0; $i < $count2; $i++) { 52 | $fakelastsigpart = str_shuffle($fakelastsigpart); 53 | $fakeshiftbacktitle = str_shuffle($fakeshiftbacktitle); 54 | echo(""); 55 | } 56 | ?> 57 | 58 |
59 | 60 | -------------------------------------------------------------------------------- /php/basic-hls-stream-based.php: -------------------------------------------------------------------------------- 1 | 29 | -------------------------------------------------------------------------------- /php/basic-rtsp.php: -------------------------------------------------------------------------------- 1 | 30 | -------------------------------------------------------------------------------- /php/jwpayer-rtmp-hls-with-proxy.php: -------------------------------------------------------------------------------- 1 | 36 | 37 |
38 | 52 | -------------------------------------------------------------------------------- /php/jwpayer-rtmp-hls.php: -------------------------------------------------------------------------------- 1 | 19 | 20 |
21 | 35 | -------------------------------------------------------------------------------- /php/ppv-rtsp-rtmp.php: -------------------------------------------------------------------------------- 1 | 2 | 25 | 26 | 27 | 28 | 29 |

30 | RTMP publish signature:
31 | RTSP publish signature: 32 |

33 | 34 | 35 | -------------------------------------------------------------------------------- /php/rtmp-flowplayer.php: -------------------------------------------------------------------------------- 1 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 48 | 49 | 50 | 51 | 52 |
53 | 54 | 55 | 56 |
57 | 58 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /python/ppv_media_signature.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding: utf8 3 | 4 | import base64 5 | from hashlib import md5 6 | from time import gmtime, strftime 7 | 8 | 9 | def ppv_url_with_signature(ip, user_id, password, validminutes, initial_url, signed_stream="", strm_len=0): 10 | """ 11 | Get URL with signature when using PayPerView method with Nimble 12 | :param ip: Viewer IP address 13 | :param user_id: User ID used in back-end web server 14 | :param password: hot-link protection password configured in WMSPanel Auth Rule 15 | :param validminutes: Valid duration of the URL in minutes. After this time, the URL will not work 16 | :param initial_url: Full URL stream you want to protect 17 | :param signed_stream: stream name to make it unique for each stream 18 | https://blog.wmspanel.com/2014/10/stream-name-hotlink-protection.html 19 | You may also use part of the name if you need to protect streams by pattern ! 20 | :param strm_len: len(signed_stream) 21 | :return: Signed URL 22 | """ 23 | today = strftime("%m/%d/%Y %I:%M:%S %p", gmtime()) 24 | key = (ip + user_id + password + today + validminutes + signed_stream).encode() 25 | m = md5(key) 26 | base64hash = base64.b64encode(m.digest()) 27 | urlsignature = ("server_time=" + today + "&hash_value=" + base64hash.decode('utf-8') + "&validminutes=" + 28 | validminutes + "&strm_len=" + str(strm_len) + "&id=" + user_id + "&checkip=true").encode() 29 | base64urlsignature = base64.b64encode(urlsignature) 30 | signedurlwithvalidinterval = initial_url + "?wmsAuthSign=" + base64urlsignature.decode('utf-8') 31 | return signedurlwithvalidinterval 32 | 33 | 34 | def verify_ppv_signature(ppv_sync, token): 35 | """ 36 | @param ppv_sync: ppv sync JSON sent by wmspanel 37 | @param token: Secret Token configured into WMS Panel push API 38 | @return: True if calculated signature == sent signature. Else, return False 39 | """ 40 | try: 41 | sync_id = ppv_sync["ID"] 42 | puzzle = ppv_sync["Puzzle"] 43 | signature = ppv_sync["Signature"] 44 | except KeyError as error: 45 | raise KeyError(error) 46 | key = (sync_id + puzzle + token).encode() 47 | m = md5(key) 48 | base64hash = base64.b64encode(m.digest()).decode() 49 | if signature == base64hash: 50 | return True 51 | else: 52 | return False 53 | 54 | 55 | def generate_ppv_solution(puzzle, token): 56 | """ 57 | @param puzzle: 'Puzzle' value in JSON that is sent by nimble POST request 58 | @param token: Secret Token configured into WMS Panel push API 59 | @return: signature solution (string) 60 | """ 61 | key = (puzzle + token).encode() 62 | m = md5(key) 63 | base64hash = base64.b64encode(m.digest()).decode() 64 | return base64hash 65 | -------------------------------------------------------------------------------- /python/wmsauth.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding: utf8 3 | 4 | import base64 5 | from hashlib import md5 6 | from time import gmtime, strftime 7 | 8 | 9 | def get_media_url_with_signature(ip, password, validminutes, initial_url, signed_stream="", strm_len=0): 10 | """ 11 | Get URL with Hot-link protection signature 12 | :param ip: Viewer IP address 13 | :param password: hot-link protection password wonfigured in WMSPanel Auth Rule 14 | :param validminutes: Valid duration of the URL in minutes. After this time, the URL will not work 15 | :param initial_url: Full URL stream you want to protect 16 | :param signed_stream: stream name to make it unique for each stream 17 | https://blog.wmspanel.com/2014/10/stream-name-hotlink-protection.html 18 | You may also use part of the name if you need to protect streams by pattern ! 19 | :param strm_len: len(signed_stream) 20 | :return: Signed URL 21 | """ 22 | today = strftime("%m/%d/%Y %I:%M:%S %p", gmtime()) 23 | key = (ip + password + today + validminutes + signed_stream).encode() 24 | m = md5(key) 25 | base64hash = base64.b64encode(m.digest()) 26 | urlsignature = ("server_time=" + today + "&hash_value=" + base64hash.decode('utf-8') + "&validminutes=" + 27 | validminutes + "&strm_len=" + str(strm_len)).encode() 28 | base64urlsignature = base64.b64encode(urlsignature) 29 | signedurlwithvalidinterval = initial_url + "?wmsAuthSign=" + base64urlsignature.decode('utf-8') 30 | return signedurlwithvalidinterval 31 | 32 | --------------------------------------------------------------------------------