Files
cloudron-box/src/routes/cloudron.js
2020-02-04 12:56:10 -08:00

297 lines
10 KiB
JavaScript

'use strict';
exports = module.exports = {
login: login,
logout: logout,
passwordResetRequest: passwordResetRequest,
passwordReset: passwordReset,
reboot: reboot,
isRebootRequired: isRebootRequired,
getConfig: getConfig,
getDisks: getDisks,
getMemory: getMemory,
getUpdateInfo: getUpdateInfo,
update: update,
checkForUpdates: checkForUpdates,
getLogs: getLogs,
getLogStream: getLogStream,
setDashboardAndMailDomain: setDashboardAndMailDomain,
prepareDashboardDomain: prepareDashboardDomain,
renewCerts: renewCerts,
getServerIp: getServerIp,
syncExternalLdap: syncExternalLdap
};
let assert = require('assert'),
async = require('async'),
auditSource = require('../auditsource.js'),
BoxError = require('../boxerror.js'),
clients = require('../clients.js'),
cloudron = require('../cloudron.js'),
constants = require('../constants.js'),
custom = require('../custom.js'),
externalLdap = require('../externalldap.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
passport = require('passport'),
speakeasy = require('speakeasy'),
sysinfo = require('../sysinfo.js'),
system = require('../system.js'),
tokendb = require('../tokendb.js'),
updater = require('../updater.js'),
users = require('../users.js'),
updateChecker = require('../updatechecker.js');
function login(req, res, next) {
passport.authenticate('local', function (error, user) {
if (error) return next(new HttpError(500, error));
if (!user) return next(new HttpError(401, 'Invalid credentials'));
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
if (!user.ghost && !user.appPassword && user.twoFactorAuthenticationEnabled) {
if (!req.body.totpToken) return next(new HttpError(401, 'A totpToken must be provided'));
let verified = speakeasy.totp.verify({ secret: user.twoFactorAuthenticationSecret, encoding: 'base32', token: req.body.totpToken, window: 2 });
if (!verified) return next(new HttpError(401, 'Invalid totpToken'));
}
const auditSource = { authType: 'cli', ip: ip };
clients.issueDeveloperToken(user, auditSource, function (error, result) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, result));
});
})(req, res, next);
}
function logout(req, res) {
var token;
// this determines the priority
if (req.body && req.body.access_token) token = req.body.access_token;
if (req.query && req.query.access_token) token = req.query.access_token;
if (req.headers && req.headers.authorization) {
var parts = req.headers.authorization.split(' ');
if (parts.length == 2) {
var scheme = parts[0];
var credentials = parts[1];
if (/^Bearer$/i.test(scheme)) token = credentials;
}
}
if (!token) return res.redirect('/login.html');
tokendb.delByAccessToken(token, function () { res.redirect('/login.html'); });
}
function passwordResetRequest(req, res, next) {
if (!req.body.identifier || typeof req.body.identifier !== 'string') return next(new HttpError(401, 'A identifier must be non-empty string'));
users.resetPasswordByIdentifier(req.body.identifier, function (error) {
if (error && error.reason !== BoxError.NOT_FOUND) console.error(error);
next(new HttpSuccess(202, {}));
});
}
function passwordReset(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.resetToken !== 'string') return next(new HttpError(400, 'Missing resetToken'));
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'Missing password'));
users.getByResetToken(req.body.resetToken, function (error, userObject) {
if (error) return next(new HttpError(401, 'Invalid resetToken'));
if (!userObject.username) return next(new HttpError(409, 'No username set'));
// setPassword clears the resetToken
users.setPassword(userObject.id, req.body.password, function (error) {
if (error && error.reason === BoxError.BAD_FIELD) return next(new HttpError(406, error.message));
if (error) return next(new HttpError(500, error));
clients.addTokenByUserId('cid-webadmin', userObject.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, { accessToken: result.accessToken }));
});
});
});
}
function reboot(req, res, next) {
// Finish the request, to let the appstore know we triggered the reboot
next(new HttpSuccess(202, {}));
cloudron.reboot(function () {});
}
function isRebootRequired(req, res, next) {
cloudron.isRebootRequired(function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { rebootRequired: result }));
});
}
function getConfig(req, res, next) {
cloudron.getConfig(function (error, cloudronConfig) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, cloudronConfig));
});
}
function getDisks(req, res, next) {
system.getDisks(function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, result));
});
}
function getMemory(req, res, next) {
system.getMemory(function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, result));
});
}
function update(req, res, next) {
if ('skipBackup' in req.body && typeof req.body.skipBackup !== 'boolean') return next(new HttpError(400, 'skipBackup must be a boolean'));
// this only initiates the update, progress can be checked via the progress route
updater.updateToLatest(req.body, auditSource.fromRequest(req), function (error, taskId) {
if (error && error.reason === BoxError.NOT_FOUND) return next(new HttpError(422, error.message));
if (error && error.reason === BoxError.BAD_STATE) return next(new HttpError(409, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, { taskId }));
});
}
function getUpdateInfo(req, res, next) {
next(new HttpSuccess(200, { update: updateChecker.getUpdateInfo() }));
}
function checkForUpdates(req, res, next) {
// it can take a while sometimes to get all the app updates one by one
req.clearTimeout();
async.series([
updateChecker.checkAppUpdates,
updateChecker.checkBoxUpdates
], function () {
next(new HttpSuccess(200, { update: updateChecker.getUpdateInfo() }));
});
}
function getLogs(req, res, next) {
assert.strictEqual(typeof req.params.unit, 'string');
var lines = 'lines' in req.query ? parseInt(req.query.lines, 10) : 10; // we ignore last-event-id
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a number'));
var options = {
lines: lines,
follow: false,
format: req.query.format || 'json'
};
cloudron.getLogs(req.params.unit, options, function (error, logStream) {
if (error) return next(BoxError.toHttpError(error));
res.writeHead(200, {
'Content-Type': 'application/x-logs',
'Content-Disposition': 'attachment; filename="log.txt"',
'Cache-Control': 'no-cache',
'X-Accel-Buffering': 'no' // disable nginx buffering
});
logStream.pipe(res);
});
}
function getLogStream(req, res, next) {
assert.strictEqual(typeof req.params.unit, 'string');
var lines = 'lines' in req.query ? parseInt(req.query.lines, 10) : 10; // we ignore last-event-id
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a valid number'));
function sse(id, data) { return 'id: ' + id + '\ndata: ' + data + '\n\n'; }
if (req.headers.accept !== 'text/event-stream') return next(new HttpError(400, 'This API call requires EventStream'));
var options = {
lines: lines,
follow: true,
format: req.query.format || 'json'
};
cloudron.getLogs(req.params.unit, options, function (error, logStream) {
if (error) return next(BoxError.toHttpError(error));
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'X-Accel-Buffering': 'no', // disable nginx buffering
'Access-Control-Allow-Origin': '*'
});
res.write('retry: 3000\n');
res.on('close', logStream.close);
logStream.on('data', function (data) {
var obj = JSON.parse(data);
res.write(sse(obj.monotonicTimestamp, JSON.stringify(obj))); // send timestamp as id
});
logStream.on('end', res.end.bind(res));
logStream.on('error', res.end.bind(res, null));
});
}
function setDashboardAndMailDomain(req, res, next) {
if (!req.body.domain || typeof req.body.domain !== 'string') return next(new HttpError(400, 'domain must be a string'));
cloudron.setDashboardAndMailDomain(req.body.domain, auditSource.fromRequest(req), function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(204, {}));
});
}
function prepareDashboardDomain(req, res, next) {
if (!req.body.domain || typeof req.body.domain !== 'string') return next(new HttpError(400, 'domain must be a string'));
cloudron.prepareDashboardDomain(req.body.domain, auditSource.fromRequest(req), function (error, taskId) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId }));
});
}
function renewCerts(req, res, next) {
cloudron.renewCerts({ domain: req.body.domain || null }, auditSource.fromRequest(req), function (error, taskId) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId }));
});
}
function syncExternalLdap(req, res, next) {
externalLdap.startSyncer(function (error, taskId) {
if (error) return next(new HttpError(500, error.message));
next(new HttpSuccess(202, { taskId: taskId }));
});
}
function getServerIp(req, res, next) {
sysinfo.getServerIp(function (error, ip) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { ip }));
});
}