diff --git a/.gitignore b/.gitignore index 8c8153b..4714669 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ config/**/* !config/.gitkeep install/db_setup.sql build/** +site/templates.js \ No newline at end of file diff --git a/README.md b/README.md index 2018270..db204c1 100644 --- a/README.md +++ b/README.md @@ -15,11 +15,13 @@ Follow the instructions after setup runs. ```bash npm run start ``` -You can also run this to skip checking the database version on startup. +You can also skip checking the database version and compiling templates (if you don't use server-side rendering) on startup. ```bash -npm run start -- --skip-migrate +npm run start -- --skip-migrate --skip-compile # don't forget to migrate manually when you update npm run migrate +# and compile templates after any changes +npm run make-templates ``` ## Contributing diff --git a/docs/REST.md b/docs/REST.md index c6004a0..9058efb 100644 --- a/docs/REST.md +++ b/docs/REST.md @@ -115,7 +115,7 @@ Obtain a signed json web token for authentication **Response**: If succesful, will return `{success: ""}` or `{success: "already verified"}` if the JWT provided is too early to be renewed. If unsuccesful, will return `{error: "invalid password"}` or `{error: "Username or Password Incorrect"}` depending on the authentication method. Note that if a JWT is available, the parameters will be ignored. -**Notes**: I've already listed nearly every response. My final note is that the JWT is set as the cookie 'Authorization', not returned in the response. +**Notes**: The returned JWT is set as the cookie httponly 'Authorization'. It will also return a non httponly cookie X-Auth-As with the username of the authenticated user. ## /api/user/update diff --git a/install/config.example.yml b/install/config.example.yml index c67d16b..be1a5a0 100644 --- a/install/config.example.yml +++ b/install/config.example.yml @@ -14,6 +14,7 @@ rtmp: http: # uncomment to set HSTS when SSL is ready #hsts: true + server_side_render: false database: user: '' diff --git a/package-lock.json b/package-lock.json index 78361b5..e6e1e5a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "satyr", - "version": "0.9.3", + "version": "0.9.4", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -963,6 +963,11 @@ "asn1.js": "^5.2.0" } }, + "jwt-decode": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.0.0.tgz", + "integrity": "sha512-RBQv2MTm3FNKQkdzhEyQwh5MbdNgMa+FyIJIK5RMWEn6hRgRHr7j55cRxGhRe6vGJDElyi6f6u/yfkP7AoXddA==" + }, "lodash": { "version": "4.17.20", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", diff --git a/package.json b/package.json index e2b210f..e921a3e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "satyr", - "version": "0.9.4", + "version": "0.10.0", "description": "A livestreaming server.", "license": "AGPL-3.0", "author": "knotteye", @@ -9,7 +9,8 @@ "cli": "ts-node src/cli.ts", "setup": "sh install/setup.sh", "migrate": "ts-node src/migrate.ts", - "invite": "ts-node src/cli.ts --invite" + "invite": "ts-node src/cli.ts --invite", + "make-templates": "nunjucks-precompile -i [\"\\.html$\",\"\\.njk$\"] templates > site/templates.js" }, "repository": { "type": "git", diff --git a/site/index.html b/site/index.html new file mode 100644 index 0000000..cd6342b --- /dev/null +++ b/site/index.html @@ -0,0 +1,23 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/site/index.js b/site/index.js new file mode 100644 index 0000000..8a06b30 --- /dev/null +++ b/site/index.js @@ -0,0 +1,170 @@ +async function render(path){ + var context = await getContext(); + switch(path){ + //nothing but context + case (path.match(/^\/about\/?$/) || {}).input: + document.body.innerHTML = nunjucks.render('about.njk', context); + modifyLinks(); + break; + case (path.match(/^\/login\/?$/) || {}).input: + document.body.innerHTML = nunjucks.render('login.njk', context); + modifyLinks(); + break; + case (path.match(/^\/register\/?$/) || {}).input: + if(!context.registration) window.location = '/'; + document.body.innerHTML = nunjucks.render('registration.njk', context); + modifyLinks(); + break; + case (path.match(/^\/changepwd\/?$/) || {}).input: + document.body.innerHTML = nunjucks.render('changepwd.njk', context); + modifyLinks(); + break; + case (path.match(/^\/chat\/?$/) || {}).input: + document.body.innerHTML = nunjucks.render('chat.html', context); + modifyLinks(); + break; + case (path.match(/^\/help\/?$/) || {}).input: + document.body.innerHTML = nunjucks.render('help.njk', context); + modifyLinks(); + break; + //need to hit the API + case (path.match(/^\/users\/live\/?$/) || {}).input: + var list = JSON.parse(await makeRequest("POST", "/api/users/live", JSON.stringify({num: 50}))); + document.body.innerHTML = nunjucks.render('live.njk', Object.assign({list: list.users}, context)); + modifyLinks(); + break; + case (path.match(/^\/users\/?$/) || {}).input: + var list = JSON.parse(await makeRequest("POST", "/api/users/all", JSON.stringify({num: 50}))); + document.body.innerHTML = nunjucks.render('list.njk', Object.assign({list: list.users}, context)); + modifyLinks(); + break; + case (path.match(/^\/profile\/chat\/?$/) || {}).input: + if(!context.auth.name) window.location = '/login'; + var config = JSON.parse(await makeRequest("GET", '/api/'+context.auth.name+'/config')); + config = { + integ: { + twitch: config.twitch, + xmpp: config.xmpp, + irc: config.irc, + discord: config.discord + } + }; + document.body.innerHTML = nunjucks.render('chat_integ.njk', Object.assign(config, context)); + modifyLinks(); + break; + case (path.match(/^\/profile\/?$/) || {}).input: + if(!context.auth.name) window.location = '/login'; + var config = JSON.parse(await makeRequest("GET", '/api/'+context.auth.name+'/config')); + config = { + meta: { + title: config.title, + about: config.about + }, + rflag: {record_flag: config.record_flag}, + twitch: config.twitch_mirror + }; + document.body.innerHTML = nunjucks.render('profile.njk', Object.assign(config, context)); + modifyLinks(); + break; + //parsing slugs + case (path.match(/^\/invite\//) || {}).input: // /invite/:code + document.body.innerHTML = nunjucks.render('invite.njk', Object.assign({icode: path.substring(8)}, context)); + modifyLinks(); + break; + //slugs and API + case (path.match(/^\/users\/.+\/?$/) || {}).input: // /users/:user + if(path.substring(path.length - 1).indexOf('/') !== -1) + var usr = path.substring(7, path.length - 1); + else var usr = path.substring(7); + var config = JSON.parse(await makeRequest("GET", '/api/'+usr+'/config')); + if(!config.title){document.body.innerHTML = nunjucks.render('404.njk', context); break;} + document.body.innerHTML = nunjucks.render('user.njk', Object.assign({about: config.about, title: config.title, username: config.username}, context)); + modifyLinks(); + break; + case (path.match(/^\/vods\/.+\/manage\/?$/) || {}).input: // /vods/:user/manage + var usr = path.substring(6, (path.length - 7)); + if(context.auth.name !== usr) window.location = '/vods/'+usr; + var vods = JSON.parse(await makeRequest("GET", '/api/'+usr+'/vods')); + document.body.innerHTML = nunjucks.render('managevods.njk', Object.assign({user: usr, list: vods.vods.filter(fn => fn.name.endsWith('.mp4'))}, context)); + modifyLinks(); + break; + case (path.match(/^\/vods\/.+\/?$/) || {}).input: // /vods/:user + if(path.substring(path.length - 1).indexOf('/') !== -1) + var usr = path.substring(6, path.length - 1); + else var usr = path.substring(6); + var vods = JSON.parse(await makeRequest("GET", '/api/'+usr+'/vods')); + document.body.innerHTML = nunjucks.render('vods.njk', Object.assign({user: usr, list: vods.vods.filter(fn => fn.name.endsWith('.mp4'))}, context)); + modifyLinks(); + break; + //root + case "/": + render('/users/live'); + break; + case "": + render('/users/live'); + break; + //404 + default: + document.body.innerHTML = nunjucks.render('404.njk', context); + modifyLinks(); + } +} + +async function getContext(){ + var info = JSON.parse(await makeRequest('GET', '/api/instance/info')); + info.sitename = info.name; + info.name = null; + info.auth = { + is: document.cookie.match(/^(.*;)?\s*X-Auth-As\s*=\s*[^;]+(.*)?$/) !== null, + name: parseCookie(document.cookie)['X-Auth-As'] + } + return info; +} + +function makeRequest(method, url, payload) { + return new Promise(function (resolve, reject) { + let xhr = new XMLHttpRequest(); + xhr.open(method, url); + xhr.onload = function () { + if (this.status >= 200 && this.status < 300) { + resolve(xhr.response); + } else { + reject({ + status: this.status, + statusText: xhr.statusText + }); + } + }; + xhr.onerror = function () { + reject({ + status: this.status, + statusText: xhr.statusText + }); + }; + !payload ? xhr.send() : xhr.send(payload); + }); +} + +function parseCookie(c){ + if(typeof(c) !== 'string' || !c.includes('=')) return {}; + return Object.assign({[c.split('=')[0].trim()]:c.split('=')[1].split(';')[0].trim()}, parseCookie(c.split(/;(.+)/)[1])); +} + +function handleLoad() { + var r = JSON.parse(document.getElementById('responseFrame').contentDocument.documentElement.textContent).success + if (typeof(r) !== 'undefined') window.location.href = '/profile' +} + +function modifyLinks() { + for (var ls = document.links, numLinks = ls.length, i=0; i{ let users = await db.query('SELECT stream_key,record_flag FROM users WHERE username='+db.raw.escape(username)); if(users[0]) Object.assign(t, users[0]); let usermeta = await db.query('SELECT title,about FROM user_meta WHERE username='+db.raw.escape(username)); - if(usermeta[0]) Object.assign(t, users[0]); + if(usermeta[0]) Object.assign(t, usermeta[0]); let ci = await db.query('SELECT irc,xmpp,twitch,discord FROM chat_integration WHERE username='+db.raw.escape(username)); if(ci[0]) Object.assign(t, ci[0]); + let tw = await db.query('SELECT enabled,twitch_key FROM twitch_mirror WHERE username='+db.raw.escape(username)); + if(tw[0]) t['twitch_mirror'] = Object.assign({}, tw[0]); } else { let um = await db.query('SELECT title,about FROM user_meta WHERE username='+db.raw.escape(username)); diff --git a/src/cleanup.ts b/src/cleanup.ts index 56c9dc9..4a3fd58 100644 --- a/src/cleanup.ts +++ b/src/cleanup.ts @@ -1,8 +1,9 @@ import * as db from "./database"; import {readdirSync} from "fs"; +import { execSync } from "child_process"; -async function init(m?: boolean) { - if(!m){ +async function init() { + if(process.argv.indexOf('--skip-migrate') === -1){ console.log('Checking database version.'); var tmp: string[] = await db.query('show tables like \"db_meta\"'); if(tmp.length === 0){ @@ -17,6 +18,15 @@ async function init(m?: boolean) { else { console.log('Skipping database version check.'); } + + if(!require('./config').config['http']['server_side_render'] && process.argv.indexOf('--skip-compile') === -1) { + console.log("Compiling templates for client-side frontend."); + execSync(process.cwd()+'/node_modules/.bin/nunjucks-precompile -i [\"\\.html$\",\"\\.njk$\"] templates > site/templates.js'); + } + else if(!require('./config').config['http']['server_side_render']){ + console.log("Skipped compiling templates for client-side frontend."); + } + //If satyr is restarted in the middle of a stream //it causes problems //Live flags in the database stay live diff --git a/src/config.ts b/src/config.ts index 0fdeaa4..d9edb2e 100644 --- a/src/config.ts +++ b/src/config.ts @@ -36,7 +36,10 @@ const config: Object = { ping: 30, ping_timeout: 60 }, localconfig['rtmp']), http: Object.assign({ - hsts: false, directory: './site', port: 8000 + hsts: false, + directory: './site', + port: 8000, + server_side_render: true }, localconfig['http']), media: Object.assign({ record: false, diff --git a/src/http.ts b/src/http.ts index 8eb06dc..f83b61b 100644 --- a/src/http.ts +++ b/src/http.ts @@ -12,9 +12,6 @@ import * as chatInteg from "./chat"; import { config } from "./config"; 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 server = http.createServer(app); @@ -55,12 +52,16 @@ async function init(){ }); } app.disable('x-powered-by'); - //site handlers + //server-side site routes + if(config['http']['server_side_render']) await initSite(config['satyr']['registration']); - //api handlers + //api routes await initAPI(); - //static files if nothing else matches first + //static files if nothing else matches app.use(express.static(config['http']['directory'])); + //client-side site routes + if(!config['http']['server_side_render']) + await initFE(); //404 Handler app.use(function (req, res, next) { if(tryDecode(req.cookies.Authorization)) { @@ -73,6 +74,21 @@ async function init(){ server.listen(config['http']['port']); } +async function initFE(){ + app.get('/', (req, res) => { + res.redirect(config['satyr']['rootredirect']); + }); + app.get('/nunjucks-slim.js', (req, res) => { + res.sendFile(process.cwd()+'/node_modules/nunjucks/browser/nunjucks-slim.js'); + }); + app.get('/chat', (req, res) => { + res.sendFile(process.cwd()+'/templates/chat.html'); + }); + app.get('*', (req, res) => { + res.sendFile(process.cwd()+'/'+config['http']['directory']+'/index.html'); + }); +} + async function newNick(socket, skip?: boolean, i?: number) { if(socket.handshake.headers['cookie'] && !skip){ let c = await parseCookie(socket.handshake.headers['cookie']); @@ -361,6 +377,7 @@ async function initAPI() { if(req.cookies.Authorization) validToken(req.cookies.Authorization).then((t) => { if(t) { if(t['exp'] - 86400 < Math.floor(Date.now() / 1000)){ + res.cookie('X-Auth-As', t['username'], {maxAge: 604800000, httpOnly: false, sameSite: 'Lax'}); return genToken(t['username']).then((t) => { res.cookie('Authorization', t, {maxAge: 604800000, httpOnly: true, sameSite: 'Lax'}); res.json({success:""}); @@ -382,6 +399,7 @@ async function initAPI() { if(!result){ genToken(req.body.username).then((t) => { res.cookie('Authorization', t, {maxAge: 604800000, httpOnly: true, sameSite: 'Lax'}); + res.cookie('X-Auth-As', req.body.username, {maxAge: 604800000, httpOnly: false, sameSite: 'Lax'}); res.json({success:""}); }) } diff --git a/src/index.ts b/src/index.ts index 889c20f..2573948 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,7 +7,7 @@ import { config } from "./config"; async function run() { await initDB(); - await clean(process.argv.indexOf('--skip-migrate') !== -1); + await clean(); await initHTTP(); await initRTMP(); await initChat(); diff --git a/templates/base.njk b/templates/base.njk index 9d57b04..f832bc3 100644 --- a/templates/base.njk +++ b/templates/base.njk @@ -6,7 +6,7 @@ {{ sitename }}