Initial commit
This commit is contained in:
+462
@@ -0,0 +1,462 @@
|
||||
/* 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;
|
||||
Reference in New Issue
Block a user