mirror of
https://github.com/IrosTheBeggar/mStream.git
synced 2025-10-27 07:31:02 +00:00
497 lines
13 KiB
JavaScript
Executable File
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 () { });
|