From d47c465ed86187d793b605dbb42eaa0df0bd2573 Mon Sep 17 00:00:00 2001 From: IrosTheBeggar Date: Wed, 22 Feb 2017 20:40:57 -0500 Subject: [PATCH] Breaking out everything into modules --- modules/download.js | 46 +++++ modules/file-explorer.js | 81 +++++++++ modules/login.js | 206 ++++++++++++++++++++++ mstream.js | 365 ++------------------------------------- 4 files changed, 347 insertions(+), 351 deletions(-) create mode 100644 modules/download.js create mode 100644 modules/file-explorer.js create mode 100644 modules/login.js diff --git a/modules/download.js b/modules/download.js new file mode 100644 index 0000000..fb308cf --- /dev/null +++ b/modules/download.js @@ -0,0 +1,46 @@ +exports.setup = function(mstream, program){ + const archiver = require('archiver'); // Zip Compression + const fe = require('path'); + + + // Download a zip file of music + mstream.post('/download', function (req, res){ + var archive = archiver('zip'); + + archive.on('error', function(err) { + console.log(err.message); + res.status(500).send('{error: '+err.message+'}'); + }); + + archive.on('end', function() { + // TODO: add logging + }); + + //set the archive name + // TODO: Rename this + res.attachment('zipped-playlist.zip'); + + //streaming magic + archive.pipe(res); + + var fileArray; + + + // Get the POSTed files + if(req.allowedFiles){ + fileArray = allowedFiles; + }else{ + fileArray = JSON.parse(req.body.fileArray); + } + + for(var i in fileArray) { + // TODO: Confirm each item in posted data is a real file + var fileString = fileArray[i]; + + // TODO: Add file by ataching user's musicdir to the relative directory supplied + archive.file(fe.normalize( fileString), { name: fe.basename(fileString) }); + } + + archive.finalize(); + }); +} diff --git a/modules/file-explorer.js b/modules/file-explorer.js new file mode 100644 index 0000000..c35a1e9 --- /dev/null +++ b/modules/file-explorer.js @@ -0,0 +1,81 @@ +exports.setup = function(mstream, program){ + + const fs = require('fs'); // File System + const fe = require('path'); + const slash = require('slash'); + + + + // parse directories + mstream.post('/dirparser', function (req, res) { + var directories = []; + var filesArray = []; + + // TODO: Make sure path is a sub-path of the user's music dir + var path = fe.join(req.user.musicDir, req.body.dir); + // Make sure it's a directory + if(!fs.statSync( path).isDirectory()){ + res.status(500).json({ error: 'Not a directory' }); + return; + } + + // Will only show these files. Prevents people from snooping around + // TODO: Move to global variable + const masterFileTypesArray = ["mp3", "flac", "wav", "ogg", "aac", "m4a"]; + var fileTypesArray; + if(req.body.filetypes){ + fileTypesArray = JSON.parse(req.body.filetypes); + }else{ + fileTypesArray = masterFileTypesArray; + } + + + // get directory contents + var files = fs.readdirSync( path); + + // loop through files + for (let i=0; i < files.length; i++) { + + try{ + var stat = fs.statSync(fe.join(path, files[i])); + }catch(error){ + // Bad file, ignore and continue + // TODO: Log This + continue; + } + + // Handle Directories + if(stat.isDirectory()){ + directories.push({ + type:"directory", + name:files[i] + }); + }else{ // Handle Files + var extension = getFileType(files[i]); + if (fileTypesArray.indexOf(extension) > -1 && masterFileTypesArray.indexOf(extension) > -1) { + filesArray.push({ + type:extension, // TODO: Should this be changed + name:files[i] + }); + } + } + } + + var returnPath = slash( fe.relative(req.user.musicDir, path) ); + if(returnPath.slice(-1) !== '/'){ + returnPath += '/'; + } + + // Send back combined list of directories and mp3s + res.send( + JSON.stringify({ path:returnPath, contents:filesArray.concat(directories)}) + ); + }); + + + + function getFileType(filename){ + return filename.split(".").pop(); + } + +} diff --git a/modules/login.js b/modules/login.js new file mode 100644 index 0000000..e59534b --- /dev/null +++ b/modules/login.js @@ -0,0 +1,206 @@ +exports.setup = function(mstream, program, express){ + // Use bcrypt for password storage + const bcrypt = require('bcrypt'); + const jwt = require('jsonwebtoken'); // used to create, sign, and verify tokens + const uuidV4 = require('uuid/v4'); + + + + + // TODO: Add New user functionality + // Check for root user and password + // Add credentials to user array + + // TODO: Need a way to store and use already hashed passwords + + + // TODO: password change function + mstream.post('/change-password-request', function (req, res) { + // Get email address from request + // validate email against user array + // Generate change password token + // Invalidate all other change password tokens + // Email the user the token + + res.sendFile( 'COMING SOON!' ); + }); + + mstream.post('/change-password', function (req, res){ + // Check token + // Get new password + // Hash password and update user array + + res.sendFile( 'COMING SOON!' ); + }); + + mstream.post('/sunset-user', function(req,res){ + // Removes all user info + }); + + mstream.post('/add-user', function(req,res){ + // Add a user + }); + + + // Create the user array + var Users = program.users; + var permissionsMap = {}; + + for (let username in Users) { + // Setup user password + generateSaltedPassword(username, Users[username]["password"]); + + // If this is a guest user, continue + if(Users[username].guestTo){ + continue; + } + + // If dir has not been added yet + if ( !(Users[username].musicDir in permissionsMap) ){ + // Generate unique vPath if necessary + // The best way is to store the vPath in the JSON file + if(!Users[username].vPath){ + Users[username].vPath = uuidV4(); + } + + // Add to permissionsMap + permissionsMap[Users[username].musicDir] = Users[username].vPath; + }else{ + Users[username].vPath = permissionsMap[Users[username].musicDir]; + } + + } + + + function generateSaltedPassword(username, password){ + bcrypt.genSalt(10, function(err, salt) { + bcrypt.hash(password, salt, function(err, hash) { + // Store hash in your password DB. + Users[username]['password'] = hash; + }); + }); + } + + // Failed Login Attempt + mstream.get('/login-failed', function (req, res) { + // Wait before sending the response + setTimeout((function() { + res.status(599).send(JSON.stringify({'Error':'Try Again'})) + }), 800); + }); + + mstream.get('/access-denied', function (req, res) { + res.status(598).send(JSON.stringify({'Error':'Access Denied'})); + }); + + mstream.get('/guest-access-denied', function (req, res) { + res.status(597).send(JSON.stringify({'Error':'Access Denied'})); + }); + + // route to authenticate a user (POST http://localhost:8080/api/authenticate) + mstream.post('/login', function(req, res) { + let username = req.body.username; + let password = req.body.password; + + // Check is user is in array + if(typeof Users[username] === 'undefined') { + // user does not exist + return res.redirect('/login-failed'); + } + + // Check is password is correct + bcrypt.compare(password, Users[username]['password'], function(err, match) { + if(match == false){ + // Password does not match + return res.redirect('/login-failed'); + } + + + var sendData = { + username: username, + } + + var vPath; + if(Users[username].guestTo){ + vPath = Users[Users[username].guestTo].vPath; + }else{ + vPath = Users[username].vPath; + } + + // return the information including token as JSON + var sendThis = { + success: true, + message: 'Welcome To mStream', + vPath: vPath, + token: jwt.sign(sendData, program.secret) // Make the token + }; + + res.send(JSON.stringify(sendThis)); + }); + }); + + // Guest Users are not allowed to access these functions + const forbiddenFunctions = ['/db/recursive-scan', '/saveplaylist', '/deleteplaylist']; + + // Middleware that checks for token + mstream.use(function(req, res, next) { + // check header or url parameters or post parameters for token + var token = req.body.token || req.query.token || req.headers['x-access-token']; + if (!token) { + return res.redirect('/access-denied'); + } + + // verifies secret and checks exp + jwt.verify(token, program.secret, function(err, decoded) { + if (err) { + return res.redirect('/access-denied'); + } + + // Check if share token + if(decoded.shareToken && decoded.shareToken === true){ + // We limit the endpoints to download and anythign in the allowedFiles array + // TODO: There's gottab be a better way to habdle vpaths + // TODO: Add vpath to allowedFiles when it's created ??? + // console.log(decodeURIComponent(req.path.substring(38))); + if(req.path !== '/download' && decoded.allowedFiles.indexOf(decodeURIComponent(req.path.substring(38))) === -1){ // The substring is to cut out the vPath + return res.redirect('/guest-access-denied'); + } + req.allowedFiles = decoded.allowedFiles; + next(); + return; + } + // User may access those files and no others + + // Check for any hardcoded restrictions baked right into token + if(decoded.restrictedFunctions && decoded.restrictedFunctions.indexOf(req.path) != -1){ + return res.redirect('/guest-access-denied'); + } + + // TODO: Verify that users in token exist and vPath matches + // TODO: Longterm goal - use vPath from request variable instead of having the user manually add it + req.user = Users[decoded.username]; + req.user.username = decoded.username; + + // Deny guest access + if(req.user.guestTo && forbiddenFunctions.indexOf(req.path) != -1){ + return res.redirect('/guest-access-denied'); + } + + + // Set user request data + // TODO: Should we clone this in stead of referencing it ??? + if(req.user.guestTo){ + // Setup guest credentials based and normal user credentials + req.user.username = req.user.guestTo; + req.user.vPath = Users[req.user.guestTo].vPath; + req.user.musicDir = Users[req.user.guestTo].musicDir; + } + next(); + }); + }); + + // Setup Music Dirs here so they are protected by middleware + for (var key in permissionsMap) { + mstream.use( '/' + permissionsMap[key] + '/' , express.static( key )); + } +} diff --git a/mstream.js b/mstream.js index 3111279..feb966a 100755 --- a/mstream.js +++ b/mstream.js @@ -10,11 +10,6 @@ const mstream = express(); const fs = require('fs'); // File System const fe = require('path'); const bodyParser = require('body-parser'); -const archiver = require('archiver'); // Zip Compression -const os = require('os'); -const crypto = require('crypto'); -const slash = require('slash'); -const uuidV4 = require('uuid/v4'); // Magic Middleware Things @@ -40,8 +35,8 @@ console.log('Access mStream on your local network: http://' + require('internal- // console.log('Access mStream on your local network: http://' + add + ':' + program.port); // }) + // Handle Port Forwarding -// TODO: Switch between uPNP and nat-pmp if(program.tunnel){ const tunnel = require('./modules/auto-port-forwarding.js').setup(program.tunnel, program.port); } @@ -66,9 +61,12 @@ try{ } } + + // JukeBox const jukebox = require('./modules/jukebox.js'); jukebox.setup2(mstream, server, program); +// Serve Jukebox Page mstream.all('/remote', function (req, res) { res.sendFile( fe.join('public', 'remote.html'), { root: __dirname }); }); @@ -76,7 +74,6 @@ mstream.all('/remote', function (req, res) { // Shared const sharedModule = require('./modules/shared.js'); sharedModule.setupBeforeSecurity(mstream, program); - // Serve the shared webapp mstream.all('/shared/*', function (req, res) { res.sendFile( fe.join('public', 'shared.html'), { root: __dirname }); @@ -87,204 +84,8 @@ mstream.all('/shared/*', function (req, res) { // Login functionality if(program.users){ - // Use bcrypt for password storage - const bcrypt = require('bcrypt'); - const jwt = require('jsonwebtoken'); // used to create, sign, and verify tokens - - - - - // TODO: Add New user functionality - // Check for root user and password - // Add credentials to user array - - // TODO: Need a way to store and use already hashed passwords - - - // TODO: password change function - mstream.post('/change-password-request', function (req, res) { - // Get email address from request - // validate email against user array - // Generate change password token - // Invalidate all other change password tokens - // Email the user the token - - res.sendFile( 'COMING SOON!' ); - }); - - mstream.post('/change-password', function (req, res){ - // Check token - // Get new password - // Hash password and update user array - - res.sendFile( 'COMING SOON!' ); - }); - - - // Create the user array - var Users = program.users; - var permissionsMap = {}; - - for (let username in Users) { - // Setup user password - generateSaltedPassword(username, Users[username]["password"]); - - // If this is a guest user, continue - if(Users[username].guestTo){ - continue; - } - - // If dir has not been added yet - if ( !(Users[username].musicDir in permissionsMap) ){ - // Generate unique vPath if necessary - // The best way is to store the vPath in the JSON file - if(!Users[username].vPath){ - Users[username].vPath = uuidV4(); - } - - // Add to permissionsMap - permissionsMap[Users[username].musicDir] = Users[username].vPath; - }else{ - Users[username].vPath = permissionsMap[Users[username].musicDir]; - } - - } - - - function generateSaltedPassword(username, password){ - bcrypt.genSalt(10, function(err, salt) { - bcrypt.hash(password, salt, function(err, hash) { - // Store hash in your password DB. - Users[username]['password'] = hash; - }); - }); - } - - // Failed Login Attempt - mstream.get('/login-failed', function (req, res) { - // Wait before sending the response - setTimeout((function() { - res.status(599).send(JSON.stringify({'Error':'Try Again'})) - }), 800); - }); - - mstream.get('/access-denied', function (req, res) { - res.status(598).send(JSON.stringify({'Error':'Access Denied'})); - }); - - mstream.get('/guest-access-denied', function (req, res) { - res.status(597).send(JSON.stringify({'Error':'Access Denied'})); - }); - - // route to authenticate a user (POST http://localhost:8080/api/authenticate) - mstream.post('/login', function(req, res) { - let username = req.body.username; - let password = req.body.password; - - // Check is user is in array - if(typeof Users[username] === 'undefined') { - // user does not exist - return res.redirect('/login-failed'); - } - - // Check is password is correct - bcrypt.compare(password, Users[username]['password'], function(err, match) { - if(match == false){ - // Password does not match - return res.redirect('/login-failed'); - } - - - var sendData = { - username: username, - } - - var vPath; - if(Users[username].guestTo){ - vPath = Users[Users[username].guestTo].vPath; - }else{ - vPath = Users[username].vPath; - } - - // return the information including token as JSON - var sendThis = { - success: true, - message: 'Welcome To mStream', - vPath: vPath, - token: jwt.sign(sendData, program.secret) // Make the token - }; - - res.send(JSON.stringify(sendThis)); - }); - }); - - // Guest Users are not allowed to access these functions - const forbiddenFunctions = ['/db/recursive-scan', '/saveplaylist', '/deleteplaylist']; - - // Middleware that checks for token - mstream.use(function(req, res, next) { - // check header or url parameters or post parameters for token - var token = req.body.token || req.query.token || req.headers['x-access-token']; - if (!token) { - return res.redirect('/access-denied'); - } - - // verifies secret and checks exp - jwt.verify(token, program.secret, function(err, decoded) { - if (err) { - return res.redirect('/access-denied'); - } - - // Check if share token - if(decoded.shareToken && decoded.shareToken === true){ - // We limit the endpoints to download and anythign in the allowedFiles array - // TODO: There's gottab be a better way to habdle vpaths - // TODO: Add vpath to allowedFiles when it's created ??? - // console.log(decodeURIComponent(req.path.substring(38))); - if(req.path !== '/download' && decoded.allowedFiles.indexOf(decodeURIComponent(req.path.substring(38))) === -1){ // The substring is to cut out the vPath - return res.redirect('/guest-access-denied'); - } - req.allowedFiles = decoded.allowedFiles; - next(); - return; - } - // User may access those files and no others - - // Check for any hardcoded restrictions baked right into token - if(decoded.restrictedFunctions && decoded.restrictedFunctions.indexOf(req.path) != -1){ - return res.redirect('/guest-access-denied'); - } - - // TODO: Verify that users in token exist and vPath matches - // TODO: Longterm goal - use vPath from request variable instead of having the user manually add it - req.user = Users[decoded.username]; - req.user.username = decoded.username; - - // Deny guest access - if(req.user.guestTo && forbiddenFunctions.indexOf(req.path) != -1){ - return res.redirect('/guest-access-denied'); - } - - - // Set user request data - // TODO: Should we clone this in stead of referencing it ??? - if(req.user.guestTo){ - // Setup guest credentials based and normal user credentials - req.user.username = req.user.guestTo; - req.user.vPath = Users[req.user.guestTo].vPath; - req.user.musicDir = Users[req.user.guestTo].musicDir; - } - next(); - }); - }); - - // Setup Music Dirs here so they are protected by middleware - for (var key in permissionsMap) { - mstream.use( '/' + permissionsMap[key] + '/' , express.static( key )); - } - + require('./modules/login.js').setup(mstream, program, express); }else{ - // Dummy data mstream.use(function(req, res, next) { req.user = { @@ -312,129 +113,21 @@ mstream.get('/ping', function(req, res){ -// parse directories -mstream.post('/dirparser', function (req, res) { - var directories = []; - var filesArray = []; - - // TODO: Make sure path is a sub-path of the user's music dir - var path = fe.join(req.user.musicDir, req.body.dir); - // Make sure it's a directory - if(!fs.statSync( path).isDirectory()){ - res.status(500).json({ error: 'Not a directory' }); - return; - } - - // Will only show these files. Prevents people from snooping around - // TODO: Move to global vairable - const masterFileTypesArray = ["mp3", "flac", "wav", "ogg", "aac", "m4a"]; - var fileTypesArray; - if(req.body.filetypes){ - fileTypesArray = JSON.parse(req.body.filetypes); - }else{ - fileTypesArray = masterFileTypesArray; - } - // get directory contents - var files = fs.readdirSync( path); +// Download Files Call +require('./modules/download.js').setup(mstream, program); - // loop through files - for (let i=0; i < files.length; i++) { +// File Explorer API Call +require('./modules/file-explorer.js').setup(mstream, program); - try{ - var stat = fs.statSync(fe.join(path, files[i])); - }catch(error){ - // Bad file, ignore and continue - // TODO: Log This - continue; - } +// Load database plugin system +require('./modules/db-management/database-master.js').setup(mstream, program); - // Handle Directories - if(stat.isDirectory()){ - directories.push({ - type:"directory", - name:files[i] - }); - }else{ // Handle Files - var extension = getFileType(files[i]); - if (fileTypesArray.indexOf(extension) > -1 && masterFileTypesArray.indexOf(extension) > -1) { - filesArray.push({ - type:extension, // TODO: Should this be changed - name:files[i] - }); - } - } - } +// Finish setting up the jukebox and shared +jukebox.setup(mstream, server, program); +sharedModule.setupAfterSecurity(mstream, program); - var returnPath = slash( fe.relative(req.user.musicDir, path) ); - if(returnPath.slice(-1) !== '/'){ - returnPath += '/'; - } - - // Send back combined list of directories and mp3s - res.send( - JSON.stringify({ path:returnPath, contents:filesArray.concat(directories)}) - ); -}); - - - -function getFileType(filename){ - return filename.split(".").pop(); -} - - -// Download a zip file of music -mstream.post('/download', function (req, res){ - var archive = archiver('zip'); - - archive.on('error', function(err) { - console.log(err.message); - res.status(500).send('{error: '+err.message+'}'); - }); - - archive.on('end', function() { - // TODO: add logging - }); - - //set the archive name - // TODO: Rename this - res.attachment('zipped-playlist.zip'); - - //streaming magic - archive.pipe(res); - - var fileArray; - - - // Get the POSTed files - if(req.allowedFiles){ - fileArray = allowedFiles; - }else{ - fileArray = JSON.parse(req.body.fileArray); - } - - for(var i in fileArray) { - // TODO: Confirm each item in posted data is a real file - var fileString = fileArray[i]; - archive.file(fe.normalize( fileString), { name: fe.basename(fileString) }); - } - - archive.finalize(); -}); - - - -// ============================================================================ - -// New Way -// We need to pull this from the program var -var dbSettings = program.database_plugin; -const mstreamDB = require('./modules/db-management/database-master.js'); -mstreamDB.setup(mstream, program); - -// ============================================================================ // TODO: Add individual song @@ -444,26 +137,9 @@ mstream.get('/db/add-songs', function(req, res){ // TODO: Get Album Art calls mstream.post( '/get-album-art', function(req, res){ - // Get filepath from post - // Check if album art is in DB - // Return If So - // Pull album art from file stream - // ??? Lookup album art via 3rd party ??? - res.send('Coming Soon!'); }); - - - -jukebox.setup(mstream, server, program); -sharedModule.setupAfterSecurity(mstream, program); - - - -//////////////////////////////////////////////////////////////////////////// -/////////////////// SPECIALITY HIGHER LEVEL COMMANDS ///////////////////// - mstream.post( '/scrape-user-info', function(req, res){ // The idea behind this is to hav a function that dumps a JSON of all relevant user info // UUIDs @@ -477,21 +153,8 @@ mstream.post( '/scrape-user-info', function(req, res){ }); -mstream.post('/sunset-user', function(req,res){ - // Removes all user info -}); - -mstream.post('/add-user', function(req,res){ - // Add a user -}); - -/////////////////// SPECIALITY HIGHER LEVEL COMMANDS /////////////////// - - // Start the server! // TODO: Check if port is in use befoe firing up server -// const server = mstream.listen(program.port, function () {}); - server.on('request', mstream); server.listen(program.port, function () { });