yunkong2.admin/lib/web.js
2018-09-14 19:26:46 +08:00

462 lines
20 KiB
JavaScript

/* jshint -W097 */
/* jshint strict: false */
/* jslint node: true */
/* jshint -W061 */
'use strict';
const Stream = require('stream');
const utils = require(__dirname + '/utils'); // Get common adapter utils
const LE = require(utils.controllerDir + '/lib/letsencrypt.js');
const express = require('express');
const fs = require('fs');
let path; // will be loaded later
let session;
let bodyParser;
let AdapterStore;
let password;
let passport;
let LocalStrategy;
let flash;
let cookieParser;
let fileUpload;
function Web(settings, adapter, onReady) {
if (!(this instanceof Web)) return new Web(settings, adapter, onReady);
const server = {
app: null,
server: null
};
const bruteForce = {};
let store = null;
let loginPage;
this.server = server;
this.close = () => server.server && server.server.close();
function decorateLogFile(filename) {
const prefix = '<html><head>' +
'<style>\n' +
' table {' +
' font-family: monospace;\n' +
' font-size: 14px;\n' +
' }\n' +
' .info {\n' +
' background: white;' +
' }\n' +
' .type {\n' +
' font-weight: bold;' +
' }\n' +
' .silly {\n' +
' background: #b3b3b3;' +
' }\n' +
' .debug {\n' +
' background: lightgray;' +
' }\n' +
' .warn {\n' +
' background: #ffdb75;' +
' color: white;' +
' }\n' +
' .error {\n' +
' background: #ff6a5b;' +
' }\n' +
'</style>\n' +
'<script>\n' +
'function decorate (line) {\n' +
' var className = "info";\n' +
' line = line.replace(/\\x1B\\[39m/g, "</span>");\n' +
' if (line.indexOf("[32m") !== -1) {\n' +
' className = "info";\n'+
' line = line.replace(/\\x1B\\[32m/g, "<span class=\\"type\\">");\n' +
' } else \n' +
' if (line.indexOf("[34m") !== -1) {\n' +
' className = "debug";\n'+
' line = line.replace(/\\x1B\\[34m/g, "<span class=\\"type\\">");\n' +
' } else \n' +
' if (line.indexOf("[33m") !== -1) {\n' +
' className = "warn";\n'+
' line = line.replace(/\\x1B\\[33m/g, "<span class=\\"type\\">");\n' +
' } else \n' +
' if (line.indexOf("[31m") !== -1) {\n' +
' className = "error";\n'+
' line = line.replace(/\\x1B\\[31m/g, "<span class=\\"type\\">");\n' +
' } else \n' +
' if (line.indexOf("[35m") !== -1) {\n' +
' className = "silly";\n'+
' line = line.replace(/\\x1B\\[35m/g, "<span class=\\"type\\">");\n' +
' } else {\n' +
' }\n' +
' return "<tr class=\\"" + className + "\\"><td>" + line + "</td></tr>";\n'+
'}\n' +
'document.addEventListener("DOMContentLoaded", function () { \n' +
' var text = document.body.innerHTML;\n' +
' var lines = text.split("\\n");\n' +
' text = "<table>";\n' +
' for (var i = 0; i < lines.length; i++) {\n' +
' if (lines[i]) text += decorate(lines[i]);\n' +
' }\n' +
' text += "</table>";\n' +
' document.body.innerHTML = text;\n' +
' window.scrollTo(0,document.body.scrollHeight);\n' +
'});\n' +
'</script>\n</head>\n<body>\n';
const suffix = '</body></html>';
const log = fs.readFileSync(filename).toString();
return prefix + log + suffix;
}
function prepareLoginTemplate() {
let def = 'background: #64b5f6;\n';
let template = fs.readFileSync(__dirname + '/../www/login/index.html').toString('utf8');
if (adapter.config.loginBackgroundColor) {
def = 'background-color: ' + adapter.config.loginBackgroundColor + ';\n'
}
if (adapter.config.loginBackgroundImage) {
def += ' background-image: url(../' + adapter.namespace + '/login-bg.png);\n';
}
if (adapter.config.loginHideLogo) {
template = template.replace('.logo { display: block }', '.logo { display: none }');
}
if (adapter.config.loginMotto) {
template = template.replace('Discover awesome. <a href="http://yunkong2.net/" target="_blank">yunkong2</a>', adapter.config.loginMotto);
}
return template.replace('background: #64b5f6;', def);
}
//settings: {
// "port": 8080,
// "auth": false,
// "secure": false,
// "bind": "0.0.0.0", // "::"
// "cache": false
//}
(function __construct () {
if (settings.port) {
server.app = express();
if (settings.auth) {
session = require('express-session');
cookieParser = require('cookie-parser');
bodyParser = require('body-parser');
AdapterStore = require(utils.controllerDir + '/lib/session.js')(session, settings.ttl);
password = require(utils.controllerDir + '/lib/password.js');
passport = require('passport');
LocalStrategy = require('passport-local').Strategy;
flash = require('connect-flash'); // TODO report error to user
store = new AdapterStore({adapter: adapter});
passport.use(new LocalStrategy(
(username, password, done) => {
if (bruteForce[username] && bruteForce[username].errors > 4) {
let minutes = (new Date().getTime() - bruteForce[username].time);
if (bruteForce[username].errors < 7) {
if ((new Date().getTime() - bruteForce[username].time) < 60000) {
minutes = 1;
} else {
minutes = 0;
}
} else
if (bruteForce[username].errors < 10) {
if ((new Date().getTime() - bruteForce[username].time) < 180000) {
minutes = Math.ceil((180000 - minutes) / 60000);
} else {
minutes = 0;
}
} else
if (bruteForce[username].errors < 15) {
if ((new Date().getTime() - bruteForce[username].time) < 600000) {
minutes = Math.ceil((600000 - minutes) / 60000);
} else {
minutes = 0;
}
} else
if ((new Date().getTime() - bruteForce[username].time) < 3600000) {
minutes = Math.ceil((3600000 - minutes) / 60000);
} else {
minutes = 0;
}
if (minutes) {
return done('Too many errors. Try again in ' + minutes + ' ' + (minutes === 1 ? 'minute' : 'minutes') + '.', false);
}
}
adapter.checkPassword(username, password, res => {
if (!res) {
bruteForce[username] = bruteForce[username] || {errors: 0};
bruteForce[username].time = new Date().getTime();
bruteForce[username].errors++;
} else if (bruteForce[username]) {
delete bruteForce[username];
}
if (res) {
return done(null, username);
} else {
return done(null, false);
}
});
}
));
passport.serializeUser((user, done) => done(null, user));
passport.deserializeUser((user, done) => done(null, user));
server.app.use(cookieParser());
server.app.use(bodyParser.urlencoded({
extended: true
}));
server.app.use(bodyParser.json());
server.app.use(session({
secret: settings.secret,
saveUninitialized: true,
resave: true,
cookie: {
maxAge: adapter.config.ttl * 1000
},
store: store
}));
server.app.use(passport.initialize());
server.app.use(passport.session());
server.app.use(flash());
server.app.post('/login', (req, res, next) => {
let redirect = '/';
if (req.body.origin) {
const parts = req.body.origin.match(/href=(.+)$/);
if (parts && parts[1]) {
redirect = decodeURIComponent(parts[1]);
}
}
passport.authenticate('local', {
successRedirect: redirect,
failureRedirect: '/login/index.html' + req.body.origin + (req.body.origin ? '&error' : '?error'),
failureFlash: 'Invalid username or password.'
})(req, res, next);
});
server.app.get('/logout', (req, res) => {
req.logout();
res.redirect('/login/index.html');
});
server.app.get('/login/index.html', (req, res) => {
loginPage = loginPage || prepareLoginTemplate();
res.contentType('text/html');
res.status(200).send(loginPage);
});
// route middleware to make sure a user is logged in
server.app.use((req, res, next) => {
if (!req.isAuthenticated()) {
if (/admin\.\d+\/login-bg\.png(\?.*)?$/.test(req.originalUrl)) {
// Read names of files for gong
adapter.objects.readFile(adapter.namespace, 'login-bg.png', null, (err, file) => {
if (!err && file) {
res.set('Content-Type', 'image/png');
res.status(200).send(file);
} else {
res.status(404).send();
}
});
} else if (/^\/login\//.test(req.originalUrl) ||
/\.ico(\?.*)?$/.test(req.originalUrl)) {
return next();
} else {
res.redirect('/login/index.html?href=' + encodeURIComponent(req.originalUrl));
}
} else {
return next();
}
});
} else {
server.app.get('/login', (req, res) => {
res.redirect('/');
});
server.app.get('/logout', (req, res) => {
res.redirect('/');
});
}
// send log files
server.app.get('/log/*', (req, res) => {
let parts = req.url.split('/');
parts = parts.splice(2);
const transport = parts.shift();
let filename = parts.join('/');
const config = adapter.systemConfig;
// detect file log
if (config && config.log && config.log.transport) {
if (config.log.transport.hasOwnProperty(transport) && config.log.transport[transport].type === 'file') {
path = path || require('path');
if (config.log.transport[transport].filename) {
parts = config.log.transport[transport].filename.replace(/\\/g, '/').split('/');
parts.pop();
filename = path.join(parts.join('/'), filename);
} else {
filename = path.join('log/', filename) ;
}
if (filename[0] !== '/' && !filename.match(/^\W:/)) {
filename = path.normalize(__dirname + '/../../../') + filename;
}
if (fs.existsSync(filename)) {
const stat = fs.lstatSync(filename);
if (stat.size > 2 * 1024 * 1024) {
res.sendFile(filename);
} else {
res.send(decorateLogFile(filename));
}
return;
}
}
}
res.status(404).send('File ' + filename + ' not found');
});
const appOptions = {};
if (settings.cache) {
appOptions.maxAge = 30758400000;
}
if (settings.tmpPathAllow && settings.tmpPath) {
server.app.use('/tmp/', express.static(settings.tmpPath, {maxAge: 0}));
fileUpload = fileUpload || require('express-fileupload');
server.app.use(fileUpload({
useTempFiles: true,
tempFilePath: settings.tmpPath
}));
server.app.post('/upload', (req, res) => {
if (!req.files) {
return res.status(400).send('No files were uploaded.');
}
// The name of the input field (i.e. "sampleFile") is used to retrieve the uploaded file
let myFile;
for (const name in req.files) {
if (req.files.hasOwnProperty(name)) {
myFile = req.files[name];
break;
}
}
if (myFile) {
if (myFile.data && myFile.data.length > 600 * 1024 * 1024) {
return res.status(500).send('File is too big. (Max 600MB)');
}
// Use the mv() method to place the file somewhere on your server
myFile.mv(settings.tmpPath + '/restore.iob', err => {
if (err) {
res.status(500).send(err);
} else {
res.send('File uploaded!');
}
});
} else {
return res.status(500).send('File not uploaded');
}
});
}
if (!fs.existsSync(__dirname + '/../www')) {
server.app.use('/', (req, res) => {
res.send('This adapter cannot be installed directly from github.<br>You must install it from npm.<br>Write for that <i>"npm install yunkong2.admin"</i> in according directory.');
});
} else {
server.app.use('/', express.static(__dirname + '/../www', appOptions));
}
// reverse proxy with url rewrite for couchdb attachments in <adapter-name>.admin
server.app.use('/adapter/', (req, res) => {
// Example: /example/?0
let url = req.url;
// add index.html
url = url.replace(/\/($|\?|#)/, '/index.html$1');
// Read config files for admin from /adapters/admin/admin/...
if (url.substring(0, '/' + adapter.name + '/'.length) === '/' + adapter.name + '/') {
url = url.replace('/' + adapter.name + '/', __dirname + '/../admin/');
url = url.replace(/\?[0-9]*/, '');
try {
if (fs.existsSync(url)) {
fs.createReadStream(url).pipe(res);
} else {
const ss = new Stream();
ss.pipe = dest => dest.write('File not found');
ss.pipe(res);
}
} catch (e) {
const s = new Stream();
s.pipe = dest => dest.write('File not found: ' + e);
s.pipe(res);
}
return;
}
url = url.split('/');
// Skip first /
url.shift();
// Get ID
const id = url.shift() + '.admin';
url = url.join('/');
const pos = url.indexOf('?');
if (pos !== -1) {
url = url.substring(0, pos);
}
adapter.readFile(id, url, null, (err, buffer, mimeType) => {
if (!buffer || err) {
res.contentType('text/html');
res.status(404).send('File ' + url + ' not found');
} else {
if (mimeType) {
res.contentType(mimeType['content-type'] || mimeType);
} else {
res.contentType('text/javascript');
}
res.send(buffer);
}
});
});
server.server = LE.createServer(server.app, settings, adapter.config.certificates, adapter.config.leConfig, adapter.log);
server.server.__server = server;
} else {
adapter.log.error('port missing');
process.exit(1);
}
if (server.server) {
settings.port = parseInt(settings.port, 10);
adapter.getPort(settings.port, port => {
if (port !== settings.port && !adapter.config.findNextPort) {
adapter.log.error('port ' + settings.port + ' already in use');
process.exit(1);
}
server.server.listen(port, (!settings.bind || settings.bind === '0.0.0.0') ? undefined : settings.bind || undefined);
adapter.log.info('http' + (settings.secure ? 's' : '') + ' server listening on port ' + port);
adapter.log.info('Use link "http' + (settings.secure ? 's' : '') + '://localhost:' + port + '" to configure.');
if (typeof onReady === 'function') {
onReady(server.server, store);
}
});
}
if (server.server) {
return server;
} else {
return null;
}
})();
return this;
}
module.exports = Web;