From f7d9c78d091e27991225dbc7e21f5877eefbbd11 Mon Sep 17 00:00:00 2001 From: knotteye Date: Mon, 25 Nov 2019 09:36:50 -0600 Subject: [PATCH 1/7] Move transcode config to mkdir callback --- src/server.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/server.ts b/src/server.ts index 90fdf88..c9a4635 100644 --- a/src/server.ts +++ b/src/server.ts @@ -73,14 +73,16 @@ function init (mediaconfig: any, satyrconfig: any) { execFile(satyrconfig.ffmpeg, ['-loglevel', 'fatal', '-analyzeduration', '0', '-i', 'rtmp://127.0.0.1:'+mediaconfig.rtmp.port+'/'+satyrconfig.privateEndpoint+'/'+key, '-vcodec', 'copy', '-acodec', 'copy', '-crf', '18', '-f', 'flv', 'rtmp://127.0.0.1:'+mediaconfig.rtmp.port+'/'+satyrconfig.publicEndpoint+'/'+results[0].username], {maxBuffer: Infinity}); //exec('ffmpeg -analyzeduration 0 -i rtmp://127.0.0.1:'+mediaconfig.rtmp.port+'/'+satyrconfig.privateEndpoint+'/'+key+' -vcodec copy -acodec copy -crf 18 -f flv rtmp://127.0.0.1:'+mediaconfig.rtmp.port+'/'+satyrconfig.publicEndpoint+'/'+results[0].username); //push to mpd after making sure directory exists - mkdir(satyrconfig.directory+'/'+satyrconfig.publicEndpoint+'/'+results[0].username, { recursive : true }, (err) => {;}); - while(true){ - if(session.audioCodec !== 0 && session.videoCodec !== 0){ - execFile(satyrconfig.ffmpeg, ['-loglevel', 'fatal', '-y', '-i', 'rtmp://127.0.0.1:'+mediaconfig.rtmp.port+'/'+satyrconfig.privateEndpoint+'/'+key, '-map', '0:2', '-map', '0:2', '-map', '0:2', '-map', '0:1', '-c:a', 'copy', '-c:v:0', 'copy', '-c:v:1', 'libx264', '-c:v:2', 'libx264', '-crf:1', '33', '-crf:2', '40', '-b:v:1', '3000K', '-b:v:2', '1500K', '-remove_at_exit', '1', '-seg_duration', '1', '-window_size', '30', '-f', 'dash', satyrconfig.directory+'/'+satyrconfig.publicEndpoint+'/'+results[0].username+'/index.mpd'], {maxBuffer: Infinity}); - break; + mkdir(satyrconfig.directory+'/'+satyrconfig.publicEndpoint+'/'+results[0].username, { recursive : true }, async (err) => { + if (err) throw err; + while(true){ + if(session.audioCodec !== 0 && session.videoCodec !== 0){ + execFile(satyrconfig.ffmpeg, ['-loglevel', 'fatal', '-y', '-i', 'rtmp://127.0.0.1:'+mediaconfig.rtmp.port+'/'+satyrconfig.privateEndpoint+'/'+key, '-map', '0:2', '-map', '0:2', '-map', '0:2', '-map', '0:1', '-c:a', 'copy', '-c:v:0', 'copy', '-c:v:1', 'libx264', '-c:v:2', 'libx264', '-crf:1', '33', '-crf:2', '40', '-b:v:1', '3000K', '-b:v:2', '1500K', '-remove_at_exit', '1', '-seg_duration', '1', '-window_size', '30', '-f', 'dash', satyrconfig.directory+'/'+satyrconfig.publicEndpoint+'/'+results[0].username+'/index.mpd'], {maxBuffer: Infinity}); + break; + } + await sleep(300); } - await sleep(300); - } + }); } else{ console.log('[NodeMediaServer] Invalid stream key for stream:',id); From cf71e663dec6a0775f7b5d74ebeac24ca376523f Mon Sep 17 00:00:00 2001 From: knotteye Date: Mon, 25 Nov 2019 11:38:12 -0600 Subject: [PATCH 2/7] Handle temporary data from database better instead of modifying njkconf in place. --- src/http.ts | 22 +++++++++------------- templates/user.njk | 6 +++--- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/src/http.ts b/src/http.ts index 65e9f52..c049fba 100644 --- a/src/http.ts +++ b/src/http.ts @@ -39,37 +39,33 @@ async function init(satyr: any, port: number, ircconf: any){ }); app.get('/users', (req, res) => { db.query('select username from users').then((result) => { - njkconf.list = result; - res.render('list.njk', njkconf); - njkconf.list = ''; + res.render('list.njk', Object.assign({list: result}, njkconf)); }); }); app.get('/users/live', (req, res) => { db.query('select username,title from user_meta where live=1;').then((result) => { - njkconf.list = result; - res.render('live.njk', njkconf); - njkconf.list = ''; + res.render('live.njk', Object.assign({list: result}, njkconf)); }); }); app.get('/users/*', (req, res) => { db.query('select username,title,about from user_meta where username='+db.raw.escape(req.url.split('/')[2].toLowerCase())).then((result) => { if(result[0]){ - njkconf.user = result[0].username; + /*njkconf.user = result[0].username; njkconf.streamtitle = result[0].title; - njkconf.about = result[0].about; - res.render('user.njk', njkconf); + njkconf.about = result[0].about;*/ + res.render('user.njk', Object.assign(result[0], njkconf)); } else res.render('404.njk', njkconf); }); }); app.get('/vods/*', (req, res) => { njkconf.user = req.url.split('/')[2].toLowerCase(); - db.query('select username from user_meta where username='+db.raw.escape(njkconf.user)).then((result) => { + db.query('select username from user_meta where username='+db.raw.escape(req.url.split('/')[2].toLowerCase())).then((result) => { if(result[0]){ fs.readdir('./site/live/'+njkconf.user, {withFileTypes: true} , (err, files) => { - if(files) njkconf.list = files.filter(fn => fn.name.endsWith('.mp4')); - else njkconf.list = []; - res.render('vods.njk', njkconf); + //if(files) njkconf.list = files.filter(fn => fn.name.endsWith('.mp4')); + //else njkconf.list = []; + res.render('vods.njk', Object.assign({user: result[0].username, list: files.filter(fn => fn.name.endsWith('.mp4'))}, njkconf)); }); } else res.render('404.njk', njkconf); diff --git a/templates/user.njk b/templates/user.njk index ed13dbe..540089c 100644 --- a/templates/user.njk +++ b/templates/user.njk @@ -7,13 +7,13 @@ function newPopup(url) { }
- {{ user }} | {{ streamtitle | escape }} Links | Watch Chat VODs + {{ username }} | {{ title | escape }} Links | Watch Chat VODs
- +
@@ -32,7 +32,7 @@ function newPopup(url) { document.querySelector(".vjs-modal-dialog-content").textContent = "The stream is currently offline."; }); player.src({ - src: '/live/{{ user }}/index.mpd', + src: '/live/{{ username }}/index.mpd', type: 'application/dash+xml' }); }) From d9b3333f216092fa184f508c226a0514c87713fc Mon Sep 17 00:00:00 2001 From: knotteye Date: Mon, 25 Nov 2019 12:55:55 -0600 Subject: [PATCH 3/7] Clean up commented out code Discard messages with only whitespace in socket.io --- src/http.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/http.ts b/src/http.ts index c049fba..2d2b5ff 100644 --- a/src/http.ts +++ b/src/http.ts @@ -50,21 +50,15 @@ async function init(satyr: any, port: number, ircconf: any){ app.get('/users/*', (req, res) => { db.query('select username,title,about from user_meta where username='+db.raw.escape(req.url.split('/')[2].toLowerCase())).then((result) => { if(result[0]){ - /*njkconf.user = result[0].username; - njkconf.streamtitle = result[0].title; - njkconf.about = result[0].about;*/ res.render('user.njk', Object.assign(result[0], njkconf)); } else res.render('404.njk', njkconf); }); }); app.get('/vods/*', (req, res) => { - njkconf.user = req.url.split('/')[2].toLowerCase(); db.query('select username from user_meta where username='+db.raw.escape(req.url.split('/')[2].toLowerCase())).then((result) => { if(result[0]){ fs.readdir('./site/live/'+njkconf.user, {withFileTypes: true} , (err, files) => { - //if(files) njkconf.list = files.filter(fn => fn.name.endsWith('.mp4')); - //else njkconf.list = []; res.render('vods.njk', Object.assign({user: result[0].username, list: files.filter(fn => fn.name.endsWith('.mp4'))}, njkconf)); }); } @@ -178,7 +172,7 @@ async function init(satyr: any, port: number, ircconf: any){ } }); socket.on('MSG', (data) => { - if(data.msg === "") return; + if(data.msg === "" || !data.msg.replace(/\s/g, '').length) return; io.to(data.room).emit('MSG', {nick: socket.nick, msg: data.msg}); if(ircconf.enable) irc.send(socket.nick, data.room, data.msg); }); @@ -200,7 +194,8 @@ async function init(satyr: any, port: number, ircconf: any){ } async function newNick(socket) { - let n: string = 'Guest'+Math.floor(Math.random() * Math.floor(100)); + //i just realized how shitty of an idea this is + let n: string = 'Guest'+Math.floor(Math.random() * Math.floor(1000)); if(store.get(n)) return newNick(socket); else { store.set(n, socket.id); From a521583e924b854d386a3f87af19c0cc6fd09fd8 Mon Sep 17 00:00:00 2001 From: knotteye Date: Wed, 27 Nov 2019 22:07:20 -0600 Subject: [PATCH 4/7] Change systemd service description --- install/satyr.service | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/satyr.service b/install/satyr.service index c5df3cc..8e535f8 100644 --- a/install/satyr.service +++ b/install/satyr.service @@ -1,5 +1,5 @@ [Unit] -Description=A livestreaming server. +Description=satyr livestreaming server After=network.target [Service] From daa2ec7a71d779ae130e2689c5e631e24f85271a Mon Sep 17 00:00:00 2001 From: knotteye Date: Wed, 27 Nov 2019 22:18:55 -0600 Subject: [PATCH 5/7] Refactor stream key handling Instead of redirecting with FFMPEG, change client streamPath to the privateEndpoint/StreamKey Hopefully this is silent, because if it isn't it's leaking the stream key to every client. --- src/server.ts | 53 +++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 41 insertions(+), 12 deletions(-) diff --git a/src/server.ts b/src/server.ts index c9a4635..d011e00 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,10 +1,13 @@ import * as NodeMediaServer from "node-media-server"; +import * as dirty from "dirty"; import { mkdir, fstat, access } from "fs"; import * as strf from "strftime"; import * as db from "./database"; const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); const { exec, execFile } = require('child_process'); +const keystore = dirty(); + function init (mediaconfig: any, satyrconfig: any) { const nms = new NodeMediaServer(mediaconfig); nms.run(); @@ -15,12 +18,12 @@ function init (mediaconfig: any, satyrconfig: any) { let app: string = StreamPath.split("/")[1]; let key: string = StreamPath.split("/")[2]; //disallow urls not formatted exactly right - if (StreamPath.split("/").length !== 3){ + if (StreamPath.split("/").length !== 3 || key.includes(' ')){ console.log("[NodeMediaServer] Malformed URL, closing connection for stream:",id); session.reject(); return false; } - if(app === satyrconfig.publicEndpoint) { + /*if(app === satyrconfig.publicEndpoint) { if(session.isLocal) { //only allow publish to public endpoint from localhost console.log("[NodeMediaServer] Local publish, stream:",`${id} ok.`); @@ -53,7 +56,7 @@ function init (mediaconfig: any, satyrconfig: any) { } return true; }); - } + }*/ if(app !== satyrconfig.privateEndpoint){ //app isn't at public endpoint if we've reached this point console.log("[NodeMediaServer] Wrong endpoint, rejecting stream:",id); @@ -63,16 +66,12 @@ function init (mediaconfig: any, satyrconfig: any) { //if the url is formatted correctly and the user is streaming to the correct private endpoint //grab the username from the database and redirect the stream there if the key is valid //otherwise kill the session - if(key.includes(' ')) { - session.reject(); - return false; - } - db.query('select username from users where stream_key='+db.raw.escape(key)+' limit 1').then(async (results) => { + db.query('select username,record_flag from users where stream_key='+db.raw.escape(key)+' limit 1').then(async (results) => { if(results[0]){ //push to rtmp - execFile(satyrconfig.ffmpeg, ['-loglevel', 'fatal', '-analyzeduration', '0', '-i', 'rtmp://127.0.0.1:'+mediaconfig.rtmp.port+'/'+satyrconfig.privateEndpoint+'/'+key, '-vcodec', 'copy', '-acodec', 'copy', '-crf', '18', '-f', 'flv', 'rtmp://127.0.0.1:'+mediaconfig.rtmp.port+'/'+satyrconfig.publicEndpoint+'/'+results[0].username], {maxBuffer: Infinity}); - //exec('ffmpeg -analyzeduration 0 -i rtmp://127.0.0.1:'+mediaconfig.rtmp.port+'/'+satyrconfig.privateEndpoint+'/'+key+' -vcodec copy -acodec copy -crf 18 -f flv rtmp://127.0.0.1:'+mediaconfig.rtmp.port+'/'+satyrconfig.publicEndpoint+'/'+results[0].username); + //execFile(satyrconfig.ffmpeg, ['-loglevel', 'fatal', '-analyzeduration', '0', '-i', 'rtmp://127.0.0.1:'+mediaconfig.rtmp.port+'/'+satyrconfig.privateEndpoint+'/'+key, '-vcodec', 'copy', '-acodec', 'copy', '-crf', '18', '-f', 'flv', 'rtmp://127.0.0.1:'+mediaconfig.rtmp.port+'/'+satyrconfig.publicEndpoint+'/'+results[0].username], {maxBuffer: Infinity}); //push to mpd after making sure directory exists + keystore[results[0].username] = key; mkdir(satyrconfig.directory+'/'+satyrconfig.publicEndpoint+'/'+results[0].username, { recursive : true }, async (err) => { if (err) throw err; while(true){ @@ -83,6 +82,24 @@ function init (mediaconfig: any, satyrconfig: any) { await sleep(300); } }); + if(results[0].record_flag && satyrconfig.record){ + console.log('[NodeMediaServer] Initiating recording for stream:',id); + mkdir(satyrconfig.directory+'/'+satyrconfig.publicEndpoint+'/'+results[0].username, { recursive : true }, (err) => { + if (err) throw err; + execFile(satyrconfig.ffmpeg, ['-loglevel', 'fatal', '-i', 'rtmp://127.0.0.1:'+mediaconfig.rtmp.port+'/'+satyrconfig.prviateEndpoint+'/'+key, '-vcodec', 'copy', '-acodec', 'copy', satyrconfig.directory+'/'+satyrconfig.publicEndpoint+'/'+results[0].username+'/'+strf('%d%b%Y-%H%M')+'.mp4'], { + detached : true, + stdio : 'inherit', + maxBuffer: Infinity + }).unref(); + //spawn an ffmpeg process to record the stream, then detach it completely + //ffmpeg can then (probably) finalize the recording if satyr crashes mid-stream + }); + } + else { + console.log('[NodeMediaServer] Skipping recording for stream:',id); + } + db.query('update user_meta set live=true where username=\''+results[0].username+'\' limit 1'); + console.log('[NodeMediaServer] Stream key ok for stream:',id); } else{ console.log('[NodeMediaServer] Invalid stream key for stream:',id); @@ -93,8 +110,12 @@ function init (mediaconfig: any, satyrconfig: any) { nms.on('donePublish', (id, StreamPath, args) => { let app: string = StreamPath.split("/")[1]; let key: string = StreamPath.split("/")[2]; - if(app === satyrconfig.publicEndpoint) { - db.query('update user_meta set live=false where username=\''+key+'\' limit 1'); + if(app === satyrconfig.privateEndpoint) { + db.query('update user_meta,users set user_meta.live=false where users.stream_key='+db.raw.escape(key)); + db.query('select username from users where stream_key='+db.raw.escape(key)+' limit 1').then(async (results) => { + keystore.rm(results[0].username); + }); + } }); nms.on('prePlay', (id, StreamPath, args) => { @@ -112,7 +133,15 @@ function init (mediaconfig: any, satyrconfig: any) { if(app === satyrconfig.privateEndpoint && !session.isLocal) { console.log("[NodeMediaServer] Non-local Play from private endpoint, rejecting client:",id); session.reject(); + return false; + } + if(app === satyrconfig.publicEndpoint) { + if(keystore[key]){ + session.playStreamPath = '/'+satyrconfig.privateEndpoint+'/'+keystore[key]; + return true; + } } + session.reject(); }); } export { init }; \ No newline at end of file From d4f92c33ff9ba818935ddbdc543bb733c702db70 Mon Sep 17 00:00:00 2001 From: knotteye Date: Thu, 28 Nov 2019 09:33:52 -0600 Subject: [PATCH 6/7] Update version to 0.4.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6e2ca0b..221094e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "satyr", - "version": "0.4.3", + "version": "0.4.4", "description": "A livestreaming server.", "license": "AGPL-3.0", "author": "knotteye", From 31426a0c4168a549a6191f59e4fcd2cd1dcfcad2 Mon Sep 17 00:00:00 2001 From: knotteye Date: Sat, 30 Nov 2019 14:15:31 -0600 Subject: [PATCH 7/7] Fix a bug where we treated local clients the same as foreign ones --- src/server.ts | 59 +++++++++++---------------------------------------- 1 file changed, 12 insertions(+), 47 deletions(-) diff --git a/src/server.ts b/src/server.ts index d011e00..a52d807 100644 --- a/src/server.ts +++ b/src/server.ts @@ -23,40 +23,6 @@ function init (mediaconfig: any, satyrconfig: any) { session.reject(); return false; } - /*if(app === satyrconfig.publicEndpoint) { - if(session.isLocal) { - //only allow publish to public endpoint from localhost - console.log("[NodeMediaServer] Local publish, stream:",`${id} ok.`); - } - else{ - console.log("[NodeMediaServer] Non-local Publish to public endpoint, rejecting stream:",id); - session.reject(); - return false; - } - console.log("[NodeMediaServer] Public endpoint, checking record flag."); - //set live flag - db.query('update user_meta set live=true where username=\''+key+'\' limit 1'); - //if this stream is from the public endpoint, check if we should be recording - return db.query('select username,record_flag from users where username=\''+key+'\' limit 1').then((results) => { - if(results[0].record_flag && satyrconfig.record){ - console.log('[NodeMediaServer] Initiating recording for stream:',id); - mkdir(satyrconfig.directory+'/'+satyrconfig.publicEndpoint+'/'+results[0].username, { recursive : true }, (err) => { - if (err) throw err; - execFile(satyrconfig.ffmpeg, ['-loglevel', 'fatal', '-i', 'rtmp://127.0.0.1:'+mediaconfig.rtmp.port+'/'+satyrconfig.publicEndpoint+'/'+results[0].username, '-vcodec', 'copy', '-acodec', 'copy', satyrconfig.directory+'/'+satyrconfig.publicEndpoint+'/'+results[0].username+'/'+strf('%d%b%Y-%H%M')+'.mp4'], { - detached : true, - stdio : 'inherit', - maxBuffer: Infinity - }).unref(); - //spawn an ffmpeg process to record the stream, then detach it completely - //ffmpeg can then (probably) finalize the recording if satyr crashes mid-stream - }); - } - else { - console.log('[NodeMediaServer] Skipping recording for stream:',id); - } - return true; - }); - }*/ if(app !== satyrconfig.privateEndpoint){ //app isn't at public endpoint if we've reached this point console.log("[NodeMediaServer] Wrong endpoint, rejecting stream:",id); @@ -72,16 +38,14 @@ function init (mediaconfig: any, satyrconfig: any) { //execFile(satyrconfig.ffmpeg, ['-loglevel', 'fatal', '-analyzeduration', '0', '-i', 'rtmp://127.0.0.1:'+mediaconfig.rtmp.port+'/'+satyrconfig.privateEndpoint+'/'+key, '-vcodec', 'copy', '-acodec', 'copy', '-crf', '18', '-f', 'flv', 'rtmp://127.0.0.1:'+mediaconfig.rtmp.port+'/'+satyrconfig.publicEndpoint+'/'+results[0].username], {maxBuffer: Infinity}); //push to mpd after making sure directory exists keystore[results[0].username] = key; - mkdir(satyrconfig.directory+'/'+satyrconfig.publicEndpoint+'/'+results[0].username, { recursive : true }, async (err) => { - if (err) throw err; - while(true){ - if(session.audioCodec !== 0 && session.videoCodec !== 0){ - execFile(satyrconfig.ffmpeg, ['-loglevel', 'fatal', '-y', '-i', 'rtmp://127.0.0.1:'+mediaconfig.rtmp.port+'/'+satyrconfig.privateEndpoint+'/'+key, '-map', '0:2', '-map', '0:2', '-map', '0:2', '-map', '0:1', '-c:a', 'copy', '-c:v:0', 'copy', '-c:v:1', 'libx264', '-c:v:2', 'libx264', '-crf:1', '33', '-crf:2', '40', '-b:v:1', '3000K', '-b:v:2', '1500K', '-remove_at_exit', '1', '-seg_duration', '1', '-window_size', '30', '-f', 'dash', satyrconfig.directory+'/'+satyrconfig.publicEndpoint+'/'+results[0].username+'/index.mpd'], {maxBuffer: Infinity}); - break; - } - await sleep(300); + mkdir(satyrconfig.directory+'/'+satyrconfig.publicEndpoint+'/'+results[0].username, { recursive : true }, ()=>{;}); + while(true){ + if(session.audioCodec !== 0 && session.videoCodec !== 0){ + execFile(satyrconfig.ffmpeg, ['-loglevel', 'fatal', '-y', '-i', 'rtmp://127.0.0.1:'+mediaconfig.rtmp.port+'/'+satyrconfig.privateEndpoint+'/'+key, '-map', '0:2', '-map', '0:2', '-map', '0:2', '-map', '0:1', '-c:a', 'copy', '-c:v:0', 'copy', '-c:v:1', 'libx264', '-c:v:2', 'libx264', '-crf:1', '33', '-crf:2', '40', '-b:v:1', '3000K', '-b:v:2', '1500K', '-remove_at_exit', '1', '-seg_duration', '1', '-window_size', '30', '-f', 'dash', satyrconfig.directory+'/'+satyrconfig.publicEndpoint+'/'+results[0].username+'/index.mpd'], {maxBuffer: Infinity}); + break; } - }); + await sleep(300); + } if(results[0].record_flag && satyrconfig.record){ console.log('[NodeMediaServer] Initiating recording for stream:',id); mkdir(satyrconfig.directory+'/'+satyrconfig.publicEndpoint+'/'+results[0].username, { recursive : true }, (err) => { @@ -128,20 +92,21 @@ function init (mediaconfig: any, satyrconfig: any) { session.reject(); return false; } - //disallow playing from the private endpoint for anyone except localhost - //(this will be the ffmpeg instance redirecting the stream) - if(app === satyrconfig.privateEndpoint && !session.isLocal) { + //localhost can play from whatever endpoint + //other clients must use private endpoint + if(app !== satyrconfig.publicEndpoint && !session.isLocal) { console.log("[NodeMediaServer] Non-local Play from private endpoint, rejecting client:",id); session.reject(); return false; } + //rewrite playpath to private endpoint serverside + //(hopefully) if(app === satyrconfig.publicEndpoint) { if(keystore[key]){ session.playStreamPath = '/'+satyrconfig.privateEndpoint+'/'+keystore[key]; return true; } } - session.reject(); }); } export { init }; \ No newline at end of file