Merge branch 'develop' into 'master'

Develop -> Master

See merge request knotteye/satyr!6
merge-requests/7/merge 0.5.3
knotteye 5 years ago
commit 24a7d3dfbf
  1. 1
      .gitignore
  2. 9
      config/default.toml
  3. 4
      install/template.local.toml
  4. 49
      package-lock.json
  5. 4
      package.json
  6. 49
      site/dashjs/AUTHORS.md
  7. 14
      site/dashjs/LICENSE.md
  8. 3
      site/dashjs/dash.all.min.js
  9. 2
      site/videojs/videojs-dash.min.js
  10. 39
      src/api.ts
  11. 2
      src/controller.ts
  12. 8
      src/database.ts
  13. 360
      src/http.ts
  14. 2
      src/server.ts
  15. 2
      templates/base.njk
  16. 7
      templates/changepwd.njk
  17. 11
      templates/changesk.njk
  18. 9
      templates/chat.html
  19. 17
      templates/login.njk
  20. 13
      templates/profile.njk
  21. 20
      templates/registration.njk
  22. 16
      templates/user.njk

1
.gitignore vendored

@ -1,6 +1,7 @@
node_modules node_modules
site/live site/live
config/local.toml config/local.toml
config/jwt.pem
config/generated.toml config/generated.toml
install/db_setup.sql install/db_setup.sql
build/** build/**

@ -42,7 +42,7 @@ ping = 30
ping_timeout = 60 ping_timeout = 60
[server.http] [server.http]
allow_origin = '*' hsts = false
directory = './site' directory = './site'
port = 8000 port = 8000
@ -50,4 +50,9 @@ port = 8000
record = false record = false
publicEndpoint = 'live' publicEndpoint = 'live'
privateEndpoint = 'stream' privateEndpoint = 'stream'
ffmpeg = '' ffmpeg = ''
[transcode]
adapative = false
variants = 3
format = 'dash'

@ -8,6 +8,10 @@ registration = false
record = false record = false
ffmpeg = '<ffmpeg>' ffmpeg = '<ffmpeg>'
[server.http]
# uncomment to set HSTS when SSL is enabled
# hsts = true
[database] [database]
user = '<dbuser>' user = '<dbuser>'
password = '<dbpass>' password = '<dbpass>'

49
package-lock.json generated

@ -1,6 +1,6 @@
{ {
"name": "satyr", "name": "satyr",
"version": "0.4.3", "version": "0.4.4",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@ -121,6 +121,16 @@
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
"integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY="
}, },
"asn1.js": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.2.0.tgz",
"integrity": "sha512-Q7hnYGGNYbcmGrCPulXfkEw7oW7qjWeM4ZTALmgpuIcZLxyqqKYWxCZg2UBm8bklrnB4m2mGyJPWfoktdORD8A==",
"requires": {
"bn.js": "^4.0.0",
"inherits": "^2.0.1",
"minimalistic-assert": "^1.0.0"
}
},
"assign-symbols": { "assign-symbols": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz",
@ -257,6 +267,11 @@
"resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz", "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz",
"integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==" "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig=="
}, },
"bn.js": {
"version": "4.11.8",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
"integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA=="
},
"body-parser": { "body-parser": {
"version": "1.19.0", "version": "1.19.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
@ -507,6 +522,22 @@
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
"integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg=="
}, },
"cookie-parser": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.4.tgz",
"integrity": "sha512-lo13tqF3JEtFO7FyA49CqbhaFkskRJ0u/UAiINgrIXeRCY41c88/zxtrECl8AKH3B0hj9q10+h3Kt8I7KlW4tw==",
"requires": {
"cookie": "0.3.1",
"cookie-signature": "1.0.6"
},
"dependencies": {
"cookie": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
"integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s="
}
}
},
"cookie-signature": { "cookie-signature": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
@ -1837,6 +1868,14 @@
"integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
"optional": true "optional": true
}, },
"jose": {
"version": "1.15.1",
"resolved": "https://registry.npmjs.org/jose/-/jose-1.15.1.tgz",
"integrity": "sha512-Gp+53zIEb68qTuyagcalDMVn1s0WrxiGBQJbEjShOdv3CYmbPIJEAN0Qtn4rCa7XgODoEa7HHuz8GoYgIpIzog==",
"requires": {
"asn1.js": "^5.2.0"
}
},
"json5": { "json5": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
@ -1940,6 +1979,11 @@
"mime-db": "1.40.0" "mime-db": "1.40.0"
} }
}, },
"minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
},
"minimatch": { "minimatch": {
"version": "3.0.4", "version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
@ -2984,8 +3028,7 @@
"typescript": { "typescript": {
"version": "3.6.3", "version": "3.6.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.3.tgz",
"integrity": "sha512-N7bceJL1CtRQ2RiG0AQME13ksR7DiuQh/QehubYcghzv20tnh+MQnQIuJddTmsbqYj+dztchykemz0zFzlvdQw==", "integrity": "sha512-N7bceJL1CtRQ2RiG0AQME13ksR7DiuQh/QehubYcghzv20tnh+MQnQIuJddTmsbqYj+dztchykemz0zFzlvdQw=="
"dev": true
}, },
"union-value": { "union-value": {
"version": "1.0.1", "version": "1.0.1",

@ -1,6 +1,6 @@
{ {
"name": "satyr", "name": "satyr",
"version": "0.4.4", "version": "0.5.3",
"description": "A livestreaming server.", "description": "A livestreaming server.",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"author": "knotteye", "author": "knotteye",
@ -17,9 +17,11 @@
"bcrypt": "^3.0.6", "bcrypt": "^3.0.6",
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
"config": "^3.2.2", "config": "^3.2.2",
"cookie-parser": "^1.4.4",
"dirty": "^1.1.0", "dirty": "^1.1.0",
"express": "^4.17.1", "express": "^4.17.1",
"flags": "^0.1.3", "flags": "^0.1.3",
"jose": "^1.15.1",
"mysql": "^2.17.1", "mysql": "^2.17.1",
"node-media-server": ">=2.1.3 <3.0.0", "node-media-server": ">=2.1.3 <3.0.0",
"nunjucks": "^3.2.0", "nunjucks": "^3.2.0",

@ -1,49 +0,0 @@
# Dash.js Authors List
#####Please add entries to the bottom of the list in the following format
* @GitHub UserName (Required) [Name and/or Organization] (Optional)
#Authors
* @nweber [Digital Primates]
* @jefftapper [Jeff Tapper, Digital Primates]
* @KozhinM [Mikail Kozhin, Microsoft Open Technologies]
* @kirkshoop [Kirk Shoop, Microsoft Open Technologies]
* @wilaw [Will Law, Akamai Technologies]
* @AkamaiDASH [Dan Sparacio, Akamai Technologies]
* @dsilhavy [Daniel Silhavy, Fraunhofer Fokus]
* @greg80303 [Greg Rutz, CableLabs]
* @heff [Steve Hefferman, Brightcove]
* @Tomjohnson916 [Tom Johnson, Brightcove]
* @jeroenwiljering [Jeroen Wijering, JWPlayer]
* @bbcrddave [David Evans, BBC R&D]
* @bbert [Bertrand Berthelot, Orange]
* @vigneshvg [Vignesh Venkatasubramanian, Google]
* @nicosang [Nicolas Angot, Orange]
* @PriyadarshiniV
* @senthil-codr [Senthil]
* @dweremeichik [Dylan Weremeichik]
* @aleal-envivio
* @mconlin
* @umavinoth
* @lbonn
* @mdale [Michael Dale, Kaltura]
* @sgrebnov [Sergey Grebnov, Microsoft Open Technologies]
* @wesleytodd [Wes Todd, Vubeology]
* @colde [Loke Dupont, Xstream]
* @rgardler [Ross Gardler, Microsoft Open Technologies]
* @squapp
* @xiaomings
* @rcollins112 [Rich Collins, Wowza]
* @timothy003 [Timothy Liang]
* @JaapHaitsma
* @72lions [Thodoris Tsiridis, 72lions]
* @TobbeMobiTV [Torbjörn Einarsson, MobiTV]
* @TobbeEdgeware [Torbjörn Einarsson, Edgeware]
* @mstorsjo [Martin Storsjö]
* @Hyddan [Daniel Hedenius]
* @qjia7
* @waqarz
* @WMSPanel [WMSPanel Team]
* @matt-hammond-bbc [Matt Hammond, BBC R&D]
* @siropeich [Siro Blanco, Epic Labs]
* @epiclabsDASH [Jesus Oliva, Epic Labs]
* @adripanico [Adrian Caballero, Epic Labs]

@ -1,14 +0,0 @@
# dash.js BSD License Agreement
The copyright in this software is being made available under the BSD License, included below. This software may be subject to other third party and contributor rights, including patent rights, and no such rights are granted under this license.
**Copyright (c) 2015, Dash Industry Forum.
**All rights reserved.**
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
* Neither the name of the Dash Industry Forum nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
**THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.**

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1,4 +1,5 @@
import * as db from "./database" import * as db from "./database"
import { unregisterUser } from "./irc";
var config: any; var config: any;
function init(conf: object){ function init(conf: object){
@ -20,18 +21,26 @@ async function register(name: string, password: string, confirm: string) {
return {"error":""}; return {"error":""};
} }
async function update(name: string, password: string, title: string, bio: string, record: boolean){ async function update(fields: object){
if(!name || !password) return {"error":"Insufficient parameters"}; if(!fields['title'] && !fields['bio'] && (fields['rec'] !== 'true' && fields['rec'] !== 'false')) return {"error":"no valid fields specified"};
let auth: boolean = await db.validatePassword(name, password); let qs: string = "";
if(!auth) return {"error":"Username or Password Incorrect"}; let f: boolean = false;
await db.query('UPDATE user_meta set title='+db.raw.escape(title)+', about='+db.raw.escape(bio)+' where username='+db.raw.escape(name)); if(fields['title']) {qs += ' user_meta.title='+db.raw.escape(fields['title']);f = true;}
if(!record) await db.query('UPDATE users set record_flag=false where username='+db.raw.escape(name)); if(fields['bio']) {
else await db.query('UPDATE users set record_flag=true where username='+db.raw.escape(name)); if(f) qs+=',';
qs += ' user_meta.about='+db.raw.escape(fields['bio']);
f=true;
}
if(typeof(fields['rec']) === 'boolean' || typeof(fields['rec']) === 'number') {
if(f) qs+=',';
qs += ' users.record_flag='+db.raw.escape(fields['rec']);
}
await db.query('UPDATE users,user_meta SET'+qs+' WHERE users.username='+db.raw.escape(fields['name'])+' AND user_meta.username='+db.raw.escape(fields['name']));
return {"success":""}; return {"success":""};
} }
async function changepwd(name: string, password: string, newpwd: string){ async function changepwd(name: string, password: string, newpwd: string){
if(!name || !password) return {"error":"Insufficient parameters"}; if(!name || !password || !newpwd) return {"error":"Insufficient parameters"};
let auth: boolean = await db.validatePassword(name, password); let auth: boolean = await db.validatePassword(name, password);
if(!auth) return {"error":"Username or Password Incorrect"}; if(!auth) return {"error":"Username or Password Incorrect"};
let newhash: string = await db.hash(newpwd); let newhash: string = await db.hash(newpwd);
@ -39,13 +48,17 @@ async function changepwd(name: string, password: string, newpwd: string){
return {"success":""}; return {"success":""};
} }
async function changesk(name: string, password: string){ async function changesk(name: string){
if(!name || !password) return {"error":"Insufficient parameters"};
let auth: boolean = await db.validatePassword(name, password);
if(!auth) return {"error":"Username or Password Incorrect"};
let key: string = await db.genKey(); let key: string = await db.genKey();
await db.query('UPDATE users set stream_key='+db.raw.escape(key)+'where username='+db.raw.escape(name)+' limit 1'); await db.query('UPDATE users set stream_key='+db.raw.escape(key)+'where username='+db.raw.escape(name)+' limit 1');
return {"success":key}; return {"success":key};
} }
export { init, register, update, changepwd, changesk }; async function login(name: string, password: string){
if(!name || !password) return {"error":"Insufficient parameters"};
let auth: boolean = await db.validatePassword(name, password);
if(!auth) return {"error":"Username or Password Incorrect"};
return false;
}
export { init, register, update, changepwd, changesk, login };

@ -59,7 +59,7 @@ async function run() {
db.init(dbcfg, bcryptcfg); db.init(dbcfg, bcryptcfg);
await cleanup.init(); await cleanup.init();
api.init(satyr); api.init(satyr);
http.init(satyr, config.server.http.port, config.ircd); http.init(satyr, config.server.http, config.ircd);
mediaserver.init(nms, satyr); mediaserver.init(nms, satyr);
console.log(`Satyr v${process.env.npm_package_version} ready`); console.log(`Satyr v${process.env.npm_package_version} ready`);
} }

@ -45,8 +45,12 @@ async function query(query: string){
} }
async function validatePassword(username: string, password: string){ async function validatePassword(username: string, password: string){
let pass: any = await query('select password_hash from users where username='+raw.escape(username)+' limit 1'); try {
return await bcrypt.compare(password, pass[0].password_hash.toString()); 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());
} catch(e) {
return false;
}
} }
async function hash(pwd){ async function hash(pwd){

@ -1,25 +1,38 @@
import * as express from "express"; import * as express from "express";
import * as njk from "nunjucks"; import * as njk from "nunjucks";
import * as bodyparser from "body-parser"; import * as bodyparser from "body-parser";
import * as fs from "fs";
import * as socketio from "socket.io"; import * as socketio from "socket.io";
import * as http from "http"; import * as http from "http";
import * as cookies from "cookie-parser";
import * as dirty from "dirty"; import * as dirty from "dirty";
import * as api from "./api"; import * as api from "./api";
import * as db from "./database"; import * as db from "./database";
import * as irc from "./irc"; import * as irc from "./irc";
import { readdir, readFileSync, writeFileSync } from "fs";
import { JWT, JWK } from "jose";
import { strict } from "assert";
import { parse } from "path";
import { isBuffer } from "util";
const app = express(); const app = express();
const server = http.createServer(app); const server = http.createServer(app);
const io = socketio(server); const io = socketio(server);
const store = dirty(); const store = dirty();
var jwkey;
try{
jwkey = JWK.asKey(readFileSync('./config/jwt.pem'));
} catch (e) {
console.log("No key found for JWT signing, generating one now.");
jwkey = JWK.generateSync('RSA', 2048, { use: 'sig' });
writeFileSync('./config/jwt.pem', jwkey.toPEM(true));
}
var njkconf; var njkconf;
async function init(satyr: any, port: number, ircconf: any){ async function init(satyr: any, http: object, ircconf: any){
njk.configure('templates', { njk.configure('templates', {
autoescape: true, autoescape : true,
express : app, express : app,
watch: false watch : false
}); });
njkconf ={ njkconf ={
sitename: satyr.name, sitename: satyr.name,
@ -28,88 +41,294 @@ async function init(satyr: any, port: number, ircconf: any){
rootredirect: satyr.rootredirect, rootredirect: satyr.rootredirect,
version: satyr.version version: satyr.version
}; };
app.use(cookies());
app.use(bodyparser.json()); app.use(bodyparser.json());
app.use(bodyparser.urlencoded({ extended: true })); app.use(bodyparser.urlencoded({ extended: true }));
if(http['hsts']){
app.use((req, res, next) => {
res.append('Strict-Transport-Security', 'max-age=5184000');
next();
});
}
app.disable('x-powered-by');
//site handlers //site handlers
await initSite(satyr.registration);
//api handlers
await initAPI();
//static files if nothing else matches first
app.use(express.static(satyr.directory));
//404 Handler
app.use(function (req, res, next) {
if(tryDecode(req.cookies.Authorization)) {
res.status(404).render('404.njk', Object.assign({auth: {is: true, name: JWT.decode(req.cookies.Authorization)['username']}}, njkconf));
}
else res.status(404).render('404.njk', njkconf);
//res.status(404).render('404.njk', njkconf);
});
await initChat(ircconf);
server.listen(http['port']);
}
async function newNick(socket, skip?: boolean) {
if(socket.handshake.headers['cookie'] && !skip){
let c = await parseCookie(socket.handshake.headers['cookie']);
let t = await validToken(c['Authorization']);
if(t) return t['username'];
}
//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, true);
else {
store.set(n, socket.id);
return n;
}
}
async function chgNick(socket, nick, f?: boolean) {
let rooms = Object.keys(socket.rooms);
for(let i=1;i<rooms.length;i++){
io.to(rooms[i]).emit('ALERT', socket.nick+' is now known as '+nick);
}
if(store.get(socket.nick)) store.rm(socket.nick);
if (!f) store.set(nick, socket.id);
socket.nick = nick;
}
async function genToken(u: string){
return await JWT.sign({
username: u
}, jwkey, {
expiresIn: '1 week',
iat: true,
kid: false
});//set jwt
}
async function validToken(t: any){
try {
let token = JWT.verify(t, jwkey);
return token;
}
catch (err){
return false;
}
}
function tryDecode(t: any){
try {
return JWT.decode(t);
}
catch (err){
return false;
}
}
async function parseCookie(c){
if(typeof(c) !== 'string' || !c.includes('=')) return {};
return Object.assign({[c.split('=')[0].trim()]:c.split('=')[1].split(';')[0].trim()}, await parseCookie(c.split(/;(.+)/)[1]));
}
async function initAPI() {
app.get('/api/users/live', (req, res) => {
db.query('select username,title from user_meta where live=1 limit 10;').then((result) => {
res.send(result);
});
});
app.get('/api/users/live/:num', (req, res) => {
if(req.params.num > 50) req.params.num = 50;
db.query('select username,title from user_meta where live=1 limit '+req.params.num+';').then((result) => {
res.send(result);
});
});
app.post('/api/register', (req, res) => {
api.register(req.body.username, req.body.password, req.body.confirm).then( (result) => {
if(result[0]) return genToken(req.body.username).then((t) => {
res.cookie('Authorization', t, {maxAge: 604800000, httpOnly: true, sameSite: 'Lax'});
res.send(result);
return;
});
res.send(result);
});
});
app.post('/api/user/update', (req, res) => {
validToken(req.cookies.Authorization).then((t) => {
if(t) {
return api.update({name: t['username'],
title: "title" in req.body ? req.body.title : false,
bio: "bio" in req.body ? req.body.bio : false,
rec: "record" in req.body ? req.body.record : "NA"
}).then((r) => {
res.send(r);
return;
});
}
else {
res.send('{"error":"invalid token"}');
return;
}
});
/*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) => {
validToken(req.cookies.Authorization).then((t) => {
if(t) {
return api.changepwd(t['username'], req.body.password, req.body.newpassword).then((r) => {
res.send(r);
return;
});
}
else {
res.send('{"error":"invalid token"}');
return;
}
});
});
app.post('/api/user/streamkey', (req, res) => {
validToken(req.cookies.Authorization).then((t) => {
if(t) {
api.changesk(t['username']).then((r) => {
res.send(r);
});
}
else {
res.send('{"error":"invalid token"}');
}
});
});
app.post('/api/login', (req, res) => {
if(req.cookies.Authorization) validToken(req.cookies.Authorization).then((t) => {
if(t) {
if(t['exp'] - 86400 < Math.floor(Date.now() / 1000)){
return genToken(t['username']).then((t) => {
res.cookie('Authorization', t, {maxAge: 604800000, httpOnly: true, sameSite: 'Lax'});
res.send('{"success":""}');
return;
});
}
else {
res.send('{"success":"already verified"}');
return;
}
}
else {
res.send('{"error":"invalid token"}');
return;
}
});
else {
api.login(req.body.username, req.body.password).then((result) => {
if(!result){
genToken(req.body.username).then((t) => {
res.cookie('Authorization', t, {maxAge: 604800000, httpOnly: true, sameSite: 'Lax'});
res.send('{"success":""}');
})
}
else {
res.send(result);
}
});
}
})
}
async function initSite(openReg) {
app.get('/', (req, res) => { app.get('/', (req, res) => {
res.redirect(njkconf.rootredirect); res.redirect(njkconf.rootredirect);
}); });
app.get('/about', (req, res) => { app.get('/about', (req, res) => {
res.render('about.njk', njkconf); if(tryDecode(req.cookies.Authorization)) {
res.render('about.njk', Object.assign({auth: {is: true, name: JWT.decode(req.cookies.Authorization)['username']}}, njkconf));
}
else res.render('about.njk',njkconf);
}); });
app.get('/users', (req, res) => { app.get('/users', (req, res) => {
db.query('select username from users').then((result) => { db.query('select username from users').then((result) => {
res.render('list.njk', Object.assign({list: result}, njkconf)); if(tryDecode(req.cookies.Authorization)) {
res.render('list.njk', Object.assign({list: result}, {auth: {is: true, name: JWT.decode(req.cookies.Authorization)['username']}}, njkconf));
}
else res.render('list.njk', Object.assign({list: result}, njkconf));
//res.render('list.njk', Object.assign({list: result}, njkconf));
}); });
}); });
app.get('/users/live', (req, res) => { app.get('/users/live', (req, res) => {
db.query('select username,title from user_meta where live=1;').then((result) => { db.query('select username,title from user_meta where live=1;').then((result) => {
res.render('live.njk', Object.assign({list: result}, njkconf)); if(tryDecode(req.cookies.Authorization)) {
res.render('live.njk', Object.assign({list: result}, {auth: {is: true, name: JWT.decode(req.cookies.Authorization)['username']}}, njkconf));
}
else res.render('live.njk', Object.assign({list: result}, njkconf));
//res.render('live.njk', Object.assign({list: result}, njkconf));
}); });
}); });
app.get('/users/*', (req, res) => { app.get('/users/:user', (req, res) => {
db.query('select username,title,about from user_meta where username='+db.raw.escape(req.url.split('/')[2].toLowerCase())).then((result) => { db.query('select username,title,about from user_meta where username='+db.raw.escape(req.params.user)).then((result) => {
if(result[0]){ if(result[0]){
res.render('user.njk', Object.assign(result[0], njkconf)); if(tryDecode(req.cookies.Authorization)) {
res.render('user.njk', Object.assign(result[0], {auth: {is: true, name: JWT.decode(req.cookies.Authorization)['username']}}, njkconf));
}
else res.render('user.njk', Object.assign(result[0], njkconf));
//res.render('user.njk', Object.assign(result[0], njkconf));
} }
else res.render('404.njk', njkconf); else if(tryDecode(req.cookies.Authorization)) {
res.status(404).render('404.njk', Object.assign({auth: {is: true, name: JWT.decode(req.cookies.Authorization)['username']}}, njkconf));
}
else res.status(404).render('404.njk', njkconf);
}); });
}); });
app.get('/vods/*', (req, res) => { app.get('/vods/:user', (req, res) => {
db.query('select username from user_meta where username='+db.raw.escape(req.url.split('/')[2].toLowerCase())).then((result) => { db.query('select username from user_meta where username='+db.raw.escape(req.params.user)).then((result) => {
if(result[0]){ if(result[0]){
fs.readdir('./site/live/'+njkconf.user, {withFileTypes: true} , (err, files) => { readdir('./site/live/'+result[0].username, {withFileTypes: true} , (err, files) => {
res.render('vods.njk', Object.assign({user: result[0].username, list: files.filter(fn => fn.name.endsWith('.mp4'))}, njkconf)); if(tryDecode(req.cookies.Authorization)) {
res.render('vods.njk', Object.assign({user: result[0].username, list: files.filter(fn => fn.name.endsWith('.mp4'))}, {auth: {is: true, name: JWT.decode(req.cookies.Authorization)['username']}}, njkconf));
}
else res.render('vods.njk', Object.assign({user: result[0].username, list: files.filter(fn => fn.name.endsWith('.mp4'))}, njkconf));
//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); else if(tryDecode(req.cookies.Authorization)) {
res.status(404).render('404.njk', Object.assign({auth: {is: true, name: JWT.decode(req.cookies.Authorization)['username']}}, njkconf));
}
else res.status(404).render('404.njk', njkconf);
}); });
}); });
app.get('/login', (req, res) => {
if(tryDecode(req.cookies.Authorization)) {
res.redirect('/profile');
}
else res.render('login.njk',njkconf);
});
app.get('/register', (req, res) => { app.get('/register', (req, res) => {
res.render('registration.njk', njkconf); if(tryDecode(req.cookies.Authorization) || !openReg) {
res.redirect(njkconf.rootredirect);
}
else res.render('registration.njk',njkconf);
}); });
app.get('/profile', (req, res) => { app.get('/profile', (req, res) => {
res.render('profile.njk', njkconf); if(tryDecode(req.cookies.Authorization)) {
res.render('profile.njk', Object.assign({auth: {is: true, name: JWT.decode(req.cookies.Authorization)['username']}}, njkconf));
}
else res.redirect('/login');
}); });
app.get('/changepwd', (req, res) => { app.get('/changepwd', (req, res) => {
res.render('changepwd.njk', njkconf); if(tryDecode(req.cookies.Authorization)) {
}); res.render('changepwd.njk', Object.assign({auth: {is: true, name: JWT.decode(req.cookies.Authorization)['username']}}, njkconf));
app.get('/changesk', (req, res) => { }
res.render('changesk.njk', njkconf); else res.redirect('/login');
}); });
app.get('/chat', (req, res) => { app.get('/chat', (req, res) => {
res.render('chat.html', njkconf); res.render('chat.html', njkconf);
}); });
app.get('/help', (req, res) => { app.get('/help', (req, res) => {
res.render('help.njk', njkconf); if(tryDecode(req.cookies.Authorization)) {
}); res.render('help.njk', Object.assign({auth: {is: true, name: JWT.decode(req.cookies.Authorization)['username']}}, njkconf));
//api handlers }
app.post('/api/register', (req, res) => { else res.render('help.njk',njkconf);
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);
}); });
}
async function initChat(ircconf: any) {
//irc peering //irc peering
if(ircconf.enable){ if(ircconf.enable){
await irc.connect({ await irc.connect({
@ -136,6 +355,21 @@ async function init(satyr: any, port: number, ircconf: any){
} }
else socket.emit('ALERT', 'Room does not exist'); else socket.emit('ALERT', 'Room does not exist');
}); });
socket.on('LIST', (data) => {
let str = "";
let client;
io.in(data.room).clients((err, clients) => {
if(err) throw err;
if(clients === []) {
socket.emit('LIST', 'The room is empty.');
return;
}
for(let i=0;i<clients.length;i++) {
str += io.sockets.connected[clients[i]].nick+', ';
}
socket.emit('LIST', str.substring(0, str.length - 2));
});
});
socket.on('LEAVEROOM', (data) => { socket.on('LEAVEROOM', (data) => {
socket.leave(data); socket.leave(data);
if(ircconf.enable) irc.part(socket.nick, data); if(ircconf.enable) irc.part(socket.nick, data);
@ -163,7 +397,7 @@ async function init(satyr: any, port: number, ircconf: any){
return false; return false;
} }
if(await db.validatePassword(data.nick, data.password)){ if(await db.validatePassword(data.nick, data.password)){
chgNick(socket, data.nick); chgNick(socket, data.nick, true);
} }
else socket.emit('ALERT','Incorrect username or password'); else socket.emit('ALERT','Incorrect username or password');
} }
@ -190,26 +424,6 @@ async function init(satyr: any, port: number, ircconf: any){
else socket.emit('ALERT', 'Not authorized to do that.'); else socket.emit('ALERT', 'Not authorized to do that.');
}); });
}); });
server.listen(port);
}
async function newNick(socket) {
//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);
return n;
}
}
async function chgNick(socket, nick) {
let rooms = Object.keys(socket.rooms);
for(let i=1;i<rooms.length;i++){
io.to(rooms[i]).emit('ALERT', socket.nick+' is now known as '+nick);
}
store.rm(socket.nick);
store.set(nick, socket.id);
socket.nick = nick;
} }
export { init }; export { init };

@ -77,7 +77,7 @@ function init (mediaconfig: any, satyrconfig: any) {
if(app === satyrconfig.privateEndpoint) { 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('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) => { db.query('select username from users where stream_key='+db.raw.escape(key)+' limit 1').then(async (results) => {
keystore.rm(results[0].username); if(results[0]) keystore.rm(results[0].username);
}); });
} }

@ -8,7 +8,7 @@
<body> <body>
<div id="wrapper"> <div id="wrapper">
<div id="header"> <div id="header">
<span style="float:left;"><h4><a href="/">{{ sitename }}</a> | <a href="/users">Users</a> <a href="/users/live">Live</a> <a href="/about">About</a></h4></span><span style="float:right;"><h4><a href="/help">Help</a> | <a href="/profile">Profile</a></h4></span> <span style="float:left;"><h4><a href="/">{{ sitename }}</a> | <a href="/users">Users</a> <a href="/users/live">Live</a> <a href="/about">About</a></h4></span><span style="float:right;"><h4><a href="/help">Help</a> | {% if auth.is %}<a href="/profile">{{ auth.name }}{% else %}<a href="/login">Log In{% endif %}</a></h4></span>
</div> </div>
<div id="content"> <div id="content">
{% block content %} {% block content %}

@ -1,11 +1,10 @@
{% extends "base.njk" %} {% extends "base.njk" %}
{% block content %} {% block content %}
<h3>Change your password on {{ sitename }}</h3><span style="font-size: small;">Not registered yet? Sign up <a href="/register">here</a>.</br> Update your <a href="/profile">profile</a> or <a href="/changesk">stream key</a>.</span> <h3>Change your password on {{ sitename }}</h3>
<p></p> <p></p>
<form action="/api/user/password" method="POST" target="responseFrame"> <form action="/api/user/password" method="POST" target="responseFrame">
Username: </br><input type="text" name="username" style="min-width: 300px" placeholder="e.g. lain"/></br> Old Password: </br><input type="password" name="password" style="min-width: 300px"/></br>
Password: </br><input type="password" name="password" style="min-width: 300px"/></br> New Password: </br><input type="password" name="newpassword" style="min-width: 300px"/></br></br>
New Password: </br><input type="password" name="newpassword" style="min-width: 300px"/></br>
<input type="submit" value="Submit"> <input type="submit" value="Submit">
</form> </form>
<iframe name="responseFrame" border="0" frameborder="0" style="display: inline;"></iframe> <iframe name="responseFrame" border="0" frameborder="0" style="display: inline;"></iframe>

@ -1,11 +0,0 @@
{% extends "base.njk" %}
{% block content %}
<h3>Get a new stream key on {{ sitename }}</h3><span style="font-size: small;">Not registered yet? Sign up <a href="/register">here</a>.</br> Update your <a href="/profile">profile</a> or <a href="/changepwd">password</a>.</span>
<p></p>
<form action="/api/user/streamkey" method="POST" target="responseFrame">
Username: </br><input type="text" name="username" style="min-width: 300px" placeholder="e.g. lain"/></br>
Password: </br><input type="password" name="password" style="min-width: 300px"/></br>
<input type="submit" value="Submit">
</form>
<iframe name="responseFrame" border="0" frameborder="0" style="display: inline;"></iframe>
{% endblock %}

@ -33,6 +33,9 @@
else if(m.startsWith('/kick')){ else if(m.startsWith('/kick')){
socket.emit('KICK', {nick: m.split(' ')[1], room: room}); socket.emit('KICK', {nick: m.split(' ')[1], room: room});
} }
else if(m.startsWith('/list')){
socket.emit('LIST', {room: room});
}
else socket.emit('MSG', {room: room, msg: m}); else socket.emit('MSG', {room: room, msg: m});
document.getElementById('m').value = ''; document.getElementById('m').value = '';
} }
@ -52,6 +55,10 @@
document.getElementById('messages').innerHTML+='<li><i>'+data.nick+' has left the chat</i></li>'; document.getElementById('messages').innerHTML+='<li><i>'+data.nick+' has left the chat</i></li>';
window.scrollTo(0,document.body.scrollHeight); window.scrollTo(0,document.body.scrollHeight);
}); });
socket.on('LIST', function(data){
document.getElementById('messages').innerHTML+='<li><i>'+data+'</i></li>';
window.scrollTo(0,document.body.scrollHeight);
});
function getUrlParameter(name) { function getUrlParameter(name) {
name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]'); name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
var regex = new RegExp('[\\?&]' + name + '=([^&#]*)'); var regex = new RegExp('[\\?&]' + name + '=([^&#]*)');
@ -71,7 +78,7 @@
</head> </head>
<body> <body>
<div id="wrapper"> <div id="wrapper">
<ul id="messages"></ul><li>&nbsp;</li><li>&nbsp;</li> <ul id="messages"></ul><li style="list-style-type: none; margin: 0; padding: 5px 10px;">&nbsp;</li><li style="list-style-type: none; margin: 0; padding: 5px 10px;">&nbsp;</li>
</div> </div>
<form id="f" action="" onSubmit="send(); return false"> <form id="f" action="" onSubmit="send(); return false">
<input id="m" autocomplete="off" /><button><img src="/send.png" alt="send" width="15px" style="display: inline-block;"></img></button> <input id="m" autocomplete="off" /><button><img src="/send.png" alt="send" width="15px" style="display: inline-block;"></img></button>

@ -0,0 +1,17 @@
{% extends "base.njk" %}
{% block content %}
<h3>Log in to {{ sitename }}</h3><span style="font-size: small;">Not registered yet? Sign up <a href="/register">here</a>.</br></br></span>
<form action="/api/login" method="POST" target="responseFrame">
Username: </br><input type="text" name="username" style="min-width: 300px" placeholder="e.g. lain"/></br>
Password: </br><input type="password" name="password" style="min-width: 300px"/></br></br>
<input type="submit" value="Submit">
</form>
<script type="text/javascript">
function handleLoad() {
var r = JSON.parse(document.getElementById('responseFrame').contentDocument.documentElement.textContent).success
if (typeof(r) !== 'undefined') window.location.href = '/profile'
}
</script>
<iframe name="responseFrame" onload="handleLoad()" border="0" frameborder="0" style="display: inline;" id="responseFrame">
</iframe>
{% endblock %}

@ -1,14 +1,15 @@
{% extends "base.njk" %} {% extends "base.njk" %}
{% block content %} {% block content %}
<h3>Update your profile on {{ sitename }}</h3><span style="font-size: small;">Not registered yet? Sign up <a href="/register">here</a>.</br> Change your <a href="/changepwd">password</a> or <a href="/changesk">stream key</a>.</span> <h3>Update your profile on {{ sitename }}</h3><span style="font-size: small;">Or, change your <a href="/changepwd">password</a>.</span>
<p></p> <p></p>
<form action="/api/user" method="POST" target="responseFrame"> <form action="/api/user/update" method="POST" target="responseFrame">
Username: </br><input type="text" name="username" style="min-width: 300px" placeholder="e.g. lain"/></br>
Password: </br><input type="password" name="password" style="min-width: 300px"/></br>
Stream Title: </br><input type="text" name="title" style="min-width: 300px"/></br> Stream Title: </br><input type="text" name="title" style="min-width: 300px"/></br>
Bio: </br><input type="text" name="bio" style="min-width: 300px; min-height: 150px;"/></br> Bio: </br><input type="text" name="bio" style="min-width: 300px; min-height: 150px;"/></br>
Record VODs: <input type="checkbox" name="record" value="true"></br> Record VODs: <input type="radio" name="record" value="true"> Yes<input type="radio" name="record" value="false" /> No</br></br>
<input type="submit" value="Submit"> <input type="submit" value="Update Profile">
</form></br>
<form action="/api/user/streamkey" method="POST" target="responseFrame">
<input type="submit" value="Request New Stream Key">
</form> </form>
<iframe name="responseFrame" border="0" frameborder="0" style="display: inline;"></iframe> <iframe name="responseFrame" border="0" frameborder="0" style="display: inline;"></iframe>
{% endblock %} {% endblock %}

@ -1,17 +1,19 @@
{% extends "base.njk" %} {% extends "base.njk" %}
{% block content %} {% block content %}
<div id="jscontainer" style="height: 100%;"> <h3>Register on {{ sitename }}</h3><span style="font-size: small;">Already registered? Log in <a href="/login">here</a>.</br></br></span>
<div id="jschild" style="width: 50%;height: 100%;text-align: left;margin: 20px;"> <!--<div id="jscontainer" style="height: 100%;">
<div id="jschild" style="width: 50%;height: 100%;text-align: left;margin: 20px;">-->
<form action="/api/register" method="POST" target="responseFrame"> <form action="/api/register" method="POST" target="responseFrame">
Username: </br><input type="text" name="username" style="min-width: 300px" placeholder="e.g. lain"/></br> Username: </br><input type="text" name="username" style="min-width: 300px" placeholder="e.g. lain"/></br>
Password: </br><input type="password" name="password" style="min-width: 300px"/></br> Password: </br><input type="password" name="password" style="min-width: 300px"/></br>
Confirm: </br><input type="password" name="confirm" style="min-width: 300px"/></br> Confirm: </br><input type="password" name="confirm" style="min-width: 300px"/></br></br>
<input type="submit" value="Submit"> <input type="submit" value="Submit">
</form> </form></br>
<!--</div>
<div id="jschild" style="width: 50%;height: 100%;text-align: left;margin: 20px;">-->
{% include "tos.html" %}</br>
<iframe name="responseFrame" border="0" frameborder="0" style="display: inline;"></iframe> <iframe name="responseFrame" border="0" frameborder="0" style="display: inline;"></iframe>
</div> <!--</div>
<div id="jschild" style="width: 50%;height: 100%;text-align: left;margin: 20px;"> </div>-->
{% include "tos.html" %}
</div>
</div>
{% endblock %} {% endblock %}

@ -10,18 +10,16 @@ function newPopup(url) {
<span style="float: left;font-size: large;"><a href="/live/{{ username }}/index.mpd">{{ username }}</a> | {{ title | escape }}</b></span><span style="float: right;font-size: large;"> Links | <a href="rtmp://{{ domain }}/live/{{ username }}">Watch</a> <a href="JavaScript:newPopup('/chat?room={{ username }}');">Chat</a> <a href="/vods/{{ username }}">VODs</a></span> <span style="float: left;font-size: large;"><a href="/live/{{ username }}/index.mpd">{{ username }}</a> | {{ title | escape }}</b></span><span style="float: right;font-size: large;"> Links | <a href="rtmp://{{ domain }}/live/{{ username }}">Watch</a> <a href="JavaScript:newPopup('/chat?room={{ username }}');">Chat</a> <a href="/vods/{{ username }}">VODs</a></span>
<div id="jscontainer"> <div id="jscontainer">
<div id="jschild" style="width: 70%;height: 100%;"> <div id="jschild" style="width: 70%;height: 100%;">
<video controls poster="/thumbnail.jpg" class="video-js vjs-default-skin" id="live-video" style="width:100%;height:100%;"></video> <video controls poster="/thumbnail.jpg" class="video-js vjs-default-skin" id="live-video" style="width:100%;height:100%;min-height: 500px;"></video>
</div> </div>
<div id="jschild" class="webchat" style="width: 30%;height: 100%;"> <div id="jschild" class="webchat" style="width: 30%;height: 100%;position: relative;">
<iframe src="/chat?room={{ username }}" frameborder="0" style="width: 100%;height: 100%;"></iframe> <iframe src="/chat?room={{ username }}" frameborder="0" style="width: 100%;height: 100%; min-height: 500px;" allowfullscreen></iframe>
</div> </div>
</div> </div>
<script>window.HELP_IMPROVE_VIDEOJS = false;</script> <script>window.HELP_IMPROVE_VIDEOJS = false;</script>
<script src="/videojs/video.min.js"></script> <script src="/videojs/video.min.js"></script>
<script src="/dashjs/dash.all.min.js"></script> <link rel="stylesheet" type="text/css" href="/videojs/video-js.min.css">
<script src="/videojs/videojs-dash.min.js"></script> <script>
<link rel="stylesheet" type="text/css" href="/videojs/video-js.min.css">
<script>
var player = videojs('live-video', { var player = videojs('live-video', {
html: { html: {
nativeCaptions: false, nativeCaptions: false,