mStream/mstream.js
2017-02-08 14:27:55 -05:00

497 lines
13 KiB
JavaScript
Executable File

#!/usr/bin/env node
"use strict";
const server = require('http').createServer();
const express = require('express');
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');
// Get the server config
const program = require('./modules/configure-json-file.js').setup(process.argv, __dirname);
if(program.error){
console.log(program.error);
process.exit();
}
// Magic Middleware Things
mstream.use(bodyParser.json()); // support json encoded bodies
mstream.use(bodyParser.urlencoded({ extended: true })); // support encoded bodies
// Setup WebApp
if(program.userinterface){
mstream.use( '/public', express.static(fe.join(__dirname, program.userinterface) ));
// Serve the webapp
mstream.get('/', function (req, res) {
res.sendFile( fe.join('public', 'mstream.html'), { root: __dirname });
});
}
// Print the local network IP
console.log('Access mStream locally: http://localhost:' + program.port);
require('dns').lookup(require('os').hostname(), function (err, add, fam) {
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);
}
// TODO: Move this to the configure module
// Setup Secret for JWT
try{
// IF user entered a filepath
if(fs.statSync(program.secret).isFile()){
program.secret = fs.readFileSync(program.secret, 'utf8');
}
}catch(error){
if(program.secret){
// just use secret as is
program.secret = String(program.secret);
}else{
// If no secret was given, generate one
require('crypto').randomBytes(48, function(err, buffer) {
program.secret = buffer.toString('hex');
});
}
}
// JukeBox
const jukebox = require('./modules/jukebox.js');
jukebox.setup2(mstream, server, program);
mstream.all('/remote', function (req, res) {
res.sendFile( fe.join('public', 'remote.html'), { root: __dirname });
});
// 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,
vPath: Users[username].vPath
}
// return the information including token as JSON
var sendThis = {
success: true,
message: 'Welcome To mStream',
vPath: sendData.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'];
// decode 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');
}
// TODO Check for restricted files
// 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(decoded.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 ));
}
}else{
// Dummy data
mstream.use(function(req, res, next) {
req.user = {
username:"mstream-user",
musicDir:process.cwd()
};
next();
});
mstream.use( '/' , express.static( process.cwd() ));
}
var sharedTokenMap = {
};
// mstream.use( '/public-shared', express.static(fe.join(__dirname, 'public-shared') ));
// Serve the webapp
mstream.all('/shared/*', function (req, res) {
res.sendFile( fe.join('public', 'shared.html'), { root: __dirname });
});
// Setup shared
mstream.post('/make-shared', function(req, res){
// get files from POST request
// Add vPath to these files
// make JSON token using files
// Set token expiration
// return token and link
});
// Get files
mstream.get('/get-shared', function(req, res){
// Decode token and
});
// Test function
// Used to determine the user has a working login token
mstream.get('/ping', function(req, res){
// TODO: Guest status
res.json({
vPath: req.user.vPath,
guest: false
});
});
// 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);
// 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();
}
// 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);
// Get the POSTed files
var 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
mstream.get('/db/add-songs', function(req, res){
res.send('Coming Soon!');
});
// 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);
////////////////////////////////////////////////////////////////////////////
/////////////////// 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
// Password hashes
// Jukebox client IDs
// DB settings
// All info in the initilization ini
// A higher level program can use this information to spin up an identical server
// That way high bandwith users can be spun onto their own processes
});
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 () { });