diff --git a/.gitignore b/.gitignore index 0cdbe36..fad7bdd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ node_modules site/live config/local.toml +config/jwt.pem config/generated.toml install/db_setup.sql build/** diff --git a/config/default.toml b/config/default.toml index ea03684..3ff79ca 100644 --- a/config/default.toml +++ b/config/default.toml @@ -50,4 +50,9 @@ port = 8000 record = false publicEndpoint = 'live' privateEndpoint = 'stream' -ffmpeg = '' \ No newline at end of file +ffmpeg = '' + +[transcode] +adapative = false +variants = 3 +format = 'dash' \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 582080b..9c53b90 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "satyr", - "version": "0.4.3", + "version": "0.4.4", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -121,6 +121,16 @@ "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", "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": { "version": "1.0.0", "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", "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": { "version": "1.19.0", "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", "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": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", @@ -1837,6 +1868,14 @@ "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", "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": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", @@ -1940,6 +1979,11 @@ "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": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", @@ -2984,8 +3028,7 @@ "typescript": { "version": "3.6.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.3.tgz", - "integrity": "sha512-N7bceJL1CtRQ2RiG0AQME13ksR7DiuQh/QehubYcghzv20tnh+MQnQIuJddTmsbqYj+dztchykemz0zFzlvdQw==", - "dev": true + "integrity": "sha512-N7bceJL1CtRQ2RiG0AQME13ksR7DiuQh/QehubYcghzv20tnh+MQnQIuJddTmsbqYj+dztchykemz0zFzlvdQw==" }, "union-value": { "version": "1.0.1", diff --git a/package.json b/package.json index 221094e..7b023d9 100644 --- a/package.json +++ b/package.json @@ -17,9 +17,11 @@ "bcrypt": "^3.0.6", "body-parser": "^1.19.0", "config": "^3.2.2", + "cookie-parser": "^1.4.4", "dirty": "^1.1.0", "express": "^4.17.1", "flags": "^0.1.3", + "jose": "^1.15.1", "mysql": "^2.17.1", "node-media-server": ">=2.1.3 <3.0.0", "nunjucks": "^3.2.0", diff --git a/src/api.ts b/src/api.ts index 5c01cd6..7ff9944 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,4 +1,5 @@ import * as db from "./database" +import { unregisterUser } from "./irc"; var config: any; function init(conf: object){ @@ -20,18 +21,26 @@ async function register(name: string, password: string, confirm: string) { return {"error":""}; } -async function update(name: string, password: string, title: string, bio: string, record: boolean){ - if(!name || !password) return {"error":"Insufficient parameters"}; - let auth: boolean = await db.validatePassword(name, password); - if(!auth) return {"error":"Username or Password Incorrect"}; - await db.query('UPDATE user_meta set title='+db.raw.escape(title)+', about='+db.raw.escape(bio)+' where username='+db.raw.escape(name)); - if(!record) await db.query('UPDATE users set record_flag=false where username='+db.raw.escape(name)); - else await db.query('UPDATE users set record_flag=true where username='+db.raw.escape(name)); +async function update(fields: object){ + if(!fields['title'] && !fields['bio'] && (fields['rec'] !== 'true' && fields['rec'] !== 'false')) return {"error":"no valid fields specified"}; + let qs: string = ""; + let f: boolean = false; + if(fields['title']) {qs += ' user_meta.title='+db.raw.escape(fields['title']);f = true;} + if(fields['bio']) { + 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":""}; } 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); if(!auth) return {"error":"Username or Password Incorrect"}; let newhash: string = await db.hash(newpwd); @@ -39,13 +48,17 @@ async function changepwd(name: string, password: string, newpwd: string){ return {"success":""}; } -async function changesk(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"}; +async function changesk(name: string){ 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'); return {"success":key}; } -export { init, register, update, changepwd, changesk }; \ No newline at end of file +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 }; \ No newline at end of file diff --git a/src/http.ts b/src/http.ts index 2d2b5ff..94302c2 100644 --- a/src/http.ts +++ b/src/http.ts @@ -4,22 +4,35 @@ import * as bodyparser from "body-parser"; import * as fs from "fs"; import * as socketio from "socket.io"; import * as http from "http"; +import * as cookies from "cookie-parser"; import * as dirty from "dirty"; import * as api from "./api"; import * as db from "./database"; import * as irc from "./irc"; +import { readFileSync, writeFileSync } from "fs"; +import { JWT, JWK } from "jose"; +import { strict } from "assert"; +import { parse } from "path"; const app = express(); const server = http.createServer(app); const io = socketio(server); 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; async function init(satyr: any, port: number, ircconf: any){ njk.configure('templates', { - autoescape: true, - express : app, - watch: false + autoescape : true, + express : app, + watch : true }); njkconf ={ sitename: satyr.name, @@ -28,88 +41,287 @@ async function init(satyr: any, port: number, ircconf: any){ rootredirect: satyr.rootredirect, version: satyr.version }; + app.use(cookies()); app.use(bodyparser.json()); app.use(bodyparser.urlencoded({ extended: true })); //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(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) { + let rooms = Object.keys(socket.rooms); + for(let i=1;i { + 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); + 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); + 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); + res.send('{"success":""}'); + }) + } + else { + res.send(result); + } + }); + } + }) +} + +async function initSite(openReg) { app.get('/', (req, res) => { res.redirect(njkconf.rootredirect); }); 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) => { 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) => { 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) => { - db.query('select username,title,about from user_meta where username='+db.raw.escape(req.url.split('/')[2].toLowerCase())).then((result) => { + app.get('/users/:user', (req, res) => { + db.query('select username,title,about from user_meta where username='+db.raw.escape(req.params.user)).then((result) => { 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) => { - db.query('select username from user_meta where username='+db.raw.escape(req.url.split('/')[2].toLowerCase())).then((result) => { + app.get('/vods/:user', (req, res) => { + db.query('select username from user_meta where username='+db.raw.escape(req.params.user)).then((result) => { if(result[0]){ - fs.readdir('./site/live/'+njkconf.user, {withFileTypes: true} , (err, files) => { - res.render('vods.njk', Object.assign({user: result[0].username, list: files.filter(fn => fn.name.endsWith('.mp4'))}, njkconf)); + fs.readdir('./site/live/'+result[0].username, {withFileTypes: true} , (err, files) => { + 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(njkconf.rootredirect); + } + else res.render('login.njk',njkconf); + }); 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) => { - 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(njkconf.rootredirect); }); app.get('/changepwd', (req, res) => { - res.render('changepwd.njk', njkconf); - }); - app.get('/changesk', (req, res) => { - res.render('changesk.njk', njkconf); + if(tryDecode(req.cookies.Authorization)) { + res.render('changepwd.njk', Object.assign({auth: {is: true, name: JWT.decode(req.cookies.Authorization)['username']}}, njkconf)); + } + else res.redirect(njkconf.rootredirect); }); app.get('/chat', (req, res) => { res.render('chat.html', njkconf); }); app.get('/help', (req, res) => { - res.render('help.njk', njkconf); - }); - //api handlers - app.post('/api/register', (req, res) => { - 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); + if(tryDecode(req.cookies.Authorization)) { + res.render('help.njk', Object.assign({auth: {is: true, name: JWT.decode(req.cookies.Authorization)['username']}}, njkconf)); + } + else res.render('help.njk',njkconf); }); +} + +async function initChat(ircconf: any) { //irc peering if(ircconf.enable){ await irc.connect({ @@ -190,26 +402,6 @@ async function init(satyr: any, port: number, ircconf: any){ 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 { - keystore.rm(results[0].username); + if(results[0]) keystore.rm(results[0].username); }); } diff --git a/templates/base.njk b/templates/base.njk index a7aa9e7..7f2a642 100644 --- a/templates/base.njk +++ b/templates/base.njk @@ -8,7 +8,7 @@
{% block content %} diff --git a/templates/changepwd.njk b/templates/changepwd.njk index aa29838..c3eaf03 100644 --- a/templates/changepwd.njk +++ b/templates/changepwd.njk @@ -1,11 +1,10 @@ {% extends "base.njk" %} {% block content %} -

Change your password on {{ sitename }}

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

Change your password on {{ sitename }}

- Username:

- Password:

- New Password:

+ Old Password:

+ New Password:


diff --git a/templates/changesk.njk b/templates/changesk.njk deleted file mode 100644 index 30802a6..0000000 --- a/templates/changesk.njk +++ /dev/null @@ -1,11 +0,0 @@ -{% extends "base.njk" %} -{% block content %} -

Get a new stream key on {{ sitename }}

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

-
- Username:

- Password:

- -
- -{% endblock %} diff --git a/templates/login.njk b/templates/login.njk new file mode 100644 index 0000000..f5cb4f8 --- /dev/null +++ b/templates/login.njk @@ -0,0 +1,10 @@ +{% extends "base.njk" %} +{% block content %} +

Log in to {{ sitename }}

Not registered yet? Sign up here.

+
+ Username:

+ Password:

+ +
+ +{% endblock %} \ No newline at end of file diff --git a/templates/profile.njk b/templates/profile.njk index 465f061..a370a6c 100644 --- a/templates/profile.njk +++ b/templates/profile.njk @@ -1,14 +1,15 @@ {% extends "base.njk" %} {% block content %} -

Update your profile on {{ sitename }}

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

Update your profile on {{ sitename }}

Or, change your password.

-
- Username:

- Password:

+ Stream Title:

Bio:

- Record VODs:
- + Record VODs: Yes No

+ +

+
+
{% endblock %} \ No newline at end of file diff --git a/templates/registration.njk b/templates/registration.njk index 2bdfe0c..5b0ba91 100644 --- a/templates/registration.njk +++ b/templates/registration.njk @@ -1,17 +1,19 @@ {% extends "base.njk" %} {% block content %} -
-
+

Register on {{ sitename }}

Already registered? Log in here.

+
Username:

Password:

- Confirm:

+ Confirm:


-
+
+ + + {% include "tos.html" %}
-
-
- {% include "tos.html" %} -
-
+ {% endblock %} \ No newline at end of file