Compare commits

..

3 Commits

Author SHA1 Message Date
Johannes Zellner 8bb15aedbd Add 1.9.3 changes 2018-01-18 12:51:18 +01:00
Girish Ramakrishnan 00b6588972 1.9.2 changes 2018-01-18 12:50:47 +01:00
Johannes Zellner 0bd6d3b9f8 The DNS provider property moved to the root dns config object 2018-01-18 12:50:20 +01:00
282 changed files with 54531 additions and 9022 deletions
+1
View File
@@ -1,6 +1,7 @@
node_modules/
coverage/
webadmin/dist/
setup/splash/website/
installer/src/certs/server.key
# vim swap files
-1
View File
@@ -2,7 +2,6 @@
"node": true,
"browser": true,
"unused": true,
"multistr": true,
"globalstrict": true,
"predef": [ "angular", "$" ],
"esnext": true
-42
View File
@@ -1181,45 +1181,3 @@
* Set max email recepient limit (in outgoing emails) to 500
* Put terminal and app logs viewer to separate window
[1.9.4]
* Fix typo causing LE cert renewals to fail
[1.10.0]
* Migrate mailboxes to support multiple domains
* Update addon containers to latest versions
* Add DigitalOcean Spaces region Singapore 1 (SGP1)
* Configure Exoscale SOS to use new SOS NG endpoint
* Fix S3 storage backend CopySource encoding rules
[1.10.1]
* Migrate mailboxes to support multiple domains
* Update addon containers to latest versions
* Add DigitalOcean Spaces region Singapore 1 (SGP1)
* Configure Exoscale SOS to use new SOS NG endpoint
* Fix S3 storage backend CopySource encoding rules
[1.10.2]
* Migrate mailboxes to support multiple domains
* Update addon containers to latest versions
* Add DigitalOcean Spaces region Singapore 1 (SGP1)
* Configure Exoscale SOS to use new SOS NG endpoint
* Fix S3 storage backend CopySource encoding rules
[1.11.0]
* Update Haraka to 2.8.17 to fix various crashes
* Report dependency error for clone if backup or domain was not found
* Enable auto-updates for major versions
[2.0.0]
* Multi-domain support
* Update Haraka to 2.8.18
* Split box and app autoupdate pattern settings
* Stop and disable any pre-installed postfix server
* Migrate altDomain as a manual DNS provider
* Use node's native dns resolve instead of dig
* DNS records can now be a A record or a CNAME record
* Fix generation of fallback certificates to include naked domain
* Merge multi-string DKIM records
* scheduler: do not start cron jobs all at once
* scheduler: give cron jobs a grace period of 30 minutes to complete
+1 -1
View File
@@ -59,6 +59,6 @@ the containers in the Cloudron.
## Community
* [Forum](https://forum.cloudron.io/)
* [Chat](https://chat.cloudron.io/)
* [Support](mailto:support@cloudron.io)
+1 -1
View File
@@ -29,7 +29,7 @@ function create_droplet() {
local ssh_key_id="$1"
local box_name="$2"
local image_region="sfo2"
local image_region="sfo1"
local ubuntu_image_slug="ubuntu-16-04-x64"
local box_size="1gb"
-4
View File
@@ -105,7 +105,3 @@ systemctl disable bind9 || true
systemctl stop dnsmasq || true
systemctl disable dnsmasq || true
# on ssdnodes postfix seems to run by default
systemctl stop postfix || true
systemctl disable postfix || true
+1 -8
View File
@@ -2,8 +2,6 @@
'use strict';
var database = require('./src/database.js');
var sendFailureLogs = require('./src/logcollector').sendFailureLogs;
function main() {
@@ -12,12 +10,7 @@ function main() {
var processName = process.argv[2];
console.log('Started crash notifier for', processName);
// mailer needs the db
database.initialize(function (error) {
if (error) return console.error('Cannot connect to database. Unable to send crash log.', error);
sendFailureLogs(processName, { unit: processName });
});
sendFailureLogs(processName, { unit: processName });
}
main();
+257
View File
@@ -0,0 +1,257 @@
/* jslint node:true */
'use strict';
var argv = require('yargs').argv,
autoprefixer = require('gulp-autoprefixer'),
concat = require('gulp-concat'),
cssnano = require('gulp-cssnano'),
ejs = require('gulp-ejs'),
gulp = require('gulp'),
rimraf = require('rimraf'),
sass = require('gulp-sass'),
serve = require('gulp-serve'),
sourcemaps = require('gulp-sourcemaps'),
uglify = require('gulp-uglify'),
url = require('url');
gulp.task('3rdparty', function () {
gulp.src([
'webadmin/src/3rdparty/**/*.js',
'webadmin/src/3rdparty/**/*.map',
'webadmin/src/3rdparty/**/*.css',
'webadmin/src/3rdparty/**/*.otf',
'webadmin/src/3rdparty/**/*.eot',
'webadmin/src/3rdparty/**/*.svg',
'webadmin/src/3rdparty/**/*.gif',
'webadmin/src/3rdparty/**/*.ttf',
'webadmin/src/3rdparty/**/*.woff',
'webadmin/src/3rdparty/**/*.woff2'
])
.pipe(gulp.dest('webadmin/dist/3rdparty/'))
.pipe(gulp.dest('setup/splash/website/3rdparty'));
gulp.src('node_modules/bootstrap-sass/assets/javascripts/bootstrap.min.js')
.pipe(gulp.dest('webadmin/dist/3rdparty/js'))
.pipe(gulp.dest('setup/splash/website/3rdparty/js'));
});
// --------------
// JavaScript
// --------------
if (argv.help || argv.h) {
console.log('Supported arguments for "gulp develop":');
console.log(' --client-id <clientId>');
console.log(' --client-secret <clientSecret>');
console.log(' --api-origin <cloudron api uri>');
process.exit(1);
}
gulp.task('js', ['js-index', 'js-logs', 'js-terminal', 'js-setup', 'js-setupdns', 'js-restore', 'js-update'], function () {});
var oauth = {
clientId: argv.clientId || 'cid-webadmin',
clientSecret: argv.clientSecret || 'unused',
apiOrigin: argv.apiOrigin || '',
apiOriginHostname: argv.apiOrigin ? url.parse(argv.apiOrigin).hostname : ''
};
console.log();
console.log('Using OAuth credentials:');
console.log(' ClientId: %s', oauth.clientId);
console.log(' ClientSecret: %s', oauth.clientSecret);
console.log(' Cloudron API: %s', oauth.apiOrigin || 'default');
console.log(' Cloudron Host: %s', oauth.apiOriginHostname);
console.log();
gulp.task('js-index', function () {
// needs special treatment for error handling
var uglifyer = uglify();
uglifyer.on('error', function (error) {
console.error(error);
});
gulp.src([
'webadmin/src/js/index.js',
'webadmin/src/js/client.js',
'webadmin/src/js/appstore.js',
'webadmin/src/js/main.js',
'webadmin/src/views/*.js'
])
.pipe(ejs({ oauth: oauth }, {}, { ext: '.js' }))
.pipe(sourcemaps.init())
.pipe(concat('index.js', { newLine: ';' }))
.pipe(uglifyer)
.pipe(sourcemaps.write())
.pipe(gulp.dest('webadmin/dist/js'));
});
gulp.task('js-logs', function () {
// needs special treatment for error handling
var uglifyer = uglify();
uglifyer.on('error', function (error) {
console.error(error);
});
gulp.src(['webadmin/src/js/logs.js', 'webadmin/src/js/client.js'])
.pipe(ejs({ oauth: oauth }, {}, { ext: '.js' }))
.pipe(sourcemaps.init())
.pipe(concat('logs.js', { newLine: ';' }))
.pipe(uglifyer)
.pipe(sourcemaps.write())
.pipe(gulp.dest('webadmin/dist/js'));
});
gulp.task('js-terminal', function () {
// needs special treatment for error handling
var uglifyer = uglify();
uglifyer.on('error', function (error) {
console.error(error);
});
gulp.src(['webadmin/src/js/terminal.js', 'webadmin/src/js/client.js'])
.pipe(ejs({ oauth: oauth }, {}, { ext: '.js' }))
.pipe(sourcemaps.init())
.pipe(concat('terminal.js', { newLine: ';' }))
.pipe(uglifyer)
.pipe(sourcemaps.write())
.pipe(gulp.dest('webadmin/dist/js'));
});
gulp.task('js-setup', function () {
// needs special treatment for error handling
var uglifyer = uglify();
uglifyer.on('error', function (error) {
console.error(error);
});
gulp.src(['webadmin/src/js/setup.js', 'webadmin/src/js/client.js'])
.pipe(ejs({ oauth: oauth }, {}, { ext: '.js' }))
.pipe(sourcemaps.init())
.pipe(concat('setup.js', { newLine: ';' }))
.pipe(uglifyer)
.pipe(sourcemaps.write())
.pipe(gulp.dest('webadmin/dist/js'));
});
gulp.task('js-setupdns', function () {
// needs special treatment for error handling
var uglifyer = uglify();
uglifyer.on('error', function (error) {
console.error(error);
});
gulp.src(['webadmin/src/js/setupdns.js', 'webadmin/src/js/client.js'])
.pipe(ejs({ oauth: oauth }, {}, { ext: '.js' }))
.pipe(sourcemaps.init())
.pipe(concat('setupdns.js', { newLine: ';' }))
.pipe(uglifyer)
.pipe(sourcemaps.write())
.pipe(gulp.dest('webadmin/dist/js'));
});
gulp.task('js-restore', function () {
// needs special treatment for error handling
var uglifyer = uglify();
uglifyer.on('error', function (error) {
console.error(error);
});
gulp.src(['webadmin/src/js/restore.js', 'webadmin/src/js/client.js'])
.pipe(ejs({ oauth: oauth }, {}, { ext: '.js' }))
.pipe(sourcemaps.init())
.pipe(concat('restore.js', { newLine: ';' }))
.pipe(uglifyer)
.pipe(sourcemaps.write())
.pipe(gulp.dest('webadmin/dist/js'));
});
gulp.task('js-update', function () {
// needs special treatment for error handling
var uglifyer = uglify();
uglifyer.on('error', function (error) {
console.error(error);
});
gulp.src(['webadmin/src/js/update.js'])
.pipe(sourcemaps.init())
.pipe(uglifyer)
.pipe(sourcemaps.write())
.pipe(gulp.dest('webadmin/dist/js'))
.pipe(gulp.dest('setup/splash/website/js'));
});
// --------------
// HTML
// --------------
gulp.task('html', ['html-views', 'html-update', 'html-templates'], function () {
return gulp.src('webadmin/src/*.html').pipe(ejs({ apiOriginHostname: oauth.apiOriginHostname }, {}, { ext: '.html' })).pipe(gulp.dest('webadmin/dist'));
});
gulp.task('html-update', function () {
return gulp.src(['webadmin/src/update.html']).pipe(gulp.dest('setup/splash/website'));
});
gulp.task('html-views', function () {
return gulp.src('webadmin/src/views/**/*.html').pipe(gulp.dest('webadmin/dist/views'));
});
gulp.task('html-templates', function () {
return gulp.src('webadmin/src/templates/**/*.html').pipe(gulp.dest('webadmin/dist/templates'));
});
// --------------
// CSS
// --------------
gulp.task('css', function () {
return gulp.src('webadmin/src/*.scss')
.pipe(sourcemaps.init())
.pipe(sass({ includePaths: ['node_modules/bootstrap-sass/assets/stylesheets/'] }).on('error', sass.logError))
.pipe(autoprefixer())
.pipe(cssnano())
.pipe(sourcemaps.write())
.pipe(gulp.dest('webadmin/dist'))
.pipe(gulp.dest('setup/splash/website'));
});
gulp.task('images', function () {
return gulp.src('webadmin/src/img/**')
.pipe(gulp.dest('webadmin/dist/img'));
});
// --------------
// Utilities
// --------------
gulp.task('watch', ['default'], function () {
gulp.watch(['webadmin/src/*.scss'], ['css']);
gulp.watch(['webadmin/src/img/*'], ['images']);
gulp.watch(['webadmin/src/**/*.html'], ['html']);
gulp.watch(['webadmin/src/views/*.html'], ['html-views']);
gulp.watch(['webadmin/src/templates/*.html'], ['html-templates']);
gulp.watch(['webadmin/src/js/update.js'], ['js-update']);
gulp.watch(['webadmin/src/js/setup.js', 'webadmin/src/js/client.js'], ['js-setup']);
gulp.watch(['webadmin/src/js/setupdns.js', 'webadmin/src/js/client.js'], ['js-setupdns']);
gulp.watch(['webadmin/src/js/restore.js', 'webadmin/src/js/client.js'], ['js-restore']);
gulp.watch(['webadmin/src/js/logs.js', 'webadmin/src/js/client.js'], ['js-logs']);
gulp.watch(['webadmin/src/js/terminal.js', 'webadmin/src/js/client.js'], ['js-terminal']);
gulp.watch(['webadmin/src/js/index.js', 'webadmin/src/js/client.js', 'webadmin/src/js/appstore.js', 'webadmin/src/js/main.js', 'webadmin/src/views/*.js'], ['js-index']);
gulp.watch(['webadmin/src/3rdparty/**/*'], ['3rdparty']);
});
gulp.task('clean', function () {
rimraf.sync('webadmin/dist');
rimraf.sync('setup/splash/website');
});
gulp.task('default', ['clean', 'html', 'js', '3rdparty', 'images', 'css'], function () {});
gulp.task('develop', ['watch'], serve({ root: 'webadmin/dist', port: 4000 }));
@@ -1,24 +0,0 @@
'use strict';
exports.up = function(db, callback) {
var cmd = 'CREATE TABLE IF NOT EXISTS mail(' +
'domain VARCHAR(128) NOT NULL UNIQUE,' +
'enabled BOOLEAN DEFAULT 0,' +
'mailFromValidation BOOLEAN DEFAULT 1,' +
'catchAllJson TEXT,' +
'relayJson TEXT,' +
'FOREIGN KEY(domain) REFERENCES domains(domain),' +
'PRIMARY KEY(domain)) CHARACTER SET utf8 COLLATE utf8_bin';
db.runSql(cmd, function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('DROP TABLE mail', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,34 +0,0 @@
'use strict';
exports.up = function(db, callback) {
db.all('SELECT * FROM domains', function (error, domains) {
if (error) return callback(error);
if (domains.length === 0) return callback();
db.all('SELECT * FROM settings', function (error, allSettings) {
if (error) return callback(error);
// defaults
var mailFromValidation = true;
var catchAll = [ ];
var relay = { provider: 'cloudron-smtp' };
var mailEnabled = false;
allSettings.forEach(function (setting) {
switch (setting.name) {
case 'mail_from_validation': mailFromValidation = !!setting.value; break;
case 'catch_all_address': catchAll = JSON.parse(setting.value); break;
case 'mail_relay': relay = JSON.parse(setting.value); break;
case 'mail_config': mailEnabled = JSON.parse(setting.value).enabled; break;
}
});
db.runSql('INSERT INTO mail (domain, enabled, mailFromValidation, catchAllJson, relayJson) VALUES (?, ?, ?, ?, ?)',
[ domains[0].domain, mailEnabled, mailFromValidation, JSON.stringify(catchAll), JSON.stringify(relay) ], callback);
});
});
};
exports.down = function(db, callback) {
callback();
};
@@ -1,44 +0,0 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
db.all('SELECT * FROM users', [ ], function (error, users) {
if (error) return callback(error);
db.all('SELECT * FROM mail WHERE enabled=1', [ ], function (error, mailDomains) {
if (error) return callback(error);
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
db.runSql.bind(db, 'ALTER TABLE users DROP INDEX users_email'),
db.runSql.bind(db, 'ALTER TABLE users ADD COLUMN fallbackEmail VARCHAR(512) DEFAULT ""'),
function setDefaults(done) {
async.eachSeries(users, function (user, iteratorCallback) {
var defaultEmail = '';
var fallbackEmail = '';
if (mailDomains.length === 0) {
defaultEmail = user.email;
fallbackEmail = user.email;
} else {
defaultEmail = user.username ? (user.username + '@' + mailDomains[0].domain) : user.email;
fallbackEmail = user.email;
}
db.runSql('UPDATE users SET email = ?, fallbackEmail = ? WHERE id = ?', [ defaultEmail, fallbackEmail, user.id ], iteratorCallback);
}, done);
},
db.runSql.bind(db, 'ALTER TABLE users ADD UNIQUE users_email (email)'),
db.runSql.bind(db, 'COMMIT')
], callback);
});
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE users DROP COLUMN fallbackEmail', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,26 +0,0 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
db.all('SELECT * FROM settings WHERE name = ?', [ 'tls_config' ], function (error, result) {
if (error) return callback(error);
var tlsConfig = (result[0] && result[0].value) ? JSON.parse(result[0].value) : { provider: 'letsencrypt-prod'};
tlsConfig.provider = tlsConfig.provider.replace(/$le\-/, 'letsencrypt-'); // old cloudrons had le-prod/le-staging
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
db.runSql.bind(db, 'ALTER TABLE domains ADD COLUMN tlsConfigJson TEXT'),
db.runSql.bind(db, 'UPDATE domains SET tlsConfigJson = ?', [ JSON.stringify(tlsConfig) ]),
db.runSql.bind(db, 'COMMIT')
], callback);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE domains DROP COLUMN tlsConfigJson', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,47 +0,0 @@
'use strict';
var async = require('async'),
fs = require('fs'),
superagent = require('superagent');
exports.up = function(db, callback) {
if (!fs.existsSync('/home/yellowtent/configs/cloudron.conf')) {
console.log('Unable to locate cloudron.conf');
return callback();
}
var config = JSON.parse(fs.readFileSync('/home/yellowtent/configs/cloudron.conf', 'utf8'));
if (config.provider !== 'caas' || !config.fqdn) {
console.log('Not caas (%s) or no fqdn', config.provider, config.fqdn);
return callback();
}
db.runSql('SELECT COUNT(*) AS total FROM users', function (error, result) {
if (error) return callback(error);
if (result[0].total === 0) {
console.log('This cloudron is not activated. It will automatically get appstore and caas configs from autoprovision logic');
return callback();
}
console.log('Downloading appstore and caas config');
superagent.get(config.apiServerOrigin + `/api/v1/boxes/${config.fqdn}/config`)
.query({ token: config.token })
.timeout(30 * 1000).end(function (error, result) {
if (error) return callback(error);
console.log('Adding %j config', result.body);
async.series([
db.runSql.bind(db, 'INSERT settings (name, value) VALUES(?, ?)', [ 'appstore_config', JSON.stringify(result.body.appstoreConfig) ]),
db.runSql.bind(db, 'INSERT settings (name, value) VALUES(?, ?)', [ 'caas_config', JSON.stringify(result.body.caasConfig) ])
], callback);
});
});
};
exports.down = function(db, callback) {
callback();
};
@@ -1,24 +0,0 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
db.runSql('SELECT * FROM settings WHERE name=?', ['autoupdate_pattern'], function (error, results) {
if (error || results.length === 0) return callback(error); // will use defaults from box code
// migrate the 'daily' update pattern
var appUpdatePattern = results[0].value;
if (appUpdatePattern === '00 00 1,3,5,23 * * *') appUpdatePattern = '00 30 1,3,5,23 * * *';
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
db.runSql.bind(db, 'DELETE FROM settings WHERE name=?', ['autoupdate_pattern']),
db.runSql.bind(db, 'INSERT settings (name, value) VALUES(?, ?)', ['app_autoupdate_pattern', appUpdatePattern]),
db.runSql.bind(db, 'COMMIT')
], callback);
});
};
exports.down = function(db, callback) {
callback();
};
@@ -1,121 +0,0 @@
'use strict';
var async = require('async'),
crypto = require('crypto'),
fs = require('fs'),
os = require('os'),
path = require('path'),
safe = require('safetydance'),
tldjs = require('tldjs');
exports.up = function(db, callback) {
db.all('SELECT * FROM apps', function (error, apps) {
if (error) return callback(error);
async.eachSeries(apps, function (app, callback) {
if (!app.altDomain) {
console.log('App %s does not use altDomain, skip', app.id);
return callback();
}
const domain = tldjs.getDomain(app.altDomain);
const subdomain = tldjs.getSubdomain(app.altDomain);
const mailboxName = (subdomain ? subdomain : JSON.parse(app.manifestJson).title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app';
console.log('App %s is on domain %s and subdomain %s with mailbox', app.id, domain, subdomain, mailboxName);
async.series([
// Add domain if not exists
function (callback) {
const query = 'INSERT INTO domains (domain, zoneName, provider, configJson, tlsConfigJson) VALUES (?, ?, ?, ?, ?)';
const args = [ domain, domain, 'manual', JSON.stringify({}), JSON.stringify({ provider: 'letsencrypt-prod' }) ];
db.runSql(query, args, function (error) {
if (error && error.code !== 'ER_DUP_ENTRY') return callback(error);
console.log('Added domain %s', domain);
// ensure we have a fallback cert for the newly added domain. This is the same as in reverseproxy.js
// WARNING this will only work on the cloudron itself not during local testing!
const certFilePath = `/home/yellowtent/boxdata/certs/${domain}.host.cert`;
const keyFilePath = `/home/yellowtent/boxdata/certs/${domain}.host.key`;
if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) { // generate it
let opensslConf = safe.fs.readFileSync('/etc/ssl/openssl.cnf', 'utf8');
let opensslConfWithSan = `${opensslConf}\n[SAN]\nsubjectAltName=DNS:${domain}\n`;
let configFile = path.join(os.tmpdir(), 'openssl-' + crypto.randomBytes(4).readUInt32LE(0) + '.conf');
let certCommand = `openssl req -x509 -newkey rsa:2048 -keyout ${keyFilePath} -out ${certFilePath} -days 3650 -subj /CN=*.${domain} -extensions SAN -config ${configFile} -nodes`;
safe.fs.writeFileSync(configFile, opensslConfWithSan, 'utf8');
if (!safe.child_process.execSync(certCommand)) return callback(safe.error.message);
safe.fs.unlinkSync(configFile);
}
callback();
});
},
// Add domain to mail table if not exists
function (callback) {
const query = 'INSERT INTO mail (domain, enabled, mailFromValidation, catchAllJson, relayJson) VALUES (?, ?, ?, ?, ?)';
const args = [ domain, 0, 1, '[]', JSON.stringify({ provider: 'cloudron-smtp' }) ];
db.runSql(query, args, function (error) {
if (error && error.code !== 'ER_DUP_ENTRY') return callback(error);
console.log('Added domain %s to mail table', domain);
callback();
});
},
// Remove old mailbox record if any
function (callback) {
const query = 'DELETE FROM mailboxes WHERE ownerId=?';
const args = [ app.id ];
db.runSql(query, args, function (error) {
if (error) return callback(error);
console.log('Cleaned up mailbox record for app %s', app.id);
callback();
});
},
// Add new mailbox record
function (callback) {
const query = 'INSERT INTO mailboxes (name, domain, ownerId, ownerType) VALUES (?, ?, ?, ?)';
const args = [ mailboxName, domain, app.id, 'app' /* mailboxdb.TYPE_APP */ ];
db.runSql(query, args, function (error) {
if (error) return callback(error);
console.log('Added mailbox record for app %s', app.id);
callback();
});
},
// Update app record
function (callback) {
const query = 'UPDATE apps SET location=?, domain=?, altDomain=? WHERE id=?';
const args = [ subdomain, domain, '', app.id ];
db.runSql(query, args, function (error) {
if (error) return error;
console.log('Updated app %s with new domain', app.id);
callback();
});
}
], callback);
}, function (error) {
if (error) return callback(error);
// finally drop the altDomain db field
db.runSql('ALTER TABLE apps DROP COLUMN altDomain', [], callback);
});
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN altDomain VARCHAR(256)', [], callback);
};
@@ -1,19 +0,0 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
db.runSql.bind(db, 'ALTER TABLE mailboxes DROP FOREIGN KEY mailboxes_domain_constraint'),
db.runSql.bind(db, 'ALTER TABLE mailboxes ADD CONSTRAINT mailboxes_domain_constraint FOREIGN KEY(domain) REFERENCES mail(domain)'),
db.runSql.bind(db, 'COMMIT')
], callback);
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE mailboxes DROP FOREIGN KEY mailboxes_domain_constraint', function (error) {
if (error) console.error(error);
callback(error);
});
};
+15 -31
View File
@@ -23,8 +23,6 @@ CREATE TABLE IF NOT EXISTS users(
modifiedAt VARCHAR(512) NOT NULL,
admin INTEGER NOT NULL,
displayName VARCHAR(512) DEFAULT '',
fallbackEmail VARCHAR(512) DEFAULT ""
PRIMARY KEY(id));
CREATE TABLE IF NOT EXISTS groups(
@@ -72,6 +70,7 @@ CREATE TABLE IF NOT EXISTS apps(
createdAt TIMESTAMP(2) NOT NULL DEFAULT CURRENT_TIMESTAMP,
updatedAt TIMESTAMP(2) NOT NULL DEFAULT CURRENT_TIMESTAMP,
memoryLimit BIGINT DEFAULT 0,
altDomain VARCHAR(256),
xFrameOptions VARCHAR(512),
sso BOOLEAN DEFAULT 1, // whether user chose to enable SSO
debugModeJson TEXT, // options for development mode
@@ -80,8 +79,8 @@ CREATE TABLE IF NOT EXISTS apps(
// the following fields do not belong here, they can be removed when we use a queue for apptask
restoreConfigJson VARCHAR(256), // used to pass backupId to restore from to apptask
oldConfigJson TEXT, // used to pass old config to apptask (configure, restore)
updateConfigJson TEXT, // used to pass new config to apptask (update)
oldConfigJson TEXT, // used to pass old config for apptask (configure, restore)
updateConfigJson TEXT, // used to pass new config for apptask (update)
FOREIGN KEY(domain) REFERENCES domains(domain),
PRIMARY KEY(id));
@@ -133,31 +132,6 @@ CREATE TABLE IF NOT EXISTS eventlog(
PRIMARY KEY (id));
CREATE TABLE IF NOT EXISTS domains(
domain VARCHAR(128) NOT NULL UNIQUE, /* if this needs to be larger, InnoDB has a limit of 767 bytes for PRIMARY KEY values! */
zoneName VARCHAR(128) NOT NULL, /* this mostly contains the domain itself again */
provider VARCHAR(16) NOT NULL,
configJson TEXT, /* JSON containing the dns backend provider config */
tlsConfigJson TEXT, /* JSON containing the tls provider config */
PRIMARY KEY (domain))
/* the default db collation is utf8mb4_unicode_ci but for the app table domain constraint we have to use the old one */
CHARACTER SET utf8 COLLATE utf8_bin;
CREATE TABLE IF NOT EXISTS mail(
domain VARCHAR(128) NOT NULL UNIQUE,
enabled BOOLEAN DEFAULT 0, /* MDA enabled */
mailFromValidation BOOLEAN DEFAULT 1,
catchAllJson TEXT,
relayJson TEXT,
FOREIGN KEY(domain) REFERENCES domains(domain),
PRIMARY KEY(domain))
CHARACTER SET utf8 COLLATE utf8_bin;
/* Future fields:
* accessRestriction - to determine who can access it. So this has foreign keys
* quota - per mailbox quota
@@ -170,6 +144,16 @@ CREATE TABLE IF NOT EXISTS mailboxes(
creationTime TIMESTAMP,
domain VARCHAR(128),
FOREIGN KEY(domain) REFERENCES mail(domain),
UNIQUE (name, domain));
FOREIGN KEY(domain) REFERENCES domains(domain),
PRIMARY KEY (name));
CREATE TABLE IF NOT EXISTS domains(
domain VARCHAR(128) NOT NULL UNIQUE, /* if this needs to be larger, InnoDB has a limit of 767 bytes for PRIMARY KEY values! */
zoneName VARCHAR(128) NOT NULL, /* this mostly contains the domain itself again */
provider VARCHAR(16) NOT NULL,
configJson TEXT, /* JSON containing the dns backend provider config */
PRIMARY KEY (domain))
/* the default db collation is utf8mb4_unicode_ci but for the app table domain constraint we have to use the old one */
CHARACTER SET utf8 COLLATE utf8_bin;
+228 -255
View File
@@ -5,76 +5,117 @@
"requires": true,
"dependencies": {
"@google-cloud/common": {
"version": "0.16.1",
"resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-0.16.1.tgz",
"integrity": "sha512-1sufDsSfgJ7fuBLq+ux8t3TlydMlyWl9kPZx2WdLINkGtf5RjvXX6EWYZiCMKe8flJ3oC0l95j5atN2uX5n3rg==",
"version": "0.13.6",
"resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-0.13.6.tgz",
"integrity": "sha1-qdjhN7xCmkSrqWif5qDkMxeE+FM=",
"requires": {
"array-uniq": "1.0.3",
"arrify": "1.0.1",
"concat-stream": "1.6.0",
"create-error-class": "3.0.2",
"duplexify": "3.5.3",
"duplexify": "3.5.1",
"ent": "2.2.0",
"extend": "3.0.1",
"google-auto-auth": "0.9.4",
"google-auto-auth": "0.7.2",
"is": "3.2.1",
"log-driver": "1.2.5",
"methmeth": "1.1.0",
"modelo": "4.2.3",
"modelo": "4.2.0",
"request": "2.83.0",
"retry-request": "3.3.1",
"split-array-stream": "1.0.3",
"stream-events": "1.0.2",
"string-format-obj": "1.1.1",
"string-format-obj": "1.1.0",
"through2": "2.0.3"
}
},
"@google-cloud/dns": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/@google-cloud/dns/-/dns-0.7.1.tgz",
"integrity": "sha512-rOq14I6vUc/Fd+8/cY4wCd6af8/+eN2wTIUSAQQM3zccn3HxgmG9mW54Wuq2iI/LBUYP5Z/+H4MNj3ZrkRIxIA==",
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/@google-cloud/dns/-/dns-0.7.0.tgz",
"integrity": "sha512-WL9PXmWiXOAKWq06nLzrMvJdumqFGOsB9q1f/DeCK7a7b/iihkyuQGNsSvpy0pf2FPYmDc8qEFtWjRw9xIApuw==",
"requires": {
"@google-cloud/common": "0.16.1",
"@google-cloud/common": "0.13.6",
"arrify": "1.0.1",
"dns-zonefile": "0.2.2",
"extend": "3.0.1",
"is": "3.2.1",
"methmeth": "1.1.0",
"string-format-obj": "1.1.1"
"string-format-obj": "1.1.0"
}
},
"@google-cloud/storage": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-1.6.0.tgz",
"integrity": "sha512-yQ63bJYoiwY220gn/KdTLPoHppAPwFHfG7VFLPwJ+1R5U1eqUN5XV2a7uPj1szGF8/gxlKm2UbE8DgoJJ76DFw==",
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-1.5.1.tgz",
"integrity": "sha512-7yaYzFWQYgi+n1dw/i5ysgzHD3ACJndxR8CbFnunQNT3NduolYSTr5WbYwWdhN5/7QiKx9g+obgP4tVz904bXw==",
"requires": {
"@google-cloud/common": "0.16.1",
"@google-cloud/common": "0.15.1",
"arrify": "1.0.1",
"async": "2.6.0",
"compressible": "2.0.13",
"concat-stream": "1.6.0",
"create-error-class": "3.0.2",
"duplexify": "3.5.3",
"duplexify": "3.5.1",
"extend": "3.0.1",
"gcs-resumable-upload": "0.9.0",
"gcs-resumable-upload": "0.8.2",
"hash-stream-validation": "0.2.1",
"is": "3.2.1",
"mime": "2.2.0",
"mime-types": "2.1.17",
"once": "1.4.0",
"pumpify": "1.4.0",
"pumpify": "1.3.5",
"request": "2.83.0",
"safe-buffer": "5.1.1",
"snakeize": "0.1.0",
"stream-events": "1.0.2",
"string-format-obj": "1.1.1",
"string-format-obj": "1.1.0",
"through2": "2.0.3"
},
"dependencies": {
"mime": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-2.2.0.tgz",
"integrity": "sha512-0Qz9uF1ATtl8RKJG4VRfOymh7PyEor6NbrI/61lRfuRe4vx9SNATrvAeTj2EWVRKjEQGskrzWkJBBY5NbaVHIA=="
"@google-cloud/common": {
"version": "0.15.1",
"resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-0.15.1.tgz",
"integrity": "sha512-cnVtHLvyiSQvb1RzXWDp7PA1sA8Jmc47+wp/xwHwdGOlQZfKog5iluZ0C/LB8iklFXpcTwlNMorqLuZ/qH0DDA==",
"requires": {
"array-uniq": "1.0.3",
"arrify": "1.0.1",
"concat-stream": "1.6.0",
"create-error-class": "3.0.2",
"duplexify": "3.5.1",
"ent": "2.2.0",
"extend": "3.0.1",
"google-auto-auth": "0.8.1",
"is": "3.2.1",
"log-driver": "1.2.5",
"methmeth": "1.1.0",
"modelo": "4.2.0",
"request": "2.83.0",
"retry-request": "3.3.1",
"split-array-stream": "1.0.3",
"stream-events": "1.0.2",
"string-format-obj": "1.1.0",
"through2": "2.0.3"
}
},
"google-auth-library": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-0.12.0.tgz",
"integrity": "sha512-79qCXtJ1VweBmmLr4yLq9S4clZB2p5Y+iACvuKk9gu4JitEnPc+bQFmYvtCYehVR44MQzD1J8DVmYW2w677IEw==",
"requires": {
"gtoken": "1.2.3",
"jws": "3.1.4",
"lodash.isstring": "4.0.1",
"lodash.merge": "4.6.0",
"request": "2.83.0"
}
},
"google-auto-auth": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/google-auto-auth/-/google-auto-auth-0.8.1.tgz",
"integrity": "sha512-v5a4mHIkhvbtKNILxnOYgOw+cin/jfLR0pEj1ids2jn9p0OyxYUXjSJbCEciuAorQao9Y55w0zJIc8yW1rIPaA==",
"requires": {
"async": "2.6.0",
"gcp-metadata": "0.3.1",
"google-auth-library": "0.12.0",
"request": "2.83.0"
}
}
}
},
@@ -118,11 +159,11 @@
}
},
"JSONStream": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.2.tgz",
"integrity": "sha1-wQI3G27Dp887hHygDCC7D85Mbeo=",
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-0.10.0.tgz",
"integrity": "sha1-dDSdDYlSK3HzDwoD/5vSDKbxKsA=",
"requires": {
"jsonparse": "1.3.1",
"jsonparse": "0.0.5",
"through": "2.3.8"
}
},
@@ -373,11 +414,12 @@
}
},
"aws-sdk": {
"version": "2.201.0",
"resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.201.0.tgz",
"integrity": "sha1-ekUoJi2Ql6vjfr9Oa/nRhR20cAM=",
"version": "2.170.0",
"resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.170.0.tgz",
"integrity": "sha1-T5z9JqJo72opadNHXtgTS7OJlSU=",
"requires": {
"buffer": "4.9.1",
"crypto-browserify": "1.0.9",
"events": "1.1.1",
"jmespath": "0.15.0",
"querystring": "0.2.0",
@@ -386,13 +428,6 @@
"uuid": "3.1.0",
"xml2js": "0.4.17",
"xmlbuilder": "4.2.1"
},
"dependencies": {
"uuid": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz",
"integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g=="
}
}
},
"aws-sign2": {
@@ -405,15 +440,6 @@
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz",
"integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4="
},
"axios": {
"version": "0.17.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.17.1.tgz",
"integrity": "sha1-LY4+XQvb1zJ/kbyBT1xXZg+Bgk0=",
"requires": {
"follow-redirects": "1.4.1",
"is-buffer": "1.1.6"
}
},
"backoff": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz",
@@ -443,9 +469,9 @@
}
},
"base64-js": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.3.tgz",
"integrity": "sha512-MsAhsUW1GxCdgYSO6tAfZrNapmUKk7mWx/k5mFY/A1gBtkaCaNapTg+FExCw1r9yeaZhqx/xPg43xgTFH6KL5w=="
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.1.tgz",
"integrity": "sha512-dwVUVIXsBZXwTuwnXI9RK8sBmgq09NDHzyR9SAph9eqk76gKK2JSQmZARC2zRC81JC2QTtxD0ARU5qTS25gIGw=="
},
"base64url": {
"version": "2.0.0",
@@ -599,7 +625,7 @@
"resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz",
"integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=",
"requires": {
"base64-js": "1.2.3",
"base64-js": "1.2.1",
"ieee754": "1.1.8",
"isarray": "1.0.0"
}
@@ -625,10 +651,21 @@
"resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.12.tgz",
"integrity": "sha1-8VDw9nSKvdcq6uhPBEA74u8RN5c=",
"requires": {
"dtrace-provider": "0.8.6",
"dtrace-provider": "0.8.5",
"moment": "2.19.4",
"mv": "2.1.1",
"safe-json-stringify": "1.1.0"
"safe-json-stringify": "1.0.4"
},
"dependencies": {
"dtrace-provider": {
"version": "0.8.5",
"resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.5.tgz",
"integrity": "sha1-mOu6Ihr6xG4cOf02hY2Pk2dSS5I=",
"optional": true,
"requires": {
"nan": "2.8.0"
}
}
}
},
"bytes": {
@@ -825,7 +862,6 @@
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz",
"integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=",
"dev": true,
"requires": {
"string-width": "1.0.2",
"strip-ansi": "3.0.1",
@@ -836,7 +872,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
"dev": true,
"requires": {
"code-point-at": "1.1.0",
"is-fullwidth-code-point": "1.0.0",
@@ -875,9 +910,9 @@
}
},
"cloudron-manifestformat": {
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/cloudron-manifestformat/-/cloudron-manifestformat-2.11.0.tgz",
"integrity": "sha512-t4KR2KmK1JDtxw1n6IVNg0+xxspk/Cpb6m1WimE9hJ0KJYsIgZNnkce47uYiG9/nWrgUSV4xcdzsS91OOvWgig==",
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/cloudron-manifestformat/-/cloudron-manifestformat-2.10.0.tgz",
"integrity": "sha1-CKq0H2nUjEkeiLPi/UfBKveWzXw=",
"requires": {
"cron": "1.3.0",
"java-packagename-regex": "1.0.0",
@@ -1009,21 +1044,6 @@
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
"integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY="
},
"compressible": {
"version": "2.0.13",
"resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.13.tgz",
"integrity": "sha1-DRAgq5JLL9tNYnmHXH1tq6a6p6k=",
"requires": {
"mime-db": "1.33.0"
},
"dependencies": {
"mime-db": {
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz",
"integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ=="
}
}
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -1055,7 +1075,7 @@
"requires": {
"dot-prop": "4.2.0",
"graceful-fs": "4.1.11",
"make-dir": "1.2.0",
"make-dir": "1.1.0",
"unique-string": "1.0.0",
"write-file-atomic": "2.3.0",
"xdg-basedir": "3.0.0"
@@ -1288,6 +1308,11 @@
}
}
},
"crypto-browserify": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-1.0.9.tgz",
"integrity": "sha1-zFRJaF37hesRyYKKzHy4erW7/MA="
},
"crypto-random-string": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz",
@@ -1525,9 +1550,9 @@
"dev": true
},
"db-migrate": {
"version": "0.10.5",
"resolved": "https://registry.npmjs.org/db-migrate/-/db-migrate-0.10.5.tgz",
"integrity": "sha512-jddRpFIxe+24U9frQBz51wtjFyVu2LnJ4QPcNRiiR4Hvuu8BKwdN2Ez42M6hLGbl7v1rXL2u+bUcNTvPyL/OwQ==",
"version": "0.10.2",
"resolved": "https://registry.npmjs.org/db-migrate/-/db-migrate-0.10.2.tgz",
"integrity": "sha512-uU3AXQ89DPKAePCHHoZSJRnLam6/zgOBhgSXVXB9Z3JeHms5OMTQuCdj1leUwYvI6ygHdQ3Z9CS6S9yOuELdcw==",
"requires": {
"balanced-match": "0.4.2",
"bluebird": "3.5.1",
@@ -1537,13 +1562,14 @@
"final-fs": "1.6.1",
"inflection": "1.12.0",
"mkdirp": "0.5.1",
"moment": "2.19.4",
"optimist": "0.6.1",
"parse-database-url": "0.3.0",
"pkginfo": "0.4.1",
"prompt": "1.0.0",
"rc": "1.2.5",
"rc": "1.2.2",
"resolve": "1.5.0",
"semver": "5.5.0",
"semver": "5.4.1",
"tunnel-ssh": "4.1.3"
}
},
@@ -1709,16 +1735,24 @@
"integrity": "sha1-PqIQoYmgD3Zn93Cnl0KNjmYnJn8="
},
"docker-modem": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-1.0.5.tgz",
"integrity": "sha512-i3J4TYW9iNp+nWzkgGvj9UwSmT6ZUFg2OsjRlUraHCaDCv8z6f0fN3q4ur0Qq27/1GPYXSjShGaE7fDznIJKUg==",
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-1.0.4.tgz",
"integrity": "sha512-pkXB9p7KWagegOXm2NsbVDBluQQLCBJzX9uYJzVbL6CHwe4d2sSbcACJ4K8ISX1l1JUUmFSiwNkBKc1uTiU4MA==",
"requires": {
"JSONStream": "1.3.2",
"debug": "3.1.0",
"JSONStream": "0.10.0",
"debug": "2.6.9",
"readable-stream": "1.0.34",
"split-ca": "1.0.1"
},
"dependencies": {
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
"ms": "2.0.0"
}
},
"isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
@@ -1743,12 +1777,12 @@
}
},
"dockerode": {
"version": "2.5.4",
"resolved": "https://registry.npmjs.org/dockerode/-/dockerode-2.5.4.tgz",
"integrity": "sha512-esqrDATdckYhkOFn4BSOrqnkj3jgBkHT07uEqTRwK6na4/Rg60vjXWRopv2BbRpvFruMmKvOSNVY4MbmVBUnWw==",
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/dockerode/-/dockerode-2.5.3.tgz",
"integrity": "sha512-LQKXR5jyI+G/+5OhZCi40m0ArY4j46g7Tl71Vtn10Ekt5TiyDzZAoqXOCS6edQpEuGbdFgSDJxleFqLxACpKJg==",
"requires": {
"concat-stream": "1.5.2",
"docker-modem": "1.0.5",
"docker-modem": "1.0.4",
"tar-fs": "1.12.0"
},
"dependencies": {
@@ -1806,9 +1840,9 @@
"integrity": "sha1-vXWcNXqqcDZeAclrewvsCKbg2Uk="
},
"dtrace-provider": {
"version": "0.8.6",
"resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.6.tgz",
"integrity": "sha1-QooiOv4DQl0s1tY0f99AxmkDVj0=",
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.7.1.tgz",
"integrity": "sha1-wGswjy8Q1dWDiuycVx5dWI3HHQQ=",
"optional": true,
"requires": {
"nan": "2.8.0"
@@ -1850,9 +1884,9 @@
}
},
"duplexify": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.5.3.tgz",
"integrity": "sha512-g8ID9OroF9hKt2POf8YLayy+9594PzmM3scI00/uBXocX3TWNgoB67hjzkFe9ITAbQOne/lLdBxHXvYUM4ZgGA==",
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.5.1.tgz",
"integrity": "sha512-j5goxHTwVED1Fpe5hh3q9R93Kip0Bg2KVAt4f8CEYM3UEwYcPSvWbXaUQOzdX/HtiNomipv+gU7ASQPDbV7pGQ==",
"requires": {
"end-of-stream": "1.4.0",
"inherits": "2.0.3",
@@ -2450,14 +2484,6 @@
"integrity": "sha1-2uRqnXj74lKSJYzB54CkHZXAN4I=",
"dev": true
},
"follow-redirects": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.4.1.tgz",
"integrity": "sha512-uxYePVPogtya1ktGnAAXOacnbIuRMB4dkvqeNz2qTtTQsuzSfbDolV+wMMKxAmCx0bLgAKLbBOkjItMbbkR1vg==",
"requires": {
"debug": "3.1.0"
}
},
"for-in": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
@@ -2606,24 +2632,23 @@
}
},
"gcp-metadata": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-0.6.1.tgz",
"integrity": "sha512-Ju3brkV7kYOBP5s3Z6HS2xd7gyH9MDfuKeB+y51SsI8GPrD37NOB5Re9fWXQQVAkd74zzVOScnNic1lcRsWD9w==",
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-0.3.1.tgz",
"integrity": "sha512-5kJPX/RXuqoLmHiOOgkSDk/LI0QaXpEvZ3pvQP4ifjGGDKZKVSOjL/GcDjXA5kLxppFCOjmmsu0Uoop9d1upaQ==",
"requires": {
"axios": "0.17.1",
"extend": "3.0.1",
"retry-axios": "0.3.0"
"retry-request": "3.3.1"
}
},
"gcs-resumable-upload": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/gcs-resumable-upload/-/gcs-resumable-upload-0.9.0.tgz",
"integrity": "sha512-+Zrmr0JKO2y/2mg953TW6JLu+NAMHqQsKzqCm7CIT24gMQakolPJCMzDleVpVjXAqB7ZCD276tcUq2ebOfqTug==",
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/gcs-resumable-upload/-/gcs-resumable-upload-0.8.2.tgz",
"integrity": "sha512-PBl1OFABYxubxfYPh000I0+JLbQzBRtNqxzgxYboIQk2tdw7BvjJ2dVukk3YH4QM6GiUwqItyNqWBuxjLH8GhA==",
"requires": {
"buffer-equal": "1.0.0",
"configstore": "3.1.1",
"google-auto-auth": "0.9.4",
"pumpify": "1.4.0",
"google-auto-auth": "0.7.2",
"pumpify": "1.3.5",
"request": "2.83.0",
"stream-events": "1.0.2",
"through2": "2.0.3"
@@ -2868,25 +2893,24 @@
}
},
"google-auth-library": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-0.12.0.tgz",
"integrity": "sha512-79qCXtJ1VweBmmLr4yLq9S4clZB2p5Y+iACvuKk9gu4JitEnPc+bQFmYvtCYehVR44MQzD1J8DVmYW2w677IEw==",
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-0.10.0.tgz",
"integrity": "sha1-bhW6vuhf0d0U2NEoopW2g41SE24=",
"requires": {
"gtoken": "1.2.3",
"jws": "3.1.4",
"lodash.isstring": "4.0.1",
"lodash.merge": "4.6.1",
"lodash.noop": "3.0.1",
"request": "2.83.0"
}
},
"google-auto-auth": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/google-auto-auth/-/google-auto-auth-0.9.4.tgz",
"integrity": "sha512-a/gSNZ2RCaJxriBO/A010IHmdiQeoZS0EE83G7R/yV/OGXM9zd3otRqlcfRUomBLXf9XgsJ0h6bCp7bo+qaPvw==",
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/google-auto-auth/-/google-auto-auth-0.7.2.tgz",
"integrity": "sha512-ux2n2AE2g3+vcLXwL4dP/M12SFMRX5dzCzBfhAEkTeAB7dpyGdOIEj7nmUx0BHKaCcUQrRWg9kT63X/Mmtk1+A==",
"requires": {
"async": "2.6.0",
"gcp-metadata": "0.6.1",
"google-auth-library": "0.12.0",
"gcp-metadata": "0.3.1",
"google-auth-library": "0.10.0",
"request": "2.83.0"
}
},
@@ -3594,7 +3618,8 @@
"is-buffer": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
"integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="
"integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==",
"dev": true
},
"is-builtin-module": {
"version": "1.0.0",
@@ -3974,9 +3999,9 @@
"dev": true
},
"jsonparse": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz",
"integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA="
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-0.0.5.tgz",
"integrity": "sha1-MwVCrT8KZUZlt3jz6y2an6UHrGQ="
},
"jsonpointer": {
"version": "4.0.1",
@@ -4060,16 +4085,16 @@
}
},
"ldapjs": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-1.0.2.tgz",
"integrity": "sha1-VE/3Ayt7g8aPBwEyjZKXqmlDQPk=",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-1.0.1.tgz",
"integrity": "sha1-NSuBKudLCo6WVJpLiWBg7uG5pUY=",
"requires": {
"asn1": "0.2.3",
"assert-plus": "1.0.0",
"backoff": "2.5.0",
"bunyan": "1.8.12",
"dashdash": "1.14.1",
"dtrace-provider": "0.8.6",
"dtrace-provider": "0.7.1",
"ldap-filter": "0.2.2",
"once": "1.4.0",
"vasync": "1.6.4",
@@ -4266,9 +4291,9 @@
"dev": true
},
"lodash.merge": {
"version": "4.6.1",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.1.tgz",
"integrity": "sha512-AOYza4+Hf5z1/0Hztxpm2/xiPZgi/cjMqdnKTUWTBSKchJlxXXuUSxCCl8rJlf4g6yww/j6mA8nC8Hw/EZWxKQ=="
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.0.tgz",
"integrity": "sha1-aYhLoUSsM/5plzemCG3v+t0PicU="
},
"lodash.mergewith": {
"version": "4.6.0",
@@ -4276,6 +4301,11 @@
"integrity": "sha1-FQzwoWeR9ZA7iJHqsVRgknS96lU=",
"dev": true
},
"lodash.noop": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/lodash.noop/-/lodash.noop-3.0.1.tgz",
"integrity": "sha1-OBiPTWUKOkdCWEObluxFsyYXEzw="
},
"lodash.restparam": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz",
@@ -4361,9 +4391,9 @@
"dev": true
},
"make-dir": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.2.0.tgz",
"integrity": "sha512-aNUAa4UMg/UougV25bbrU4ZaaKNjJ/3/xnvg/twpmKROPdKZPZ9wGgI0opdZzO8q/zUFawoUuixuOv33eZ61Iw==",
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.1.0.tgz",
"integrity": "sha512-0Pkui4wLJ7rxvmfUvs87skoEaxmu0hCUApF8nonzpl7q//FWp9zu8W61Scz4sd/kUiqDxvUhtoam2efDyiBzcA==",
"requires": {
"pify": "3.0.0"
}
@@ -4440,7 +4470,7 @@
"resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz",
"integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=",
"requires": {
"mimic-fn": "1.2.0"
"mimic-fn": "1.1.0"
}
},
"memoizee": {
@@ -4522,9 +4552,9 @@
}
},
"mime": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-2.2.0.tgz",
"integrity": "sha512-0Qz9uF1ATtl8RKJG4VRfOymh7PyEor6NbrI/61lRfuRe4vx9SNATrvAeTj2EWVRKjEQGskrzWkJBBY5NbaVHIA=="
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/mime/-/mime-2.0.3.tgz",
"integrity": "sha512-TrpAd/vX3xaLPDgVRm6JkZwLR0KHfukMdU2wTEbqMDdCnY6Yo3mE+mjs9YE6oMNw2QRfXVeBEYpmpO94BIqiug=="
},
"mime-db": {
"version": "1.30.0",
@@ -4540,9 +4570,9 @@
}
},
"mimic-fn": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz",
"integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ=="
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.1.0.tgz",
"integrity": "sha1-5md4PZLonb00KBi1IwudYqZyrRg="
},
"minimatch": {
"version": "3.0.4",
@@ -4587,9 +4617,9 @@
}
},
"mocha": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-5.0.1.tgz",
"integrity": "sha512-SpwyojlnE/WRBNGtvJSNfllfm5PqEDFxcWluSIgLeSBJtXG4DmoX2NNAeEA7rP5kK+79VgtVq8nG6HskaL1ykg==",
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-4.0.1.tgz",
"integrity": "sha512-evDmhkoA+cBNiQQQdSKZa2b9+W2mpLoj50367lhy+Klnx9OV8XlCIhigUnn1gaTFLQCa0kdNhEGDr0hCXOQFDw==",
"dev": true,
"requires": {
"browser-stdout": "1.3.0",
@@ -4630,9 +4660,9 @@
}
},
"modelo": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/modelo/-/modelo-4.2.3.tgz",
"integrity": "sha512-9DITV2YEMcw7XojdfvGl3gDD8J9QjZTJ7ZOUuSAkP+F3T6rDbzMJuPktxptsdHYEvZcmXrCD3LMOhdSAEq6zKA=="
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/modelo/-/modelo-4.2.0.tgz",
"integrity": "sha1-O0tCACOmbKfjK9uhbnEJN+FNGws="
},
"moment": {
"version": "2.19.4",
@@ -4821,7 +4851,7 @@
"mkdirp": "0.5.1",
"propagate": "0.4.0",
"qs": "6.5.1",
"semver": "5.5.0"
"semver": "5.4.1"
},
"dependencies": {
"debug": {
@@ -5056,7 +5086,7 @@
"stringstream": "0.0.5",
"tough-cookie": "2.3.3",
"tunnel-agent": "0.4.3",
"uuid": "3.2.1"
"uuid": "3.1.0"
}
},
"sntp": {
@@ -5077,9 +5107,9 @@
}
},
"nodemailer": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-4.6.0.tgz",
"integrity": "sha512-yYTtNAmukmu0qI/zAYGggOvOdMgwacp4jmvCyXrGao/84Elz+2lDGOhVg6bkvOhkRK/hnun3XoJ+AV7w8x4wbQ=="
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-4.4.1.tgz",
"integrity": "sha512-1bnszJJXatcHJhLpxQ1XMkLDjCjPKvGKMtRQ73FOsoNln3UQjddEQmz6fAwM3aj0GtQ3dQX9qtMHPelz63GU7A=="
},
"nodemailer-fetch": {
"version": "1.6.0",
@@ -5126,7 +5156,7 @@
"requires": {
"hosted-git-info": "2.5.0",
"is-builtin-module": "1.0.0",
"semver": "5.5.0",
"semver": "5.4.1",
"validate-npm-package-license": "3.0.1"
}
},
@@ -5475,26 +5505,18 @@
"integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4="
},
"p-limit": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.2.0.tgz",
"integrity": "sha512-Y/OtIaXtUPr4/YpMv1pCL5L5ed0rumAaAeBSj12F+bSlMdys7i8oQF/GUJmfpTS/QoaRrS/k6pma29haJpsMng==",
"requires": {
"p-try": "1.0.0"
}
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.1.0.tgz",
"integrity": "sha1-sH/y2aXYi+yAYDWJWiurZqJ5iLw="
},
"p-locate": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz",
"integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=",
"requires": {
"p-limit": "1.2.0"
"p-limit": "1.1.0"
}
},
"p-try": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz",
"integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M="
},
"parse-database-url": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/parse-database-url/-/parse-database-url-0.3.0.tgz",
@@ -6856,24 +6878,13 @@
}
},
"pumpify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.4.0.tgz",
"integrity": "sha512-2kmNR9ry+Pf45opRVirpNuIFotsxUGLaYqxIwuR77AYrYRMuFCz9eryHBS52L360O+NcR383CL4QYlMKPq4zYA==",
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.3.5.tgz",
"integrity": "sha1-G2ccYZlAq8rqwK0OOjwWS+dgmTs=",
"requires": {
"duplexify": "3.5.3",
"duplexify": "3.5.1",
"inherits": "2.0.3",
"pump": "2.0.1"
},
"dependencies": {
"pump": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz",
"integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==",
"requires": {
"end-of-stream": "1.4.0",
"once": "1.4.0"
}
}
"pump": "1.0.3"
}
},
"punycode": {
@@ -6929,9 +6940,9 @@
}
},
"rc": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.5.tgz",
"integrity": "sha1-J1zWh/bjs2zHVrqibf7oCnkDAf0=",
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.2.tgz",
"integrity": "sha1-2M6ctX6NZNnHut2YdsfDTL48cHc=",
"requires": {
"deep-extend": "0.4.2",
"ini": "1.3.5",
@@ -7146,7 +7157,7 @@
"stringstream": "0.0.5",
"tough-cookie": "2.3.3",
"tunnel-agent": "0.6.0",
"uuid": "3.2.1"
"uuid": "3.1.0"
}
},
"require-directory": {
@@ -7183,11 +7194,6 @@
"integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=",
"dev": true
},
"retry-axios": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/retry-axios/-/retry-axios-0.3.0.tgz",
"integrity": "sha512-6vOCghodB5p5N/ZOqug7A3WsT42TULZ7NErUi4lP3KtwtXgz4hE/43LWHsFuHuBfXRmOm/tjXBWAjnObrcy+yg=="
},
"retry-request": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/retry-request/-/retry-request-3.3.1.tgz",
@@ -7236,9 +7242,9 @@
"integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg=="
},
"safe-json-stringify": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.1.0.tgz",
"integrity": "sha512-EzBtUaFH9bHYPc69wqjp0efJI/DPNHdFbGE3uIMn4sVbO0zx8vZ8cG4WKxQfOpUOKsQyGBiT2mTqnCw+6nLswA==",
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.0.4.tgz",
"integrity": "sha1-gaCY9Efku8P/MxKiQ1IbwGDvWRE=",
"optional": true
},
"safetydance": {
@@ -7349,9 +7355,9 @@
}
},
"semver": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz",
"integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA=="
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz",
"integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg=="
},
"send": {
"version": "0.16.1",
@@ -7465,7 +7471,7 @@
"resolved": "https://registry.npmjs.org/showdown/-/showdown-1.8.5.tgz",
"integrity": "sha1-QYOQzFvS0Y3ql5uaGyKhVqep7Nc=",
"requires": {
"yargs": "10.1.2"
"yargs": "10.0.3"
}
},
"sigmund": {
@@ -7726,7 +7732,7 @@
"integrity": "sha1-URGNFUVV31Rp7h9n4M8efoosDjo=",
"requires": {
"asn1": "0.2.3",
"semver": "5.5.0",
"semver": "5.4.1",
"streamsearch": "0.1.2"
}
},
@@ -7833,9 +7839,9 @@
"dev": true
},
"string-format-obj": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string-format-obj/-/string-format-obj-1.1.1.tgz",
"integrity": "sha512-Mm+sROy+pHJmx0P/0Bs1uxIX6UhGJGj6xDGQZ5zh9v/SZRmLGevp+p0VJxV7lirrkAmQ2mvva/gHKpnF/pTb+Q=="
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/string-format-obj/-/string-format-obj-1.1.0.tgz",
"integrity": "sha1-djVhCx7zlwE+hHi+mKFw4EmD0Gg="
},
"string-width": {
"version": "2.1.1",
@@ -8075,9 +8081,9 @@
}
},
"tldjs": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tldjs/-/tldjs-2.3.1.tgz",
"integrity": "sha512-W/YVH/QczLUxVjnQhFC61Iq232NWu3TqDdO0S/MtXVz4xybejBov4ud+CIwN9aYqjOecEqIy0PscGkwpG9ZyTw==",
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tldjs/-/tldjs-2.2.0.tgz",
"integrity": "sha512-5b5t+HKprfccAFRAsH/fzDR4O+UgO6vStvbaJo10jvMcUavlwxR3Jrn2WmXfjG3k22T7b4pqqfput38nr1RpJQ==",
"requires": {
"punycode": "1.4.1"
}
@@ -8525,9 +8531,9 @@
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
},
"uuid": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz",
"integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA=="
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz",
"integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g=="
},
"v8flags": {
"version": "2.1.1",
@@ -8554,9 +8560,9 @@
}
},
"validator": {
"version": "9.4.1",
"resolved": "https://registry.npmjs.org/validator/-/validator-9.4.1.tgz",
"integrity": "sha512-YV5KjzvRmSyJ1ee/Dm5UED0G+1L4GZnLN3w6/T+zZm8scVua4sOhYKWTUrKa0H/tMiJyO9QLHMPN+9mB/aMunA=="
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/validator/-/validator-9.2.0.tgz",
"integrity": "sha512-6Ij4Eo0KM4LkR0d0IegOwluG5453uqT5QyF5SV5Ezvm8/zmkKI/L4eoraafZGlZPC9guLkwKzgypcw8VGWWnGA=="
},
"vary": {
"version": "1.1.2",
@@ -8825,9 +8831,9 @@
}
},
"ws": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz",
"integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==",
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-3.3.2.tgz",
"integrity": "sha512-t+WGpsNxhMR4v6EClXS8r8km5ZljKJzyGhJf7goJz9k5Ye3+b5Bvno5rjqPuIBn5mnn5GBb7o8IrIWHxX1qOLQ==",
"requires": {
"async-limiter": "1.0.0",
"safe-buffer": "5.1.1",
@@ -8878,11 +8884,11 @@
"integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI="
},
"yargs": {
"version": "10.1.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-10.1.2.tgz",
"integrity": "sha512-ivSoxqBGYOqQVruxD35+EyCFDYNEFL/Uo6FcOnz+9xZdZzK0Zzw4r4KhbrME1Oo2gOggwJod2MnsdamSG7H9ig==",
"version": "10.0.3",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-10.0.3.tgz",
"integrity": "sha512-DqBpQ8NAUX4GyPP/ijDGHsJya4tYqLQrjPr95HNsr1YwL3+daCfvBwg7+gIC6IdJhR2kATh3hb61vjzMWEtjdw==",
"requires": {
"cliui": "4.0.0",
"cliui": "3.2.0",
"decamelize": "1.2.0",
"find-up": "2.1.0",
"get-caller-file": "1.0.2",
@@ -8893,40 +8899,7 @@
"string-width": "2.1.1",
"which-module": "2.0.0",
"y18n": "3.2.1",
"yargs-parser": "8.1.0"
},
"dependencies": {
"ansi-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
"integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg="
},
"cliui": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-4.0.0.tgz",
"integrity": "sha512-nY3W5Gu2racvdDk//ELReY+dHjb9PlIcVDFXP72nVIhq2Gy3LuVXYwJoPVudwQnv1shtohpgkdCKT2YaKY0CKw==",
"requires": {
"string-width": "2.1.1",
"strip-ansi": "4.0.0",
"wrap-ansi": "2.1.0"
}
},
"strip-ansi": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
"integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
"requires": {
"ansi-regex": "3.0.0"
}
},
"yargs-parser": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-8.1.0.tgz",
"integrity": "sha512-yP+6QqN8BmrgW2ggLtTbdrOyBNSI7zBa4IykmiV5R1wl1JWNxQvWhMfMdmzIYtKU7oP3OOInY/tl2ov3BDjnJQ==",
"requires": {
"camelcase": "4.1.0"
}
}
"yargs-parser": "8.0.0"
}
},
"yargs-parser": {
+16 -16
View File
@@ -14,13 +14,13 @@
"node": ">=4.0.0 <=4.1.1"
},
"dependencies": {
"@google-cloud/dns": "^0.7.1",
"@google-cloud/storage": "^1.6.0",
"@google-cloud/dns": "^0.7.0",
"@google-cloud/storage": "^1.2.1",
"@sindresorhus/df": "^2.1.0",
"async": "^2.6.0",
"aws-sdk": "^2.201.0",
"aws-sdk": "^2.151.0",
"body-parser": "^1.18.2",
"cloudron-manifestformat": "^2.11.0",
"cloudron-manifestformat": "^2.10.0",
"connect-ensure-login": "^0.1.1",
"connect-lastmile": "^1.0.2",
"connect-timeout": "^1.9.0",
@@ -28,24 +28,24 @@
"cookie-session": "^1.3.2",
"cron": "^1.3.0",
"csurf": "^1.6.6",
"db-migrate": "^0.10.5",
"db-migrate": "^0.10.0-beta.24",
"db-migrate-mysql": "^1.1.10",
"debug": "^3.1.0",
"dockerode": "^2.5.4",
"dockerode": "^2.5.3",
"ejs": "^2.5.7",
"ejs-cli": "^2.0.0",
"express": "^4.16.2",
"express-session": "^1.15.6",
"hat": "0.0.3",
"json": "^9.0.3",
"ldapjs": "^1.0.2",
"ldapjs": "^1.0.0",
"lodash.chunk": "^4.2.0",
"mime": "^2.2.0",
"mime": "^2.0.3",
"moment-timezone": "^0.5.14",
"morgan": "^1.9.0",
"multiparty": "^4.1.2",
"mysql": "^2.15.0",
"nodemailer": "^4.6.0",
"nodemailer": "^4.4.0",
"nodemailer-smtp-transport": "^2.7.4",
"oauth2orize": "^1.11.0",
"once": "^1.3.2",
@@ -62,19 +62,19 @@
"request": "^2.83.0",
"s3-block-read-stream": "^0.2.0",
"safetydance": "^0.7.1",
"semver": "^5.5.0",
"semver": "^5.4.1",
"showdown": "^1.8.2",
"split": "^1.0.0",
"superagent": "^3.8.1",
"supererror": "^0.7.1",
"tar-fs": "^1.16.0",
"tar-stream": "^1.5.5",
"tldjs": "^2.3.1",
"tldjs": "^2.2.0",
"underscore": "^1.7.0",
"uuid": "^3.2.1",
"uuid": "^3.1.0",
"valid-url": "^1.0.9",
"validator": "^9.4.1",
"ws": "^3.3.3"
"validator": "^9.1.1",
"ws": "^3.3.1"
},
"devDependencies": {
"bootstrap-sass": "^3.3.3",
@@ -91,12 +91,12 @@
"hock": "^1.3.2",
"istanbul": "*",
"js2xmlparser": "^3.0.0",
"mocha": "^5.0.1",
"mocha": "*",
"mock-aws-s3": "git+https://github.com/cloudron-io/mock-aws-s3.git",
"nock": "^9.0.14",
"node-sass": "^4.6.1",
"readdirp": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz",
"yargs": "^10.1.2"
"yargs": "^10.0.3"
},
"scripts": {
"migrate_local": "DATABASE_URL=mysql://root:@localhost/box node_modules/.bin/db-migrate up",
+132 -56
View File
@@ -26,10 +26,6 @@ readonly physical_memory=$(LC_ALL=C free -m | awk '/Mem:/ { print $2 }')
readonly disk_size_bytes=$(LC_ALL=C df --output=size / | tail -n1)
readonly disk_size_gb=$((${disk_size_bytes}/1024/1024))
readonly RED='\033[31m'
readonly GREEN='\033[32m'
readonly DONE='\033[m'
# verify the system has minimum requirements met
if [[ "${rootfs_type}" != "ext4" ]]; then
echo "Error: Cloudron requires '/' to be ext4" # see #364
@@ -48,37 +44,55 @@ fi
initBaseImage="true"
# provisioning data
domain=""
adminLocation="my"
zoneName=""
provider=""
tlsProvider="le-prod"
requestedVersion=""
apiServerOrigin="https://api.cloudron.io"
webServerOrigin="https://cloudron.io"
dataJson=""
prerelease="false"
sourceTarballUrl=""
rebootServer="true"
baseDataDir=""
args=$(getopt -o "" -l "help,skip-baseimage-init,data-dir:,provider:,version:,env:,prerelease,skip-reboot" -n "$0" -- "$@")
# these are here for pre-1.9 compat
encryptionKey=""
restoreUrl=""
args=$(getopt -o "" -l "domain:,help,skip-baseimage-init,data:,data-dir:,provider:,encryption-key:,restore-url:,tls-provider:,version:,dns-provider:,env:,admin-location:,prerelease,skip-reboot,source-url:" -n "$0" -- "$@")
eval set -- "${args}"
while true; do
case "$1" in
--domain) domain="$2"; shift 2;;
--admin-location) adminLocation="$2"; shift 2;;
--help) echo "See https://cloudron.io/documentation/installation/ on how to install Cloudron"; exit 0;;
--provider) provider="$2"; shift 2;;
--encryption-key) encryptionKey="$2"; shift 2;;
--restore-url) restoreUrl="$2"; shift 2;;
--tls-provider) tlsProvider="$2"; shift 2;;
--version) requestedVersion="$2"; shift 2;;
--env)
if [[ "$2" == "dev" ]]; then
apiServerOrigin="https://api.dev.cloudron.io"
webServerOrigin="https://dev.cloudron.io"
tlsProvider="le-staging"
prerelease="true"
elif [[ "$2" == "staging" ]]; then
apiServerOrigin="https://api.staging.cloudron.io"
webServerOrigin="https://staging.cloudron.io"
tlsProvider="le-staging"
prerelease="true"
fi
shift 2;;
--skip-baseimage-init) initBaseImage="false"; shift;;
--skip-reboot) rebootServer="false"; shift;;
--data) dataJson="$2"; shift 2;;
--prerelease) prerelease="true"; shift;;
--source-url) sourceTarballUrl="$2"; version="0.0.1+custom"; shift 2;;
--data-dir) baseDataDir=$(realpath "$2"); shift 2;;
--) break;;
*) echo "Unknown option $1"; exit 1;;
@@ -86,34 +100,39 @@ while true; do
done
# validate arguments in the absence of data
if [[ -z "${provider}" ]]; then
echo "--provider is required (azure, cloudscale, digitalocean, ec2, exoscale, hetzner, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic)"
exit 1
elif [[ \
"${provider}" != "ami" && \
"${provider}" != "azure" && \
"${provider}" != "caas" && \
"${provider}" != "cloudscale" && \
"${provider}" != "digitalocean" && \
"${provider}" != "ec2" && \
"${provider}" != "exoscale" && \
"${provider}" != "gce" && \
"${provider}" != "hetzner" && \
"${provider}" != "lightsail" && \
"${provider}" != "linode" && \
"${provider}" != "ovh" && \
"${provider}" != "rosehosting" && \
"${provider}" != "scaleway" && \
"${provider}" != "vultr" && \
"${provider}" != "generic" \
]]; then
echo "--provider must be one of: azure, cloudscale.ch, digitalocean, ec2, exoscale, gce, hetzner, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic"
exit 1
fi
if [[ -z "${dataJson}" ]]; then
if [[ -z "${provider}" ]]; then
echo "--provider is required (azure, cloudscale.ch, digitalocean, ec2, exoscale, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic)"
exit 1
elif [[ \
"${provider}" != "ami" && \
"${provider}" != "azure" && \
"${provider}" != "cloudscale.ch" && \
"${provider}" != "digitalocean" && \
"${provider}" != "ec2" && \
"${provider}" != "exoscale" && \
"${provider}" != "gce" && \
"${provider}" != "lightsail" && \
"${provider}" != "linode" && \
"${provider}" != "ovh" && \
"${provider}" != "rosehosting" && \
"${provider}" != "scaleway" && \
"${provider}" != "vultr" && \
"${provider}" != "generic" \
]]; then
echo "--provider must be one of: azure, cloudscale.ch, digitalocean, ec2, exoscale, gce, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic"
exit 1
fi
if [[ -n "${baseDataDir}" && ! -d "${baseDataDir}" ]]; then
echo "${baseDataDir} does not exist"
exit 1
if [[ "${tlsProvider}" != "fallback" && "${tlsProvider}" != "le-prod" && "${tlsProvider}" != "le-staging" ]]; then
echo "--tls-provider must be one of: le-prod, le-staging, fallback"
exit 1
fi
if [[ -n "${baseDataDir}" && ! -d "${baseDataDir}" ]]; then
echo "${baseDataDir} does not exist"
exit 1
fi
fi
echo ""
@@ -124,7 +143,7 @@ echo ""
echo " Follow setup logs in a second terminal with:"
echo " $ tail -f ${LOG_FILE}"
echo ""
echo " Join us at https://forum.cloudron.io for any questions."
echo " Join us at https://chat.cloudron.io for any questions."
echo ""
if [[ "${initBaseImage}" == "true" ]]; then
@@ -141,33 +160,74 @@ if [[ "${initBaseImage}" == "true" ]]; then
fi
echo "=> Checking version"
if ! releaseJson=$($curl -s "${apiServerOrigin}/api/v1/releases?prerelease=${prerelease}&boxVersion=${requestedVersion}"); then
echo "Failed to get release information"
exit 1
fi
if [[ "${sourceTarballUrl}" == "" ]]; then
if ! releaseJson=$($curl -s "${apiServerOrigin}/api/v1/releases?prerelease=${prerelease}&boxVersion=${requestedVersion}"); then
echo "Failed to get release information"
exit 1
fi
if [[ "$requestedVersion" == "" ]]; then
version=$(echo "${releaseJson}" | python3 -c 'import json,sys;obj=json.load(sys.stdin);print(obj["version"])')
else
version="${requestedVersion}"
fi
if [[ "$requestedVersion" == "" ]]; then
version=$(echo "${releaseJson}" | python3 -c 'import json,sys;obj=json.load(sys.stdin);print(obj["version"])')
else
version="${requestedVersion}"
fi
if ! sourceTarballUrl=$(echo "${releaseJson}" | python3 -c 'import json,sys;obj=json.load(sys.stdin);print(obj["info"]["sourceTarballUrl"])'); then
echo "No source code for version '${requestedVersion:-latest}'"
exit 1
if ! sourceTarballUrl=$(echo "${releaseJson}" | python3 -c 'import json,sys;obj=json.load(sys.stdin);print(obj["info"]["sourceTarballUrl"])'); then
echo "No source code for version '${requestedVersion:-latest}'"
exit 1
fi
fi
# Build data
# tlsConfig, dnsConfig, backupConfig are here for backward compat with < 1.9
# from 1.9, we use autoprovision.json
data=$(cat <<EOF
{
"provider": "${provider}",
"apiServerOrigin": "${apiServerOrigin}",
"webServerOrigin": "${webServerOrigin}",
"version": "${version}"
}
if [[ -z "${dataJson}" ]]; then
if [[ -z "${restoreUrl}" ]]; then
data=$(cat <<EOF
{
"fqdn": "${domain}",
"adminLocation": "${adminLocation}",
"adminFqdn": "${adminLocation}.${domain}",
"zoneName": "${zoneName}",
"provider": "${provider}",
"apiServerOrigin": "${apiServerOrigin}",
"webServerOrigin": "${webServerOrigin}",
"version": "${version}",
"tlsConfig": {
"provider": "${tlsProvider}"
},
"backupConfig" : {
"provider": "filesystem",
"backupFolder": "/var/backups",
"key": "${encryptionKey}",
"format": "tgz",
"retentionSecs": 172800
}
}
EOF
)
)
else
data=$(cat <<EOF
{
"fqdn": "${domain}",
"adminLocation": "${adminLocation}",
"adminFqdn": "${adminLocation}.${domain}",
"zoneName": "${zoneName}",
"provider": "${provider}",
"apiServerOrigin": "${apiServerOrigin}",
"webServerOrigin": "${webServerOrigin}",
"restore": {
"url": "${restoreUrl}",
"key": "${encryptionKey}"
},
"version": "${version}"
}
EOF
)
fi
else
data="${dataJson}"
fi
echo "=> Downloading version ${version} ..."
box_src_tmp_dir=$(mktemp -dt box-src-XXXXXX)
@@ -198,15 +258,31 @@ echo -n "=> Waiting for cloudron to be ready (this takes some time) ..."
while true; do
echo -n "."
if status=$($curl -q -f "http://localhost:3000/api/v1/cloudron/status" 2>/dev/null); then
break # we are up and running
[[ -z "$domain" ]] && break # with no domain, we are up and running
[[ "$status" == *"\"tls\": true"* ]] && break # with a domain, wait for the cert
fi
sleep 10
done
echo -e "\n\n${GREEN}Visit https://<IP> to finish setup once the server has rebooted.${DONE}"
autoprovision_data=$(cat <<EOF
{
"tlsConfig": {
"provider": "${tlsProvider}"
}
}
EOF
)
echo "${autoprovision_data}" > /home/yellowtent/configs/autoprovision.json
if [[ -n "${domain}" ]]; then
echo -e "\n\nVisit https://my.${domain} to finish setup once the server has rebooted.\n"
else
echo -e "\n\nVisit https://<IP> to finish setup once the server has rebooted.\n"
fi
if [[ "${rebootServer}" == "true" ]]; then
echo -e "\nRebooting this server now to let changes take effect.\n"
echo -e "\n\nRebooting this server now to let bootloader changes take effect.\n"
systemctl stop mysql # sometimes mysql ends up having corrupt privilege tables
systemctl reboot
fi
+8 -19
View File
@@ -7,15 +7,17 @@ set -eu
[[ $(uname -s) == "Darwin" ]] && GNU_GETOPT="/usr/local/opt/gnu-getopt/bin/getopt" || GNU_GETOPT="getopt"
readonly GNU_GETOPT
args=$(${GNU_GETOPT} -o "" -l "output:" -n "$0" -- "$@")
args=$(${GNU_GETOPT} -o "" -l "revision:,output:" -n "$0" -- "$@")
eval set -- "${args}"
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
commitish="HEAD"
bundle_file=""
while true; do
case "$1" in
--revision) commitish="$2"; shift 2;;
--output) bundle_file="$2"; shift 2;;
--) break;;
*) echo "Unknown option $1"; exit 1;;
@@ -25,12 +27,7 @@ done
readonly TMPDIR=${TMPDIR:-/tmp} # why is this not set on mint?
if ! $(cd "${SOURCE_DIR}" && git diff --exit-code >/dev/null); then
echo "You have local changes in box, stash or commit them to proceed"
exit 1
fi
if ! $(cd "${SOURCE_DIR}/../webadmin" && git diff --exit-code >/dev/null); then
echo "You have local changes in webadmin, stash or commit them to proceed"
echo "You have local changes, stash or commit them to proceed"
exit 1
fi
@@ -39,21 +36,13 @@ if [[ "$(node --version)" != "v8.9.3" ]]; then
exit 1
fi
box_version=$(cd "${SOURCE_DIR}" && git rev-parse "HEAD")
branch=$(git rev-parse --abbrev-ref HEAD)
if [[ "${branch}" == "master" ]]; then
webadmin_version=$(cd "${SOURCE_DIR}/../webadmin" && git rev-parse "${branch}")
else
webadmin_version=$(cd "${SOURCE_DIR}/../webadmin" && git fetch && git rev-parse "origin/${branch}")
fi
version=$(cd "${SOURCE_DIR}" && git rev-parse "${commitish}")
bundle_dir=$(mktemp -d -t box 2>/dev/null || mktemp -d box-XXXXXXXXXX --tmpdir=$TMPDIR)
[[ -z "$bundle_file" ]] && bundle_file="${TMPDIR}/box-${box_version:0:10}-${webadmin_version:0:10}.tar.gz"
[[ -z "$bundle_file" ]] && bundle_file="${TMPDIR}/box-${version}.tar.gz"
chmod "o+rx,g+rx" "${bundle_dir}" # otherwise extracted tarball director won't be readable by others/group
echo "Checking out code box version [${box_version}] and webadmin version [${webadmin_version}] into ${bundle_dir}"
(cd "${SOURCE_DIR}" && git archive --format=tar ${box_version} | (cd "${bundle_dir}" && tar xf -))
(cd "${SOURCE_DIR}/../webadmin" && git archive --format=tar ${webadmin_version} | (cd "${bundle_dir}" && tar xf -))
(cp "${SOURCE_DIR}/../webadmin/LICENSE" "${bundle_dir}")
echo "Checking out code [${version}] into ${bundle_dir}"
(cd "${SOURCE_DIR}" && git archive --format=tar ${version} | (cd "${bundle_dir}" && tar xf -))
if diff "${TMPDIR}/boxtarball.cache/package-lock.json.all" "${bundle_dir}/package-lock.json" >/dev/null 2>&1; then
echo "Reusing dev modules from cache"
+9 -8
View File
@@ -40,22 +40,22 @@ if [[ $(docker version --format {{.Client.Version}}) != "17.09.0-ce" ]]; then
# https://download.docker.com/linux/ubuntu/dists/xenial/stable/binary-amd64/Packages
if [[ $(sha256sum /tmp/docker.deb | cut -d' ' -f1) != "d33f6eb134f0ab0876148bd96de95ea47d583d7f2cddfdc6757979453f9bd9bf" ]]; then
echo "==> installer: docker binary download is corrupt"
echo "docker binary download is corrupt"
exit 5
fi
echo "==> installer: Waiting for all dpkg tasks to finish..."
echo "Waiting for all dpkg tasks to finish..."
while fuser /var/lib/dpkg/lock; do
sleep 1
done
while ! dpkg --force-confold --configure -a; do
echo "==> installer: Failed to fix packages. Retry"
echo "Failed to fix packages. Retry"
sleep 1
done
while ! apt install -y /tmp/docker.deb; do
echo "==> installer: Failed to install docker. Retry"
echo "Failed to install docker. Retry"
sleep 1
done
@@ -78,12 +78,12 @@ for try in `seq 1 10`; do
# however by default npm drops privileges for npm rebuild
# https://docs.npmjs.com/misc/config#unsafe-perm
if cd "${box_src_tmp_dir}" && npm rebuild --unsafe-perm; then break; fi
echo "==> installer: Failed to rebuild, trying again"
echo "Failed to rebuild, trying again"
sleep 5
done
if [[ ${try} -eq 10 ]]; then
echo "==> installer: npm rebuild failed, giving up"
echo "npm rebuild failed"
exit 4
fi
@@ -92,8 +92,9 @@ if ! id "${USER}" 2>/dev/null; then
fi
if [[ "${is_update}" == "yes" ]]; then
echo "==> installer: stop cloudron.target service for update"
${BOX_SRC_DIR}/setup/stop.sh
echo "Setting up update splash screen"
"${box_src_tmp_dir}/setup/splashpage.sh" --data "${arg_data}" || true # show splash from new code
${BOX_SRC_DIR}/setup/stop.sh # stop the old code
fi
# setup links to data directory
+16 -6
View File
@@ -3,13 +3,16 @@
source_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
json="${source_dir}/../node_modules/.bin/json"
# IMPORTANT: Fix cloudron.js:doUpdate if you add/remove any arg. keep these sorted for readability
arg_api_server_origin=""
arg_fqdn="" # remove after 1.10
arg_admin_domain=""
arg_fqdn=""
arg_admin_location=""
arg_admin_fqdn=""
arg_zone_name=""
arg_is_custom_domain="false" # can be removed after 1.9
arg_retire_reason=""
arg_retire_info=""
arg_token=""
arg_version=""
arg_web_server_origin=""
arg_provider=""
@@ -32,13 +35,16 @@ while true; do
# these params must be valid in all cases
arg_fqdn=$(echo "$2" | $json fqdn)
arg_admin_fqdn=$(echo "$2" | $json adminFqdn)
arg_zone_name=$(echo "$2" | $json zoneName)
[[ "${arg_zone_name}" == "" ]] && arg_zone_name="${arg_fqdn}"
# can be removed after 1.9
arg_is_custom_domain=$(echo "$2" | $json isCustomDomain)
[[ "${arg_is_custom_domain}" == "" ]] && arg_is_custom_domain="true"
arg_admin_location=$(echo "$2" | $json adminLocation)
[[ "${arg_admin_location}" == "" ]] && arg_admin_location="my"
arg_admin_domain=$(echo "$2" | $json adminDomain)
[[ "${arg_admin_domain}" == "" ]] && arg_admin_domain="${arg_fqdn}"
# only update/restore have this valid (but not migrate)
arg_api_server_origin=$(echo "$2" | $json apiServerOrigin)
[[ "${arg_api_server_origin}" == "" ]] && arg_api_server_origin="https://api.cloudron.io"
@@ -52,6 +58,8 @@ while true; do
arg_is_demo=$(echo "$2" | $json isDemo)
[[ "${arg_is_demo}" == "" ]] && arg_is_demo="false"
arg_token=$(echo "$2" | $json token)
arg_provider=$(echo "$2" | $json provider)
[[ "${arg_provider}" == "" ]] && arg_provider="generic"
@@ -64,8 +72,10 @@ done
echo "Parsed arguments:"
echo "api server: ${arg_api_server_origin}"
echo "admin fqdn: ${arg_admin_fqdn}"
echo "fqdn: ${arg_fqdn}"
echo "custom domain: ${arg_is_custom_domain}"
# do not dump these as they might become available via logs API
#echo "token: ${arg_token}"
echo "version: ${arg_version}"
echo "web server: ${arg_web_server_origin}"
echo "provider: ${arg_provider}"
+48
View File
@@ -0,0 +1,48 @@
#!/bin/bash
set -eu -o pipefail
readonly SETUP_WEBSITE_DIR="/home/yellowtent/setup/website"
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly box_src_dir="$(realpath ${script_dir}/..)"
readonly PLATFORM_DATA_DIR="/home/yellowtent/platformdata"
echo "Setting up nginx update page"
if [[ ! -f "${PLATFORM_DATA_DIR}/nginx/applications/admin.conf" ]]; then
echo "No admin.conf found. This Cloudron has no domain yet. Skip splash setup"
exit
fi
source "${script_dir}/argparser.sh" "$@" # this injects the arg_* variables used below
# keep this is sync with config.js appFqdn()
admin_origin="https://${arg_admin_fqdn}"
# copy the website
rm -rf "${SETUP_WEBSITE_DIR}" && mkdir -p "${SETUP_WEBSITE_DIR}"
cp -r "${script_dir}/splash/website/"* "${SETUP_WEBSITE_DIR}"
# create nginx config
readonly current_infra=$(node -e "console.log(require('${script_dir}/../src/infra_version.js').version);")
existing_infra="none"
[[ -f "${PLATFORM_DATA_DIR}/INFRA_VERSION" ]] && existing_infra=$(node -e "console.log(JSON.parse(require('fs').readFileSync('${PLATFORM_DATA_DIR}/INFRA_VERSION', 'utf8')).version);")
if [[ "${arg_retire_reason}" != "" || "${existing_infra}" != "${current_infra}" ]]; then
echo "Showing progress bar on all subdomains in retired mode or infra update. retire: ${arg_retire_reason} existing: ${existing_infra} current: ${current_infra}"
rm -f ${PLATFORM_DATA_DIR}/nginx/applications/*
${box_src_dir}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/appconfig.ejs" \
-O "{ \"vhost\": \"~^(.+)\$\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"splash\", \"sourceDir\": \"${SETUP_WEBSITE_DIR}\", \"certFilePath\": \"cert/host.cert\", \"keyFilePath\": \"cert/host.key\", \"xFrameOptions\": \"SAMEORIGIN\", \"robotsTxtQuoted\": null, \"hasIPv6\": false }" > "${PLATFORM_DATA_DIR}/nginx/applications/admin.conf"
else
echo "Show progress bar only on admin domain for normal update"
${box_src_dir}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/appconfig.ejs" \
-O "{ \"vhost\": \"${arg_admin_fqdn}\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"splash\", \"sourceDir\": \"${SETUP_WEBSITE_DIR}\", \"certFilePath\": \"cert/host.cert\", \"keyFilePath\": \"cert/host.key\", \"xFrameOptions\": \"SAMEORIGIN\", \"robotsTxtQuoted\": null, \"hasIPv6\": false }" > "${PLATFORM_DATA_DIR}/nginx/applications/admin.conf"
fi
if [[ "${arg_retire_reason}" == "migrate" ]]; then
echo "{ \"migrate\": { \"percent\": \"10\", \"message\": \"Migrating cloudron. This could take up to 15 minutes.\", \"info\": ${arg_retire_info} }, \"backup\": null, \"apiServerOrigin\": \"${arg_api_server_origin}\" }" > "${SETUP_WEBSITE_DIR}/progress.json"
else
echo '{ "update": { "percent": "10", "message": "Updating cloudron software" }, "backup": null }' > "${SETUP_WEBSITE_DIR}/progress.json"
fi
nginx -s reload
+26 -11
View File
@@ -11,6 +11,7 @@ readonly PLATFORM_DATA_DIR="${HOME_DIR}/platformdata" # platform data
readonly APPS_DATA_DIR="${HOME_DIR}/appsdata" # app data
readonly BOX_DATA_DIR="${HOME_DIR}/boxdata" # box data
readonly CONFIG_DIR="${HOME_DIR}/configs"
readonly SETUP_PROGRESS_JSON="${HOME_DIR}/setup/website/progress.json"
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400"
@@ -18,11 +19,19 @@ readonly script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${script_dir}/argparser.sh" "$@" # this injects the arg_* variables used below
echo "==> Configuring host"
set_progress() {
local percent="$1"
local message="$2"
echo "==> ${percent} - ${message}"
(echo "{ \"update\": { \"percent\": \"${percent}\", \"message\": \"${message}\" }, \"backup\": {} }" > "${SETUP_PROGRESS_JSON}") 2> /dev/null || true # as this will fail in non-update mode
}
set_progress "20" "Configuring host"
sed -e 's/^#NTP=/NTP=0.ubuntu.pool.ntp.org 1.ubuntu.pool.ntp.org 2.ubuntu.pool.ntp.org 3.ubuntu.pool.ntp.org/' -i /etc/systemd/timesyncd.conf
timedatectl set-ntp 1
timedatectl set-timezone UTC
hostnamectl set-hostname "${arg_admin_fqdn}"
hostnamectl set-hostname "${arg_fqdn}"
echo "==> Configuring docker"
cp "${script_dir}/start/docker-cloudron-app.apparmor" /etc/apparmor.d/docker-cloudron-app
@@ -105,7 +114,7 @@ systemctl restart systemd-journald
setfacl -n -m u:${USER}:r /var/log/journal/*/system.journal
echo "==> Creating config directory"
mkdir -p "${CONFIG_DIR}"
rm -rf "${CONFIG_DIR}" && mkdir "${CONFIG_DIR}"
echo "==> Setting up unbound"
# DO uses Google nameservers by default. This causes RBL queries to fail (host 2.0.0.127.zen.spamhaus.org)
@@ -163,9 +172,6 @@ if ! grep -q "^Restart=" /etc/systemd/system/multi-user.target.wants/nginx.servi
echo -e "\n[Service]\nRestart=always\n" >> /etc/systemd/system/multi-user.target.wants/nginx.service
systemctl daemon-reload
fi
# remove this migration after 1.10
[[ -f /etc/nginx/cert/host.cert ]] && cp /etc/nginx/cert/host.cert "/etc/nginx/cert/${arg_admin_domain}.host.cert"
[[ -f /etc/nginx/cert/host.key ]] && cp /etc/nginx/cert/host.key "/etc/nginx/cert/${arg_admin_domain}.host.key"
systemctl start nginx
# bookkeep the version as part of data
@@ -193,22 +199,31 @@ readonly mysql_root_password="password"
mysqladmin -u root -ppassword password password # reset default root password
mysql -u root -p${mysql_root_password} -e 'CREATE DATABASE IF NOT EXISTS box'
echo "==> Migrating data"
set_progress "40" "Migrating data"
sudo -u "${USER}" -H bash <<EOF
set -eu
cd "${BOX_SRC_DIR}"
BOX_ENV=cloudron DATABASE_URL=mysql://root:${mysql_root_password}@127.0.0.1/box "${BOX_SRC_DIR}/node_modules/.bin/db-migrate" up
EOF
if [[ -z "${arg_admin_fqdn:-}" ]]; then
# can be removed after 1.9
admin_fqdn=$([[ "${arg_is_custom_domain}" == "true" ]] && echo "${arg_admin_location}.${arg_fqdn}" || echo "${arg_admin_location}-${arg_fqdn}")
else
admin_fqdn="${arg_admin_fqdn}"
fi
echo "==> Creating cloudron.conf"
cat > "${CONFIG_DIR}/cloudron.conf" <<CONF_END
{
"version": "${arg_version}",
"token": "${arg_token}",
"apiServerOrigin": "${arg_api_server_origin}",
"webServerOrigin": "${arg_web_server_origin}",
"adminDomain": "${arg_admin_domain}",
"adminFqdn": "${arg_admin_fqdn}",
"fqdn": "${arg_fqdn}",
"adminFqdn": "${admin_fqdn}",
"adminLocation": "${arg_admin_location}",
"zoneName": "${arg_zone_name}",
"provider": "${arg_provider}",
"isDemo": ${arg_is_demo}
}
@@ -241,9 +256,9 @@ find "${BOX_DATA_DIR}" -mindepth 1 -maxdepth 1 -not -path "${BOX_DATA_DIR}/mail"
chown "${USER}:${USER}" "${BOX_DATA_DIR}/mail"
chown "${USER}:${USER}" -R "${BOX_DATA_DIR}/mail/dkim" # this is owned by box currently since it generates the keys
echo "==> Starting Cloudron"
set_progress "60" "Starting Cloudron"
systemctl start cloudron.target
sleep 2 # give systemd sometime to start the processes
echo "==> Almost done"
set_progress "90" "Almost done"
+34
View File
@@ -165,6 +165,40 @@ server {
}
<% } else if ( endpoint === 'app' ) { %>
proxy_pass http://127.0.0.1:<%= port %>;
<% } else if ( endpoint === 'splash' ) { %>
root <%= sourceDir %>;
error_page 503 /update.html;
location /update.html {
add_header Cache-Control no-cache;
}
location /theme.css {
add_header Cache-Control no-cache;
}
location /3rdparty/ {
add_header Cache-Control no-cache;
}
location /js/ {
add_header Cache-Control no-cache;
}
location /progress.json {
add_header Cache-Control no-cache;
}
location /api/v1/cloudron/progress {
add_header Cache-Control no-cache;
default_type application/json;
alias <%= sourceDir %>/progress.json;
}
location / {
return 503;
}
<% } %>
}
}
+34 -69
View File
@@ -28,7 +28,6 @@ var appdb = require('./appdb.js'),
generatePassword = require('password-generator'),
hat = require('hat'),
infra = require('./infra_version.js'),
mail = require('./mail.js'),
mailboxdb = require('./mailboxdb.js'),
once = require('once'),
path = require('path'),
@@ -113,9 +112,10 @@ var KNOWN_ADDONS = {
var RMAPPDIR_CMD = path.join(__dirname, 'scripts/rmappdir.sh');
function debugApp(app, args) {
assert(typeof app === 'object');
assert(!app || typeof app === 'object');
debug(app.fqdn + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
var prefix = app ? app.intrinsicFqdn : '(no app)';
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
}
function setupAddons(app, addons, callback) {
@@ -250,7 +250,7 @@ function setupOauth(app, options, callback) {
if (!app.sso) return callback(null);
var appId = app.id;
var redirectURI = 'https://' + app.fqdn;
var redirectURI = 'https://' + (app.altDomain || app.intrinsicFqdn);
var scope = 'profile';
clients.delByAppIdAndType(appId, clients.TYPE_OAUTH, function (error) { // remove existing creds
@@ -291,27 +291,20 @@ function setupEmail(app, options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
mail.getAll(function (error, mailDomains) {
if (error) return callback(error);
// note that "external" access info can be derived from MAIL_DOMAIN (since it's part of user documentation)
var env = [
{ name: 'MAIL_SMTP_SERVER', value: 'mail' },
{ name: 'MAIL_SMTP_PORT', value: '2525' },
{ name: 'MAIL_IMAP_SERVER', value: 'mail' },
{ name: 'MAIL_IMAP_PORT', value: '9993' },
{ name: 'MAIL_SIEVE_SERVER', value: 'mail' },
{ name: 'MAIL_SIEVE_PORT', value: '4190' },
{ name: 'MAIL_DOMAIN', value: config.fqdn() }
];
const mailInDomains = mailDomains.filter(function (d) { return d.enabled; }).map(function (d) { return d.domain; }).join(',');
debugApp(app, 'Setting up Email');
// note that "external" access info can be derived from MAIL_DOMAIN (since it's part of user documentation)
var env = [
{ name: 'MAIL_SMTP_SERVER', value: 'mail' },
{ name: 'MAIL_SMTP_PORT', value: '2525' },
{ name: 'MAIL_IMAP_SERVER', value: 'mail' },
{ name: 'MAIL_IMAP_PORT', value: '9993' },
{ name: 'MAIL_SIEVE_SERVER', value: 'mail' },
{ name: 'MAIL_SIEVE_PORT', value: '4190' },
{ name: 'MAIL_DOMAIN', value: app.domain },
{ name: 'MAIL_DOMAINS', value: mailInDomains }
];
debugApp(app, 'Setting up Email');
appdb.setAddonConfig(app.id, 'email', env, callback);
});
appdb.setAddonConfig(app.id, 'email', env, callback);
}
function teardownEmail(app, options, callback) {
@@ -372,11 +365,11 @@ function setupSendMail(app, options, callback) {
var env = [
{ name: 'MAIL_SMTP_SERVER', value: 'mail' },
{ name: 'MAIL_SMTP_PORT', value: '2525' },
{ name: 'MAIL_SMTPS_PORT', value: '2465' },
{ name: 'MAIL_SMTP_USERNAME', value: mailbox.name + '@' + app.domain },
{ name: 'MAIL_SMTPS_PORT', value: '4650' },
{ name: 'MAIL_SMTP_USERNAME', value: mailbox.name },
{ name: 'MAIL_SMTP_PASSWORD', value: password },
{ name: 'MAIL_FROM', value: mailbox.name + '@' + app.domain },
{ name: 'MAIL_DOMAIN', value: app.domain }
{ name: 'MAIL_FROM', value: mailbox.name + '@' + config.fqdn() },
{ name: 'MAIL_DOMAIN', value: config.fqdn() }
];
debugApp(app, 'Setting sendmail addon config to %j', env);
appdb.setAddonConfig(app.id, 'sendmail', env, callback);
@@ -409,10 +402,10 @@ function setupRecvMail(app, options, callback) {
var env = [
{ name: 'MAIL_IMAP_SERVER', value: 'mail' },
{ name: 'MAIL_IMAP_PORT', value: '9993' },
{ name: 'MAIL_IMAP_USERNAME', value: mailbox.name + '@' + app.domain },
{ name: 'MAIL_IMAP_USERNAME', value: mailbox.name },
{ name: 'MAIL_IMAP_PASSWORD', value: password },
{ name: 'MAIL_TO', value: mailbox.name + '@' + app.domain },
{ name: 'MAIL_DOMAIN', value: app.domain }
{ name: 'MAIL_TO', value: mailbox.name + '@' + config.fqdn() },
{ name: 'MAIL_DOMAIN', value: config.fqdn() }
];
debugApp(app, 'Setting sendmail addon config to %j', env);
@@ -467,10 +460,6 @@ function teardownMySql(app, options, callback) {
}
function backupMySql(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'Backing up mysql');
callback = once(callback); // ChildProcess exit may or may not be called after error
@@ -484,10 +473,6 @@ function backupMySql(app, options, callback) {
}
function restoreMySql(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
callback = once(callback); // ChildProcess exit may or may not be called after error
setupMySql(app, options, function (error) {
@@ -540,10 +525,6 @@ function teardownPostgreSql(app, options, callback) {
}
function backupPostgreSql(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'Backing up postgresql');
callback = once(callback); // ChildProcess exit may or may not be called after error
@@ -557,10 +538,6 @@ function backupPostgreSql(app, options, callback) {
}
function restorePostgreSql(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
callback = once(callback);
setupPostgreSql(app, options, function (error) {
@@ -614,10 +591,6 @@ function teardownMongoDb(app, options, callback) {
}
function backupMongoDb(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'Backing up mongodb');
callback = once(callback); // ChildProcess exit may or may not be called after error
@@ -631,10 +604,6 @@ function backupMongoDb(app, options, callback) {
}
function restoreMongoDb(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
callback = once(callback); // ChildProcess exit may or may not be called after error
setupMongoDb(app, options, function (error) {
@@ -678,7 +647,7 @@ function setupRedis(app, options, callback) {
}
const tag = infra.images.redis.tag, redisName = 'redis-' + app.id;
const label = app.fqdn;
const label = app.intrinsicFqdn;
// note that we do not add appId label because this interferes with the stop/start app logic
const cmd = `docker run --restart=always -d --name=${redisName} \
--label=location=${label} \
@@ -716,31 +685,27 @@ function teardownRedis(app, options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var container = dockerConnection.getContainer('redis-' + app.id);
var container = dockerConnection.getContainer('redis-' + app.id);
var removeOptions = {
force: true, // kill container if it's running
v: true // removes volumes associated with the container
};
var removeOptions = {
force: true, // kill container if it's running
v: true // removes volumes associated with the container
};
container.remove(removeOptions, function (error) {
if (error && error.statusCode !== 404) return callback(new Error('Error removing container:' + error));
container.remove(removeOptions, function (error) {
if (error && error.statusCode !== 404) return callback(new Error('Error removing container:' + error));
safe.fs.unlinkSync(paths.ADDON_CONFIG_DIR, 'redis-' + app.id + '_vars.sh');
safe.fs.unlinkSync(paths.ADDON_CONFIG_DIR, 'redis-' + app.id + '_vars.sh');
shell.sudo('teardownRedis', [ RMAPPDIR_CMD, app.id + '/redis', true /* delete directory */ ], function (error /* ,stdout , stderr*/) {
shell.sudo('teardownRedis', [ RMAPPDIR_CMD, app.id + '/redis', true /* delete directory */ ], function (error, stdout, stderr) {
if (error) return callback(new Error('Error removing redis data:' + error));
appdb.unsetAddonConfig(app.id, 'redis', callback);
});
});
});
}
function backupRedis(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'Backing up redis');
var cmd = [ '/addons/redis/service.sh', 'backup' ]; // the redis dir is volume mounted
+5 -7
View File
@@ -60,8 +60,8 @@ var assert = require('assert'),
var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationState', 'apps.installationProgress', 'apps.runState',
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.httpPort', 'apps.location', 'apps.domain', 'apps.dnsRecordId',
'apps.accessRestrictionJson', 'apps.restoreConfigJson', 'apps.oldConfigJson', 'apps.updateConfigJson', 'apps.memoryLimit',
'apps.xFrameOptions', 'apps.sso', 'apps.debugModeJson', 'apps.robotsTxt', 'apps.enableBackup',
'apps.accessRestrictionJson', 'apps.restoreConfigJson', 'apps.oldConfigJson', 'apps.updateConfigJson', 'apps.memoryLimit',
'apps.altDomain', 'apps.xFrameOptions', 'apps.sso', 'apps.debugModeJson', 'apps.robotsTxt', 'apps.enableBackup',
'apps.creationTime', 'apps.updateTime' ].join(',');
var PORT_BINDINGS_FIELDS = [ 'hostPort', 'environmentVariable', 'appId' ].join(',');
@@ -196,18 +196,17 @@ function add(id, appStoreId, manifest, location, domain, portBindings, data, cal
var accessRestriction = data.accessRestriction || null;
var accessRestrictionJson = JSON.stringify(accessRestriction);
var memoryLimit = data.memoryLimit || 0;
var altDomain = data.altDomain || null;
var xFrameOptions = data.xFrameOptions || '';
var installationState = data.installationState || exports.ISTATE_PENDING_INSTALL;
var restoreConfigJson = data.restoreConfig ? JSON.stringify(data.restoreConfig) : null; // used when cloning
var sso = 'sso' in data ? data.sso : null;
var robotsTxt = 'robotsTxt' in data ? data.robotsTxt : null;
var debugModeJson = data.debugMode ? JSON.stringify(data.debugMode) : null;
var queries = [];
queries.push({
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, location, domain, accessRestrictionJson, memoryLimit, xFrameOptions, restoreConfigJson, sso, debugModeJson, robotsTxt) ' +
' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, manifestJson, installationState, location, domain, accessRestrictionJson, memoryLimit, xFrameOptions, restoreConfigJson, sso, debugModeJson, robotsTxt ]
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, location, domain, accessRestrictionJson, memoryLimit, altDomain, xFrameOptions, restoreConfigJson, sso, debugModeJson) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, manifestJson, installationState, location, domain, accessRestrictionJson, memoryLimit, altDomain, xFrameOptions, restoreConfigJson, sso, debugModeJson ]
});
Object.keys(portBindings).forEach(function (env) {
@@ -227,7 +226,6 @@ function add(id, appStoreId, manifest, location, domain, portBindings, data, cal
database.transaction(queries, function (error) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error.message));
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND, 'no such domain'));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null);
+7 -5
View File
@@ -5,6 +5,7 @@ var appdb = require('./appdb.js'),
assert = require('assert'),
async = require('async'),
DatabaseError = require('./databaseerror.js'),
config = require('./config.js'),
debug = require('debug')('box:apphealthmonitor'),
docker = require('./docker.js').connection,
mailer = require('./mailer.js'),
@@ -23,9 +24,13 @@ var gRunTimeout = null;
var gDockerEventStream = null;
function debugApp(app) {
assert(typeof app === 'object');
assert(!app || typeof app === 'object');
debug(app.fqdn + ' ' + app.manifest.id + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)) + ' - ' + app.id);
var prefix = app ? app.intrinsicFqdn : '(no app)';
var manifestAppId = app ? app.manifest.id : '';
var id = app ? app.id : '';
debug(prefix + ' ' + manifestAppId + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)) + ' - ' + id);
}
function setHealth(app, health, callback) {
@@ -66,9 +71,6 @@ function setHealth(app, health, callback) {
// callback is called with error for fatal errors and not if health check failed
function checkAppHealth(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
if (app.installationState !== appdb.ISTATE_INSTALLED || app.runState !== appdb.RSTATE_RUNNING) {
debugApp(app, 'skipped. istate:%s rstate:%s', app.installationState, app.runState);
return callback(null);
+83 -79
View File
@@ -54,6 +54,7 @@ var addons = require('./addons.js'),
async = require('async'),
backups = require('./backups.js'),
BackupsError = backups.BackupsError,
certificates = require('./certificates.js'),
config = require('./config.js'),
constants = require('./constants.js'),
DatabaseError = require('./databaseerror.js'),
@@ -70,7 +71,6 @@ var addons = require('./addons.js'),
os = require('os'),
path = require('path'),
paths = require('./paths.js'),
reverseProxy = require('./reverseproxy.js'),
safe = require('safetydance'),
semver = require('semver'),
spawn = require('child_process').spawn,
@@ -130,7 +130,8 @@ function validateHostname(location, domain, hostname) {
const RESERVED_LOCATIONS = [
constants.API_LOCATION,
constants.SMTP_LOCATION,
constants.IMAP_LOCATION
constants.IMAP_LOCATION,
constants.POSTMAN_LOCATION
];
if (RESERVED_LOCATIONS.indexOf(location) !== -1) return new AppsError(AppsError.BAD_FIELD, location + ' is reserved');
@@ -306,19 +307,17 @@ function getDuplicateErrorDetails(location, portBindings, error) {
return new AppsError(AppsError.ALREADY_EXISTS);
}
// app configs that is useful for 'archival' into the app backup config.json
function getAppConfig(app) {
return {
manifest: app.manifest,
location: app.location,
domain: app.domain,
fqdn: app.fqdn,
intrinsicFqdn: app.intrinsicFqdn,
accessRestriction: app.accessRestriction,
portBindings: app.portBindings,
memoryLimit: app.memoryLimit,
xFrameOptions: app.xFrameOptions || 'SAMEORIGIN',
robotsTxt: app.robotsTxt,
sso: app.sso
altDomain: app.altDomain
};
}
@@ -364,8 +363,10 @@ function get(appId, callback) {
domaindb.get(app.domain, function (error, result) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
app.intrinsicFqdn = domains.fqdn(app.location, app.domain, result.provider);
app.iconUrl = getIconUrlSync(app);
app.fqdn = domains.fqdn(app.location, app.domain, result.provider);
app.fqdn = app.altDomain || app.intrinsicFqdn;
app.cnameTarget = app.altDomain ? app.intrinsicFqdn : null;
callback(null, app);
});
@@ -386,8 +387,10 @@ function getByIpAddress(ip, callback) {
domaindb.get(app.domain, function (error, result) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
app.intrinsicFqdn = domains.fqdn(app.location, app.domain, result.provider);
app.iconUrl = getIconUrlSync(app);
app.fqdn = domains.fqdn(app.location, app.domain, result.provider);
app.fqdn = app.altDomain || app.intrinsicFqdn;
app.cnameTarget = app.altDomain ? app.intrinsicFqdn : null;
callback(null, app);
});
@@ -405,8 +408,10 @@ function getAll(callback) {
domaindb.get(app.domain, function (error, result) {
if (error) return iteratorDone(new AppsError(AppsError.INTERNAL_ERROR, error));
app.intrinsicFqdn = domains.fqdn(app.location, app.domain, result.provider);
app.iconUrl = getIconUrlSync(app);
app.fqdn = domains.fqdn(app.location, app.domain, result.provider);
app.fqdn = app.altDomain || app.intrinsicFqdn;
app.cnameTarget = app.altDomain ? app.intrinsicFqdn : null;
iteratorDone();
});
@@ -445,7 +450,7 @@ function downloadManifest(appStoreId, manifest, callback) {
superagent.get(url).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppsError(AppsError.EXTERNAL_ERROR, 'Network error downloading manifest:' + error.message));
if (result.statusCode !== 200) return callback(new AppsError(AppsError.NOT_FOUND, util.format('Failed to get app info from store.', result.statusCode, result.text)));
if (result.statusCode !== 200) return callback(new AppsError(AppsError.EXTERNAL_ERROR, util.format('Failed to get app info from store.', result.statusCode, result.text)));
callback(null, parts[0], result.body.manifest);
});
@@ -464,6 +469,7 @@ function install(data, auditSource, callback) {
cert = data.cert || null,
key = data.key || null,
memoryLimit = data.memoryLimit || 0,
altDomain = data.altDomain || null,
xFrameOptions = data.xFrameOptions || 'SAMEORIGIN',
sso = 'sso' in data ? data.sso : null,
debugMode = data.debugMode || null,
@@ -508,6 +514,8 @@ function install(data, auditSource, callback) {
// if sso was unspecified, enable it by default if possible
if (sso === null) sso = !!manifest.addons['ldap'] || !!manifest.addons['oauth'];
if (altDomain !== null && !validator.isFQDN(altDomain)) return callback(new AppsError(AppsError.BAD_FIELD, 'Invalid external domain'));
var appId = uuid.v4();
if (icon) {
@@ -522,15 +530,13 @@ function install(data, auditSource, callback) {
if (error && error.reason === DomainError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such domain'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Could not get domain info:' + error.message));
var fqdn = domains.fqdn(location, domain, domainObject.provider);
var intrinsicFqdn = domains.fqdn(location, domain, domainObject.provider);
error = validateHostname(location, domain, fqdn);
error = validateHostname(location, domain, intrinsicFqdn);
if (error) return callback(error);
if (cert && key) {
error = reverseProxy.validateCertificate(fqdn, cert, key);
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
}
error = certificates.validateCertificate(cert, key, intrinsicFqdn);
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
debug('Will install app with id : ' + appId);
@@ -543,36 +549,32 @@ function install(data, auditSource, callback) {
var data = {
accessRestriction: accessRestriction,
memoryLimit: memoryLimit,
altDomain: altDomain,
xFrameOptions: xFrameOptions,
sso: sso,
debugMode: debugMode,
mailboxName: (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app',
restoreConfig: backupId ? { backupId: backupId, backupFormat: backupFormat } : null,
enableBackup: enableBackup,
robotsTxt: robotsTxt
robotsTxt: robotsTxt,
intrinsicFqdn: intrinsicFqdn
};
appdb.add(appId, appStoreId, manifest, location, domain, portBindings, data, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location, portBindings, error));
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, error.message));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
// save cert to boxdata/certs
if (cert && key) {
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, fqdn + '.user.cert'), cert)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving cert: ' + safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, fqdn + '.user.key'), key)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving key: ' + safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, intrinsicFqdn + '.user.cert'), cert)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving cert: ' + safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, intrinsicFqdn + '.user.key'), key)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving key: ' + safe.error.message));
}
taskmanager.restartAppTask(appId);
// fetch fresh app object for eventlog
get(appId, function (error, result) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_APP_INSTALL, auditSource, { appId: appId, location: location, domain: domain, manifest: manifest, backupId: backupId });
eventlog.add(eventlog.ACTION_APP_INSTALL, auditSource, { appId: appId, app: result });
callback(null, { id : appId });
});
callback(null, { id : appId });
});
});
});
@@ -585,8 +587,9 @@ function configure(appId, data, auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
get(appId, function (error, app) {
if (error) return callback(error);
appdb.get(appId, function (error, app) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
var domain, location, portBindings, values = { };
if ('location' in data) location = values.location = data.location.toLowerCase();
@@ -601,6 +604,11 @@ function configure(appId, data, auditSource, callback) {
if (error) return callback(error);
}
if ('altDomain' in data) {
values.altDomain = data.altDomain;
if (values.altDomain !== null && !validator.isFQDN(values.altDomain)) return callback(new AppsError(AppsError.BAD_FIELD, 'Invalid external domain'));
}
if ('portBindings' in data) {
portBindings = values.portBindings = data.portBindings;
error = validatePortBindings(values.portBindings, app.manifest.tcpPorts);
@@ -637,22 +645,22 @@ function configure(appId, data, auditSource, callback) {
if (error && error.reason === DomainError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such domain'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Could not get domain info:' + error.message));
var fqdn = domains.fqdn(location, domain, domainObject.provider);
var intrinsicFqdn = domains.fqdn(location, domain, domainObject.provider);
error = validateHostname(location, domain, fqdn);
error = validateHostname(location, domain, intrinsicFqdn);
if (error) return callback(error);
// save cert to boxdata/certs. TODO: move this to apptask when we have a real task queue
if ('cert' in data && 'key' in data) {
if (data.cert && data.key) {
error = reverseProxy.validateCertificate(fqdn, data.cert, data.key);
error = certificates.validateCertificate(data.cert, data.key, intrinsicFqdn);
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${fqdn}.user.cert`), data.cert)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving cert: ' + safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${fqdn}.user.key`), data.key)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving key: ' + safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, intrinsicFqdn + '.user.cert'), data.cert)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving cert: ' + safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, intrinsicFqdn + '.user.key'), data.key)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving key: ' + safe.error.message));
} else { // remove existing cert/key
if (!safe.fs.unlinkSync(path.join(paths.APP_CERTS_DIR, `${fqdn}.user.cert`))) debug('Error removing cert: ' + safe.error.message);
if (!safe.fs.unlinkSync(path.join(paths.APP_CERTS_DIR, `${fqdn}..user.key`))) debug('Error removing key: ' + safe.error.message);
if (!safe.fs.unlinkSync(path.join(paths.APP_CERTS_DIR, intrinsicFqdn + '.user.cert'))) debug('Error removing cert: ' + safe.error.message);
if (!safe.fs.unlinkSync(path.join(paths.APP_CERTS_DIR, intrinsicFqdn + '.user.key'))) debug('Error removing key: ' + safe.error.message);
}
}
@@ -676,14 +684,9 @@ function configure(appId, data, auditSource, callback) {
taskmanager.restartAppTask(appId);
// fetch fresh app object for eventlog
get(appId, function (error, result) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: appId });
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: appId, app: result });
callback(null);
});
callback(null);
});
});
});
@@ -723,8 +726,9 @@ function update(appId, data, auditSource, callback) {
}
}
get(appId, function (error, app) {
if (error) return callback(error);
appdb.get(appId, function (error, app) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
// prevent user from installing a app with different manifest id over an existing app
// this allows cloudron install -f --app <appid> for an app installed from the appStore
@@ -749,7 +753,7 @@ function update(appId, data, auditSource, callback) {
taskmanager.restartAppTask(appId);
eventlog.add(eventlog.ACTION_APP_UPDATE, auditSource, { appId: appId, toManifest: manifest, fromManifest: app.manifest, force: data.force, app: app });
eventlog.add(eventlog.ACTION_APP_UPDATE, auditSource, { appId: appId, toManifest: manifest, fromManifest: app.manifest, force: data.force });
// clear update indicator, if update fails, it will come back through the update checker
updateChecker.resetAppUpdateInfo(appId);
@@ -773,8 +777,10 @@ function getLogs(appId, options, callback) {
debug('Getting logs for %s', appId);
get(appId, function (error, app) {
if (error) return callback(error);
appdb.get(appId, function (error, app) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
var lines = options.lines || 100,
follow = !!options.follow,
@@ -818,8 +824,9 @@ function restore(appId, data, auditSource, callback) {
debug('Will restore app with id:%s', appId);
get(appId, function (error, app) {
if (error) return callback(error);
appdb.get(appId, function (error, app) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
// for empty or null backupId, use existing manifest to mimic a reinstall
var func = data.backupId ? backups.get.bind(null, data.backupId) : function (next) { return next(null, { manifest: app.manifest }); };
@@ -848,7 +855,7 @@ function restore(appId, data, auditSource, callback) {
taskmanager.restartAppTask(appId);
eventlog.add(eventlog.ACTION_APP_RESTORE, auditSource, { appId: appId, app: app });
eventlog.add(eventlog.ACTION_APP_RESTORE, auditSource, { appId: appId });
callback(null);
});
@@ -874,12 +881,13 @@ function clone(appId, data, auditSource, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof portBindings, 'object');
get(appId, function (error, app) {
if (error) return callback(error);
appdb.get(appId, function (error, app) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
backups.get(backupId, function (error, backupInfo) {
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
if (error && error.reason === BackupsError.NOT_FOUND) return callback(new AppsError(AppsError.EXTERNAL_ERROR, 'Backup not found'));
if (error && error.reason === BackupsError.NOT_FOUND) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
if (!backupInfo.manifest) callback(new AppsError(AppsError.EXTERNAL_ERROR, 'Could not get restore config'));
@@ -892,10 +900,12 @@ function clone(appId, data, auditSource, callback) {
if (error) return callback(error);
domains.get(domain, function (error, domainObject) {
if (error && error.reason === DomainError.NOT_FOUND) return callback(new AppsError(AppsError.EXTERNAL_ERROR, 'No such domain'));
if (error && error.reason === DomainError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such domain'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Could not get domain info:' + error.message));
error = validateHostname(location, domain, domains.fqdn(location, domain, domainObject.provider));
var intrinsicFqdn = domains.fqdn(location, domain, domainObject.provider);
error = validateHostname(location, domain, intrinsicFqdn);
if (error) return callback(error);
var newAppId = uuid.v4(), manifest = backupInfo.manifest;
@@ -913,9 +923,7 @@ function clone(appId, data, auditSource, callback) {
xFrameOptions: app.xFrameOptions,
restoreConfig: { backupId: backupId, backupFormat: backupInfo.format },
sso: !!app.sso,
mailboxName: (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app',
enableBackup: app.enableBackup,
robotsTxt: app.robotsTxt
mailboxName: (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app'
};
appdb.add(newAppId, app.appStoreId, manifest, location, domain, portBindings, data, function (error) {
@@ -924,14 +932,9 @@ function clone(appId, data, auditSource, callback) {
taskmanager.restartAppTask(newAppId);
// fetch fresh app object for eventlog
get(appId, function (error, result) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_APP_CLONE, auditSource, { appId: newAppId, oldAppId: appId, backupId: backupId, location: location, manifest: manifest });
eventlog.add(eventlog.ACTION_APP_CLONE, auditSource, { appId: newAppId, oldAppId: appId, backupId: backupId, oldApp: app, newApp: result });
callback(null, { id : newAppId });
});
callback(null, { id : newAppId });
});
});
});
@@ -946,10 +949,10 @@ function uninstall(appId, auditSource, callback) {
debug('Will uninstall app with id:%s', appId);
get(appId, function (error, app) {
get(appId, function (error, result) {
if (error) return callback(error);
appstore.unpurchase(appId, app.appStoreId, function (error) {
appstore.unpurchase(appId, result.appStoreId, function (error) {
if (error && error.reason === AppstoreError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
if (error && error.reason === AppstoreError.BILLING_REQUIRED) return callback(new AppsError(AppsError.BILLING_REQUIRED, error.message));
if (error && error.reason === AppstoreError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
@@ -960,7 +963,7 @@ function uninstall(appId, auditSource, callback) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_APP_UNINSTALL, auditSource, { appId: appId, app: app });
eventlog.add(eventlog.ACTION_APP_UNINSTALL, auditSource, { appId: appId });
taskmanager.startAppTask(appId, callback);
});
@@ -1011,7 +1014,7 @@ function checkManifestConstraints(manifest) {
}
if (semver.valid(manifest.minBoxVersion) && semver.gt(manifest.minBoxVersion, config.version())) {
return new AppsError(AppsError.BAD_FIELD, 'App version requires a new platform version');
return new AppsError(AppsError.BAD_FIELD, 'minBoxVersion exceeds Box version');
}
return null;
@@ -1025,8 +1028,9 @@ function exec(appId, options, callback) {
var cmd = options.cmd || [ '/bin/bash' ];
assert(util.isArray(cmd) && cmd.length > 0);
get(appId, function (error, app) {
if (error) return callback(error);
appdb.get(appId, function (error, app) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
if (app.installationState !== appdb.ISTATE_INSTALLED || app.runState !== appdb.RSTATE_RUNNING) {
return callback(new AppsError(AppsError.BAD_STATE, 'App not installed or running'));
@@ -1165,17 +1169,17 @@ function listBackups(page, perPage, appId, callback) {
function restoreInstalledApps(callback) {
assert.strictEqual(typeof callback, 'function');
getAll(function (error, apps) {
appdb.getAll(function (error, apps) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
async.map(apps, function (app, iteratorDone) {
debug('marking %s for restore', app.intrinsicFqdn);
backups.getByAppIdPaged(1, 1, app.id, function (error, results) {
var restoreConfig = !error && results.length ? { backupId: results[0].id, backupFormat: results[0].format } : null;
debug(`marking ${app.fqdn} for restore using restore config ${JSON.stringify(restoreConfig)}`);
appdb.setInstallationCommand(app.id, appdb.ISTATE_PENDING_RESTORE, { restoreConfig: restoreConfig, oldConfig: null }, function (error) {
if (error) debug(`Error marking ${app.fqdn} for restore: ${JSON.stringify(error)}`);
if (error) debug('did not mark %s for restore', app.intrinsicFqdn, error);
iteratorDone(); // always succeed
});
@@ -1187,14 +1191,14 @@ function restoreInstalledApps(callback) {
function configureInstalledApps(callback) {
assert.strictEqual(typeof callback, 'function');
getAll(function (error, apps) {
appdb.getAll(function (error, apps) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
async.map(apps, function (app, iteratorDone) {
debug(`marking ${app.fqdn} for reconfigure`);
debug('marking %s for reconfigure', app.intrinsicFqdn);
appdb.setInstallationCommand(app.id, appdb.ISTATE_PENDING_CONFIGURE, { oldConfig: null }, function (error) {
if (error) debug(`Error marking ${app.fqdn} for reconfigure: ${JSON.stringify(error)}`);
if (error) debug('did not mark %s for reconfigure', app.intrinsicFqdn, error);
iteratorDone(); // always succeed
});
+75 -122
View File
@@ -18,17 +18,11 @@ exports = module.exports = {
AppstoreError: AppstoreError
};
var apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
var assert = require('assert'),
config = require('./config.js'),
debug = require('debug')('box:appstore'),
domains = require('./domains.js'),
eventlog = require('./eventlog.js'),
mail = require('./mail.js'),
os = require('os'),
safe = require('safetydance'),
semver = require('semver'),
settings = require('./settings.js'),
superagent = require('superagent'),
util = require('util');
@@ -62,12 +56,23 @@ var NOOP_CALLBACK = function (error) { if (error) debug(error); };
function getAppstoreConfig(callback) {
assert.strictEqual(typeof callback, 'function');
settings.getAppstoreConfig(function (error, result) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
if (!result.token) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED));
// Caas Cloudrons do not store appstore credentials in their local database
if (config.provider() === 'caas') {
var url = config.apiServerOrigin() + '/api/v1/exchangeBoxTokenWithUserToken';
superagent.post(url).query({ token: config.token() }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
if (result.statusCode !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App unpurchase failed. %s %j', result.status, result.body)));
callback(null, result);
});
callback(null, result.body);
});
} else {
settings.getAppstoreConfig(function (error, result) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
if (!result.token) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED));
callback(null, result);
});
}
}
function getSubscription(callback) {
@@ -130,7 +135,7 @@ function unpurchase(appId, appstoreId, callback) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
if (result.statusCode === 403 || result.statusCode === 401) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED));
if (result.statusCode === 404) return callback(null); // was never purchased
if (result.statusCode !== 201 && result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App unpurchase failed. %s %j', result.status, result.body)));
if (result.statusCode !== 201 && result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App purchase failed. %s %j', result.status, result.body)));
superagent.del(url).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
@@ -143,87 +148,62 @@ function unpurchase(appId, appstoreId, callback) {
});
}
function sendAliveStatus(callback) {
function sendAliveStatus(data, callback) {
callback = callback || NOOP_CALLBACK;
var allSettings, allDomains, mailDomains, loginEvents;
settings.getAll(function (error, result) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
async.series([
function (callback) {
settings.getAll(function (error, result) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
allSettings = result;
callback();
});
},
function (callback) {
domains.getAll(function (error, result) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
allDomains = result;
callback();
});
},
function (callback) {
mail.getAll(function (error, result) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
mailDomains = result;
callback();
});
},
function (callback) {
eventlog.getAllPaged([ eventlog.ACTION_USER_LOGIN ], null, 1, 1, function (error, result) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
loginEvents = result;
callback();
});
}
], function (error) {
if (error) return callback(error);
eventlog.getAllPaged(eventlog.ACTION_USER_LOGIN, null, 1, 1, function (error, loginEvents) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
var backendSettings = {
backupConfig: {
provider: allSettings[settings.BACKUP_CONFIG_KEY].provider,
hardlinks: !allSettings[settings.BACKUP_CONFIG_KEY].noHardlinks
},
domainConfig: {
count: allDomains.length,
domains: Array.from(new Set(allDomains.map(function (d) { return { domain: d.domain, provider: d.provider }; })))
},
mailConfig: {
outboundCount: mailDomains.length,
inboundCount: mailDomains.filter(function (d) { return d.enabled; }).length,
catchAllCount: mailDomains.filter(function (d) { return d.catchAll.length !== 0; }).length,
relayProviders: Array.from(new Set(mailDomains.map(function (d) { return d.relay.provider; })))
},
appAutoupdatePattern: allSettings[settings.APP_AUTOUPDATE_PATTERN_KEY],
boxAutoupdatePattern: allSettings[settings.BOX_AUTOUPDATE_PATTERN_KEY],
timeZone: allSettings[settings.TIME_ZONE_KEY],
};
var backendSettings = {
tlsConfig: {
provider: result[settings.TLS_CONFIG_KEY].provider
},
backupConfig: {
provider: result[settings.BACKUP_CONFIG_KEY].provider,
hardlinks: !result[settings.BACKUP_CONFIG_KEY].noHardlinks
},
mailConfig: {
enabled: result[settings.MAIL_CONFIG_KEY].enabled
},
mailRelay: {
provider: result[settings.MAIL_RELAY_KEY].provider
},
mailCatchAll: {
count: result[settings.CATCH_ALL_ADDRESS_KEY].length
},
autoupdatePattern: result[settings.AUTOUPDATE_PATTERN_KEY],
timeZone: result[settings.TIME_ZONE_KEY],
};
var data = {
version: config.version(),
adminFqdn: config.adminFqdn(),
provider: config.provider(),
backendSettings: backendSettings,
machine: {
cpus: os.cpus(),
totalmem: os.totalmem()
},
events: {
lastLogin: loginEvents[0] ? (new Date(loginEvents[0].creationTime).getTime()) : 0
}
};
var data = {
domain: config.fqdn(),
version: config.version(),
adminFqdn: config.adminFqdn(),
provider: config.provider(),
backendSettings: backendSettings,
machine: {
cpus: os.cpus(),
totalmem: os.totalmem()
},
events: {
lastLogin: loginEvents[0] ? (new Date(loginEvents[0].creationTime).getTime()) : 0
}
};
getAppstoreConfig(function (error, appstoreConfig) {
if (error) return callback(error);
getAppstoreConfig(function (error, appstoreConfig) {
if (error) return callback(error);
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/alive';
superagent.post(url).send(data).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
if (result.statusCode === 404) return callback(new AppstoreError(AppstoreError.NOT_FOUND));
if (result.statusCode !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Sending alive status failed. %s %j', result.status, result.body)));
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/alive';
superagent.post(url).send(data).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
if (result.statusCode === 404) return callback(new AppstoreError(AppstoreError.NOT_FOUND));
if (result.statusCode !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Sending alive status failed. %s %j', result.status, result.body)));
callback(null);
callback(null);
});
});
});
});
@@ -240,16 +220,10 @@ function getBoxUpdate(callback) {
superagent.get(url).query({ accessToken: appstoreConfig.token, boxVersion: config.version() }).timeout(10 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
if (result.statusCode === 204) return callback(null); // no update
if (result.statusCode !== 200 || !result.body) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
var updateInfo = result.body;
if (!semver.valid(updateInfo.version) || semver.gt(config.version(), updateInfo.version)) {
return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
}
if (result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
// { version, changelog, upgrade, sourceTarballUrl}
callback(null, updateInfo);
callback(null, result.body);
});
});
}
@@ -266,21 +240,10 @@ function getAppUpdate(app, callback) {
superagent.get(url).query({ accessToken: appstoreConfig.token, boxVersion: config.version(), appId: app.appStoreId, appVersion: app.manifest.version }).timeout(10 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
if (result.statusCode === 204) return callback(null); // no update
if (result.statusCode !== 200 || !result.body) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
const updateInfo = result.body;
// for the appstore, x.y.z is the same as x.y.z-0 but in semver, x.y.z > x.y.z-0
const curAppVersion = semver.prerelease(app.manifest.version) ? app.manifest.version : `${app.manifest.version}-0`;
// do some sanity checks
if (!safe.query(updateInfo, 'manifest.version') || semver.gt(curAppVersion, safe.query(updateInfo, 'manifest.version'))) {
debug('Skipping malformed update of app %s version: %s. got %j', app.id, curAppVersion, updateInfo);
return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Malformed update: %s %s', result.statusCode, result.text)));
}
if (result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
// { id, creationDate, manifest }
callback(null, updateInfo);
callback(null, result.body);
});
});
}
@@ -312,26 +275,16 @@ function sendFeedback(info, callback) {
assert.strictEqual(typeof info.description, 'string');
assert.strictEqual(typeof callback, 'function');
function collectAppInfoIfNeeded(callback) {
if (!info.appId) return callback();
apps.get(info.appId, callback);
}
getAppstoreConfig(function (error, appstoreConfig) {
if (error) return callback(error);
collectAppInfoIfNeeded(function (error, result) {
if (error) console.error('Unable to get app info', error);
if (result) info.app = result;
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/feedback';
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/feedback';
superagent.post(url).query({ accessToken: appstoreConfig.token }).send(info).timeout(10 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
if (result.statusCode !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
superagent.post(url).query({ accessToken: appstoreConfig.token }).send(info).timeout(10 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
if (result.statusCode !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
callback(null);
});
callback(null);
});
});
}
+56 -25
View File
@@ -8,14 +8,15 @@ exports = module.exports = {
// exported for testing
_reserveHttpPort: reserveHttpPort,
_configureReverseProxy: configureReverseProxy,
_unconfigureReverseProxy: unconfigureReverseProxy,
_configureNginx: configureNginx,
_unconfigureNginx: unconfigureNginx,
_createVolume: createVolume,
_deleteVolume: deleteVolume,
_verifyManifest: verifyManifest,
_registerSubdomain: registerSubdomain,
_unregisterSubdomain: unregisterSubdomain,
_waitForDnsPropagation: waitForDnsPropagation
_waitForDnsPropagation: waitForDnsPropagation,
_waitForAltDomainDnsPropagation: waitForAltDomainDnsPropagation
};
require('supererror')({ splatchError: true });
@@ -31,6 +32,7 @@ var addons = require('./addons.js'),
assert = require('assert'),
async = require('async'),
backups = require('./backups.js'),
certificates = require('./certificates.js'),
config = require('./config.js'),
database = require('./database.js'),
DatabaseError = require('./databaseerror.js'),
@@ -42,10 +44,10 @@ var addons = require('./addons.js'),
fs = require('fs'),
manifestFormat = require('cloudron-manifestformat'),
net = require('net'),
nginx = require('./nginx.js'),
os = require('os'),
path = require('path'),
paths = require('./paths.js'),
reverseProxy = require('./reverseproxy.js'),
safe = require('safetydance'),
shell = require('./shell.js'),
superagent = require('superagent'),
@@ -70,7 +72,8 @@ function initialize(callback) {
function debugApp(app) {
assert.strictEqual(typeof app, 'object');
debug(app.fqdn + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
var prefix = app ? (app.intrinsicFqdn || '(bare)') : '(no app)';
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
}
// updates the app object and the database
@@ -110,19 +113,23 @@ function reserveHttpPort(app, callback) {
});
}
function configureReverseProxy(app, callback) {
function configureNginx(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
reverseProxy.configureApp(app, { userId: null, username: 'apptask' }, callback);
certificates.ensureCertificate(app, function (error, certFilePath, keyFilePath) {
if (error) return callback(error);
nginx.configureApp(app, certFilePath, keyFilePath, callback);
});
}
function unconfigureReverseProxy(app, callback) {
function unconfigureNginx(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
// TODO: maybe revoke the cert
reverseProxy.unconfigureApp(app, callback);
nginx.unconfigureApp(app, callback);
}
function createContainer(app, callback) {
@@ -264,16 +271,16 @@ function registerSubdomain(app, overwrite, callback) {
if (error) return callback(error);
async.retry({ times: 200, interval: 5000 }, function (retryCallback) {
debugApp(app, 'Registering subdomain location [%s] overwrite: %s', app.fqdn, overwrite);
debugApp(app, 'Registering subdomain location [%s] overwrite: %s', app.intrinsicFqdn, overwrite);
// get the current record before updating it
domains.getDnsRecords(app.location, app.domain, 'A', function (error, values) {
domains.getDNSRecords(app.location, app.domain, 'A', function (error, values) {
if (error) return retryCallback(error);
// refuse to update any existing DNS record for custom domains that we did not create
if (values.length !== 0 && !overwrite) return retryCallback(null, new Error('DNS Record already exists'));
domains.upsertDnsRecords(app.location, app.domain, 'A', [ ip ], function (error, changeId) {
domains.upsertDNSRecords(app.location, app.domain, 'A', [ ip ], function (error, changeId) {
if (error && (error.reason === DomainError.STILL_BUSY || error.reason === DomainError.EXTERNAL_ERROR)) return retryCallback(error); // try again
retryCallback(null, error || changeId);
@@ -303,9 +310,9 @@ function unregisterSubdomain(app, location, domain, callback) {
if (error) return callback(error);
async.retry({ times: 30, interval: 5000 }, function (retryCallback) {
debugApp(app, 'Unregistering subdomain: %s', app.fqdn);
debugApp(app, 'Unregistering subdomain: %s', app.intrinsicFqdn);
domains.removeDnsRecords(location, domain, 'A', [ ip ], function (error) {
domains.removeDNSRecords(location, domain, 'A', [ ip ], function (error) {
if (error && error.reason === DomainError.NOT_FOUND) return retryCallback(null, null); // domain can be not found if oldConfig.domain or restoreConfig.domain was removed
if (error && (error.reason === DomainError.STILL_BUSY || error.reason === DomainError.EXTERNAL_ERROR)) return retryCallback(error); // try again
@@ -341,10 +348,27 @@ function waitForDnsPropagation(app, callback) {
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error);
domains.waitForDnsRecord(app.fqdn, app.domain, ip, { interval: 5000, times: 120 }, callback);
domains.waitForDNSRecord(app.intrinsicFqdn, app.domain, ip, 'A', { interval: 5000, times: 120 }, callback);
});
}
function waitForAltDomainDnsPropagation(app, callback) {
if (!app.altDomain) return callback(null);
// try for 10 minutes before giving up. this allows the user to "reconfigure" the app in the case where
// an app has an external domain and cloudron is migrated to custom domain.
var isNakedDomain = tld.getDomain(app.altDomain) === app.altDomain;
if (isNakedDomain) { // check naked domains with A record since CNAME records don't work there
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error);
domains.waitForDNSRecord(app.altDomain, tld.getDomain(app.altDomain), ip, 'A', { interval: 10000, times: 60 }, callback);
});
} else {
domains.waitForDNSRecord(app.altDomain, tld.getDomain(app.altDomain), app.intrinsicFqdn + '.', 'CNAME', { interval: 10000, times: 60 }, callback);
}
}
// Ordering is based on the following rationale:
// - configure nginx, icon, oauth
// - register subdomain.
@@ -369,7 +393,7 @@ function install(app, callback) {
// teardown for re-installs
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
unconfigureReverseProxy.bind(null, app),
unconfigureNginx.bind(null, app),
removeCollectdProfile.bind(null, app),
removeLogrotateConfig.bind(null, app),
stopApp.bind(null, app),
@@ -427,8 +451,11 @@ function install(app, callback) {
updateApp.bind(null, app, { installationProgress: '85, Waiting for DNS propagation' }),
exports._waitForDnsPropagation.bind(null, app),
updateApp.bind(null, app, { installationProgress: '95, Configuring reverse proxy' }),
configureReverseProxy.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Waiting for External Domain setup' }),
exports._waitForAltDomainDnsPropagation.bind(null, app), // required when restoring and !restoreConfig
updateApp.bind(null, app, { installationProgress: '95, Configure nginx' }),
configureNginx.bind(null, app),
// done!
function (callback) {
@@ -472,11 +499,11 @@ function configure(app, callback) {
assert.strictEqual(typeof callback, 'function');
// oldConfig can be null during an infra update
var locationChanged = app.oldConfig && (app.oldConfig.fqdn !== app.fqdn);
var locationChanged = app.oldConfig && (app.oldConfig.intrinsicFqdn !== app.intrinsicFqdn);
async.series([
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
unconfigureReverseProxy.bind(null, app),
unconfigureNginx.bind(null, app),
removeCollectdProfile.bind(null, app),
removeLogrotateConfig.bind(null, app),
stopApp.bind(null, app),
@@ -484,7 +511,8 @@ function configure(app, callback) {
function (next) {
if (!locationChanged) return next();
unregisterSubdomain(app, app.oldConfig.location, app.oldConfig.domain, next);
// the config.fqdn() fallback can be removed after 1.9
unregisterSubdomain(app, app.oldConfig.location, app.oldConfig.domain || config.fqdn(), next);
},
reserveHttpPort.bind(null, app),
@@ -519,8 +547,11 @@ function configure(app, callback) {
updateApp.bind(null, app, { installationProgress: '80, Waiting for DNS propagation' }),
exports._waitForDnsPropagation.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Configuring reverse proxy' }),
configureReverseProxy.bind(null, app),
updateApp.bind(null, app, { installationProgress: '85, Waiting for External Domain setup' }),
exports._waitForAltDomainDnsPropagation.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Configuring Nginx' }),
configureNginx.bind(null, app),
// done!
function (callback) {
@@ -679,8 +710,8 @@ function uninstall(app, callback) {
updateApp.bind(null, app, { installationProgress: '80, Cleanup icon' }),
removeIcon.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Unconfiguring reverse proxy' }),
unconfigureReverseProxy.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Unconfiguring Nginx' }),
unconfigureNginx.bind(null, app),
updateApp.bind(null, app, { installationProgress: '95, Remove app from database' }),
appdb.del.bind(null, app.id)
+14 -14
View File
@@ -25,22 +25,22 @@ var assert = require('assert'),
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
passport.serializeUser(function (user, callback) {
callback(null, user.id);
});
passport.deserializeUser(function(userId, callback) {
user.get(userId, function (error, result) {
if (error) return callback(error);
var md5 = crypto.createHash('md5').update(result.email).digest('hex');
var md5 = crypto.createHash('md5').update(result.alternateEmail || result.email).digest('hex');
result.gravatar = 'https://www.gravatar.com/avatar/' + md5 + '.jpg?s=24&d=mm';
callback(null, result);
});
});
passport.use(new LocalStrategy(function (username, password, callback) {
if (username.indexOf('@') === -1) {
user.verifyWithUsername(username, password, function (error, result) {
@@ -60,7 +60,7 @@ function initialize(callback) {
});
}
}));
passport.use(new BasicStrategy(function (username, password, callback) {
if (username.indexOf('cid-') === 0) {
debug('BasicStrategy: detected client id %s instead of username:password', username);
@@ -82,7 +82,7 @@ function initialize(callback) {
});
}
}));
passport.use(new ClientPasswordStrategy(function (clientId, clientSecret, callback) {
clients.get(clientId, function(error, client) {
if (error && error.reason === ClientsError.NOT_FOUND) return callback(null, false);
@@ -91,15 +91,15 @@ function initialize(callback) {
return callback(null, client);
});
}));
passport.use(new BearerStrategy(accessTokenAuth));
callback(null);
}
function uninitialize(callback) {
assert.strictEqual(typeof callback, 'function');
callback(null);
}
@@ -110,15 +110,15 @@ function accessTokenAuth(accessToken, callback) {
tokendb.get(accessToken, function (error, token) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, false);
if (error) return callback(error);
// scopes here can define what capabilities that token carries
// passport put the 'info' object into req.authInfo, where we can further validate the scopes
var info = { scope: token.scope };
user.get(token.identifier, function (error, user) {
if (error && error.reason === UserError.NOT_FOUND) return callback(null, false);
if (error) return callback(error);
callback(null, user, info);
});
});
+16 -19
View File
@@ -68,9 +68,10 @@ var NOOP_CALLBACK = function (error) { if (error) debug(error); };
var BACKUPTASK_CMD = path.join(__dirname, 'backuptask.js');
function debugApp(app) {
assert(typeof app === 'object');
assert(!app || typeof app === 'object');
debug(app.fqdn + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
var prefix = app ? app.intrinsicFqdn : '(no app)';
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
}
function BackupsError(reason, errorOrMessage) {
@@ -231,11 +232,6 @@ function sync(backupConfig, backupId, dataDir, callback) {
assert.strictEqual(typeof dataDir, 'string');
assert.strictEqual(typeof callback, 'function');
function setBackupProgress(message) {
debug(message);
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, message);
}
syncer.sync(dataDir, function processTask(task, iteratorCallback) {
debug('sync: processing task: %j', task);
var backupFilePath = path.join(getBackupFilePath(backupConfig, backupId, backupConfig.format), task.path);
@@ -243,23 +239,24 @@ function sync(backupConfig, backupId, dataDir, callback) {
if (task.operation === 'removedir') {
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, `Removing directory ${task.path}`);
return api(backupConfig.provider).removeDir(backupConfig, backupFilePath)
.on('progress', setBackupProgress)
.on('progress', function (detail) {
debug(`sync: ${detail}`);
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, detail);
})
.on('done', iteratorCallback);
} else if (task.operation === 'remove') {
setBackupProgress(`Removing ${task.path}`);
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, `Removing ${task.path}`);
return api(backupConfig.provider).remove(backupConfig, backupFilePath, iteratorCallback);
}
var retryCount = 0;
async.retry({ times: 5, interval: 20000 }, function (retryCallback) {
retryCallback = once(retryCallback); // protect again upload() erroring much later after read stream error
++retryCount;
debug(`${task.operation} ${task.path} try ${retryCount}`);
if (task.operation === 'add') {
setBackupProgress(`Adding ${task.path}`);
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, `Adding ${task.path}`);
var stream = fs.createReadStream(path.join(dataDir, task.path));
stream.on('error', function (error) { setBackupProgress(`read stream error for ${task.path}: ${error.message}`); retryCallback(); }); // ignore error if file disappears
stream.on('error', function () { return retryCallback(); }); // ignore error if file disappears
api(backupConfig.provider).upload(backupConfig, backupFilePath, stream, retryCallback);
}
}, iteratorCallback);
@@ -297,6 +294,8 @@ function upload(backupId, format, dataDir, callback) {
assert.strictEqual(typeof dataDir, 'string');
assert.strictEqual(typeof callback, 'function');
callback = once(callback);
debug('upload: id %s format %s dataDir %s', backupId, format, dataDir);
settings.getBackupConfig(function (error, backupConfig) {
@@ -304,8 +303,6 @@ function upload(backupId, format, dataDir, callback) {
if (format === 'tgz') {
async.retry({ times: 5, interval: 20000 }, function (retryCallback) {
retryCallback = once(retryCallback); // protect again upload() erroring much later after tar stream error
var tarStream = createTarPackStream(dataDir, backupConfig.key || null);
tarStream.on('error', retryCallback); // already returns BackupsError
@@ -723,7 +720,7 @@ function backupApp(app, callback) {
const timestamp = (new Date()).toISOString().replace(/[T.]/g, '-').replace(/[:Z]/g,'');
safe.fs.unlinkSync(paths.BACKUP_LOG_FILE); // start fresh log file
progress.set(progress.BACKUP, 10, 'Backing up ' + app.fqdn);
progress.set(progress.BACKUP, 10, 'Backing up ' + (app.altDomain || app.intrinsicFqdn));
backupAppWithTimestamp(app, timestamp, function (error) {
progress.set(progress.BACKUP, 100, error ? error.message : '');
@@ -750,12 +747,12 @@ function backupBoxAndApps(auditSource, callback) {
var step = 100/(allApps.length+2);
async.mapSeries(allApps, function iterator(app, iteratorCallback) {
progress.set(progress.BACKUP, step * processed, 'Backing up ' + app.fqdn);
progress.set(progress.BACKUP, step * processed, 'Backing up ' + (app.altDomain || app.intrinsicFqdn));
++processed;
if (!app.enableBackup) {
progress.set(progress.BACKUP, step * processed, 'Skipped backup ' + app.fqdn);
progress.set(progress.BACKUP, step * processed, 'Skipped backup ' + (app.altDomain || app.intrinsicFqdn));
return iteratorCallback(null, null); // nothing to backup
}
@@ -765,7 +762,7 @@ function backupBoxAndApps(auditSource, callback) {
return iteratorCallback(error);
}
progress.set(progress.BACKUP, step * processed, 'Backed up ' + app.fqdn);
progress.set(progress.BACKUP, step * processed, 'Backed up ' + (app.altDomain || app.intrinsicFqdn));
iteratorCallback(null, backupId || null); // clear backupId if is in BAD_STATE and never backed up
});
+48 -120
View File
@@ -1,16 +1,11 @@
'use strict';
exports = module.exports = {
verifySetupToken: verifySetupToken,
setupDone: setupDone,
changePlan: changePlan,
upgrade: upgrade,
sendHeartbeat: sendHeartbeat,
getBoxAndUserDetails: getBoxAndUserDetails,
setPtrRecord: setPtrRecord,
CaasError: CaasError
setPtrRecord: setPtrRecord
};
var assert = require('assert'),
@@ -20,13 +15,13 @@ var assert = require('assert'),
locker = require('./locker.js'),
path = require('path'),
progress = require('./progress.js'),
settings = require('./settings.js'),
shell = require('./shell.js'),
superagent = require('superagent'),
util = require('util'),
_ = require('underscore');
const RETIRE_CMD = path.join(__dirname, 'scripts/retire.sh');
var gBoxAndUserDetails = null; // cached cloudron details like region,size...
function CaasError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
@@ -48,10 +43,9 @@ function CaasError(reason, errorOrMessage) {
}
util.inherits(CaasError, Error);
CaasError.BAD_FIELD = 'Field error';
CaasError.BAD_STATE = 'Bad state';
CaasError.INVALID_TOKEN = 'Invalid Token';
CaasError.INTERNAL_ERROR = 'Internal Error';
CaasError.EXTERNAL_ERROR = 'External Error';
CaasError.BAD_STATE = 'Bad state';
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
@@ -62,64 +56,14 @@ function retire(reason, info, callback) {
var data = {
apiServerOrigin: config.apiServerOrigin(),
adminFqdn: config.adminFqdn()
adminFqdn: config.adminFqdn(),
fqdn: config.fqdn()
};
shell.sudo('retire', [ RETIRE_CMD, reason, JSON.stringify(info), JSON.stringify(data) ], callback);
}
function getCaasConfig(callback) {
assert.strictEqual(typeof callback, 'function');
settings.getCaasConfig(function (error, result) {
if (error) return callback(new CaasError(CaasError.INTERNAL_ERROR, error));
callback(null, result);
});
}
function verifySetupToken(setupToken, callback) {
assert.strictEqual(typeof setupToken, 'string');
assert.strictEqual(typeof callback, 'function');
settings.getCaasConfig(function (error, caasConfig) {
if (error) return callback(new CaasError(CaasError.INTERNAL_ERROR, error));
superagent.get(config.apiServerOrigin() + '/api/v1/boxes/' + caasConfig.boxId + '/setup/verify').query({ setupToken: setupToken })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(new CaasError(CaasError.EXTERNAL_ERROR, error));
if (result.statusCode === 403) return callback(new CaasError(CaasError.INVALID_TOKEN));
if (result.statusCode === 409) return callback(new CaasError(CaasError.BAD_STATE, 'Already setup'));
if (result.statusCode !== 200) return callback(new CaasError(CaasError.EXTERNAL_ERROR, error));
callback(null);
});
});
}
function setupDone(setupToken, callback) {
assert.strictEqual(typeof setupToken, 'string');
assert.strictEqual(typeof callback, 'function');
settings.getCaasConfig(function (error, caasConfig) {
if (error) return callback(new CaasError(CaasError.INTERNAL_ERROR, error));
// Now let the api server know we got activated
superagent.post(config.apiServerOrigin() + '/api/v1/boxes/' + caasConfig.boxId + '/setup/done').query({ setupToken: setupToken })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(new CaasError(CaasError.EXTERNAL_ERROR, error));
if (result.statusCode === 403) return callback(new CaasError(CaasError.INVALID_TOKEN));
if (result.statusCode === 409) return callback(new CaasError(CaasError.BAD_STATE, 'Already setup'));
if (result.statusCode !== 201) return callback(new CaasError(CaasError.EXTERNAL_ERROR, error));
callback(null);
});
});
}
function doMigrate(options, caasConfig, callback) {
function doMigrate(options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof caasConfig, 'object');
assert.strictEqual(typeof callback, 'function');
var error = locker.lock(locker.OP_MIGRATE);
@@ -140,8 +84,8 @@ function doMigrate(options, caasConfig, callback) {
debug('migrate: domain: %s size %s region %s', options.domain, options.size, options.region);
superagent
.post(config.apiServerOrigin() + '/api/v1/boxes/' + caasConfig.boxId + '/migrate')
.query({ token: caasConfig.token })
.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/migrate')
.query({ token: config.token() })
.send(options)
.timeout(30 * 1000)
.end(function (error, result) {
@@ -165,11 +109,7 @@ function changePlan(options, callback) {
if (config.isDemo()) return callback(new CaasError(CaasError.BAD_FIELD, 'Not allowed in demo mode'));
getCaasConfig(function (error, result) {
if (error) return callback(error);
doMigrate(options, result, callback);
});
doMigrate(options, callback);
}
// this function expects a lock
@@ -186,81 +126,69 @@ function upgrade(boxUpdateInfo, callback) {
backups.backupBoxAndApps({ userId: null, username: 'upgrader' }, function (error) {
if (error) return upgradeError(error);
getCaasConfig(function (error, result) {
if (error) return upgradeError(error);
superagent.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/upgrade')
.query({ token: config.token() })
.send({ version: boxUpdateInfo.version })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return upgradeError(new Error('Network error making upgrade request: ' + error));
if (result.statusCode !== 202) return upgradeError(new Error(util.format('Server not ready to upgrade. statusCode: %s body: %j', result.status, result.body)));
superagent.post(config.apiServerOrigin() + '/api/v1/boxes/' + result.boxId + '/upgrade')
.query({ token: result.token })
.send({ version: boxUpdateInfo.version })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return upgradeError(new Error('Network error making upgrade request: ' + error));
if (result.statusCode !== 202) return upgradeError(new Error(util.format('Server not ready to upgrade. statusCode: %s body: %j', result.status, result.body)));
progress.set(progress.UPDATE, 10, 'Updating base system');
progress.set(progress.UPDATE, 10, 'Updating base system');
// no need to unlock since this is the last thing we ever do on this box
callback();
// no need to unlock since this is the last thing we ever do on this box
callback();
retire('upgrade');
});
});
retire('upgrade');
});
});
}
function sendHeartbeat() {
assert(config.provider() === 'caas', 'Heartbeat is only sent for managed cloudrons');
getCaasConfig(function (error, result) {
if (error) return debug('Caas config missing', error);
var url = config.apiServerOrigin() + '/api/v1/boxes/' + result.boxId + '/heartbeat';
superagent.post(url).query({ token: result.token, version: config.version() }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) debug('Network error sending heartbeat.', error);
else if (result.statusCode !== 200) debug('Server responded to heartbeat with %s %s', result.statusCode, result.text);
else debug('Heartbeat sent to %s', url);
});
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/heartbeat';
superagent.post(url).query({ token: config.token(), version: config.version() }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) debug('Network error sending heartbeat.', error);
else if (result.statusCode !== 200) debug('Server responded to heartbeat with %s %s', result.statusCode, result.text);
else debug('Heartbeat sent to %s', url);
});
}
function getBoxAndUserDetails(callback) {
assert.strictEqual(typeof callback, 'function');
if (gBoxAndUserDetails) return callback(null, gBoxAndUserDetails);
if (config.provider() !== 'caas') return callback(null, {});
getCaasConfig(function (error, caasConfig) {
if (error) return callback(error);
superagent
.get(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn())
.query({ token: config.token() })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(new CaasError(CaasError.EXTERNAL_ERROR, 'Cannot reach appstore'));
if (result.statusCode !== 200) return callback(new CaasError(CaasError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
superagent
.get(config.apiServerOrigin() + '/api/v1/boxes/' + caasConfig.boxId)
.query({ token: caasConfig.token })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(new CaasError(CaasError.EXTERNAL_ERROR, 'Cannot reach appstore'));
if (result.statusCode !== 200) return callback(new CaasError(CaasError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
gBoxAndUserDetails = result.body;
return callback(null, result.body);
});
});
return callback(null, gBoxAndUserDetails);
});
}
function setPtrRecord(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
getCaasConfig(function (error, result) {
if (error) return callback(error);
superagent
.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/ptr')
.query({ token: config.token() })
.send({ domain: domain })
.timeout(5 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(new CaasError(CaasError.EXTERNAL_ERROR, 'Cannot reach appstore'));
if (result.statusCode !== 202) return callback(new CaasError(CaasError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
superagent
.post(config.apiServerOrigin() + '/api/v1/boxes/' + result.boxId + '/ptr')
.query({ token: result.token })
.send({ domain: domain })
.timeout(5 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(new CaasError(CaasError.EXTERNAL_ERROR, 'Cannot reach appstore'));
if (result.statusCode !== 202) return callback(new CaasError(CaasError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
return callback(null);
});
});
return callback(null);
});
}
+4 -4
View File
@@ -10,12 +10,12 @@ exports = module.exports = {
var assert = require('assert'),
debug = require('debug')('box:cert/caas.js');
function getCertificate(vhost, options, callback) {
assert.strictEqual(typeof vhost, 'string');
function getCertificate(domain, options, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debug('getCertificate: using fallback certificate', vhost);
debug('getCertificate: using fallback certificate', domain);
return callback(null, '', '');
return callback(null, 'cert/host.cert', 'cert/host.key');
}
+3 -3
View File
@@ -10,12 +10,12 @@ exports = module.exports = {
var assert = require('assert'),
debug = require('debug')('box:cert/fallback.js');
function getCertificate(vhost, options, callback) {
assert.strictEqual(typeof vhost, 'string');
function getCertificate(domain, options, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debug('getCertificate: using fallback certificate', vhost);
debug('getCertificate: using fallback certificate', domain);
return callback(null, '', '');
}
+447
View File
@@ -0,0 +1,447 @@
'use strict';
exports = module.exports = {
CertificatesError: CertificatesError,
ensureFallbackCertificate: ensureFallbackCertificate,
setFallbackCertificate: setFallbackCertificate,
getFallbackCertificate: getFallbackCertificate,
validateCertificate: validateCertificate,
ensureCertificate: ensureCertificate,
setAdminCertificate: setAdminCertificate,
getAdminCertificate: getAdminCertificate,
renewAll: renewAll,
initialize: initialize,
uninitialize: uninitialize,
events: null,
EVENT_CERT_CHANGED: 'cert_changed',
// exported for testing
_getApi: getApi
};
var acme = require('./cert/acme.js'),
apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
caas = require('./cert/caas.js'),
config = require('./config.js'),
constants = require('./constants.js'),
debug = require('debug')('box:certificates'),
eventlog = require('./eventlog.js'),
fallback = require('./cert/fallback.js'),
fs = require('fs'),
mailer = require('./mailer.js'),
nginx = require('./nginx.js'),
path = require('path'),
paths = require('./paths.js'),
safe = require('safetydance'),
settings = require('./settings.js'),
user = require('./user.js'),
util = require('util');
function CertificatesError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
Error.call(this);
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.reason = reason;
if (typeof errorOrMessage === 'undefined') {
this.message = reason;
} else if (typeof errorOrMessage === 'string') {
this.message = errorOrMessage;
} else {
this.message = 'Internal error';
this.nestedError = errorOrMessage;
}
}
util.inherits(CertificatesError, Error);
CertificatesError.INTERNAL_ERROR = 'Internal Error';
CertificatesError.INVALID_CERT = 'Invalid certificate';
CertificatesError.NOT_FOUND = 'Not Found';
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
exports.events = new (require('events').EventEmitter)();
callback();
}
function uninitialize(callback) {
assert.strictEqual(typeof callback, 'function');
exports.events = null;
callback();
}
function getApi(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
settings.getTlsConfig(function (error, tlsConfig) {
if (error) return callback(error);
if (tlsConfig.provider === 'fallback') return callback(null, fallback, {});
// use acme if we have altDomain or the tlsConfig is not caas
var api = (app.altDomain || tlsConfig.provider !== 'caas') ? acme : caas;
var options = { };
if (tlsConfig.provider === 'caas') {
options.prod = true; // with altDomain, we will choose acme setting based on this
} else { // acme
options.prod = tlsConfig.provider.match(/.*-prod/) !== null;
}
// registering user with an email requires A or MX record (https://github.com/letsencrypt/boulder/issues/1197)
// we cannot use admin@fqdn because the user might not have set it up.
// we simply update the account with the latest email we have each time when getting letsencrypt certs
// https://github.com/ietf-wg-acme/acme/issues/30
user.getOwner(function (error, owner) {
options.email = error ? 'support@cloudron.io' : (owner.alternateEmail || owner.email); // can error if not activated yet
callback(null, api, options);
});
});
}
function ensureFallbackCertificate(callback) {
// ensure a fallback certificate that much of our code requires
var certFilePath = path.join(paths.APP_CERTS_DIR, 'host.cert');
var keyFilePath = path.join(paths.APP_CERTS_DIR, 'host.key');
var fallbackCertPath = path.join(paths.NGINX_CERT_DIR, 'host.cert');
var fallbackKeyPath = path.join(paths.NGINX_CERT_DIR, 'host.key');
if (fs.existsSync(fallbackCertPath) && fs.existsSync(fallbackKeyPath)) {
debug('ensureFallbackCertificate: pre-existing fallback certs');
return callback();
}
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) { // existing custom fallback certs (when restarting, restoring, updating)
debug('ensureFallbackCertificate: using fallback certs provided by user');
if (!safe.child_process.execSync('cp ' + certFilePath + ' ' + fallbackCertPath)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
if (!safe.child_process.execSync('cp ' + keyFilePath + ' ' + fallbackKeyPath)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
return callback();
}
// generate a self-signed cert. it's in backup dir so that we don't create a new cert across restarts
// FIXME: this cert does not cover the naked domain. needs SAN
if (config.fqdn()) {
debug('ensureFallbackCertificate: generating self-signed certificate');
var certCommand = util.format('openssl req -x509 -newkey rsa:2048 -keyout %s -out %s -days 3650 -subj /CN=*.%s -nodes', keyFilePath, certFilePath, config.fqdn());
safe.child_process.execSync(certCommand);
if (!safe.child_process.execSync('cp ' + certFilePath + ' ' + fallbackCertPath)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
if (!safe.child_process.execSync('cp ' + keyFilePath + ' ' + fallbackKeyPath)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
return callback();
} else {
debug('ensureFallbackCertificate: cannot generate fallback certificate without domain');
return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, 'No domain set'));
}
}
function isExpiringSync(certFilePath, hours) {
assert.strictEqual(typeof certFilePath, 'string');
assert.strictEqual(typeof hours, 'number');
if (!fs.existsSync(certFilePath)) return 2; // not found
var result = safe.child_process.spawnSync('/usr/bin/openssl', [ 'x509', '-checkend', String(60 * 60 * hours), '-in', certFilePath ]);
debug('isExpiringSync: %s %s %s', certFilePath, result.stdout.toString('utf8').trim(), result.status);
return result.status === 1; // 1 - expired 0 - not expired
}
function renewAll(auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
debug('renewAll: Checking certificates for renewal');
apps.getAll(function (error, allApps) {
if (error) return callback(error);
allApps.push({ intrinsicFqdn: config.adminFqdn() }); // inject fake webadmin app
var expiringApps = [ ];
for (var i = 0; i < allApps.length; i++) {
var appDomain = allApps[i].altDomain || allApps[i].instrincFqdn;
var certFilePath = path.join(paths.APP_CERTS_DIR, appDomain + '.user.cert');
var keyFilePath = path.join(paths.APP_CERTS_DIR, appDomain + '.user.key');
if (safe.fs.existsSync(certFilePath) && safe.fs.existsSync(keyFilePath)) {
debug('renewAll: existing user key file for %s. skipping', appDomain);
continue;
}
// check if we have an auto cert to be renewed
certFilePath = path.join(paths.APP_CERTS_DIR, appDomain + '.cert');
keyFilePath = path.join(paths.APP_CERTS_DIR, appDomain + '.key');
if (!safe.fs.existsSync(keyFilePath)) {
debug('renewAll: no existing key file for %s. skipping', appDomain);
continue;
}
if (isExpiringSync(certFilePath, 24 * 30)) { // expired or not found
expiringApps.push(allApps[i]);
}
}
debug('renewAll: %j needs to be renewed', expiringApps.map(function (app) { return app.altDomain || app.intrinsicFqdn; }));
async.eachSeries(expiringApps, function iterator(app, iteratorCallback) {
var domain = app.altDomain || app.intrinsicFqdn;
getApi(app, function (error, api, apiOptions) {
if (error) return callback(error);
debug('renewAll: renewing cert for %s with options %j', domain, apiOptions);
api.getCertificate(domain, apiOptions, function (error) {
var certFilePath = path.join(paths.APP_CERTS_DIR, domain + '.cert');
var keyFilePath = path.join(paths.APP_CERTS_DIR, domain + '.key');
var errorMessage = error ? error.message : '';
eventlog.add(eventlog.ACTION_CERTIFICATE_RENEWAL, auditSource, { domain: domain, errorMessage: errorMessage });
if (error) {
debug('renewAll: could not renew cert for %s because %s', domain, error);
mailer.certificateRenewalError(domain, errorMessage);
// check if we should fallback if we expire in the coming day
if (!isExpiringSync(certFilePath, 24 * 1)) return iteratorCallback();
debug('renewAll: using fallback certs for %s since it expires soon', domain, error);
// if no cert was returned use fallback, the fallback provider will not provide any for example
var fallbackCertFilePath = path.join(paths.NGINX_CERT_DIR, domain + '.cert');
var fallbackKeyFilePath = path.join(paths.NGINX_CERT_DIR, domain + '.key');
certFilePath = fs.existsSync(fallbackCertFilePath) ? fallbackCertFilePath : 'cert/host.cert';
keyFilePath = fs.existsSync(fallbackKeyFilePath) ? fallbackKeyFilePath : 'cert/host.key';
} else {
debug('renewAll: certificate for %s renewed', domain);
}
// reconfigure and reload nginx. this is required for the case where we got a renewed cert after fallback
var configureFunc = app.intrinsicFqdn === config.adminFqdn() ?
nginx.configureAdmin.bind(null, certFilePath, keyFilePath, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn())
: nginx.configureApp.bind(null, app, certFilePath, keyFilePath);
configureFunc(function (ignoredError) {
if (ignoredError) debug('fallbackExpiredCertificates: error reconfiguring app', ignoredError);
exports.events.emit(exports.EVENT_CERT_CHANGED, domain);
iteratorCallback(); // move to next app
});
});
});
});
});
}
// note: https://tools.ietf.org/html/rfc4346#section-7.4.2 (certificate_list) requires that the
// servers certificate appears first (and not the intermediate cert)
function validateCertificate(cert, key, fqdn) {
assert(cert === null || typeof cert === 'string');
assert(key === null || typeof key === 'string');
assert.strictEqual(typeof fqdn, 'string');
function matchesDomain(domain) {
if (typeof domain !== 'string') return false;
if (domain === fqdn) return true;
if (domain.indexOf('*') === 0 && domain.slice(2) === fqdn.slice(fqdn.indexOf('.') + 1)) return true;
return false;
}
if (cert === null && key === null) return null;
if (!cert && key) return new Error('missing cert');
if (cert && !key) return new Error('missing key');
var result = safe.child_process.execSync('openssl x509 -noout -checkhost "' + fqdn + '"', { encoding: 'utf8', input: cert });
if (!result) return new Error('Invalid certificate. Unable to get certificate subject.');
// if no match, check alt names
if (result.indexOf('does match certificate') === -1) {
// https://github.com/drwetter/testssl.sh/pull/383
var cmd = 'openssl x509 -noout -text | grep -A3 "Subject Alternative Name" | \
grep "DNS:" | \
sed -e "s/DNS://g" -e "s/ //g" -e "s/,/ /g" -e "s/othername:<unsupported>//g"';
result = safe.child_process.execSync(cmd, { encoding: 'utf8', input: cert });
var altNames = result ? [ ] : result.trim().split(' '); // might fail if cert has no SAN
debug('validateCertificate: detected altNames as %j', altNames);
// check altNames
if (!altNames.some(matchesDomain)) return new Error(util.format('Certificate is not valid for this domain. Expecting %s in %j', fqdn, altNames));
}
// http://httpd.apache.org/docs/2.0/ssl/ssl_faq.html#verify
var certModulus = safe.child_process.execSync('openssl x509 -noout -modulus', { encoding: 'utf8', input: cert });
var keyModulus = safe.child_process.execSync('openssl rsa -noout -modulus', { encoding: 'utf8', input: key });
if (certModulus !== keyModulus) return new Error('Key does not match the certificate.');
// check expiration
result = safe.child_process.execSync('openssl x509 -checkend 0', { encoding: 'utf8', input: cert });
if (!result) return new Error('Certificate is expired.');
return null;
}
function setFallbackCertificate(cert, key, fqdn, callback) {
assert.strictEqual(typeof cert, 'string');
assert.strictEqual(typeof key, 'string');
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof callback, 'function');
var error = validateCertificate(cert, key, '*.' + fqdn);
if (error) return callback(new CertificatesError(CertificatesError.INVALID_CERT, error.message));
// backup the cert
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, fqdn + '.cert'), cert)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, fqdn + '.key'), key)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
// copy over fallback cert
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, fqdn + '.cert'), cert)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, fqdn + '.key'), key)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
exports.events.emit(exports.EVENT_CERT_CHANGED, '*.' + fqdn);
nginx.reload(function (error) {
if (error) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, error));
return callback(null);
});
}
function getFallbackCertificate(fqdn, callback) {
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof callback, 'function');
var cert = safe.fs.readFileSync(path.join(paths.NGINX_CERT_DIR, fqdn + '.cert'), 'utf-8');
var key = safe.fs.readFileSync(path.join(paths.NGINX_CERT_DIR, fqdn + '.key'), 'utf-8');
if (!cert || !key) return callback(new CertificatesError(CertificatesError.NOT_FOUND));
callback(null, { cert: cert, key: key });
}
function setAdminCertificate(cert, key, callback) {
assert.strictEqual(typeof cert, 'string');
assert.strictEqual(typeof key, 'string');
assert.strictEqual(typeof callback, 'function');
var vhost = config.adminFqdn();
var certFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.user.cert');
var keyFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.user.key');
var error = validateCertificate(cert, key, vhost);
if (error) return callback(new CertificatesError(CertificatesError.INVALID_CERT, error.message));
// backup the cert
if (!safe.fs.writeFileSync(certFilePath, cert)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
if (!safe.fs.writeFileSync(keyFilePath, key)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
exports.events.emit(exports.EVENT_CERT_CHANGED, vhost);
nginx.configureAdmin(certFilePath, keyFilePath, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn(), callback);
}
function getAdminCertificatePath(callback) {
assert.strictEqual(typeof callback, 'function');
var vhost = config.adminFqdn();
var certFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.user.cert');
var keyFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.user.key');
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, certFilePath, keyFilePath);
certFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.cert');
keyFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.key');
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, certFilePath, keyFilePath);
// any user fallback cert is always copied over to nginx cert dir
callback(null, path.join(paths.NGINX_CERT_DIR, 'host.cert'), path.join(paths.NGINX_CERT_DIR, 'host.key'));
}
function getAdminCertificate(callback) {
assert.strictEqual(typeof callback, 'function');
getAdminCertificatePath(function (error, certFilePath, keyFilePath) {
if (error) return callback(error);
var cert = safe.fs.readFileSync(certFilePath);
if (!cert) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error));
var key = safe.fs.readFileSync(keyFilePath);
if (!cert) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error));
return callback(null, cert, key);
});
}
function ensureCertificate(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
var domain = app.altDomain || app.intrinsicFqdn;
var certFilePath = path.join(paths.APP_CERTS_DIR, domain + '.user.cert');
var keyFilePath = path.join(paths.APP_CERTS_DIR, domain + '.user.key');
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) {
debug('ensureCertificate: %s. user certificate already exists at %s', domain, keyFilePath);
return callback(null, certFilePath, keyFilePath);
}
certFilePath = path.join(paths.APP_CERTS_DIR, domain + '.cert');
keyFilePath = path.join(paths.APP_CERTS_DIR, domain + '.key');
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) {
debug('ensureCertificate: %s. certificate already exists at %s', domain, keyFilePath);
if (!isExpiringSync(certFilePath, 24 * 1)) return callback(null, certFilePath, keyFilePath);
debug('ensureCertificate: %s cert require renewal', domain);
} else {
debug('ensureCertificate: %s cert does not exist', domain);
}
getApi(app, function (error, api, apiOptions) {
if (error) return callback(error);
debug('ensureCertificate: getting certificate for %s with options %j', domain, apiOptions);
api.getCertificate(domain, apiOptions, function (error, certFilePath, keyFilePath) {
if (error) debug('ensureCertificate: could not get certificate. using fallback certs', error);
// if no cert was returned use fallback, the fallback provider will not provide any for example
if (!certFilePath || !keyFilePath) {
var fallbackCertFilePath = path.join(paths.NGINX_CERT_DIR, app.domain + '.cert');
var fallbackKeyFilePath = path.join(paths.NGINX_CERT_DIR, app.domain + '.key');
certFilePath = fs.existsSync(fallbackCertFilePath) ? fallbackCertFilePath : 'cert/host.cert';
keyFilePath = fs.existsSync(fallbackKeyFilePath) ? fallbackKeyFilePath : 'cert/host.key';
}
callback(null, certFilePath, keyFilePath);
});
});
}
+6 -6
View File
@@ -39,6 +39,7 @@ var apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
clientdb = require('./clientdb.js'),
config = require('./config.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:clients'),
hat = require('hat'),
@@ -191,7 +192,7 @@ function getAll(callback) {
if (record.type === exports.TYPE_PROXY) record.name = result.manifest.title + ' Website Proxy';
if (record.type === exports.TYPE_OAUTH) record.name = result.manifest.title + ' OAuth';
record.domain = result.fqdn;
record.domain = result.altDomain || result.intrinsicFqdn;
tmp.push(record);
@@ -317,8 +318,7 @@ function delToken(clientId, tokenId, callback) {
});
}
function addDefaultClients(origin, callback) {
assert.strictEqual(typeof origin, 'string');
function addDefaultClients(callback) {
assert.strictEqual(typeof callback, 'function');
debug('Adding default clients');
@@ -329,8 +329,8 @@ function addDefaultClients(origin, callback) {
// id, appId, type, clientSecret, redirectURI, scope
async.series([
clientdb.upsert.bind(null, 'cid-webadmin', 'Settings', 'built-in', 'secret-webadmin', origin, ADMIN_SCOPES),
clientdb.upsert.bind(null, 'cid-sdk', 'SDK', 'built-in', 'secret-sdk', origin, '*,roleSdk'),
clientdb.upsert.bind(null, 'cid-cli', 'Cloudron Tool', 'built-in', 'secret-cli', origin, '*, roleSdk')
clientdb.upsert.bind(null, 'cid-webadmin', 'Settings', 'built-in', 'secret-webadmin', config.adminOrigin(), ADMIN_SCOPES),
clientdb.upsert.bind(null, 'cid-sdk', 'SDK', 'built-in', 'secret-sdk', config.adminOrigin(), '*,roleSdk'),
clientdb.upsert.bind(null, 'cid-cli', 'Cloudron Tool', 'built-in', 'secret-cli', config.adminOrigin(), '*, roleSdk')
], callback);
}
+494 -11
View File
@@ -5,50 +5,77 @@ exports = module.exports = {
initialize: initialize,
uninitialize: uninitialize,
activate: activate,
getConfig: getConfig,
getStatus: getStatus,
getDisks: getDisks,
dnsSetup: dnsSetup,
getLogs: getLogs,
updateToLatest: updateToLatest,
restore: restore,
reboot: reboot,
onActivated: onActivated,
checkDiskSpace: checkDiskSpace,
checkDiskSpace: checkDiskSpace
readDkimPublicKeySync: readDkimPublicKeySync,
refreshDNS: refreshDNS,
configureWebadmin: configureWebadmin
};
var assert = require('assert'),
var appdb = require('./appdb.js'),
apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
backups = require('./backups.js'),
BackupsError = require('./backups.js').BackupsError,
caas = require('./caas.js'),
certificates = require('./certificates.js'),
child_process = require('child_process'),
clients = require('./clients.js'),
config = require('./config.js'),
constants = require('./constants.js'),
cron = require('./cron.js'),
debug = require('debug')('box:cloudron'),
df = require('@sindresorhus/df'),
domains = require('./domains.js'),
DomainError = domains.DomainError,
eventlog = require('./eventlog.js'),
fs = require('fs'),
locker = require('./locker.js'),
mailer = require('./mailer.js'),
nginx = require('./nginx.js'),
os = require('os'),
path = require('path'),
paths = require('./paths.js'),
platform = require('./platform.js'),
progress = require('./progress.js'),
reverseProxy = require('./reverseproxy.js'),
safe = require('safetydance'),
semver = require('semver'),
settings = require('./settings.js'),
settingsdb = require('./settingsdb.js'),
SettingsError = settings.SettingsError,
shell = require('./shell.js'),
spawn = require('child_process').spawn,
split = require('split'),
superagent = require('superagent'),
sysinfo = require('./sysinfo.js'),
tld = require('tldjs'),
tokendb = require('./tokendb.js'),
updateChecker = require('./updatechecker.js'),
user = require('./user.js'),
UserError = user.UserError,
util = require('util'),
_ = require('underscore');
var REBOOT_CMD = path.join(__dirname, 'scripts/reboot.sh'),
UPDATE_CMD = path.join(__dirname, 'scripts/update.sh');
UPDATE_CMD = path.join(__dirname, 'scripts/update.sh'),
RESTART_CMD = path.join(__dirname, 'scripts/restart.sh');
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
var gWebadminStatus = { dns: false, tls: false, configuring: false, restoring: false };
function CloudronError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
@@ -71,6 +98,8 @@ util.inherits(CloudronError, Error);
CloudronError.BAD_FIELD = 'Field error';
CloudronError.INTERNAL_ERROR = 'Internal Error';
CloudronError.EXTERNAL_ERROR = 'External Error';
CloudronError.ALREADY_PROVISIONED = 'Already Provisioned';
CloudronError.ALREADY_SETUP = 'Already Setup';
CloudronError.BAD_STATE = 'Bad state';
CloudronError.ALREADY_UPTODATE = 'No Update Available';
CloudronError.NOT_FOUND = 'Not found';
@@ -79,12 +108,21 @@ CloudronError.SELF_UPGRADE_NOT_SUPPORTED = 'Self upgrade not supported';
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
gWebadminStatus = { dns: false, tls: false, configuring: false, restoring: false };
async.series([
certificates.initialize,
settings.initialize,
reverseProxy.configureDefaultServer,
cron.initialize, // required for caas heartbeat before activation
configureDefaultServer,
onDomainConfigured,
onActivated
], callback);
], function (error) {
if (error) return callback(error);
configureWebadmin(NOOP_CALLBACK); // for restore() and caas initial setup. do not block
callback();
});
}
function uninitialize(callback) {
@@ -93,10 +131,24 @@ function uninitialize(callback) {
async.series([
cron.uninitialize,
platform.stop,
certificates.uninitialize,
settings.uninitialize
], callback);
}
function onDomainConfigured(callback) {
callback = callback || NOOP_CALLBACK;
if (!config.fqdn()) return callback();
async.series([
clients.addDefaultClients,
certificates.ensureFallbackCertificate,
ensureDkimKey,
cron.initialize // required for caas heartbeat before activation
], callback);
}
function onActivated(callback) {
callback = callback || NOOP_CALLBACK;
@@ -111,6 +163,238 @@ function onActivated(callback) {
});
}
function autoprovision(callback) {
assert.strictEqual(typeof callback, 'function');
const confJson = safe.fs.readFileSync(paths.AUTO_PROVISION_FILE, 'utf8');
if (!confJson) return callback();
const conf = safe.JSON.parse(confJson);
if (!conf) return callback();
async.eachSeries(Object.keys(conf), function (key, iteratorDone) {
var name;
switch (key) {
case 'dnsConfig': name = 'dns_config'; break;
case 'tlsConfig': name = 'tls_config'; break;
case 'backupConfig': name = 'backup_config'; break;
case 'tlsCert':
debug(`autoprovision: ${key}`);
return fs.writeFile(path.join(paths.NGINX_CERT_DIR, 'host.cert'), conf[key], iteratorDone);
case 'tlsKey':
debug(`autoprovision: ${key}`);
return fs.writeFile(path.join(paths.NGINX_CERT_DIR, 'host.key'), conf[key], iteratorDone);
default:
debug(`autoprovision: ${key} ignored`);
return iteratorDone();
}
debug(`autoprovision: ${name}`);
settingsdb.set(name, JSON.stringify(conf[key]), iteratorDone);
}, callback);
}
function dnsSetup(adminFqdn, domain, zoneName, provider, dnsConfig, callback) {
assert.strictEqual(typeof adminFqdn, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof provider, 'string');
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof callback, 'function');
if (config.fqdn()) return callback(new CloudronError(CloudronError.ALREADY_SETUP));
if (!zoneName) zoneName = tld.getDomain(domain) || domain;
debug('dnsSetup: Setting up Cloudron with domain %s and zone %s', domain, zoneName);
function done(error) {
if (error && error.reason === DomainError.BAD_FIELD) return callback(new CloudronError(CloudronError.BAD_FIELD, error.message));
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
autoprovision(function (error) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
config.setFqdn(domain); // set fqdn only after dns config is valid, otherwise cannot re-setup if we failed
config.setAdminFqdn(adminFqdn);
config.setAdminLocation('my');
config.setZoneName(zoneName);
callback();
async.series([ // do not block
onDomainConfigured,
configureWebadmin
], NOOP_CALLBACK);
});
}
domains.get(domain, function (error, result) {
if (error && error.reason !== DomainError.NOT_FOUND) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
if (!result) domains.add(domain, zoneName, provider, dnsConfig, null /* cert */, done);
else domains.update(domain, provider, dnsConfig, null /* cert */, done);
});
}
function configureDefaultServer(callback) {
callback = callback || NOOP_CALLBACK;
debug('configureDefaultServer: domain %s', config.fqdn());
if (process.env.BOX_ENV === 'test') return callback();
var certFilePath = path.join(paths.NGINX_CERT_DIR, 'default.cert');
var keyFilePath = path.join(paths.NGINX_CERT_DIR, 'default.key');
if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) {
debug('configureDefaultServer: create new cert');
var cn = 'cloudron-' + (new Date()).toISOString(); // randomize date a bit to keep firefox happy
var certCommand = util.format('openssl req -x509 -newkey rsa:2048 -keyout %s -out %s -days 3650 -subj /CN=%s -nodes', keyFilePath, certFilePath, cn);
safe.child_process.execSync(certCommand);
}
nginx.configureAdmin(certFilePath, keyFilePath, 'default.conf', '', function (error) {
if (error) return callback(error);
debug('configureDefaultServer: done');
callback(null);
});
}
function configureWebadmin(callback) {
callback = callback || NOOP_CALLBACK;
debug('configureWebadmin: fqdn:%s status:%j', config.fqdn(), gWebadminStatus);
if (process.env.BOX_ENV === 'test' || !config.fqdn() || gWebadminStatus.configuring) return callback();
gWebadminStatus.configuring = true; // re-entracy guard
function done(error) {
gWebadminStatus.configuring = false;
debug('configureWebadmin: done error: %j', error || {});
callback(error);
}
function configureNginx(error) {
debug('configureNginx: dns update: %j', error || {});
certificates.ensureCertificate({ domain: config.fqdn(), location: config.adminLocation(), intrinsicFqdn: config.adminFqdn() }, function (error, certFilePath, keyFilePath) {
if (error) return done(error);
gWebadminStatus.tls = true;
nginx.configureAdmin(certFilePath, keyFilePath, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn(), done);
});
}
// update the DNS. configure nginx regardless of whether it succeeded so that
// box is accessible even if dns creds are invalid
sysinfo.getPublicIp(function (error, ip) {
if (error) return configureNginx(error);
addDnsRecords(ip, function (error) {
if (error) return configureNginx(error);
domains.waitForDNSRecord(config.adminFqdn(), config.fqdn(), ip, 'A', { interval: 30000, times: 50000 }, function (error) {
if (error) return configureNginx(error);
gWebadminStatus.dns = true;
configureNginx();
});
});
});
}
function setTimeZone(ip, callback) {
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
debug('setTimeZone ip:%s', ip);
superagent.get('https://geolocation.cloudron.io/json').query({ ip: ip }).timeout(10 * 1000).end(function (error, result) {
if ((error && !error.response) || result.statusCode !== 200) {
debug('Failed to get geo location: %s', error.message);
return callback(null);
}
var timezone = safe.query(result.body, 'location.time_zone');
if (!timezone || typeof timezone !== 'string') {
debug('No timezone in geoip response : %j', result.body);
return callback(null);
}
debug('Setting timezone to ', timezone);
settings.setTimeZone(timezone, callback);
});
}
function activate(username, password, email, displayName, ip, auditSource, callback) {
assert.strictEqual(typeof username, 'string');
assert.strictEqual(typeof password, 'string');
assert.strictEqual(typeof email, 'string');
assert.strictEqual(typeof displayName, 'string');
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
debug('activating user:%s email:%s', username, email);
setTimeZone(ip, function () { }); // TODO: get this from user. note that timezone is detected based on the browser location and not the cloudron region
user.createOwner(username, password, email, displayName, auditSource, function (error, userObject) {
if (error && error.reason === UserError.ALREADY_EXISTS) return callback(new CloudronError(CloudronError.ALREADY_PROVISIONED));
if (error && error.reason === UserError.BAD_FIELD) return callback(new CloudronError(CloudronError.BAD_FIELD, error.message));
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
clients.get('cid-webadmin', function (error, result) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
// Also generate a token so the admin creation can also act as a login
var token = tokendb.generateToken();
var expires = Date.now() + constants.DEFAULT_TOKEN_EXPIRATION;
tokendb.add(token, userObject.id, result.id, expires, '*', function (error) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_ACTIVATE, auditSource, { });
onActivated();
callback(null, { token: token, expires: expires });
});
});
});
}
function getStatus(callback) {
assert.strictEqual(typeof callback, 'function');
user.count(function (error, count) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
settings.getCloudronName(function (error, cloudronName) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
callback(null, {
activated: count !== 0,
version: config.version(),
apiServerOrigin: config.apiServerOrigin(), // used by CaaS tool
provider: config.provider(),
cloudronName: cloudronName,
adminFqdn: config.fqdn() ? config.adminFqdn() : null,
webadminStatus: gWebadminStatus
});
});
});
}
function getDisks(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -164,7 +448,7 @@ function getConfig(callback) {
callback(null, {
apiServerOrigin: config.apiServerOrigin(),
webServerOrigin: config.webServerOrigin(),
adminDomain: config.adminDomain(),
fqdn: config.fqdn(),
adminLocation: config.adminLocation(),
adminFqdn: config.adminFqdn(),
mailFqdn: config.mailFqdn(),
@@ -185,6 +469,167 @@ function getConfig(callback) {
});
}
function ensureDkimKey(callback) {
assert(config.fqdn(), 'fqdn is not set');
var dkimPath = path.join(paths.MAIL_DATA_DIR, 'dkim/' + config.fqdn());
var dkimPrivateKeyFile = path.join(dkimPath, 'private');
var dkimPublicKeyFile = path.join(dkimPath, 'public');
if (!fs.existsSync(dkimPrivateKeyFile) || !fs.existsSync(dkimPublicKeyFile)) {
debug('Generating new DKIM keys');
if (!safe.fs.mkdirSync(dkimPath) && safe.error.code !== 'EEXIST') {
debug('Error creating dkim.', safe.error);
return null;
}
child_process.execSync('openssl genrsa -out ' + dkimPrivateKeyFile + ' 1024');
child_process.execSync('openssl rsa -in ' + dkimPrivateKeyFile + ' -out ' + dkimPublicKeyFile + ' -pubout -outform PEM');
} else {
debug('DKIM keys already present');
}
callback();
}
function readDkimPublicKeySync() {
if (!config.fqdn()) {
debug('Cannot read dkim public key without a domain.', safe.error);
return null;
}
var dkimPath = path.join(paths.MAIL_DATA_DIR, 'dkim/' + config.fqdn());
var dkimPublicKeyFile = path.join(dkimPath, 'public');
var publicKey = safe.fs.readFileSync(dkimPublicKeyFile, 'utf8');
if (publicKey === null) {
debug('Error reading dkim public key.', safe.error);
return null;
}
// remove header, footer and new lines
publicKey = publicKey.split('\n').slice(1, -2).join('');
return publicKey;
}
// NOTE: if you change the SPF record here, be sure the wait check in mailer.js
// https://agari.zendesk.com/hc/en-us/articles/202952749-How-long-can-my-SPF-record-be-
function txtRecordsWithSpf(callback) {
assert.strictEqual(typeof callback, 'function');
domains.getDNSRecords('', config.fqdn(), 'TXT', function (error, txtRecords) {
if (error) return callback(error);
debug('txtRecordsWithSpf: current txt records - %j', txtRecords);
var i, matches, validSpf;
for (i = 0; i < txtRecords.length; i++) {
matches = txtRecords[i].match(/^("?v=spf1) /); // DO backend may return without quotes
if (matches === null) continue;
// this won't work if the entry is arbitrarily "split" across quoted strings
validSpf = txtRecords[i].indexOf('a:' + config.adminFqdn()) !== -1;
break; // there can only be one SPF record
}
if (validSpf) return callback(null, null);
if (!matches) { // no spf record was found, create one
txtRecords.push('"v=spf1 a:' + config.adminFqdn() + ' ~all"');
debug('txtRecordsWithSpf: adding txt record');
} else { // just add ourself
txtRecords[i] = matches[1] + ' a:' + config.adminFqdn() + txtRecords[i].slice(matches[1].length);
debug('txtRecordsWithSpf: inserting txt record');
}
return callback(null, txtRecords);
});
}
function addDnsRecords(ip, callback) {
assert.strictEqual(typeof ip, 'string');
callback = callback || NOOP_CALLBACK;
if (process.env.BOX_ENV === 'test') return callback();
var dkimKey = readDkimPublicKeySync();
if (!dkimKey) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, new Error('Failed to read dkim public key')));
var webadminRecord = { subdomain: config.adminLocation(), domain: config.fqdn(), type: 'A', values: [ ip ] };
// t=s limits the domainkey to this domain and not it's subdomains
var dkimRecord = { subdomain: config.dkimSelector() + '._domainkey', domain: config.fqdn(), type: 'TXT', values: [ '"v=DKIM1; t=s; p=' + dkimKey + '"' ] };
var records = [ ];
records.push(webadminRecord);
records.push(dkimRecord);
debug('addDnsRecords: %j', records);
async.retry({ times: 10, interval: 20000 }, function (retryCallback) {
txtRecordsWithSpf(function (error, txtRecords) {
if (error) return retryCallback(error);
if (txtRecords) records.push({ subdomain: '', domain: config.fqdn(), type: 'TXT', values: txtRecords });
debug('addDnsRecords: will update %j', records);
async.mapSeries(records, function (record, iteratorCallback) {
domains.upsertDNSRecords(record.subdomain, record.domain, record.type, record.values, iteratorCallback);
}, function (error, changeIds) {
if (error) debug('addDnsRecords: failed to update : %s. will retry', error);
else debug('addDnsRecords: records %j added with changeIds %j', records, changeIds);
retryCallback(error);
});
});
}, function (error) {
if (error) debug('addDnsRecords: done updating records with error:', error);
else debug('addDnsRecords: done');
callback(error);
});
}
function restore(backupConfig, backupId, version, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof version, 'string');
assert.strictEqual(typeof callback, 'function');
if (!semver.valid(version)) return callback(new CloudronError(CloudronError.BAD_STATE, 'version is not a valid semver'));
if (semver.major(config.version()) !== semver.major(version) || semver.minor(config.version()) !== semver.minor(version)) return callback(new CloudronError(CloudronError.BAD_STATE, `Run cloudron-setup with --version ${version} to restore from this backup`));
user.count(function (error, count) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
if (count) return callback(new CloudronError(CloudronError.ALREADY_PROVISIONED, 'Already activated'));
backups.testConfig(backupConfig, function (error) {
if (error && error.reason === BackupsError.BAD_FIELD) return callback(new CloudronError(CloudronError.BAD_FIELD, error.message));
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new CloudronError(CloudronError.EXTERNAL_ERROR, error.message));
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
debug(`restore: restoring from ${backupId} from provider ${backupConfig.provider}`);
gWebadminStatus.restoring = true;
callback(null); // do no block
async.series([
backups.restore.bind(null, backupConfig, backupId),
autoprovision,
shell.sudo.bind(null, 'restart', [ RESTART_CMD ])
], function (error) {
debug('restore:', error);
gWebadminStatus.restoring = false;
});
});
});
}
function reboot(callback) {
shell.sudo('reboot', [ REBOOT_CMD ], callback);
}
@@ -274,17 +719,21 @@ function doUpdate(boxUpdateInfo, callback) {
// NOTE: this data is opaque and will be passed through the installer.sh
var data= {
provider: config.provider(),
token: config.token(),
apiServerOrigin: config.apiServerOrigin(),
webServerOrigin: config.webServerOrigin(),
adminDomain: config.adminDomain(),
fqdn: config.fqdn(),
adminFqdn: config.adminFqdn(),
adminLocation: config.adminLocation(),
isDemo: config.isDemo(),
zoneName: config.zoneName(),
appstore: {
token: config.token(),
apiServerOrigin: config.apiServerOrigin()
},
caas: {
token: config.token(),
apiServerOrigin: config.apiServerOrigin(),
webServerOrigin: config.webServerOrigin()
},
@@ -294,7 +743,7 @@ function doUpdate(boxUpdateInfo, callback) {
debug('updating box %s %j', boxUpdateInfo.sourceTarballUrl, _.omit(data, 'tlsCert', 'tlsKey', 'token', 'appstore', 'caas'));
progress.set(progress.UPDATE, 5, 'Downloading and installing new version');
progress.set(progress.UPDATE, 5, 'Downloading and extracting new version');
shell.sudo('update', [ UPDATE_CMD, boxUpdateInfo.sourceTarballUrl, JSON.stringify(data) ], function (error) {
if (error) return updateError(error);
@@ -346,6 +795,40 @@ function checkDiskSpace(callback) {
});
}
// called for dynamic dns setups where we have to update the IP
function refreshDNS(callback) {
callback = callback || NOOP_CALLBACK;
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
debug('refreshDNS: current ip %s', ip);
addDnsRecords(ip, function (error) {
if (error) return callback(error);
debug('refreshDNS: done for system records');
apps.getAll(function (error, result) {
if (error) return callback(error);
async.each(result, function (app, callback) {
// do not change state of installing apps since apptask will error if dns record already exists
if (app.installationState !== appdb.ISTATE_INSTALLED) return callback();
domains.upsertDNSRecords(app.location, app.domain, 'A', [ ip ], callback);
}, function (error) {
if (error) return callback(error);
debug('refreshDNS: done for apps');
callback();
});
});
});
});
}
function getLogs(options, callback) {
assert(options && typeof options === 'object');
assert.strictEqual(typeof callback, 'function');
+34 -13
View File
@@ -16,11 +16,12 @@ exports = module.exports = {
provider: provider,
apiServerOrigin: apiServerOrigin,
webServerOrigin: webServerOrigin,
adminDomain: adminDomain,
setFqdn: setAdminDomain,
setAdminDomain: setAdminDomain,
fqdn: fqdn,
zoneName: zoneName,
setFqdn: setFqdn,
setAdminFqdn: setAdminFqdn,
setAdminLocation: setAdminLocation,
token: token,
version: version,
setVersion: setVersion,
database: database,
@@ -33,6 +34,7 @@ exports = module.exports = {
adminFqdn: adminFqdn,
mailLocation: mailLocation,
mailFqdn: mailFqdn,
setZoneName: setZoneName,
hasIPv6: hasIPv6,
dkimSelector: dkimSelector,
@@ -46,6 +48,7 @@ var assert = require('assert'),
fs = require('fs'),
path = require('path'),
safe = require('safetydance'),
tld = require('tldjs'),
_ = require('underscore');
@@ -70,10 +73,12 @@ function saveSync() {
// only save values we want to have in the cloudron.conf, see start.sh
var conf = {
version: data.version,
token: data.token,
apiServerOrigin: data.apiServerOrigin,
webServerOrigin: data.webServerOrigin,
adminDomain: data.adminDomain,
fqdn: data.fqdn,
adminFqdn: data.adminFqdn,
zoneName: data.zoneName,
adminLocation: data.adminLocation,
provider: data.provider,
isDemo: data.isDemo
@@ -92,14 +97,16 @@ function _reset(callback) {
function initConfig() {
// setup defaults
data.fqdn = '';
data.adminFqdn = '';
data.adminDomain = '';
data.zoneName = '';
data.adminLocation = 'my';
data.port = 3000;
data.token = null;
data.version = null;
data.apiServerOrigin = null;
data.webServerOrigin = null;
data.provider = 'generic';
data.provider = 'caas';
data.smtpPort = 2525; // this value comes from mail container
data.sysadminPort = 3001;
data.ldapPort = 3002;
@@ -117,6 +124,7 @@ function initConfig() {
if (exports.TEST) {
data.version = '1.1.1-test';
data.port = 5454;
data.token = 'APPSTORE_TOKEN';
data.apiServerOrigin = 'http://localhost:6060'; // hock doesn't support https
data.database.password = '';
data.database.name = 'boxtest';
@@ -140,7 +148,6 @@ function set(key, value) {
} else {
data = safe.set(data, key, value);
}
saveSync();
}
@@ -158,12 +165,24 @@ function webServerOrigin() {
return get('webServerOrigin');
}
function setAdminDomain(domain) {
set('adminDomain', domain);
function setFqdn(fqdn) {
set('fqdn', fqdn);
}
function adminDomain() {
return get('adminDomain');
function fqdn() {
return get('fqdn');
}
function setZoneName(zone) {
set('zoneName', zone);
}
function zoneName() {
var zone = get('zoneName');
if (zone) return zone;
// TODO: move this to migration code path instead
return tld.getDomain(fqdn()) || '';
}
function mailLocation() {
@@ -203,6 +222,10 @@ function sysadminOrigin() {
return 'http://127.0.0.1:' + get('sysadminPort');
}
function token() {
return get('token');
}
function version() {
return get('version');
}
@@ -228,8 +251,6 @@ function hasIPv6() {
return fs.existsSync(IPV6_PROC_FILE);
}
// it has to change with the adminLocation so that multiple cloudrons
// can send out emails at the same time.
function dkimSelector() {
var loc = adminLocation();
return loc === 'my' ? 'cloudron' : `cloudron-${loc.replace(/\./g, '')}`;
+2 -3
View File
@@ -4,14 +4,13 @@ exports = module.exports = {
API_LOCATION: 'api', // this is unused but reserved for future use (#403)
SMTP_LOCATION: 'smtp',
IMAP_LOCATION: 'imap',
POSTMAN_LOCATION: 'postman', // used in dovecot bounces
// These are combined into one array because users and groups become mailboxes
RESERVED_NAMES: [
// Reserved usernames
// https://github.com/gogits/gogs/blob/52c8f691630548fe091d30bcfe8164545a05d3d5/models/repo.go#L393
// apps like wordpress, gogs don't like these
// postmaster is used in dovecot and haraka
'admin', 'no-reply', 'postmaster', 'mailer-daemon',
'admin', 'no-reply', 'postmaster', 'mailer-daemon', // apps like wordpress, gogs don't like these
// Reserved groups
'admins', 'users' // ldap code uses 'users' pseudo group
+24 -47
View File
@@ -10,24 +10,23 @@ var apps = require('./apps.js'),
assert = require('assert'),
backups = require('./backups.js'),
caas = require('./caas.js'),
certificates = require('./certificates.js'),
cloudron = require('./cloudron.js'),
config = require('./config.js'),
constants = require('./constants.js'),
CronJob = require('cron').CronJob,
debug = require('debug')('box:cron'),
digest = require('./digest.js'),
dyndns = require('./dyndns.js'),
eventlog = require('./eventlog.js'),
janitor = require('./janitor.js'),
reverseProxy = require('./reverseproxy.js'),
scheduler = require('./scheduler.js'),
settings = require('./settings.js'),
semver = require('semver'),
updateChecker = require('./updatechecker.js');
var gJobs = {
alive: null, // send periodic stats
appAutoUpdater: null,
boxAutoUpdater: null,
autoUpdater: null,
appUpdateChecker: null,
backup: null,
boxUpdateChecker: null,
@@ -78,17 +77,15 @@ function initialize(callback) {
});
settings.events.on(settings.TIME_ZONE_KEY, recreateJobs);
settings.events.on(settings.APP_AUTOUPDATE_PATTERN_KEY, appAutoupdatePatternChanged);
settings.events.on(settings.BOX_AUTOUPDATE_PATTERN_KEY, boxAutoupdatePatternChanged);
settings.events.on(settings.DYNAMIC_DNS_KEY, dynamicDnsChanged);
settings.events.on(settings.AUTOUPDATE_PATTERN_KEY, autoupdatePatternChanged);
settings.events.on(settings.DYNAMIC_DNS_KEY, dynamicDNSChanged);
settings.getAll(function (error, allSettings) {
if (error) return callback(error);
recreateJobs(allSettings[settings.TIME_ZONE_KEY]);
appAutoupdatePatternChanged(allSettings[settings.APP_AUTOUPDATE_PATTERN_KEY]);
boxAutoupdatePatternChanged(allSettings[settings.BOX_AUTOUPDATE_PATTERN_KEY]);
dynamicDnsChanged(allSettings[settings.DYNAMIC_DNS_KEY]);
autoupdatePatternChanged(allSettings[settings.AUTOUPDATE_PATTERN_KEY]);
dynamicDNSChanged(allSettings[settings.DYNAMIC_DNS_KEY]);
callback();
});
@@ -177,7 +174,7 @@ function recreateJobs(tz) {
if (gJobs.certificateRenew) gJobs.certificateRenew.stop();
gJobs.certificateRenew = new CronJob({
cronTime: '00 00 */12 * * *', // every 12 hours
onTick: reverseProxy.renewAll.bind(null, AUDIT_SOURCE, NOOP_CALLBACK),
onTick: certificates.renewAll.bind(null, AUDIT_SOURCE, NOOP_CALLBACK),
start: true,
timeZone: tz
});
@@ -191,51 +188,32 @@ function recreateJobs(tz) {
});
}
function boxAutoupdatePatternChanged(pattern) {
function autoupdatePatternChanged(pattern) {
assert.strictEqual(typeof pattern, 'string');
assert(gJobs.boxUpdateCheckerJob);
debug('Box auto update pattern changed to %s', pattern);
debug('Auto update pattern changed to %s', pattern);
if (gJobs.boxAutoUpdater) gJobs.boxAutoUpdater.stop();
if (gJobs.autoUpdater) gJobs.autoUpdater.stop();
if (pattern === constants.AUTOUPDATE_PATTERN_NEVER) return;
gJobs.boxAutoUpdater = new CronJob({
gJobs.autoUpdater = new CronJob({
cronTime: pattern,
onTick: function() {
var updateInfo = updateChecker.getUpdateInfo();
if (updateInfo.box) {
debug('Starting autoupdate to %j', updateInfo.box);
cloudron.updateToLatest(AUDIT_SOURCE, NOOP_CALLBACK);
} else {
debug('No box auto updates available');
}
},
start: true,
timeZone: gJobs.boxUpdateCheckerJob.cronTime.zone // hack
});
}
function appAutoupdatePatternChanged(pattern) {
assert.strictEqual(typeof pattern, 'string');
assert(gJobs.boxUpdateCheckerJob);
debug('Apps auto update pattern changed to %s', pattern);
if (gJobs.appAutoUpdater) gJobs.appAutoUpdater.stop();
if (pattern === constants.AUTOUPDATE_PATTERN_NEVER) return;
gJobs.appAutoUpdater = new CronJob({
cronTime: pattern,
onTick: function() {
var updateInfo = updateChecker.getUpdateInfo();
if (updateInfo.apps) {
if (semver.major(updateInfo.box.version) === semver.major(config.version())) {
debug('Starting autoupdate to %j', updateInfo.box);
cloudron.updateToLatest(AUDIT_SOURCE, NOOP_CALLBACK);
} else {
debug('Block automatic update for major version');
}
} else if (updateInfo.apps) {
debug('Starting app update to %j', updateInfo.apps);
apps.autoupdateApps(updateInfo.apps, AUDIT_SOURCE, NOOP_CALLBACK);
} else {
debug('No app auto updates available');
debug('No auto updates available');
}
},
start: true,
@@ -243,7 +221,7 @@ function appAutoupdatePatternChanged(pattern) {
});
}
function dynamicDnsChanged(enabled) {
function dynamicDNSChanged(enabled) {
assert.strictEqual(typeof enabled, 'boolean');
assert(gJobs.boxUpdateCheckerJob);
@@ -252,7 +230,7 @@ function dynamicDnsChanged(enabled) {
if (enabled) {
gJobs.dynamicDNS = new CronJob({
cronTime: '00 */10 * * * *',
onTick: dyndns.sync,
onTick: cloudron.refreshDNS,
start: true,
timeZone: gJobs.boxUpdateCheckerJob.cronTime.zone // hack
});
@@ -266,9 +244,8 @@ function uninitialize(callback) {
assert.strictEqual(typeof callback, 'function');
settings.events.removeListener(settings.TIME_ZONE_KEY, recreateJobs);
settings.events.removeListener(settings.APP_AUTOUPDATE_PATTERN_KEY, appAutoupdatePatternChanged);
settings.events.removeListener(settings.BOX_AUTOUPDATE_PATTERN_KEY, boxAutoupdatePatternChanged);
settings.events.removeListener(settings.DYNAMIC_DNS_KEY, dynamicDnsChanged);
settings.events.removeListener(settings.AUTOUPDATE_PATTERN_KEY, autoupdatePatternChanged);
settings.events.removeListener(settings.DYNAMIC_DNS_KEY, dynamicDNSChanged);
for (var job in gJobs) {
if (!gJobs[job]) continue;
+1
View File
@@ -104,6 +104,7 @@ function clear(callback) {
async.series([
child_process.exec.bind(null, cmd),
require('./clientdb.js')._addDefaultClients,
require('./domaindb.js')._addDefaultDomain,
require('./groupdb.js')._addDefaultGroups
], callback);
}
+5 -6
View File
@@ -13,7 +13,6 @@ var assert = require('assert'),
constants = require('./constants.js'),
eventlog = require('./eventlog.js'),
tokendb = require('./tokendb.js'),
user = require('./user.js'),
util = require('util');
function DeveloperError(reason, errorOrMessage) {
@@ -38,19 +37,19 @@ util.inherits(DeveloperError, Error);
DeveloperError.INTERNAL_ERROR = 'Internal Error';
DeveloperError.EXTERNAL_ERROR = 'External Error';
function issueDeveloperToken(userObject, ip, callback) {
assert.strictEqual(typeof userObject, 'object');
assert.strictEqual(typeof ip, 'string');
function issueDeveloperToken(user, auditSource, callback) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
var token = tokendb.generateToken();
var expiresAt = Date.now() + constants.DEFAULT_TOKEN_EXPIRATION;
var scopes = '*,' + clients.SCOPE_ROLE_SDK;
tokendb.add(token, userObject.id, 'cid-cli', expiresAt, scopes, function (error) {
tokendb.add(token, user.id, 'cid-cli', expiresAt, scopes, function (error) {
if (error) return callback(new DeveloperError(DeveloperError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'cli', ip: ip }, { userId: userObject.id, user: user.removePrivateFields(userObject) });
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource, { authType: 'cli', userId: user.id, username: user.username });
callback(null, { token: token, expiresAt: new Date(expiresAt).toISOString() });
});
+46
View File
@@ -0,0 +1,46 @@
'use strict';
exports = module.exports = {
resolve: resolve
};
var assert = require('assert'),
child_process = require('child_process'),
debug = require('debug')('box:dig');
function resolve(domain, type, options, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
// dig @server cloudron.io TXT +short
var args = [ ];
if (options.server) args.push('@' + options.server);
if (type === 'PTR') {
args.push('-x', domain);
} else {
args.push(domain, type);
}
args.push('+short');
child_process.execFile('/usr/bin/dig', args, { encoding: 'utf8', killSignal: 'SIGKILL', timeout: options.timeout || 0 }, function (error, stdout, stderr) {
if (error && error.killed) error.code = 'ETIMEDOUT';
if (error || stderr) debug('resolve error (%j): %j %s %s', args, error, stdout, stderr);
if (error) return callback(error);
debug('resolve (%j): %s', args, stdout);
if (!stdout) return callback(); // timeout or no result
var lines = stdout.trim().split('\n');
if (type === 'MX') {
lines = lines.map(function (line) {
var parts = line.split(' ');
return { priority: parts[0], exchange: parts[1] };
});
}
return callback(null, lines);
});
}
+53 -53
View File
@@ -11,7 +11,7 @@ exports = module.exports = {
var assert = require('assert'),
async = require('async'),
debug = require('debug')('box:dns/cloudflare'),
dns = require('../native-dns.js'),
dns = require('dns'),
DomainError = require('../domains.js').DomainError,
superagent = require('superagent'),
util = require('util'),
@@ -46,19 +46,19 @@ function getZoneByName(dnsConfig, zoneName, callback) {
assert.strictEqual(typeof callback, 'function');
superagent.get(CLOUDFLARE_ENDPOINT + '/zones?name=' + zoneName + '&status=active')
.set('X-Auth-Key', dnsConfig.token)
.set('X-Auth-Email', dnsConfig.email)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(error);
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
if (!result.body.result.length) return callback(new DomainError(DomainError.NOT_FOUND, util.format('%s %j', result.statusCode, result.body)));
.set('X-Auth-Key', dnsConfig.token)
.set('X-Auth-Email', dnsConfig.email)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(error);
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
if (!result.body.result.length) return callback(new DomainError(DomainError.NOT_FOUND, util.format('%s %j', result.statusCode, result.body)));
callback(null, result.body.result[0]);
});
callback(null, result.body.result[0]);
});
}
function getDnsRecordsByZoneId(dnsConfig, zoneId, zoneName, subdomain, type, callback) {
function getDNSRecordsByZoneId(dnsConfig, zoneId, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneId, 'string');
assert.strictEqual(typeof zoneName, 'string');
@@ -69,18 +69,18 @@ function getDnsRecordsByZoneId(dnsConfig, zoneId, zoneName, subdomain, type, cal
var fqdn = subdomain === '' ? zoneName : subdomain + '.' + zoneName;
superagent.get(CLOUDFLARE_ENDPOINT + '/zones/' + zoneId + '/dns_records')
.set('X-Auth-Key',dnsConfig.token)
.set('X-Auth-Email',dnsConfig.email)
.query({ type: type, name: fqdn })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(error);
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
.set('X-Auth-Key',dnsConfig.token)
.set('X-Auth-Email',dnsConfig.email)
.query({ type: type, name: fqdn })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(error);
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
var tmp = result.body.result;
var tmp = result.body.result;
return callback(null, tmp);
});
return callback(null, tmp);
});
}
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
@@ -100,7 +100,7 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
var zoneId = result.id;
getDnsRecordsByZoneId(dnsConfig, zoneId, zoneName, subdomain, type, function (error, result) {
getDNSRecordsByZoneId(dnsConfig, zoneId, zoneName, subdomain, type, function (error, result) {
if (error) return callback(error);
var dnsRecords = result;
@@ -126,31 +126,31 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
if (i >= dnsRecords.length) {
superagent.post(CLOUDFLARE_ENDPOINT + '/zones/'+ zoneId + '/dns_records')
.set('X-Auth-Key',dnsConfig.token)
.set('X-Auth-Email',dnsConfig.email)
.send(data)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(error);
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
.set('X-Auth-Key',dnsConfig.token)
.set('X-Auth-Email',dnsConfig.email)
.send(data)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(error);
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
callback(null);
});
callback(null);
});
} else {
superagent.put(CLOUDFLARE_ENDPOINT + '/zones/'+ zoneId + '/dns_records/' + dnsRecords[i].id)
.set('X-Auth-Key',dnsConfig.token)
.set('X-Auth-Email',dnsConfig.email)
.send(data)
.timeout(30 * 1000)
.end(function (error, result) {
.set('X-Auth-Key',dnsConfig.token)
.set('X-Auth-Email',dnsConfig.email)
.send(data)
.timeout(30 * 1000)
.end(function (error, result) {
// increment, as we have consumed the record
++i;
++i;
if (error && !error.response) return callback(error);
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
if (error && !error.response) return callback(error);
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
callback(null);
});
callback(null);
});
}
}, function (error) {
if (error) return callback(error);
@@ -171,7 +171,7 @@ function get(dnsConfig, zoneName, subdomain, type, callback) {
getZoneByName(dnsConfig, zoneName, function(error, result){
if (error) return callback(error);
getDnsRecordsByZoneId(dnsConfig, result.id, zoneName, subdomain, type, function(error, result) {
getDNSRecordsByZoneId(dnsConfig, result.id, zoneName, subdomain, type, function(error, result) {
if (error) return callback(error);
var tmp = result.map(function (record) { return record.content; });
@@ -193,7 +193,7 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
getZoneByName(dnsConfig, zoneName, function(error, result){
if (error) return callback(error);
getDnsRecordsByZoneId(dnsConfig, result.id, zoneName, subdomain, type, function(error, result) {
getDNSRecordsByZoneId(dnsConfig, result.id, zoneName, subdomain, type, function(error, result) {
if (error) return callback(error);
if (result.length === 0) return callback(null);
@@ -206,17 +206,17 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
async.eachSeries(tmp, function (record, callback) {
superagent.del(CLOUDFLARE_ENDPOINT + '/zones/'+ zoneId + '/dns_records/' + record.id)
.set('X-Auth-Key',dnsConfig.token)
.set('X-Auth-Email',dnsConfig.email)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(error);
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
.set('X-Auth-Key',dnsConfig.token)
.set('X-Auth-Email',dnsConfig.email)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(error);
if (result.statusCode !== 204 || result.body.success !== true) return translateRequestError(result, callback);
debug('del: done');
debug('del: done');
callback(null);
});
callback(null);
});
}, function (error) {
if (error) return callback(error);
@@ -243,7 +243,7 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
dns.resolveNs(zoneName, function (error, nameservers) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainError(DomainError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
if (error || !nameservers) return callback(new DomainError(DomainError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
+2 -2
View File
@@ -11,7 +11,7 @@ exports = module.exports = {
var assert = require('assert'),
async = require('async'),
debug = require('debug')('box:dns/digitalocean'),
dns = require('../native-dns.js'),
dns = require('dns'),
DomainError = require('../domains.js').DomainError,
safe = require('safetydance'),
superagent = require('superagent'),
@@ -210,7 +210,7 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
dns.resolveNs(zoneName, function (error, nameservers) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainError(DomainError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
if (error || !nameservers) return callback(new DomainError(DomainError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
+3 -2
View File
@@ -9,8 +9,9 @@ exports = module.exports = {
};
var assert = require('assert'),
config = require('../config.js'),
debug = require('debug')('box:dns/gcdns'),
dns = require('../native-dns.js'),
dns = require('dns'),
DomainError = require('../domains.js').DomainError,
GCDNS = require('@google-cloud/dns'),
util = require('util'),
@@ -172,7 +173,7 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
var credentials = getDnsCredentials(dnsConfig);
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
dns.resolveNs(zoneName, function (error, resolvedNS) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainError(DomainError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
if (error || !resolvedNS) return callback(new DomainError(DomainError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
+2 -2
View File
@@ -10,7 +10,7 @@ exports = module.exports = {
var assert = require('assert'),
debug = require('debug')('box:dns/manual'),
dns = require('../native-dns.js'),
dns = require('dns'),
DomainError = require('../domains.js').DomainError,
util = require('util');
@@ -56,7 +56,7 @@ function verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback) {
assert.strictEqual(typeof callback, 'function');
// Very basic check if the nameservers can be fetched
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
dns.resolveNs(zoneName, function (error, nameservers) {
if (error || !nameservers) return callback(new DomainError(DomainError.BAD_FIELD, 'Unable to get nameservers'));
callback(null, { wildcard: !!dnsConfig.wildcard });
+3 -2
View File
@@ -46,10 +46,11 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
return callback();
}
function waitForDns(domain, zoneName, value, options, callback) {
function waitForDns(domain, zoneName, value, type, options, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof value, 'string');
assert(typeof value === 'string' || util.isRegExp(value));
assert(type === 'A' || type === 'CNAME' || type === 'TXT');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
+4 -3
View File
@@ -13,8 +13,9 @@ exports = module.exports = {
var assert = require('assert'),
AWS = require('aws-sdk'),
config = require('../config.js'),
debug = require('debug')('box:dns/route53'),
dns = require('../native-dns.js'),
dns = require('dns'),
DomainError = require('../domains.js').DomainError,
util = require('util'),
_ = require('underscore');
@@ -192,7 +193,7 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
};
var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
route53.changeResourceRecordSets(params, function(error) {
route53.changeResourceRecordSets(params, function(error, result) {
if (error && error.code === 'AccessDenied') return callback(new DomainError(DomainError.ACCESS_DENIED, error.message));
if (error && error.code === 'InvalidClientTokenId') return callback(new DomainError(DomainError.ACCESS_DENIED, error.message));
if (error && error.message && error.message.indexOf('it was not found') !== -1) {
@@ -233,7 +234,7 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
dns.resolveNs(zoneName, function (error, nameservers) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainError(DomainError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
if (error || !nameservers) return callback(new DomainError(DomainError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
+44 -45
View File
@@ -5,59 +5,53 @@ exports = module.exports = waitForDns;
var assert = require('assert'),
async = require('async'),
debug = require('debug')('box:dns/waitfordns'),
dns = require('../native-dns.js'),
DomainError = require('../domains.js').DomainError;
dig = require('../dig.js'),
dns = require('dns'),
DomainError = require('../domains.js').DomainError,
util = require('util');
function resolveIp(hostname, options, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
// try A record at authoritative server
debug(`resolveIp: Checking if ${hostname} has A record at ${options.server}`);
dns.resolve(hostname, 'A', options, function (error, results) {
if (!error && results.length !== 0) return callback(null, results);
// try CNAME record at authoritative server
debug(`resolveIp: Checking if ${hostname} has CNAME record at ${options.server}`);
dns.resolve(hostname, 'CNAME', options, function (error, results) {
if (error || results.length === 0) return callback(error, results);
// recurse lookup the CNAME record
debug(`resolveIp: Resolving ${hostname}'s CNAME record ${results[0]}`);
dns.resolve(results[0], 'A', { server: '127.0.0.1', timeout: options.timeout }, callback);
});
});
}
function isChangeSynced(domain, value, nameserver, callback) {
function isChangeSynced(domain, value, type, nameserver, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof value, 'string');
assert(util.isRegExp(value));
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof nameserver, 'string');
assert.strictEqual(typeof callback, 'function');
// ns records cannot have cname
dns.resolve(nameserver, 'A', { timeout: 5000 }, function (error, nsIps) {
dns.resolve4(nameserver, function (error, nsIps) {
if (error || !nsIps || nsIps.length === 0) {
debug(`isChangeSynced: cannot resolve NS ${nameserver}`); // it's fine if one or more ns are dead
return callback(null, true);
debug('nameserver %s does not resolve. assuming it stays bad.', nameserver); // it's fine if one or more ns are dead
return callback(true);
}
async.every(nsIps, function (nsIp, iteratorCallback) {
resolveIp(domain, { server: nsIp, timeout: 5000 }, function (error, answer) {
if (error && error.code === 'TIMEOUT') {
debug(`isChangeSynced: NS ${nameserver} (${nsIp}) timed out when resolving ${domain}`);
dig.resolve(domain, type, { server: nsIp, timeout: 5000 }, function (error, answer) {
if (error && error.code === 'ETIMEDOUT') {
debug('nameserver %s (%s) timed out when trying to resolve %s', nameserver, nsIp, domain);
return iteratorCallback(null, true); // should be ok if dns server is down
}
if (error) {
debug(`isChangeSynced: NS ${nameserver} (${nsIp}) errored when resolve ${domain}: ${error}`);
debug('nameserver %s (%s) returned error trying to resolve %s: %s', nameserver, nsIp, domain, error);
return iteratorCallback(null, false);
}
debug(`isChangeSynced: ${domain} was resolved to ${answer} at NS ${nameserver} (${nsIp}). Expecting ${value}`);
if (!answer || answer.length === 0) {
debug('bad answer from nameserver %s (%s) resolving %s (%s)', nameserver, nsIp, domain, type);
return iteratorCallback(null, false);
}
iteratorCallback(null, answer.length === 1 && answer[0] === value);
debug('isChangeSynced: ns: %s (%s), name:%s Actual:%j Expecting:%s', nameserver, nsIp, domain, answer, value);
var match = answer.some(function (a) {
return ((type === 'A' && value.test(a)) ||
(type === 'CNAME' && value.test(a)) ||
(type === 'TXT' && value.test(a)));
});
if (match) return iteratorCallback(null, true); // done!
iteratorCallback(null, false);
});
}, callback);
@@ -65,25 +59,30 @@ function isChangeSynced(domain, value, nameserver, callback) {
}
// check if IP change has propagated to every nameserver
function waitForDns(domain, zoneName, value, options, callback) {
function waitForDns(domain, zoneName, value, type, options, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof value, 'string');
assert(typeof value === 'string' || util.isRegExp(value));
assert(type === 'A' || type === 'CNAME' || type === 'TXT');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
debug('waitForDns: domain %s to be %s in zone %s.', domain, value, zoneName);
if (typeof value === 'string') {
// http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript
value = new RegExp('^' + value.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') + '$');
}
var attempt = 0;
debug('waitForIp: domain %s to be %s in zone %s.', domain, value, zoneName);
var attempt = 1;
async.retry(options, function (retryCallback) {
++attempt;
debug(`waitForDns (try ${attempt}): ${domain} to be ${value} in zone ${zoneName}`);
debug('waitForDNS: %s (zone: %s) attempt %s.', domain, zoneName, attempt++);
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
dns.resolveNs(zoneName, function (error, nameservers) {
if (error || !nameservers) return retryCallback(error || new DomainError(DomainError.EXTERNAL_ERROR, 'Unable to get nameservers'));
async.every(nameservers, isChangeSynced.bind(null, domain, value), function (error, synced) {
debug('waitForDns: %s %s ns: %j', domain, synced ? 'done' : 'not done', nameservers);
async.every(nameservers, isChangeSynced.bind(null, domain, value, type), function (error, synced) {
debug('waitForIp: %s %s ns: %j', domain, synced ? 'done' : 'not done', nameservers);
retryCallback(synced ? null : new DomainError(DomainError.EXTERNAL_ERROR, 'ETRYAGAIN'));
});
@@ -91,7 +90,7 @@ function waitForDns(domain, zoneName, value, options, callback) {
}, function retryDone(error) {
if (error) return callback(error);
debug(`waitForDns: ${domain} has propagated`);
debug('waitForDNS: %s done.', domain);
callback(null);
});
+5 -9
View File
@@ -15,7 +15,6 @@ exports = module.exports = {
createSubcontainer: createSubcontainer,
getContainerIdByIp: getContainerIdByIp,
inspect: inspect,
inspectByName: inspect,
execContainer: execContainer
};
@@ -50,9 +49,10 @@ var addons = require('./addons.js'),
_ = require('underscore');
function debugApp(app, args) {
assert(typeof app === 'object');
assert(!app || typeof app === 'object');
debug(app.fqdn + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
var prefix = app ? app.intrinsicFqdn : '(no app)';
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
}
function pullImage(manifest, callback) {
@@ -129,7 +129,7 @@ function createSubcontainer(app, name, cmd, options, callback) {
var manifest = app.manifest;
var exposedPorts = {}, dockerPortBindings = { };
var domain = app.fqdn;
var domain = app.altDomain || app.intrinsicFqdn;
var stdEnv = [
'CLOUDRON=1',
'WEBADMIN_ORIGIN=' + config.adminOrigin(),
@@ -163,10 +163,6 @@ function createSubcontainer(app, name, cmd, options, callback) {
memoryLimit = constants.DEFAULT_MEMORY_LIMIT;
}
// give scheduler tasks twice the memory limit since background jobs take more memory
// if required, we can make this a manifest and runtime argument later
if (!isAppContainer) memoryLimit *= 2;
// apparmor is disabled on few servers
var enableSecurityOpt = config.CLOUDRON && safe(function () { return child_process.spawnSync('aa-enabled').status === 0; }, false);
@@ -190,7 +186,7 @@ function createSubcontainer(app, name, cmd, options, callback) {
'/run': {}
},
Labels: {
'fqdn': app.fqdn,
'fqdn': app.intrinsicFqdn,
'appId': app.id,
'isSubcontainer': String(!isAppContainer)
},
+40 -34
View File
@@ -7,23 +7,22 @@ exports = module.exports = {
get: get,
getAll: getAll,
update: update,
upsert: upsert,
del: del,
_clear: clear
_clear: clear,
_addDefaultDomain: addDefaultDomain
};
var assert = require('assert'),
database = require('./database.js'),
DatabaseError = require('./databaseerror'),
config = require('./config.js'),
safe = require('safetydance');
var DOMAINS_FIELDS = [ 'domain', 'zoneName', 'provider', 'configJson', 'tlsConfigJson' ].join(',');
function postProcess(data) {
data.config = safe.JSON.parse(data.configJson);
data.tlsConfig = safe.JSON.parse(data.tlsConfigJson);
delete data.configJson;
delete data.tlsConfigJson;
return data;
}
@@ -32,7 +31,7 @@ function get(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
database.query(`SELECT ${DOMAINS_FIELDS} FROM domains WHERE domain=?`, [ domain ], function (error, result) {
database.query('SELECT * FROM domains WHERE domain=?', [ domain ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
@@ -43,7 +42,7 @@ function get(domain, callback) {
}
function getAll(callback) {
database.query(`SELECT ${DOMAINS_FIELDS} FROM domains ORDER BY domain`, function (error, results) {
database.query('SELECT * FROM domains ORDER BY domain', function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
results.forEach(postProcess);
@@ -52,16 +51,14 @@ function getAll(callback) {
});
}
function add(name, domain, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'object');
assert.strictEqual(typeof domain.zoneName, 'string');
assert.strictEqual(typeof domain.provider, 'string');
assert.strictEqual(typeof domain.config, 'object');
assert.strictEqual(typeof domain.tlsConfig, 'object');
function add(domain, zoneName, provider, config, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof provider, 'string');
assert.strictEqual(typeof config, 'object');
assert.strictEqual(typeof callback, 'function');
database.query('INSERT INTO domains (domain, zoneName, provider, configJson, tlsConfigJson) VALUES (?, ?, ?, ?, ?)', [ name, domain.zoneName, domain.provider, JSON.stringify(domain.config), JSON.stringify(domain.tlsConfig) ], function (error) {
database.query('INSERT INTO domains (domain, zoneName, provider, configJson) VALUES (?, ?, ?, ?)', [ domain, zoneName, provider, JSON.stringify(config) ], function (error) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
@@ -69,27 +66,27 @@ function add(name, domain, callback) {
});
}
function update(name, domain, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'object');
function upsert(domain, zoneName, provider, config, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof provider, 'string');
assert.strictEqual(typeof config, 'object');
assert.strictEqual(typeof callback, 'function');
var args = [ ], fields = [ ];
for (var k in domain) {
if (k === 'config') {
fields.push('configJson = ?');
args.push(JSON.stringify(domain[k]));
} else if (k === 'tlsConfig') {
fields.push('tlsConfigJson = ?');
args.push(JSON.stringify(domain[k]));
} else {
fields.push(k + ' = ?');
args.push(domain[k]);
}
}
args.push(name);
database.query('REPLACE INTO domains (domain, zoneName, provider, configJson) VALUES (?, ?, ?, ?)', [ domain, zoneName, provider, JSON.stringify(config) ], function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
database.query('UPDATE domains SET ' + fields.join(', ') + ' WHERE domain=?', args, function (error) {
callback(null);
});
}
function update(domain, provider, config, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof provider, 'string');
assert.strictEqual(typeof config, 'object');
assert.strictEqual(typeof callback, 'function');
database.query('UPDATE domains SET provider=?, configJson=? WHERE domain=?', [ provider, JSON.stringify(config), domain ], function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
@@ -104,7 +101,7 @@ function del(domain, callback) {
database.query('DELETE FROM domains WHERE domain=?', [ domain ], function (error, result) {
if (error && error.code === 'ER_ROW_IS_REFERENCED_2') return callback(new DatabaseError(DatabaseError.IN_USE));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
callback(null);
});
@@ -117,3 +114,12 @@ function clear(callback) {
callback(error);
});
}
function addDefaultDomain(callback) {
assert(config.fqdn(), 'no fqdn set in config, cannot continue');
add(config.fqdn(), config.zoneName(), 'manual', { }, function (error) {
if (error && error.reason !== DatabaseError.ALREADY_EXISTS) return callback(error);
callback();
});
}
+40 -64
View File
@@ -10,11 +10,11 @@ module.exports = exports = {
fqdn: fqdn,
setAdmin: setAdmin,
getDnsRecords: getDnsRecords,
upsertDnsRecords: upsertDnsRecords,
removeDnsRecords: removeDnsRecords,
getDNSRecords: getDNSRecords,
upsertDNSRecords: upsertDNSRecords,
removeDNSRecords: removeDNSRecords,
waitForDnsRecord: waitForDnsRecord,
waitForDNSRecord: waitForDNSRecord,
DomainError: DomainError
};
@@ -22,13 +22,12 @@ module.exports = exports = {
var assert = require('assert'),
caas = require('./caas.js'),
config = require('./config.js'),
certificates = require('./certificates.js'),
CertificatesError = certificates.CertificatesError,
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:domains'),
domaindb = require('./domaindb.js'),
path = require('path'),
reverseProxy = require('./reverseproxy.js'),
ReverseProxyError = reverseProxy.ReverseProxyError,
safe = require('safetydance'),
shell = require('./shell.js'),
sysinfo = require('./sysinfo.js'),
tld = require('tldjs'),
@@ -83,6 +82,7 @@ function api(provider) {
}
}
// TODO make it return a DomainError instead of DomainError
function verifyDnsConfig(config, domain, zoneName, provider, ip, callback) {
assert(config && typeof config === 'object'); // the dns config to test with
assert.strictEqual(typeof domain, 'string');
@@ -98,32 +98,22 @@ function verifyDnsConfig(config, domain, zoneName, provider, ip, callback) {
}
function add(domain, zoneName, provider, config, fallbackCertificate, tlsConfig, callback) {
function add(domain, zoneName, provider, config, fallbackCertificate, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof provider, 'string');
assert.strictEqual(typeof config, 'object');
assert.strictEqual(typeof fallbackCertificate, 'object');
assert.strictEqual(typeof tlsConfig, 'object');
assert.strictEqual(typeof callback, 'function');
if (!tld.isValid(domain)) return callback(new DomainError(DomainError.BAD_FIELD, 'Invalid domain'));
if (zoneName) {
if (!tld.isValid(zoneName)) return callback(new DomainError(DomainError.BAD_FIELD, 'Invalid zoneName'));
} else {
zoneName = tld.getDomain(domain) || domain;
}
if (!tld.isValid(zoneName)) return callback(new DomainError(DomainError.BAD_FIELD, 'Invalid zoneName'));
if (fallbackCertificate) {
let error = reverseProxy.validateCertificate(`test.${domain}`, fallbackCertificate.cert, fallbackCertificate.key);
let error = certificates.validateCertificate(fallbackCertificate.cert, fallbackCertificate.key, domain);
if (error) return callback(new DomainError(DomainError.BAD_FIELD, error.message));
}
if (tlsConfig.provider !== 'fallback' && tlsConfig.provider !== 'caas' && tlsConfig.provider.indexOf('letsencrypt-') !== 0) {
return callback(new DomainError(DomainError.BAD_FIELD, 'tlsConfig.provider must be caas, fallback or le-*'));
}
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, 'Error getting IP:' + error.message));
@@ -135,13 +125,15 @@ function add(domain, zoneName, provider, config, fallbackCertificate, tlsConfig,
if (error && error.reason === DomainError.INVALID_PROVIDER) return callback(new DomainError(DomainError.BAD_FIELD, error.message));
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
domaindb.add(domain, { zoneName: zoneName, provider: provider, config: result, tlsConfig: tlsConfig }, function (error) {
domaindb.add(domain, zoneName, provider, result, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new DomainError(DomainError.ALREADY_EXISTS));
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
reverseProxy.setFallbackCertificate(domain, fallbackCertificate, function (error) {
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
if (!fallbackCertificate) return callback();
// cert validation already happened above no need to check all errors again
certificates.setFallbackCertificate(fallbackCertificate.cert, fallbackCertificate.key, domain, function (error) {
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
callback();
});
});
@@ -158,15 +150,10 @@ function get(domain, callback) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DomainError(DomainError.NOT_FOUND));
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
reverseProxy.getFallbackCertificate(domain, function (error, bundle) {
if (error && error.reason !== ReverseProxyError.NOT_FOUND) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
certificates.getFallbackCertificate(domain, function (error, fallbackCertificate) {
if (error && error.reason !== CertificatesError.NOT_FOUND) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
var cert = safe.fs.readFileSync(bundle.certFilePath, 'utf-8');
var key = safe.fs.readFileSync(bundle.keyFilePath, 'utf-8');
if (!cert || !key) return callback(new DomainError(DomainError.INTERNAL_ERROR, 'unable to read certificates from disk'));
result.fallbackCertificate = { cert: cert, key: key };
if (fallbackCertificate) result.fallbackCertificate = fallbackCertificate;
return callback(null, result);
});
@@ -183,12 +170,11 @@ function getAll(callback) {
});
}
function update(domain, provider, config, fallbackCertificate, tlsConfig, callback) {
function update(domain, provider, config, fallbackCertificate, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof provider, 'string');
assert.strictEqual(typeof config, 'object');
assert.strictEqual(typeof fallbackCertificate, 'object');
assert.strictEqual(typeof tlsConfig, 'object');
assert.strictEqual(typeof callback, 'function');
domaindb.get(domain, function (error, result) {
@@ -196,14 +182,10 @@ function update(domain, provider, config, fallbackCertificate, tlsConfig, callba
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
if (fallbackCertificate) {
let error = reverseProxy.validateCertificate(`test.${domain}`, fallbackCertificate.cert, fallbackCertificate.key);
let error = certificates.validateCertificate(fallbackCertificate.cert, fallbackCertificate.key, domain);
if (error) return callback(new DomainError(DomainError.BAD_FIELD, error.message));
}
if (tlsConfig.provider !== 'fallback' && tlsConfig.provider !== 'caas' && tlsConfig.provider.indexOf('letsencrypt-') !== 0) {
return callback(new DomainError(DomainError.BAD_FIELD, 'tlsConfig.provider must be caas, fallback or letsencrypt-*'));
}
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, 'Error getting IP:' + error.message));
@@ -215,15 +197,15 @@ function update(domain, provider, config, fallbackCertificate, tlsConfig, callba
if (error && error.reason === DomainError.INVALID_PROVIDER) return callback(new DomainError(DomainError.BAD_FIELD, error.message));
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
domaindb.update(domain, { provider: provider, config: result, tlsConfig: tlsConfig }, function (error) {
domaindb.update(domain, provider, result, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DomainError(DomainError.NOT_FOUND));
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
if (!fallbackCertificate) return callback();
reverseProxy.setFallbackCertificate(domain, fallbackCertificate, function (error) {
// cert validation already happened above no need to check all errors again
certificates.setFallbackCertificate(fallbackCertificate.cert, fallbackCertificate.key, domain, function (error) {
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
callback();
});
});
@@ -245,18 +227,7 @@ function del(domain, callback) {
});
}
function getName(domain, subdomain) {
// support special caas domains
if (domain.provider === 'caas') return subdomain;
if (domain.domain === domain.zoneName) return subdomain;
var part = domain.domain.slice(0, -domain.zoneName.length - 1);
return subdomain === '' ? part : subdomain + '.' + part;
}
function getDnsRecords(subdomain, domain, type, callback) {
function getDNSRecords(subdomain, domain, type, callback) {
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof type, 'string');
@@ -265,7 +236,7 @@ function getDnsRecords(subdomain, domain, type, callback) {
get(domain, function (error, result) {
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
api(result.provider).get(result.config, result.zoneName, getName(result, subdomain), type, function (error, values) {
api(result.provider).get(result.config, result.zoneName, subdomain, type, function (error, values) {
if (error) return callback(error);
callback(null, values);
@@ -273,7 +244,7 @@ function getDnsRecords(subdomain, domain, type, callback) {
});
}
function upsertDnsRecords(subdomain, domain, type, values, callback) {
function upsertDNSRecords(subdomain, domain, type, values, callback) {
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof type, 'string');
@@ -285,7 +256,7 @@ function upsertDnsRecords(subdomain, domain, type, values, callback) {
get(domain, function (error, result) {
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
api(result.provider).upsert(result.config, result.zoneName, getName(result, subdomain), type, values, function (error, changeId) {
api(result.provider).upsert(result.config, result.zoneName, subdomain, type, values, function (error, changeId) {
if (error) return callback(error);
callback(null, changeId);
@@ -293,7 +264,7 @@ function upsertDnsRecords(subdomain, domain, type, values, callback) {
});
}
function removeDnsRecords(subdomain, domain, type, values, callback) {
function removeDNSRecords(subdomain, domain, type, values, callback) {
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof type, 'string');
@@ -305,7 +276,7 @@ function removeDnsRecords(subdomain, domain, type, values, callback) {
get(domain, function (error, result) {
if (error) return callback(error);
api(result.provider).del(result.config, result.zoneName, getName(result, subdomain), type, values, function (error) {
api(result.provider).del(result.config, result.zoneName, subdomain, type, values, function (error) {
if (error && error.reason !== DomainError.NOT_FOUND) return callback(error);
callback(null);
@@ -313,18 +284,22 @@ function removeDnsRecords(subdomain, domain, type, values, callback) {
});
}
// only wait for A record
function waitForDnsRecord(fqdn, domain, value, options, callback) {
function waitForDNSRecord(fqdn, domain, value, type, options, callback) {
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof value, 'string');
assert(typeof value === 'string' || util.isRegExp(value));
assert(type === 'A' || type === 'CNAME' || type === 'TXT');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
get(domain, function (error, result) {
if (error) return callback(error);
// domain can be not found when waiting for altDomain. When we migrate altDomain, this can never happen
if (error && error.reason !== DomainError.NOT_FOUND) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
api(result.provider).waitForDns(fqdn, result ? result.zoneName : domain, value, options, callback);
// hack for lack of provider with altDomain. When we migrate altDomain, this will be automatically "manual"
const provider = result ? result.provider : 'manual';
api(provider).waitForDns(fqdn, result ? result.zoneName : domain, value, type, options, callback);
});
}
@@ -342,9 +317,10 @@ function setAdmin(domain, callback) {
setPtrRecord(domain, function (error) {
if (error) return callback(new DomainError(DomainError.EXTERNAL_ERROR, 'Error setting PTR record:' + error.message));
config.setAdminDomain(result.domain);
config.setFqdn(result.domain);
config.setAdminLocation('my');
config.setAdminFqdn('my' + (result.provider === 'caas' ? '-' : '.') + result.domain);
config.setZoneName(result.zoneName);
callback();
-49
View File
@@ -1,49 +0,0 @@
'use strict';
exports = module.exports = {
sync: sync
};
var appdb = require('./appdb.js'),
apps = require('./apps.js'),
async = require('async'),
config = require('./config.js'),
debug = require('debug')('box:dyndns'),
domains = require('./domains.js'),
sysinfo = require('./sysinfo.js');
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
// called for dynamic dns setups where we have to update the IP
function sync(callback) {
callback = callback || NOOP_CALLBACK;
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error);
debug('refreshDNS: current ip %s', ip);
domains.upsertDnsRecords(config.adminLocation(), config.adminDomain(), 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('refreshDNS: done for admin location');
apps.getAll(function (error, result) {
if (error) return callback(error);
async.each(result, function (app, callback) {
// do not change state of installing apps since apptask will error if dns record already exists
if (app.installationState !== appdb.ISTATE_INSTALLED) return callback();
domains.upsertDnsRecords(app.location, app.domain, 'A', [ ip ], callback);
}, function (error) {
if (error) return callback(error);
debug('refreshDNS: done for apps');
callback();
});
});
});
});
}
+398
View File
@@ -0,0 +1,398 @@
'use strict';
exports = module.exports = {
verifyRelay: verifyRelay,
getStatus: getStatus,
checkRblStatus: checkRblStatus,
EmailError: EmailError
};
var assert = require('assert'),
async = require('async'),
cloudron = require('./cloudron.js'),
config = require('./config.js'),
debug = require('debug')('box:email'),
dig = require('./dig.js'),
net = require('net'),
nodemailer = require('nodemailer'),
safe = require('safetydance'),
settings = require('./settings.js'),
smtpTransport = require('nodemailer-smtp-transport'),
sysinfo = require('./sysinfo.js'),
util = require('util'),
_ = require('underscore');
const digOptions = { server: '127.0.0.1', port: 53, timeout: 5000 };
function EmailError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
Error.call(this);
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.reason = reason;
if (typeof errorOrMessage === 'undefined') {
this.message = reason;
} else if (typeof errorOrMessage === 'string') {
this.message = errorOrMessage;
} else {
this.message = 'Internal error';
this.nestedError = errorOrMessage;
}
}
util.inherits(EmailError, Error);
EmailError.INTERNAL_ERROR = 'Internal Error';
EmailError.BAD_FIELD = 'Bad Field';
function checkOutboundPort25(callback) {
assert.strictEqual(typeof callback, 'function');
var smtpServer = _.sample([
'smtp.gmail.com',
'smtp.live.com',
'smtp.mail.yahoo.com',
'smtp.o2.ie',
'smtp.comcast.net',
'outgoing.verizon.net'
]);
var relay = {
value: 'OK',
status: false
};
var client = new net.Socket();
client.setTimeout(5000);
client.connect(25, smtpServer);
client.on('connect', function () {
relay.status = true;
relay.value = 'OK';
client.destroy(); // do not use end() because it still triggers timeout
callback(null, relay);
});
client.on('timeout', function () {
relay.status = false;
relay.value = 'Connect to ' + smtpServer + ' timed out';
client.destroy();
callback(new Error('Timeout'), relay);
});
client.on('error', function (error) {
relay.status = false;
relay.value = 'Connect to ' + smtpServer + ' failed: ' + error.message;
client.destroy();
callback(error, relay);
});
}
function checkSmtpRelay(relay, callback) {
var result = {
value: 'OK',
status: false
};
var transporter = nodemailer.createTransport(smtpTransport({
host: relay.host,
port: relay.port,
auth: {
user: relay.username,
pass: relay.password
}
}));
transporter.verify(function(error) {
result.status = !error;
if (error) {
result.value = error.message;
return callback(error, result);
}
callback(null, result);
});
}
function verifyRelay(relay, callback) {
assert.strictEqual(typeof relay, 'object');
assert.strictEqual(typeof callback, 'function');
var verifier = relay.provider === 'cloudron-smtp' ? checkOutboundPort25 : checkSmtpRelay.bind(null, relay);
verifier(function (error) {
if (error) return callback(new EmailError(EmailError.BAD_FIELD, error.message));
callback();
});
}
function checkDkim(callback) {
var dkim = {
domain: config.dkimSelector() + '._domainkey.' + config.fqdn(),
type: 'TXT',
expected: null,
value: null,
status: false
};
var dkimKey = cloudron.readDkimPublicKeySync();
if (!dkimKey) return callback(new Error('Failed to read dkim public key'), dkim);
dkim.expected = '"v=DKIM1; t=s; p=' + dkimKey + '"';
dig.resolve(dkim.domain, dkim.type, digOptions, function (error, txtRecords) {
if (error && error.code === 'ENOTFOUND') return callback(null, dkim); // not setup
if (error) return callback(error, dkim);
if (Array.isArray(txtRecords) && txtRecords.length !== 0) {
dkim.value = txtRecords[0];
dkim.status = (dkim.value === dkim.expected);
}
callback(null, dkim);
});
}
function checkSpf(callback) {
var spf = {
domain: config.fqdn(),
type: 'TXT',
value: null,
expected: '"v=spf1 a:' + config.adminFqdn() + ' ~all"',
status: false
};
// https://agari.zendesk.com/hc/en-us/articles/202952749-How-long-can-my-SPF-record-be-
dig.resolve(spf.domain, spf.type, digOptions, function (error, txtRecords) {
if (error && error.code === 'ENOTFOUND') return callback(null, spf); // not setup
if (error) return callback(error, spf);
if (!Array.isArray(txtRecords)) return callback(null, spf);
var i;
for (i = 0; i < txtRecords.length; i++) {
if (txtRecords[i].indexOf('"v=spf1 ') !== 0) continue; // not SPF
spf.value = txtRecords[i];
spf.status = spf.value.indexOf(' a:' + config.adminFqdn()) !== -1;
break;
}
if (spf.status) {
spf.expected = spf.value;
} else if (i !== txtRecords.length) {
spf.expected = '"v=spf1 a:' + config.adminFqdn() + ' ' + spf.value.slice('"v=spf1 '.length);
}
callback(null, spf);
});
}
function checkMx(callback) {
var mx = {
domain: config.fqdn(),
type: 'MX',
value: null,
expected: '10 ' + config.mailFqdn() + '.',
status: false
};
dig.resolve(mx.domain, mx.type, digOptions, function (error, mxRecords) {
if (error && error.code === 'ENOTFOUND') return callback(null, mx); // not setup
if (error) return callback(error, mx);
if (Array.isArray(mxRecords) && mxRecords.length !== 0) {
mx.status = mxRecords.length == 1 && mxRecords[0].exchange === (config.mailFqdn() + '.');
mx.value = mxRecords.map(function (r) { return r.priority + ' ' + r.exchange; }).join(' ');
}
callback(null, mx);
});
}
function checkDmarc(callback) {
var dmarc = {
domain: '_dmarc.' + config.fqdn(),
type: 'TXT',
value: null,
expected: '"v=DMARC1; p=reject; pct=100"',
status: false
};
dig.resolve(dmarc.domain, dmarc.type, digOptions, function (error, txtRecords) {
if (error && error.code === 'ENOTFOUND') return callback(null, dmarc); // not setup
if (error) return callback(error, dmarc);
if (Array.isArray(txtRecords) && txtRecords.length !== 0) {
dmarc.value = txtRecords[0];
dmarc.status = (dmarc.value === dmarc.expected);
}
callback(null, dmarc);
});
}
function checkPtr(callback) {
var ptr = {
domain: null,
type: 'PTR',
value: null,
expected: config.mailFqdn() + '.',
status: false
};
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error, ptr);
ptr.domain = ip.split('.').reverse().join('.') + '.in-addr.arpa';
dig.resolve(ip, 'PTR', digOptions, function (error, ptrRecords) {
if (error && error.code === 'ENOTFOUND') return callback(null, ptr); // not setup
if (error) return callback(error, ptr);
if (Array.isArray(ptrRecords) && ptrRecords.length !== 0) {
ptr.value = ptrRecords.join(' ');
ptr.status = ptrRecords.some(function (v) { return v === ptr.expected; });
}
return callback(null, ptr);
});
});
}
// https://raw.githubusercontent.com/jawsome/node-dnsbl/master/list.json
const RBL_LIST = [
{
'name': 'Barracuda',
'dns': 'b.barracudacentral.org',
'site': 'http://www.barracudacentral.org/rbl/removal-request'
},
{
'name': 'SpamCop',
'dns': 'bl.spamcop.net',
'site': 'http://spamcop.net'
},
{
'name': 'Sorbs Aggregate Zone',
'dns': 'dnsbl.sorbs.net',
'site': 'http://dnsbl.sorbs.net/'
},
{
'name': 'Sorbs spam.dnsbl Zone',
'dns': 'spam.dnsbl.sorbs.net',
'site': 'http://sorbs.net'
},
{
'name': 'Composite Blocking List',
'dns': 'cbl.abuseat.org',
'site': 'http://www.abuseat.org'
},
{
'name': 'SpamHaus Zen',
'dns': 'zen.spamhaus.org',
'site': 'http://spamhaus.org'
},
{
'name': 'Multi SURBL',
'dns': 'multi.surbl.org',
'site': 'http://www.surbl.org'
},
{
'name': 'Spam Cannibal',
'dns': 'bl.spamcannibal.org',
'site': 'http://www.spamcannibal.org/cannibal.cgi'
},
{
'name': 'dnsbl.abuse.ch',
'dns': 'spam.abuse.ch',
'site': 'http://dnsbl.abuse.ch/'
},
{
'name': 'The Unsubscribe Blacklist(UBL)',
'dns': 'ubl.unsubscore.com ',
'site': 'http://www.lashback.com/blacklist/'
},
{
'name': 'UCEPROTECT Network',
'dns': 'dnsbl-1.uceprotect.net',
'site': 'http://www.uceprotect.net/en'
}
];
function checkRblStatus(callback) {
assert.strictEqual(typeof callback, 'function');
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error, ip);
var flippedIp = ip.split('.').reverse().join('.');
// https://tools.ietf.org/html/rfc5782
async.map(RBL_LIST, function (rblServer, iteratorDone) {
dig.resolve(flippedIp + '.' + rblServer.dns, 'A', digOptions, function (error, records) {
if (error || !records) return iteratorDone(null, null); // not listed
debug('checkRblStatus: %s (ip: %s) is in the blacklist of %j', config.fqdn(), flippedIp, rblServer);
var result = _.extend({ }, rblServer);
dig.resolve(flippedIp + '.' + rblServer.dns, 'TXT', digOptions, function (error, txtRecords) {
result.txtRecords = error || !txtRecords ? 'No txt record' : txtRecords;
debug('checkRblStatus: %s (error: %s) (txtRecords: %j)', config.fqdn(), error, txtRecords);
return iteratorDone(null, result);
});
});
}, function (ignoredError, blacklistedServers) {
blacklistedServers = blacklistedServers.filter(function(b) { return b !== null; });
debug('checkRblStatus: %s (ip: %s) servers: %j', config.fqdn(), ip, blacklistedServers);
return callback(null, { status: blacklistedServers.length === 0, ip: ip, servers: blacklistedServers });
});
});
}
function getStatus(callback) {
assert.strictEqual(typeof callback, 'function');
var results = {};
function recordResult(what, func) {
return function (callback) {
func(function (error, result) {
if (error) debug('Ignored error - ' + what + ':', error);
safe.set(results, what, result);
callback();
});
};
}
settings.getMailRelay(function (error, relay) {
if (error) return callback(error);
var checks = [
recordResult('dns.mx', checkMx),
recordResult('dns.dmarc', checkDmarc)
];
if (relay.provider === 'cloudron-smtp') {
// these tests currently only make sense when using Cloudron's SMTP server at this point
checks.push(
recordResult('dns.spf', checkSpf),
recordResult('dns.dkim', checkDkim),
recordResult('dns.ptr', checkPtr),
recordResult('relay', checkOutboundPort25),
recordResult('rbl', checkRblStatus)
);
} else {
checks.push(recordResult('relay', checkSmtpRelay.bind(null, relay)));
}
async.parallel(checks, function () {
callback(null, results);
});
});
}
+4 -3
View File
@@ -22,6 +22,7 @@ exports = module.exports = {
ACTION_BACKUP_START: 'backup.start',
ACTION_BACKUP_CLEANUP: 'backup.cleanup',
ACTION_CERTIFICATE_RENEWAL: 'certificate.renew',
ACTION_CLI_MODE: 'settings.climode',
ACTION_START: 'cloudron.start',
ACTION_UPDATE: 'cloudron.update',
ACTION_USER_ADD: 'user.add',
@@ -90,14 +91,14 @@ function get(id, callback) {
});
}
function getAllPaged(actions, search, page, perPage, callback) {
assert(Array.isArray(actions));
function getAllPaged(action, search, page, perPage, callback) {
assert(typeof action === 'string' || action === null);
assert(typeof search === 'string' || search === null);
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function');
eventlogdb.getAllPaged(actions, search, page, perPage, function (error, events) {
eventlogdb.getAllPaged(action, search, page, perPage, function (error, events) {
if (error) return callback(new EventLogError(EventLogError.INTERNAL_ERROR, error));
callback(null, events);
+8 -9
View File
@@ -40,8 +40,8 @@ function get(eventId, callback) {
});
}
function getAllPaged(actions, search, page, perPage, callback) {
assert(Array.isArray(actions));
function getAllPaged(action, search, page, perPage, callback) {
assert(typeof action === 'string' || action === null);
assert(typeof search === 'string' || search === null);
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
@@ -50,15 +50,14 @@ function getAllPaged(actions, search, page, perPage, callback) {
var data = [];
var query = 'SELECT ' + EVENTLOGS_FIELDS + ' FROM eventlog';
if (actions.length || search) query += ' WHERE';
if (action || search) query += ' WHERE';
if (search) query += ' (source LIKE ' + mysql.escape('%' + search + '%') + ' OR data LIKE ' + mysql.escape('%' + search + '%') + ')';
if (action && search) query += ' AND ';
if (actions.length && search) query += ' AND ( ';
actions.forEach(function (action, i) {
query += ' (action LIKE ' + mysql.escape(`%${action}%`) + ') ';
if (i < actions.length-1) query += ' OR ';
});
if (actions.length && search) query += ' ) ';
if (action) {
query += ' action=?';
data.push(action);
}
query += ' ORDER BY creationTime DESC LIMIT ?,?';
+10 -3
View File
@@ -24,8 +24,10 @@ exports = module.exports = {
var assert = require('assert'),
constants = require('./constants.js'),
config = require('./config.js'),
database = require('./database.js'),
DatabaseError = require('./databaseerror');
DatabaseError = require('./databaseerror'),
mailboxdb = require('./mailboxdb.js');
var GROUPS_FIELDS = [ 'id', 'name' ].join(',');
@@ -87,9 +89,13 @@ function add(id, name, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('INSERT INTO groups (id, name) VALUES (?, ?)', [ id, name ], function (error, result) {
var queries = [];
queries.push({ query: 'INSERT INTO mailboxes (name, domain, ownerId, ownerType) VALUES (?, ?, ?, ?)', args: [ name, config.fqdn(), id, mailboxdb.TYPE_GROUP ] });
queries.push({ query: 'INSERT INTO groups (id, name) VALUES (?, ?)', args: [ id, name ] });
database.transaction(queries, function (error, result) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error));
if (error || result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error || result[1].affectedRows !== 1) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null);
});
@@ -103,6 +109,7 @@ function del(id, callback) {
var queries = [];
queries.push({ query: 'DELETE FROM groupMembers WHERE groupId = ?', args: [ id ] });
queries.push({ query: 'DELETE FROM groups WHERE id = ?', args: [ id ] });
queries.push({ query: 'DELETE FROM mailboxes WHERE ownerId=?', args: [ id ] });
database.transaction(queries, function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
+3 -8
View File
@@ -24,7 +24,6 @@ var assert = require('assert'),
constants = require('./constants.js'),
DatabaseError = require('./databaseerror.js'),
groupdb = require('./groupdb.js'),
mailboxdb = require('./mailboxdb.js'),
util = require('util'),
uuid = require('uuid');
@@ -100,15 +99,11 @@ function remove(id, callback) {
// never allow admin group to be deleted
if (id === constants.ADMIN_GROUP_ID) return callback(new GroupError(GroupError.NOT_ALLOWED));
mailboxdb.delByOwnerId(id, function (error) {
groupdb.del(id, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupError(GroupError.NOT_FOUND));
if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, error));
groupdb.del(id, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupError(GroupError.NOT_FOUND));
if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, error));
callback(null);
});
callback(null);
});
}
+7 -7
View File
@@ -7,18 +7,18 @@
exports = module.exports = {
// a major version makes all apps restore from backup. #451 must be fixed before we do this.
// a minor version makes all apps re-configure themselves
'version': '48.9.0',
'version': '48.8.0',
'baseImages': [ 'cloudron/base:0.10.0' ],
// Note that if any of the databases include an upgrade, bump the infra version above
// This is because we upgrade using dumps instead of mysql_upgrade, pg_upgrade etc
'images': {
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:1.0.0' },
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:1.0.0' },
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:1.0.1' },
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:1.0.0' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:1.2.0' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:1.0.0' }
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:0.18.0' },
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:0.17.1' },
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:0.13.0' },
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:0.11.0' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:0.40.0' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:0.12.0' }
}
};
+48 -65
View File
@@ -16,8 +16,6 @@ var assert = require('assert'),
user = require('./user.js'),
UserError = user.UserError,
ldap = require('ldapjs'),
mail = require('./mail.js'),
MailError = mail.MailError,
mailboxdb = require('./mailboxdb.js'),
safe = require('safetydance');
@@ -154,7 +152,7 @@ function userSearch(req, res, next) {
cn: entry.id,
uid: entry.id,
mail: entry.email,
mailAlternateAddress: entry.fallbackEmail,
mailAlternateAddress: entry.alternateEmail,
displayname: displayName,
givenName: firstName,
username: entry.username,
@@ -260,11 +258,14 @@ function mailboxSearch(req, res, next) {
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()));
var name = req.dn.rdns[0].attrs.cn.value.toLowerCase();
// allow login via email
var parts = name.split('@');
if (parts[1] === config.fqdn()) {
name = parts[0];
}
mailboxdb.getMailbox(parts[0], parts[1], function (error, mailbox) {
mailboxdb.getMailbox(name, config.fqdn(), 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.toString()));
@@ -273,9 +274,9 @@ function mailboxSearch(req, res, next) {
attributes: {
objectclass: ['mailbox'],
objectcategory: 'mailbox',
cn: `${mailbox.name}@${mailbox.domain}`,
uid: `${mailbox.name}@${mailbox.domain}`,
mail: `${mailbox.name}@${mailbox.domain}`,
cn: mailbox.name,
uid: mailbox.name,
mail: mailbox.name + '@' + config.fqdn(),
ownerType: mailbox.ownerType
}
};
@@ -297,24 +298,19 @@ function mailAliasSearch(req, res, next) {
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()));
mailboxdb.getAlias(parts[0], parts[1], function (error, alias) {
mailboxdb.getAlias(req.dn.rdns[0].attrs.cn.value.toLowerCase(), config.fqdn(), function (error, alias) {
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.toString()));
// https://wiki.debian.org/LDAP/MigrationTools/Examples
// https://docs.oracle.com/cd/E19455-01/806-5580/6jej518pp/index.html
// member is fully qualified - https://docs.oracle.com/cd/E19957-01/816-6082-10/chap4.doc.html#43314
var obj = {
dn: req.dn.toString(),
attributes: {
objectclass: ['nisMailAlias'],
objectcategory: 'nisMailAlias',
cn: `${alias.name}@${alias.domain}`,
rfc822MailMember: `${alias.aliasTarget}@${alias.domain}`
cn: alias.name,
rfc822MailMember: alias.aliasTarget
}
};
@@ -335,24 +331,19 @@ function mailingListSearch(req, res, next) {
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()));
mailboxdb.getGroup(parts[0], parts[1], function (error, group) {
mailboxdb.getGroup(req.dn.rdns[0].attrs.cn.value.toLowerCase(), config.fqdn(), function (error, group) {
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.toString()));
// http://ldapwiki.willeke.com/wiki/Original%20Mailgroup%20Schema%20From%20Netscape
// members are fully qualified (https://docs.oracle.com/cd/E19444-01/816-6018-10/groups.htm#13356)
var obj = {
dn: req.dn.toString(),
attributes: {
objectclass: ['mailGroup'],
objectcategory: 'mailGroup',
cn: `${group.name}@${group.domain}`, // fully qualified
mail: `${group.name}@${group.domain}`,
mgrpRFC822MailMember: group.members.map(function (m) { return `${m}@${group.domain}`; })
cn: group.name,
mail: group.name + '@' + config.fqdn(),
mgrpRFC822MailMember: group.members
}
};
@@ -410,7 +401,7 @@ function authorizeUserForApp(req, res, next) {
// we return no such object, to avoid leakage of a users existence
if (!result) return next(new ldap.NoSuchObjectError(req.dn.toString()));
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', appId: app.id, app: app }, { userId: req.user.id, user: user.removePrivateFields(req.user) });
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', appId: app.id }, { userId: req.user.id });
res.end();
});
@@ -418,51 +409,43 @@ function authorizeUserForApp(req, res, next) {
}
function authenticateMailbox(req, res, next) {
debug('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()));
var name = req.dn.rdns[0].attrs.cn.value.toLowerCase();
mailboxdb.getMailbox(parts[0], parts[1], function (error, mailbox) {
// allow login via email
var parts = name.split('@');
if (parts[1] === config.fqdn()) {
name = parts[0];
}
mailboxdb.getMailbox(name, config.fqdn(), 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));
mail.get(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 (mailbox.ownerType === mailboxdb.TYPE_APP) {
var addonId = req.dn.rdns[1].attrs.ou.value.toLowerCase(); // 'sendmail' or 'recvmail'
var name;
if (addonId === 'sendmail') name = 'MAIL_SMTP_PASSWORD';
else if (addonId === 'recvmail') name = 'MAIL_IMAP_PASSWORD';
else return next(new ldap.OperationsError('Invalid DN'));
if (mailbox.ownerType === mailboxdb.TYPE_APP) {
var addonId = req.dn.rdns[1].attrs.ou.value.toLowerCase(); // 'sendmail' or 'recvmail'
var name;
if (addonId === 'sendmail') name = 'MAIL_SMTP_PASSWORD';
else if (addonId === 'recvmail') name = 'MAIL_IMAP_PASSWORD';
else return next(new ldap.OperationsError('Invalid DN'));
appdb.getAddonConfigByName(mailbox.ownerId, addonId, name, function (error, value) {
if (error) return next(new ldap.OperationsError(error.message));
if (req.credentials !== value) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
appdb.getAddonConfigByName(mailbox.ownerId, addonId, name, function (error, value) {
if (error) return next(new ldap.OperationsError(error.message));
if (req.credentials !== value) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
eventlog.add(eventlog.ACTION_APP_LOGIN, { authType: 'ldap', mailboxId: name }, { appId: mailbox.ownerId, addonId: addonId });
return res.end();
});
} else if (mailbox.ownerType === mailboxdb.TYPE_USER) {
if (!domain.enabled) return next(new ldap.NoSuchObjectError(req.dn.toString()));
user.verifyWithUsername(parts[0], req.credentials || '', function (error, result) {
if (error && error.reason === UserError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error && error.reason === UserError.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: user.removePrivateFields(result) });
res.end();
});
} else {
return next(new ldap.OperationsError('Unknown ownerType for mailbox'));
}
});
eventlog.add(eventlog.ACTION_APP_LOGIN, { authType: 'ldap', mailboxId: name }, { appId: mailbox.ownerId, addonId: addonId });
return res.end();
});
} else if (mailbox.ownerType === mailboxdb.TYPE_USER) {
authenticateUser(req, res, function (error) {
if (error) return next(error);
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: name }, { userId: req.user.username });
res.end();
});
} else {
return next(new ldap.OperationsError('Unknown ownerType for mailbox'));
}
});
}
-1053
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -9,7 +9,7 @@ This is most likely a problem in the application.
To resolve this, you can try the following:
* Restart the app in the app configuration dialog
* Restore the app to the latest backup
* Contact us via support@cloudron.io or https://forum.cloudron.io
* Contact us via support@cloudron.io or https://chat.cloudron.io
Powered by https://cloudron.io
+2 -2
View File
@@ -1,8 +1,8 @@
<%if (format === 'text') { %>
Dear <%= cloudronName %> Admin,
Dear Cloudron Admin,
creating a backup has failed.
creating a backup of <%= fqdn %> has failed.
-------------------------------------
+2 -2
View File
@@ -2,7 +2,7 @@
Dear <%= cloudronName %> Admin,
Version <%= newBoxVersion %> is now available!
Version <%= newBoxVersion %> for Cloudron <%= fqdn %> is now available!
Changelog:
<% for (var i = 0; i < changelog.length; i++) { %>
@@ -27,7 +27,7 @@ Sent at: <%= new Date().toUTCString() %>
<div style="width: 650px; text-align: left;">
<p>
Version <b><%= newBoxVersion %></b> is now available!
Version <b><%= newBoxVersion %></b> for Cloudron <%= fqdn %> is now available!
</p>
<h5>Changelog:</h5>
+1 -1
View File
@@ -2,7 +2,7 @@
Dear <%= cloudronName %> Admin,
This is a summary of the activities on your Cloudron.
This is a summary of the activities on your Cloudron <%= fqdn %>.
<% if (info.usersAdded.length) { -%>
The following users were added:
+2 -2
View File
@@ -1,8 +1,8 @@
<%if (format === 'text') { %>
Dear <%= cloudronName %> Admin,
Dear Cloudron Admin,
<%= program %> exited unexpectedly using too much memory!
<%= program %> on <%= fqdn %> exited unexpectedly using too much memory!
The app has been restarted now. Should this message appear repeatedly or
undefined behavior is observed, give the app more memory.
+2 -2
View File
@@ -1,8 +1,8 @@
<%if (format === 'text') { %>
Dear <%= cloudronName %> Admin,
Dear Cloudron Admin,
your server is running out of disk space.
<%= fqdn %> is running out of disk space.
Disk space logs are attached.
+2 -2
View File
@@ -1,6 +1,6 @@
<%if (format === 'text') { %>
Hi <%= user.displayName || user.username || user.email %>,
Hi <%= user.displayName || user.username || user.alternateEmail || user.email %>,
Someone, hopefully you, has requested your account's password
be reset. If you did not request this reset, please ignore this message.
@@ -18,7 +18,7 @@ Powered by https://cloudron.io
<img src="<%= cloudronAvatarUrl %>" width="128px" height="128px"/>
<h3>Hi <%= user.displayName || user.username || user.email %>,</h3>
<h3>Hi <%= user.displayName || user.username || user.alternateEmail || user.email %>,</h3>
<p>
Someone, hopefully you, has requested your account's password be reset.<br/>
+1 -1
View File
@@ -1,6 +1,6 @@
<%if (format === 'text') { %>
Test email from <%= cloudronName %>,
Test email from <%= fqdn %>,
If you can read this, your Cloudron email settings are good.
+1
View File
@@ -1,6 +1,7 @@
{
"format": "html",
"webadminUrl": "https://my.cloudron.io",
"fqdn": "my.cloudron.io",
"cloudronName": "Smartserver",
"cloudronAvatarUrl": "https://cloudron.io/img/logo.png",
"info": {
+2 -2
View File
@@ -1,8 +1,8 @@
<%if (format === 'text') { %>
Dear <%= cloudronName %> Admin,
Dear Cloudron Admin,
Unfortunately <%= program %> exited unexpectedly!
Unfortunately <%= program %> on <%= fqdn %> exited unexpectedly!
Please see some excerpt of the logs below:
+2 -2
View File
@@ -2,7 +2,7 @@
Dear <%= cloudronName %> Admin,
A new user with email <%= user.email %> was added to <%= cloudronName %>.
A new user with email <%= user.alternateEmail || user.email %> was added to <%= cloudronName %>.
<% if (inviteLink) { %>
As requested, this user has not been sent an invitation email.
@@ -24,7 +24,7 @@ Powered by https://cloudron.io
<h3>Dear <%= cloudronName %> Admin,</h3>
<p>
A new user with email <%= user.email %> was added to <%= cloudronName %>.
A new user with email <%= user.alternateEmail || user.email %> was added to <%= cloudronName %>.
</p>
<% if (inviteLink) { %>
+1 -1
View File
@@ -2,7 +2,7 @@
Dear Cloudron Admin,
User <%= user.username || user.email %> <%= event %>.
User <%= user.username || user.alternateEmail || user.email %> <%= event %>.
Powered by https://cloudron.io
+2 -2
View File
@@ -1,6 +1,6 @@
<%if (format === 'text') { %>
Dear <%= user.displayName || user.username || user.email %>,
Dear <%= user.displayName || user.username || user.alternateEmail || user.email %>,
Welcome to <%= cloudronName %>!
@@ -20,7 +20,7 @@ Powered by https://cloudron.io
<img src="<%= cloudronAvatarUrl %>" width="128px" height="128px"/>
<h3>Hi <%= user.displayName || user.username || user.email %>,</h3>
<h3>Hi <%= user.displayName || user.username || user.alternateEmail || user.email %>,</h3>
<h2>Welcome to <%= cloudronName %>!</h2>
+1 -27
View File
@@ -6,7 +6,6 @@ exports = module.exports = {
listAliases: listAliases,
listMailboxes: listMailboxes,
listGroups: listGroups,
getMailbox: getMailbox,
getGroup: getGroup,
@@ -17,7 +16,6 @@ exports = module.exports = {
getByOwnerId: getByOwnerId,
delByOwnerId: delByOwnerId,
delByDomain: delByDomain,
updateName: updateName,
@@ -73,18 +71,6 @@ function del(name, domain, callback) {
});
}
function delByDomain(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
// deletes aliases as well
database.query('DELETE FROM mailboxes WHERE domain = ?', [ domain ], function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null);
});
}
function delByOwnerId(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -140,18 +126,6 @@ function listMailboxes(domain, callback) {
});
}
function listGroups(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE domain = ? AND ownerType = ? AND aliasTarget IS NULL', [ domain, exports.TYPE_GROUP ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
callback(null, results);
});
}
function getGroup(name, domain, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
@@ -204,7 +178,7 @@ function setAliasesForName(name, domain, aliases, callback) {
queries.push({ query: 'DELETE FROM mailboxes WHERE aliasTarget = ? AND domain = ?', args: [ name, domain ] });
aliases.forEach(function (alias) {
queries.push({ query: 'INSERT INTO mailboxes (name, domain, aliasTarget, ownerId, ownerType) VALUES (?, ?, ?, ?, ?)',
args: [ alias, domain, name, results[0].ownerId, results[0].ownerType ] });
args: [ alias, domain, name, results[0].ownerId, results[0].ownerType ] });
});
database.transaction(queries, function (error) {
-123
View File
@@ -1,123 +0,0 @@
'use strict';
exports = module.exports = {
add: add,
del: del,
get: get,
getAll: getAll,
update: update,
_clear: clear,
TYPE_USER: 'user',
TYPE_APP: 'app',
TYPE_GROUP: 'group'
};
var assert = require('assert'),
database = require('./database.js'),
DatabaseError = require('./databaseerror.js'),
safe = require('safetydance');
var MAILDB_FIELDS = [ 'domain', 'enabled', 'mailFromValidation', 'catchAllJson', 'relayJson' ].join(',');
function postProcess(data) {
data.enabled = !!data.enabled; // int to boolean
data.mailFromValidation = !!data.mailFromValidation; // int to boolean
data.catchAll = safe.JSON.parse(data.catchAllJson) || [ ];
delete data.catchAllJson;
data.relay = safe.JSON.parse(data.relayJson) || { provider: 'cloudron-smtp' };
delete data.relayJson;
return data;
}
function add(domain, callback) {
assert.strictEqual(typeof domain, 'string');
database.query('INSERT INTO mail (domain) VALUES (?)', [ domain ], function (error) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, 'mail domain already exists'));
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND), 'no such domain');
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null);
});
}
function clear(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('TRUNCATE TABLE mail', [], function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null);
});
}
function del(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
// deletes aliases as well
database.query('DELETE FROM mail WHERE domain=?', [ domain ], function (error, result) {
if (error && error.code === 'ER_ROW_IS_REFERENCED_2') return callback(new DatabaseError(DatabaseError.IN_USE));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
callback(null);
});
}
function get(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + MAILDB_FIELDS + ' FROM mail WHERE domain = ?', [ domain ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
callback(null, postProcess(results[0]));
});
}
function getAll(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + MAILDB_FIELDS + ' FROM mail ORDER BY domain', function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
results.forEach(function (result) { postProcess(result); });
callback(null, results);
});
}
function update(domain, data, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof callback, 'function');
var args = [ ];
var fields = [ ];
for (var k in data) {
if (k === 'catchAll') {
fields.push('catchAllJson = ?');
args.push(JSON.stringify(data[k]));
} else if (k === 'relay') {
fields.push('relayJson = ?');
args.push(JSON.stringify(data[k]));
} else {
fields.push(k + ' = ?');
args.push(data[k]);
}
}
args.push(domain);
database.query('UPDATE mail SET ' + fields.join(', ') + ' WHERE domain=?', args, function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
callback(null);
});
}
+196 -192
View File
@@ -32,7 +32,6 @@ var assert = require('assert'),
debug = require('debug')('box:mailer'),
docker = require('./docker.js').connection,
ejs = require('ejs'),
mail = require('./mail.js'),
nodemailer = require('nodemailer'),
path = require('path'),
safe = require('safetydance'),
@@ -60,38 +59,12 @@ function splatchError(error) {
return util.inspect(result, { depth: null, showHidden: true });
}
// This will collect the most common details required for notification emails
function getMailConfig(callback) {
assert.strictEqual(typeof callback, 'function');
getAdminEmails(function (error, adminEmails) {
if (error) return callback(error);
settings.getCloudronName(function (error, cloudronName) {
// this is not fatal
if (error) {
debug(error);
cloudronName = 'Cloudron';
}
mail.getAll(function (error, domains) {
if (error) return callback(error);
if (domains.length === 0) return callback('No domains configured');
const defaultDomain = domains[0];
callback(null, {
adminEmails: adminEmails,
cloudronName: cloudronName,
notificationDomain: defaultDomain.domain,
notificationFrom: `"${cloudronName}" <no-reply@${defaultDomain.domain}>`
});
});
});
});
function mailConfig() {
return {
from: '"Cloudron" <no-reply@' + config.fqdn() + '>'
};
}
function processQueue() {
sendMails(gMailQueue);
gMailQueue = [ ];
@@ -164,7 +137,7 @@ function getAdminEmails(callback) {
if (admins.length === 0) return callback(new Error('No admins on this cloudron')); // box not activated yet
var adminEmails = [ ];
adminEmails.push(admins[0].fallbackEmail);
if (admins[0].alternateEmail) adminEmails.push(admins[0].alternateEmail);
admins.forEach(function (admin) { adminEmails.push(admin.email); });
callback(null, adminEmails);
@@ -175,16 +148,16 @@ function mailUserEventToAdmins(user, event) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof event, 'string');
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
getAdminEmails(function (error, adminEmails) {
if (error) return debug('Error getting admins', error);
var adminEmails = _.difference(mailConfig.adminEmails, [ user.email ]);
adminEmails = _.difference(adminEmails, [ user.email ]);
var mailOptions = {
from: mailConfig.notificationFrom,
from: mailConfig().from,
to: adminEmails.join(', '),
subject: util.format('[%s] %s %s', mailConfig.cloudronName, user.username || user.fallbackEmail || user.email, event),
text: render('user_event.ejs', { user: user, event: event, format: 'text' }),
subject: util.format('[%s] %s %s', config.fqdn(), user.username || user.alternateEmail || user.email, event),
text: render('user_event.ejs', { fqdn: config.fqdn(), user: user, event: event, format: 'text' }),
};
enqueue(mailOptions);
@@ -197,15 +170,19 @@ function sendInvite(user, invitor) {
debug('Sending invite mail');
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
settings.getCloudronName(function (error, cloudronName) {
if (error) {
debug(error);
cloudronName = 'Cloudron';
}
var templateData = {
user: user,
webadminUrl: config.adminOrigin(),
setupLink: config.adminOrigin() + '/api/v1/session/account/setup.html?reset_token=' + user.resetToken,
fqdn: config.fqdn(),
invitor: invitor,
cloudronName: mailConfig.cloudronName,
cloudronName: cloudronName,
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
};
@@ -216,9 +193,9 @@ function sendInvite(user, invitor) {
templateDataHTML.format = 'html';
var mailOptions = {
from: mailConfig.notificationFrom,
to: user.fallbackEmail,
subject: util.format('Welcome to %s', mailConfig.cloudronName),
from: mailConfig().from,
to: user.alternateEmail || user.email,
subject: util.format('Welcome to %s', cloudronName),
text: render('welcome_user.ejs', templateDataText),
html: render('welcome_user.ejs', templateDataHTML)
};
@@ -233,33 +210,41 @@ function userAdded(user, inviteSent) {
debug('Sending mail for userAdded %s including invite link', inviteSent ? 'not' : '');
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
getAdminEmails(function (error, adminEmails) {
if (error) return debug('Error getting admins', error);
var adminEmails = _.difference(mailConfig.adminEmails, [ user.email ]);
adminEmails = _.difference(adminEmails, [ user.email ]);
var templateData = {
user: user,
inviteLink: inviteSent ? null : config.adminOrigin() + '/api/v1/session/account/setup.html?reset_token=' + user.resetToken,
cloudronName: mailConfig.cloudronName,
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
};
settings.getCloudronName(function (error, cloudronName) {
if (error) {
debug(error);
cloudronName = 'Cloudron';
}
var templateDataText = JSON.parse(JSON.stringify(templateData));
templateDataText.format = 'text';
var templateData = {
fqdn: config.fqdn(),
user: user,
inviteLink: inviteSent ? null : config.adminOrigin() + '/api/v1/session/account/setup.html?reset_token=' + user.resetToken,
cloudronName: cloudronName,
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
};
var templateDataHTML = JSON.parse(JSON.stringify(templateData));
templateDataHTML.format = 'html';
var templateDataText = JSON.parse(JSON.stringify(templateData));
templateDataText.format = 'text';
var mailOptions = {
from: mailConfig.notificationFrom,
to: adminEmails.join(', '),
subject: util.format('[%s] User %s added', mailConfig.cloudronName, user.fallbackEmail),
text: render('user_added.ejs', templateDataText),
html: render('user_added.ejs', templateDataHTML)
};
var templateDataHTML = JSON.parse(JSON.stringify(templateData));
templateDataHTML.format = 'html';
enqueue(mailOptions);
var mailOptions = {
from: mailConfig().from,
to: adminEmails.join(', '),
subject: util.format('[%s] User %s added', config.fqdn(), user.alternateEmail || user.email),
text: render('user_added.ejs', templateDataText),
html: render('user_added.ejs', templateDataHTML)
};
enqueue(mailOptions);
});
});
}
@@ -285,13 +270,17 @@ function passwordReset(user) {
debug('Sending mail for password reset for user %s.', user.email, user.id);
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
settings.getCloudronName(function (error, cloudronName) {
if (error) {
debug(error);
cloudronName = 'Cloudron';
}
var templateData = {
fqdn: config.fqdn(),
user: user,
resetLink: config.adminOrigin() + '/api/v1/session/password/reset.html?reset_token=' + user.resetToken,
cloudronName: mailConfig.cloudronName,
cloudronName: cloudronName,
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
};
@@ -302,9 +291,9 @@ function passwordReset(user) {
templateDataHTML.format = 'html';
var mailOptions = {
from: mailConfig.notificationFrom,
to: user.fallbackEmail,
subject: util.format('[%s] Password Reset', mailConfig.cloudronName),
from: mailConfig().from,
to: user.alternateEmail || user.email,
subject: util.format('[%s] Password Reset', config.fqdn()),
text: render('password_reset.ejs', templateDataText),
html: render('password_reset.ejs', templateDataHTML)
};
@@ -318,14 +307,14 @@ function appDied(app) {
debug('Sending mail for app %s @ %s died', app.id, app.fqdn);
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
getAdminEmails(function (error, adminEmails) {
if (error) return debug('Error getting admins', error);
var mailOptions = {
from: mailConfig.notificationFrom,
to: config.provider() === 'caas' ? 'support@cloudron.io' : mailConfig.adminEmails.join(', '),
subject: util.format('[%s] App %s is down', mailConfig.cloudronName, app.fqdn),
text: render('app_down.ejs', { title: app.manifest.title, appFqdn: app.fqdn, format: 'text' })
from: mailConfig().from,
to: config.provider() === 'caas' ? 'support@cloudron.io' : adminEmails.join(', '),
subject: util.format('[%s] App %s is down', config.fqdn(), app.fqdn),
text: render('app_down.ejs', { fqdn: config.fqdn(), title: app.manifest.title, appFqdn: app.fqdn, format: 'text' })
};
enqueue(mailOptions);
@@ -337,36 +326,44 @@ function boxUpdateAvailable(hasSubscription, newBoxVersion, changelog) {
assert.strictEqual(typeof newBoxVersion, 'string');
assert(util.isArray(changelog));
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
getAdminEmails(function (error, adminEmails) {
if (error) return debug('Error getting admins', error);
var converter = new showdown.Converter();
settings.getCloudronName(function (error, cloudronName) {
if (error) {
debug(error);
cloudronName = 'Cloudron';
}
var templateData = {
webadminUrl: config.adminOrigin(),
newBoxVersion: newBoxVersion,
hasSubscription: hasSubscription,
changelog: changelog,
changelogHTML: changelog.map(function (e) { return converter.makeHtml(e); }),
cloudronName: mailConfig.cloudronName,
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
};
var converter = new showdown.Converter();
var templateDataText = JSON.parse(JSON.stringify(templateData));
templateDataText.format = 'text';
var templateData = {
fqdn: config.fqdn(),
webadminUrl: config.adminOrigin(),
newBoxVersion: newBoxVersion,
hasSubscription: hasSubscription,
changelog: changelog,
changelogHTML: changelog.map(function (e) { return converter.makeHtml(e); }),
cloudronName: cloudronName,
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
};
var templateDataHTML = JSON.parse(JSON.stringify(templateData));
templateDataHTML.format = 'html';
var templateDataText = JSON.parse(JSON.stringify(templateData));
templateDataText.format = 'text';
var mailOptions = {
from: mailConfig.notificationFrom,
to: mailConfig.adminEmails.join(', '),
subject: util.format('%s has a new update available', mailConfig.cloudronName),
text: render('box_update_available.ejs', templateDataText),
html: render('box_update_available.ejs', templateDataHTML)
};
var templateDataHTML = JSON.parse(JSON.stringify(templateData));
templateDataHTML.format = 'html';
enqueue(mailOptions);
var mailOptions = {
from: mailConfig().from,
to: adminEmails.join(', '),
subject: util.format('%s has a new update available', config.fqdn()),
text: render('box_update_available.ejs', templateDataText),
html: render('box_update_available.ejs', templateDataHTML)
};
enqueue(mailOptions);
});
});
}
@@ -375,81 +372,97 @@ function appUpdateAvailable(app, hasSubscription, info) {
assert.strictEqual(typeof hasSubscription, 'boolean');
assert.strictEqual(typeof info, 'object');
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
getAdminEmails(function (error, adminEmails) {
if (error) return debug('Error getting admins', error);
var converter = new showdown.Converter();
settings.getCloudronName(function (error, cloudronName) {
if (error) {
debug(error);
cloudronName = 'Cloudron';
}
var templateData = {
webadminUrl: config.adminOrigin(),
hasSubscription: hasSubscription,
app: app,
updateInfo: info,
changelogHTML: converter.makeHtml(info.manifest.changelog),
cloudronName: mailConfig.cloudronName,
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
};
var converter = new showdown.Converter();
var templateDataText = JSON.parse(JSON.stringify(templateData));
templateDataText.format = 'text';
var templateData = {
fqdn: config.fqdn(),
webadminUrl: config.adminOrigin(),
hasSubscription: hasSubscription,
app: app,
updateInfo: info,
changelogHTML: converter.makeHtml(info.manifest.changelog),
cloudronName: cloudronName,
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
};
var templateDataHTML = JSON.parse(JSON.stringify(templateData));
templateDataHTML.format = 'html';
var templateDataText = JSON.parse(JSON.stringify(templateData));
templateDataText.format = 'text';
var mailOptions = {
from: mailConfig.notificationFrom,
to: mailConfig.adminEmails.join(', '),
subject: util.format('App %s has a new update available', app.fqdn),
text: render('app_update_available.ejs', templateDataText),
html: render('app_update_available.ejs', templateDataHTML)
};
var templateDataHTML = JSON.parse(JSON.stringify(templateData));
templateDataHTML.format = 'html';
enqueue(mailOptions);
var mailOptions = {
from: mailConfig().from,
to: adminEmails.join(', '),
subject: util.format('App %s has a new update available', app.fqdn),
text: render('app_update_available.ejs', templateDataText),
html: render('app_update_available.ejs', templateDataHTML)
};
enqueue(mailOptions);
});
});
}
function sendDigest(info) {
assert.strictEqual(typeof info, 'object');
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
getAdminEmails(function (error, adminEmails) {
if (error) return debug('Error getting admins', error);
var templateData = {
webadminUrl: config.adminOrigin(),
cloudronName: mailConfig.cloudronName,
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar',
info: info
};
settings.getCloudronName(function (error, cloudronName) {
if (error) {
debug(error);
cloudronName = 'Cloudron';
}
var templateDataText = JSON.parse(JSON.stringify(templateData));
templateDataText.format = 'text';
var templateData = {
fqdn: config.fqdn(),
webadminUrl: config.adminOrigin(),
cloudronName: cloudronName,
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar',
info: info
};
var templateDataHTML = JSON.parse(JSON.stringify(templateData));
templateDataHTML.format = 'html';
var templateDataText = JSON.parse(JSON.stringify(templateData));
templateDataText.format = 'text';
var mailOptions = {
from: mailConfig.notificationFrom,
to: mailConfig.adminEmails.join(', '),
subject: util.format('[%s] Cloudron - Weekly activity digest', mailConfig.cloudronName),
text: render('digest.ejs', templateDataText),
html: render('digest.ejs', templateDataHTML)
};
var templateDataHTML = JSON.parse(JSON.stringify(templateData));
templateDataHTML.format = 'html';
enqueue(mailOptions);
var mailOptions = {
from: mailConfig().from,
to: adminEmails.join(', '),
subject: util.format('[%s] Cloudron - Weekly activity digest', config.fqdn()),
text: render('digest.ejs', templateDataText),
html: render('digest.ejs', templateDataHTML)
};
enqueue(mailOptions);
});
});
}
function outOfDiskSpace(message) {
assert.strictEqual(typeof message, 'string');
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
getAdminEmails(function (error, adminEmails) {
if (error) return debug('Error getting admins', error);
var mailOptions = {
from: mailConfig.notificationFrom,
to: config.provider() === 'caas' ? 'support@cloudron.io' : mailConfig.adminEmails.join(', '),
subject: util.format('[%s] Out of disk space alert', mailConfig.cloudronName),
text: render('out_of_disk_space.ejs', { cloudronName: mailConfig.cloudronName, message: message, format: 'text' })
from: mailConfig().from,
to: config.provider() === 'caas' ? 'support@cloudron.io' : adminEmails.join(', '),
subject: util.format('[%s] Out of disk space alert', config.fqdn()),
text: render('out_of_disk_space.ejs', { fqdn: config.fqdn(), message: message, format: 'text' })
};
sendMails([ mailOptions ]);
@@ -459,14 +472,14 @@ function outOfDiskSpace(message) {
function backupFailed(error) {
var message = splatchError(error);
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
getAdminEmails(function (error, adminEmails) {
if (error) return debug('Error getting admins', error);
var mailOptions = {
from: mailConfig.notificationFrom,
to: config.provider() === 'caas' ? 'support@cloudron.io' : mailConfig.adminEmails.join(', '),
subject: util.format('[%s] Failed to backup', mailConfig.cloudronName),
text: render('backup_failed.ejs', { cloudronName: mailConfig.cloudronName, message: message, format: 'text' })
from: mailConfig().from,
to: config.provider() === 'caas' ? 'support@cloudron.io' : adminEmails.join(', '),
subject: util.format('[%s] Failed to backup', config.fqdn()),
text: render('backup_failed.ejs', { fqdn: config.fqdn(), message: message, format: 'text' })
};
enqueue(mailOptions);
@@ -477,12 +490,12 @@ function certificateRenewalError(domain, message) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof message, 'string');
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
getAdminEmails(function (error, adminEmails) {
if (error) return debug('Error getting admins', error);
var mailOptions = {
from: mailConfig.notificationFrom,
to: config.provider() === 'caas' ? 'support@cloudron.io' : mailConfig.adminEmails.join(', '),
from: mailConfig().from,
to: config.provider() === 'caas' ? 'support@cloudron.io' : adminEmails.join(', '),
subject: util.format('[%s] Certificate renewal error', domain),
text: render('certificate_renewal_error.ejs', { domain: domain, message: message, format: 'text' })
};
@@ -495,14 +508,14 @@ function oomEvent(program, context) {
assert.strictEqual(typeof program, 'string');
assert.strictEqual(typeof context, 'string');
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
getAdminEmails(function (error, adminEmails) {
if (error) return debug('Error getting admins', error);
var mailOptions = {
from: mailConfig.notificationFrom,
to: config.provider() === 'caas' ? 'support@cloudron.io' : mailConfig.adminEmails.join(', '),
subject: util.format('[%s] %s exited unexpectedly', mailConfig.cloudronName, program),
text: render('oom_event.ejs', { cloudronName: mailConfig.cloudronName, program: program, context: context, format: 'text' })
from: mailConfig().from,
to: config.provider() === 'caas' ? 'support@cloudron.io' : adminEmails.join(', '),
subject: util.format('[%s] %s exited unexpectedly', config.fqdn(), program),
text: render('oom_event.ejs', { fqdn: config.fqdn(), program: program, context: context, format: 'text' })
};
sendMails([ mailOptions ]);
@@ -510,7 +523,7 @@ function oomEvent(program, context) {
}
// this function bypasses the queue intentionally. it is also expected to work without the mailer module initialized
// NOTE: crashnotifier should ideally be able to send mail when there is no db, however we need the 'from' address domain from the db
// NOTE: crashnotifier should be able to send mail when there is no db
function unexpectedExit(program, context, callback) {
assert.strictEqual(typeof program, 'string');
assert.strictEqual(typeof context, 'string');
@@ -518,36 +531,27 @@ function unexpectedExit(program, context, callback) {
if (config.provider() !== 'caas') return callback(); // no way to get admins without db access
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
var mailOptions = {
from: mailConfig().from,
to: 'support@cloudron.io',
subject: util.format('[%s] %s exited unexpectedly', config.fqdn(), program),
text: render('unexpected_exit.ejs', { fqdn: config.fqdn(), program: program, context: context, format: 'text' })
};
var mailOptions = {
from: mailConfig.notificationFrom,
to: 'support@cloudron.io',
subject: util.format('[%s] %s exited unexpectedly', mailConfig.cloudronName, program),
text: render('unexpected_exit.ejs', { cloudronName: mailConfig.cloudronName, program: program, context: context, format: 'text' })
};
sendMails([ mailOptions ], callback);
});
sendMails([ mailOptions ], callback);
}
function sendTestMail(domain, email) {
assert.strictEqual(typeof domain, 'string');
function sendTestMail(email) {
assert.strictEqual(typeof email, 'string');
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
var mailOptions = {
from: mailConfig().from,
to: email,
subject: util.format('Test Email from %s', config.fqdn()),
text: render('test.ejs', { fqdn: config.fqdn(), format: 'text'})
};
var mailOptions = {
from: `"${mailConfig.cloudronName}" <no-reply@${domain}>`,
to: email,
subject: util.format('Test Email from %s', mailConfig.cloudronName),
text: render('test.ejs', { cloudronName: mailConfig.cloudronName, format: 'text'})
};
enqueue(mailOptions);
});
enqueue(mailOptions);
}
function _getMailQueue() {
-35
View File
@@ -1,35 +0,0 @@
'use strict';
exports = module.exports = {
resolve: resolve
};
var assert = require('assert'),
dns = require('dns');
// a note on TXT records. It doesn't have quotes ("") at the DNS level. Those quotes
// are added for DNS server software to enclose spaces. Such quotes may also be returned
// by the DNS REST API of some providers
function resolve(hostname, rrtype, options, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof rrtype, 'string');
assert(options && typeof options === 'object');
assert.strictEqual(typeof callback, 'function');
const resolver = new dns.Resolver();
if (options.server) resolver.setServers([ options.server ]);
// should callback with ECANCELLED but looks like we might hit https://github.com/nodejs/node/issues/14814
const timerId = setTimeout(resolver.cancel.bind(resolver), options.timeout || 5000);
resolver.resolve(hostname, rrtype, function (error, result) {
clearTimeout(timerId);
if (error && error.code === 'ECANCELLED') error.code = 'TIMEOUT';
// result is an empty array if there was no error but there is no record. when you query a random
// domain, it errors with ENOTFOUND. But if you query an existing domain (A record) but with different
// type (CNAME) it is not an error and empty array
callback(error, result);
});
}
+108
View File
@@ -0,0 +1,108 @@
'use strict';
var assert = require('assert'),
config = require('./config.js'),
debug = require('debug')('box:nginx'),
ejs = require('ejs'),
fs = require('fs'),
path = require('path'),
paths = require('./paths.js'),
safe = require('safetydance'),
shell = require('./shell.js');
exports = module.exports = {
configureAdmin: configureAdmin,
configureApp: configureApp,
unconfigureApp: unconfigureApp,
reload: reload,
removeAppConfigs: removeAppConfigs
};
var NGINX_APPCONFIG_EJS = fs.readFileSync(__dirname + '/../setup/start/nginx/appconfig.ejs', { encoding: 'utf8' }),
RELOAD_NGINX_CMD = path.join(__dirname, 'scripts/reloadnginx.sh');
function configureAdmin(certFilePath, keyFilePath, configFileName, vhost, callback) {
assert.strictEqual(typeof certFilePath, 'string');
assert.strictEqual(typeof keyFilePath, 'string');
assert.strictEqual(typeof configFileName, 'string');
assert.strictEqual(typeof vhost, 'string');
assert.strictEqual(typeof callback, 'function');
var data = {
sourceDir: path.resolve(__dirname, '..'),
adminOrigin: config.adminOrigin(),
vhost: vhost, // if vhost is empty it will become the default_server
hasIPv6: config.hasIPv6(),
endpoint: 'admin',
certFilePath: certFilePath,
keyFilePath: keyFilePath,
xFrameOptions: 'SAMEORIGIN',
robotsTxtQuoted: JSON.stringify('User-agent: *\nDisallow: /\n')
};
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, configFileName);
if (!safe.fs.writeFileSync(nginxConfigFilename, nginxConf)) return callback(safe.error);
reload(callback);
}
function configureApp(app, certFilePath, keyFilePath, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof certFilePath, 'string');
assert.strictEqual(typeof keyFilePath, 'string');
assert.strictEqual(typeof callback, 'function');
var sourceDir = path.resolve(__dirname, '..');
var endpoint = 'app';
var vhost = app.altDomain || app.intrinsicFqdn;
var data = {
sourceDir: sourceDir,
adminOrigin: config.adminOrigin(),
vhost: vhost,
hasIPv6: config.hasIPv6(),
port: app.httpPort,
endpoint: endpoint,
certFilePath: certFilePath,
keyFilePath: keyFilePath,
robotsTxtQuoted: app.robotsTxt ? JSON.stringify(app.robotsTxt) : null,
xFrameOptions: app.xFrameOptions || 'SAMEORIGIN' // once all apps have been updated/
};
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, app.id + '.conf');
debug('writing config for "%s" to %s with options %j', vhost, nginxConfigFilename, data);
if (!safe.fs.writeFileSync(nginxConfigFilename, nginxConf)) {
debug('Error creating nginx config for "%s" : %s', vhost, safe.error.message);
return callback(safe.error);
}
reload(callback);
}
function unconfigureApp(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
var vhost = app.altDomain || app.intrinsicFqdn;
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, app.id + '.conf');
if (!safe.fs.unlinkSync(nginxConfigFilename)) {
if (safe.error.code !== 'ENOENT') debug('Error removing nginx configuration of "%s": %s', vhost, safe.error.message);
return callback(null);
}
reload(callback);
}
function reload(callback) {
shell.sudo('reload', [ RELOAD_NGINX_CMD ], callback);
}
function removeAppConfigs() {
for (var appConfigFile of fs.readdirSync(paths.NGINX_APPCONFIG_DIR)) {
fs.unlinkSync(path.join(paths.NGINX_APPCONFIG_DIR, appConfigFile));
}
}
+1 -1
View File
@@ -33,7 +33,7 @@
</div>
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Sign in"/>
</form>
<a href="/api/v1/session/password/resetRequest.html">Reset password</a>
<a href="/api/v1/session/password/resetRequest.html">Reset your password</a>
</div>
</div>
</div>
+1 -1
View File
@@ -5,7 +5,7 @@
<div class="layout-content">
<center>
<h2>Reset password</h2>
<h2>Reset your password</h2>
</center>
<br/>
+120 -14
View File
@@ -4,26 +4,29 @@ exports = module.exports = {
start: start,
stop: stop,
handleCertChanged: handleCertChanged
createMailConfig: createMailConfig
};
var apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
config = require('./config.js'),
certificates = require('./certificates.js'),
debug = require('debug')('box:platform'),
domains = require('./domains.js'),
fs = require('fs'),
hat = require('hat'),
infra = require('./infra_version.js'),
locker = require('./locker.js'),
mail = require('./mail.js'),
nginx = require('./nginx.js'),
os = require('os'),
paths = require('./paths.js'),
reverseProxy = require('./reverseproxy.js'),
safe = require('safetydance'),
semver = require('semver'),
settings = require('./settings.js'),
shell = require('./shell.js'),
taskmanager = require('./taskmanager.js'),
user = require('./user.js'),
util = require('util'),
_ = require('underscore');
@@ -38,6 +41,14 @@ function start(callback) {
debug('initializing addon infrastructure');
// restart mail container if any of these keys change
settings.events.on(settings.MAIL_CONFIG_KEY, function () { startMail(NOOP_CALLBACK); });
settings.events.on(settings.MAIL_RELAY_KEY, function () { startMail(NOOP_CALLBACK); });
certificates.events.on(certificates.EVENT_CERT_CHANGED, function (domain) {
if (domain === '*.' + config.fqdn() || domain === config.adminFqdn()) startMail(NOOP_CALLBACK);
});
var existingInfra = { version: 'none' };
if (fs.existsSync(paths.INFRA_VERSION_FILE)) {
existingInfra = safe.JSON.parse(fs.readFileSync(paths.INFRA_VERSION_FILE, 'utf8'));
@@ -229,13 +240,116 @@ function startMongodb(callback) {
setTimeout(callback, 5000);
}
function createMailConfig(callback) {
assert.strictEqual(typeof callback, 'function');
const fqdn = config.fqdn();
const mailFqdn = config.mailFqdn();
const alertsFrom = 'no-reply@' + config.fqdn();
debug('createMailConfig: generating mail config');
user.getOwner(function (error, owner) {
var alertsTo = config.provider() === 'caas' ? [ 'support@cloudron.io' ] : [ ];
alertsTo.concat(error ? [] : owner.email).join(','); // owner may not exist yet
settings.getAll(function (error, result) {
if (error) return callback(error);
var catchAll = result[settings.CATCH_ALL_ADDRESS_KEY].join(',');
var mailFromValidation = result[settings.MAIL_FROM_VALIDATION_KEY];
if (!safe.fs.writeFileSync(paths.ADDON_CONFIG_DIR + '/mail/mail.ini',
`mail_domain=${fqdn}\nmail_server_name=${mailFqdn}\nalerts_from=${alertsFrom}\nalerts_to=${alertsTo}\ncatch_all=${catchAll}\nmail_from_validation=${mailFromValidation}\ndkim_selector=${config.dkimSelector()}\n`, 'utf8')) {
return callback(new Error('Could not create mail var file:' + safe.error.message));
}
var relay = result[settings.MAIL_RELAY_KEY];
const enabled = relay.provider !== 'cloudron-smtp' ? true : false,
host = relay.host || '',
port = relay.port || 25,
username = relay.username || '',
password = relay.password || '';
if (!safe.fs.writeFileSync(paths.ADDON_CONFIG_DIR + '/mail/smtp_forward.ini',
`enable_outbound=${enabled}\nhost=${host}\nport=${port}\nenable_tls=true\nauth_type=plain\nauth_user=${username}\nauth_pass=${password}`, 'utf8')) {
return callback(new Error('Could not create mail var file:' + safe.error.message));
}
callback();
});
});
}
function startMail(callback) {
// mail (note: 2525 is hardcoded in mail container and app use this port)
// MAIL_SERVER_NAME is the hostname of the mailserver i.e server uses these certs
// MAIL_DOMAIN is the domain for which this server is relaying mails
// mail container uses /app/data for backed up data and /run for restart-able data
const tag = infra.images.mail.tag;
const memoryLimit = Math.max((1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 128, 256);
// admin and mail share the same certificate
certificates.getAdminCertificate(function (error, cert, key) {
if (error) return callback(error);
// the setup script copies dhparams.pem to /addons/mail
if (!safe.fs.writeFileSync(paths.ADDON_CONFIG_DIR + '/mail/tls_cert.pem', cert)) return callback(new Error('Could not create cert file:' + safe.error.message));
if (!safe.fs.writeFileSync(paths.ADDON_CONFIG_DIR + '/mail/tls_key.pem', key)) return callback(new Error('Could not create key file:' + safe.error.message));
settings.getMailConfig(function (error, mailConfig) {
if (error) return callback(error);
shell.execSync('startMail', 'docker rm -f mail || true');
createMailConfig(function (error) {
if (error) return callback(error);
var ports = mailConfig.enabled ? '-p 587:2525 -p 993:9993 -p 4190:4190 -p 25:2525' : '';
const cmd = `docker run --restart=always -d --name="mail" \
--net cloudron \
--net-alias mail \
-m ${memoryLimit}m \
--memory-swap ${memoryLimit * 2}m \
--dns 172.18.0.1 \
--dns-search=. \
--env ENABLE_MDA=${mailConfig.enabled} \
-v "${paths.MAIL_DATA_DIR}:/app/data" \
-v "${paths.PLATFORM_DATA_DIR}/addons/mail:/etc/mail" \
${ports} \
--read-only -v /run -v /tmp ${tag}`;
shell.execSync('startMail', cmd);
if (!mailConfig.enabled || process.env.BOX_ENV === 'test') return callback();
// Add MX and DMARC record. Note that DMARC policy depends on DKIM signing and thus works
// only if we use our internal mail server.
var records = [
{ subdomain: '_dmarc', type: 'TXT', values: [ '"v=DMARC1; p=reject; pct=100"' ] },
{ subdomain: '', type: 'MX', values: [ '10 ' + config.mailFqdn() + '.' ] }
];
async.mapSeries(records, function (record, iteratorCallback) {
domains.upsertDNSRecords(record.subdomain, config.fqdn(), record.type, record.values, iteratorCallback);
}, NOOP_CALLBACK); // do not crash if DNS creds do not work in startup sequence
callback();
});
});
});
}
function startAddons(existingInfra, callback) {
var startFuncs = [ ];
// always start addons on any infra change, regardless of minor or major update
if (existingInfra.version !== infra.version) {
debug('startAddons: no existing infra or infra upgrade. starting all addons');
startFuncs.push(startGraphite, startMysql, startPostgresql, startMongodb, mail.startMail);
startFuncs.push(startGraphite, startMysql, startPostgresql, startMongodb, startMail);
} else {
assert.strictEqual(typeof existingInfra.images, 'object');
@@ -243,7 +357,7 @@ function startAddons(existingInfra, callback) {
if (infra.images.mysql.tag !== existingInfra.images.mysql.tag) startFuncs.push(startMysql);
if (infra.images.postgresql.tag !== existingInfra.images.postgresql.tag) startFuncs.push(startPostgresql);
if (infra.images.mongodb.tag !== existingInfra.images.mongodb.tag) startFuncs.push(startMongodb);
if (infra.images.mail.tag !== existingInfra.images.mail.tag) startFuncs.push(mail.startMail);
if (infra.images.mail.tag !== existingInfra.images.mail.tag) startFuncs.push(startMail);
debug('startAddons: existing infra. incremental addon create %j', startFuncs.map(function (f) { return f.name; }));
}
@@ -265,15 +379,7 @@ function startApps(existingInfra, callback) {
apps.restoreInstalledApps(callback);
} else {
debug('startApps: reconfiguring installed apps');
reverseProxy.removeAppConfigs(); // should we change the cert location, nginx will not start
nginx.removeAppConfigs(); // should we change the cert location, nginx will not start
apps.configureInstalledApps(callback);
}
}
function handleCertChanged(cn) {
assert.strictEqual(typeof cn, 'string');
if (cn === '*.' + config.adminDomain() || cn === config.adminFqdn()) {
mail.startMail(NOOP_CALLBACK);
}
}
-435
View File
@@ -1,435 +0,0 @@
'use strict';
exports = module.exports = {
ReverseProxyError: ReverseProxyError,
setFallbackCertificate: setFallbackCertificate,
getFallbackCertificate: getFallbackCertificate,
validateCertificate: validateCertificate,
getCertificate: getCertificate,
renewAll: renewAll,
configureDefaultServer: configureDefaultServer,
configureAdmin: configureAdmin,
configureApp: configureApp,
unconfigureApp: unconfigureApp,
reload: reload,
removeAppConfigs: removeAppConfigs,
// exported for testing
_getApi: getApi
};
var acme = require('./cert/acme.js'),
apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
caas = require('./cert/caas.js'),
config = require('./config.js'),
constants = require('./constants.js'),
crypto = require('crypto'),
debug = require('debug')('box:certificates'),
domains = require('./domains.js'),
ejs = require('ejs'),
eventlog = require('./eventlog.js'),
fallback = require('./cert/fallback.js'),
fs = require('fs'),
mailer = require('./mailer.js'),
os = require('os'),
path = require('path'),
paths = require('./paths.js'),
platform = require('./platform.js'),
safe = require('safetydance'),
shell = require('./shell.js'),
user = require('./user.js'),
util = require('util');
var NGINX_APPCONFIG_EJS = fs.readFileSync(__dirname + '/../setup/start/nginx/appconfig.ejs', { encoding: 'utf8' }),
RELOAD_NGINX_CMD = path.join(__dirname, 'scripts/reloadnginx.sh'),
NOOP_CALLBACK = function (error) { if (error) debug(error); };
function ReverseProxyError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
Error.call(this);
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.reason = reason;
if (typeof errorOrMessage === 'undefined') {
this.message = reason;
} else if (typeof errorOrMessage === 'string') {
this.message = errorOrMessage;
} else {
this.message = 'Internal error';
this.nestedError = errorOrMessage;
}
}
util.inherits(ReverseProxyError, Error);
ReverseProxyError.INTERNAL_ERROR = 'Internal Error';
ReverseProxyError.INVALID_CERT = 'Invalid certificate';
ReverseProxyError.NOT_FOUND = 'Not Found';
function getApi(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
domains.get(app.domain, function (error, domain) {
if (error) return callback(error);
if (domain.tlsConfig.provider === 'fallback') return callback(null, fallback, {});
var api = domain.tlsConfig.provider === 'caas' ? caas : acme;
var options = { };
if (domain.tlsConfig.provider === 'caas') {
options.prod = true;
} else { // acme
options.prod = domain.tlsConfig.provider.match(/.*-prod/) !== null; // matches 'le-prod' or 'letsencrypt-prod'
}
// registering user with an email requires A or MX record (https://github.com/letsencrypt/boulder/issues/1197)
// we cannot use admin@fqdn because the user might not have set it up.
// we simply update the account with the latest email we have each time when getting letsencrypt certs
// https://github.com/ietf-wg-acme/acme/issues/30
user.getOwner(function (error, owner) {
options.email = error ? 'support@cloudron.io' : (owner.fallbackEmail || owner.email); // can error if not activated yet
callback(null, api, options);
});
});
}
function isExpiringSync(certFilePath, hours) {
assert.strictEqual(typeof certFilePath, 'string');
assert.strictEqual(typeof hours, 'number');
if (!fs.existsSync(certFilePath)) return 2; // not found
var result = safe.child_process.spawnSync('/usr/bin/openssl', [ 'x509', '-checkend', String(60 * 60 * hours), '-in', certFilePath ]);
debug('isExpiringSync: %s %s %s', certFilePath, result.stdout.toString('utf8').trim(), result.status);
return result.status === 1; // 1 - expired 0 - not expired
}
// note: https://tools.ietf.org/html/rfc4346#section-7.4.2 (certificate_list) requires that the
// servers certificate appears first (and not the intermediate cert)
function validateCertificate(domain, cert, key) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof cert, 'string');
assert.strictEqual(typeof key, 'string');
function matchesDomain(candidate) {
if (typeof candidate !== 'string') return false;
if (candidate === domain) return true;
if (candidate.indexOf('*') === 0 && candidate.slice(2) === domain.slice(domain.indexOf('.') + 1)) return true;
return false;
}
// check for empty cert and key strings
if (!cert && key) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'missing cert');
if (cert && !key) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'missing key');
// -checkhost checks for SAN or CN exclusively. SAN takes precedence and if present, ignores the CN.
var result = safe.child_process.execSync(`openssl x509 -noout -checkhost "${domain}"`, { encoding: 'utf8', input: cert });
if (!result) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'Unable to get certificate subject.');
if (result.indexOf('does match certificate') === -1) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, `Certificate is not valid for this domain. Expecting ${domain}`);
// http://httpd.apache.org/docs/2.0/ssl/ssl_faq.html#verify
var certModulus = safe.child_process.execSync('openssl x509 -noout -modulus', { encoding: 'utf8', input: cert });
var keyModulus = safe.child_process.execSync('openssl rsa -noout -modulus', { encoding: 'utf8', input: key });
if (certModulus !== keyModulus) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'Key does not match the certificate.');
// check expiration
result = safe.child_process.execSync('openssl x509 -checkend 0', { encoding: 'utf8', input: cert });
if (!result) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'Certificate has expired.');
return null;
}
function reload(callback) {
if (process.env.BOX_ENV === 'test') return callback();
shell.sudo('reload', [ RELOAD_NGINX_CMD ], callback);
}
function setFallbackCertificate(domain, fallback, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof fallback, 'object');
assert.strictEqual(typeof callback, 'function');
const certFilePath = path.join(paths.APP_CERTS_DIR, `${domain}.host.cert`);
const keyFilePath = path.join(paths.APP_CERTS_DIR, `${domain}.host.key`);
if (fallback) {
// backup the cert
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${domain}.host.cert`), fallback.cert)) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${domain}.host.key`), fallback.key)) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, safe.error.message));
} else if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) { // generate it
let opensslConf = safe.fs.readFileSync('/etc/ssl/openssl.cnf', 'utf8');
// SAN must contain all the domains since CN check is based on implementation if SAN is found. -checkhost also checks only SAN if present!
let opensslConfWithSan = `${opensslConf}\n[SAN]\nsubjectAltName=DNS:${domain},DNS:*.${domain}\n`;
let configFile = path.join(os.tmpdir(), 'openssl-' + crypto.randomBytes(4).readUInt32LE(0) + '.conf');
safe.fs.writeFileSync(configFile, opensslConfWithSan, 'utf8');
let certCommand = util.format(`openssl req -x509 -newkey rsa:2048 -keyout ${keyFilePath} -out ${certFilePath} -days 3650 -subj /CN=*.${domain} -extensions SAN -config ${configFile} -nodes`);
if (!safe.child_process.execSync(certCommand)) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, safe.error.message));
safe.fs.unlinkSync(configFile);
}
platform.handleCertChanged('*.' + domain);
reload(function (error) {
if (error) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, error));
return callback(null);
});
}
function getFallbackCertificate(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
// check for any pre-provisioned (caas) certs. they get first priority
var certFilePath = path.join(paths.NGINX_CERT_DIR, `${domain}.host.cert`);
var keyFilePath = path.join(paths.NGINX_CERT_DIR, `${domain}.host.key`);
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, { certFilePath, keyFilePath });
// check for auto-generated or user set fallback certs
certFilePath = path.join(paths.APP_CERTS_DIR, `${domain}.host.cert`);
keyFilePath = path.join(paths.APP_CERTS_DIR, `${domain}.host.key`);
callback(null, { certFilePath, keyFilePath });
}
function getCertificate(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
var certFilePath = path.join(paths.APP_CERTS_DIR, `${app.fqdn}.user.cert`);
var keyFilePath = path.join(paths.APP_CERTS_DIR, `${app.fqdn}.user.key`);
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, { certFilePath, keyFilePath });
certFilePath = path.join(paths.APP_CERTS_DIR, `${app.fqdn}.cert`);
keyFilePath = path.join(paths.APP_CERTS_DIR, `${app.fqdn}.key`);
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, { certFilePath, keyFilePath });
return getFallbackCertificate(app.domain, callback);
}
function ensureCertificate(app, auditSource, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
const vhost = app.fqdn;
var certFilePath = path.join(paths.APP_CERTS_DIR, `${vhost}.user.cert`);
var keyFilePath = path.join(paths.APP_CERTS_DIR, `${vhost}.user.key`);
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) {
debug('ensureCertificate: %s. user certificate already exists at %s', vhost, keyFilePath);
return callback(null, { certFilePath, keyFilePath, reason: 'user' });
}
certFilePath = path.join(paths.APP_CERTS_DIR, `${vhost}.cert`);
keyFilePath = path.join(paths.APP_CERTS_DIR, `${vhost}.key`);
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) {
debug('ensureCertificate: %s. certificate already exists at %s', vhost, keyFilePath);
if (!isExpiringSync(certFilePath, 24 * 30)) return callback(null, { certFilePath, keyFilePath, reason: 'existing-le' });
debug('ensureCertificate: %s cert require renewal', vhost);
} else {
debug('ensureCertificate: %s cert does not exist', vhost);
}
getApi(app, function (error, api, apiOptions) {
if (error) return callback(error);
debug('ensureCertificate: getting certificate for %s with options %j', vhost, apiOptions);
api.getCertificate(vhost, apiOptions, function (error, certFilePath, keyFilePath) {
var errorMessage = error ? error.message : '';
if (error) {
debug('ensureCertificate: could not get certificate. using fallback certs', error);
mailer.certificateRenewalError(vhost, errorMessage);
}
eventlog.add(eventlog.ACTION_CERTIFICATE_RENEWAL, auditSource, { domain: vhost, errorMessage: errorMessage });
// if no cert was returned use fallback. the fallback/caas provider will not provide any for example
if (!certFilePath || !keyFilePath) return getFallbackCertificate(app.domain, callback);
callback(null, { certFilePath, keyFilePath, reason: 'new-le' });
});
});
}
function configureAdminInternal(bundle, configFileName, vhost, callback) {
assert.strictEqual(typeof bundle, 'object');
assert.strictEqual(typeof configFileName, 'string');
assert.strictEqual(typeof vhost, 'string');
assert.strictEqual(typeof callback, 'function');
var data = {
sourceDir: path.resolve(__dirname, '..'),
adminOrigin: config.adminOrigin(),
vhost: vhost, // if vhost is empty it will become the default_server
hasIPv6: config.hasIPv6(),
endpoint: 'admin',
certFilePath: bundle.certFilePath,
keyFilePath: bundle.keyFilePath,
xFrameOptions: 'SAMEORIGIN',
robotsTxtQuoted: JSON.stringify('User-agent: *\nDisallow: /\n')
};
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, configFileName);
if (!safe.fs.writeFileSync(nginxConfigFilename, nginxConf)) return callback(safe.error);
reload(callback);
}
function configureAdmin(auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
var adminApp = { domain: config.adminDomain(), fqdn: config.adminFqdn() };
ensureCertificate(adminApp, auditSource, function (error, bundle) {
if (error) return callback(error);
configureAdminInternal(bundle, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn(), callback);
});
}
function configureAppInternal(app, bundle, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof bundle, 'object');
assert.strictEqual(typeof callback, 'function');
var sourceDir = path.resolve(__dirname, '..');
var endpoint = 'app';
var data = {
sourceDir: sourceDir,
adminOrigin: config.adminOrigin(),
vhost: app.fqdn,
hasIPv6: config.hasIPv6(),
port: app.httpPort,
endpoint: endpoint,
certFilePath: bundle.certFilePath,
keyFilePath: bundle.keyFilePath,
robotsTxtQuoted: app.robotsTxt ? JSON.stringify(app.robotsTxt) : null,
xFrameOptions: app.xFrameOptions || 'SAMEORIGIN' // once all apps have been updated/
};
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, app.id + '.conf');
debug('writing config for "%s" to %s with options %j', app.fqdn, nginxConfigFilename, data);
if (!safe.fs.writeFileSync(nginxConfigFilename, nginxConf)) {
debug('Error creating nginx config for "%s" : %s', app.fqdn, safe.error.message);
return callback(safe.error);
}
reload(callback);
}
function configureApp(app, auditSource, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
ensureCertificate(app, auditSource, function (error, bundle) {
if (error) return callback(error);
configureAppInternal(app, bundle, callback);
});
}
function unconfigureApp(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, app.id + '.conf');
if (!safe.fs.unlinkSync(nginxConfigFilename)) {
if (safe.error.code !== 'ENOENT') debug('Error removing nginx configuration of "%s": %s', app.fqdn, safe.error.message);
return callback(null);
}
reload(callback);
}
function renewAll(auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
debug('renewAll: Checking certificates for renewal');
apps.getAll(function (error, allApps) {
if (error) return callback(error);
allApps.push({ domain: config.adminDomain(), fqdn: config.adminFqdn() }); // inject fake webadmin app
async.eachSeries(allApps, function (app, iteratorCallback) {
ensureCertificate(app, auditSource, function (error, bundle) {
if (bundle.reason !== 'new-le' && bundle.reason !== 'fallback') return iteratorCallback();
// reconfigure for the case where we got a renewed cert after fallback
var configureFunc = app.fqdn === config.adminFqdn() ?
configureAdminInternal.bind(null, bundle, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn())
: configureAppInternal.bind(null, app, bundle);
configureFunc(function (ignoredError) {
if (ignoredError) debug('fallbackExpiredCertificates: error reconfiguring app', ignoredError);
platform.handleCertChanged(app.fqdn);
iteratorCallback(); // move to next app
});
});
});
});
}
function removeAppConfigs() {
for (var appConfigFile of fs.readdirSync(paths.NGINX_APPCONFIG_DIR)) {
fs.unlinkSync(path.join(paths.NGINX_APPCONFIG_DIR, appConfigFile));
}
}
function configureDefaultServer(callback) {
callback = callback || NOOP_CALLBACK;
var certFilePath = path.join(paths.NGINX_CERT_DIR, 'default.cert');
var keyFilePath = path.join(paths.NGINX_CERT_DIR, 'default.key');
if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) {
debug('configureDefaultServer: create new cert');
var cn = 'cloudron-' + (new Date()).toISOString(); // randomize date a bit to keep firefox happy
var certCommand = util.format('openssl req -x509 -newkey rsa:2048 -keyout %s -out %s -days 3650 -subj /CN=%s -nodes', keyFilePath, certFilePath, cn);
safe.child_process.execSync(certCommand);
}
configureAdminInternal({ certFilePath, keyFilePath }, 'default.conf', '', function (error) {
if (error) return callback(error);
debug('configureDefaultServer: done');
callback(null);
});
}
+6 -5
View File
@@ -58,6 +58,8 @@ function removeInternalAppFields(app) {
iconUrl: app.iconUrl,
fqdn: app.fqdn,
memoryLimit: app.memoryLimit,
altDomain: app.altDomain,
cnameTarget: app.cnameTarget,
xFrameOptions: app.xFrameOptions,
sso: app.sso,
debugMode: app.debugMode,
@@ -132,6 +134,9 @@ function installApp(req, res, next) {
if ('memoryLimit' in data && typeof data.memoryLimit !== 'number') return next(new HttpError(400, 'memoryLimit is not a number'));
// falsy value in altDomain unsets it
if (data.altDomain && typeof data.altDomain !== 'string') return next(new HttpError(400, 'altDomain must be a string'));
if (data.xFrameOptions && typeof data.xFrameOptions !== 'string') return next(new HttpError(400, 'xFrameOptions must be a string'));
if ('sso' in data && typeof data.sso !== 'boolean') return next(new HttpError(400, 'sso must be a boolean'));
@@ -144,7 +149,6 @@ function installApp(req, res, next) {
debug('Installing app :%j', data);
apps.install(data, auditSource(req), function (error, app) {
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === AppsError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
if (error && error.reason === AppsError.PORT_RESERVED) return next(new HttpError(409, 'Port ' + error.message + ' is reserved.'));
if (error && error.reason === AppsError.PORT_CONFLICT) return next(new HttpError(409, 'Port ' + error.message + ' is already in use.'));
@@ -176,6 +180,7 @@ function configureApp(req, res, next) {
if (!data.cert && data.key) return next(new HttpError(400, 'cert must be provided'));
if ('memoryLimit' in data && typeof data.memoryLimit !== 'number') return next(new HttpError(400, 'memoryLimit is not a number'));
if (data.altDomain && typeof data.altDomain !== 'string') return next(new HttpError(400, 'altDomain must be a string'));
if (data.xFrameOptions && typeof data.xFrameOptions !== 'string') return next(new HttpError(400, 'xFrameOptions must be a string'));
if ('enableBackup' in data && typeof data.enableBackup !== 'boolean') return next(new HttpError(400, 'enableBackup must be a boolean'));
@@ -237,13 +242,9 @@ function cloneApp(req, res, next) {
apps.clone(req.params.id, data, auditSource(req), function (error, result) {
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
if (error && error.reason === AppsError.PORT_RESERVED) return next(new HttpError(409, 'Port ' + error.message + ' is reserved.'));
if (error && error.reason === AppsError.PORT_CONFLICT) return next(new HttpError(409, 'Port ' + error.message + ' is already in use.'));
if (error && error.reason === AppsError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
if (error && error.reason === AppsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message));
if (error && error.reason === AppsError.BILLING_REQUIRED) return next(new HttpError(402, 'Billing required'));
if (error && error.reason === AppsError.BAD_CERTIFICATE) return next(new HttpError(400, error.message));
if (error && error.reason === AppsError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
if (error) return next(new HttpError(500, error));
+153 -3
View File
@@ -1,6 +1,12 @@
'use strict';
exports = module.exports = {
activate: activate,
dnsSetup: dnsSetup,
setupTokenAuth: setupTokenAuth,
providerTokenAuth: providerTokenAuth,
getStatus: getStatus,
restore: restore,
reboot: reboot,
getProgress: getProgress,
getConfig: getConfig,
@@ -9,7 +15,8 @@ exports = module.exports = {
feedback: feedback,
checkForUpdates: checkForUpdates,
getLogs: getLogs,
getLogStream: getLogStream
getLogStream: getLogStream,
sendTestMail: sendTestMail
};
var appstore = require('../appstore.js'),
@@ -18,9 +25,13 @@ var appstore = require('../appstore.js'),
async = require('async'),
cloudron = require('../cloudron.js'),
CloudronError = cloudron.CloudronError,
config = require('../config.js'),
debug = require('debug')('box:routes/cloudron'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
progress = require('../progress.js'),
mailer = require('../mailer.js'),
superagent = require('superagent'),
updateChecker = require('../updatechecker.js'),
_ = require('underscore');
@@ -29,6 +40,136 @@ function auditSource(req) {
return { ip: ip, username: req.user ? req.user.username : null, userId: req.user ? req.user.id : null };
}
function activate(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.username !== 'string') return next(new HttpError(400, 'username must be string'));
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'password must be string'));
if (typeof req.body.email !== 'string') return next(new HttpError(400, 'email must be string'));
if ('displayName' in req.body && typeof req.body.displayName !== 'string') return next(new HttpError(400, 'displayName must be string'));
var username = req.body.username;
var password = req.body.password;
var email = req.body.email;
var displayName = req.body.displayName || '';
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
debug('activate: username:%s ip:%s', username, ip);
cloudron.activate(username, password, email, displayName, ip, auditSource(req), function (error, info) {
if (error && error.reason === CloudronError.ALREADY_PROVISIONED) return next(new HttpError(409, 'Already setup'));
if (error && error.reason === CloudronError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
// only in caas case do we have to notify the api server about activation
if (config.provider() !== 'caas') return next(new HttpSuccess(201, info));
// Now let the api server know we got activated
superagent.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/setup/done').query({ setupToken: req.query.setupToken })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return next(new HttpError(500, error));
if (result.statusCode === 403) return next(new HttpError(403, 'Invalid token'));
if (result.statusCode === 409) return next(new HttpError(409, 'Already setup'));
if (result.statusCode !== 201) return next(new HttpError(500, result.text || 'Internal error'));
next(new HttpSuccess(201, info));
});
});
}
function restore(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (!req.body.backupConfig || typeof req.body.backupConfig !== 'object') return next(new HttpError(400, 'backupConfig is required'));
var backupConfig = req.body.backupConfig;
if (typeof backupConfig.provider !== 'string') return next(new HttpError(400, 'provider is required'));
if ('key' in backupConfig && typeof backupConfig.key !== 'string') return next(new HttpError(400, 'key must be a string'));
if (typeof backupConfig.format !== 'string') return next(new HttpError(400, 'format must be a string'));
if ('acceptSelfSignedCerts' in backupConfig && typeof backupConfig.acceptSelfSignedCerts !== 'boolean') return next(new HttpError(400, 'format must be a boolean'));
if (typeof req.body.backupId !== 'string') return next(new HttpError(400, 'backupId must be a string or null'));
if (typeof req.body.version !== 'string') return next(new HttpError(400, 'version must be a string'));
cloudron.restore(backupConfig, req.body.backupId, req.body.version, function (error) {
if (error && error.reason === CloudronError.ALREADY_SETUP) return next(new HttpError(409, error.message));
if (error && error.reason === CloudronError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === CloudronError.BAD_STATE) return next(new HttpError(409, error.message));
if (error && error.reason === CloudronError.EXTERNAL_ERROR) return next(new HttpError(402, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200));
});
}
function dnsSetup(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.provider !== 'string' || !req.body.provider) return next(new HttpError(400, 'provider is required'));
if (typeof req.body.domain !== 'string' || !req.body.domain) return next(new HttpError(400, 'domain is required'));
if (typeof req.body.adminFqdn !== 'string' || !req.body.domain) return next(new HttpError(400, 'adminFqdn is required'));
if ('zoneName' in req.body && typeof req.body.zoneName !== 'string') return next(new HttpError(400, 'zoneName must be a string'));
if (!req.body.config || typeof req.body.config !== 'object') return next(new HttpError(400, 'config must be an object'));
cloudron.dnsSetup(req.body.adminFqdn.toLowerCase(), req.body.domain.toLowerCase(), req.body.zoneName || '', req.body.provider, req.body.config, function (error) {
if (error && error.reason === CloudronError.ALREADY_SETUP) return next(new HttpError(409, error.message));
if (error && error.reason === CloudronError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200));
});
}
function setupTokenAuth(req, res, next) {
assert.strictEqual(typeof req.query, 'object');
if (config.provider() === 'caas') {
if (typeof req.query.setupToken !== 'string' || !req.query.setupToken) return next(new HttpError(400, 'setupToken must be a non empty string'));
superagent.get(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/setup/verify').query({ setupToken:req.query.setupToken })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return next(new HttpError(500, error));
if (result.statusCode === 403) return next(new HttpError(403, 'Invalid token'));
if (result.statusCode === 409) return next(new HttpError(409, 'Already setup'));
if (result.statusCode !== 200) return next(new HttpError(500, result.text || 'Internal error'));
next();
});
} else {
next();
}
}
function providerTokenAuth(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (config.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'));
if (result.text !== req.body.providerToken) return next(new HttpError(403, 'Invalid providerToken'));
next();
});
} else {
next();
}
}
function getStatus(req, res, next) {
cloudron.getStatus(function (error, status) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, status));
});
}
function getProgress(req, res, next) {
return next(new HttpSuccess(200, progress.getAll()));
}
@@ -89,9 +230,8 @@ function feedback(req, res, next) {
if (VALID_TYPES.indexOf(req.body.type) === -1) return next(new HttpError(400, 'unknown type'));
if (typeof req.body.subject !== 'string' || !req.body.subject) return next(new HttpError(400, 'subject must be string'));
if (typeof req.body.description !== 'string' || !req.body.description) return next(new HttpError(400, 'description must be string'));
if (req.body.appId && typeof req.body.appId !== 'string') return next(new HttpError(400, 'appId must be string'));
appstore.sendFeedback(_.extend(req.body, { email: req.user.email, displayName: req.user.displayName }), function (error) {
appstore.sendFeedback(_.extend(req.body, { email: req.user.alternateEmail || req.user.email, displayName: req.user.displayName }), function (error) {
if (error && error.reason === AppstoreError.BILLING_REQUIRED) return next(new HttpError(402, 'Login to App Store to create support tickets. You can also email support@cloudron.io'));
if (error) return next(new HttpError(503, 'Error contacting cloudron.io. Please email support@cloudron.io'));
@@ -165,3 +305,13 @@ function getLogStream(req, res, next) {
logStream.on('error', res.end.bind(res, null));
});
}
function sendTestMail(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (!req.body.email || typeof req.body.email !== 'string') return next(new HttpError(400, 'email must be a non-empty string'));
mailer.sendTestMail(req.body.email);
next(new HttpSuccess(202));
}
+6 -3
View File
@@ -9,14 +9,17 @@ var developer = require('../developer.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess;
function auditSource(req) {
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
return { ip: ip, username: req.user ? req.user.username : null, userId: req.user ? req.user.id : null };
}
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;
developer.issueDeveloperToken(user, ip, function (error, result) {
developer.issueDeveloperToken(user, auditSource(req), function (error, result) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { token: result.token, expiresAt: result.expiresAt }));
+4 -16
View File
@@ -26,13 +26,8 @@ function add(req, res, next) {
if ('fallbackCertificate' in req.body && typeof req.body.fallbackCertificate !== 'object') return next(new HttpError(400, 'fallbackCertificate must be a object with cert and key strings'));
if (req.body.fallbackCertificate && (!req.body.cert || typeof req.body.cert !== 'string')) return next(new HttpError(400, 'fallbackCertificate.cert must be a string'));
if (req.body.fallbackCertificate && (!req.body.key || typeof req.body.key !== 'string')) return next(new HttpError(400, 'fallbackCertificate.key must be a string'));
if ('tlsConfig' in req.body && typeof req.body.tlsConfig !== 'object') return next(new HttpError(400, 'tlsConfig must be a object with a provider string property'));
if (req.body.tlsConfig && (!req.body.tlsConfig.provider || typeof req.body.tlsConfig.provider !== 'string')) return next(new HttpError(400, 'tlsConfig.provider must be a string'));
// some DNS providers like DigitalOcean take a really long time to verify credentials (https://github.com/expressjs/timeout/issues/26)
req.clearTimeout();
domains.add(req.body.domain, req.body.zoneName || '', req.body.provider, req.body.config, req.body.fallbackCertificate || null, req.body.tlsConfig || { provider: 'letsencrypt-prod' }, function (error) {
domains.add(req.body.domain, req.body.zoneName || req.body.domain, req.body.provider, req.body.config, req.body.fallbackCertificate || null, function (error) {
if (error && error.reason === DomainError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
if (error && error.reason === DomainError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === DomainError.INVALID_PROVIDER) return next(new HttpError(400, error.message));
@@ -49,8 +44,6 @@ function get(req, res, next) {
if (error && error.reason === DomainError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
delete result.fallbackCertificate.key; // do not return the 'key'. in caas, this is private
next(new HttpSuccess(200, result));
});
}
@@ -72,13 +65,8 @@ function update(req, res, next) {
if ('fallbackCertificate' in req.body && typeof req.body.fallbackCertificate !== 'object') return next(new HttpError(400, 'fallbackCertificate must be a object with cert and key strings'));
if (req.body.fallbackCertificate && (!req.body.fallbackCertificate.cert || typeof req.body.fallbackCertificate.cert !== 'string')) return next(new HttpError(400, 'fallbackCertificate.cert must be a string'));
if (req.body.fallbackCertificate && (!req.body.fallbackCertificate.key || typeof req.body.fallbackCertificate.key !== 'string')) return next(new HttpError(400, 'fallbackCertificate.key must be a string'));
if ('tlsConfig' in req.body && typeof req.body.tlsConfig !== 'object') return next(new HttpError(400, 'tlsConfig must be a object with a provider string property'));
if (req.body.tlsConfig && (!req.body.tlsConfig.provider || typeof req.body.tlsConfig.provider !== 'string')) return next(new HttpError(400, 'tlsConfig.provider must be a string'));
// some DNS providers like DigitalOcean take a really long time to verify credentials (https://github.com/expressjs/timeout/issues/26)
req.clearTimeout();
domains.update(req.params.domain, req.body.provider, req.body.config, req.body.fallbackCertificate || null, req.body.tlsConfig || { provider: 'letsencrypt-prod' }, function (error) {
domains.update(req.params.domain, req.body.provider, req.body.config, req.body.fallbackCertificate || null, function (error) {
if (error && error.reason === DomainError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === DomainError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === DomainError.INVALID_PROVIDER) return next(new HttpError(400, error.message));
@@ -93,10 +81,10 @@ function del(req, res, next) {
domains.del(req.params.domain, function (error) {
if (error && error.reason === DomainError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === DomainError.IN_USE) return next(new HttpError(409, 'Domain is still in use. Remove all apps using this domain'));
if (error && error.reason === DomainError.IN_USE) return next(new HttpError(409, 'Domain is still in use'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(204));
next(new HttpSuccess(204, {}));
});
}
+1 -5
View File
@@ -15,14 +15,10 @@ function get(req, res, next) {
var perPage = typeof req.query.per_page !== 'undefined'? parseInt(req.query.per_page) : 25;
if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a postive number'));
if (req.query.actions && typeof req.query.actions !== 'string') return next(new HttpError(400, 'actions must be a comma separated string'));
if (req.query.action && typeof req.query.action !== 'string') return next(new HttpError(400, 'action must be a string'));
if (req.query.search && typeof req.query.search !== 'string') return next(new HttpError(400, 'search must be a string'));
var actions = req.query.actions ? req.query.actions.split(',').map(function (s) { return s.trim(); }) : [];
if (req.query.action) actions.push(req.query.action);
eventlog.getAllPaged(actions, req.query.search || null, page, perPage, function (error, result) {
eventlog.getAllPaged(req.query.action || null, req.query.search || null, page, perPage, function (error, result) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { eventlogs: result }));
-2
View File
@@ -12,9 +12,7 @@ exports = module.exports = {
graphs: require('./graphs.js'),
groups: require('./groups.js'),
oauth2: require('./oauth2.js'),
mail: require('./mail.js'),
profile: require('./profile.js'),
setup: require('./setup.js'),
sysadmin: require('./sysadmin.js'),
settings: require('./settings.js'),
ssh: require('./ssh.js'),
-311
View File
@@ -1,311 +0,0 @@
'use strict';
exports = module.exports = {
get: get,
add: add,
update: update,
del: del,
getStatus: getStatus,
setMailFromValidation: setMailFromValidation,
setCatchAllAddress: setCatchAllAddress,
setMailRelay: setMailRelay,
setMailEnabled: setMailEnabled,
sendTestMail: sendTestMail,
getMailboxes: getMailboxes,
getUserMailbox: getUserMailbox,
enableUserMailbox: enableUserMailbox,
disableUserMailbox: disableUserMailbox,
getAliases: getAliases,
setAliases: setAliases,
getLists: getLists,
getList: getList,
addList: addList,
removeList: removeList
};
var assert = require('assert'),
mail = require('../mail.js'),
MailError = mail.MailError,
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess;
function get(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
mail.get(req.params.domain, function (error, result) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, result));
});
}
function add(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.domain !== 'string') return next(new HttpError(400, 'domain must be a string'));
mail.add(req.body.domain, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === MailError.ALREADY_EXISTS) return next(new HttpError(409, 'domain already exists'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(201, { domain: req.body.domain }));
});
}
function update(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.domain, 'string');
mail.update(req.params.domain, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202));
});
}
function del(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
mail.del(req.params.domain, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === MailError.IN_USE) return next(new HttpError(409, 'Mail domain is still in use. Remove existing mailboxes'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(204));
});
}
function getStatus(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
// can take a while to query all the DNS entries
req.clearTimeout();
mail.getStatus(req.params.domain, function (error, records) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, records));
});
}
function setMailFromValidation(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.enabled !== 'boolean') return next(new HttpError(400, 'enabled is required'));
mail.setMailFromValidation(req.params.domain, req.body.enabled, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === MailError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202));
});
}
function setCatchAllAddress(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.body, 'object');
if (!req.body.address || !Array.isArray(req.body.address)) return next(new HttpError(400, 'address array is required'));
for (var i = 0; i < req.body.address.length; i++) {
if (typeof req.body.address[i] !== 'string') return next(new HttpError(400, 'address must be an array of string'));
}
mail.setCatchAllAddress(req.params.domain, req.body.address, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === MailError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202));
});
}
function setMailRelay(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider is required'));
if ('host' in req.body && typeof req.body.host !== 'string') return next(new HttpError(400, 'host must be a string'));
if ('port' in req.body && typeof req.body.port !== 'number') return next(new HttpError(400, 'port must be a string'));
if ('username' in req.body && typeof req.body.username !== 'string') return next(new HttpError(400, 'username must be a string'));
if ('password' in req.body && typeof req.body.password !== 'string') return next(new HttpError(400, 'password must be a string'));
mail.setMailRelay(req.params.domain, req.body, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === MailError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202));
});
}
function setMailEnabled(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.enabled !== 'boolean') return next(new HttpError(400, 'enabled is required'));
mail.setMailEnabled(req.params.domain, !!req.body.enabled, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === MailError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202));
});
}
function sendTestMail(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.body, 'object');
if (!req.body.to || typeof req.body.to !== 'string') return next(new HttpError(400, 'to must be a non-empty string'));
mail.sendTestMail(req.params.domain, req.body.to, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202));
});
}
function getMailboxes(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
mail.getMailboxes(req.params.domain, function (error, result) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { mailboxes: result }));
});
}
function getUserMailbox(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.params.userId, 'string');
mail.getUserMailbox(req.params.domain, req.params.userId, function (error, result) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { mailbox: result }));
});
}
function enableUserMailbox(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.params.userId, 'string');
mail.enableUserMailbox(req.params.domain, req.params.userId, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === MailError.ALREADY_EXISTS) return next(new HttpSuccess(201, {}));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(201, {}));
});
}
function disableUserMailbox(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.params.userId, 'string');
mail.disableUserMailbox(req.params.domain, req.params.userId, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpSuccess(201, {}));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(201, {}));
});
}
function getAliases(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.params.userId, 'string');
mail.getAliases(req.params.domain, req.params.userId, function (error, result) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { aliases: result }));
});
}
function setAliases(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.params.userId, 'string');
assert.strictEqual(typeof req.body, 'object');
if (!Array.isArray(req.body.aliases)) return next(new HttpError(400, 'aliases must be an array'));
for (var i = 0; i < req.body.aliases.length; i++) {
if (typeof req.body.aliases[i] !== 'string') return next(new HttpError(400, 'alias must be a string'));
}
mail.setAliases(req.params.domain, req.params.userId, req.body.aliases, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202));
});
}
function getLists(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
mail.getLists(req.params.domain, function (error, result) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { lists: result }));
});
}
function getList(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.params.groupId, 'string');
mail.getList(req.params.domain, req.params.groupId, function (error, result) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { list: result }));
});
}
function addList(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.groupId !== 'string') return next(new HttpError(400, 'groupId must be a string'));
mail.addList(req.params.domain, req.body.groupId, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === MailError.ALREADY_EXISTS) return next(new HttpError(409, 'list already exists'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(201, {}));
});
}
function removeList(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.params.groupId, 'string');
mail.removeList(req.params.domain, req.params.groupId, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(204));
});
}
+5 -6
View File
@@ -26,10 +26,9 @@ var apps = require('../apps'),
util = require('util'),
_ = require('underscore');
// appObject is optional here
function auditSource(req, appId, appObject) {
function auditSource(req, appId) {
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
return { authType: 'oauth', ip: ip, appId: appId, app: appObject };
return { authType: 'oauth', ip: ip, appId: appId };
}
// create OAuth 2.0 server
@@ -240,7 +239,7 @@ function loginForm(req, res) {
apps.get(result.appId, function (error, result) {
if (error) return sendErrorPageOrRedirect(req, res, 'Unknown Application for those OAuth credentials');
var applicationName = result.fqdn;
var applicationName = result.altDomain || result.intrinsicFqdn;
render(applicationName, '/api/v1/apps/' + result.id + '/icon');
});
});
@@ -448,7 +447,7 @@ var authorization = [
var type = req.oauth2.client.type;
if (type === clients.TYPE_EXTERNAL || type === clients.TYPE_BUILT_IN) {
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource(req, req.oauth2.client.appId), { userId: req.oauth2.user.id, user: user.removePrivateFields(req.oauth2.user) });
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource(req, req.oauth2.client.appId), { userId: req.oauth2.user.id });
return next();
}
@@ -459,7 +458,7 @@ var authorization = [
if (error) return sendError(req, res, 'Internal error');
if (!access) return sendErrorPageOrRedirect(req, res, 'No access to this app.');
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource(req, appObject.id, appObject), { userId: req.oauth2.user.id, user: user.removePrivateFields(req.oauth2.user) });
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource(req, appObject.id), { userId: req.oauth2.user.id });
next();
});
+2 -3
View File
@@ -25,7 +25,7 @@ function get(req, res, next) {
id: req.user.id,
username: req.user.username,
email: req.user.email,
fallbackEmail: req.user.fallbackEmail,
alternateEmail: req.user.alternateEmail,
admin: req.user.admin,
displayName: req.user.displayName
}));
@@ -36,10 +36,9 @@ function update(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if ('email' in req.body && typeof req.body.email !== 'string') return next(new HttpError(400, 'email must be string'));
if ('fallbackEmail' in req.body && typeof req.body.fallbackEmail !== 'string') return next(new HttpError(400, 'fallbackEmail must be string'));
if ('displayName' in req.body && typeof req.body.displayName !== 'string') return next(new HttpError(400, 'displayName must be string'));
var data = _.pick(req.body, 'email', 'fallbackEmail', 'displayName');
var data = _.pick(req.body, 'email', 'displayName');
user.update(req.user.id, data, auditSource(req), function (error) {
if (error && error.reason === UserError.BAD_FIELD) return next(new HttpError(400, error.message));
+142 -31
View File
@@ -1,11 +1,8 @@
'use strict';
exports = module.exports = {
getAppAutoupdatePattern: getAppAutoupdatePattern,
setAppAutoupdatePattern: setAppAutoupdatePattern,
getBoxAutoupdatePattern: getBoxAutoupdatePattern,
setBoxAutoupdatePattern: setBoxAutoupdatePattern,
getAutoupdatePattern: getAutoupdatePattern,
setAutoupdatePattern: setAutoupdatePattern,
getCloudronName: getCloudronName,
setCloudronName: setCloudronName,
@@ -13,58 +10,57 @@ exports = module.exports = {
getCloudronAvatar: getCloudronAvatar,
setCloudronAvatar: setCloudronAvatar,
getEmailStatus: getEmailStatus,
getBackupConfig: getBackupConfig,
setBackupConfig: setBackupConfig,
getTimeZone: getTimeZone,
setTimeZone: setTimeZone,
getMailConfig: getMailConfig,
setMailConfig: setMailConfig,
getMailRelay: getMailRelay,
setMailRelay: setMailRelay,
getCatchAllAddress: getCatchAllAddress,
setCatchAllAddress: setCatchAllAddress,
getMailFromValidation: getMailFromValidation,
setMailFromValidation: setMailFromValidation,
getAppstoreConfig: getAppstoreConfig,
setAppstoreConfig: setAppstoreConfig
setAppstoreConfig: setAppstoreConfig,
setAdminCertificate: setAdminCertificate
};
var assert = require('assert'),
certificates = require('../certificates.js'),
CertificatesError = require('../certificates.js').CertificatesError,
config = require('../config.js'),
email = require('../email.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
safe = require('safetydance'),
settings = require('../settings.js'),
SettingsError = settings.SettingsError;
function getAppAutoupdatePattern(req, res, next) {
settings.getAppAutoupdatePattern(function (error, pattern) {
function getAutoupdatePattern(req, res, next) {
settings.getAutoupdatePattern(function (error, pattern) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { pattern: pattern }));
});
}
function setAppAutoupdatePattern(req, res, next) {
function setAutoupdatePattern(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.pattern !== 'string') return next(new HttpError(400, 'pattern is required'));
settings.setAppAutoupdatePattern(req.body.pattern, function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200));
});
}
function getBoxAutoupdatePattern(req, res, next) {
settings.getBoxAutoupdatePattern(function (error, pattern) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { pattern: pattern }));
});
}
function setBoxAutoupdatePattern(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.pattern !== 'string') return next(new HttpError(400, 'pattern is required'));
settings.setBoxAutoupdatePattern(req.body.pattern, function (error) {
settings.setAutoupdatePattern(req.body.pattern, function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
@@ -114,6 +110,98 @@ function setTimeZone(req, res, next) {
});
}
function getMailConfig(req, res, next) {
settings.getMailConfig(function (error, mail) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, mail));
});
}
function setMailConfig(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.enabled !== 'boolean') return next(new HttpError(400, 'enabled is required'));
settings.setMailConfig({ enabled: req.body.enabled }, function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202));
});
}
function getMailFromValidation(req, res, next) {
settings.getMailFromValidation(function (error, enabled) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { enabled: enabled }));
});
}
function setMailFromValidation(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.enabled !== 'boolean') return next(new HttpError(400, 'enabled is required'));
settings.setMailFromValidation(req.body.enabled, function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202));
});
}
function getMailRelay(req, res, next) {
settings.getMailRelay(function (error, mail) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, mail));
});
}
function setMailRelay(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider is required'));
if ('host' in req.body && typeof req.body.host !== 'string') return next(new HttpError(400, 'host must be a string'));
if ('port' in req.body && typeof req.body.port !== 'number') return next(new HttpError(400, 'port must be a string'));
if ('username' in req.body && typeof req.body.username !== 'string') return next(new HttpError(400, 'username must be a string'));
if ('password' in req.body && typeof req.body.password !== 'string') return next(new HttpError(400, 'password must be a string'));
settings.setMailRelay(req.body, function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202));
});
}
function getCatchAllAddress(req, res, next) {
settings.getCatchAllAddress(function (error, address) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { address: address }));
});
}
function setCatchAllAddress(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (!req.body.address || !Array.isArray(req.body.address)) return next(new HttpError(400, 'address array is required'));
for (var i = 0; i < req.body.address.length; i++) {
if (typeof req.body.address[i] !== 'string') return next(new HttpError(400, 'address must be an array of string'));
}
settings.setCatchAllAddress(req.body.address, function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202));
});
}
function setCloudronAvatar(req, res, next) {
assert.strictEqual(typeof req.files, 'object');
@@ -139,6 +227,14 @@ function getCloudronAvatar(req, res, next) {
});
}
function getEmailStatus(req, res, next) {
email.getStatus(function (error, records) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, records));
});
}
function getBackupConfig(req, res, next) {
settings.getBackupConfig(function (error, config) {
if (error) return next(new HttpError(500, error));
@@ -196,3 +292,18 @@ function setAppstoreConfig(req, res, next) {
});
});
}
// only webadmin cert, until it can be treated just like a normal app
function setAdminCertificate(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (!req.body.cert || typeof req.body.cert !== 'string') return next(new HttpError(400, 'cert must be a string'));
if (!req.body.key || typeof req.body.key !== 'string') return next(new HttpError(400, 'key must be a string'));
certificates.setAdminCertificate(req.body.cert, req.body.key, function (error) {
if (error && error.reason === CertificatesError.INVALID_CERT) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, {}));
});
}
-154
View File
@@ -1,154 +0,0 @@
'use strict';
exports = module.exports = {
providerTokenAuth: providerTokenAuth,
setupTokenAuth: setupTokenAuth,
dnsSetup: dnsSetup,
activate: activate,
restore: restore,
getStatus: getStatus,
};
var assert = require('assert'),
caas = require('../caas.js'),
CaasError = require('../caas.js').CaasError,
config = require('../config.js'),
debug = require('debug')('box:routes/setup'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
setup = require('../setup.js'),
SetupError = require('../setup.js').SetupError,
superagent = require('superagent');
function auditSource(req) {
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
return { ip: ip, username: req.user ? req.user.username : null, userId: req.user ? req.user.id : null };
}
function providerTokenAuth(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (config.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'));
if (result.text !== req.body.providerToken) return next(new HttpError(403, 'Invalid providerToken'));
next();
});
} else {
next();
}
}
function setupTokenAuth(req, res, next) {
assert.strictEqual(typeof req.query, 'object');
if (config.provider() !== 'caas') return next();
if (typeof req.query.setupToken !== 'string' || !req.query.setupToken) return next(new HttpError(400, 'setupToken must be a non empty string'));
caas.verifySetupToken(req.query.setupToken, function (error) {
if (error && error.reason === CaasError.BAD_STATE) return next(new HttpError(409, 'Already setup'));
if (error && error.reason === CaasError.INVALID_TOKEN) return next(new HttpError(403, 'Invalid token'));
if (error && error.reason === CaasError.EXTERNAL_ERROR) return next(new HttpError(503, error.message));
if (error) return next(new HttpError(500, error));
next();
});
}
function dnsSetup(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.provider !== 'string' || !req.body.provider) return next(new HttpError(400, 'provider is required'));
if (typeof req.body.domain !== 'string' || !req.body.domain) return next(new HttpError(400, 'domain is required'));
if (typeof req.body.adminFqdn !== 'string' || !req.body.adminFqdn) return next(new HttpError(400, 'adminFqdn is required'));
if ('zoneName' in req.body && typeof req.body.zoneName !== 'string') return next(new HttpError(400, 'zoneName must be a string'));
if (!req.body.config || typeof req.body.config !== 'object') return next(new HttpError(400, 'config must be an object'));
if ('tlsConfig' in req.body && typeof req.body.tlsConfig !== 'object') return next(new HttpError(400, 'tlsConfig must be an object'));
if (req.body.tlsConfig && (!req.body.tlsConfig.provider || typeof req.body.tlsConfig.provider !== 'string')) return next(new HttpError(400, 'tlsConfig.provider must be a string'));
setup.dnsSetup(req.body.adminFqdn.toLowerCase(), req.body.domain.toLowerCase(), req.body.zoneName || '', req.body.provider, req.body.config, req.body.tlsConfig || { provider: 'letsencrypt-prod' }, function (error) {
if (error && error.reason === SetupError.ALREADY_SETUP) return next(new HttpError(409, error.message));
if (error && error.reason === SetupError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200));
});
}
function getStatus(req, res, next) {
setup.getStatus(function (error, status) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, status));
});
}
function activate(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.username !== 'string') return next(new HttpError(400, 'username must be string'));
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'password must be string'));
if (typeof req.body.email !== 'string') return next(new HttpError(400, 'email must be string'));
if ('displayName' in req.body && typeof req.body.displayName !== 'string') return next(new HttpError(400, 'displayName must be string'));
var username = req.body.username;
var password = req.body.password;
var email = req.body.email;
var displayName = req.body.displayName || '';
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
debug('activate: username:%s ip:%s', username, ip);
setup.activate(username, password, email, displayName, ip, auditSource(req), function (error, info) {
if (error && error.reason === SetupError.ALREADY_PROVISIONED) return next(new HttpError(409, 'Already setup'));
if (error && error.reason === SetupError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
// only in caas case do we have to notify the api server about activation
if (config.provider() !== 'caas') return next(new HttpSuccess(201, info));
caas.setupDone(req.query.setupToken, function (error) {
if (error && error.reason === CaasError.BAD_STATE) return next(new HttpError(409, 'Already setup'));
if (error && error.reason === CaasError.INVALID_TOKEN) return next(new HttpError(403, 'Invalid token'));
if (error && error.reason === CaasError.EXTERNAL_ERROR) return next(new HttpError(503, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(201, info));
});
});
}
function restore(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (!req.body.backupConfig || typeof req.body.backupConfig !== 'object') return next(new HttpError(400, 'backupConfig is required'));
var backupConfig = req.body.backupConfig;
if (typeof backupConfig.provider !== 'string') return next(new HttpError(400, 'provider is required'));
if ('key' in backupConfig && typeof backupConfig.key !== 'string') return next(new HttpError(400, 'key must be a string'));
if (typeof backupConfig.format !== 'string') return next(new HttpError(400, 'format must be a string'));
if ('acceptSelfSignedCerts' in backupConfig && typeof backupConfig.acceptSelfSignedCerts !== 'boolean') return next(new HttpError(400, 'format must be a boolean'));
if (typeof req.body.backupId !== 'string') return next(new HttpError(400, 'backupId must be a string or null'));
if (typeof req.body.version !== 'string') return next(new HttpError(400, 'version must be a string'));
setup.restore(backupConfig, req.body.backupId, req.body.version, function (error) {
if (error && error.reason === SetupError.ALREADY_SETUP) return next(new HttpError(409, error.message));
if (error && error.reason === SetupError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === SetupError.BAD_STATE) return next(new HttpError(409, error.message));
if (error && error.reason === SetupError.EXTERNAL_ERROR) return next(new HttpError(402, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200));
});
}
+120 -116
View File
@@ -9,12 +9,12 @@
var appdb = require('../../appdb.js'),
apps = require('../../apps.js'),
assert = require('assert'),
path = require('path'),
async = require('async'),
child_process = require('child_process'),
clients = require('../../clients.js'),
config = require('../../config.js'),
constants = require('../../constants.js'),
apphealthmonitor = require('../../apphealthmonitor.js'),
database = require('../../database.js'),
docker = require('../../docker.js').connection,
expect = require('expect.js'),
@@ -22,10 +22,10 @@ var appdb = require('../../appdb.js'),
hock = require('hock'),
http = require('http'),
https = require('https'),
js2xml = require('js2xmlparser').parse,
ldap = require('../../ldap.js'),
net = require('net'),
nock = require('nock'),
path = require('path'),
paths = require('../../paths.js'),
safe = require('safetydance'),
server = require('../../server.js'),
@@ -41,23 +41,15 @@ var SERVER_URL = 'http://localhost:' + config.get('port');
// Test image information
var TEST_IMAGE_REPO = 'cloudron/test';
var TEST_IMAGE_TAG = '25.4.0';
var TEST_IMAGE_TAG = '25.2.0';
var TEST_IMAGE = TEST_IMAGE_REPO + ':' + TEST_IMAGE_TAG;
const DOMAIN_0 = {
domain: 'example-apps-test.com',
zoneName: 'example-apps-test.com',
config: {},
provider: 'noop',
fallbackCertificate: null,
tlsConfig: { provider: 'fallback' }
};
const CLOUDRON_ID = 'somecloudronid';
// var TEST_IMAGE_ID = child_process.execSync('docker inspect --format={{.Id}} ' + TEST_IMAGE).toString('utf8').trim();
var APP_STORE_ID = 'test', APP_ID;
var APP_LOCATION = 'appslocation';
var APP_DOMAIN = 'example-apps-test.com';
var APP_LOCATION_2 = 'appslocationtwo';
var APP_DOMAIN_2 = 'example-apps-test.com';
var APP_LOCATION_NEW = 'appslocationnew';
var APP_MANIFEST = JSON.parse(fs.readFileSync(__dirname + '/../../../../test-app/CloudronManifest.json', 'utf8'));
@@ -66,19 +58,13 @@ APP_MANIFEST.dockerImage = TEST_IMAGE;
var APP_MANIFEST_1 = JSON.parse(fs.readFileSync(__dirname + '/../../../../test-app/CloudronManifest.json', 'utf8'));
APP_MANIFEST_1.dockerImage = TEST_IMAGE;
const USERNAME = 'superadmin';
const PASSWORD = 'Foobar?1337';
const EMAIL ='admin@me.com';
const USER_1_APPSTORE_TOKEN = 'appstoretoken';
const USERNAME_1 = 'user';
const EMAIL_1 ='user@me.com';
var user_1_id = null;
// authentication token
var token = null;
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='admin@me.com';
var USER_1_ID = null, USERNAME_1 = 'user', EMAIL_1 ='user@me.com';
var token = null; // authentication token
var token_1 = null;
var awsHostedZones;
function startDockerProxy(interceptor, callback) {
assert.strictEqual(typeof interceptor, 'function');
@@ -120,7 +106,6 @@ function checkAddons(appEntry, done) {
delete body.recvmail; // unclear why dovecot mail delivery won't work
delete body.stdenv; // cannot access APP_ORIGIN
delete body.email; // sieve will fail not sure why yet
for (var key in body) {
if (body[key] !== 'OK') return callback('Not done yet: ' + JSON.stringify(body));
@@ -166,30 +151,44 @@ function startBox(done) {
safe.fs.unlinkSync(paths.INFRA_VERSION_FILE);
child_process.execSync('docker ps -qa | xargs --no-run-if-empty docker rm -f');
config.setFqdn(APP_DOMAIN);
config.setZoneName('foobar.com');
awsHostedZones = {
HostedZones: [{
Id: '/hostedzone/ZONEID',
Name: config.zoneName() + '.',
CallerReference: '305AFD59-9D73-4502-B020-F4E6F889CB30',
ResourceRecordSetCount: 2,
ChangeInfo: {
Id: '/change/CKRTFJA0ANHXB',
Status: 'INSYNC'
}
}],
IsTruncated: false,
MaxItems: '100'
};
async.series([
// first clear, then start server. otherwise, taskmanager spins up tasks for obsolete appIds
database.initialize,
database._clear,
server.start,
server.start.bind(server),
ldap.start,
function (callback) {
superagent.post(SERVER_URL + '/api/v1/cloudron/dns_setup')
.send({ provider: 'noop', domain: DOMAIN_0.domain, adminFqdn: 'my.' + DOMAIN_0.domain, config: DOMAIN_0.config, tlsConfig: DOMAIN_0.tlsConfig })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(200);
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
callback();
});
},
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();
expect(result.statusCode).to.eql(201);
expect(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
// stash for further use
token = result.body.token;
@@ -205,7 +204,7 @@ function startBox(done) {
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
user_1_id = res.body.id;
USER_1_ID = res.body.id;
callback(null);
});
@@ -215,7 +214,7 @@ function startBox(done) {
token_1 = tokendb.generateToken();
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
tokendb.add(token_1, user_1_id, 'test-client-id', Date.now() + 1000000, '*', callback);
tokendb.add(token_1, USER_1_ID, 'test-client-id', Date.now() + 100000, '*', callback);
},
function (callback) {
@@ -233,7 +232,11 @@ function startBox(done) {
}
return false;
}, callback);
}
},
settings.setDnsConfig.bind(null, { provider: 'route53', accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey', endpoint: 'http://localhost:5353' }, config.fqdn(), config.zoneName()),
settings.setTlsConfig.bind(null, { provider: 'caas' }),
settings.setBackupConfig.bind(null, { provider: 'caas', token: 'BACKUP_TOKEN', bucket: 'Bucket', prefix: 'Prefix' })
], function (error) {
if (error) return done(error);
@@ -250,7 +253,6 @@ function stopBox(done) {
// db is not cleaned up here since it's too late to call it after server.stop. if called before server.stop taskmanager apptasks are unhappy :/
async.series([
apphealthmonitor.stop,
taskmanager.stopPendingTasks,
taskmanager.waitForPendingTasks,
appdb._clear,
@@ -319,24 +321,13 @@ describe('App API', function () {
});
});
it('app install fails - missing domain', function (done) {
it('app install fails - invalid location', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ manifest: APP_MANIFEST, location: 'some', accessRestriction: null })
.send({ manifest: APP_MANIFEST, location: '!awesome', accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('domain is required');
done();
});
});
it('app install fails - non-existing domain', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ manifest: APP_MANIFEST, location: 'some', accessRestriction: null, domain: 'doesnotexist.com' })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
expect(res.body.message).to.eql('No such domain');
expect(res.body.message).to.eql('Hostname can only contain alphanumerics and hyphen');
done();
});
});
@@ -344,7 +335,7 @@ describe('App API', function () {
it('app install fails - invalid location type', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ manifest: APP_MANIFEST, location: 42, accessRestriction: null, domain: DOMAIN_0.domain })
.send({ manifest: APP_MANIFEST, location: 42, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('location is required');
@@ -355,7 +346,7 @@ describe('App API', function () {
it('app install fails - reserved admin location', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ manifest: APP_MANIFEST, location: 'my', accessRestriction: null, domain: DOMAIN_0.domain })
.send({ manifest: APP_MANIFEST, location: 'my', accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('my is reserved');
@@ -366,7 +357,7 @@ describe('App API', function () {
it('app install fails - reserved api location', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ manifest: APP_MANIFEST, location: constants.API_LOCATION, accessRestriction: null, domain: DOMAIN_0.domain })
.send({ manifest: APP_MANIFEST, location: constants.API_LOCATION, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql(constants.API_LOCATION + ' is reserved');
@@ -377,7 +368,7 @@ describe('App API', function () {
it('app install fails - portBindings must be object', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ manifest: APP_MANIFEST, location: APP_LOCATION, portBindings: 23, accessRestriction: null, domain: DOMAIN_0.domain })
.send({ manifest: APP_MANIFEST, location: APP_LOCATION, portBindings: 23, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('portBindings must be an object');
@@ -388,7 +379,7 @@ describe('App API', function () {
it('app install fails - accessRestriction is required', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ manifest: APP_MANIFEST, location: APP_LOCATION, portBindings: {}, domain: DOMAIN_0.domain })
.send({ manifest: APP_MANIFEST, location: APP_LOCATION, portBindings: {} })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('accessRestriction is required');
@@ -399,7 +390,7 @@ describe('App API', function () {
it('app install fails - accessRestriction type is wrong', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ manifest: APP_MANIFEST, location: APP_LOCATION, portBindings: {}, accessRestriction: '', domain: DOMAIN_0.domain })
.send({ manifest: APP_MANIFEST, location: APP_LOCATION, portBindings: {}, accessRestriction: '' })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('accessRestriction is required');
@@ -410,7 +401,7 @@ describe('App API', function () {
it('app install fails for non admin', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token_1 })
.send({ manifest: APP_MANIFEST, location: APP_LOCATION, portBindings: null, accessRestriction: null, domain: DOMAIN_0.domain })
.send({ manifest: APP_MANIFEST, location: APP_LOCATION, portBindings: null, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(403);
done();
@@ -422,9 +413,9 @@ describe('App API', function () {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, location: APP_LOCATION, portBindings: null, domain: DOMAIN_0.domain, accessRestriction: { users: [ 'someuser' ], groups: [] } })
.send({ appStoreId: APP_STORE_ID, location: APP_LOCATION, portBindings: null, accessRestriction: { users: [ 'someuser' ], groups: [] } })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
expect(res.statusCode).to.equal(400);
expect(fake.isDone()).to.be.ok();
done();
});
@@ -432,45 +423,40 @@ describe('App API', function () {
it('app install fails due to purchase failure', function (done) {
var fake1 = nock(config.apiServerOrigin()).get('/api/v1/apps/test').reply(200, { manifest: APP_MANIFEST });
var fake2 = nock(config.apiServerOrigin()).post('/api/v1/exchangeBoxTokenWithUserToken?token=APPSTORE_TOKEN').reply(403);
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, location: APP_LOCATION, domain: DOMAIN_0.domain, portBindings: null, accessRestriction: null })
.send({ appStoreId: APP_STORE_ID, location: APP_LOCATION, portBindings: null, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(402);
expect(res.statusCode).to.equal(503);
expect(fake1.isDone()).to.be.ok();
expect(fake2.isDone()).to.be.ok();
done();
});
});
it('app install succeeds with purchase', function (done) {
var fake1 = nock(config.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/users/' + user_1_id + '/cloudrons') >= 0; }, { 'domain': DOMAIN_0.domain }).reply(201, { cloudron: { id: CLOUDRON_ID } });
var fake2 = nock(config.apiServerOrigin()).get('/api/v1/apps/' + APP_STORE_ID).reply(200, { manifest: APP_MANIFEST });
var fake3 = nock(config.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/users/' + user_1_id + '/cloudrons/' + CLOUDRON_ID + '/apps/') >= 0; }, { 'appstoreId': APP_STORE_ID }).reply(201, { });
settings.setAppstoreConfig({ userId: user_1_id, token: USER_1_APPSTORE_TOKEN }, function (error) {
if (error) return done(error);
var fake1 = nock(config.apiServerOrigin()).get('/api/v1/apps/' + APP_STORE_ID).reply(200, { manifest: APP_MANIFEST });
var fake2 = nock(config.apiServerOrigin()).post('/api/v1/exchangeBoxTokenWithUserToken?token=APPSTORE_TOKEN').reply(201, { userId: 'USER_ID', cloudronId: 'CLOUDRON_ID', token: 'ACCESS_TOKEN' });
var fake3 = nock(config.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/users/USER_ID/cloudrons/CLOUDRON_ID/apps/') >= 0; }, { 'appstoreId': APP_STORE_ID }).reply(201, { });
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, location: APP_LOCATION, portBindings: null, accessRestriction: { users: [ 'someuser' ], groups: [] } })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
expect(res.body.id).to.be.a('string');
APP_ID = res.body.id;
expect(fake1.isDone()).to.be.ok();
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, location: APP_LOCATION, domain: DOMAIN_0.domain, portBindings: null, accessRestriction: { users: [ 'someuser' ], groups: [] } })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
expect(res.body.id).to.be.a('string');
APP_ID = res.body.id;
expect(fake2.isDone()).to.be.ok();
expect(fake3.isDone()).to.be.ok();
done();
});
done();
});
});
it('app install fails because of conflicting location', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ manifest: APP_MANIFEST, location: APP_LOCATION, domain: DOMAIN_0.domain, portBindings: null, accessRestriction: null })
.send({ manifest: APP_MANIFEST, location: APP_LOCATION, portBindings: null, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(409);
done();
@@ -560,33 +546,32 @@ describe('App API', function () {
});
it('can uninstall app', function (done) {
var fake1 = nock(config.apiServerOrigin()).get(function (uri) { return uri.indexOf('/api/v1/users/' + user_1_id + '/cloudrons/' + CLOUDRON_ID + '/apps/') >= 0; }).reply(200, { });
var fake2 = nock(config.apiServerOrigin()).delete(function (uri) { return uri.indexOf('/api/v1/users/' + user_1_id + '/cloudrons/' + CLOUDRON_ID + '/apps/') >= 0; }).reply(204, { });
var fake1 = nock(config.apiServerOrigin()).post('/api/v1/exchangeBoxTokenWithUserToken?token=APPSTORE_TOKEN').reply(201, { userId: 'USER_ID', cloudronId: 'CLOUDRON_ID', token: 'ACCESS_TOKEN' });
var fake2 = nock(config.apiServerOrigin()).get(function (uri) { return uri.indexOf('/api/v1/users/USER_ID/cloudrons/CLOUDRON_ID/apps/') >= 0; }).reply(200, { });
var fake3 = nock(config.apiServerOrigin()).delete(function (uri) { return uri.indexOf('/api/v1/users/USER_ID/cloudrons/CLOUDRON_ID/apps/') >= 0; }).reply(204, { });
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/uninstall')
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
expect(fake1.isDone()).to.be.ok();
expect(fake2.isDone()).to.be.ok();
done();
});
});
it('app install succeeds again', function (done) {
var fake1 = nock(config.apiServerOrigin()).get('/api/v1/apps/' + APP_STORE_ID).reply(200, { manifest: APP_MANIFEST });
var fake2 = nock(config.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/users/' + user_1_id + '/cloudrons/' + CLOUDRON_ID + '/apps/') >= 0; }, { 'appstoreId': APP_STORE_ID }).reply(201, { });
var fake2 = nock(config.apiServerOrigin()).post('/api/v1/exchangeBoxTokenWithUserToken?token=APPSTORE_TOKEN').reply(201, { userId: 'USER_ID', cloudronId: 'CLOUDRON_ID', token: 'ACCESS_TOKEN' });
var fake3 = nock(config.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/users/USER_ID/cloudrons/CLOUDRON_ID/apps/') >= 0; }, { 'appstoreId': APP_STORE_ID }).reply(201, { });
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, location: APP_LOCATION_2, domain: DOMAIN_0.domain, portBindings: null, accessRestriction: null })
.send({ appStoreId: APP_STORE_ID, location: APP_LOCATION_2, portBindings: null, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
expect(res.body.id).to.be.a('string');
APP_ID = res.body.id;
expect(fake1.isDone()).to.be.ok();
expect(fake2.isDone()).to.be.ok();
done();
});
});
@@ -605,7 +590,7 @@ describe('App API', function () {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ manifest: APP_MANIFEST, location: APP_LOCATION+APP_LOCATION, domain: DOMAIN_0.domain, portBindings: null, accessRestriction: null })
.send({ manifest: APP_MANIFEST, location: APP_LOCATION+APP_LOCATION, portBindings: null, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
expect(res.body.id).to.be.a('string');
@@ -629,11 +614,13 @@ describe('App installation', function () {
this.timeout(100000);
var apiHockInstance = hock.createHock({ throwOnUnmatched: false }), apiHockServer;
var awsHockInstance = hock.createHock({ throwOnUnmatched: false }), awsHockServer;
// *.foobar.com
var validCert1, validKey1;
before(function (done) {
child_process.execSync('openssl req -subj "/CN=*.' + DOMAIN_0.domain + '/O=My Company Name LTD./C=US" -new -newkey rsa:2048 -days 365 -nodes -x509 -keyout /tmp/server.key -out /tmp/server.crt');
child_process.execSync('openssl req -subj "/CN=*.foobar.com/O=My Company Name LTD./C=US" -new -newkey rsa:2048 -days 365 -nodes -x509 -keyout /tmp/server.key -out /tmp/server.crt');
validKey1 = fs.readFileSync('/tmp/server.key', 'utf8');
validCert1 = fs.readFileSync('/tmp/server.crt', 'utf8');
@@ -641,37 +628,54 @@ describe('App installation', function () {
async.series([
startBox,
apphealthmonitor.start,
function (callback) {
apiHockInstance
.get('/api/v1/apps/' + APP_STORE_ID + '/versions/' + APP_MANIFEST.version + '/icon')
.replyWithFile(200, path.resolve(__dirname, '../../../assets/avatar.png'));
.replyWithFile(200, path.resolve(__dirname, '../../../webadmin/src/img/appicon_fallback.png'));
var port = parseInt(url.parse(config.apiServerOrigin()).port, 10);
apiHockServer = http.createServer(apiHockInstance.handler).listen(port, callback);
},
settings.setDnsConfig.bind(null, { provider: 'route53', accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey', endpoint: 'http://localhost:5353' }, config.fqdn(), config.zoneName()),
settings.setTlsConfig.bind(null, { provider: 'caas' }),
function (callback) {
var fake1 = nock(config.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/users/' + user_1_id + '/cloudrons') >= 0; }, { 'domain': DOMAIN_0.domain }).reply(201, { cloudron: { id: CLOUDRON_ID } });
settings.setAppstoreConfig({ userId: user_1_id, token: USER_1_APPSTORE_TOKEN }, function (error) {
if (error) return callback(error);
awsHockInstance
.get('/2013-04-01/hostedzone')
.max(Infinity)
.reply(200, js2xml('ListHostedZonesResponse', awsHostedZones, { wrapHandlers: { HostedZones: () => 'HostedZone'} }), { 'Content-Type': 'application/xml' })
.filteringPathRegEx(/name=[^&]*/, 'name=location')
.get('/2013-04-01/hostedzone/ZONEID/rrset?maxitems=1&name=location&type=A')
.max(Infinity)
.reply(200, js2xml('ListResourceRecordSetsResponse', { ResourceRecordSets: [ ] }, { 'Content-Type': 'application/xml' }))
.filteringRequestBody(function (unusedBody) { return ''; }) // strip out body
.post('/2013-04-01/hostedzone/ZONEID/rrset/')
.max(Infinity)
.reply(200, js2xml('ChangeResourceRecordSetsResponse', { ChangeInfo: { Id: 'dnsrecordid', Status: 'INSYNC' } }), { 'Content-Type': 'application/xml' });
expect(fake1.isDone()).to.be.ok();
callback();
});
awsHockServer = http.createServer(awsHockInstance.handler).listen(5353, callback);
}
], done);
});
after(stopBox);
after(function (done) {
APP_ID = null;
async.series([
apiHockServer.close.bind(apiHockServer),
awsHockServer.close.bind(awsHockServer),
stopBox
], done);
});
var appResult = null, appEntry = null;
it('can install test app', function (done) {
var fake1 = nock(config.apiServerOrigin()).get('/api/v1/apps/' + APP_STORE_ID).reply(200, { manifest: APP_MANIFEST });
var fake2 = nock(config.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/users/' + user_1_id + '/cloudrons/' + CLOUDRON_ID + '/apps/') >= 0; }, { 'appstoreId': APP_STORE_ID }).reply(201, { });
var fake2 = nock(config.apiServerOrigin()).post('/api/v1/exchangeBoxTokenWithUserToken?token=APPSTORE_TOKEN').reply(201, { userId: 'USER_ID', cloudronId: 'CLOUDRON_ID', token: 'ACCESS_TOKEN' });
var fake3 = nock(config.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/users/USER_ID/cloudrons/CLOUDRON_ID/apps/') >= 0; }, { 'appstoreId': APP_STORE_ID }).reply(201, { });
var count = 0;
function checkInstallStatus() {
@@ -688,11 +692,10 @@ describe('App installation', function () {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, location: APP_LOCATION, domain: DOMAIN_0.domain, portBindings: { ECHO_SERVER_PORT: 7171 }, accessRestriction: null })
.send({ appStoreId: APP_STORE_ID, location: APP_LOCATION, portBindings: { ECHO_SERVER_PORT: 7171 }, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
expect(fake1.isDone()).to.be.ok();
expect(fake2.isDone()).to.be.ok();
APP_ID = res.body.id;
checkInstallStatus();
});
@@ -721,8 +724,8 @@ describe('App installation', function () {
expect(data.Config.Env).to.contain('WEBADMIN_ORIGIN=' + config.adminOrigin());
expect(data.Config.Env).to.contain('API_ORIGIN=' + config.adminOrigin());
expect(data.Config.Env).to.contain('CLOUDRON=1');
expect(data.Config.Env).to.contain('APP_ORIGIN=https://' + APP_LOCATION + '.' + DOMAIN_0.domain);
expect(data.Config.Env).to.contain('APP_DOMAIN=' + APP_LOCATION + '.' + DOMAIN_0.domain);
expect(data.Config.Env).to.contain('APP_ORIGIN=https://' + APP_LOCATION + '.' + APP_DOMAIN);
expect(data.Config.Env).to.contain('APP_DOMAIN=' + APP_LOCATION + '.' + APP_DOMAIN);
// Hostname must not be set of app fqdn or app location!
expect(data.Config.Hostname).to.not.contain(APP_LOCATION);
expect(data.Config.Env).to.contain('ECHO_SERVER_PORT=7171');
@@ -1100,8 +1103,9 @@ describe('App installation', function () {
});
it('can uninstall app', function (done) {
var fake1 = nock(config.apiServerOrigin()).get(function (uri) { return uri.indexOf('/api/v1/users/' + user_1_id + '/cloudrons/' + CLOUDRON_ID + '/apps/') >= 0; }).reply(200, { });
var fake2 = nock(config.apiServerOrigin()).delete(function (uri) { return uri.indexOf('/api/v1/users/' + user_1_id + '/cloudrons/' + CLOUDRON_ID + '/apps/') >= 0; }).reply(204, { });
var fake1 = nock(config.apiServerOrigin()).post('/api/v1/exchangeBoxTokenWithUserToken?token=APPSTORE_TOKEN').reply(201, { userId: 'USER_ID', cloudronId: 'CLOUDRON_ID', token: 'ACCESS_TOKEN' });
var fake2 = nock(config.apiServerOrigin()).get(function (uri) { return uri.indexOf('/api/v1/users/USER_ID/cloudrons/CLOUDRON_ID/apps/') >= 0; }).reply(200, { });
var fake3 = nock(config.apiServerOrigin()).delete(function (uri) { return uri.indexOf('/api/v1/users/USER_ID/cloudrons/CLOUDRON_ID/apps/') >= 0; }).reply(204, { });
var count = 0;
function checkUninstallStatus() {
@@ -1119,10 +1123,6 @@ describe('App installation', function () {
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
expect(fake1.isDone()).to.be.ok();
expect(fake2.isDone()).to.be.ok();
checkUninstallStatus();
});
});
@@ -1148,7 +1148,11 @@ describe('App installation', function () {
it('uninstalled - unregistered subdomain', function (done) {
apiHockInstance.done(function (error) { // checks if all the apiHockServer APIs were called
expect(!error).to.be.ok();
done();
awsHockInstance.done(function (error) {
expect(!error).to.be.ok();
done();
});
});
});
+22 -17
View File
@@ -9,44 +9,40 @@ var appdb = require('../../appdb.js'),
async = require('async'),
config = require('../../config.js'),
database = require('../../database.js'),
domains = require('../../domains.js'),
expect = require('expect.js'),
nock = require('nock'),
superagent = require('superagent'),
server = require('../../server.js'),
settings = require('../../settings.js');
const SERVER_URL = 'http://localhost:' + config.get('port');
const USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
const DOMAIN_0 = {
domain: 'example-backups-test.com',
zoneName: 'example-backups-test.com',
config: {},
provider: 'noop',
fallbackCertificate: null,
tlsConfig: { provider: 'fallback' }
};
var SERVER_URL = 'http://localhost:' + config.get('port');
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var token = null;
function setup(done) {
nock.cleanAll();
config._reset();
config.setVersion('1.2.3');
config.setFqdn('example-backups-test.com');
async.series([
server.start,
server.start.bind(server),
database._clear,
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0.zoneName, DOMAIN_0.provider, DOMAIN_0.config, DOMAIN_0.fallbackCertificate, DOMAIN_0.tlsConfig),
function createAdmin(callback) {
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
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();
expect(result.statusCode).to.eql(201);
expect(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
// stash token for further use
token = result.body.token;
@@ -57,7 +53,7 @@ function setup(done) {
function addApp(callback) {
var manifest = { version: '0.0.1', manifestVersion: 1, dockerImage: 'foo', healthCheckPath: '/', httpPort: 3, title: 'ok', addons: { } };
appdb.add('appid', 'appStoreId', manifest, 'location', DOMAIN_0.domain, [ ] /* portBindings */, { }, callback);
appdb.add('appid', 'appStoreId', manifest, 'location', config.fqdn(), [ ] /* portBindings */, { }, callback);
},
function createSettings(callback) {
@@ -75,6 +71,9 @@ function cleanup(done) {
}
describe('Backups API', function () {
var scope1 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=BACKUP_TOKEN')
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey' } }, { 'Content-Type': 'application/json' });
before(setup);
after(cleanup);
@@ -103,7 +102,13 @@ describe('Backups API', function () {
.end(function (error, result) {
expect(result.statusCode).to.equal(202);
done();
function checkAppstoreServerCalled() {
if (scope1.isDone()) return done();
setTimeout(checkAppstoreServerCalled, 100);
}
checkAppstoreServerCalled();
});
});
});

Some files were not shown because too many files have changed in this diff Show More