mStream/mstream-electron.js
2018-03-08 20:40:19 -05:00

524 lines
15 KiB
JavaScript

const {app, BrowserWindow, ipcMain, Tray, Menu, dialog, shell} = require('electron');
const fs = require('fs');
const fe = require('path');
const os = require('os');
const publicIp = require('public-ip');
const semver = require('semver')
const superagent = require('superagent');
const currentVer = '0.8.0';
var apiKey;
const ddnsDomain = 'https://ddns.mstream.io';
let appIcon = null;
const mkdirp = require('mkdirp');
const AutoLaunch = require('auto-launch');
var mstreamAutoLaunch = new AutoLaunch({
name: 'mStream'
});
if (!fs.existsSync(fe.join(app.getPath('userData'), 'image-cache'))){
mkdirp(fe.join(app.getPath('userData'), 'image-cache'), function(){});
}
if (!fs.existsSync(fe.join(app.getPath('userData'), 'save'))){
mkdirp(fe.join(app.getPath('userData'), 'save'), function(){});
}
// Errors
process.on('uncaughtException', function (error) {
// Handle Known Errors
if(error.code === 'EADDRINUSE'){
// Handle the error
dialog.showErrorBox("Server Boot Error", "The port you selected is already in use. Please choose another");
}else if(error.code === 'BAD CERTS'){
dialog.showErrorBox("Server Boot Error", "Faield to create HTTPS server. Plese check your certs and try again. "+os.EOL+os.EOL+os.EOL+"ERROR MESSAGE: " + error.message);
}
// Unknown Errors
else{
dialog.showErrorBox("Unknown Error", "Unknown Error with code: " + error.code +os.EOL+os.EOL+os.EOL+"ERROR MESSAGE: " + error.message);
console.log(error);
// TODO: Dump error details to a file
}
// Temporarily disable autoboot
fs.writeFileSync( fe.join(app.getPath('userData'), 'save/temp-boot-disable.json'), JSON.stringify({disable:true}), 'utf8');
// Reboot the app
app.relaunch();
app.quit();
});
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', createMainWindow);
// Quit if server hasn't been started
app.on('window-all-closed', function () {
if(!server){
app.quit();
}
if(process.platform === 'darwin'){
app.dock.hide()
}
})
app.on('activate', function () {
// On OS X it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (mainWindow === null) {
createMainWindow();
}
})
// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let mainWindow;
function createMainWindow () {
if(server || mainWindow){
// TODO: Should we diplay a stats window here?
return;
}
// Create the browser window.
mainWindow = new BrowserWindow({width: 550 , height: 775, icon: fe.join(__dirname, '/electron/mstream-logo-cut.png') });
// and load the index.html of the app.
mainWindow.loadURL('file://' + __dirname + '/electron/index2.html');
mainWindow.setMenu(null);
// Open the DevTools.
// mainWindow.webContents.openDevTools()
// Emitted when the window is closed.
mainWindow.on('closed', function () {
// Dereference the window object, usually you would store windows
// in an array if your app supports multi windows, this is the time
// when you should delete the corresponding element.
mainWindow = null;
});
}
let infoWindow;
function createInfoWindow(name){
// Close current Info Window
if(infoWindow){
infoWindow.close();
}
// Create new Window
infoWindow = new BrowserWindow({width: 600 , height: 400, icon: fe.join(__dirname, '/electron/mstream-logo-cut.png') });
// and load the index.html of the app.
infoWindow.loadURL('file://' + __dirname + '/electron/windows/' + name + '.html');
infoWindow.setMenu(null);
// Emitted when the window is closed.
infoWindow.on('closed', function () {
infoWindow = null;
});
}
// TODO: Combine this function into the info window function
let learnMoreWindow;
function createLearnMoreWindow(){
if(learnMoreWindow){
return
}
learnMoreWindow = new BrowserWindow({width: 1050 , height: 950, icon: fe.join(__dirname, '/mstream-logo-cut.png') });
// and load the index.html of the app.
learnMoreWindow.loadURL('file://' + __dirname + '/electron/windows/managed-ddns-ssl-learn-more.html');
learnMoreWindow.setMenu(null);
// Emitted when the window is closed.
learnMoreWindow.on('closed', function () {
learnMoreWindow = null;
});
}
ipcMain.on('port-forward-window', function(event, arg) {
createInfoWindow('portforward');
});
ipcMain.on('auto-boot-window', function(event, arg) {
createInfoWindow('autoboot');
});
ipcMain.on('managed-window', function(event, arg) {
createLearnMoreWindow();
});
// Boot Server Event
ipcMain.once('start-server', function(event, arg) {
bootServer(arg);
});
// Flush DNS Cache event
ipcMain.once('flush-dns-cache', function(event, arg) {
flushDNSCache();
});
var server;
function bootServer(program2) {
// TODO: Verify port and folder
var program = {
port: program2.port,
userinterface: 'public',
database_plugin: {
dbPath: fe.join(app.getPath('userData'), 'save/mstreamXdb.lite')
},
// musicDir: program2.filepath
folders: {
'media': {'root': program2.filepath}
}
}
// Generate Secret Key if there isn't one already
try{
if(fs.statSync(fe.join(app.getPath('userData'), 'save/secret.key')).isFile()){
program.secret = fs.readFileSync(fe.join(app.getPath('userData'), 'save/secret.key'), 'utf8');
}
}catch(error){
let buff = require('crypto').randomBytes(256).toString('hex');
program.secret = buff;
fs.writeFileSync( fe.join(app.getPath('userData'), 'save/secret.key'), buff, 'utf8');
}
if(program2.user){
program.users = {};
program.users[program2.user] = {};
program.users[program2.user].password = program2.password;
program.users[program2.user].vpaths = ['media'];
}
if(program2.cert && program2.key){
program.ssl = {};
program.ssl.key = program2.key;
program.ssl.cert = program2.cert;
}
if(program2.tunnel){
program.tunnel = {}
if(program2.interval && program2.refresh){
program.tunnel.refreshInterval = program2.interval;
}
// if(program.gateway){
// program3.tunnel.gateway = program.gateway;
// }
if(program2.protocol){
program.tunnel.protocol = program2.protocol;
}
}
program.albumArtDir = fe.join(app.getPath('userData'), 'image-cache');
// Save config
if((program2.saveconfig && program2.saveconfig == true) || (program2.autoboot && program2.autoboot === true)){
fs.writeFileSync( fe.join(app.getPath('userData'), 'save/mstreaserver-config.json'), JSON.stringify(program2), 'utf8');
fs.writeFileSync( fe.join(app.getPath('userData'), 'save/temp-boot-disable.json'), JSON.stringify({disable:false}), 'utf8');
}
// Tray Template Object
var trayTemplate = [
{ label: 'mStream Express v' + currentVer, click: function(){
shell.openExternal('http://mstream.io/mstream-express');
}},
{ label: 'Check for latest version', click: function(err, res){
superagent.get('https://ddns.mstream.io/current-version/mstream-express').end(function(err, res){
if (err || !res.ok) {
console.log('Error checking for latest version');
} else {
if(semver.gt(res.text, currentVer)){
trayTemplate[1].label = 'Download latest version v' + res.text;
trayTemplate[1].click = function(){
shell.openExternal('http://mstream.io/mstream-express');
}
appIcon.setContextMenu(Menu.buildFromTemplate(trayTemplate));
shell.openExternal('http://mstream.io/mstream-express');
}else{
createInfoWindow('latest-ver');
}
}
});
}},
{type: 'separator'},
{ label: 'Links', submenu: []},
{type: 'separator'},
{ label: 'Disable Autoboot', click: function(){
// app.isQuiting = true;
// app.quit();
mstreamAutoLaunch.disable();
try{
if( fs.statSync(fe.join(app.getPath('userData'), 'save/mstreaserver-config.json')).isFile()){
var loadJson = JSON.parse(fs.readFileSync(fe.join(app.getPath('userData'), 'save/mstreaserver-config.json'), 'utf8'));
loadJson.autoboot = false;
fs.writeFileSync( fe.join(app.getPath('userData'), 'save/mstreaserver-config.json'), JSON.stringify(loadJson), 'utf8');
}
}catch(error){
console.log('Failed To Load JSON');
return;
}
} },
{ label: 'Restart and Reconfigure', click: function(){
fs.writeFileSync( fe.join(app.getPath('userData'), 'save/temp-boot-disable.json'), JSON.stringify({disable:true}), 'utf8');
app.relaunch();
app.isQuiting = true;
app.quit();
} },
{ label: 'Advanced Options', submenu: [
{ label: 'Flush DNS (exprimental)', click: function(){
flushDNSCache();
} }
] },
{type: 'separator'},
{ label: 'Managed DDNS + SSL', submenu: [
{ label: 'Learn More', click: function(){
createLearnMoreWindow();
} },
{type: 'separator'},
{ label: 'Restart Server To Sign Up', click: function(){
fs.writeFileSync( fe.join(app.getPath('userData'), 'save/temp-boot-disable.json'), JSON.stringify({disable:true}), 'utf8');
app.relaunch();
app.isQuiting = true;
app.quit();
} },
] },
{type: 'separator'},
{ label: 'Donate', click: function(){
shell.openExternal('https://www.patreon.com/mstream')
} },
{type: 'separator'},
{ label: 'Quit', click: function(){
app.isQuiting = true;
app.quit();
} }
]
// Create Tray Icon
// Determine appropriate icon for platform
var trayImage = '';
if (process.platform === 'darwin') {
trayImage = fe.join(__dirname, '/electron/images/icon.png');
}
else {
trayImage = fe.join(__dirname, '/electron/mstream-logo-cut.png')
}
appIcon = new Tray(trayImage);
var contextMenu = Menu.buildFromTemplate(trayTemplate);
// TODO: Try booting server in forked thread instead. Might give some speed improvements
server = require('./mstream.js');
server.logit = function(msg){
// Push to Window
// ipcMain.send('info', msg);
if(mainWindow){
mainWindow.webContents.send('info' , msg);
}
trayTemplate[3].submenu = [];
// Update tray icon
for (var property in server.addresses) {
if (server.addresses.hasOwnProperty(property) && server.addresses[property]) {
let add = server.addresses[property];
trayTemplate[3].submenu.push({
label: add, click: function(){
shell.openExternal(add)
}
})
appIcon.setContextMenu(Menu.buildFromTemplate(trayTemplate));
}
}
}
// Boot Server
server.serveit(program);
// Call this again for Linux because we modified the context menu
appIcon.setContextMenu(contextMenu);
if(program2.autoboot && program2.autoboot === true){
mstreamAutoLaunch.enable();
mstreamAutoLaunch.isEnabled()
.then(function(isEnabled){
if(isEnabled){
return;
}
mstreamAutoLaunch.enable();
})
.catch(function(err){
// handle error
});
}
// else{
// mstreamAutoLaunch.disable();
// }
// Automatically check for new versions every day
checkForNewVer(trayTemplate);
setInterval(function(){ checkForNewVer(trayTemplate); }, 43200000);
// Check if the user is logged in
var configFile = fe.join(app.getPath('userData'), 'save/mstream-api-token.json');
try{
if( fs.statSync(configFile).isFile()){
apiKey = fs.readFileSync(configFile, 'utf8');
// Make sure key is valid and working
superagent.get('https://ddns.mstream.io/login-status?token=' + apiKey).end(function(err, res){
if (err || !res.ok) {
console.log('Error checking login status');
console.log(err);
return;
}
// Update IP every minute
setInterval(function(){ updateIP(); }, 60000);
// trayTemplate[7].submenu.push({ label: 'Update IP', click: function(){
// updateIP();
// }});
trayTemplate[9].submenu = [
{ label: 'Force IP Update', click: function(){
updateIP();
}},
{type: 'separator'},
{ label: 'Logout', click: function(){
app.isQuiting = true;
fs.writeFileSync( fe.join(app.getPath('userData'), 'save/mstream-api-token.json'), '', 'utf8');
fs.writeFileSync( fe.join(app.getPath('userData'), 'save/temp-boot-disable.json'), JSON.stringify({disable:true}), 'utf8');
app.relaunch();
app.quit();
}}
]
try{
var parsedRes = JSON.parse(res.text);
// Add domain to list of domains
var add = 'https://' + parsedRes.full_domain + ':' + program.port;
trayTemplate[3].submenu.push({
label: add, click: function(){
shell.openExternal(add)
}
})
}catch(err){
}
// trayTemplate.push({ label: 'Logout', click: function(){
// fs.writeFileSync( fe.join(app.getPath('userData'), 'save/mstream-api-token.json'), '', 'utf8');
// app.relaunch();
// app.quit();
// }})
appIcon.setContextMenu(Menu.buildFromTemplate(trayTemplate));
});
}
}catch(error){
return;
}
}
function checkForNewVer(trayTemplate){
superagent.get('https://ddns.mstream.io/current-version/mstream-express').end(function(err, res){
if (err || !res.ok) {
console.log('Error checking for latest version');
} else {
if(semver.gt(res.text, currentVer)){
trayTemplate[1].label = 'New Version Available: v' + res.text;
trayTemplate[1].click = function(){
shell.openExternal('http://mstream.io/mstream-express');
}
appIcon.setContextMenu(Menu.buildFromTemplate(trayTemplate));
}
}
});
}
// TODO: Experimental function to clean the users cache
function flushDNSCache(){
if(process.platform === 'win32'){
const ls = require('child_process').spawn('ipconfig.exe', ["\/flushdns"]);
ls.stdout.on('data', (data) => {
console.log(`stdout: ${data}`);
});
ls.stderr.on('data', (data) => {
console.log(`stderr: ${data}`);
});
ls.on('close', (code) => {
console.log(`child process exited with code ${code}`);
});
}
}
// Function that updates IP
var currentIP;
function updateIP(){
console.log('UPDATING IP')
publicIp.v4().then(ip => {
if(ip !== currentIP){
superagent.post('https://ddns.mstream.io/update/ip')
.set('x-access-token', apiKey)
.set('Accept', 'application/json')
.send({ ip: ip})
.end(function(err, res){
console.log('Update happened');
if (err || !res.ok) {
console.log('Update IP failed');
console.log(err);
}
});
}
currentIP = ip;
});
}