Compare commits

..

21 Commits

Author SHA1 Message Date
Girish Ramakrishnan 887cbb0b22 make percent non-zero 2019-12-18 09:33:44 -08:00
Johannes Zellner ca4fdc1be8 Add azure-image provider argument 2019-12-17 16:42:25 +01:00
Girish Ramakrishnan 93199c7f5b eventlog: support ticket and ssh 2019-12-16 14:06:55 -08:00
Girish Ramakrishnan 4c6566f42f stopped apps should not be updated or auto-updated 2019-12-16 13:29:15 -08:00
Johannes Zellner c38f7d7f93 Make properties explicitly available 2019-12-16 15:21:26 +01:00
Girish Ramakrishnan da85cea329 avatar: remove query param
let the ui add the size and default
2019-12-13 13:45:02 -08:00
Girish Ramakrishnan d5c70a2b11 Add sshd port warning 2019-12-13 11:32:36 -08:00
Girish Ramakrishnan fe355b4bac 4.4.2 changes 2019-12-12 20:44:54 -08:00
Girish Ramakrishnan a7dee6be51 cloudron.runSystemChecks should take a callback 2019-12-12 20:41:03 -08:00
Girish Ramakrishnan 2817dc0603 Not required to run any cron job immediately 2019-12-12 20:39:40 -08:00
Girish Ramakrishnan 6f36c72e88 Fix crash in mail.checkConfiguration 2019-12-12 20:36:27 -08:00
Girish Ramakrishnan 45e806c455 typo in comment 2019-12-12 19:54:59 -08:00
Johannes Zellner bbdd76dd37 Fix and add memory route tests 2019-12-12 13:21:24 +01:00
Johannes Zellner 09921e86c0 Remove redunandant memory property from config
we have a specific route for this now
2019-12-12 12:14:08 +01:00
Girish Ramakrishnan d6e4b64103 4.4.1 changes 2019-12-11 15:27:47 -08:00
Girish Ramakrishnan 9dd3e4537a return 422 on instance id mismatch
the ui redirects otherwise
2019-12-11 15:13:38 -08:00
Girish Ramakrishnan a5f31e8724 Revert "rename ami to aws-mp"
This reverts commit 72ac00b69a.

Existing code relies on this, so don't change it
2019-12-11 12:56:30 -08:00
Girish Ramakrishnan 72ac00b69a rename ami to aws-mp 2019-12-11 12:27:55 -08:00
Girish Ramakrishnan ae5722a7d4 eventlog: typo when mail list is removed 2019-12-11 10:05:45 -08:00
Johannes Zellner 4e3192d450 Avoid double dns setup tracking 2019-12-11 14:02:40 +01:00
Johannes Zellner ccca3aca04 Send setup state to get the actually correct ip 2019-12-10 18:01:07 +01:00
19 changed files with 169 additions and 31 deletions
+8
View File
@@ -1747,3 +1747,11 @@
* Fix various repair workflows
* acme2: Implement post-as-get
[4.4.1]
* ami: fix AWS provider validation
[4.4.2]
* Fix crash when reporting that DKIM is not setup correctly
* Stopped apps cannot be updated or auto-updated
* eventlog: track support ticket creation and remote support status
+3
View File
@@ -123,6 +123,9 @@ timedatectl set-ntp 1
# mysql follows the system timezone
timedatectl set-timezone UTC
echo "==> Adding sshd configuration warning"
sed -e '/Port 22/ i # NOTE: Cloudron only supports moving SSH to port 202. See https://cloudron.io/documentation/security/#securing-ssh-access' -i /etc/ssh/sshd_config
# Disable bind for good measure (on online.net, kimsufi servers these are pre-installed and conflicts with unbound)
systemctl stop bind9 || true
systemctl disable bind9 || true
+1
View File
@@ -99,6 +99,7 @@ if [[ -z "${provider}" ]]; then
elif [[ \
"${provider}" != "ami" && \
"${provider}" != "azure" && \
"${provider}" != "azure-image" && \
"${provider}" != "caas" && \
"${provider}" != "cloudscale" && \
"${provider}" != "contabo" && \
+3
View File
@@ -333,6 +333,9 @@ function getService(serviceName, callback) {
var tmp = {
name: serviceName,
status: null,
memoryUsed: 0,
memoryPercent: 0,
error: null,
config: {
// If a property is not set then we cannot change it through the api, see below
// memory: 0,
+4
View File
@@ -1229,6 +1229,8 @@ function update(appId, data, auditSource, callback) {
error = checkAppState(app, exports.ISTATE_PENDING_UPDATE);
if (error) return callback(error);
if (app.runState === exports.RSTATE_STOPPED) return callback(new BoxError(BoxError.BAD_STATE, 'Stopped apps cannot be updated'));
downloadManifest(data.appStoreId, data.manifest, function (error, appStoreId, manifest) {
if (error) return callback(error);
@@ -1749,6 +1751,8 @@ function canAutoupdateApp(app, newManifest) {
if (!app.enableAutomaticUpdate) return false;
if ((semver.major(app.manifest.version) !== 0) && (semver.major(app.manifest.version) !== semver.major(newManifest.version))) return false; // major changes are blocking
if (app.runState === exports.RSTATE_STOPPED) return false; // stopped apps won't run migration scripts and shouldn't be updated
const newTcpPorts = newManifest.tcpPorts || { };
const newUdpPorts = newManifest.udpPorts || { };
const portBindings = app.portBindings; // this is never null
+36 -1
View File
@@ -5,6 +5,9 @@ exports = module.exports = {
getApp: getApp,
getAppVersion: getAppVersion,
trackBeginSetup: trackBeginSetup,
trackFinishedSetup: trackFinishedSetup,
registerWithLoginCredentials: registerWithLoginCredentials,
registerWithLicense: registerWithLicense,
@@ -375,6 +378,35 @@ function registerCloudron(data, callback) {
});
}
// This works without a Cloudron token as this Cloudron was not yet registered
let gBeginSetupAlreadyTracked = false;
function trackBeginSetup(provider) {
assert.strictEqual(typeof provider, 'string');
// avoid browser reload double tracking, not perfect since box might restart, but covers most cases and is simple
if (gBeginSetupAlreadyTracked) return;
gBeginSetupAlreadyTracked = true;
const url = `${settings.apiServerOrigin()}/api/v1/helper/setup_begin`;
superagent.post(url).send({ provider }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return console.error(error.message);
if (result.statusCode !== 200) return console.error(error.message);
});
}
// This works without a Cloudron token as this Cloudron was not yet registered
function trackFinishedSetup(domain) {
assert.strictEqual(typeof domain, 'string');
const url = `${settings.apiServerOrigin()}/api/v1/helper/setup_finished`;
superagent.post(url).send({ domain }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return console.error(error.message);
if (result.statusCode !== 200) return console.error(error.message);
});
}
function registerWithLicense(license, domain, callback) {
assert.strictEqual(typeof license, 'string');
assert.strictEqual(typeof domain, 'string');
@@ -415,13 +447,14 @@ function registerWithLoginCredentials(options, callback) {
});
}
function createTicket(info, callback) {
function createTicket(info, auditSource, callback) {
assert.strictEqual(typeof info, 'object');
assert.strictEqual(typeof info.email, 'string');
assert.strictEqual(typeof info.displayName, 'string');
assert.strictEqual(typeof info.type, 'string');
assert.strictEqual(typeof info.subject, 'string');
assert.strictEqual(typeof info.description, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
function collectAppInfoIfNeeded(callback) {
@@ -446,6 +479,8 @@ function createTicket(info, callback) {
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
if (result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
eventlog.add(eventlog.ACTION_SUPPORT_TICKET, auditSource, info);
callback(null);
});
});
+1 -1
View File
@@ -825,7 +825,7 @@ function update(app, args, progressCallback, callback) {
async.series([
// this protects against the theoretical possibility of an app being marked for update from
// a previous version of box code
progressCallback.bind(null, { percent: 0, message: 'Verify manifest' }),
progressCallback.bind(null, { percent: 5, message: 'Verify manifest' }),
verifyManifest.bind(null, updateConfig.manifest),
function (next) {
+2 -2
View File
@@ -48,7 +48,7 @@ function scheduleTask(appId, taskId, callback) {
if (Object.keys(gActiveTasks).length >= TASK_CONCURRENCY) {
debug(`Reached concurrency limit, queueing task id ${taskId}`);
tasks.update(taskId, { percent: 0, message: 'Waiting for other app tasks to complete' }, NOOP_CALLBACK);
tasks.update(taskId, { percent: 1, message: 'Waiting for other app tasks to complete' }, NOOP_CALLBACK);
gPendingTasks.push({ appId, taskId, callback });
return;
}
@@ -57,7 +57,7 @@ function scheduleTask(appId, taskId, callback) {
if (lockError) {
debug(`Could not get lock. ${lockError.message}, queueing task id ${taskId}`);
tasks.update(taskId, { percent: 0, message: waitText(lockError.operation) }, NOOP_CALLBACK);
tasks.update(taskId, { percent: 1, message: waitText(lockError.operation) }, NOOP_CALLBACK);
gPendingTasks.push({ appId, taskId, callback });
return;
}
+4 -5
View File
@@ -134,7 +134,6 @@ function getConfig(callback) {
mailFqdn: settings.mailFqdn(),
version: constants.VERSION,
isDemo: settings.isDemo(),
memory: os.totalmem(),
provider: settings.provider(),
cloudronName: allSettings[settings.CLOUDRON_NAME_KEY],
uiSpec: custom.uiSpec()
@@ -154,14 +153,14 @@ function isRebootRequired(callback) {
}
// called from cron.js
function runSystemChecks() {
function runSystemChecks(callback) {
assert.strictEqual(typeof callback, 'function');
async.parallel([
checkBackupConfiguration,
checkMailStatus,
checkRebootRequired
], function (error) {
debug('runSystemChecks: done', error);
});
], callback);
}
function checkBackupConfiguration(callback) {
+1 -3
View File
@@ -107,9 +107,8 @@ function recreateJobs(tz) {
if (gJobs.systemChecks) gJobs.systemChecks.stop();
gJobs.systemChecks = new CronJob({
cronTime: '00 30 * * * *', // every 30 minutes. if you change this interval, change the notification messages with correct duration
onTick: cloudron.runSystemChecks,
onTick: () => cloudron.runSystemChecks(NOOP_CALLBACK),
start: true,
runOnInit: true, // run system check immediately
timeZone: tz
});
@@ -118,7 +117,6 @@ function recreateJobs(tz) {
cronTime: '00 30 * * * *', // every 30 minutes. if you change this interval, change the notification messages with correct duration
onTick: () => system.checkDiskSpace(NOOP_CALLBACK),
start: true,
runOnInit: true, // run system check immediately
timeZone: tz
});
+3
View File
@@ -57,6 +57,9 @@ exports = module.exports = {
ACTION_DYNDNS_UPDATE: 'dyndns.update',
ACTION_SUPPORT_TICKET: 'support.ticket',
ACTION_SUPPORT_SSH: 'support.ssh',
ACTION_PROCESS_CRASH: 'system.crash'
};
+4 -4
View File
@@ -199,7 +199,7 @@ function checkDkim(mailDomain, callback) {
};
var dkimKey = readDkimPublicKeySync(domain);
if (!dkimKey) return callback(new BoxError(BoxError.FS_ERROR, `Failed to read dkim public key of ${domain}`));
if (!dkimKey) return callback(new BoxError(BoxError.FS_ERROR, `Failed to read dkim public key of ${domain}`), dkim);
dkim.expected = 'v=DKIM1; t=s; p=' + dkimKey;
@@ -456,7 +456,7 @@ function getStatus(domain, callback) {
// ensure we always have a valid toplevel properties for the api
var results = {
dns: {}, // { mx/dmar/dkim/spf/ptr: { expected, value, name, domain, type } }
dns: {}, // { mx/dmarc/dkim/spf/ptr: { expected, value, name, domain, type } }
rbl: {}, // { status, ip, servers: [{name,site,dns}]} optional. only for cloudron-smtp
relay: {} // { status, value } always checked
};
@@ -466,7 +466,7 @@ function getStatus(domain, callback) {
func(function (error, result) {
if (error) debug('Ignored error - ' + what + ':', error);
safe.set(results, what, result);
safe.set(results, what, result || {});
callback();
});
@@ -1264,7 +1264,7 @@ function removeList(name, domain, auditSource, callback) {
mailboxdb.del(name, domain, function (error) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_MAIL_LIST_ADD, auditSource, { name, domain });
eventlog.add(eventlog.ACTION_MAIL_LIST_REMOVE, auditSource, { name, domain });
callback();
});
+1 -1
View File
@@ -39,7 +39,7 @@ function get(req, res, next) {
twoFactorAuthenticationEnabled: req.user.twoFactorAuthenticationEnabled,
admin: req.user.admin,
source: req.user.source,
avatarUrl: fs.existsSync(path.join(paths.PROFILE_ICONS_DIR, req.user.id)) ? `${settings.adminOrigin()}/api/v1/profile/avatar/${req.user.id}` : `https://www.gravatar.com/avatar/${emailHash}.jpg?s=128&d=mm`
avatarUrl: fs.existsSync(path.join(paths.PROFILE_ICONS_DIR, req.user.id)) ? `${settings.adminOrigin()}/api/v1/profile/avatar/${req.user.id}` : `https://www.gravatar.com/avatar/${emailHash}.jpg`
}));
}
+14 -6
View File
@@ -9,14 +9,15 @@ exports = module.exports = {
};
var assert = require('assert'),
appstore = require('../appstore.js'),
auditSource = require('../auditsource.js'),
BoxError = require('../boxerror.js'),
debug = require('debug')('box:routes/setup'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
provision = require('../provision.js'),
settings = require('../settings.js'),
superagent = require('superagent');
request = require('request'),
settings = require('../settings.js');
function providerTokenAuth(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
@@ -24,11 +25,11 @@ function providerTokenAuth(req, res, next) {
if (settings.provider() === 'ami') {
if (typeof req.body.providerToken !== 'string' || !req.body.providerToken) return next(new HttpError(400, 'providerToken must be a non empty string'));
superagent.get('http://169.254.169.254/latest/meta-data/instance-id').timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return next(new HttpError(500, error));
if (result.statusCode !== 200) return next(new HttpError(500, 'Unable to get meta data'));
request.get('http://169.254.169.254/latest/meta-data/instance-id', { timeout: 30 * 1000 }, function (error, result) {
if (error) return next(new HttpError(422, `Network error getting EC2 metadata: ${error.message}`));
if (result.statusCode !== 200) return next(new HttpError(422, `Unable to get EC2 meta data. statusCode: ${result.statusCode}`));
if (result.text !== req.body.providerToken) return next(new HttpError(401, 'Invalid providerToken'));
if (result.body !== req.body.providerToken) return next(new HttpError(422, 'Instance ID does not match'));
next();
});
@@ -62,6 +63,8 @@ function setup(req, res, next) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
appstore.trackFinishedSetup(dnsConfig.domain);
});
}
@@ -116,5 +119,10 @@ function getStatus(req, res, next) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, status));
// check if Cloudron is not in setup state nor activated and let appstore know of the attempt
if (!status.activated && !status.setup.active && !status.restore.active) {
appstore.trackBeginSetup(status.provider);
}
});
}
+3 -2
View File
@@ -9,6 +9,7 @@ exports = module.exports = {
var appstore = require('../appstore.js'),
assert = require('assert'),
auditSource = require('../auditsource.js'),
custom = require('../custom.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
@@ -29,7 +30,7 @@ function createTicket(req, res, next) {
if (req.body.appId && typeof req.body.appId !== 'string') return next(new HttpError(400, 'appId must be string'));
if (req.body.altEmail && typeof req.body.altEmail !== 'string') return next(new HttpError(400, 'altEmail must be string'));
appstore.createTicket(_.extend({ }, req.body, { email: req.user.email, displayName: req.user.displayName }), function (error) {
appstore.createTicket(_.extend({ }, req.body, { email: req.user.email, displayName: req.user.displayName }), auditSource.fromRequest(req), function (error) {
if (error) return next(new HttpError(503, `Error contacting cloudron.io: ${error.message}. Please email ${custom.spec().support.email}`));
next(new HttpSuccess(201, { message: `An email for sent to ${custom.spec().support.email}. We will get back shortly!` }));
@@ -43,7 +44,7 @@ function enableRemoteSupport(req, res, next) {
if (typeof req.body.enable !== 'boolean') return next(new HttpError(400, 'enabled is required'));
support.enableRemoteSupport(req.body.enable, function (error) {
support.enableRemoteSupport(req.body.enable, auditSource.fromRequest(req), function (error) {
if (error) return next(new HttpError(503, 'Error enabling remote support. Try running "cloudron-support --enable-ssh" on the server'));
next(new HttpSuccess(202, {}));
+71 -1
View File
@@ -186,7 +186,6 @@ describe('Cloudron', function () {
expect(result.body.webServerOrigin).to.eql('https://cloudron.io');
expect(result.body.adminFqdn).to.eql(settings.adminFqdn());
expect(result.body.version).to.eql(constants.VERSION);
expect(result.body.memory).to.eql(os.totalmem());
expect(result.body.cloudronName).to.be.a('string');
done();
@@ -271,4 +270,75 @@ describe('Cloudron', function () {
req.on('error', done);
});
});
describe('misc routes', function () {
before(function (done) {
async.series([
setup,
function (callback) {
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
// stash token for further use
token = result.body.token;
callback();
});
},
function (callback) {
superagent.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
.send({ username: USERNAME_1, email: EMAIL_1, invite: false })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(201);
token_1 = hat(8 * 32);
userId_1 = result.body.id;
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
tokendb.add({ id: 'tid-1', accessToken: token_1, identifier: userId_1, clientId: 'test-client-id', expires: Date.now() + 100000, scope: 'cloudron', name: '' }, callback);
});
}
], done);
});
after(cleanup);
describe('memory', function () {
it('cannot get without token', function (done) {
superagent.get(SERVER_URL + '/api/v1/cloudron/memory')
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('succeeds (admin)', function (done) {
superagent.get(SERVER_URL + '/api/v1/cloudron/memory')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.memory).to.eql(os.totalmem());
expect(result.body.swap).to.be.a('number');
done();
});
});
it('fails (non-admin)', function (done) {
superagent.get(SERVER_URL + '/api/v1/cloudron/memory')
.query({ access_token: token_1 })
.end(function (error, result) {
expect(result.statusCode).to.equal(403);
done();
});
});
});
});
});
+1 -1
View File
@@ -184,7 +184,7 @@ function initializeExpressSync() {
// working off the user behind the provided token
router.get ('/api/v1/profile', profileScope, routes.profile.get);
router.post('/api/v1/profile', profileScope, routes.profile.update);
router.get ('/api/v1/profile/avatar/:identifier', routes.profile.getAvatar);
router.get ('/api/v1/profile/avatar/:identifier', routes.profile.getAvatar); // this is not scoped so it can used directly in img tag
router.post('/api/v1/profile/avatar', profileScope, multipart, routes.profile.setAvatar);
router.del ('/api/v1/profile/avatar', profileScope, routes.profile.clearAvatar);
router.post('/api/v1/profile/password', profileScope, routes.users.verifyPassword, routes.profile.changePassword);
+8 -3
View File
@@ -8,11 +8,12 @@ exports = module.exports = {
let assert = require('assert'),
BoxError = require('./boxerror.js'),
constants = require('./constants.js'),
shell = require('./shell.js'),
eventlog = require('./eventlog.js'),
once = require('once'),
path = require('path'),
paths = require('./paths.js'),
settings = require('./settings.js');
settings = require('./settings.js'),
shell = require('./shell.js');
// the logic here is also used in the cloudron-support tool
const AUTHORIZED_KEYS_CMD = path.join(__dirname, 'scripts/remotesupport.sh');
@@ -48,13 +49,17 @@ function getRemoteSupport(callback) {
cp.stdout.on('data', (data) => result = result + data.toString('utf8'));
}
function enableRemoteSupport(enable, callback) {
function enableRemoteSupport(enable, auditSource, callback) {
assert.strictEqual(typeof enable, 'boolean');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
let si = sshInfo();
shell.sudo('support', [ AUTHORIZED_KEYS_CMD, enable ? 'enable' : 'disable', si.filePath, si.user ], {}, function (error) {
if (error) callback(new BoxError(BoxError.FS_ERROR, error));
eventlog.add(eventlog.ACTION_SUPPORT_SSH, auditSource, { enable });
callback();
});
}
+1 -1
View File
@@ -125,7 +125,7 @@ function add(type, args, callback) {
assert(Array.isArray(args));
assert.strictEqual(typeof callback, 'function');
taskdb.add({ type: type, percent: 0, message: 'Queued', args: args }, function (error, taskId) {
taskdb.add({ type: type, percent: 1, message: 'Queued', args: args }, function (error, taskId) {
if (error) return callback(error);
callback(null, taskId);