diff --git a/config/default.toml b/config/default.toml index 9af96b5..250df3f 100644 --- a/config/default.toml +++ b/config/default.toml @@ -8,7 +8,8 @@ name = '' domain = '' registration = false webFormat = 'hls' -restrictedNames = ['live','stream'] +restrictedNames = ['live'] +rootredirect = '/users/live' [ircd] enable = false @@ -45,7 +46,6 @@ allow_origin = '*' directory = './site' [media] -streamKeys = false record = false publicEndpoint = 'live' privateEndpoint = 'stream' diff --git a/install/db_template.sql b/install/db_template.sql index 4393693..97c0f6d 100644 --- a/install/db_template.sql +++ b/install/db_template.sql @@ -7,5 +7,10 @@ CREATE TABLE users( password_hash BINARY(60), stream_key CHAR(20), record_flag TINYINT, - is_mod TINYINT +); +CREATE TABLE user_meta( + username VARCHAR(25), + title VARCHAR(120), + about VARCHAR(5000), + live TINYINT ); \ No newline at end of file diff --git a/install/setup.sh b/install/setup.sh index 58a64b4..6741d48 100644 --- a/install/setup.sh +++ b/install/setup.sh @@ -13,6 +13,8 @@ do echo "Please the domain name for your instance.[]" read domain done +echo "Enter the contact email for the instance." +read email echo "Please enter the path to the ffmpeg binary on your system.[$(which ffmpeg)]" read ffmpeg ffmpeg="${ffmpeg:=$(which ffmpeg)}" @@ -30,13 +32,13 @@ read dbhost dbhost="${dbhost:=localhost}" if [ "$dbhost" != "localhost" ] then -echo "Please enter the ip this server will connect to the database with.[*]" +echo "Please enter the public ip or hostname this server will connect to the database with.[*]" read dbclient dbclient="${dbclient:='*'}" else dbclient="localhost" fi -sed -e "s##$name#g" -e "s##$domain#g" -e "s##$ffmpeg#g" -e "s##$dbuser#g" -e "s##$dbname#g" -e "s##$dbpass#g" -e "s##$dbhost#g" install/template.local.toml > config/generated.toml +sed -e "s##$name#g" -e "s##$domain#g" -e "s##$ffmpeg#g" -e "s##$dbuser#g" -e "s##$dbname#g" -e "s##$dbpass#g" -e "s##$dbhost#g" -e "s##$email#g" install/template.local.toml > config/generated.toml sed -e "s##$dbuser#g" -e "s##$dbname#g" -e "s##$dbpass#g" -e "s##$dbhost#g" -e "s##$dbclient#g" install/db_template.sql > install/db_setup.sql echo "A setup script for the database has been generated at install/db_setup.sql. Please run it by connecting to your database software and executing 'source install/db_setup.sql;''" echo "A default configuration file has been generated at config/generated.toml" diff --git a/install/template.local.toml b/install/template.local.toml index d3bc491..92d6d27 100644 --- a/install/template.local.toml +++ b/install/template.local.toml @@ -1,10 +1,10 @@ [satyr] name = '' domain = '' +email = '' registration = false [media] -streamKeys = false record = false ffmpeg = '' diff --git a/site/logo.svg b/site/logo.svg new file mode 100644 index 0000000..5ed78f3 --- /dev/null +++ b/site/logo.svg @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +image/svg+xmlOpenclipartpan pipes2007-07-20T14:57:30I cannot find what the proper name of this instrument must be - any idea post here. For now I am calling it pan pipes but I don't think they have finger holes. This is from a concert brochure for the 54th Regt. Band from Library of Congresshttp://openclipart.org/detail/4518/pan-pipes-by-johnny_automaticjohnny_automaticclip artclipartexternalsourcefluteimageinstrumentlocmediamusicmusicalpipespngpublic domainsvg diff --git a/site/styles.css b/site/styles.css index e3b4b52..5ceb2b1 100644 --- a/site/styles.css +++ b/site/styles.css @@ -13,14 +13,18 @@ a { justify-content: center; flex-wrap: nowrap; flex-direction: column; - margin-top: 0px; + margin: 0 0 0 0; min-height: 100vh; } #header { text-align: center; - margin-top: 0px; - margin-bottom: 10px; + box-shadow: 0px 0px 7px rgb(14, 15, 17); + padding: 0px 10px 0px 10px; + margin-top: -10px; + margin-left: -8px; + margin-right: -8px; + margin-bottom: 0px; } #content { diff --git a/src/api.ts b/src/api.ts index 8d09c99..1565257 100644 --- a/src/api.ts +++ b/src/api.ts @@ -5,35 +5,44 @@ function init(conf: object){ config = conf; } -async function register(name: string, password: string, streamer: boolean) { - if(!config.registration){ - return {"error":"registration disabled"}; - } - else { - if(name.includes(';') || name.includes(' ')) return {"error":"illegal characters"}; - let s: boolean; - if(streamer && config.streamKeys) s = true; - else s = false; - let r: boolean = await db.addUser(name, password, s, false); - if(r) return {"success":""}; - else return {"error":""}; +async function register(name: string, password: string, confirm: string) { + if(!config.registration) return {"error":"registration disabled"}; + if(name.includes(';') || name.includes(' ') || name.includes('\'')) return {"error":"illegal characters"}; + if(password !== confirm) return {"error":"mismatched passwords"}; + for(let i=0;i { + db.addUser(flags.get('add'), flags.get('password')).then((result) => { if(result) console.log("User added successfully."); else console.log("Could not add user. Is the password field empty?"); process.exit(); @@ -28,20 +26,4 @@ if(flags.get('remove') !== ''){ else console.log("Could not remove user."); process.exit(); }); -} - -if(flags.get('mkstreamer') !== ''){ - db.addStreamKey(flags.get('mkstreamer')).then((result) => { - if(result) console.log("Key added successfully."); - else console.log("Could not add key."); - process.exit(); - }); -} - -if(flags.get('rmstreamer') !== ''){ - db.rmStreamKey(flags.get('rmstreamer')).then((result) => { - if(result) console.log("Key removed successfully."); - else console.log("Could not remove key."); - process.exit(); - }); } \ No newline at end of file diff --git a/src/controller.ts b/src/controller.ts index c727a04..4e52fe3 100644 --- a/src/controller.ts +++ b/src/controller.ts @@ -11,13 +11,14 @@ function run(): void{ const satyr: object = { privateEndpoint: config.media.privateEndpoint, record: config.media.record, - streamKeys: config.media.streamKeys, registration: config.satyr.registration, webFormat: config.satyr.webFormat, restrictedNames: config.satyr.restrictedNames, name: config.satyr.name, domain: config.satyr.domain, - email: config.satyr.email + email: config.satyr.email, + rootredirect: config.satyr.rootredirect, + version: process.env.npm_package_version }; const nms: object = { logType: config.server.logs, diff --git a/src/database.ts b/src/database.ts index 3271cb3..e69d70c 100644 --- a/src/database.ts +++ b/src/database.ts @@ -10,24 +10,23 @@ function init (db: object, bcrypt: object){ cryptoconfig = bcrypt; } -async function addUser(name: string, password: string, streamer: boolean, admin: boolean){ +async function addUser(name: string, password: string){ //does not respect registration setting in config - //nor stream keys if(password === '') return false; - let key: string = ' '; - if (streamer) key = await genKey(); + let key: string = await genKey(); let hash: string = await bcrypt.hash(password, cryptoconfig.saltRounds); - let dupe = await query('select * from users where username=\''+name+'\''); + let dupe = await query('select * from users where username='+raw.escape(name)); if(dupe[0]) return false; - let q: string = 'INSERT INTO users (username, password_hash, stream_key, record_flag, is_mod) VALUES (\''+name+'\', \''+hash+'\', \''+key+'\', 0, '+admin+')'; - await query(q); + await query('INSERT INTO users (username, password_hash, stream_key, record_flag) VALUES ('+raw.escape(name)+', '+raw.escape(hash)+', '+raw.escape(key)+', 0'); + await query('INSERT INTO user_meta (username, title, about, live) VALUES ('+raw.escape(name)+',\'\',\'\',false)'); return true; } async function rmUser(name: string){ - let exist = await query('select * from users where username=\''+name+'\''); + let exist = await query('select * from users where username='+raw.escape(name)); if(!exist[0]) return false; - await query('delete from users where username=\''+name+'\' limit 1'); + await query('delete from users where username='+raw.escape(name)+' limit 1'); + await query('delete from user_meta where username='+raw.escape(name)+' limit 1'); return true; } @@ -38,21 +37,6 @@ async function genKey(){ else return key; } -async function addStreamKey(name: string){ - let exist = await query('select * from users where username=\''+name+'\''); - if(!exist[0]) return false; - let key = await genKey(); - await query('update users set stream_key=\''+key+'\' where username=\''+name+'\' limit 1'); - return true; -} - -async function rmStreamKey(name: string){ - let exist = await query('select * from users where username=\''+name+'\''); - if(!exist[0]) return false; - await query('update users set stream_key=\'\' where username=\''+name+'\' limit 1'); - return true; -} - async function query(query: string){ return new Promise(resolve => raw.query(query, (error, results, fields) => { if(error) throw error; @@ -61,8 +45,12 @@ async function query(query: string){ } async function validatePassword(username: string, password: string){ - let pass: any= await query('select password from users where username=\''+username+'\' limit 1'); - return await bcrypt.compare(password, pass[0].password_hash); + let pass: any = await query('select password_hash from users where username='+raw.escape(username)+' limit 1'); + return await bcrypt.compare(password, pass[0].password_hash.toString()); +} + +async function hash(pwd){ + return await bcrypt.hash(pwd, cryptoconfig.saltRounds); } -export { query, raw, init, addUser, rmUser, addStreamKey, rmStreamKey, validatePassword }; \ No newline at end of file +export { query, raw, init, addUser, rmUser, validatePassword, hash, genKey }; \ No newline at end of file diff --git a/src/http.ts b/src/http.ts index 507692d..5f07be4 100644 --- a/src/http.ts +++ b/src/http.ts @@ -1,6 +1,7 @@ import * as express from "express"; import * as njk from "nunjucks"; import * as bodyparser from "body-parser"; +import * as fs from "fs"; import * as api from "./api"; import * as db from "./database"; @@ -18,30 +19,95 @@ function init(satyr: any){ sitename: satyr.name, domain: satyr.domain, email: satyr.email, - user: '', - streamtitle: '', + rootredirect: satyr.rootredirect, + version: satyr.version }; app.use(bodyparser.json()); app.use(bodyparser.urlencoded({ extended: true })); + //site handlers app.get('/', (req, res) => { - res.render('index.njk', njkconf); + res.redirect(njkconf.rootredirect); }); app.get('/about', (req, res) => { res.render('about.njk', njkconf); }); + app.get('/users', (req, res) => { + db.query('select username from users').then((result) => { + njkconf.list = result; + res.render('list.njk', njkconf); + njkconf.list = ''; + }); + }); + 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 = ''; + }); + }); app.get('/users/*', (req, res) => { njkconf.user = req.url.split('/')[2].toLowerCase(); - res.render('user.njk', njkconf); + db.query('select title,about from user_meta where username='+db.raw.escape(njkconf.user)).then((result) => { + if(result[0]){ + njkconf.streamtitle = result[0].title; + njkconf.about = result[0].about; + res.render('user.njk', njkconf); + } + else res.render('404.njk', njkconf); + }); }); - app.get('/registration', (req, res) => { + 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) => { + 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); + }); + } + else res.render('404.njk', njkconf); + }); + }); + app.get('/register', (req, res) => { res.render('registration.njk', njkconf); }); + app.get('/profile', (req, res) => { + res.render('profile.njk', njkconf); + }); + app.get('/changepwd', (req, res) => { + res.render('changepwd.njk', njkconf); + }); + app.get('/changesk', (req, res) => { + res.render('changesk.njk', njkconf); + }); + //api handlers app.post('/api/register', (req, res) => { - api.register(req.body.username, req.body.password, req.body.streamer).then( (result) => { - res.send({"error":""}); + api.register(req.body.username, req.body.password, req.body.confirm).then( (result) => { + res.send(result); + }); + }); + app.post('/api/user', (req, res) => { + api.update(req.body.username, req.body.password, req.body.title, req.body.bio, req.body.record).then((result) => { + res.send(result); }); }); + app.post('/api/user/password', (req, res) => { + api.changepwd(req.body.username, req.body.password, req.body.newpassword).then((result) => { + res.send(result); + }); + }); + app.post('/api/user/streamkey', (req, res) => { + api.changesk(req.body.username, req.body.password).then((result) => { + res.send(result); + }) + }); + //static files if nothing else matches first app.use(express.static('site')); + //404 Handler + app.use(function (req, res, next) { + res.status(404).render('404.njk', njkconf); + }); } export { init }; \ No newline at end of file diff --git a/src/server.ts b/src/server.ts index 2b1258f..cc20280 100644 --- a/src/server.ts +++ b/src/server.ts @@ -8,7 +8,7 @@ function init (mediaconfig: any, satyrconfig: any) { nms.run(); nms.on('postPublish', (id, StreamPath, args) => { - console.log("[NodeMediaServer] Prepublish Hook for stream:",id); + console.log("[NodeMediaServer] Publish Hook for stream:",id); let session = nms.getSession(id); let app: string = StreamPath.split("/")[1]; let key: string = StreamPath.split("/")[2]; @@ -31,6 +31,8 @@ function init (mediaconfig: any, satyrconfig: any) { 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){ @@ -61,7 +63,11 @@ 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 - db.query('select username from users where stream_key=\''+key+'\' limit 1').then((results) => { + if(key.includes(' ')) { + session.reject(); + return false; + } + db.query('select username from users where stream_key='+db.raw.escape(key)+' limit 1').then((results) => { if(results[0]){ 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+'/'+mediaconfig.trans.tasks[0].app+'/'+results[0].username); console.log('[NodeMediaServer] Stream key okay for stream:',id); @@ -72,6 +78,13 @@ 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 === mediaconfig.trans.tasks[0].app) { + db.query('update user_meta set live=false where username=\''+key+'\' limit 1'); + } + }); nms.on('prePlay', (id, StreamPath, args) => { let session = nms.getSession(id); let app: string = StreamPath.split("/")[1]; diff --git a/templates/404.njk b/templates/404.njk new file mode 100644 index 0000000..add9710 --- /dev/null +++ b/templates/404.njk @@ -0,0 +1,5 @@ +{% extends "base.njk" %} +{% block content %} +

+Couldn't find that page! +{% endblock %} \ No newline at end of file diff --git a/templates/about.html b/templates/about.html index f9ffa57..a298cb0 100644 --- a/templates/about.html +++ b/templates/about.html @@ -1 +1 @@ -

Add a description of your instance here!

\ No newline at end of file +

Add a description of your instance here!
You can find this file in templates/about.html

\ No newline at end of file diff --git a/templates/base.njk b/templates/base.njk index c751750..c9aad78 100644 --- a/templates/base.njk +++ b/templates/base.njk @@ -1,12 +1,14 @@ + + {{ sitename }}
{% block content %} @@ -17,10 +19,10 @@
- +
{{ sitename }}
diff --git a/templates/changepwd.njk b/templates/changepwd.njk new file mode 100644 index 0000000..aa29838 --- /dev/null +++ b/templates/changepwd.njk @@ -0,0 +1,12 @@ +{% extends "base.njk" %} +{% block content %} +

Change your password on {{ sitename }}

Not registered yet? Sign up here.
Update your profile or stream key.
+

+
+ Username:

+ Password:

+ New Password:

+ +
+ +{% endblock %} \ No newline at end of file diff --git a/templates/changesk.njk b/templates/changesk.njk new file mode 100644 index 0000000..d5e6dbc --- /dev/null +++ b/templates/changesk.njk @@ -0,0 +1,11 @@ +{% extends "base.njk" %} +{% block content %} +

Change your password on {{ sitename }}

Not registered yet? Sign up here.
Update your profile or password.
+

+
+ Username:

+ Password:

+ +
+ +{% endblock %} \ No newline at end of file diff --git a/templates/index.njk b/templates/index.njk deleted file mode 100644 index 61e86ce..0000000 --- a/templates/index.njk +++ /dev/null @@ -1,4 +0,0 @@ -{% extends "base.njk" %} -{% block content %} -What to put on index? -{% endblock %} \ No newline at end of file diff --git a/templates/list.njk b/templates/list.njk new file mode 100644 index 0000000..75e8f73 --- /dev/null +++ b/templates/list.njk @@ -0,0 +1,9 @@ +{% extends "base.njk" %} +{% block content %} +

Streaming on {{ sitename }}

+ {% asyncEach user in list%} + {{ user.username }}

+ {% else %} + No users found! + {% endeach %} +{% endblock %} \ No newline at end of file diff --git a/templates/live.njk b/templates/live.njk new file mode 100644 index 0000000..04bc2a9 --- /dev/null +++ b/templates/live.njk @@ -0,0 +1,9 @@ +{% extends "base.njk" %} +{% block content %} +

Currently live on {{ sitename }}

+ {% for user in list%} + {{ user.username | capitalize}} | {{user.title}}

+ {% else %} + No one is live! + {% endfor %} +{% endblock %} \ No newline at end of file diff --git a/templates/login.njk b/templates/login.njk deleted file mode 100644 index e69de29..0000000 diff --git a/templates/profile.njk b/templates/profile.njk index e69de29..465f061 100644 --- a/templates/profile.njk +++ b/templates/profile.njk @@ -0,0 +1,14 @@ +{% extends "base.njk" %} +{% block content %} +

Update your profile on {{ sitename }}

Not registered yet? Sign up here.
Change your password or stream key.
+

+
+ Username:

+ Password:

+ Stream Title:

+ Bio:

+ Record VODs:
+ +
+ +{% endblock %} \ No newline at end of file diff --git a/templates/registration.njk b/templates/registration.njk index f836c5a..2bdfe0c 100644 --- a/templates/registration.njk +++ b/templates/registration.njk @@ -2,12 +2,13 @@ {% block content %}
-
+ Username:

Password:

- Request Stream Key:
+ Confirm:

+
{% include "tos.html" %} diff --git a/templates/tos.html b/templates/tos.html index a9cb745..a13c391 100644 --- a/templates/tos.html +++ b/templates/tos.html @@ -1,2 +1,2 @@ -This is example terms of service!
+This is an example TOS!
You should change it by editing templates/tos.html \ No newline at end of file diff --git a/templates/user.njk b/templates/user.njk index 081341a..d8a12c4 100644 --- a/templates/user.njk +++ b/templates/user.njk @@ -1,6 +1,7 @@ {% extends "base.njk" %} {% block content %} - {{ user | capitalize }}'s StreamDirect Links: RTMP HLS +
+ {{ user }} | {{ streamtitle | escape }} Links | VODs
@@ -28,4 +29,6 @@ }); })
+ + {{ about | escape }} {% endblock %} \ No newline at end of file diff --git a/templates/vods.njk b/templates/vods.njk new file mode 100644 index 0000000..97c4aaf --- /dev/null +++ b/templates/vods.njk @@ -0,0 +1,9 @@ +{% extends "base.njk" %} +{% block content %} +

{{ user }}'s VODs

+ {% asyncEach vid in list%} + {{ vid.name }}

+ {% else %} + No recordings found! + {% endeach %} +{% endblock %} \ No newline at end of file