Compare commits

...

5 Commits

Author SHA1 Message Date
Girish Ramakrishnan fd723cf7eb 3.4.3 changes 2018-12-16 21:07:53 -08:00
Girish Ramakrishnan f2c8102a6a track last oom time using a global variable
because it was a local variable, we were just sending out oom mails
like crazy

also, fixes an issue that if docker.getEvents gets stuck because
docker does not respond then we do not do any health monitoring.
i guess this can happen if the docker API gets stuck.

(cherry picked from commit a536e9fc4b)
2018-12-16 21:07:14 -08:00
Girish Ramakrishnan 6589ba0988 Add LDAP_MAILBOXES_BASE_DN
this got removed by mistake in the email refactor assuming this
was unused (but it is used by sogo)
2018-12-16 18:44:46 -08:00
Girish Ramakrishnan 2d7f0c3ebe 3.4.2 changes 2018-12-15 09:28:18 -08:00
Girish Ramakrishnan a66bc7192d 3.4.1 changes 2018-12-14 17:35:54 -08:00
6 changed files with 157 additions and 38 deletions
+31
View File
@@ -1474,3 +1474,34 @@
* Flexible mailbox management * Flexible mailbox management
* Automatic updates can be toggled per app * Automatic updates can be toggled per app
[3.4.1]
* Improve error page
* Add system view to manage addons and view their status
* Fix iconset regression for account and Cloudron name edits
* Add server reboot button and warn if reboot is required for security updates
* Backup and update tasks are now cancelable
* Move graphite away from port 3000 (reserved by ESXi)
* Flexible mailbox management
* Automatic updates can be toggled per app
[3.4.2]
* Improve error page
* Add system view to manage addons and view their status
* Fix iconset regression for account and Cloudron name edits
* Add server reboot button and warn if reboot is required for security updates
* Backup and update tasks are now cancelable
* Move graphite away from port 3000 (reserved by ESXi)
* Flexible mailbox management
* Automatic updates can be toggled per app
[3.4.3]
* Improve error page
* Add system view to manage addons and view their status
* Fix iconset regression for account and Cloudron name edits
* Add server reboot button and warn if reboot is required for security updates
* Backup and update tasks are now cancelable
* Move graphite away from port 3000 (reserved by ESXi)
* Flexible mailbox management
* Automatic updates can be toggled per app
* Fix issue where OOM mails are sent out without a rate limit
+2 -1
View File
@@ -815,7 +815,8 @@ function setupEmail(app, options, callback) {
{ name: 'MAIL_SIEVE_SERVER', value: 'mail' }, { name: 'MAIL_SIEVE_SERVER', value: 'mail' },
{ name: 'MAIL_SIEVE_PORT', value: '4190' }, { name: 'MAIL_SIEVE_PORT', value: '4190' },
{ name: 'MAIL_DOMAIN', value: app.domain }, { name: 'MAIL_DOMAIN', value: app.domain },
{ name: 'MAIL_DOMAINS', value: mailInDomains } { name: 'MAIL_DOMAINS', value: mailInDomains },
{ name: 'LDAP_MAILBOXES_BASE_DN', value: 'ou=mailboxes,dc=cloudron' }
]; ];
debugApp(app, 'Setting up Email'); debugApp(app, 'Setting up Email');
+19 -19
View File
@@ -15,11 +15,12 @@ exports = module.exports = {
run: run run: run
}; };
var HEALTHCHECK_INTERVAL = 10 * 1000; // every 10 seconds. this needs to be small since the UI makes only healthy apps clickable const HEALTHCHECK_INTERVAL = 10 * 1000; // every 10 seconds. this needs to be small since the UI makes only healthy apps clickable
var UNHEALTHY_THRESHOLD = 10 * 60 * 1000; // 10 minutes const UNHEALTHY_THRESHOLD = 10 * 60 * 1000; // 10 minutes
var gHealthInfo = { }; // { time, emailSent } let gHealthInfo = { }; // { time, emailSent }
const NOOP_CALLBACK = function (error) { if (error) console.error(error); }; const OOM_MAIL_LIMIT = 60 * 60 * 1000; // 60 minutes
let gLastOomMailTime = Date.now() - (5 * 60 * 1000); // pretend we sent email 5 minutes ago
function debugApp(app) { function debugApp(app) {
assert(typeof app === 'object'); assert(typeof app === 'object');
@@ -118,13 +119,11 @@ function checkAppHealth(app, callback) {
apt-get update && apt-get install stress apt-get update && apt-get install stress
stress --vm 1 --vm-bytes 200M --vm-hang 0 stress --vm 1 --vm-bytes 200M --vm-hang 0
*/ */
function processDockerEvents(interval, callback) { function processDockerEvents(intervalSecs, callback) {
assert.strictEqual(typeof interval, 'number'); assert.strictEqual(typeof intervalSecs, 'number');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
const OOM_MAIL_LIMIT = 60 * 60 * 1000; // 60 minutes const since = ((new Date().getTime() / 1000) - intervalSecs).toFixed(0);
let lastOomMailTime = new Date(new Date() - OOM_MAIL_LIMIT);
const since = ((new Date().getTime() / 1000) - interval).toFixed(0);
const until = ((new Date().getTime() / 1000) - 1).toFixed(0); const until = ((new Date().getTime() / 1000) - 1).toFixed(0);
docker.getEvents({ since: since, until: until, filters: JSON.stringify({ event: [ 'oom' ] }) }, function (error, stream) { docker.getEvents({ since: since, until: until, filters: JSON.stringify({ event: [ 'oom' ] }) }, function (error, stream) {
@@ -136,15 +135,17 @@ function processDockerEvents(interval, callback) {
appdb.getByContainerId(ev.id, function (error, app) { // this can error for addons appdb.getByContainerId(ev.id, function (error, app) { // this can error for addons
var program = error || !app.appStoreId ? ev.id : app.appStoreId; var program = error || !app.appStoreId ? ev.id : app.appStoreId;
var context = JSON.stringify(ev); var context = JSON.stringify(ev);
var now = new Date(); var now = Date.now();
if (app) context = context + '\n\n' + JSON.stringify(app, null, 4) + '\n'; if (app) context = context + '\n\n' + JSON.stringify(app, null, 4) + '\n';
debug('OOM Context: %s', context); const notifyUser = (!app || !app.debugMode) && (now - gLastOomMailTime > OOM_MAIL_LIMIT);
debug('OOM Context: %s. notifyUser: %s. lastOomTime: %s (now: %s)', context, notifyUser, gLastOomMailTime, now);
// do not send mails for dev apps // do not send mails for dev apps
if ((!app || !app.debugMode) && (now - lastOomMailTime > OOM_MAIL_LIMIT)) { if (notifyUser) {
mailer.oomEvent(program, context); // app can be null if it's an addon crash mailer.oomEvent(program, context); // app can be null if it's an addon crash
lastOomMailTime = now; gLastOomMailTime = now;
} }
}); });
}); });
@@ -181,14 +182,13 @@ function processApp(callback) {
}); });
} }
function run(interval, callback) { function run(intervalSecs, callback) {
assert.strictEqual(typeof interval, 'number'); assert.strictEqual(typeof intervalSecs, 'number');
assert.strictEqual(typeof callback, 'function');
callback = callback || NOOP_CALLBACK;
async.series([ async.series([
processDockerEvents.bind(null, interval), processApp, // this is first because docker.getEvents seems to get 'stuck' sometimes
processApp processDockerEvents.bind(null, intervalSecs)
], function (error) { ], function (error) {
if (error) debug(error); if (error) debug(error);
+2 -3
View File
@@ -22,7 +22,6 @@ var appHealthMonitor = require('./apphealthmonitor.js'),
dyndns = require('./dyndns.js'), dyndns = require('./dyndns.js'),
eventlog = require('./eventlog.js'), eventlog = require('./eventlog.js'),
janitor = require('./janitor.js'), janitor = require('./janitor.js'),
reverseProxy = require('./reverseproxy.js'),
scheduler = require('./scheduler.js'), scheduler = require('./scheduler.js'),
settings = require('./settings.js'), settings = require('./settings.js'),
updater = require('./updater.js'), updater = require('./updater.js'),
@@ -48,7 +47,7 @@ var gJobs = {
appHealthMonitor: null appHealthMonitor: null
}; };
var NOOP_CALLBACK = function (error) { if (error) console.error(error); }; var NOOP_CALLBACK = function (error) { if (error) debug(error); };
var AUDIT_SOURCE = { userId: null, username: 'cron' }; var AUDIT_SOURCE = { userId: null, username: 'cron' };
// cron format // cron format
@@ -202,7 +201,7 @@ function recreateJobs(tz) {
if (gJobs.appHealthMonitor) gJobs.appHealthMonitor.stop(); if (gJobs.appHealthMonitor) gJobs.appHealthMonitor.stop();
gJobs.appHealthMonitor = new CronJob({ gJobs.appHealthMonitor = new CronJob({
cronTime: '*/10 * * * * *', // every 10 seconds cronTime: '*/10 * * * * *', // every 10 seconds
onTick: appHealthMonitor.run.bind(null, 10), onTick: appHealthMonitor.run.bind(null, 10, NOOP_CALLBACK),
start: true, start: true,
timeZone: tz timeZone: tz
}); });
+39 -7
View File
@@ -450,8 +450,39 @@ function authorizeUserForApp(req, res, next) {
}); });
} }
function authenticateMailbox(req, res, next) { function authenticateUserMailbox(req, res, next) {
debug('mailbox auth: %s (from %s)', req.dn.toString(), req.connection.ldap.id); debug('user mailbox auth: %s (from %s)', req.dn.toString(), req.connection.ldap.id);
if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString()));
var email = req.dn.rdns[0].attrs.cn.value.toLowerCase();
var parts = email.split('@');
if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString()));
mail.getDomain(parts[1], function (error, domain) {
if (error && error.reason === MailError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.message));
if (!domain.enabled) return next(new ldap.NoSuchObjectError(req.dn.toString()));
mailboxdb.getMailbox(parts[0], parts[1], function (error, mailbox) {
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.message));
users.verify(mailbox.ownerId, req.credentials || '', function (error, result) {
if (error && error.reason === UsersError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error && error.reason === UsersError.WRONG_PASSWORD) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.message));
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: email }, { userId: result.id, user: users.removePrivateFields(result) });
res.end();
});
});
});
}
function authenticateMailAddon(req, res, next) {
debug('mail addon auth: %s (from %s)', req.dn.toString(), req.connection.ldap.id);
if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString())); if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString()));
@@ -516,12 +547,13 @@ function start(callback) {
gServer.bind('ou=users,dc=cloudron', authenticateApp, authenticateUser, authorizeUserForApp); gServer.bind('ou=users,dc=cloudron', authenticateApp, authenticateUser, authorizeUserForApp);
// http://www.ietf.org/proceedings/43/I-D/draft-srivastava-ldap-mail-00.txt // http://www.ietf.org/proceedings/43/I-D/draft-srivastava-ldap-mail-00.txt
gServer.search('ou=mailboxes,dc=cloudron', mailboxSearch); gServer.search('ou=mailboxes,dc=cloudron', mailboxSearch); // haraka, dovecot
gServer.search('ou=mailaliases,dc=cloudron', mailAliasSearch); gServer.bind('ou=mailboxes,dc=cloudron', authenticateUserMailbox); // apps like sogo can use domain=${domain} to authenticate a mailbox
gServer.search('ou=mailinglists,dc=cloudron', mailingListSearch); gServer.search('ou=mailaliases,dc=cloudron', mailAliasSearch); // haraka
gServer.search('ou=mailinglists,dc=cloudron', mailingListSearch); // haraka
gServer.bind('ou=recvmail,dc=cloudron', authenticateMailbox); // dovecot gServer.bind('ou=recvmail,dc=cloudron', authenticateMailAddon); // dovecot
gServer.bind('ou=sendmail,dc=cloudron', authenticateMailbox); // haraka gServer.bind('ou=sendmail,dc=cloudron', authenticateMailAddon); // haraka
gServer.compare('cn=users,ou=groups,dc=cloudron', authenticateApp, groupUsersCompare); gServer.compare('cn=users,ou=groups,dc=cloudron', authenticateApp, groupUsersCompare);
gServer.compare('cn=admins,ou=groups,dc=cloudron', authenticateApp, groupAdminsCompare); gServer.compare('cn=admins,ou=groups,dc=cloudron', authenticateApp, groupAdminsCompare);
+64 -8
View File
@@ -110,6 +110,8 @@ function setup(done) {
appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, APP_0.ownerId, apps._translatePortBindings(APP_0.portBindings, APP_0.manifest), APP_0, callback); appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, APP_0.ownerId, apps._translatePortBindings(APP_0.portBindings, APP_0.manifest), APP_0, callback);
}); });
}, },
(done) => mailboxdb.addMailbox(USER_0.username.toLowerCase(), DOMAIN_0.domain, USER_0.id, done),
(done) => mailboxdb.setAliasesForName(USER_0.username.toLowerCase(), DOMAIN_0.domain, [ USER_0_ALIAS.toLocaleLowerCase() ], done),
appdb.update.bind(null, APP_0.id, { containerId: APP_0.containerId }), appdb.update.bind(null, APP_0.id, { containerId: APP_0.containerId }),
appdb.setAddonConfig.bind(null, APP_0.id, 'sendmail', [{ name: 'MAIL_SMTP_PASSWORD', value : 'sendmailpassword' }]), appdb.setAddonConfig.bind(null, APP_0.id, 'sendmail', [{ name: 'MAIL_SMTP_PASSWORD', value : 'sendmailpassword' }]),
appdb.setAddonConfig.bind(null, APP_0.id, 'recvmail', [{ name: 'MAIL_IMAP_PASSWORD', value : 'recvmailpassword' }]), appdb.setAddonConfig.bind(null, APP_0.id, 'recvmail', [{ name: 'MAIL_IMAP_PASSWORD', value : 'recvmailpassword' }]),
@@ -812,10 +814,6 @@ describe('Ldap', function () {
} }
describe('search mailbox', function () { describe('search mailbox', function () {
before(function (done) {
mailboxdb.addMailbox(USER_0.username.toLowerCase(), DOMAIN_0.domain, USER_0.id, done);
});
it('get specific mailbox by email', function (done) { it('get specific mailbox by email', function (done) {
ldapSearch('cn=' + USER_0.username + '@example.com,ou=mailboxes,dc=cloudron', 'objectclass=mailbox', function (error, entries) { ldapSearch('cn=' + USER_0.username + '@example.com,ou=mailboxes,dc=cloudron', 'objectclass=mailbox', function (error, entries) {
if (error) return done(error); if (error) return done(error);
@@ -848,10 +846,6 @@ describe('Ldap', function () {
}); });
describe('search aliases', function () { describe('search aliases', function () {
before(function (done) {
mailboxdb.setAliasesForName(USER_0.username.toLowerCase(), DOMAIN_0.domain, [ USER_0_ALIAS.toLocaleLowerCase() ], done);
});
it('get specific alias', function (done) { it('get specific alias', function (done) {
ldapSearch('cn=' + USER_0_ALIAS + '@example.com,ou=mailaliases,dc=cloudron', 'objectclass=nismailalias', function (error, entries) { ldapSearch('cn=' + USER_0_ALIAS + '@example.com,ou=mailaliases,dc=cloudron', 'objectclass=nismailalias', function (error, entries) {
if (error) return done(error); if (error) return done(error);
@@ -919,6 +913,68 @@ describe('Ldap', function () {
}); });
}); });
describe('user mailbox bind', function () {
it('email disabled - cannot find domain email', function (done) {
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
client.bind('cn=' + USER_0.username + '@example.com,domain=example.com,ou=mailboxes,dc=cloudron', USER_0.password + 'nope', function (error) {
expect(error).to.be.a(ldap.NoSuchObjectError);
client.unbind(done);
});
});
it('email enabled - does not allow with invalid password', function (done) {
// use maildb to not trigger further events
maildb.update(DOMAIN_0.domain, { enabled: true }, function (error) {
expect(error).not.to.be.ok();
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
client.bind('cn=' + USER_0.username + '@example.com,domain=example.com,ou=mailboxes,dc=cloudron', USER_0.password + 'nope', function (error) {
expect(error).to.be.a(ldap.InvalidCredentialsError);
client.unbind();
maildb.update(DOMAIN_0.domain, { enabled: false }, done);
});
});
});
it('email enabled - allows with valid password', function (done) {
// use maildb to not trigger further events
maildb.update(DOMAIN_0.domain, { enabled: true }, function (error) {
expect(error).not.to.be.ok();
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
client.bind('cn=' + USER_0.username + '@example.com,domain=example.com,ou=mailboxes,dc=cloudron', USER_0.password, function (error) {
expect(error).not.to.be.ok();
client.unbind();
maildb.update(DOMAIN_0.domain, { enabled: false }, done);
});
});
});
it('email enabled - cannot auth with alias', function (done) {
// use maildb to not trigger further events
maildb.update(DOMAIN_0.domain, { enabled: true }, function (error) {
expect(error).not.to.be.ok();
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
client.bind('cn=' + USER_0_ALIAS + '@example.com,domain=example.com,ou=mailboxes,dc=cloudron', USER_0.password, function (error) {
expect(error).to.be.a(ldap.NoSuchObjectError);
client.unbind();
maildb.update(DOMAIN_0.domain, { enabled: false }, done);
});
});
});
});
describe('user sendmail bind', function () { describe('user sendmail bind', function () {
it('email disabled - cannot find domain email', function (done) { it('email disabled - cannot find domain email', function (done) {
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') }); var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });