diff --git a/app.js b/app.js index a7089b5..544bf55 100644 --- a/app.js +++ b/app.js @@ -1,19 +1,20 @@ const connectSession = require('connect-redis') const session = require('express-session') const bodyParser = require('body-parser') +const nunjucks = require('nunjucks') +const passport = require('passport') const express = require('express') const request = require('request') -const nunjucks = require('nunjucks') const sqlite = require('sqlite') const xml2js = require('xml2js') -const path = require('path') -const toml = require('toml') -const http = require('http') -const fs = require('fs') const WebSocket = require('ws') const uuid = require('uuid/v4') const redis = require('redis') +const path = require('path') +const toml = require('toml') +const http = require('http') const URL = require('url') +const fs = require('fs') require('express-async-errors') @@ -40,32 +41,26 @@ try { config = Object.assign({ 'Streaming': { - 'Port': '9322', - 'Database': 'streaming.db', - 'StreamServer': 'https://tv.icynet.eu/live/', - 'ServerHost': 'icynet.eu', - 'PublishAddress': 'rtmp://{host}:1935/hls-live/{streamer}', - 'Secret': 'changeme' + 'port': '9322', + 'database': 'streaming.db', + 'streamServer': 'https://tv.icynet.eu/live/', + 'serverHost': 'icynet.eu', + 'publishAddress': 'rtmp://{host}:1935/hls-live/{streamer}', + 'secret': 'changeme' }, 'Auth': { - 'Server': 'http://localhost:8282', - 'Redirect': 'http://localhost:5000/auth/_callback/' - }, - 'OAuth2': { - 'ClientID': '1', - 'ClientSecret': 'changeme' + 'strategy': 'passport-oauth2', + 'callbackURL': 'http://localhost:5000/auth/_callback/', + 'clientID': '1', + 'clientSecret': 'changeme' } }, config) // Constants const oauthAuth = '{server}/oauth2/authorize?response_type=code&state={state}&redirect_uri={redirect}&client_id={client}&scope=image' -const port = parseInt(config['Streaming']['Port']) -const streamServer = config['Streaming']['StreamServer'] -const streamServerHost = config['Streaming']['ServerHost'] -const authServer = config['Auth']['Server'] -const oauthRedirect = config['Auth']['Redirect'] -const oauthId = config['OAuth2']['ClientID'].toString() -const oauthSecret = config['OAuth2']['ClientSecret'] +const port = parseInt(config['Streaming']['port']) +const streamServer = config['Streaming']['streamServer'] +const streamServerHost = config['Streaming']['serverHost'] const streamAppName = streamServer.match(/\/([\w-_]+)\/$/)[1] function teval (str, obj) { @@ -79,7 +74,7 @@ function teval (str, obj) { // Database const dbPromise = Promise.resolve() - .then(() => sqlite.open(path.join(process.cwd(), config['Streaming']['Database']), { Promise, cache: true })) + .then(() => sqlite.open(path.join(process.cwd(), config['Streaming']['database']), { Promise, cache: true })) .then(db => db.migrate()) // Setup server @@ -87,6 +82,24 @@ const app = express() const server = http.createServer(app) const wss = new WebSocket.Server({ clientTracking: false, noServer: true }) +// Authentication +const Strategy = require(config['Auth']['strategy']) +const strategyConfig = Object.assign({}, config['Auth']) +if (!strategyConfig.provider) strategyConfig.provider = strategyConfig.strategy.replace('passport-', '') +passport.use(new Strategy(strategyConfig, function (accessToken, refreshToken, profile, done) { + process.nextTick(function() { + return done(null, profile) + }) +})) + +passport.serializeUser(function (user, done) { + done(null, user) +}) + +passport.deserializeUser(function (user, done) { + done(null, user) +}) + app.enable('trust proxy', 1) app.use(bodyParser.urlencoded({ extended: false })) @@ -101,7 +114,7 @@ nunjucks.configure('templates', { const sessionParser = session({ key: 'Streamserver Session', - secret: config['Streaming']['Secret'], + secret: config['Streaming']['secret'], resave: false, saveUninitialized: true, store: new SessionStore({ client: redis.createClient() }), @@ -113,6 +126,9 @@ const sessionParser = session({ app.use(sessionParser) +app.use(passport.initialize()) +app.use(passport.session()) + // Parse stream metrics from the stat.xml file async function pullMetrics (uuid) { let statPath = streamServer + 'stat' @@ -188,7 +204,7 @@ app.post('/publish', async (req, res) => { console.log('=> Streamer %s has started streaming!', streamer.name) // Generate real publish address for the server - let publishAddress = config['Streaming']['PublishAddress'] + let publishAddress = config['Streaming']['publishAddress'] .replace('{streamer}', streamer.name) .replace('{host}', '127.0.0.1') @@ -219,97 +235,50 @@ app.post('/publish_done', async (req, res) => { // Front-end server // OAuth2 authenticator -app.get('/login', async (req, res) => { - if (req.session.user) return res.redirect('/') - req.session.state = uuid() - res.redirect(teval(oauthAuth, { state: req.session.state, redirect: oauthRedirect, client: oauthId, server: authServer })) -}) - -app.get('/auth/_callback', async (req, res) => { - let state = req.session.state - if (!state) throw new Error('Something went wrong!') - - let code = req.query.code - let provState = req.query.state - if (!code || state !== provState) throw new Error('Something went wrong!') - delete req.session.state - - // Aquire token - let { body } = await post(authServer + '/oauth2/token', { - form: { - grant_type: 'authorization_code', - code: code, - redirect_uri: oauthRedirect, - client_id: oauthId, - client_secret: oauthSecret - }, - auth: { - user: oauthId, - pass: oauthSecret - } - }) - - if (!body) throw new Error('Could not obtain access token!') - try { - body = JSON.parse(body) - } catch (e) { - console.error(e, body) - throw new Error('Authorization server gave us an invalid response!') - } - - if (body['error']) { - throw new Error(body['error'] + ': ' + body['error_description']) - } - - let token = body.access_token - - // Get user information - let { body: bodyNew } = await get(authServer + '/oauth2/user', { auth: { bearer: token } }) - try { - bodyNew = JSON.parse(bodyNew) - } catch (e) { - console.error(e, bodyNew) - throw new Error('Authorization server gave us an invalid response for user!') - } +app.get('/login', passport.authenticate(strategyConfig.provider, Object.assign({}, strategyConfig.authOptions || {}))) +app.get('/auth/_callback', passport.authenticate(strategyConfig.provider, { failureRedirect: '/' }), async (req, res) => { + dev && console.log(req.user.username, 'logged in') // Get user from database let db = await dbPromise - let user = await db.get('SELECT * FROM signed_users WHERE uuid=?', bodyNew.uuid) + let user = await db.get('SELECT * FROM signed_users WHERE uuid=?', req.user.uuid) if (!user) { - await db.run('INSERT INTO signed_users (uuid,name) VALUES (?,?)', bodyNew.uuid, bodyNew.username) + await db.run('INSERT INTO signed_users (uuid,name) VALUES (?,?)', req.user.uuid, req.user.username) } - req.session.login = bodyNew.uuid - req.session.username = bodyNew.username - // Lets see if this user is a streamer - let streamer = await db.get('SELECT * FROM channels WHERE user_uuid = ?', bodyNew.uuid) - if (streamer) cache.streamers[bodyNew.uuid] = streamer + let streamer = await db.get('SELECT * FROM channels WHERE user_uuid = ?', req.user.uuid) + if (streamer) cache.streamers[req.user.uuid] = streamer res.redirect('/') }) app.get('/logout', (req, res) => { - req.session.destroy() + req.logout() res.redirect('/') }) +function authed (req, res, next) { + if (req.isAuthenticated() && req.isStreamer) return next() + res.jsonp({ error: 'Unauthorized' }) +} + // Views app.use('/dist', express.static(path.join(__dirname, 'dist'), { maxAge: dev ? 0 : 2678400000 })) app.use(async function (req, res, next) { req.isStreamer = false - if (!req.session.login) return next() + if (!req.isAuthenticated()) return next() - res.locals.session = { uuid: req.session.login, username: req.session.username } + res.locals.user = req.user - if (!cache.streamers[req.session.login]) { + if (!cache.streamers[req.user.uuid]) { let db = await dbPromise - let streamer = await db.get('SELECT * FROM channels WHERE user_uuid = ?', req.session.login) - if (streamer) cache.streamers[req.session.login] = streamer + let streamer = await db.get('SELECT * FROM channels WHERE user_uuid = ?', req.user.uuid) + if (streamer) cache.streamers[req.user.uuid] = streamer } - if (cache.streamers[req.session.login]) { + if (cache.streamers[req.user.uuid]) { req.isStreamer = true return next() } @@ -323,16 +292,14 @@ app.get('/', (req, res) => { }) // Dashboard -app.get('/dashboard', (req, res, next) => { - if (!req.isStreamer) return next(new Error('Unauthorized')) - let stream = cache.streamers[req.session.login] +app.get('/dashboard', authed, (req, res) => { + let stream = cache.streamers[req.user.uuid] res.render('dashboard.html', { stream: stream.key, server: 'rtmp://' + streamServerHost + '/live/' }) }) // Stats -app.get('/dashboard/stats', async (req, res) => { - if (!req.isStreamer) return res.jsonp({ error: 'Unauthorized' }) - let stream = cache.streamers[req.session.login] +app.get('/dashboard/stats', authed, async (req, res) => { + let stream = cache.streamers[req.user.uuid] let data try { @@ -346,9 +313,8 @@ app.get('/dashboard/stats', async (req, res) => { }) // Data -app.get('/dashboard/data', async (req, res) => { - if (!req.isStreamer) return res.jsonp({ error: 'Unauthorized' }) - let stream = cache.streamers[req.session.login] +app.get('/dashboard/data', authed, async (req, res) => { + let stream = cache.streamers[req.user.uuid] let data let db = await dbPromise @@ -364,7 +330,7 @@ app.get('/dashboard/data', async (req, res) => { res.jsonp({ 'name': data.name, 'key': stream.key, - 'uuid': req.session.login, + 'uuid': req.user.uuid, 'live': data.live_at != null, 'live_at': new Date(parseInt(data.live_at)), 'last_stream': new Date(parseInt(data.last_stream)) @@ -372,9 +338,8 @@ app.get('/dashboard/data', async (req, res) => { }) // Get links -app.get('/dashboard/link', async (req, res) => { - if (!req.isStreamer) return res.jsonp({ error: 'Unauthorized' }) - let user = req.session.login +app.get('/dashboard/link', authed, async (req, res) => { + let user = req.user.uuid let db = await dbPromise let links = await db.all('SELECT * FROM link WHERE uuid = ?', user) @@ -383,9 +348,8 @@ app.get('/dashboard/link', async (req, res) => { }) // Add link URL -app.post('/dashboard/link', async (req, res) => { - if (!req.isStreamer) return res.jsonp({ error: 'Unauthorized' }) - let user = req.session.login +app.post('/dashboard/link', authed, async (req, res) => { + let user = req.user.uuid let name = req.body.name let url = req.body.url @@ -413,9 +377,8 @@ app.post('/dashboard/link', async (req, res) => { }) // Remove link URL -app.post('/dashboard/link/delete', async (req, res) => { - if (!req.isStreamer) return res.jsonp({ error: 'Unauthorized' }) - let user = req.session.login +app.post('/dashboard/link/delete', authed, async (req, res) => { + let user = req.user.uuid if (req.body.name == null && req.body.url == null) return res.jsonp({ error: 'Missing parameters!' }) @@ -469,8 +432,8 @@ app.use((error, req, res, next) => { // Socket Server wss.on('connection', (ws, request, client) => { - const userId = request.session.login || request.session.id - const username = request.session.username + const userId = request.user.uuid || request.session.id + const username = request.user.username let myChannels = [] dev && console.log(userId, 'connected') @@ -538,4 +501,5 @@ server.listen(port, host, () => { })().catch(e => console.error(e.stack)) console.log('Listening on %s:%d', host, port) + console.log('Authentication module: %s (%s)', strategyConfig.strategy, strategyConfig.provider) }) diff --git a/config.example.toml b/config.example.toml index 1a50c33..1c11e8b 100644 --- a/config.example.toml +++ b/config.example.toml @@ -1,15 +1,16 @@ [Streaming] - Port = 5000 - Database = "streaming.db" - StreamServer = "http://localhost:5000/live/" - ServerHost = "localhost:1935" - PublishAddress = "rtmp://{host}:1935/hls-live/{streamer}" - Secret = "changeme" + port = 5000 + database = "streaming.db" + streamServer = "http://localhost:5000/live/" + serverHost = "localhost:1935" + publishAddress = "rtmp://{host}:1935/hls-live/{streamer}" + secret = "changeme" [Auth] - Server = "http://localhost:8282" - Redirect = "http://localhost:5000/auth/_callback/" - -[OAuth2] - ClientID = 1 - ClientSecret = "hackme" + provider = "oauth2" + strategy = "passport-oauth2" + callbackURL = "http://localhost:5000/auth/_callback/" + authorizationURL = "http://localhost/oauth2/authorize" + tokenURL = "http://localhost/oauth2/token" + clientID = 1 + clientSecret = "hackme" diff --git a/package-lock.json b/package-lock.json index 5b11bbe..49c0abc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "icytv", - "version": "2.0.0", + "version": "2.1.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -4570,6 +4570,20 @@ "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=" }, + "passport": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.4.0.tgz", + "integrity": "sha1-xQlWkTR71a07XhgCOMORTRbwWBE=", + "requires": { + "passport-strategy": "1.x.x", + "pause": "0.0.1" + } + }, + "passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ=" + }, "path-browserify": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz", @@ -4624,6 +4638,11 @@ "pify": "^3.0.0" } }, + "pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10=" + }, "pbkdf2": { "version": "3.0.16", "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.16.tgz", diff --git a/package.json b/package.json index d069208..c15a8d5 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "express-async-errors": "^3.1.1", "express-session": "^1.16.1", "nunjucks": "^3.2.0", + "passport": "^0.4.0", "redis": "^2.8.0", "request": "^2.88.0", "sqlite": "^3.0.3", diff --git a/templates/index.html b/templates/index.html index 33b0c8c..3c21857 100644 --- a/templates/index.html +++ b/templates/index.html @@ -13,8 +13,8 @@

IcyTV