rdio-scanner/server/index.js
2021-05-19 15:25:12 -04:00

226 lines
9.1 KiB
JavaScript

/*
* *****************************************************************************
* Copyright (C) 2019-2021 Chrystian Huot <chrystian.huot@saubeo.solutions>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
'use strict';
import { spawnSync } from 'child_process';
import compression from 'compression';
import cors from 'cors';
import EventEmitter from 'events';
import dotenv from 'dotenv';
import express from 'express';
import fs from 'fs';
import helmet from 'helmet';
import http from 'http';
import https from 'https';
import path from 'path';
import Sequelize from 'sequelize';
import url from 'url';
import { RdioScanner } from './lib/rdio-scanner/main.js';
export class App extends EventEmitter {
constructor() {
super();
const dirname = path.dirname(url.fileURLToPath(import.meta.url));
const configFile = path.resolve(process.env.APP_DATA || dirname, 'config.json');
const staticFile = 'index.html';
const staticDir = fs.existsSync(path.resolve(dirname, `../client/${staticFile}`))
? path.resolve(dirname, '../client')
: path.resolve(dirname, '../client/dist/rdio-scanner');
const openssl = !spawnSync('openssl', ['version']).error;
dotenv.config();
if (fs.existsSync(configFile)) {
this.config = JSON.parse(fs.readFileSync(configFile));
}
if (this.config === null || typeof this.config !== 'object') {
this.config = {};
}
const nodejs = this.config.nodejs || {};
this.config.nodejs = {
env: typeof nodejs.env === 'string' && nodejs.env.length ? nodejs.env : 'production',
host: typeof nodejs.host === 'string' && nodejs.host.length ? nodejs.host : '0.0.0.0',
port: typeof nodejs.port === 'number' ? nodejs.port : 3000,
ssl: typeof nodejs.ssl === 'boolean' ? nodejs.ssl : false,
sslCa: typeof nodejs.sslCa === 'string' && nodejs.sslCa.length ? nodejs.sslCa : 'ca.crt',
sslCert: typeof nodejs.sslCert === 'string' && nodejs.sslCert.length ? nodejs.sslCert : 'server.crt',
sslKey: typeof nodejs.sslKey === 'string' && nodejs.sslKey.length ? nodejs.sslKey : 'server.key',
};
const sequelize = this.config.sequelize || {};
this.config.sequelize = typeof sequelize.dialect === 'string' && sequelize.dialect.length && sequelize.dialect !== 'sqlite' ? {
dialect: sequelize.dialect,
database: typeof sequelize.database === 'string' && sequelize.database.length ? sequelize.database : 'db',
host: typeof sequelize.host === 'string' && sequelize.host.length ? sequelize.host : '',
port: typeof sequelize.port === 'number' ? sequelize.port : 3306,
username: typeof sequelize.username === 'string' && sequelize.username.length ? sequelize.username : 'user',
password: typeof sequelize.password === 'string' && sequelize.password.length ? sequelize.password : 'pass',
} : {
dialect: 'sqlite',
storage: typeof sequelize.storage === 'string' && sequelize.storage.length ? sequelize.storage : 'database.sqlite',
};
const sslCaCert = path.resolve(process.env.APP_DATA || dirname, this.config.nodejs.sslCa);
const sslServerCert = path.resolve(process.env.APP_DATA || dirname, this.config.nodejs.sslCert);
const sslServerKey = path.resolve(process.env.APP_DATA || dirname, this.config.nodejs.sslKey);
if (openssl && !(fs.existsSync(sslCaCert) && fs.existsSync(sslServerCert) && fs.existsSync(sslServerKey))) {
const sslCaKey = sslCaCert.replace(/\..*$/, '.key');
const sslCaSerial = sslCaCert.replace(/\..*$/, '.srl');
const sslServerCsr = sslServerCert.replace(/\..*$/, '.csr');
spawnSync('openssl', [
'req', '-new', '-newkey', 'rsa:4096', '-batch', '-nodes', '-x509', '-days', '7305',
'-subj', '/CN=Rdio Scanner CA', '-keyout', sslCaKey, '-out', sslCaCert
]);
spawnSync('openssl', [
'req', '-new', '-newkey', 'rsa:4096', '-batch', '-nodes', '-subj', '/CN=Rdio Scanner',
'-keyout', sslServerKey, '-out', sslServerCsr,
]);
spawnSync('openssl', [
'x509', '-in', sslServerCsr, '-days', '7305', '-req', '-CA', sslCaCert,
'-CAkey', sslCaKey, '-CAcreateserial', '-CAserial', sslCaSerial, '-out', sslServerCert,
]);
fs.rmSync(sslCaSerial);
fs.rmSync(sslServerCsr);
}
if (this.config.sequelize === null || typeof this.config.sequelize !== 'object') {
this.config.sequelize = {};
}
if (typeof this.config.sequelize.dialect !== 'string' || !this.config.sequelize.dialect.length) {
this.config.sequelize.dialect = 'sqlite';
}
if (this.config.sequelize.dialect === 'sqlite' &&
(typeof this.config.sequelize.storage !== 'string' || !this.config.sequelize.storage.length)) {
this.config.sequelize.storage = 'database.sqlite';
}
this.router = express();
this.router.disable('x-powered-by');
this.router.use(compression());
this.router.use(cors());
this.router.use(express.json({ limit: '10mb' }));
this.router.use(express.urlencoded({ extended: false, limit: '10mb' }));
this.router.use(helmet({ contentSecurityPolicy: false }));
this.router.use(express.static(staticDir));
this.router.use((req, res, next) => {
if (
req.path.startsWith('/api') ||
req.accepts('html', 'json', 'xml') !== 'html' ||
path.extname(req.path) !== ''
) {
return next();
}
if (fs.existsSync(path.join(staticDir, staticFile))) {
return res.sendFile(staticFile, { root: staticDir });
} else {
return res.send('A new build is being prepared. Please check back in a few minutes.');
}
});
this.router.set(this.config.nodejs.port);
this.sequelize = new Sequelize(Object.assign({}, this.config.sequelize, {
dialectOptions: { autoJsonMap: false },
logging: false,
storage: this.config.sequelize.storage
? path.resolve(process.env.APP_DATA || process.env.DB_STORAGE || dirname, this.config.sequelize.storage)
: '',
}));
if (
this.config.nodejs.env !== 'development' && this.config.nodejs.ssl === true &&
fs.existsSync(sslServerCert) && fs.existsSync(sslServerKey)
) {
const options = {
cert: fs.readFileSync(sslServerCert),
key: fs.readFileSync(sslServerKey),
};
if (fs.existsSync(this.config.nodejs.sslCA)) {
options.ca = fs.readFileSync(sslCaCert);
}
this.httpServer = https.createServer(options, this.router);
} else {
this.httpServer = http.createServer(this.router);
}
this.once('ready', () => {
let config = Object.keys(this.config)
.sort((a, b) => a.localeCompare(b))
.reduce((conf, key) => {
conf[key] = this.config[key];
return conf;
}, {});
try {
fs.writeFileSync(configFile, JSON.stringify(config, null, 2));
} catch (error) {
console.error(error.message);
}
});
RdioScanner.migrate(this.sequelize).then(() => {
this.rdioScanner = new RdioScanner(this);
this.rdioScanner.once('ready', () => {
// Starting from v5.1, config is now in the database.
// Delete obsolete config from the config.json file.
if ('rdioScanner' in this.config) {
delete this.config.rdioScanner;
}
this.httpServer.listen(this.config.nodejs.port, this.config.nodejs.host, () => {
const scheme = this.httpServer instanceof https.Server ? 'https' : 'http';
this.url = `${scheme}://${this.config.nodejs.host}:${this.config.nodejs.port}`;
console.log(`Server is running at ${this.url}`);
});
this.emit('ready');
});
});
}
}
export const app = new App();