diff --git a/CHANGES b/CHANGES index be4fa4cb7..c447c83db 100644 --- a/CHANGES +++ b/CHANGES @@ -1111,3 +1111,32 @@ * Update node to 6.11.5 * Display package version of installed apps in the info dialog +[1.8.1] +* Update node modules +* Allow a restore operation if app is already restoring +* Remove pre-install bundle support since it was hardly used +* Make the test email mail address configurable +* Allow admins to access all apps +* Send feedback via appstore API (instead of email) +* Show documentation URL in the app info dialog +* Update Lets Encrypt agrement URL (https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf) + +[1.8.2] +* Update node modules +* Allow a restore operation if app is already restoring +* Remove pre-install bundle support since it was hardly used +* Make the test email mail address configurable +* Allow admins to access all apps +* Send feedback via appstore API (instead of email) +* Show documentation URL in the app info dialog +* Update Lets Encrypt agrement URL (https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf) + +[1.8.3] +* Ensure domain database record exists + +[1.9.0] +* Prepare database for multidomain +* Add Cloudron restore UI +* Do not put app in errored state if backup fails +* Display backup progress in CaaS + diff --git a/LICENSE b/LICENSE index 1e579d4d6..9db967735 100644 --- a/LICENSE +++ b/LICENSE @@ -630,7 +630,7 @@ state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. box - Copyright (C) 2016 Cloudron UG + Copyright (C) 2016,2017 Cloudron UG This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published diff --git a/README.md b/README.md index fb28cc321..5e17e83ca 100644 --- a/README.md +++ b/README.md @@ -33,9 +33,9 @@ or [pay us a coffee](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_ * Trivially migrate to another server keeping your apps and data (for example, switch your infrastructure provider or move to a bigger server). -* Comprehensive [REST API](https://cloudron.io/references/api.html). +* Comprehensive [REST API](https://cloudron.io/documentation/developer/api/). -* [CLI](https://git.cloudron.io/cloudron/cloudron-cli) to configure apps. +* [CLI](https://cloudron.io/documentation/cli/) to configure apps. * Alerts, audit logs, graphs, dns management ... and much more @@ -49,17 +49,12 @@ You can install the Cloudron platform on your own server or get a managed server from cloudron.io. In either case, the Cloudron platform will keep your server and apps up-to-date and secure. -* [Selfhosting](https://cloudron.io/references/selfhosting.html) - [Pricing](https://cloudron.io/pricing.html) +* [Selfhosting](https://cloudron.io/documentation/installation/) - [Pricing](https://cloudron.io/pricing.html) * [Managed Hosting](https://cloudron.io/managed.html) -The wiki has instructions on how you can install and update the Cloudron and the -apps from source. - ## Documentation -* [User manual](https://cloudron.io/references/usermanual.html) -* [Developer docs](https://cloudron.io/documentation.html) -* [Architecture](https://cloudron.io/references/architecture.html) +* [Documentation](https://cloudron.io/documentation/) ## Related repos @@ -69,12 +64,12 @@ the containers in the Cloudron. The [graphite repo](https://git.cloudron.io/cloudron/docker-graphite) contains the graphite code that collects metrics for graphs. -The addons are located in separate repositories +The addons are located in separate repositories: + * [Redis](https://git.cloudron.io/cloudron/redis-addon) * [Postgresql](https://git.cloudron.io/cloudron/postgresql-addon) * [MySQL](https://git.cloudron.io/cloudron/mysql-addon) * [Mongodb](https://git.cloudron.io/cloudron/mongodb-addon) -* [Mail](https://git.cloudron.io/cloudron/mail-addon) ## Community diff --git a/gulpfile.js b/gulpfile.js index d9caf876a..12c94406b 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -50,7 +50,7 @@ if (argv.help || argv.h) { process.exit(1); } -gulp.task('js', ['js-index', 'js-setup', 'js-setupdns', 'js-update'], function () {}); +gulp.task('js', ['js-index', 'js-setup', 'js-setupdns', 'js-restore', 'js-update'], function () {}); var oauth = { clientId: argv.clientId || 'cid-webadmin', @@ -122,6 +122,23 @@ gulp.task('js-setupdns', function () { .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(); @@ -191,6 +208,7 @@ gulp.task('watch', ['default'], function () { 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/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']); }); diff --git a/migrations/20160208164735-groups-add-admin.js b/migrations/20160208164735-groups-add-admin.js index 2eedc9836..0986f4c16 100644 --- a/migrations/20160208164735-groups-add-admin.js +++ b/migrations/20160208164735-groups-add-admin.js @@ -2,7 +2,7 @@ var async = require('async'); -var ADMIN_GROUP_ID = 'admin'; // see groups.js +var ADMIN_GROUP_ID = 'admin'; // see constants.js exports.up = function(db, callback) { async.series([ diff --git a/migrations/20171116191443-backups-add-manifestJson.js b/migrations/20171116191443-backups-add-manifestJson.js new file mode 100644 index 000000000..7f9a7e98e --- /dev/null +++ b/migrations/20171116191443-backups-add-manifestJson.js @@ -0,0 +1,40 @@ +'use strict'; + +var async = require('async'); + +exports.up = function(db, callback) { + async.series([ + db.runSql.bind(db, 'ALTER TABLE backups ADD COLUMN manifestJson TEXT'), + + db.runSql.bind(db, 'START TRANSACTION;'), + + // fill all the backups with restoreConfigs from current apps + function addManifests(callback) { + console.log('Importing manifests'); + + db.all('SELECT * FROM backups WHERE type="app"', function (error, backups) { + if (error) return callback(error); + + async.eachSeries(backups, function (backup, next) { + var m = backup.restoreConfigJson ? JSON.parse(backup.restoreConfigJson) : null; + if (m) m = JSON.stringify(m.manifest); + + db.runSql('UPDATE backups SET manifestJson=? WHERE id=?', [ m, backup.id ], next); + }, callback); + }); + }, + + db.runSql.bind(db, 'COMMIT'), + + // remove the restoreConfig + db.runSql.bind(db, 'ALTER TABLE backups DROP COLUMN restoreConfigJson') + ], callback); +}; + +exports.down = function(db, callback) { + async.series([ + db.runSql.bind(db, 'ALTER TABLE backups DROP COLUMN manifestJson'), + db.runSql.bind(db, 'ALTER TABLE backups ADD COLUMN restoreConfigJson TEXT'), + ], callback); +}; + diff --git a/migrations/20171116203507-apps-rename-newConfigJson-to-updateConfigJson.js b/migrations/20171116203507-apps-rename-newConfigJson-to-updateConfigJson.js new file mode 100644 index 000000000..dee949c89 --- /dev/null +++ b/migrations/20171116203507-apps-rename-newConfigJson-to-updateConfigJson.js @@ -0,0 +1,15 @@ +'use strict'; + +exports.up = function(db, callback) { + db.runSql('ALTER TABLE apps CHANGE newConfigJson updateConfigJson TEXT', [], function (error) { + if (error) console.error(error); + callback(error); + }); +}; + +exports.down = function(db, callback) { + db.runSql('ALTER TABLE apps CHANGE updateConfigJson newConfigJson TEXT', [], function (error) { + if (error) console.error(error); + callback(error); + }); +}; diff --git a/migrations/20171116224051-apps-rename-lastBackupId-to-restoreConfigJson.js b/migrations/20171116224051-apps-rename-lastBackupId-to-restoreConfigJson.js new file mode 100644 index 000000000..40c6b37bb --- /dev/null +++ b/migrations/20171116224051-apps-rename-lastBackupId-to-restoreConfigJson.js @@ -0,0 +1,15 @@ +'use strict'; + +exports.up = function(db, callback) { + db.runSql('ALTER TABLE apps CHANGE lastBackupId restoreConfigJson TEXT', [], function (error) { + if (error) console.error(error); + callback(error); + }); +}; + +exports.down = function(db, callback) { + db.runSql('ALTER TABLE apps CHANGE restoreConfigJson lastBackupId TEXT', [], function (error) { + if (error) console.error(error); + callback(error); + }); +}; diff --git a/migrations/20171118000001-apps-add-domain.js b/migrations/20171118000001-apps-add-domain.js new file mode 100644 index 000000000..2416836e3 --- /dev/null +++ b/migrations/20171118000001-apps-add-domain.js @@ -0,0 +1,70 @@ +'use strict'; + +var async = require('async'), + safe = require('safetydance'); + +exports.up = function(db, callback) { + // first check precondtion of domain entry in settings + db.all('SELECT * FROM settings WHERE name = ?', [ 'domain' ], function (error, result) { + if (error) return callback(error); + + var domain = {}; + if (result[0]) domain = safe.JSON.parse(result[0].value) || {}; + + async.series([ + db.runSql.bind(db, 'START TRANSACTION;'), + function addAppsDomainColumn(done) { + db.runSql('ALTER TABLE apps ADD COLUMN domain VARCHAR(128)', [], done); + }, + function setAppDomain(done) { + if (!domain.fqdn) return done(); // skip for new cloudrons without a domain + db.runSql('UPDATE apps SET domain = ?', [ domain.fqdn ], done); + }, + function addAppsLocationDomainUniqueConstraint(done) { + db.runSql('ALTER TABLE apps ADD UNIQUE location_domain_unique_index (location, domain)', [], done); + }, + function removePresetupAdminGroupIfNew(done) { + // do not delete on update, will update the record in setMailboxesDomain() + if (domain.fqdn) return done(); + + // this will be finally created once we have a domain when we create the owner in user.js + const ADMIN_GROUP_ID = 'admin'; // see constants.js + db.runSql('DELETE FROM groups WHERE id = ?', [ ADMIN_GROUP_ID ], function (error) { + if (error) return done(error); + + db.runSql('DELETE FROM mailboxes WHERE ownerId = ?', [ ADMIN_GROUP_ID ], done); + }); + }, + function addMailboxesDomainColumn(done) { + db.runSql('ALTER TABLE mailboxes ADD COLUMN domain VARCHAR(128)', [], done); + }, + function setMailboxesDomain(done) { + if (!domain.fqdn) return done(); // skip for new cloudrons without a domain + db.runSql('UPDATE mailboxes SET domain = ?', [ domain.fqdn ], done); + }, + function dropAppsLocationUniqueConstraint(done) { + db.runSql('ALTER TABLE apps DROP INDEX location', [], done); + }, + db.runSql.bind(db, 'COMMIT') + ], callback); + }); +}; + +exports.down = function(db, callback) { + async.series([ + db.runSql.bind(db, 'START TRANSACTION;'), + function dropMailboxesDomainColumn(done) { + db.runSql('ALTER TABLE mailboxes DROP COLUMN domain', [], done); + }, + function dropLocationDomainUniqueConstraint(done) { + db.runSql('ALTER TABLE apps DROP INDEX location_domain_unique_index', [], done); + }, + function dropAppsDomainColumn(done) { + db.runSql('ALTER TABLE apps DROP COLUMN domain', [], done); + }, + function addAppsLocationUniqueConstraint(done) { + db.runSql('ALTER TABLE apps ADD UNIQUE location (location)', [], done); + }, + db.runSql.bind(db, 'COMMIT') + ], callback); +}; diff --git a/migrations/20171118000002-domains-add-table.js b/migrations/20171118000002-domains-add-table.js new file mode 100644 index 000000000..02336235f --- /dev/null +++ b/migrations/20171118000002-domains-add-table.js @@ -0,0 +1,60 @@ +'use strict'; + +var async = require('async'), + safe = require('safetydance'); + +exports.up = function(db, callback) { + var fqdn, zoneName, configJson; + + async.series([ + function gatherDomain(done) { + db.all('SELECT * FROM settings WHERE name = ?', [ 'domain' ], function (error, result) { + if (error) return done(error); + + var domain = {}; + if (result[0]) domain = safe.JSON.parse(result[0].value) || {}; + + fqdn = domain.fqdn || ''; + zoneName = domain.zoneName || fqdn; + + done(); + }); + }, + function gatherDNSConfig(done) { + db.all('SELECT * FROM settings WHERE name = ?', [ 'dns_config' ], function (error, result) { + if (error) return done(error); + + configJson = (result[0] && result[0].value) ? result[0].value : JSON.stringify({ provider: 'manual'}); + + // caas dns config needs an fqdn + var config = JSON.parse(configJson); + if (config.provider === 'caas') config.fqdn = fqdn; + configJson = JSON.stringify(config); + + done(); + }); + }, + db.runSql.bind(db, 'START TRANSACTION;'), + function createDomainsTable(done) { + var cmd = ` + CREATE TABLE domains( + domain VARCHAR(128) NOT NULL UNIQUE, + zoneName VARCHAR(128) NOT NULL, + configJson TEXT, + PRIMARY KEY (domain)) CHARACTER SET utf8 COLLATE utf8_bin + `; + + db.runSql(cmd, [], done); + }, + function addInitialDomain(done) { + if (!fqdn) return done(); + + db.runSql('INSERT INTO domains (domain, zoneName, configJson) VALUES (?, ?, ?)', [ fqdn, zoneName, configJson ], done); + }, + db.runSql.bind(db, 'COMMIT') + ], callback); +}; + +exports.down = function(db, callback) { + db.runSql('DROP TABLE domains', callback); +}; diff --git a/migrations/20171118000003-apps-add-domain-constraint.js b/migrations/20171118000003-apps-add-domain-constraint.js new file mode 100644 index 000000000..7743bf4d5 --- /dev/null +++ b/migrations/20171118000003-apps-add-domain-constraint.js @@ -0,0 +1,15 @@ +'use strict'; + +exports.up = function(db, callback) { + db.runSql('ALTER TABLE apps ADD CONSTRAINT apps_domain_constraint FOREIGN KEY(domain) REFERENCES domains(domain)', function (error) { + if (error) console.error(error); + callback(error); + }); +}; + +exports.down = function(db, callback) { + db.runSql('ALTER TABLE apps DROP FOREIGN KEY apps_domain_constraint', function (error) { + if (error) console.error(error); + callback(error); + }); +}; diff --git a/migrations/20171118000004-mailboxes-add-domain-constraint.js b/migrations/20171118000004-mailboxes-add-domain-constraint.js new file mode 100644 index 000000000..4cca04c00 --- /dev/null +++ b/migrations/20171118000004-mailboxes-add-domain-constraint.js @@ -0,0 +1,15 @@ +'use strict'; + +exports.up = function(db, callback) { + db.runSql('ALTER TABLE mailboxes ADD CONSTRAINT mailboxes_domain_constraint FOREIGN KEY(domain) REFERENCES domains(domain)', function (error) { + if (error) console.error(error); + callback(error); + }); +}; + +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); + }); +}; diff --git a/migrations/20171118000005-mailboxes-drop-name-constraint.js b/migrations/20171118000005-mailboxes-drop-name-constraint.js new file mode 100644 index 000000000..c641f75ad --- /dev/null +++ b/migrations/20171118000005-mailboxes-drop-name-constraint.js @@ -0,0 +1,15 @@ +'use strict'; + +exports.up = function(db, callback) { + db.runSql('ALTER TABLE mailboxes DROP PRIMARY KEY', function (error) { + if (error) console.error(error); + callback(error); + }); +}; + +exports.down = function(db, callback) { + db.runSql('ALTER TABLE mailboxes ADD PRIMARY KEY(name)', function (error) { + if (error) console.error(error); + callback(error); + }); +}; diff --git a/migrations/20171118000006-mailboxes-add-name-domain-constraint.js b/migrations/20171118000006-mailboxes-add-name-domain-constraint.js new file mode 100644 index 000000000..8bf234050 --- /dev/null +++ b/migrations/20171118000006-mailboxes-add-name-domain-constraint.js @@ -0,0 +1,15 @@ +'use strict'; + +exports.up = function(db, callback) { + db.runSql('ALTER TABLE mailboxes ADD UNIQUE mailboxes_name_domain_unique_index (name, domain)', function (error) { + if (error) console.error(error); + callback(error); + }); +}; + +exports.down = function(db, callback) { + db.runSql('ALTER TABLE mailboxes DROP INDEX mailboxes_name_domain_unique_index', function (error) { + if (error) console.error(error); + callback(error); + }); +}; diff --git a/migrations/20171119203452-apps-add-updateTime.js b/migrations/20171119203452-apps-add-updateTime.js new file mode 100644 index 000000000..6a309ade4 --- /dev/null +++ b/migrations/20171119203452-apps-add-updateTime.js @@ -0,0 +1,15 @@ +'use strict'; + +exports.up = function(db, callback) { + db.runSql('ALTER TABLE apps ADD COLUMN updateTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP', function (error) { + if (error) console.error(error); + callback(error); + }); +}; + +exports.down = function(db, callback) { + db.runSql('ALTER TABLE apps DROP COLUMN updateTime', function (error) { + if (error) console.error(error); + callback(error); + }); +}; diff --git a/migrations/20171119204952-apps-rename-createdAt-to-creationTime.js b/migrations/20171119204952-apps-rename-createdAt-to-creationTime.js new file mode 100644 index 000000000..86a3785c5 --- /dev/null +++ b/migrations/20171119204952-apps-rename-createdAt-to-creationTime.js @@ -0,0 +1,15 @@ +'use strict'; + +exports.up = function(db, callback) { + db.runSql('ALTER TABLE apps CHANGE createdAt creationTime TIMESTAMP(2) NOT NULL', [], function (error) { + if (error) console.error(error); + callback(error); + }); +}; + +exports.down = function(db, callback) { + db.runSql('ALTER TABLE apps CHANGE creationTime createdAt TIMESTAMP(2) NOT NULL', [], function (error) { + if (error) console.error(error); + callback(error); + }); +}; diff --git a/migrations/20171129005712-settings-add-fqdn-to-caas-backup-config.js b/migrations/20171129005712-settings-add-fqdn-to-caas-backup-config.js new file mode 100644 index 000000000..20afea4e0 --- /dev/null +++ b/migrations/20171129005712-settings-add-fqdn-to-caas-backup-config.js @@ -0,0 +1,27 @@ +'use strict'; + +var async = require('async'); + +exports.up = function(db, callback) { + db.all('SELECT * FROM domains', function (error, domains) { + if (error) return callback(error); + + var caasDomains = domains.filter(function (d) { return JSON.parse(d.configJson).provider === 'caas'; }); + if (caasDomains.length === 0) return callback(); + var caasDomain = caasDomains[0].domain; + + db.all('SELECT * FROM settings WHERE name=?', [ 'backup_config' ], function (error, settings) { + if (error) return callback(error); + + var setting = settings[0]; + var config = JSON.parse(setting.value); + config.fqdn = caasDomain; + + db.runSql('UPDATE settings SET value=? WHERE name=?', [ JSON.stringify(config), setting.name ], callback); + }); + }); +}; + +exports.down = function(db, callback) { + callback(); +}; diff --git a/migrations/20171205124434-settings-default-backupConfig.js b/migrations/20171205124434-settings-default-backupConfig.js new file mode 100644 index 000000000..d71f41742 --- /dev/null +++ b/migrations/20171205124434-settings-default-backupConfig.js @@ -0,0 +1,23 @@ +'use strict'; + +exports.up = function(db, callback) { + var backupConfig = { + "provider": "filesystem", + "backupFolder": "/var/backups", + "format": "tgz", + "retentionSecs": 172800 + }; + + db.runSql('INSERT settings (name, value) VALUES(?, ?)', [ 'backup_config', JSON.stringify(backupConfig) ], function (error) { + if (!error || error.code === 'ER_DUP_ENTRY') return callback(); // dup entry is OK for existing cloudrons + + callback(error); + }); +}; + +exports.down = function(db, callback) { + db.runSql('DELETE FROM settings WHERE name=?', ['backup_config'], function (error) { + if (error) console.error(error); + callback(error); + }); +}; diff --git a/migrations/schema.sql b/migrations/schema.sql index 94dd4fd01..64901f8e5 100644 --- a/migrations/schema.sql +++ b/migrations/schema.sql @@ -9,6 +9,10 @@ #### BLOB - stored offline from table row (use for binary data) #### https://dev.mysql.com/doc/refman/5.0/en/storage-requirements.html +# The code uses zero dates. Make sure sql_mode does NOT have NO_ZERO_DATE +# http://johnemb.blogspot.com/2014/09/adding-or-removing-individual-sql-modes.html +# SET GLOBAL sql_mode=(SELECT REPLACE(@@sql_mode,'NO_ZERO_DATE','')); + CREATE TABLE IF NOT EXISTS users( id VARCHAR(128) NOT NULL UNIQUE, username VARCHAR(254) UNIQUE, @@ -59,23 +63,26 @@ CREATE TABLE IF NOT EXISTS apps( containerId VARCHAR(128), manifestJson TEXT, httpPort INTEGER, // this is the nginx proxy port and not manifest.httpPort - location VARCHAR(128) NOT NULL UNIQUE, + location VARCHAR(128) NOT NULL, + domain VARCHAR(128) NOT NULL, dnsRecordId VARCHAR(512), // tracks any id that we got back to track dns updates accessRestrictionJson TEXT, // { users: [ ], groups: [ ] } 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 robotsTxt TEXT, - enableBackup BOOLEAN DEFAULT 1, + enableBackup BOOLEAN DEFAULT 1, // misnomer: controls automatic daily backups // the following fields do not belong here, they can be removed when we use a queue for apptask - lastBackupId VARCHAR(128), // used to pass backupId to restore from to apptask + restoreConfigJson VARCHAR(256), // used to pass backupId to restore from to apptask oldConfigJson TEXT, // used to pass old config for apptask (configure, restore) - newConfigJson TEXT, // used to pass new config for apptask (update) + updateConfigJson TEXT, // used to pass new config for apptask (update) + FOREIGN KEY(domain) REFERENCES domains(domain), PRIMARY KEY(id)); CREATE TABLE IF NOT EXISTS appPortBindings( @@ -111,7 +118,7 @@ CREATE TABLE IF NOT EXISTS backups( type VARCHAR(16) NOT NULL, /* 'box' or 'app' */ dependsOn TEXT, /* comma separate list of objects this backup depends on */ state VARCHAR(16) NOT NULL, - restoreConfigJson TEXT, /* JSON including the manifest of the backed up app */ + manifestJson TEXT, /* to validate if the app can be installed in this version of box */ format VARCHAR(16) DEFAULT "tgz", PRIMARY KEY (id)); @@ -135,5 +142,17 @@ CREATE TABLE IF NOT EXISTS mailboxes( ownerType VARCHAR(16) NOT NULL, /* 'app' or 'user' or 'group' */ aliasTarget VARCHAR(128), /* the target name type is an alias */ creationTime TIMESTAMP, + domain VARCHAR(128), + 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 */ + 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; diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index e49264eba..34ae6881d 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -243,9 +243,9 @@ } }, "aws-sdk": { - "version": "2.149.0", - "from": "aws-sdk@>=2.149.0 <3.0.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.149.0.tgz" + "version": "2.151.0", + "from": "aws-sdk@2.151.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.151.0.tgz" }, "aws-sign2": { "version": "0.7.0", @@ -502,9 +502,9 @@ "dev": true }, "cloudron-manifestformat": { - "version": "2.9.0", - "from": "cloudron-manifestformat@>=2.9.0 <3.0.0", - "resolved": "https://registry.npmjs.org/cloudron-manifestformat/-/cloudron-manifestformat-2.9.0.tgz", + "version": "2.10.0", + "from": "cloudron-manifestformat@2.10.0", + "resolved": "https://registry.npmjs.org/cloudron-manifestformat/-/cloudron-manifestformat-2.10.0.tgz", "dependencies": { "safetydance": { "version": "0.0.15", @@ -646,9 +646,9 @@ "resolved": "https://registry.npmjs.org/connect-ensure-login/-/connect-ensure-login-0.1.1.tgz" }, "connect-lastmile": { - "version": "0.1.0", - "from": "connect-lastmile@>=0.1.0 <0.2.0", - "resolved": "https://registry.npmjs.org/connect-lastmile/-/connect-lastmile-0.1.0.tgz", + "version": "1.0.2", + "from": "connect-lastmile@1.0.2", + "resolved": "https://registry.npmjs.org/connect-lastmile/-/connect-lastmile-1.0.2.tgz", "dependencies": { "debug": { "version": "2.1.3", diff --git a/package.json b/package.json index db257c92f..4ac95f15a 100644 --- a/package.json +++ b/package.json @@ -18,11 +18,11 @@ "@google-cloud/storage": "^1.2.1", "@sindresorhus/df": "^2.1.0", "async": "^2.6.0", - "aws-sdk": "^2.149.0", + "aws-sdk": "^2.151.0", "body-parser": "^1.18.2", - "cloudron-manifestformat": "^2.9.0", + "cloudron-manifestformat": "^2.10.0", "connect-ensure-login": "^0.1.1", - "connect-lastmile": "^0.1.0", + "connect-lastmile": "^1.0.2", "connect-timeout": "^1.9.0", "cookie-parser": "^1.3.5", "cookie-session": "^1.3.2", diff --git a/scripts/cloudron-setup b/scripts/cloudron-setup index f1b18fe2f..23f53a3ed 100755 --- a/scripts/cloudron-setup +++ b/scripts/cloudron-setup @@ -48,9 +48,6 @@ domain="" adminLocation="my" zoneName="" provider="" -encryptionKey="" -restoreUrl="" -dnsProvider="manual" tlsProvider="le-prod" requestedVersion="" apiServerOrigin="https://api.cloudron.io" @@ -61,8 +58,9 @@ sourceTarballUrl="" rebootServer="true" baseDataDir="" -# TODO this is still there for the restore case, see other occasions below -versionsUrl="https://s3.amazonaws.com/prod-cloudron-releases/versions.json" +# 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}" @@ -76,17 +74,14 @@ while true; do --encryption-key) encryptionKey="$2"; shift 2;; --restore-url) restoreUrl="$2"; shift 2;; --tls-provider) tlsProvider="$2"; shift 2;; - --dns-provider) dnsProvider="$2"; shift 2;; --version) requestedVersion="$2"; shift 2;; --env) if [[ "$2" == "dev" ]]; then - versionsUrl="https://s3.amazonaws.com/dev-cloudron-releases/versions.json" apiServerOrigin="https://api.dev.cloudron.io" webServerOrigin="https://dev.cloudron.io" tlsProvider="le-staging" prerelease="true" elif [[ "$2" == "staging" ]]; then - versionsUrl="https://s3.amazonaws.com/staging-cloudron-releases/versions.json" apiServerOrigin="https://api.staging.cloudron.io" webServerOrigin="https://staging.cloudron.io" tlsProvider="le-staging" @@ -134,14 +129,6 @@ if [[ -z "${dataJson}" ]]; then exit 1 fi - if [[ -z "${dnsProvider}" ]]; then - echo "--dns-provider is required (noop, manual)" - exit 1 - elif [[ "${dnsProvider}" != "noop" && "${dnsProvider}" != "manual" ]]; then - echo "--dns-provider must be one of : manual, noop" - exit 1 - fi - if [[ -n "${baseDataDir}" && ! -d "${baseDataDir}" ]]; then echo "${baseDataDir} does not exist" exit 1 @@ -192,39 +179,35 @@ if [[ "${sourceTarballUrl}" == "" ]]; then fi # Build data -# TODO versionsUrl is still there for the cloudron restore case +# tlsConfig, dnsConfig, backupConfig are here for backward compat with < 1.9 +# from 1.9, we use autoprovision.json if [[ -z "${dataJson}" ]]; then if [[ -z "${restoreUrl}" ]]; then data=$(cat < Installing version ${version} (this takes some time) ..." echo "${data}" > "${DATA_FILE}" -# poor mans semver -if [[ ${version} == "0.10"* ]]; then - if ! /bin/bash "${box_src_tmp_dir}/scripts/installer.sh" --data-file "${DATA_FILE}" &>> "${LOG_FILE}"; then - echo "Failed to install cloudron. See ${LOG_FILE} for details" - exit 1 - fi -else - if ! /bin/bash "${box_src_tmp_dir}/scripts/installer.sh" --data-file "${DATA_FILE}" --data-dir "${baseDataDir}" &>> "${LOG_FILE}"; then - echo "Failed to install cloudron. See ${LOG_FILE} for details" - exit 1 - fi +if ! /bin/bash "${box_src_tmp_dir}/scripts/installer.sh" --data-file "${DATA_FILE}" --data-dir "${baseDataDir}" &>> "${LOG_FILE}"; then + echo "Failed to install cloudron. See ${LOG_FILE} for details" + exit 1 fi rm "${DATA_FILE}" @@ -287,6 +262,17 @@ while true; do sleep 10 done +autoprovision_data=$(cat < /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 diff --git a/scripts/installer.sh b/scripts/installer.sh index 3c724d295..53d0a07e3 100755 --- a/scripts/installer.sh +++ b/scripts/installer.sh @@ -109,9 +109,6 @@ fi # ensure we are not inside the source directory, which we will remove now cd /root -echo "==> installer: updating packages" -# add logic to update apt packages here - echo "==> installer: switching the box code" rm -rf "${BOX_SRC_DIR}" mv "${box_src_tmp_dir}" "${BOX_SRC_DIR}" diff --git a/setup/argparser.sh b/setup/argparser.sh index 1eae2357e..4ecde6e14 100644 --- a/setup/argparser.sh +++ b/setup/argparser.sh @@ -9,20 +9,12 @@ arg_fqdn="" arg_admin_location="" arg_zone_name="" arg_is_custom_domain="false" -arg_restore_key="" -arg_restore_url="" arg_retire_reason="" arg_retire_info="" -arg_tls_config="" -arg_tls_cert="" -arg_tls_key="" arg_token="" arg_version="" arg_web_server_origin="" -arg_backup_config="" -arg_dns_config="" arg_provider="" -arg_app_bundle="" arg_is_demo="false" args=$(getopt -o "" -l "data:,retire-reason:,retire-info:" -n "$0" -- "$@") @@ -42,6 +34,7 @@ while true; do # these params must be valid in all cases arg_fqdn=$(echo "$2" | $json fqdn) arg_zone_name=$(echo "$2" | $json zoneName) + [[ "${arg_zone_name}" == "" ]] && arg_zone_name="${arg_fqdn}" arg_is_custom_domain=$(echo "$2" | $json isCustomDomain) [[ "${arg_is_custom_domain}" == "" ]] && arg_is_custom_domain="true" @@ -59,36 +52,14 @@ while true; do arg_version=$(echo "$2" | $json version) # read possibly empty parameters here - arg_app_bundle=$(echo "$2" | $json appBundle) - [[ "${arg_app_bundle}" == "" ]] && arg_app_bundle="[]" - arg_is_demo=$(echo "$2" | $json isDemo) [[ "${arg_is_demo}" == "" ]] && arg_is_demo="false" - arg_tls_cert=$(echo "$2" | $json tlsCert) - [[ "${arg_tls_cert}" == "null" ]] && arg_tls_cert="" - arg_tls_key=$(echo "$2" | $json tlsKey) - [[ "${arg_tls_key}" == "null" ]] && arg_tls_key="" arg_token=$(echo "$2" | $json token) arg_provider=$(echo "$2" | $json provider) [[ "${arg_provider}" == "" ]] && arg_provider="generic" - arg_tls_config=$(echo "$2" | $json tlsConfig) - [[ "${arg_tls_config}" == "null" ]] && arg_tls_config="" - - arg_restore_url=$(echo "$2" | $json restore.url) - [[ "${arg_restore_url}" == "null" ]] && arg_restore_url="" - - arg_restore_key=$(echo "$2" | $json restore.key) - [[ "${arg_restore_key}" == "null" ]] && arg_restore_key="" - - arg_backup_config=$(echo "$2" | $json backupConfig) - [[ "${arg_backup_config}" == "null" ]] && arg_backup_config="" - - arg_dns_config=$(echo "$2" | $json dnsConfig) - [[ "${arg_dns_config}" == "null" ]] && arg_dns_config="" - shift 2 ;; --) break;; @@ -100,13 +71,8 @@ echo "Parsed arguments:" echo "api server: ${arg_api_server_origin}" echo "fqdn: ${arg_fqdn}" echo "custom domain: ${arg_is_custom_domain}" -echo "restore url: ${arg_restore_url}" -echo "tls cert: ${arg_tls_cert}" # do not dump these as they might become available via logs API -#echo "restore key: ${arg_restore_key}" -#echo "tls key: ${arg_tls_key}" #echo "token: ${arg_token}" -echo "tlsConfig: ${arg_tls_config}" echo "version: ${arg_version}" echo "web server: ${arg_web_server_origin}" echo "provider: ${arg_provider}" diff --git a/setup/start.sh b/setup/start.sh index e6fab6b4b..15adf671b 100755 --- a/setup/start.sh +++ b/setup/start.sh @@ -199,42 +199,6 @@ 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' -if [[ -n "${arg_restore_url}" ]]; then - set_progress "30" "Downloading restore data" - - readonly restore_dir="${arg_restore_url#file://}" - - if [[ -d "${restore_dir}" ]]; then # rsync backup - echo "==> Copying backup: ${restore_dir}" - if [[ $(stat -c "%d" "${BOX_DATA_DIR}") == $(stat -c "%d" "${restore_dir}") ]]; then - cp -rfl "${restore_dir}/." "${BOX_DATA_DIR}" - else - cp -rf "${restore_dir}/." "${BOX_DATA_DIR}" - fi - else # tgz backup - decrypt="" - if [[ "${arg_restore_url}" == *.tar.gz.enc || -n "${arg_restore_key}" ]]; then - echo "==> Downloading encrypted backup: ${arg_restore_url} and key: ${arg_restore_key}" - decrypt=(openssl aes-256-cbc -d -nosalt -pass "pass:${arg_restore_key}") - elif [[ "${arg_restore_url}" == *.tar.gz ]]; then - echo "==> Downloading backup: ${arg_restore_url}" - decrypt=(cat -) - fi - - while true; do - if $curl -L "${arg_restore_url}" | "${decrypt[@]}" \ - | tar -zxf - --overwrite -C "${BOX_DATA_DIR}"; then break; fi - echo "Failed to download data, trying again" - done - fi - - set_progress "35" "Setting up MySQL" - if [[ -f "${BOX_DATA_DIR}/box.mysqldump" ]]; then - echo "==> Importing existing database into MySQL" - mysql -u root -p${mysql_root_password} box < "${BOX_DATA_DIR}/box.mysqldump" - fi -fi - set_progress "40" "Migrating data" sudo -u "${USER}" -H bash < Adding automated configs" -mysql -u root -p${mysql_root_password} -e "REPLACE INTO settings (name, value) VALUES (\"domain\", '{ \"fqdn\": \"$arg_fqdn\", \"zoneName\": \"$arg_zone_name\", \"adminLocation\": \"$arg_admin_location\" }')" box - -if [[ ! -z "${arg_backup_config}" ]]; then - mysql -u root -p${mysql_root_password} \ - -e "REPLACE INTO settings (name, value) VALUES (\"backup_config\", '$arg_backup_config')" box -fi - -if [[ ! -z "${arg_dns_config}" ]]; then - mysql -u root -p${mysql_root_password} \ - -e "REPLACE INTO settings (name, value) VALUES (\"dns_config\", '$arg_dns_config')" box -fi - -if [[ ! -z "${arg_tls_config}" ]]; then - mysql -u root -p${mysql_root_password} \ - -e "REPLACE INTO settings (name, value) VALUES (\"tls_config\", '$arg_tls_config')" box -fi - echo "==> Creating cloudron.conf" cat > "${CONFIG_DIR}/cloudron.conf" < "${CONFIG_DIR}/cloudron.conf" < "${CONFIG_DIR}/host.cert" - echo "${arg_tls_key}" > "${CONFIG_DIR}/host.key" -fi echo "==> Creating config.json for webadmin" cat > "${BOX_SRC_DIR}/webadmin/dist/config.json" < 63) return new AppsError(AppsError.BAD_FIELD, 'Hostname length cannot be greater than 63'); - if (location.match(/^[A-Za-z0-9-]+$/) === null) return new AppsError(AppsError.BAD_FIELD, 'Hostname can only contain alphanumerics and hyphen'); - if (location[0] === '-' || location[location.length-1] === '-') return new AppsError(AppsError.BAD_FIELD, 'Hostname cannot start or end with hyphen'); - if (location.length + 1 /* hyphen */ + fqdn.length > 253) return new AppsError(AppsError.BAD_FIELD, 'FQDN length exceeds 253 characters'); + // workaround https://github.com/oncletom/tld.js/issues/73 + var tmp = hostname.replace('_', '-'); + if (!tld.isValid(tmp)) return new AppsError(AppsError.BAD_FIELD, 'Hostname is not a valid domain name'); + + if (hostname.length > 253) return new AppsError(AppsError.BAD_FIELD, 'Hostname length exceeds 253 characters'); + + if (location === null) return new AppsError(AppsError.BAD_FIELD, 'Invalid subdomain'); + if (location.match(/^[A-Za-z0-9-]+$/) === null) return new AppsError(AppsError.BAD_FIELD, 'Subdomain can only contain alphanumerics and hyphen'); + if (location.length > 63) return new AppsError(AppsError.BAD_FIELD, 'Subdomain exceeds 63 characters'); + if (location.startsWith('-') || location.endsWith('-')) return new AppsError(AppsError.BAD_FIELD, 'Subdomain cannot start or end with hyphen'); return null; } @@ -259,6 +276,12 @@ function validateRobotsTxt(robotsTxt) { return null; } +function validateBackupFormat(format) { + if (format === 'tgz' || format == 'rsync') return null; + + return new AppsError(AppsError.BAD_FIELD, 'Invalid backup format'); +} + function getDuplicateErrorDetails(location, portBindings, error) { assert.strictEqual(typeof location, 'string'); assert.strictEqual(typeof portBindings, 'object'); @@ -285,6 +308,7 @@ function getAppConfig(app) { return { manifest: app.manifest, location: app.location, + domain: app.domain, accessRestriction: app.accessRestriction, portBindings: app.portBindings, memoryLimit: app.memoryLimit, @@ -309,12 +333,18 @@ function hasAccessTo(app, user, callback) { if (app.accessRestriction.users.some(function (e) { return e === user.id; })) return callback(null, true); // check group access - if (!app.accessRestriction.groups) return callback(null, false); + groups.getGroups(user.id, function (error, groupIds) { + if (error) return callback(null, false); - async.some(app.accessRestriction.groups, function (groupId, iteratorDone) { - groups.isMember(groupId, user.id, iteratorDone); - }, function (error, result) { - callback(null, !error && result); + const isAdmin = groupIds.indexOf(constants.ADMIN_GROUP_ID) !== -1; + + if (isAdmin) return callback(null, true); // admins can always access any app + + if (!app.accessRestriction.groups) return callback(null, false); + + if (app.accessRestriction.groups.some(function (gid) { return groupIds.indexOf(gid) !== -1; })) return callback(null, true); + + callback(null, false); }); } @@ -327,8 +357,8 @@ function get(appId, callback) { if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error)); app.iconUrl = getIconUrlSync(app); - app.fqdn = app.altDomain || config.appFqdn(app.location); - app.cnameTarget = app.altDomain ? config.appFqdn(app.location) : null; + app.fqdn = app.altDomain || config.appFqdn(app); + app.cnameTarget = app.altDomain ? config.appFqdn(app) : null; callback(null, app); }); @@ -346,8 +376,8 @@ function getByIpAddress(ip, callback) { if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error)); app.iconUrl = getIconUrlSync(app); - app.fqdn = app.altDomain || config.appFqdn(app.location); - app.cnameTarget = app.altDomain ? config.appFqdn(app.location) : null; + app.fqdn = app.altDomain || config.appFqdn(app); + app.cnameTarget = app.altDomain ? config.appFqdn(app) : null; callback(null, app); }); @@ -362,8 +392,8 @@ function getAll(callback) { apps.forEach(function (app) { app.iconUrl = getIconUrlSync(app); - app.fqdn = app.altDomain || config.appFqdn(app.location); - app.cnameTarget = app.altDomain ? config.appFqdn(app.location) : null; + app.fqdn = app.altDomain || config.appFqdn(app); + app.cnameTarget = app.altDomain ? config.appFqdn(app) : null; }); callback(null, apps); @@ -409,6 +439,7 @@ function install(data, auditSource, callback) { assert.strictEqual(typeof callback, 'function'); var location = data.location.toLowerCase(), + domain = data.domain.toLowerCase(), portBindings = data.portBindings || null, accessRestriction = data.accessRestriction || null, icon = data.icon || null, @@ -421,7 +452,8 @@ function install(data, auditSource, callback) { debugMode = data.debugMode || null, robotsTxt = data.robotsTxt || null, enableBackup = 'enableBackup' in data ? data.enableBackup : true, - backupId = data.backupId || null; + backupId = data.backupId || null, + backupFormat = data.backupFormat || 'tgz'; assert(data.appStoreId || data.manifest); // atleast one of them is required @@ -434,7 +466,7 @@ function install(data, auditSource, callback) { error = checkManifestConstraints(manifest); if (error) return callback(error); - error = validateHostname(location, config.fqdn()); + error = validateHostname(location, domain); if (error) return callback(error); error = validatePortBindings(portBindings, manifest.tcpPorts); @@ -455,6 +487,9 @@ function install(data, auditSource, callback) { error = validateRobotsTxt(robotsTxt); if (error) return callback(error); + error = validateBackupFormat(backupFormat); + if (error) return callback(error); + if ('sso' in data && !('optionalSso' in manifest)) return callback(new AppsError(AppsError.BAD_FIELD, 'sso can only be specified for apps with optionalSso')); // if sso was unspecified, enable it by default if possible if (sso === null) sso = !!manifest.addons['ldap'] || !!manifest.addons['oauth']; @@ -471,7 +506,7 @@ function install(data, auditSource, callback) { } } - error = certificates.validateCertificate(cert, key, config.appFqdn(location)); + error = certificates.validateCertificate(cert, key, config.appFqdn({ domain: domain, location: location })); if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message)); debug('Will install app with id : ' + appId); @@ -490,24 +525,24 @@ function install(data, auditSource, callback) { sso: sso, debugMode: debugMode, mailboxName: (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app', - lastBackupId: backupId, + restoreConfig: backupId ? { backupId: backupId, backupFormat: backupFormat } : null, enableBackup: enableBackup, robotsTxt: robotsTxt }; - appdb.add(appId, appStoreId, manifest, location, portBindings, data, function (error) { + 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) 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, config.appFqdn(location) + '.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, config.appFqdn(location) + '.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, config.appFqdn({ domain: domain, location: location }) + '.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, config.appFqdn({ domain: domain, location: location }) + '.user.key'), key)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving key: ' + safe.error.message)); } taskmanager.restartAppTask(appId); - eventlog.add(eventlog.ACTION_APP_INSTALL, auditSource, { appId: appId, location: location, manifest: manifest, backupId: backupId }); + eventlog.add(eventlog.ACTION_APP_INSTALL, auditSource, { appId: appId, location: location, domain: domain, manifest: manifest, backupId: backupId }); callback(null, { id : appId }); }); @@ -525,14 +560,15 @@ function configure(appId, data, 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)); - var location, portBindings, values = { }; - if ('location' in data) { - location = values.location = data.location.toLowerCase(); - error = validateHostname(values.location, config.fqdn()); - if (error) return callback(error); - } else { - location = app.location; - } + var domain, location, portBindings, values = { }; + if ('location' in data) location = values.location = data.location.toLowerCase(); + else location = app.location; + + if ('domain' in data) domain = values.domain = data.domain.toLowerCase(); + else domain = app.domain; + + error = validateHostname(location, domain); + if (error) return callback(error); if ('accessRestriction' in data) { values.accessRestriction = data.accessRestriction; @@ -580,14 +616,14 @@ function configure(appId, data, auditSource, callback) { // 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 = certificates.validateCertificate(data.cert, data.key, config.appFqdn(location)); + error = certificates.validateCertificate(data.cert, data.key, config.appFqdn({ domain: domain, location: location })); if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message)); - if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, config.appFqdn(location) + '.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, config.appFqdn(location) + '.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, config.appFqdn({ domain: domain, location: location }) + '.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, config.appFqdn({ domain: domain, location: location }) + '.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, config.appFqdn(location) + '.user.cert'))) debug('Error removing cert: ' + safe.error.message); - if (!safe.fs.unlinkSync(path.join(paths.APP_CERTS_DIR, config.appFqdn(location) + '.user.key'))) debug('Error removing key: ' + safe.error.message); + if (!safe.fs.unlinkSync(path.join(paths.APP_CERTS_DIR, config.appFqdn({ domain: domain, location: location }) + '.user.cert'))) debug('Error removing cert: ' + safe.error.message); + if (!safe.fs.unlinkSync(path.join(paths.APP_CERTS_DIR, config.appFqdn({ domain: domain, location: location }) + '.user.key'))) debug('Error removing key: ' + safe.error.message); } } @@ -599,7 +635,7 @@ function configure(appId, data, auditSource, callback) { var oldName = (app.location ? app.location : app.manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app'; var newName = (location ? location : app.manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app'; - mailboxdb.updateName(oldName, newName, function (error) { + mailboxdb.updateName(oldName, values.oldConfig.domain, newName, domain, function (error) { if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new AppsError(AppsError.ALREADY_EXISTS, 'This mailbox is already taken')); if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE)); if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error)); @@ -630,7 +666,7 @@ function update(appId, data, auditSource, callback) { downloadManifest(data.appStoreId, data.manifest, function (error, appStoreId, manifest) { if (error) return callback(error); - var newConfig = { }; + var updateConfig = { }; error = manifestFormat.parse(manifest); if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Manifest error:' + error.message)); @@ -638,7 +674,7 @@ function update(appId, data, auditSource, callback) { error = checkManifestConstraints(manifest); if (error) return callback(error); - newConfig.manifest = manifest; + updateConfig.manifest = manifest; if ('icon' in data) { if (data.icon) { @@ -658,22 +694,22 @@ function update(appId, data, auditSource, callback) { // prevent user from installing a app with different manifest id over an existing app // this allows cloudron install -f --app for an app installed from the appStore - if (app.manifest.id !== newConfig.manifest.id) { + if (app.manifest.id !== updateConfig.manifest.id) { if (!data.force) return callback(new AppsError(AppsError.BAD_FIELD, 'manifest id does not match. force to override')); // clear appStoreId so that this app does not get updates anymore - newConfig.appStoreId = ''; + updateConfig.appStoreId = ''; } // do not update apps in debug mode if (app.debugMode && !data.force) return callback(new AppsError(AppsError.BAD_STATE, 'debug mode enabled. force to override')); // Ensure we update the memory limit in case the new app requires more memory as a minimum - // 0 and -1 are special newConfig for memory limit indicating unset and unlimited - if (app.memoryLimit > 0 && newConfig.manifest.memoryLimit && app.memoryLimit < newConfig.manifest.memoryLimit) { - newConfig.memoryLimit = newConfig.manifest.memoryLimit; + // 0 and -1 are special updateConfig for memory limit indicating unset and unlimited + if (app.memoryLimit > 0 && updateConfig.manifest.memoryLimit && app.memoryLimit < updateConfig.manifest.memoryLimit) { + updateConfig.memoryLimit = updateConfig.manifest.memoryLimit; } - appdb.setInstallationCommand(appId, data.force ? appdb.ISTATE_PENDING_FORCE_UPDATE : appdb.ISTATE_PENDING_UPDATE, { newConfig: newConfig }, function (error) { + appdb.setInstallationCommand(appId, data.force ? appdb.ISTATE_PENDING_FORCE_UPDATE : appdb.ISTATE_PENDING_UPDATE, { updateConfig: updateConfig }, function (error) { if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE)); // might be a bad guess if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error)); @@ -755,22 +791,22 @@ function restore(appId, data, auditSource, callback) { 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.getRestoreConfig.bind(null, data.backupId) : function (next) { return next(null, { manifest: app.manifest }); }; + var func = data.backupId ? backups.get.bind(null, data.backupId) : function (next) { return next(null, { manifest: app.manifest }); }; - func(function (error, restoreConfig) { + func(function (error, backupInfo) { if (error && error.reason === BackupsError.NOT_FOUND) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message)); if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message)); if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error)); - if (!restoreConfig) callback(new AppsError(AppsError.EXTERNAL_ERROR, 'Could not get restore config')); + if (!backupInfo.manifest) callback(new AppsError(AppsError.EXTERNAL_ERROR, 'Could not get restore manifest')); // re-validate because this new box version may not accept old configs - error = checkManifestConstraints(restoreConfig.manifest); + error = checkManifestConstraints(backupInfo.manifest); if (error) return callback(error); var values = { - lastBackupId: data.backupId || null, // when null, apptask simply reinstalls - manifest: restoreConfig.manifest, + restoreConfig: data.backupId ? { backupId: data.backupId, backupFormat: backupInfo.format } : null, // when null, apptask simply reinstalls + manifest: backupInfo.manifest, oldConfig: getAppConfig(app) }; @@ -798,37 +834,39 @@ function clone(appId, data, auditSource, callback) { debug('Will clone app with id:%s', appId); var location = data.location.toLowerCase(), + domain = data.domain.toLowerCase(), portBindings = data.portBindings || null, backupId = data.backupId; assert.strictEqual(typeof backupId, 'string'); assert.strictEqual(typeof location, 'string'); + assert.strictEqual(typeof domain, 'string'); assert.strictEqual(typeof portBindings, 'object'); 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.getRestoreConfig(backupId, function (error, restoreConfig) { + 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, error.message)); if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error)); - if (!restoreConfig) callback(new AppsError(AppsError.EXTERNAL_ERROR, 'Could not get restore config')); + if (!backupInfo.manifest) callback(new AppsError(AppsError.EXTERNAL_ERROR, 'Could not get restore config')); // re-validate because this new box version may not accept old configs - error = checkManifestConstraints(restoreConfig.manifest); + error = checkManifestConstraints(backupInfo.manifest); if (error) return callback(error); - error = validateHostname(location, config.fqdn()); + error = validateHostname(location, domain); if (error) return callback(error); - error = validatePortBindings(portBindings, restoreConfig.manifest.tcpPorts); + error = validatePortBindings(portBindings, backupInfo.manifest.tcpPorts); if (error) return callback(error); - var newAppId = uuid.v4(), appStoreId = app.appStoreId, manifest = restoreConfig.manifest; + var newAppId = uuid.v4(), appStoreId = app.appStoreId, manifest = backupInfo.manifest; - appstore.purchase(newAppId, appStoreId, function (error) { + appstore.purchase(newAppId, app.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)); @@ -839,12 +877,12 @@ function clone(appId, data, auditSource, callback) { memoryLimit: app.memoryLimit, accessRestriction: app.accessRestriction, xFrameOptions: app.xFrameOptions, - lastBackupId: backupId, + restoreConfig: { backupId: backupId, backupFormat: backupInfo.format }, sso: !!app.sso, mailboxName: (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app' }; - appdb.add(newAppId, appStoreId, manifest, location, portBindings, data, function (error) { + appdb.add(newAppId, app.appStoreId, manifest, location, domain, portBindings, data, function (error) { if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location, portBindings, error)); if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error)); @@ -1022,7 +1060,7 @@ function autoupdateApps(updateInfo, auditSource, callback) { // updateInfo is { if (error) { debug('Cannot autoupdate app %s : %s', appId, error.message); return iteratorDone(); - } + } error = canAutoupdateApp(app, updateInfo[appId].manifest); if (error) { @@ -1090,12 +1128,16 @@ function restoreInstalledApps(callback) { if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error)); async.map(apps, function (app, iteratorDone) { - debug('marking %s for restore', app.location || app.id); + debug('marking %s for restore', config.appFqdn(app)); - appdb.setInstallationCommand(app.id, appdb.ISTATE_PENDING_RESTORE, { oldConfig: null }, function (error) { - if (error) debug('did not mark %s for restore', app.location || app.id, error); + backups.getByAppIdPaged(1, 1, app.id, function (error, results) { + var restoreConfig = !error && results.length ? { backupId: results[0].id, backupFormat: results[0].format } : null; - iteratorDone(); // always succeed + appdb.setInstallationCommand(app.id, appdb.ISTATE_PENDING_RESTORE, { restoreConfig: restoreConfig, oldConfig: null }, function (error) { + if (error) debug('did not mark %s for restore', config.appFqdn(app), error); + + iteratorDone(); // always succeed + }); }); }, callback); }); @@ -1108,10 +1150,10 @@ function configureInstalledApps(callback) { if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error)); async.map(apps, function (app, iteratorDone) { - debug('marking %s for reconfigure', app.location || app.id); + debug('marking %s for reconfigure', config.appFqdn(app)); appdb.setInstallationCommand(app.id, appdb.ISTATE_PENDING_CONFIGURE, { oldConfig: null }, function (error) { - if (error) debug('did not mark %s for reconfigure', app.location || app.id, error); + if (error) debug('did not mark %s for reconfigure', config.appFqdn(app), error); iteratorDone(); // always succeed }); @@ -1193,7 +1235,7 @@ function uploadFile(appId, sourceFilePath, destFilePath, callback) { if (error) return callback(error); var readFile = fs.createReadStream(sourceFilePath); - readFile.on('error', console.error); + readFile.on('error', callback); readFile.pipe(stream); diff --git a/src/appstore.js b/src/appstore.js index b01d0c814..fcc858217 100644 --- a/src/appstore.js +++ b/src/appstore.js @@ -13,6 +13,8 @@ exports = module.exports = { getAccount: getAccount, + sendFeedback: sendFeedback, + AppstoreError: AppstoreError }; @@ -156,10 +158,6 @@ function sendAliveStatus(data, callback) { if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error)); var backendSettings = { - dnsConfig: { - provider: result[settings.DNS_CONFIG_KEY].provider, - wildcard: result[settings.DNS_CONFIG_KEY].provider === 'manual' ? result[settings.DNS_CONFIG_KEY].wildcard : undefined - }, tlsConfig: { provider: result[settings.TLS_CONFIG_KEY].provider }, @@ -267,3 +265,26 @@ function getAccount(callback) { }); }); } + +function sendFeedback(info, callback) { + assert.strictEqual(typeof info, 'object'); + assert.strictEqual(typeof info.email, 'string'); + assert.strictEqual(typeof info.displayName, 'string'); + assert.strictEqual(typeof info.type, 'string'); + assert.strictEqual(typeof info.subject, 'string'); + assert.strictEqual(typeof info.description, 'string'); + assert.strictEqual(typeof callback, 'function'); + + getAppstoreConfig(function (error, appstoreConfig) { + if (error) return callback(error); + + 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))); + + callback(null); + }); + }); +} diff --git a/src/apptask.js b/src/apptask.js index 4a0b4969f..7ca527557 100644 --- a/src/apptask.js +++ b/src/apptask.js @@ -38,6 +38,8 @@ var addons = require('./addons.js'), DatabaseError = require('./databaseerror.js'), debug = require('debug')('box:apptask'), docker = require('./docker.js'), + domains = require('./domains.js'), + DomainError = domains.DomainError, ejs = require('ejs'), fs = require('fs'), manifestFormat = require('cloudron-manifestformat'), @@ -48,8 +50,6 @@ var addons = require('./addons.js'), paths = require('./paths.js'), safe = require('safetydance'), shell = require('./shell.js'), - SubdomainError = require('./subdomains.js').SubdomainError, - subdomains = require('./subdomains.js'), superagent = require('superagent'), sysinfo = require('./sysinfo.js'), tld = require('tldjs'), @@ -72,7 +72,7 @@ function initialize(callback) { function debugApp(app) { assert.strictEqual(typeof app, 'object'); - var prefix = app ? (app.location || '(bare)') : '(no app)'; + var prefix = app ? (config.appFqdn(app) || '(bare)') : '(no app)'; debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1))); } @@ -252,18 +252,18 @@ 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.location, overwrite); + debugApp(app, 'Registering subdomain location [%s] overwrite: %s', config.appFqdn(app), overwrite); // get the current record before updating it - subdomains.get(app.location, '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 // note that the appstore sets up the naked domain for non-custom domains if (config.isCustomDomain() && values.length !== 0 && !overwrite) return retryCallback(null, new Error('DNS Record already exists')); - subdomains.upsert(app.location, 'A', [ ip ], function (error, changeId) { - if (error && (error.reason === SubdomainError.STILL_BUSY || error.reason === SubdomainError.EXTERNAL_ERROR)) return retryCallback(error); // try again + 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); }); @@ -277,11 +277,15 @@ function registerSubdomain(app, overwrite, callback) { }); } -function unregisterSubdomain(app, location, callback) { +function unregisterSubdomain(app, location, domain, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof location, 'string'); + assert.strictEqual(typeof domain, 'string'); assert.strictEqual(typeof callback, 'function'); + // FIXME remove the oldConfig.domain fallback in following releases + domain = domain || config.fqdn(); + // do not unregister bare domain because we show a error/cloudron info page there if (!config.isCustomDomain() && location === '') { debugApp(app, 'Skip unregister of empty subdomain'); @@ -297,10 +301,10 @@ function unregisterSubdomain(app, location, callback) { if (error) return callback(error); async.retry({ times: 30, interval: 5000 }, function (retryCallback) { - debugApp(app, 'Unregistering subdomain: %s', location); + debugApp(app, 'Unregistering subdomain: %s', config.appFqdn({ domain: domain, location: location })); - subdomains.remove(location, 'A', [ ip ], function (error) { - if (error && (error.reason === SubdomainError.STILL_BUSY || error.reason === SubdomainError.EXTERNAL_ERROR)) return retryCallback(error); // try again + domains.removeDNSRecords(location, domain, 'A', [ ip ], function (error) { + if (error && (error.reason === DomainError.STILL_BUSY || error.reason === DomainError.EXTERNAL_ERROR)) return retryCallback(error); // try again retryCallback(null, error); }); @@ -334,7 +338,7 @@ function waitForDnsPropagation(app, callback) { sysinfo.getPublicIp(function (error, ip) { if (error) return callback(error); - subdomains.waitForDns(config.appFqdn(app.location), ip, 'A', { interval: 5000, times: 120 }, callback); + domains.waitForDNSRecord(config.appFqdn(app), app.domain, ip, 'A', { interval: 5000, times: 120 }, callback); }); } @@ -348,10 +352,10 @@ function waitForAltDomainDnsPropagation(app, callback) { sysinfo.getPublicIp(function (error, ip) { if (error) return callback(error); - subdomains.waitForDns(app.altDomain, ip, 'A', { interval: 10000, times: 60 }, callback); + domains.waitForDNSRecord(app.altDomain, tld.getDomain(app.altDomain), ip, 'A', { interval: 10000, times: 60 }, callback); }); } else { - subdomains.waitForDns(app.altDomain, config.appFqdn(app.location) + '.', 'CNAME', { interval: 10000, times: 60 }, callback); + domains.waitForDNSRecord(app.altDomain, tld.getDomain(app.altDomain), config.appFqdn(app) + '.', 'CNAME', { interval: 10000, times: 60 }, callback); } } @@ -389,7 +393,7 @@ function install(app, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof callback, 'function'); - const backupId = app.lastBackupId, isRestoring = app.installationState === appdb.ISTATE_PENDING_RESTORE; + const restoreConfig = app.restoreConfig, isRestoring = app.installationState === appdb.ISTATE_PENDING_RESTORE; async.series([ // this protects against the theoretical possibility of an app being marked for install/restore from @@ -429,7 +433,7 @@ function install(app, callback) { createVolume.bind(null, app), function restoreFromBackup(next) { - if (!backupId) { + if (!restoreConfig) { async.series([ updateApp.bind(null, app, { installationProgress: '60, Setting up addons' }), addons.setupAddons.bind(null, app, app.manifest.addons), @@ -437,7 +441,7 @@ function install(app, callback) { } else { async.series([ updateApp.bind(null, app, { installationProgress: '60, Download backup and restoring addons' }), - backups.restoreApp.bind(null, app, app.manifest.addons, backupId), + backups.restoreApp.bind(null, app, app.manifest.addons, restoreConfig), ], next); } }, @@ -457,7 +461,7 @@ function install(app, callback) { exports._waitForDnsPropagation.bind(null, app), updateApp.bind(null, app, { installationProgress: '90, Waiting for External Domain setup' }), - exports._waitForAltDomainDnsPropagation.bind(null, app), // required when restoring and !lastBackupId + exports._waitForAltDomainDnsPropagation.bind(null, app), // required when restoring and !restoreConfig updateApp.bind(null, app, { installationProgress: '95, Configure nginx' }), configureNginx.bind(null, app), @@ -482,7 +486,7 @@ function backup(app, callback) { async.series([ updateApp.bind(null, app, { installationProgress: '10, Backing up' }), - backups.backupApp.bind(null, app, app.manifest), + backups.backupApp.bind(null, app), // done! function (callback) { @@ -504,7 +508,7 @@ function configure(app, callback) { assert.strictEqual(typeof callback, 'function'); // oldConfig can be null during an infra update - var locationChanged = app.oldConfig && app.oldConfig.location !== app.location; + var locationChanged = app.oldConfig && (config.appFqdn(app.oldConfig) !== config.appFqdn(app)); async.series([ updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }), @@ -515,7 +519,7 @@ function configure(app, callback) { deleteContainers.bind(null, app), function (next) { if (!locationChanged) return next(); - unregisterSubdomain(app, app.oldConfig.location, next); + unregisterSubdomain(app, app.oldConfig.location, app.oldConfig.domain, next); }, reserveHttpPort.bind(null, app), @@ -575,31 +579,34 @@ function update(app, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof callback, 'function'); - debugApp(app, `Updating to ${app.newConfig.manifest.version}`); + debugApp(app, `Updating to ${app.updateConfig.manifest.version}`); // app does not want these addons anymore // FIXME: this does not handle option changes (like multipleDatabases) - var unusedAddons = _.omit(app.manifest.addons, Object.keys(app.newConfig.manifest.addons)); + var unusedAddons = _.omit(app.manifest.addons, Object.keys(app.updateConfig.manifest.addons)); async.series([ // this protects against the theoretical possibility of an app being marked for update from // a previous version of box code updateApp.bind(null, app, { installationProgress: '0, Verify manifest' }), - verifyManifest.bind(null, app.newConfig.manifest), + verifyManifest.bind(null, app.updateConfig.manifest), function (next) { if (app.installationState === appdb.ISTATE_PENDING_FORCE_UPDATE) return next(null); async.series([ updateApp.bind(null, app, { installationProgress: '15, Backing up app' }), - backups.backupApp.bind(null, app, app.manifest) - ], next); + backups.backupApp.bind(null, app) + ], function (error) { + if (error) error.backupError = true; + next(error); + }); }, // download new image before app is stopped. this is so we can reduce downtime // and also not remove the 'common' layers when the old image is deleted updateApp.bind(null, app, { installationProgress: '25, Downloading image' }), - docker.downloadImage.bind(null, app.newConfig.manifest), + docker.downloadImage.bind(null, app.updateConfig.manifest), // note: we cleanup first and then backup. this is done so that the app is not running should backup fail // we cannot easily 'recover' from backup failures because we have to revert manfest and portBindings @@ -609,7 +616,7 @@ function update(app, callback) { stopApp.bind(null, app), deleteContainers.bind(null, app), function deleteImageIfChanged(done) { - if (app.manifest.dockerImage === app.newConfig.manifest.dockerImage) return done(); + if (app.manifest.dockerImage === app.updateConfig.manifest.dockerImage) return done(); docker.deleteImage(app.manifest, done); }, @@ -621,7 +628,7 @@ function update(app, callback) { function (next) { // make sure we always have objects var currentPorts = app.portBindings || {}; - var newPorts = app.newConfig.manifest.tcpPorts || {}; + var newPorts = app.updateConfig.manifest.tcpPorts || {}; async.each(Object.keys(currentPorts), function (portName, callback) { if (newPorts[portName]) return callback(); // port still in use @@ -639,7 +646,7 @@ function update(app, callback) { }, // switch over to the new config. manifest, memoryLimit, portBindings, appstoreId are updated here - updateApp.bind(null, app, app.newConfig), + updateApp.bind(null, app, app.updateConfig), updateApp.bind(null, app, { installationProgress: '45, Downloading icon' }), downloadIcon.bind(null, app), @@ -661,14 +668,18 @@ function update(app, callback) { // done! function (callback) { debugApp(app, 'updated'); - updateApp(app, { installationState: appdb.ISTATE_INSTALLED, installationProgress: '', health: null, newConfig: null }, callback); + updateApp(app, { installationState: appdb.ISTATE_INSTALLED, installationProgress: '', health: null, updateConfig: null, updateTime: new Date() }, callback); } ], function seriesDone(error) { - if (error) { + if (error && error.backupError) { + debugApp(app, 'update aborted because backup failed', error); + updateApp(app, { installationState: appdb.ISTATE_INSTALLED, installationProgress: '', health: null, updateConfig: null }, callback.bind(null, error)); + } else if (error) { debugApp(app, 'Error updating app: %s', error); - return updateApp(app, { installationState: appdb.ISTATE_ERROR, installationProgress: error.message }, callback.bind(null, error)); + updateApp(app, { installationState: appdb.ISTATE_ERROR, installationProgress: error.message, updateTime: new Date() }, callback.bind(null, error)); + } else { + callback(null); } - callback(null); }); } @@ -701,7 +712,7 @@ function uninstall(app, callback) { docker.deleteImage.bind(null, app.manifest), updateApp.bind(null, app, { installationProgress: '60, Unregistering subdomain' }), - unregisterSubdomain.bind(null, app, app.location), + unregisterSubdomain.bind(null, app, app.location, app.domain), updateApp.bind(null, app, { installationProgress: '80, Cleanup icon' }), removeIcon.bind(null, app), diff --git a/src/backupdb.js b/src/backupdb.js index 6bd257520..08fbb6b1e 100644 --- a/src/backupdb.js +++ b/src/backupdb.js @@ -6,7 +6,7 @@ var assert = require('assert'), safe = require('safetydance'), util = require('util'); -var BACKUPS_FIELDS = [ 'id', 'creationTime', 'version', 'type', 'dependsOn', 'state', 'restoreConfigJson', 'format' ]; +var BACKUPS_FIELDS = [ 'id', 'creationTime', 'version', 'type', 'dependsOn', 'state', 'manifestJson', 'format' ]; exports = module.exports = { add: add, @@ -34,8 +34,8 @@ function postProcess(result) { result.dependsOn = result.dependsOn ? result.dependsOn.split(',') : [ ]; - result.restoreConfig = result.restoreConfigJson ? safe.JSON.parse(result.restoreConfigJson) : null; - delete result.restoreConfigJson; + result.manifest = result.manifestJson ? safe.JSON.parse(result.manifestJson) : null; + delete result.manifestJson; } function getByTypeAndStatePaged(type, state, page, perPage, callback) { @@ -109,15 +109,15 @@ function add(backup, callback) { assert.strictEqual(typeof backup.version, 'string'); assert(backup.type === exports.BACKUP_TYPE_APP || backup.type === exports.BACKUP_TYPE_BOX); assert(util.isArray(backup.dependsOn)); - assert.strictEqual(typeof backup.restoreConfig, 'object'); + assert.strictEqual(typeof backup.manifest, 'object'); assert.strictEqual(typeof backup.format, 'string'); assert.strictEqual(typeof callback, 'function'); var creationTime = backup.creationTime || new Date(); // allow tests to set the time - var restoreConfig = backup.restoreConfig ? JSON.stringify(backup.restoreConfig) : ''; + var manifestJson = JSON.stringify(backup.manifest); - database.query('INSERT INTO backups (id, version, type, creationTime, state, dependsOn, restoreConfigJson, format) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', - [ backup.id, backup.version, backup.type, creationTime, exports.BACKUP_STATE_NORMAL, backup.dependsOn.join(','), restoreConfig, backup.format ], + database.query('INSERT INTO backups (id, version, type, creationTime, state, dependsOn, manifestJson, format) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', + [ backup.id, backup.version, backup.type, creationTime, exports.BACKUP_STATE_NORMAL, backup.dependsOn.join(','), manifestJson, backup.format ], function (error) { if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS)); if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error)); diff --git a/src/backups.js b/src/backups.js index 3ced90829..871d84ac3 100644 --- a/src/backups.js +++ b/src/backups.js @@ -8,18 +8,19 @@ exports = module.exports = { getByStatePaged: getByStatePaged, getByAppIdPaged: getByAppIdPaged, - getRestoreConfig: getRestoreConfig, + get: get, ensureBackup: ensureBackup, backup: backup, + restore: restore, + backupApp: backupApp, restoreApp: restoreApp, backupBoxAndApps: backupBoxAndApps, upload: upload, - download: download, cleanup: cleanup, cleanupCacheFilesSync: cleanupCacheFilesSync, @@ -41,6 +42,7 @@ var addons = require('./addons.js'), backupdb = require('./backupdb.js'), config = require('./config.js'), crypto = require('crypto'), + database = require('./database.js'), DatabaseError = require('./databaseerror.js'), debug = require('debug')('box:backups'), eventlog = require('./eventlog.js'), @@ -68,7 +70,7 @@ var BACKUPTASK_CMD = path.join(__dirname, 'backuptask.js'); function debugApp(app) { assert(!app || typeof app === 'object'); - var prefix = app ? app.location : '(no app)'; + var prefix = app ? config.appFqdn(app) : '(no app)'; debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1))); } @@ -151,16 +153,15 @@ function getByAppIdPaged(page, perPage, appId, callback) { }); } -function getRestoreConfig(backupId, callback) { +function get(backupId, callback) { assert.strictEqual(typeof backupId, 'string'); assert.strictEqual(typeof callback, 'function'); backupdb.get(backupId, function (error, result) { if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new BackupsError(BackupsError.NOT_FOUND, error)); if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error)); - if (!result.restoreConfig) return callback(new BackupsError(BackupsError.NOT_FOUND, error)); - callback(null, result.restoreConfig); + callback(null, result); }); } @@ -370,8 +371,10 @@ function restoreFsMetadata(appDataDir, callback) { log('Recreating empty directories'); - var metadata = safe.JSON.parse(safe.fs.readFileSync(path.join(appDataDir, 'fsmetadata.json'), 'utf8')); - if (metadata === null) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, 'Error loading fsmetadata.txt:' + safe.error.message)); + var metadataJson = safe.fs.readFileSync(path.join(appDataDir, 'fsmetadata.json'), 'utf8') + if (metadataJson === null) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, 'Error loading fsmetadata.txt:' + safe.error.message)); + var metadata = safe.JSON.parse(metadataJson); + if (metadata === null) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, 'Error parsing fsmetadata.txt:' + safe.error.message)); async.eachSeries(metadata.emptyDirs, function createPath(emptyDir, iteratorDone) { mkdirp(path.join(appDataDir, emptyDir), iteratorDone); @@ -388,7 +391,8 @@ function restoreFsMetadata(appDataDir, callback) { }); } -function download(backupId, format, dataDir, callback) { +function download(backupConfig, backupId, format, dataDir, callback) { + assert.strictEqual(typeof backupConfig, 'object'); assert.strictEqual(typeof backupId, 'string'); assert.strictEqual(typeof format, 'string'); assert.strictEqual(typeof dataDir, 'string'); @@ -398,44 +402,54 @@ function download(backupId, format, dataDir, callback) { log(`Downloading ${backupId} of format ${format} to ${dataDir}`); - settings.getBackupConfig(function (error, backupConfig) { - if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error)); + if (format === 'tgz') { + api(backupConfig.provider).download(backupConfig, getBackupFilePath(backupConfig, backupId, format), function (error, sourceStream) { + if (error) return callback(error); - if (format === 'tgz') { - api(backupConfig.provider).download(backupConfig, getBackupFilePath(backupConfig, backupId, format), function (error, sourceStream) { - if (error) return callback(error); + tarExtract(sourceStream, dataDir, backupConfig.key || null, callback); + }); + } else { + var events = api(backupConfig.provider).downloadDir(backupConfig, getBackupFilePath(backupConfig, backupId, format), dataDir); + events.on('progress', log); + events.on('done', function (error) { + if (error) return callback(error); - tarExtract(sourceStream, dataDir, backupConfig.key || null, callback); - }); - } else { - var events = api(backupConfig.provider).downloadDir(backupConfig, getBackupFilePath(backupConfig, backupId, format), dataDir); - events.on('progress', log); - events.on('done', function (error) { - if (error) return callback(error); + restoreFsMetadata(dataDir, callback); + }); + } +} - restoreFsMetadata(dataDir, callback); - }); - } +function restore(backupConfig, backupId, callback) { + assert.strictEqual(typeof backupConfig, 'object'); + assert.strictEqual(typeof backupId, 'string'); + assert.strictEqual(typeof callback, 'function'); + + download(backupConfig, backupId, backupConfig.format, paths.BOX_DATA_DIR, function (error) { + if (error) return callback(error); + + database.importFromFile(`${paths.BOX_DATA_DIR}/box.mysqldump`, function (error) { + if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error)); + + callback(); + }); }); } -function restoreApp(app, addonsToRestore, backupId, callback) { +function restoreApp(app, addonsToRestore, restoreConfig, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof addonsToRestore, 'object'); - assert.strictEqual(typeof backupId, 'string'); + assert.strictEqual(typeof restoreConfig, 'object'); assert.strictEqual(typeof callback, 'function'); - assert(app.lastBackupId); var appDataDir = safe.fs.realpathSync(path.join(paths.APPS_DATA_DIR, app.id)); var startTime = new Date(); - backupdb.get(backupId, function (error, result) { - if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new BackupsError(BackupsError.NOT_FOUND, error)); + settings.getBackupConfig(function (error, backupConfig) { if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error)); async.series([ - download.bind(null, backupId, result.format, appDataDir), + download.bind(null, backupConfig, restoreConfig.backupId, restoreConfig.backupFormat, appDataDir), addons.restoreAddons.bind(null, app, addonsToRestore) ], function (error) { debug('restoreApp: time: %s', (new Date() - startTime)/1000); @@ -513,13 +527,7 @@ function snapshotBox(callback) { log('Snapshotting box'); - var password = config.database().password ? '-p' + config.database().password : '--skip-password'; - var mysqlDumpArgs = [ - '-c', - `/usr/bin/mysqldump -u root ${password} --single-transaction --routines \ - --triggers ${config.database().name} > "${paths.BOX_DATA_DIR}/box.mysqldump"` - ]; - shell.exec('backupBox', '/bin/bash', mysqlDumpArgs, { }, function (error) { + database.exportToFile(`${paths.BOX_DATA_DIR}/box.mysqldump`, function (error) { if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error)); return callback(); @@ -560,7 +568,7 @@ function rotateBoxBackup(backupConfig, timestamp, appBackupIds, callback) { log(`Rotating box backup to id ${backupId}`); - backupdb.add({ id: backupId, version: config.version(), type: backupdb.BACKUP_TYPE_BOX, dependsOn: appBackupIds, restoreConfig: null, format: format }, function (error) { + backupdb.add({ id: backupId, version: config.version(), type: backupdb.BACKUP_TYPE_BOX, dependsOn: appBackupIds, manifest: null, format: format }, function (error) { if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error)); var copy = api(backupConfig.provider).copy(backupConfig, getBackupFilePath(backupConfig, 'snapshot/box', format), getBackupFilePath(backupConfig, backupId, format)); @@ -610,36 +618,19 @@ function canBackupApp(app) { app.installationState === appdb.ISTATE_PENDING_UPDATE; // called from apptask } -function snapshotApp(app, manifest, callback) { +function snapshotApp(app, callback) { assert.strictEqual(typeof app, 'object'); - assert(manifest && typeof manifest === 'object'); assert.strictEqual(typeof callback, 'function'); log(`Snapshotting app ${app.id}`); - var restoreConfig = apps.getAppConfig(app); - restoreConfig.manifest = manifest; - - if (!safe.fs.writeFileSync(path.join(paths.APPS_DATA_DIR, app.id + '/config.json'), JSON.stringify(restoreConfig))) { + if (!safe.fs.writeFileSync(path.join(paths.APPS_DATA_DIR, app.id + '/config.json'), JSON.stringify(apps.getAppConfig(app)))) { return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, 'Error creating config.json: ' + safe.error.message)); } - addons.backupAddons(app, manifest.addons, function (error) { + addons.backupAddons(app, app.manifest.addons, function (error) { if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message)); - return callback(null, restoreConfig); - }); -} - -function setRestorePoint(appId, lastBackupId, callback) { - assert.strictEqual(typeof appId, 'string'); - assert.strictEqual(typeof lastBackupId, 'string'); - assert.strictEqual(typeof callback, 'function'); - - appdb.update(appId, { lastBackupId: lastBackupId }, function (error) { - if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new BackupsError(BackupsError.NOT_FOUND, 'No such app')); - if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error)); - return callback(null); }); } @@ -654,14 +645,13 @@ function rotateAppBackup(backupConfig, app, timestamp, callback) { if (!snapshotInfo) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, 'Snapshot info missing or corrupt')); var snapshotTime = snapshotInfo.timestamp.replace(/[T.]/g, '-').replace(/[:Z]/g,''); - var restoreConfig = snapshotInfo.restoreConfig; - var manifest = restoreConfig.manifest; + var manifest = snapshotInfo.restoreConfig ? snapshotInfo.restoreConfig.manifest : snapshotInfo.manifest; // compat var backupId = util.format('%s/app_%s_%s_v%s', timestamp, app.id, snapshotTime, manifest.version); const format = backupConfig.format; log(`Rotating app backup of ${app.id} to id ${backupId}`); - backupdb.add({ id: backupId, version: manifest.version, type: backupdb.BACKUP_TYPE_APP, dependsOn: [ ], restoreConfig: restoreConfig, format: format }, function (error) { + backupdb.add({ id: backupId, version: manifest.version, type: backupdb.BACKUP_TYPE_APP, dependsOn: [ ], manifest: manifest, format: format }, function (error) { if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error)); var copy = api(backupConfig.provider).copy(backupConfig, getBackupFilePath(backupConfig, `snapshot/app_${app.id}`, format), getBackupFilePath(backupConfig, backupId, format)); @@ -675,27 +665,22 @@ function rotateAppBackup(backupConfig, app, timestamp, callback) { log(`Rotated app backup of ${app.id} successfully to id ${backupId}`); - setRestorePoint(app.id, backupId, function (error) { - if (error) return callback(error); - - return callback(null, backupId); - }); + callback(null, backupId); }); }); }); } -function uploadAppSnapshot(backupConfig, app, manifest, callback) { +function uploadAppSnapshot(backupConfig, app, callback) { assert.strictEqual(typeof backupConfig, 'object'); assert.strictEqual(typeof app, 'object'); - assert(manifest && typeof manifest === 'object'); assert.strictEqual(typeof callback, 'function'); if (!canBackupApp(app)) return callback(); // nothing to do var startTime = new Date(); - snapshotApp(app, manifest, function (error, restoreConfig) { + snapshotApp(app, function (error) { if (error) return callback(error); var backupId = util.format('snapshot/app_%s', app.id); @@ -705,14 +690,13 @@ function uploadAppSnapshot(backupConfig, app, manifest, callback) { debugApp(app, 'uploadAppSnapshot: %s done time: %s secs', backupId, (new Date() - startTime)/1000); - setSnapshotInfo(app.id, { timestamp: new Date().toISOString(), restoreConfig: restoreConfig, format: backupConfig.format }, callback); + setSnapshotInfo(app.id, { timestamp: new Date().toISOString(), manifest: app.manifest, format: backupConfig.format }, callback); }); }); } -function backupAppWithTimestamp(app, manifest, timestamp, callback) { +function backupAppWithTimestamp(app, timestamp, callback) { assert.strictEqual(typeof app, 'object'); - assert(manifest && typeof manifest === 'object'); assert.strictEqual(typeof timestamp, 'string'); assert.strictEqual(typeof callback, 'function'); @@ -721,7 +705,7 @@ function backupAppWithTimestamp(app, manifest, timestamp, callback) { settings.getBackupConfig(function (error, backupConfig) { if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error)); - uploadAppSnapshot(backupConfig, app, manifest, function (error) { + uploadAppSnapshot(backupConfig, app, function (error) { if (error) return callback(error); rotateAppBackup(backupConfig, app, timestamp, callback); @@ -729,17 +713,16 @@ function backupAppWithTimestamp(app, manifest, timestamp, callback) { }); } -function backupApp(app, manifest, callback) { +function backupApp(app, callback) { assert.strictEqual(typeof app, 'object'); - assert(manifest && typeof manifest === 'object'); assert.strictEqual(typeof callback, 'function'); 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.altDomain || config.appFqdn(app.location))); + progress.set(progress.BACKUP, 10, 'Backing up ' + (app.altDomain || config.appFqdn(app))); - backupAppWithTimestamp(app, manifest, timestamp, function (error) { + backupAppWithTimestamp(app, timestamp, function (error) { progress.set(progress.BACKUP, 100, error ? error.message : ''); callback(error); @@ -764,22 +747,22 @@ 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.altDomain || config.appFqdn(app.location))); + progress.set(progress.BACKUP, step * processed, 'Backing up ' + (app.altDomain || config.appFqdn(app))); ++processed; if (!app.enableBackup) { - progress.set(progress.BACKUP, step * processed, 'Skipped backup ' + (app.altDomain || config.appFqdn(app.location))); - return iteratorCallback(null, app.lastBackupId); // just use the last backup + progress.set(progress.BACKUP, step * processed, 'Skipped backup ' + (app.altDomain || config.appFqdn(app))); + return iteratorCallback(null, null); // nothing to backup } - backupAppWithTimestamp(app, app.manifest, timestamp, function (error, backupId) { + backupAppWithTimestamp(app, timestamp, function (error, backupId) { if (error && error.reason !== BackupsError.BAD_STATE) { debugApp(app, 'Unable to backup', error); return iteratorCallback(error); } - progress.set(progress.BACKUP, step * processed, 'Backed up ' + (app.altDomain || config.appFqdn(app.location))); + progress.set(progress.BACKUP, step * processed, 'Backed up ' + (app.altDomain || config.appFqdn(app))); iteratorCallback(null, backupId || null); // clear backupId if is in BAD_STATE and never backed up }); @@ -1030,3 +1013,4 @@ function cleanup(auditSource, callback) { }); }); } + diff --git a/src/cert/acme.js b/src/cert/acme.js index b007314d4..72da06ef2 100644 --- a/src/cert/acme.js +++ b/src/cert/acme.js @@ -16,7 +16,7 @@ var assert = require('assert'), var CA_PROD = 'https://acme-v01.api.letsencrypt.org', CA_STAGING = 'https://acme-staging.api.letsencrypt.org', - LE_AGREEMENT = 'https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf'; + LE_AGREEMENT = 'https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf'; exports = module.exports = { getCertificate: getCertificate, diff --git a/src/cert/fallback.js b/src/cert/fallback.js index 8527bdb07..fa645f68a 100644 --- a/src/cert/fallback.js +++ b/src/cert/fallback.js @@ -17,5 +17,5 @@ function getCertificate(domain, options, callback) { debug('getCertificate: using fallback certificate', domain); - return callback(null, 'cert/host.cert', 'cert/host.key'); + return callback(null, '', ''); } diff --git a/src/certificates.js b/src/certificates.js index c3ae8a69f..afc9a1da5 100644 --- a/src/certificates.js +++ b/src/certificates.js @@ -5,6 +5,7 @@ exports = module.exports = { ensureFallbackCertificate: ensureFallbackCertificate, setFallbackCertificate: setFallbackCertificate, + getFallbackCertificate: getFallbackCertificate, validateCertificate: validateCertificate, ensureCertificate: ensureCertificate, @@ -121,6 +122,11 @@ function ensureFallbackCertificate(callback) { 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)); @@ -129,15 +135,6 @@ function ensureFallbackCertificate(callback) { return callback(); } - if (config.tlsCert() && config.tlsKey()) { - // cert from CaaS or cloudron-setup. these files should _not_ be part of the backup - debug('ensureFallbackCertificate: using CaaS/cloudron-setup fallback certs'); - if (!safe.fs.writeFileSync(fallbackCertPath, config.tlsCert())) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message)); - if (!safe.fs.writeFileSync(fallbackKeyPath, config.tlsKey())) 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()) { @@ -177,11 +174,11 @@ function renewAll(auditSource, callback) { apps.getAll(function (error, allApps) { if (error) return callback(error); - allApps.push({ location: config.adminLocation() }); // inject fake webadmin app + allApps.push({ location: config.adminLocation(), domain: config.fqdn() }); // inject fake webadmin app var expiringApps = [ ]; for (var i = 0; i < allApps.length; i++) { - var appDomain = allApps[i].altDomain || config.appFqdn(allApps[i].location); + var appDomain = allApps[i].altDomain || config.appFqdn(allApps[i]); var certFilePath = path.join(paths.APP_CERTS_DIR, appDomain + '.user.cert'); var keyFilePath = path.join(paths.APP_CERTS_DIR, appDomain + '.user.key'); @@ -205,10 +202,10 @@ function renewAll(auditSource, callback) { } } - debug('renewAll: %j needs to be renewed', expiringApps.map(function (a) { return a.altDomain || config.appFqdn(a.location); })); + debug('renewAll: %j needs to be renewed', expiringApps.map(function (app) { return app.altDomain || config.appFqdn(app); })); async.eachSeries(expiringApps, function iterator(app, iteratorCallback) { - var domain = app.altDomain || config.appFqdn(app.location); + var domain = app.altDomain || config.appFqdn(app); getApi(app, function (error, api, apiOptions) { if (error) return callback(error); @@ -232,14 +229,18 @@ function renewAll(auditSource, callback) { debug('renewAll: using fallback certs for %s since it expires soon', domain, error); - certFilePath = 'cert/host.cert'; - keyFilePath = 'cert/host.key'; + // 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.location === config.adminLocation() ? + var configureFunc = config.appFqdn(app) === config.adminFqdn() ? nginx.configureAdmin.bind(null, certFilePath, keyFilePath, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn()) : nginx.configureApp.bind(null, app, certFilePath, keyFilePath); @@ -276,51 +277,52 @@ function validateCertificate(cert, key, fqdn) { 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(util.format('could not get cert subject')); + 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" | \ + 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://g"`; + sed -e "s/DNS://g" -e "s/ //g" -e "s/,/ /g" -e "s/othername://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('cert is not valid for this domain. Expecting %s in %j', fqdn, 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 cert'); + if (certModulus !== keyModulus) return new Error('Key does not match the certificate.'); - // check expiration + // check expiration result = safe.child_process.execSync('openssl x509 -checkend 0', { encoding: 'utf8', input: cert }); - if (!result) return new Error('cert expired'); + if (!result) return new Error('Certificate is expired.'); return null; } -function setFallbackCertificate(cert, key, callback) { +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, '*.' + config.fqdn()); + 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, 'host.cert'), cert)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message)); - if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, 'host.key'), key)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message)); + 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, 'host.cert'), cert)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message)); - if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, 'host.key'), key)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message)); + 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, '*.' + config.fqdn()); + exports.events.emit(exports.EVENT_CERT_CHANGED, '*.' + fqdn); nginx.reload(function (error) { if (error) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, error)); @@ -329,11 +331,16 @@ function setFallbackCertificate(cert, key, callback) { }); } -function getFallbackCertificatePath(callback) { +function getFallbackCertificate(fqdn, callback) { + assert.strictEqual(typeof fqdn, 'string'); assert.strictEqual(typeof callback, 'function'); - // 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')); + 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) { @@ -371,7 +378,8 @@ function getAdminCertificatePath(callback) { if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, certFilePath, keyFilePath); - getFallbackCertificatePath(callback); + // 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) { @@ -394,7 +402,7 @@ function ensureCertificate(app, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof callback, 'function'); - var domain = app.altDomain || config.appFqdn(app.location); + var domain = app.altDomain || config.appFqdn(app); var certFilePath = path.join(paths.APP_CERTS_DIR, domain + '.user.cert'); var keyFilePath = path.join(paths.APP_CERTS_DIR, domain + '.user.key'); @@ -422,9 +430,15 @@ function ensureCertificate(app, callback) { 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); - return callback(null, 'cert/host.cert', 'cert/host.key'); // use fallback certs + 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); diff --git a/src/cloudron.js b/src/cloudron.js index 92c7da076..1980578a3 100644 --- a/src/cloudron.js +++ b/src/cloudron.js @@ -15,6 +15,7 @@ exports = module.exports = { sendCaasHeartbeat: sendCaasHeartbeat, updateToLatest: updateToLatest, + restore: restore, reboot: reboot, retire: retire, migrate: migrate, @@ -31,6 +32,7 @@ var appdb = require('./appdb.js'), assert = require('assert'), async = require('async'), backups = require('./backups.js'), + BackupsError = require('./backups.js').BackupsError, certificates = require('./certificates.js'), child_process = require('child_process'), clients = require('./clients.js'), @@ -39,6 +41,9 @@ var appdb = require('./appdb.js'), cron = require('./cron.js'), debug = require('debug')('box:cloudron'), df = require('@sindresorhus/df'), + domaindb = require('./domaindb.js'), + domains = require('./domains.js'), + DomainError = domains.DomainError, eventlog = require('./eventlog.js'), fs = require('fs'), locker = require('./locker.js'), @@ -50,12 +55,13 @@ var appdb = require('./appdb.js'), platform = require('./platform.js'), progress = require('./progress.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'), - subdomains = require('./subdomains.js'), superagent = require('superagent'), sysinfo = require('./sysinfo.js'), tld = require('tldjs'), @@ -68,7 +74,8 @@ var appdb = require('./appdb.js'), var REBOOT_CMD = path.join(__dirname, 'scripts/reboot.sh'), UPDATE_CMD = path.join(__dirname, 'scripts/update.sh'), - RETIRE_CMD = path.join(__dirname, 'scripts/retire.sh'); + RETIRE_CMD = path.join(__dirname, 'scripts/retire.sh'), + RESTART_CMD = path.join(__dirname, 'scripts/restart.sh'); var NOOP_CALLBACK = function (error) { if (error) debug(error); }; @@ -86,7 +93,7 @@ const BOX_AND_USER_TEMPLATE = { }; var gBoxAndUserDetails = null, // cached cloudron details like region,size... - gWebadminStatus = { dns: false, tls: false, configuring: false }; + gWebadminStatus = { dns: false, tls: false, configuring: false, restoring: false }; function CloudronError(reason, errorOrMessage) { assert.strictEqual(typeof reason, 'string'); @@ -120,15 +127,15 @@ CloudronError.SELF_UPGRADE_NOT_SUPPORTED = 'Self upgrade not supported'; function initialize(callback) { assert.strictEqual(typeof callback, 'function'); - gWebadminStatus = { dns: false, tls: false, configuring: false }; + gWebadminStatus = { dns: false, tls: false, configuring: false, restoring: false }; gBoxAndUserDetails = null; async.series([ certificates.initialize, settings.initialize, - installAppBundle, configureDefaultServer, - onDomainConfigured + onDomainConfigured, + onActivated ], function (error) { if (error) return callback(error); @@ -143,7 +150,6 @@ function uninitialize(callback) { async.series([ cron.uninitialize, - mailer.stop, platform.stop, certificates.uninitialize, settings.uninitialize @@ -159,12 +165,55 @@ function onDomainConfigured(callback) { clients.addDefaultClients, certificates.ensureFallbackCertificate, ensureDkimKey, - platform.start, // requires fallback certs for mail container - mailer.start, // this requires the "mail" container to be running - cron.initialize + cron.initialize // required for caas heartbeat before activation ], callback); } +function onActivated(callback) { + callback = callback || NOOP_CALLBACK; + + // Starting the platform after a user is available means: + // 1. mail bounces can now be sent to the cloudron owner + // 2. the restore code path can run without sudo (since mail/ is non-root) + user.count(function (error, count) { + if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error)); + if (!count) return callback(); // not activated + + platform.start(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(dnsConfig, domain, zoneName, callback) { assert.strictEqual(typeof dnsConfig, 'object'); assert.strictEqual(typeof domain, 'string'); @@ -177,19 +226,30 @@ function dnsSetup(dnsConfig, domain, zoneName, callback) { debug('dnsSetup: Setting up Cloudron with domain %s and zone %s', domain, zoneName); - settings.setDnsConfig(dnsConfig, domain, zoneName, function (error) { - if (error && error.reason === SettingsError.BAD_FIELD) return callback(new CloudronError(CloudronError.BAD_FIELD, error.message)); + 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)); - config.setFqdn(domain); // set fqdn only after dns config is valid, otherwise cannot re-setup if we failed - config.setZoneName(zoneName); + autoprovision(function (error) { + if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error)); - async.series([ // do not block - onDomainConfigured, - configureWebadmin - ], NOOP_CALLBACK); + config.setFqdn(domain); // set fqdn only after dns config is valid, otherwise cannot re-setup if we failed + config.setZoneName(zoneName); - callback(); + 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, dnsConfig, null /* cert */, done); + else domains.update(domain, dnsConfig, null /* cert */, done); }); } @@ -231,14 +291,14 @@ function configureWebadmin(callback) { function done(error) { gWebadminStatus.configuring = false; - debug('configureWebadmin: done error:%j', error); + debug('configureWebadmin: done error: %j', error || {}); callback(error); } function configureNginx(error) { - debug('configureNginx: dns update:%j', error); + debug('configureNginx: dns update: %j', error || {}); - certificates.ensureCertificate({ location: config.adminLocation() }, function (error, certFilePath, keyFilePath) { + certificates.ensureCertificate({ domain: config.fqdn(), location: config.adminLocation() }, function (error, certFilePath, keyFilePath) { if (error) return done(error); gWebadminStatus.tls = true; @@ -255,7 +315,7 @@ function configureWebadmin(callback) { addDnsRecords(ip, function (error) { if (error) return configureNginx(error); - subdomains.waitForDns(config.adminFqdn(), ip, 'A', { interval: 30000, times: 50000 }, function (error) { + domains.waitForDNSRecord(config.adminFqdn(), config.fqdn(), ip, 'A', { interval: 30000, times: 50000 }, function (error) { if (error) return configureNginx(error); gWebadminStatus.dns = true; @@ -321,7 +381,7 @@ function activate(username, password, email, displayName, ip, auditSource, callb eventlog.add(eventlog.ACTION_ACTIVATE, auditSource, { }); - platform.createMailConfig(NOOP_CALLBACK); // bounces can now be sent to the cloudron owner + onActivated(); callback(null, { token: token, expires: expires }); }); @@ -410,31 +470,26 @@ function getConfig(callback) { settings.getCloudronName(function (error, cloudronName) { if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error)); - settings.getDeveloperMode(function (error, developerMode) { - if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error)); - - callback(null, { - apiServerOrigin: config.apiServerOrigin(), - webServerOrigin: config.webServerOrigin(), - fqdn: config.fqdn(), - adminLocation: config.adminLocation(), - adminFqdn: config.adminFqdn(), - mailFqdn: config.mailFqdn(), - version: config.version(), - update: updateChecker.getUpdateInfo(), - progress: progress.getAll(), - isCustomDomain: config.isCustomDomain(), - isDemo: config.isDemo(), - developerMode: developerMode, - region: result.box.region, - size: result.box.size, - billing: !!result.user.billing, - plan: result.box.plan, - currency: result.user.currency, - memory: os.totalmem(), - provider: config.provider(), - cloudronName: cloudronName - }); + callback(null, { + apiServerOrigin: config.apiServerOrigin(), + webServerOrigin: config.webServerOrigin(), + fqdn: config.fqdn(), + adminLocation: config.adminLocation(), + adminFqdn: config.adminFqdn(), + mailFqdn: config.mailFqdn(), + version: config.version(), + update: updateChecker.getUpdateInfo(), + progress: progress.getAll(), + isCustomDomain: config.isCustomDomain(), + isDemo: config.isDemo(), + region: result.box.region, + size: result.box.size, + billing: !!result.user.billing, + plan: result.box.plan, + currency: result.user.currency, + memory: os.totalmem(), + provider: config.provider(), + cloudronName: cloudronName }); }); }); @@ -502,7 +557,7 @@ function readDkimPublicKeySync() { function txtRecordsWithSpf(callback) { assert.strictEqual(typeof callback, 'function'); - subdomains.get('', 'TXT', function (error, txtRecords) { + domains.getDNSRecords('', config.fqdn(), 'TXT', function (error, txtRecords) { if (error) return callback(error); debug('txtRecordsWithSpf: current txt records - %j', txtRecords); @@ -541,9 +596,9 @@ function addDnsRecords(ip, 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(), type: 'A', values: [ ip ] }; + 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', type: 'TXT', values: [ '"v=DKIM1; t=s; p=' + dkimKey + '"' ] }; + var dkimRecord = { subdomain: config.dkimSelector() + '._domainkey', domain: config.fqdn(), type: 'TXT', values: [ '"v=DKIM1; t=s; p=' + dkimKey + '"' ] }; var records = [ ]; if (config.isCustomDomain()) { @@ -551,7 +606,7 @@ function addDnsRecords(ip, callback) { records.push(dkimRecord); } else { // for non-custom domains, we show a noapp.html page - var nakedDomainRecord = { subdomain: '', type: 'A', values: [ ip ] }; + var nakedDomainRecord = { subdomain: '', domain: config.fqdn(), type: 'A', values: [ ip ] }; records.push(nakedDomainRecord); records.push(webadminRecord); @@ -564,12 +619,12 @@ function addDnsRecords(ip, callback) { txtRecordsWithSpf(function (error, txtRecords) { if (error) return retryCallback(error); - if (txtRecords) records.push({ subdomain: '', type: 'TXT', values: txtRecords }); + if (txtRecords) records.push({ subdomain: '', domain: config.fqdn(), type: 'TXT', values: txtRecords }); debug('addDnsRecords: will update %j', records); async.mapSeries(records, function (record, iteratorCallback) { - subdomains.upsert(record.subdomain, record.type, record.values, 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); @@ -585,6 +640,42 @@ function addDnsRecords(ip, callback) { }); } +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); } @@ -710,8 +801,6 @@ function doUpdate(boxUpdateInfo, callback) { webServerOrigin: config.webServerOrigin(), fqdn: config.fqdn(), adminLocation: config.adminLocation(), - tlsCert: config.tlsCert(), - tlsKey: config.tlsKey(), isCustomDomain: config.isCustomDomain(), isDemo: config.isDemo(), zoneName: config.zoneName(), @@ -741,36 +830,6 @@ function doUpdate(boxUpdateInfo, callback) { }); } -function installAppBundle(callback) { - assert.strictEqual(typeof callback, 'function'); - - if (fs.existsSync(paths.FIRST_RUN_FILE)) return callback(); - - var bundle = config.get('appBundle'); - debug('initialize: installing app bundle on first run: %j', bundle); - - if (!bundle || bundle.length === 0) return callback(); - - async.eachSeries(bundle, function (appInfo, iteratorCallback) { - debug('autoInstall: installing %s at %s', appInfo.appstoreId, appInfo.location); - - var data = { - appStoreId: appInfo.appstoreId, - location: appInfo.location, - portBindings: appInfo.portBindings || null, - accessRestriction: appInfo.accessRestriction || null, - }; - - apps.install(data, { userId: null, username: 'autoinstaller' }, iteratorCallback); - }, function (error) { - if (error) debug('autoInstallApps: ', error); - - fs.writeFileSync(paths.FIRST_RUN_FILE, 'been there, done that', 'utf8'); - - callback(); - }); -} - function checkDiskSpace(callback) { callback = callback || NOOP_CALLBACK; @@ -877,12 +936,20 @@ function migrate(options, callback) { var dnsConfig = _.pick(options, 'domain', 'provider', 'accessKeyId', 'secretAccessKey', 'region', 'endpoint', 'token', 'zoneName'); - settings.setDnsConfig(dnsConfig, options.domain, options.zoneName || tld.getDomain(options.domain), function (error) { - if (error && error.reason === SettingsError.BAD_FIELD) return callback(new CloudronError(CloudronError.BAD_FIELD, error.message)); - if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error)); + domains.get(options.domain, function (error, result) { + if (error && error.reason !== DomainError.NOT_FOUND) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error)); - // TODO: should probably rollback dns config if migrate fails - doMigrate(options, callback); + var func; + if (!result) func = domains.add.bind(null, options.domain, options.zoneName, dnsConfig, null); + else func = domains.update.bind(null, options.domain, dnsConfig, null); + + func(function (error) { + if (error && error.reason === DomainError.BAD_FIELD) return callback(new CloudronError(CloudronError.BAD_FIELD, error.message)); + if (error) return callback(new SettingsError(CloudronError.INTERNAL_ERROR, error)); + + // TODO: should probably rollback dns config if migrate fails + doMigrate(options, callback); + }); }); } @@ -907,7 +974,7 @@ function refreshDNS(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(); - subdomains.upsert(app.location, 'A', [ ip ], callback); + domains.upsertDNSRecords(app.location, app.domain, 'A', [ ip ], callback); }, function (error) { if (error) return callback(error); diff --git a/src/config.js b/src/config.js index e4fbd74d7..5c9117fb7 100644 --- a/src/config.js +++ b/src/config.js @@ -40,9 +40,6 @@ exports = module.exports = { isDemo: isDemo, - tlsCert: tlsCert, - tlsKey: tlsKey, - // for testing resets to defaults _reset: _reset }; @@ -54,6 +51,10 @@ var assert = require('assert'), tld = require('tldjs'), _ = require('underscore'); + +// assert on unknown environment can't proceed +assert(exports.CLOUDRON || exports.TEST, 'Unknown environment. This should not happen!'); + var homeDir = process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE; var data = { }; @@ -65,8 +66,25 @@ function baseDir() { var cloudronConfigFileName = path.join(baseDir(), 'configs/cloudron.conf'); +// only tests can run without a config file on disk, they use the defaults with runtime overrides +if (exports.CLOUDRON) assert(fs.existsSync(cloudronConfigFileName), 'No cloudron.conf found, cannot proceed'); + function saveSync() { - fs.writeFileSync(cloudronConfigFileName, JSON.stringify(data, null, 4)); // functions are ignored by JSON.stringify + // 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, + fqdn: data.fqdn, + zoneName: data.zoneName, + adminLocation: data.adminLocation, + isCustomDomain: data.isCustomDomain, + provider: data.provider, + isDemo: data.isDemo + }; + + fs.writeFileSync(cloudronConfigFileName, JSON.stringify(conf, null, 4)); // functions are ignored by JSON.stringify } function _reset(callback) { @@ -79,46 +97,42 @@ function _reset(callback) { function initConfig() { // setup defaults - data.fqdn = 'localhost'; + data.fqdn = ''; data.zoneName = ''; data.adminLocation = 'my'; - + data.port = 3000; data.token = null; data.version = null; data.isCustomDomain = true; + data.apiServerOrigin = null; data.webServerOrigin = null; - data.smtpPort = 2525; // // this value comes from mail container + data.provider = 'caas'; + data.smtpPort = 2525; // this value comes from mail container data.sysadminPort = 3001; data.ldapPort = 3002; - data.provider = 'caas'; - data.appBundle = [ ]; - if (exports.CLOUDRON) { - data.port = 3000; - data.apiServerOrigin = null; - data.database = null; - } else if (exports.TEST) { + // keep in sync with start.sh + data.database = { + hostname: '127.0.0.1', + username: 'root', + password: 'password', + port: 3306, + name: 'box' + }; + + // overrides for local testings + if (exports.TEST) { + data.version = '1.1.1-test'; data.port = 5454; - data.apiServerOrigin = 'http://localhost:6060'; // hock doesn't support https - data.database = { - hostname: '127.0.0.1', - username: 'root', - password: '', - port: 3306, - name: 'boxtest' - }; data.token = 'APPSTORE_TOKEN'; - } else { - assert(false, 'Unknown environment. This should not happen!'); + data.apiServerOrigin = 'http://localhost:6060'; // hock doesn't support https + data.database.password = ''; + data.database.name = 'boxtest'; } - if (safe.fs.existsSync(cloudronConfigFileName)) { - var existingData = safe.JSON.parse(safe.fs.readFileSync(cloudronConfigFileName, 'utf8')); - _.extend(data, existingData); // overwrite defaults with saved config - return; - } - - saveSync(); + // overwrite defaults with saved config + var existingData = safe.JSON.parse(safe.fs.readFileSync(cloudronConfigFileName, 'utf8')); + _.extend(data, existingData); } initConfig(); @@ -172,11 +186,15 @@ function zoneName() { } // keep this in sync with start.sh admin.conf generation code -function appFqdn(location) { - assert.strictEqual(typeof location, 'string'); +function appFqdn(app) { + assert.strictEqual(typeof app, 'object'); + assert.strictEqual(typeof app.location, 'string'); + assert.strictEqual(typeof app.domain, 'string'); - if (location === '') return fqdn(); - return isCustomDomain() ? location + '.' + fqdn() : location + '-' + fqdn(); + if (app.location === '') return app.domain; + + // caas still has subdomains with a dash + return app.location + (isCustomDomain() ? '.' : '-') + app.domain; } function mailLocation() { @@ -184,7 +202,7 @@ function mailLocation() { } function mailFqdn() { - return appFqdn(mailLocation()); + return appFqdn({ domain: fqdn(), location: mailLocation() }); } function adminLocation() { @@ -192,11 +210,11 @@ function adminLocation() { } function adminFqdn() { - return appFqdn(adminLocation()); + return appFqdn({ domain: fqdn(), location: adminLocation() }); } function adminOrigin() { - return 'https://' + appFqdn(adminLocation()); + return 'https://' + adminFqdn(); } function internalAdminOrigin() { @@ -235,16 +253,6 @@ function provider() { return get('provider'); } -function tlsCert() { - var certFile = path.join(baseDir(), 'configs/host.cert'); - return safe.fs.readFileSync(certFile, 'utf8'); -} - -function tlsKey() { - var keyFile = path.join(baseDir(), 'configs/host.key'); - return safe.fs.readFileSync(keyFile, 'utf8'); -} - function hasIPv6() { const IPV6_PROC_FILE = '/proc/net/if_inet6'; return fs.existsSync(IPV6_PROC_FILE); diff --git a/src/constants.js b/src/constants.js index 793aba4a8..da8ca1bd5 100644 --- a/src/constants.js +++ b/src/constants.js @@ -19,8 +19,8 @@ exports = module.exports = { ADMIN_NAME: 'Settings', ADMIN_CLIENT_ID: 'webadmin', // oauth client id - ADMIN_APPID: 'admin', // admin appid (settingsdb) + ADMIN_GROUP_NAME: 'admin', ADMIN_GROUP_ID: 'admin', NGINX_ADMIN_CONFIG_FILE_NAME: 'admin.conf', diff --git a/src/cron.js b/src/cron.js index 03855dde4..9988caa30 100644 --- a/src/cron.js +++ b/src/cron.js @@ -23,21 +23,23 @@ var apps = require('./apps.js'), semver = require('semver'), updateChecker = require('./updatechecker.js'); -var gAliveJob = null, // send periodic stats - gAppUpdateCheckerJob = null, - gAutoupdaterJob = null, - gBackupJob = null, - gBoxUpdateCheckerJob = null, - gCertificateRenewJob = null, - gCheckDiskSpaceJob = null, - gCleanupBackupsJob = null, - gCleanupEventlogJob = null, - gCleanupTokensJob = null, - gDockerVolumeCleanerJob = null, - gDynamicDNSJob = null, - gCaasHeartbeatJob = null, // for CaaS health check - gSchedulerSyncJob = null, - gDigestEmailJob = null; +var gJobs = { + alive: null, // send periodic stats + autoUpdater: null, + appUpdateChecker: null, + backup: null, + boxUpdateChecker: null, + caasHeartbeat: null, + checkDiskSpace: null, + certificateRenew: null, + cleanupBackups: null, + cleanupEventlog: null, + cleanupTokens: null, + digestEmail: null, + dockerVolumeCleaner: null, + dynamicDNS: null, + schedulerSync: null +}; var NOOP_CALLBACK = function (error) { if (error) console.error(error); }; var AUDIT_SOURCE = { userId: null, username: 'cron' }; @@ -54,22 +56,20 @@ function initialize(callback) { assert.strictEqual(typeof callback, 'function'); if (config.provider() === 'caas') { - gCaasHeartbeatJob = new CronJob({ - cronTime: '00 */1 * * * *', // every minute - onTick: cloudron.sendCaasHeartbeat, - start: false - }); // hack: send the first heartbeat only after we are running for 60 seconds // required as we end up sending a heartbeat and then cloudron-setup reboots the server - setTimeout(function () { - if (!gCaasHeartbeatJob) return; // already uninitalized - gCaasHeartbeatJob.start(); - cloudron.sendCaasHeartbeat(); - }, 1000 * 60); + var seconds = (new Date()).getSeconds() - 1; + if (seconds === -1) seconds = 59; + + gJobs.caasHeartbeat = new CronJob({ + cronTime: `${seconds} */1 * * * *`, // every minute + onTick: cloudron.sendCaasHeartbeat, + start: true + }); } var randomHourMinute = Math.floor(60*Math.random()); - gAliveJob = new CronJob({ + gJobs.alive = new CronJob({ cronTime: '00 ' + randomHourMinute + ' * * * *', // every hour on a random minute onTick: appstore.sendAliveStatus, start: true @@ -95,16 +95,16 @@ function recreateJobs(tz) { debug('Creating jobs with timezone %s', tz); - if (gBackupJob) gBackupJob.stop(); - gBackupJob = new CronJob({ + if (gJobs.backup) gJobs.backup.stop(); + gJobs.backup = new CronJob({ cronTime: '00 00 */6 * * *', // every 6 hours. backups.ensureBackup() will only trigger a backup once per day onTick: backups.ensureBackup.bind(null, AUDIT_SOURCE, NOOP_CALLBACK), start: true, timeZone: tz }); - if (gCheckDiskSpaceJob) gCheckDiskSpaceJob.stop(); - gCheckDiskSpaceJob = new CronJob({ + if (gJobs.checkDiskSpace) gJobs.checkDiskSpace.stop(); + gJobs.checkDiskSpace = new CronJob({ cronTime: '00 30 */4 * * *', // every 4 hours onTick: cloudron.checkDiskSpace, start: true, @@ -114,72 +114,72 @@ function recreateJobs(tz) { // randomized pattern per cloudron every hour var randomMinute = Math.floor(60*Math.random()); - if (gBoxUpdateCheckerJob) gBoxUpdateCheckerJob.stop(); - gBoxUpdateCheckerJob = new CronJob({ + if (gJobs.boxUpdateCheckerJob) gJobs.boxUpdateCheckerJob.stop(); + gJobs.boxUpdateCheckerJob = new CronJob({ cronTime: '00 ' + randomMinute + ' * * * *', // once an hour onTick: updateChecker.checkBoxUpdates, start: true, timeZone: tz }); - if (gAppUpdateCheckerJob) gAppUpdateCheckerJob.stop(); - gAppUpdateCheckerJob = new CronJob({ + if (gJobs.appUpdateChecker) gJobs.appUpdateChecker.stop(); + gJobs.appUpdateChecker = new CronJob({ cronTime: '00 ' + randomMinute + ' * * * *', // once an hour onTick: updateChecker.checkAppUpdates, start: true, timeZone: tz }); - if (gCleanupTokensJob) gCleanupTokensJob.stop(); - gCleanupTokensJob = new CronJob({ + if (gJobs.cleanupTokens) gJobs.cleanupTokens.stop(); + gJobs.cleanupTokens = new CronJob({ cronTime: '00 */30 * * * *', // every 30 minutes onTick: janitor.cleanupTokens, start: true, timeZone: tz }); - if (gCleanupBackupsJob) gCleanupBackupsJob.stop(); - gCleanupBackupsJob = new CronJob({ + if (gJobs.cleanupBackups) gJobs.cleanupBackups.stop(); + gJobs.cleanupBackups = new CronJob({ cronTime: '00 45 */6 * * *', // every 6 hours. try not to overlap with ensureBackup job onTick: backups.cleanup.bind(null, AUDIT_SOURCE, NOOP_CALLBACK), start: true, timeZone: tz }); - if (gCleanupEventlogJob) gCleanupEventlogJob.stop(); - gCleanupEventlogJob = new CronJob({ + if (gJobs.cleanupEventlog) gJobs.cleanupEventlog.stop(); + gJobs.cleanupEventlog = new CronJob({ cronTime: '00 */30 * * * *', // every 30 minutes onTick: eventlog.cleanup, start: true, timeZone: tz }); - if (gDockerVolumeCleanerJob) gDockerVolumeCleanerJob.stop(); - gDockerVolumeCleanerJob = new CronJob({ + if (gJobs.dockerVolumeCleaner) gJobs.dockerVolumeCleaner.stop(); + gJobs.dockerVolumeCleaner = new CronJob({ cronTime: '00 00 */12 * * *', // every 12 hours onTick: janitor.cleanupDockerVolumes, start: true, timeZone: tz }); - if (gSchedulerSyncJob) gSchedulerSyncJob.stop(); - gSchedulerSyncJob = new CronJob({ + if (gJobs.schedulerSync) gJobs.schedulerSync.stop(); + gJobs.schedulerSync = new CronJob({ cronTime: config.TEST ? '*/10 * * * * *' : '00 */1 * * * *', // every minute onTick: scheduler.sync, start: true, timeZone: tz }); - if (gCertificateRenewJob) gCertificateRenewJob.stop(); - gCertificateRenewJob = new CronJob({ + if (gJobs.certificateRenew) gJobs.certificateRenew.stop(); + gJobs.certificateRenew = new CronJob({ cronTime: '00 00 */12 * * *', // every 12 hours onTick: certificates.renewAll.bind(null, AUDIT_SOURCE, NOOP_CALLBACK), start: true, timeZone: tz }); - if (gDigestEmailJob) gDigestEmailJob.stop(); - gDigestEmailJob = new CronJob({ + if (gJobs.digestEmail) gJobs.digestEmail.stop(); + gJobs.digestEmail = new CronJob({ cronTime: '00 00 00 * * 3', // every wednesday onTick: digest.maybeSend, start: true, @@ -189,15 +189,15 @@ function recreateJobs(tz) { function autoupdatePatternChanged(pattern) { assert.strictEqual(typeof pattern, 'string'); - assert(gBoxUpdateCheckerJob); + assert(gJobs.boxUpdateCheckerJob); debug('Auto update pattern changed to %s', pattern); - if (gAutoupdaterJob) gAutoupdaterJob.stop(); + if (gJobs.autoUpdater) gJobs.autoUpdater.stop(); if (pattern === constants.AUTOUPDATE_PATTERN_NEVER) return; - gAutoupdaterJob = new CronJob({ + gJobs.autoUpdater = new CronJob({ cronTime: pattern, onTick: function() { var updateInfo = updateChecker.getUpdateInfo(); @@ -216,26 +216,26 @@ function autoupdatePatternChanged(pattern) { } }, start: true, - timeZone: gBoxUpdateCheckerJob.cronTime.zone // hack + timeZone: gJobs.boxUpdateCheckerJob.cronTime.zone // hack }); } function dynamicDNSChanged(enabled) { assert.strictEqual(typeof enabled, 'boolean'); - assert(gBoxUpdateCheckerJob); + assert(gJobs.boxUpdateCheckerJob); debug('Dynamic DNS setting changed to %s', enabled); if (enabled) { - gDynamicDNSJob = new CronJob({ + gJobs.dynamicDNS = new CronJob({ cronTime: '00 */10 * * * *', onTick: cloudron.refreshDNS, start: true, - timeZone: gBoxUpdateCheckerJob.cronTime.zone // hack + timeZone: gJobs.boxUpdateCheckerJob.cronTime.zone // hack }); } else { - if (gDynamicDNSJob) gDynamicDNSJob.stop(); - gDynamicDNSJob = null; + if (gJobs.dynamicDNS) gJobs.dynamicDNS.stop(); + gJobs.dynamicDNS = null; } } @@ -244,48 +244,13 @@ function uninitialize(callback) { settings.events.removeListener(settings.TIME_ZONE_KEY, recreateJobs); settings.events.removeListener(settings.AUTOUPDATE_PATTERN_KEY, autoupdatePatternChanged); + settings.events.removeListener(settings.DYNAMIC_DNS_KEY, dynamicDNSChanged); - if (gAutoupdaterJob) gAutoupdaterJob.stop(); - gAutoupdaterJob = null; - - if (gBoxUpdateCheckerJob) gBoxUpdateCheckerJob.stop(); - gBoxUpdateCheckerJob = null; - - if (gAppUpdateCheckerJob) gAppUpdateCheckerJob.stop(); - gAppUpdateCheckerJob = null; - - if (gCaasHeartbeatJob) gCaasHeartbeatJob.stop(); - gCaasHeartbeatJob = null; - - if (gAliveJob) gAliveJob.stop(); - gAliveJob = null; - - if (gBackupJob) gBackupJob.stop(); - gBackupJob = null; - - if (gCleanupTokensJob) gCleanupTokensJob.stop(); - gCleanupTokensJob = null; - - if (gCleanupBackupsJob) gCleanupBackupsJob.stop(); - gCleanupBackupsJob = null; - - if (gCleanupEventlogJob) gCleanupEventlogJob.stop(); - gCleanupEventlogJob = null; - - if (gDockerVolumeCleanerJob) gDockerVolumeCleanerJob.stop(); - gDockerVolumeCleanerJob = null; - - if (gSchedulerSyncJob) gSchedulerSyncJob.stop(); - gSchedulerSyncJob = null; - - if (gCertificateRenewJob) gCertificateRenewJob.stop(); - gCertificateRenewJob = null; - - if (gDynamicDNSJob) gDynamicDNSJob.stop(); - gDynamicDNSJob = null; - - if (gDigestEmailJob) gDigestEmailJob.stop(); - gDigestEmailJob = null; + for (var job in gJobs) { + if (!gJobs[job]) continue; + gJobs[job].stop(); + gJobs[job] = null; + } callback(); } diff --git a/src/database.js b/src/database.js index b9ef8dd61..eabb99702 100644 --- a/src/database.js +++ b/src/database.js @@ -10,6 +10,9 @@ exports = module.exports = { rollback: rollback, commit: commit, + importFromFile: importFromFile, + exportToFile: exportToFile, + _clear: clear }; @@ -101,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); } @@ -183,3 +187,27 @@ function transaction(queries, callback) { }); } +function importFromFile(file, callback) { + assert.strictEqual(typeof file, 'string'); + assert.strictEqual(typeof callback, 'function'); + + var password = config.database().password ? '-p' + config.database().password : '--skip-password'; + + var cmd = `/usr/bin/mysql -u ${config.database().username} ${password} ${config.database().name} < ${file}`; + + async.series([ + query.bind(null, 'CREATE DATABASE IF NOT EXISTS box'), + child_process.exec.bind(null, cmd) + ], callback); +} + +function exportToFile(file, callback) { + assert.strictEqual(typeof file, 'string'); + assert.strictEqual(typeof callback, 'function'); + + var password = config.database().password ? '-p' + config.database().password : '--skip-password'; + var cmd = `/usr/bin/mysqldump -u root ${password} --single-transaction --routines \ + --triggers ${config.database().name} > "${file}"`; + + child_process.exec(cmd, callback); +} diff --git a/src/developer.js b/src/developer.js index 66ccca3d5..7e5c2d397 100644 --- a/src/developer.js +++ b/src/developer.js @@ -5,8 +5,6 @@ exports = module.exports = { DeveloperError: DeveloperError, - isEnabled: isEnabled, - setEnabled: setEnabled, issueDeveloperToken: issueDeveloperToken }; @@ -15,7 +13,6 @@ var assert = require('assert'), constants = require('./constants.js'), eventlog = require('./eventlog.js'), tokendb = require('./tokendb.js'), - settings = require('./settings.js'), util = require('util'); function DeveloperError(reason, errorOrMessage) { @@ -40,29 +37,6 @@ util.inherits(DeveloperError, Error); DeveloperError.INTERNAL_ERROR = 'Internal Error'; DeveloperError.EXTERNAL_ERROR = 'External Error'; -function isEnabled(callback) { - assert.strictEqual(typeof callback, 'function'); - - settings.getDeveloperMode(function (error, enabled) { - if (error) return callback(new DeveloperError(DeveloperError.INTERNAL_ERROR, error)); - callback(null, enabled); - }); -} - -function setEnabled(enabled, auditSource, callback) { - assert.strictEqual(typeof enabled, 'boolean'); - assert.strictEqual(typeof auditSource, 'object'); - assert.strictEqual(typeof callback, 'function'); - - settings.setDeveloperMode(enabled, function (error) { - if (error) return callback(new DeveloperError(DeveloperError.INTERNAL_ERROR, error)); - - eventlog.add(eventlog.ACTION_CLI_MODE, auditSource, { enabled: enabled }); - - callback(null); - }); -} - function issueDeveloperToken(user, auditSource, callback) { assert.strictEqual(typeof user, 'object'); assert.strictEqual(typeof auditSource, 'object'); diff --git a/src/dns/caas.js b/src/dns/caas.js index 518b9ee36..fdafed9b4 100644 --- a/src/dns/caas.js +++ b/src/dns/caas.js @@ -11,10 +11,17 @@ exports = module.exports = { var assert = require('assert'), config = require('../config.js'), debug = require('debug')('box:dns/caas'), - SubdomainError = require('../subdomains.js').SubdomainError, + DomainError = require('../domains.js').DomainError, superagent = require('superagent'), util = require('util'); +function getFqdn(subdomain, domain) { + assert.strictEqual(typeof subdomain, 'string'); + assert.strictEqual(typeof domain, 'string'); + + return (subdomain === '') ? domain : subdomain + '-' + domain; +} + function add(dnsConfig, zoneName, subdomain, type, values, callback) { assert.strictEqual(typeof dnsConfig, 'object'); assert.strictEqual(typeof zoneName, 'string'); @@ -23,9 +30,9 @@ function add(dnsConfig, zoneName, subdomain, type, values, callback) { assert(util.isArray(values)); assert.strictEqual(typeof callback, 'function'); - var fqdn = subdomain !== '' && type === 'TXT' ? subdomain + '.' + config.fqdn() : config.appFqdn(subdomain); + var fqdn = subdomain !== '' && type === 'TXT' ? subdomain + '.' + dnsConfig.fqdn : getFqdn(subdomain, dnsConfig.fqdn); - debug('add: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values); + debug('add: %s for zone %s of type %s with values %j', subdomain, dnsConfig.fqdn, type, values); var data = { type: type, @@ -38,10 +45,10 @@ function add(dnsConfig, zoneName, subdomain, type, values, callback) { .send(data) .timeout(30 * 1000) .end(function (error, result) { - if (error && !error.response) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('Network error %s', error.message))); - if (result.statusCode === 400) return callback(new SubdomainError(SubdomainError.BAD_FIELD, result.body.message)); - if (result.statusCode === 420) return callback(new SubdomainError(SubdomainError.STILL_BUSY)); - if (result.statusCode !== 201) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body))); + if (error && !error.response) return callback(new DomainError(DomainError.EXTERNAL_ERROR, util.format('Network error %s', error.message))); + if (result.statusCode === 400) return callback(new DomainError(DomainError.BAD_FIELD, result.body.message)); + if (result.statusCode === 420) return callback(new DomainError(DomainError.STILL_BUSY)); + if (result.statusCode !== 201) return callback(new DomainError(DomainError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body))); return callback(null, result.body.changeId); }); @@ -54,17 +61,17 @@ function get(dnsConfig, zoneName, subdomain, type, callback) { assert.strictEqual(typeof type, 'string'); assert.strictEqual(typeof callback, 'function'); - var fqdn = subdomain !== '' && type === 'TXT' ? subdomain + '.' + config.fqdn() : config.appFqdn(subdomain); + var fqdn = subdomain !== '' && type === 'TXT' ? subdomain + '.' + dnsConfig.fqdn : getFqdn(subdomain, dnsConfig.fqdn); - debug('get: zoneName: %s subdomain: %s type: %s fqdn: %s', zoneName, subdomain, type, fqdn); + debug('get: zoneName: %s subdomain: %s type: %s fqdn: %s', dnsConfig.fqdn, subdomain, type, fqdn); superagent .get(config.apiServerOrigin() + '/api/v1/domains/' + fqdn) .query({ token: dnsConfig.token, type: type }) .timeout(30 * 1000) .end(function (error, result) { - if (error && !error.response) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('Network error %s', error.message))); - if (result.statusCode !== 200) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body))); + if (error && !error.response) return callback(new DomainError(DomainError.EXTERNAL_ERROR, util.format('Network error %s', error.message))); + if (result.statusCode !== 200) return callback(new DomainError(DomainError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body))); return callback(null, result.body.values); }); @@ -89,7 +96,7 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) { assert(util.isArray(values)); assert.strictEqual(typeof callback, 'function'); - debug('del: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values); + debug('del: %s for zone %s of type %s with values %j', subdomain, dnsConfig.fqdn, type, values); var data = { type: type, @@ -97,16 +104,16 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) { }; superagent - .del(config.apiServerOrigin() + '/api/v1/domains/' + config.appFqdn(subdomain)) + .del(config.apiServerOrigin() + '/api/v1/domains/' + getFqdn(subdomain, dnsConfig.fqdn)) .query({ token: dnsConfig.token }) .send(data) .timeout(30 * 1000) .end(function (error, result) { - if (error && !error.response) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('Network error %s', error.message))); - if (result.statusCode === 400) return callback(new SubdomainError(SubdomainError.BAD_FIELD, result.body.message)); - if (result.statusCode === 420) return callback(new SubdomainError(SubdomainError.STILL_BUSY)); - if (result.statusCode === 404) return callback(new SubdomainError(SubdomainError.NOT_FOUND)); - if (result.statusCode !== 204) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body))); + if (error && !error.response) return callback(new DomainError(DomainError.EXTERNAL_ERROR, util.format('Network error %s', error.message))); + if (result.statusCode === 400) return callback(new DomainError(DomainError.BAD_FIELD, result.body.message)); + if (result.statusCode === 420) return callback(new DomainError(DomainError.STILL_BUSY)); + if (result.statusCode === 404) return callback(new DomainError(DomainError.NOT_FOUND)); + if (result.statusCode !== 204) return callback(new DomainError(DomainError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body))); return callback(null); }); @@ -120,7 +127,9 @@ function verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback) { assert.strictEqual(typeof callback, 'function'); var credentials = { - provider: dnsConfig.provider + provider: dnsConfig.provider, + token: dnsConfig.token, + fqdn: domain }; return callback(null, credentials); diff --git a/src/dns/cloudflare.js b/src/dns/cloudflare.js index 48bebc20c..aa8752ba3 100644 --- a/src/dns/cloudflare.js +++ b/src/dns/cloudflare.js @@ -10,12 +10,12 @@ exports = module.exports = { var assert = require('assert'), async = require('async'), - dns = require('dns'), - _ = require('underscore'), - SubdomainError = require('../subdomains.js').SubdomainError, - superagent = require('superagent'), debug = require('debug')('box:dns/cloudflare'), - util = require('util'); + dns = require('dns'), + DomainError = require('../domains.js').DomainError, + superagent = require('superagent'), + util = require('util'), + _ = require('underscore'); // we are using latest v4 stable API https://api.cloudflare.com/#getting-started-endpoints var CLOUDFLARE_ENDPOINT = 'https://api.cloudflare.com/client/v4'; @@ -24,8 +24,8 @@ function translateRequestError(result, callback) { assert.strictEqual(typeof result, 'object'); assert.strictEqual(typeof callback, 'function'); - if (result.statusCode === 404) return callback(new SubdomainError(SubdomainError.NOT_FOUND, util.format('%s %j', result.statusCode, 'API does not exist'))); - if (result.statusCode === 422) return callback(new SubdomainError(SubdomainError.BAD_FIELD, result.body.message)); + if (result.statusCode === 404) return callback(new DomainError(DomainError.NOT_FOUND, util.format('%s %j', result.statusCode, 'API does not exist'))); + if (result.statusCode === 422) return callback(new DomainError(DomainError.BAD_FIELD, result.body.message)); if ((result.statusCode === 400 || result.statusCode === 401 || result.statusCode === 403) && result.body.errors.length > 0) { let error = result.body.errors[0]; let message = error.message; @@ -34,10 +34,10 @@ function translateRequestError(result, callback) { else message = 'Invalid credentials'; } - return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, message)); + return callback(new DomainError(DomainError.ACCESS_DENIED, message)); } - callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body))); + callback(new DomainError(DomainError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body))); } function getZoneByName(dnsConfig, zoneName, callback) { @@ -52,7 +52,7 @@ function getZoneByName(dnsConfig, zoneName, callback) { .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 SubdomainError(SubdomainError.NOT_FOUND, util.format('%s %j', result.statusCode, result.body))); + 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]); }); @@ -233,8 +233,8 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) { assert.strictEqual(typeof ip, 'string'); assert.strictEqual(typeof callback, 'function'); - if (!dnsConfig.token || typeof dnsConfig.token !== 'string') return callback(new SubdomainError(SubdomainError.BAD_FIELD, 'token must be a non-empty string')); - if (!dnsConfig.email || typeof dnsConfig.email !== 'string') return callback(new SubdomainError(SubdomainError.BAD_FIELD, 'email must be a non-empty string')); + if (!dnsConfig.token || typeof dnsConfig.token !== 'string') return callback(new DomainError(DomainError.BAD_FIELD, 'token must be a non-empty string')); + if (!dnsConfig.email || typeof dnsConfig.email !== 'string') return callback(new DomainError(DomainError.BAD_FIELD, 'email must be a non-empty string')); var credentials = { provider: dnsConfig.provider, @@ -245,23 +245,31 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) { if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here dns.resolveNs(zoneName, function (error, nameservers) { - if (error && error.code === 'ENOTFOUND') return callback(new SubdomainError(SubdomainError.BAD_FIELD, 'Unable to resolve nameservers for this domain')); - if (error || !nameservers) return callback(new SubdomainError(SubdomainError.BAD_FIELD, error ? error.message : 'Unable to get 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')); getZoneByName(dnsConfig, zoneName, function(error, result) { if (error) return callback(error); if (!_.isEqual(result.name_servers.sort(), nameservers.sort())) { debug('verifyDnsConfig: %j and %j do not match', nameservers, result.name_servers); - return callback(new SubdomainError(SubdomainError.BAD_FIELD, 'Domain nameservers are not set to Cloudflare')); + return callback(new DomainError(DomainError.BAD_FIELD, 'Domain nameservers are not set to Cloudflare')); } - upsert(credentials, zoneName, 'my', 'A', [ ip ], function (error, changeId) { + const testSubdomain = 'cloudrontestdns'; + + upsert(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error, changeId) { if (error) return callback(error); - debug('verifyDnsConfig: A record added with change id %s', changeId); + debug('verifyDnsConfig: Test A record added with change id %s', changeId); - callback(null, credentials); + del(dnsConfig, zoneName, testSubdomain, 'A', [ ip ], function (error) { + if (error) return callback(error); + + debug('verifyDnsConfig: Test A record removed again'); + + callback(null, credentials); + }); }); }); }); diff --git a/src/dns/digitalocean.js b/src/dns/digitalocean.js index 464d77cd2..6f43a26e9 100644 --- a/src/dns/digitalocean.js +++ b/src/dns/digitalocean.js @@ -10,11 +10,10 @@ exports = module.exports = { var assert = require('assert'), async = require('async'), - config = require('../config.js'), debug = require('debug')('box:dns/digitalocean'), dns = require('dns'), + DomainError = require('../domains.js').DomainError, safe = require('safetydance'), - SubdomainError = require('../subdomains.js').SubdomainError, superagent = require('superagent'), util = require('util'); @@ -40,10 +39,10 @@ function getInternal(dnsConfig, zoneName, subdomain, type, callback) { .set('Authorization', 'Bearer ' + dnsConfig.token) .timeout(30 * 1000) .end(function (error, result) { - if (error && !error.response) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('Network error %s', error.message))); - if (result.statusCode === 404) return callback(new SubdomainError(SubdomainError.NOT_FOUND, formatError(result))); - if (result.statusCode === 403 || result.statusCode === 401) return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, formatError(result))); - if (result.statusCode !== 200) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, formatError(result))); + if (error && !error.response) return callback(new DomainError(DomainError.EXTERNAL_ERROR, util.format('Network error %s', error.message))); + if (result.statusCode === 404) return callback(new DomainError(DomainError.NOT_FOUND, formatError(result))); + if (result.statusCode === 403 || result.statusCode === 401) return callback(new DomainError(DomainError.ACCESS_DENIED, formatError(result))); + if (result.statusCode !== 200) return callback(new DomainError(DomainError.EXTERNAL_ERROR, formatError(result))); matchingRecords = matchingRecords.concat(result.body.domain_records.filter(function (record) { return (record.type === type && record.name === subdomain); @@ -80,7 +79,7 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) { // used to track available records to update instead of create var i = 0, recordIds = []; - async.eachSeries(values, function (value, callback) { + async.eachSeries(values, function (value, iteratorCallback) { var priority = null; if (type === 'MX') { @@ -102,14 +101,14 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) { .send(data) .timeout(30 * 1000) .end(function (error, result) { - if (error && !error.response) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('Network error %s', error.message))); - if (result.statusCode === 403 || result.statusCode === 401) return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, formatError(result))); - if (result.statusCode === 422) return callback(new SubdomainError(SubdomainError.BAD_FIELD, result.body.message)); - if (result.statusCode !== 201) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, formatError(result))); + if (error && !error.response) return iteratorCallback(new DomainError(DomainError.EXTERNAL_ERROR, util.format('Network error %s', error.message))); + if (result.statusCode === 403 || result.statusCode === 401) return iteratorCallback(new DomainError(DomainError.ACCESS_DENIED, formatError(result))); + if (result.statusCode === 422) return iteratorCallback(new DomainError(DomainError.BAD_FIELD, result.body.message)); + if (result.statusCode !== 201) return iteratorCallback(new DomainError(DomainError.EXTERNAL_ERROR, formatError(result))); recordIds.push(safe.query(result.body, 'domain_record.id')); - return callback(null); + return iteratorCallback(null); }); } else { superagent.put(DIGITALOCEAN_ENDPOINT + '/v2/domains/' + zoneName + '/records/' + result[i].id) @@ -120,17 +119,17 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) { // increment, as we have consumed the record ++i; - if (error && !error.response) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('Network error %s', error.message))); - if (result.statusCode === 403 || result.statusCode === 401) return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, formatError(result))); - if (result.statusCode === 422) return callback(new SubdomainError(SubdomainError.BAD_FIELD, result.body.message)); - if (result.statusCode !== 200) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, formatError(result))); + if (error && !error.response) return iteratorCallback(new DomainError(DomainError.EXTERNAL_ERROR, util.format('Network error %s', error.message))); + if (result.statusCode === 403 || result.statusCode === 401) return iteratorCallback(new DomainError(DomainError.ACCESS_DENIED, formatError(result))); + if (result.statusCode === 422) return iteratorCallback(new DomainError(DomainError.BAD_FIELD, result.body.message)); + if (result.statusCode !== 200) return iteratorCallback(new DomainError(DomainError.EXTERNAL_ERROR, formatError(result))); recordIds.push(safe.query(result.body, 'domain_record.id')); - return callback(null); + return iteratorCallback(null); }); } - }, function (error, id) { + }, function (error) { if (error) return callback(error); callback(null, '' + recordIds[0]); // DO ids are integers @@ -186,10 +185,10 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) { .set('Authorization', 'Bearer ' + dnsConfig.token) .timeout(30 * 1000) .end(function (error, result) { - if (error && !error.response) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('Network error %s', error.message))); + if (error && !error.response) return callback(new DomainError(DomainError.EXTERNAL_ERROR, util.format('Network error %s', error.message))); if (result.statusCode === 404) return callback(null); - if (result.statusCode === 403 || result.statusCode === 401) return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, formatError(result))); - if (result.statusCode !== 204) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, formatError(result))); + if (result.statusCode === 403 || result.statusCode === 401) return callback(new DomainError(DomainError.ACCESS_DENIED, formatError(result))); + if (result.statusCode !== 204) return callback(new DomainError(DomainError.EXTERNAL_ERROR, formatError(result))); debug('del: done'); @@ -213,22 +212,28 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) { if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here dns.resolveNs(zoneName, function (error, nameservers) { - if (error && error.code === 'ENOTFOUND') return callback(new SubdomainError(SubdomainError.BAD_FIELD, 'Unable to resolve nameservers for this domain')); - if (error || !nameservers) return callback(new SubdomainError(SubdomainError.BAD_FIELD, error ? error.message : 'Unable to get 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')); if (nameservers.map(function (n) { return n.toLowerCase(); }).indexOf('ns1.digitalocean.com') === -1) { debug('verifyDnsConfig: %j does not contains DO NS', nameservers); - return callback(new SubdomainError(SubdomainError.BAD_FIELD, 'Domain nameservers are not set to Digital Ocean')); + return callback(new DomainError(DomainError.BAD_FIELD, 'Domain nameservers are not set to Digital Ocean')); } - const name = config.adminLocation() + (fqdn === zoneName ? '' : '.' + fqdn.slice(0, - zoneName.length - 1)); + const testSubdomain = 'cloudrontestdns'; - upsert(credentials, zoneName, name, 'A', [ ip ], function (error, changeId) { + upsert(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error, changeId) { if (error) return callback(error); - debug('verifyDnsConfig: A record added with change id %s', changeId); + debug('verifyDnsConfig: Test A record added with change id %s', changeId); - callback(null, credentials); + del(dnsConfig, zoneName, testSubdomain, 'A', [ ip ], function (error) { + if (error) return callback(error); + + debug('verifyDnsConfig: Test A record removed again'); + + callback(null, credentials); + }); }); }); } diff --git a/src/dns/gcdns.js b/src/dns/gcdns.js index 5199469eb..9a0eed6b7 100644 --- a/src/dns/gcdns.js +++ b/src/dns/gcdns.js @@ -12,8 +12,8 @@ var assert = require('assert'), config = require('../config.js'), debug = require('debug')('box:dns/gcdns'), dns = require('dns'), + DomainError = require('../domains.js').DomainError, GCDNS = require('@google-cloud/dns'), - SubdomainError = require('../subdomains.js').SubdomainError, util = require('util'), _ = require('underscore'); @@ -44,20 +44,20 @@ function getZoneByName(dnsConfig, zoneName, callback) { var gcdns = GCDNS(getDnsCredentials(dnsConfig)); gcdns.getZones(function (error, zones) { - if (error && error.message === 'invalid_grant') return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, 'The key was probably revoked')); - if (error && error.reason === 'No such domain') return callback(new SubdomainError(SubdomainError.NOT_FOUND, error.message)); - if (error && error.code === 403) return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, error.message)); - if (error && error.code === 404) return callback(new SubdomainError(SubdomainError.NOT_FOUND, error.message)); + if (error && error.message === 'invalid_grant') return callback(new DomainError(DomainError.ACCESS_DENIED, 'The key was probably revoked')); + if (error && error.reason === 'No such domain') return callback(new DomainError(DomainError.NOT_FOUND, error.message)); + if (error && error.code === 403) return callback(new DomainError(DomainError.ACCESS_DENIED, error.message)); + if (error && error.code === 404) return callback(new DomainError(DomainError.NOT_FOUND, error.message)); if (error) { debug('gcdns.getZones', error); - return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error)); + return callback(new DomainError(DomainError.EXTERNAL_ERROR, error)); } var zone = zones.filter(function (zone) { return zone.metadata.dnsName.slice(0, -1) === zoneName; // the zone name contains a '.' at the end })[0]; - if (!zone) return callback(new SubdomainError(SubdomainError.NOT_FOUND, 'no such zone')); + if (!zone) return callback(new DomainError(DomainError.NOT_FOUND, 'no such zone')); callback(null, zone); //zone.metadata ~= {name="", dnsName="", nameServers:[]} }); @@ -79,10 +79,10 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) { var domain = (subdomain ? subdomain + '.' : '') + zoneName + '.'; zone.getRecords({ type: type, name: domain }, function (error, oldRecords) { - if (error && error.code === 403) return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, error.message)); + if (error && error.code === 403) return callback(new DomainError(DomainError.ACCESS_DENIED, error.message)); if (error) { debug('upsert->zone.getRecords', error); - return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error.message)); + return callback(new DomainError(DomainError.EXTERNAL_ERROR, error.message)); } var newRecord = zone.record(type, { @@ -92,11 +92,11 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) { }); zone.createChange({ delete: oldRecords, add: newRecord }, function(error, change) { - if (error && error.code === 403) return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, error.message)); - if (error && error.code === 412) return callback(new SubdomainError(SubdomainError.STILL_BUSY, error.message)); + if (error && error.code === 403) return callback(new DomainError(DomainError.ACCESS_DENIED, error.message)); + if (error && error.code === 412) return callback(new DomainError(DomainError.STILL_BUSY, error.message)); if (error) { debug('upsert->zone.createChange', error); - return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error.message)); + return callback(new DomainError(DomainError.EXTERNAL_ERROR, error.message)); } callback(null, change.id); @@ -121,8 +121,8 @@ function get(dnsConfig, zoneName, subdomain, type, callback) { }; zone.getRecords(params, function (error, records) { - if (error && error.code === 403) return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, error.message)); - if (error) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error)); + if (error && error.code === 403) return callback(new DomainError(DomainError.ACCESS_DENIED, error.message)); + if (error) return callback(new DomainError(DomainError.EXTERNAL_ERROR, error)); if (records.length === 0) return callback(null, [ ]); return callback(null, records[0].data); @@ -144,18 +144,18 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) { var domain = (subdomain ? subdomain + '.' : '') + zoneName + '.'; zone.getRecords({ type: type, name: domain }, function(error, oldRecords) { - if (error && error.code === 403) return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, error.message)); + if (error && error.code === 403) return callback(new DomainError(DomainError.ACCESS_DENIED, error.message)); if (error) { debug('del->zone.getRecords', error); - return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error.message)); + return callback(new DomainError(DomainError.EXTERNAL_ERROR, error.message)); } zone.deleteRecords(oldRecords, function (error, change) { - if (error && error.code === 403) return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, error.message)); - if (error && error.code === 412) return callback(new SubdomainError(SubdomainError.STILL_BUSY, error.message)); + if (error && error.code === 403) return callback(new DomainError(DomainError.ACCESS_DENIED, error.message)); + if (error && error.code === 412) return callback(new DomainError(DomainError.STILL_BUSY, error.message)); if (error) { debug('del->zone.createChange', error); - return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error.message)); + return callback(new DomainError(DomainError.EXTERNAL_ERROR, error.message)); } callback(null, change.id); @@ -175,8 +175,8 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) { if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here dns.resolveNs(zoneName, function (error, resolvedNS) { - if (error && error.code === 'ENOTFOUND') return callback(new SubdomainError(SubdomainError.BAD_FIELD, 'Unable to resolve nameservers for this domain')); - if (error || !resolvedNS) return callback(new SubdomainError(SubdomainError.BAD_FIELD, error ? error.message : 'Unable to get nameservers')); + 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')); getZoneByName(credentials, zoneName, function (error, zone) { if (error) return callback(error); @@ -184,17 +184,23 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) { var definedNS = zone.metadata.nameServers.sort().map(function(r) { return r.replace(/\.$/, ''); }); if (!_.isEqual(definedNS, resolvedNS.sort())) { debug('verifyDnsConfig: %j and %j do not match', resolvedNS, definedNS); - return callback(new SubdomainError(SubdomainError.BAD_FIELD, 'Domain nameservers are not set to Google Cloud DNS')); + return callback(new DomainError(DomainError.BAD_FIELD, 'Domain nameservers are not set to Google Cloud DNS')); } - const name = config.adminLocation() + (fqdn === zoneName ? '' : '.' + fqdn.slice(0, - zoneName.length - 1)); + const testSubdomain = 'cloudrontestdns'; - upsert(credentials, zoneName, name, 'A', [ ip ], function (error, changeId) { + upsert(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error, changeId) { if (error) return callback(error); - debug('verifyDnsConfig: A record added with change id %s', changeId); + debug('verifyDnsConfig: Test A record added with change id %s', changeId); - callback(null, credentials); + del(dnsConfig, zoneName, testSubdomain, 'A', [ ip ], function (error) { + if (error) return callback(error); + + debug('verifyDnsConfig: Test A record removed again'); + + callback(null, credentials); + }); }); }); }); diff --git a/src/dns/interface.js b/src/dns/interface.js index ac6b5ae40..15aeb0a80 100644 --- a/src/dns/interface.js +++ b/src/dns/interface.js @@ -15,7 +15,7 @@ exports = module.exports = { }; var assert = require('assert'), - SubdomainError = require('../subdomains.js').SubdomainError, + DomainError = require('../domains.js').DomainError, util = require('util'); function upsert(dnsConfig, zoneName, subdomain, type, values, callback) { diff --git a/src/dns/manual.js b/src/dns/manual.js index 4f6ef1860..d6bbe3501 100644 --- a/src/dns/manual.js +++ b/src/dns/manual.js @@ -9,12 +9,9 @@ exports = module.exports = { }; var assert = require('assert'), - async = require('async'), - config = require('../config.js'), debug = require('debug')('box:dns/manual'), - dig = require('../dig.js'), dns = require('dns'), - SubdomainError = require('../subdomains.js').SubdomainError, + DomainError = require('../domains.js').DomainError, util = require('util'); function upsert(dnsConfig, zoneName, subdomain, type, values, callback) { @@ -58,50 +55,10 @@ function verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback) { assert.strictEqual(typeof ip, 'string'); assert.strictEqual(typeof callback, 'function'); - var adminDomain = config.adminLocation() + '.' + domain; - + // Very basic check if the nameservers can be fetched dns.resolveNs(zoneName, function (error, nameservers) { - if (error || !nameservers) return callback(new SubdomainError(SubdomainError.BAD_FIELD, 'Unable to get nameservers')); + if (error || !nameservers) return callback(new DomainError(DomainError.BAD_FIELD, 'Unable to get nameservers')); - async.every(nameservers, function (nameserver, everyNsCallback) { - // ns records cannot have cname - dns.resolve4(nameserver, function (error, nsIps) { - if (error || !nsIps || nsIps.length === 0) { - return everyNsCallback(new SubdomainError(SubdomainError.BAD_FIELD, 'Unable to resolve nameservers for this domain')); - } - - async.every(nsIps, function (nsIp, everyIpCallback) { - dig.resolve(adminDomain, 'A', { 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, adminDomain); - return everyIpCallback(null, true); // should be ok if dns server is down - } - - if (error) { - debug('nameserver %s (%s) returned error trying to resolve %s: %s', nameserver, nsIp, adminDomain, error); - return everyIpCallback(null, false); - } - - if (!answer || answer.length === 0) { - debug('bad answer from nameserver %s (%s) resolving %s (%s): %j', nameserver, nsIp, adminDomain, 'A', answer); - return everyIpCallback(null, false); - } - - debug('verifyDnsConfig: ns: %s (%s), name:%s Actual:%j Expecting:%s', nameserver, nsIp, adminDomain, answer, ip); - - var match = answer.some(function (a) { return a === ip; }); - - if (match) return everyIpCallback(null, true); // done! - - everyIpCallback(null, false); - }); - }, everyNsCallback); - }); - }, function (error, success) { - if (error) return callback(error); - if (!success) return callback(new SubdomainError(SubdomainError.BAD_FIELD, 'The domain ' + adminDomain + ' does not resolve to the server\'s IP ' + ip)); - - callback(null, { provider: dnsConfig.provider, wildcard: !!dnsConfig.wildcard }); - }); + callback(null, { provider: dnsConfig.provider, wildcard: !!dnsConfig.wildcard }); }); } diff --git a/src/dns/route53.js b/src/dns/route53.js index 80b01a663..51ba34615 100644 --- a/src/dns/route53.js +++ b/src/dns/route53.js @@ -16,7 +16,7 @@ var assert = require('assert'), config = require('../config.js'), debug = require('debug')('box:dns/route53'), dns = require('dns'), - SubdomainError = require('../subdomains.js').SubdomainError, + DomainError = require('../domains.js').DomainError, util = require('util'), _ = require('underscore'); @@ -41,15 +41,15 @@ function getZoneByName(dnsConfig, zoneName, callback) { var route53 = new AWS.Route53(getDnsCredentials(dnsConfig)); route53.listHostedZones({}, function (error, result) { - if (error && error.code === 'AccessDenied') return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, error.message)); - if (error && error.code === 'InvalidClientTokenId') return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, error.message)); - if (error) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error.message)); + 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) return callback(new DomainError(DomainError.EXTERNAL_ERROR, error.message)); var zone = result.HostedZones.filter(function (zone) { return zone.Name.slice(0, -1) === zoneName; // aws zone name contains a '.' at the end })[0]; - if (!zone) return callback(new SubdomainError(SubdomainError.NOT_FOUND, 'no such zone')); + if (!zone) return callback(new DomainError(DomainError.NOT_FOUND, 'no such zone')); callback(null, zone); }); @@ -65,9 +65,9 @@ function getHostedZone(dnsConfig, zoneName, callback) { var route53 = new AWS.Route53(getDnsCredentials(dnsConfig)); route53.getHostedZone({ Id: zone.Id }, function (error, result) { - if (error && error.code === 'AccessDenied') return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, error.message)); - if (error && error.code === 'InvalidClientTokenId') return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, error.message)); - if (error) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error.message)); + 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) return callback(new DomainError(DomainError.EXTERNAL_ERROR, error.message)); callback(null, result); }); @@ -107,11 +107,11 @@ function add(dnsConfig, zoneName, subdomain, type, values, callback) { var route53 = new AWS.Route53(getDnsCredentials(dnsConfig)); route53.changeResourceRecordSets(params, function(error, result) { - if (error && error.code === 'AccessDenied') return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, error.message)); - if (error && error.code === 'InvalidClientTokenId') return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, error.message)); - if (error && error.code === 'PriorRequestNotComplete') return callback(new SubdomainError(SubdomainError.STILL_BUSY, error.message)); - if (error && error.code === 'InvalidChangeBatch') return callback(new SubdomainError(SubdomainError.BAD_FIELD, error.message)); - if (error) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error.message)); + 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.code === 'PriorRequestNotComplete') return callback(new DomainError(DomainError.STILL_BUSY, error.message)); + if (error && error.code === 'InvalidChangeBatch') return callback(new DomainError(DomainError.BAD_FIELD, error.message)); + if (error) return callback(new DomainError(DomainError.EXTERNAL_ERROR, error.message)); callback(null, result.ChangeInfo.Id); }); @@ -148,9 +148,9 @@ function get(dnsConfig, zoneName, subdomain, type, callback) { var route53 = new AWS.Route53(getDnsCredentials(dnsConfig)); route53.listResourceRecordSets(params, function (error, result) { - if (error && error.code === 'AccessDenied') return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, error.message)); - if (error && error.code === 'InvalidClientTokenId') return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, error.message)); - if (error) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error.message)); + 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) return callback(new DomainError(DomainError.EXTERNAL_ERROR, error.message)); if (result.ResourceRecordSets.length === 0) return callback(null, [ ]); if (result.ResourceRecordSets[0].Name !== params.StartRecordName || result.ResourceRecordSets[0].Type !== params.StartRecordType) return callback(null, [ ]); @@ -194,23 +194,23 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) { var route53 = new AWS.Route53(getDnsCredentials(dnsConfig)); route53.changeResourceRecordSets(params, function(error, result) { - if (error && error.code === 'AccessDenied') return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, error.message)); - if (error && error.code === 'InvalidClientTokenId') return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, error.message)); + 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) { debug('del: resource record set not found.', error); - return callback(new SubdomainError(SubdomainError.NOT_FOUND, error.message)); + return callback(new DomainError(DomainError.NOT_FOUND, error.message)); } else if (error && error.code === 'NoSuchHostedZone') { debug('del: hosted zone not found.', error); - return callback(new SubdomainError(SubdomainError.NOT_FOUND, error.message)); + return callback(new DomainError(DomainError.NOT_FOUND, error.message)); } else if (error && error.code === 'PriorRequestNotComplete') { debug('del: resource is still busy', error); - return callback(new SubdomainError(SubdomainError.STILL_BUSY, error.message)); + return callback(new DomainError(DomainError.STILL_BUSY, error.message)); } else if (error && error.code === 'InvalidChangeBatch') { debug('del: invalid change batch. No such record to be deleted.'); - return callback(new SubdomainError(SubdomainError.NOT_FOUND, error.message)); + return callback(new DomainError(DomainError.NOT_FOUND, error.message)); } else if (error) { debug('del: error', error); - return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error.message)); + return callback(new DomainError(DomainError.EXTERNAL_ERROR, error.message)); } callback(null); @@ -236,25 +236,31 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) { if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here dns.resolveNs(zoneName, function (error, nameservers) { - if (error && error.code === 'ENOTFOUND') return callback(new SubdomainError(SubdomainError.BAD_FIELD, 'Unable to resolve nameservers for this domain')); - if (error || !nameservers) return callback(new SubdomainError(SubdomainError.BAD_FIELD, error ? error.message : 'Unable to get 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')); getHostedZone(credentials, zoneName, function (error, zone) { if (error) return callback(error); if (!_.isEqual(zone.DelegationSet.NameServers.sort(), nameservers.sort())) { debug('verifyDnsConfig: %j and %j do not match', nameservers, zone.DelegationSet.NameServers); - return callback(new SubdomainError(SubdomainError.BAD_FIELD, 'Domain nameservers are not set to Route53')); + return callback(new DomainError(DomainError.BAD_FIELD, 'Domain nameservers are not set to Route53')); } - const name = config.adminLocation() + (fqdn === zoneName ? '' : '.' + fqdn.slice(0, - zoneName.length - 1)); + const testSubdomain = 'cloudrontestdns'; - upsert(credentials, zoneName, name, 'A', [ ip ], function (error, changeId) { + upsert(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error, changeId) { if (error) return callback(error); - debug('verifyDnsConfig: A record added with change id %s', changeId); + debug('verifyDnsConfig: Test A record added with change id %s', changeId); - callback(null, credentials); + del(dnsConfig, zoneName, testSubdomain, 'A', [ ip ], function (error) { + if (error) return callback(error); + + debug('verifyDnsConfig: Test A record removed again'); + + callback(null, credentials); + }); }); }); }); diff --git a/src/dns/waitfordns.js b/src/dns/waitfordns.js index 3e2dc2365..5d1a3287d 100644 --- a/src/dns/waitfordns.js +++ b/src/dns/waitfordns.js @@ -7,7 +7,7 @@ var assert = require('assert'), debug = require('debug')('box:dns/waitfordns'), dig = require('../dig.js'), dns = require('dns'), - SubdomainError = require('../subdomains.js').SubdomainError, + DomainError = require('../domains.js').DomainError, util = require('util'); function isChangeSynced(domain, value, type, nameserver, callback) { @@ -79,12 +79,12 @@ function waitForDns(domain, zoneName, value, type, options, callback) { debug('waitForDNS: %s attempt %s.', domain, attempt++); dns.resolveNs(zoneName, function (error, nameservers) { - if (error || !nameservers) return retryCallback(error || new SubdomainError(SubdomainError.EXTERNAL_ERROR, 'Unable to get nameservers')); + if (error || !nameservers) return retryCallback(error || new DomainError(DomainError.EXTERNAL_ERROR, 'Unable to get 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 SubdomainError(SubdomainError.EXTERNAL_ERROR, 'ETRYAGAIN')); + retryCallback(synced ? null : new DomainError(DomainError.EXTERNAL_ERROR, 'ETRYAGAIN')); }); }); }, function retryDone(error) { diff --git a/src/docker.js b/src/docker.js index 54afd42a3..c8d566f13 100644 --- a/src/docker.js +++ b/src/docker.js @@ -51,7 +51,7 @@ var addons = require('./addons.js'), function debugApp(app, args) { assert(!app || typeof app === 'object'); - var prefix = app ? (app.location || '(bare)') : '(no app)'; + var prefix = app ? config.appFqdn(app) : '(no app)'; debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1))); } @@ -129,7 +129,7 @@ function createSubcontainer(app, name, cmd, options, callback) { var manifest = app.manifest; var exposedPorts = {}, dockerPortBindings = { }; - var domain = app.altDomain || config.appFqdn(app.location); + var domain = app.altDomain || config.appFqdn(app); var stdEnv = [ 'CLOUDRON=1', 'WEBADMIN_ORIGIN=' + config.adminOrigin(), @@ -186,7 +186,7 @@ function createSubcontainer(app, name, cmd, options, callback) { '/run': {} }, Labels: { - 'location': app.location, + 'fqdn': config.appFqdn(app), 'appId': app.id, 'isSubcontainer': String(!isAppContainer) }, diff --git a/src/domaindb.js b/src/domaindb.js new file mode 100644 index 000000000..03c403ea5 --- /dev/null +++ b/src/domaindb.js @@ -0,0 +1,122 @@ +/* jslint node:true */ + +'use strict'; + +exports = module.exports = { + add: add, + get: get, + getAll: getAll, + update: update, + upsert: upsert, + del: del, + + _clear: clear, + _addDefaultDomain: addDefaultDomain +}; + +var assert = require('assert'), + database = require('./database.js'), + DatabaseError = require('./databaseerror'), + config = require('./config.js'), + safe = require('safetydance'); + +function postProcess(data) { + data.config = safe.JSON.parse(data.configJson); + delete data.configJson; + + return data; +} + +function get(domain, callback) { + assert.strictEqual(typeof domain, 'string'); + assert.strictEqual(typeof callback, 'function'); + + 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)); + + postProcess(result[0]); + + callback(null, result[0]); + }); +} + +function getAll(callback) { + database.query('SELECT * FROM domains ORDER BY domain', function (error, results) { + if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error)); + + results.forEach(postProcess); + + callback(null, results); + }); +} + +function add(domain, zoneName, config, callback) { + assert.strictEqual(typeof domain, 'string'); + assert.strictEqual(typeof zoneName, 'string'); + assert.strictEqual(typeof config, 'object'); + assert.strictEqual(typeof callback, 'function'); + + database.query('INSERT INTO domains (domain, zoneName, configJson) VALUES (?, ?, ?)', [ domain, zoneName, 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)); + + callback(null); + }); +} + +function upsert(domain, zoneName, config, callback) { + assert.strictEqual(typeof domain, 'string'); + assert.strictEqual(typeof zoneName, 'string'); + assert.strictEqual(typeof config, 'object'); + assert.strictEqual(typeof callback, 'function'); + + database.query('REPLACE INTO domains (domain, zoneName, configJson) VALUES (?, ?, ?)', [ domain, zoneName, JSON.stringify(config) ], function (error) { + if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error)); + + callback(null); + }); +} + +function update(domain, config, callback) { + assert.strictEqual(typeof domain, 'string'); + assert.strictEqual(typeof config, 'object'); + assert.strictEqual(typeof callback, 'function'); + + database.query('UPDATE domains SET configJson=? WHERE domain=?', [ 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)); + + callback(null); + }); +} + +function del(domain, callback) { + assert.strictEqual(typeof domain, 'string'); + assert.strictEqual(typeof callback, 'function'); + + 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 !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND)); + + callback(null); + }); +} + +function clear(callback) { + database.query('DELETE FROM domains', function (error) { + if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error)); + + callback(error); + }); +} + +function addDefaultDomain(callback) { + assert(config.fqdn(), 'no fqdn set in config, cannot continue'); + + add(config.fqdn(), config.zoneName(), {}, function (error) { + if (error && error.reason !== DatabaseError.ALREADY_EXISTS) return callback(error); + callback(); + }); +} diff --git a/src/domains.js b/src/domains.js new file mode 100644 index 000000000..8d5c27b4e --- /dev/null +++ b/src/domains.js @@ -0,0 +1,292 @@ +'use strict'; + +module.exports = exports = { + add: add, + get: get, + getAll: getAll, + update: update, + del: del, + + getDNSRecords: getDNSRecords, + upsertDNSRecords: upsertDNSRecords, + removeDNSRecords: removeDNSRecords, + + waitForDNSRecord: waitForDNSRecord, + + DomainError: DomainError +}; + +var assert = require('assert'), + certificates = require('./certificates.js'), + CertificatesError = certificates.CertificatesError, + DatabaseError = require('./databaseerror.js'), + debug = require('debug')('box:domains'), + domaindb = require('./domaindb.js'), + sysinfo = require('./sysinfo.js'), + tld = require('tldjs'), + util = require('util'); + +function DomainError(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(DomainError, Error); + +DomainError.NOT_FOUND = 'No such domain'; +DomainError.ALREADY_EXISTS = 'Domain already exists'; +DomainError.EXTERNAL_ERROR = 'External error'; +DomainError.BAD_FIELD = 'Bad Field'; +DomainError.STILL_BUSY = 'Still busy'; +DomainError.IN_USE = 'In Use'; +DomainError.INTERNAL_ERROR = 'Internal error'; +DomainError.ACCESS_DENIED = 'Access denied'; +DomainError.INVALID_PROVIDER = 'provider must be route53, gcdns, digitalocean, cloudflare, noop, manual or caas'; + +// choose which subdomain backend we use for test purpose we use route53 +function api(provider) { + assert.strictEqual(typeof provider, 'string'); + + switch (provider) { + case 'caas': return require('./dns/caas.js'); + case 'cloudflare': return require('./dns/cloudflare.js'); + case 'route53': return require('./dns/route53.js'); + case 'gcdns': return require('./dns/gcdns.js'); + case 'digitalocean': return require('./dns/digitalocean.js'); + case 'noop': return require('./dns/noop.js'); + case 'manual': return require('./dns/manual.js'); + default: return null; + } +} + +// TODO make it return a DomainError instead of DomainError +function verifyDnsConfig(config, domain, zoneName, ip, callback) { + assert(config && typeof config === 'object'); // the dns config to test with + assert(typeof config.provider === 'string'); + assert.strictEqual(typeof domain, 'string'); + assert.strictEqual(typeof zoneName, 'string'); + assert.strictEqual(typeof ip, 'string'); + assert.strictEqual(typeof callback, 'function'); + + var backend = api(config.provider); + if (!backend) return callback(new DomainError(DomainError.INVALID_PROVIDER)); + + api(config.provider).verifyDnsConfig(config, domain, zoneName, ip, callback); +} + + +function add(domain, zoneName, config, fallbackCertificate, callback) { + assert.strictEqual(typeof domain, 'string'); + assert.strictEqual(typeof zoneName, 'string'); + assert.strictEqual(typeof config, 'object'); + assert.strictEqual(typeof fallbackCertificate, 'object'); + assert.strictEqual(typeof callback, 'function'); + + if (!tld.isValid(domain)) return callback(new DomainError(DomainError.BAD_FIELD, 'Invalid domain')); + if (!tld.isValid(zoneName)) return callback(new DomainError(DomainError.BAD_FIELD, 'Invalid zoneName')); + + if (fallbackCertificate) { + let error = certificates.validateCertificate(fallbackCertificate.cert, fallbackCertificate.key, domain); + if (error) return callback(new DomainError(DomainError.BAD_FIELD, error.message)); + } + + sysinfo.getPublicIp(function (error, ip) { + if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, 'Error getting IP:' + error.message)); + + verifyDnsConfig(config, domain, zoneName, ip, function (error, result) { + if (error && error.reason === DomainError.ACCESS_DENIED) return callback(new DomainError(DomainError.BAD_FIELD, 'Error adding A record. Access denied')); + if (error && error.reason === DomainError.NOT_FOUND) return callback(new DomainError(DomainError.BAD_FIELD, 'Zone not found')); + if (error && error.reason === DomainError.EXTERNAL_ERROR) return callback(new DomainError(DomainError.BAD_FIELD, 'Error adding A record:' + error.message)); + if (error && error.reason === DomainError.BAD_FIELD) return callback(new DomainError(DomainError.BAD_FIELD, error.message)); + 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, 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)); + + 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(); + }); + }); + }); + }); +} + +function get(domain, callback) { + assert.strictEqual(typeof domain, 'string'); + assert.strictEqual(typeof callback, 'function'); + + domaindb.get(domain, function (error, result) { + // TODO try to find subdomain entries maybe based on zoneNames or so + if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DomainError(DomainError.NOT_FOUND)); + if (error) 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)); + + if (fallbackCertificate) result.fallbackCertificate = fallbackCertificate; + + return callback(null, result); + }); + }); +} + +function getAll(callback) { + assert.strictEqual(typeof callback, 'function'); + + domaindb.getAll(function (error, result) { + if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error)); + + return callback(null, result); + }); +} + +function update(domain, config, fallbackCertificate, callback) { + assert.strictEqual(typeof domain, 'string'); + assert.strictEqual(typeof config, 'object'); + assert.strictEqual(typeof fallbackCertificate, 'object'); + assert.strictEqual(typeof callback, 'function'); + + domaindb.get(domain, function (error, result) { + 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) { + let error = certificates.validateCertificate(fallbackCertificate.cert, fallbackCertificate.key, domain); + if (error) return callback(new DomainError(DomainError.BAD_FIELD, error.message)); + } + + sysinfo.getPublicIp(function (error, ip) { + if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, 'Error getting IP:' + error.message)); + + verifyDnsConfig(config, domain, result.zoneName, ip, function (error, result) { + if (error && error.reason === DomainError.ACCESS_DENIED) return callback(new DomainError(DomainError.BAD_FIELD, 'Error adding A record. Access denied')); + if (error && error.reason === DomainError.NOT_FOUND) return callback(new DomainError(DomainError.BAD_FIELD, 'Zone not found')); + if (error && error.reason === DomainError.EXTERNAL_ERROR) return callback(new DomainError(DomainError.BAD_FIELD, 'Error adding A record:' + error.message)); + if (error && error.reason === DomainError.BAD_FIELD) return callback(new DomainError(DomainError.BAD_FIELD, error.message)); + 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, 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(); + + // 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(); + }); + }); + }); + }); + }); +} + +function del(domain, callback) { + assert.strictEqual(typeof domain, 'string'); + assert.strictEqual(typeof callback, 'function'); + + domaindb.del(domain, function (error) { + if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DomainError(DomainError.NOT_FOUND)); + if (error && error.reason === DatabaseError.IN_USE) return callback(new DomainError(DomainError.IN_USE)); + if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error)); + + return callback(null); + }); +} + +function getDNSRecords(subdomain, domain, type, callback) { + assert.strictEqual(typeof subdomain, 'string'); + assert.strictEqual(typeof domain, 'string'); + assert.strictEqual(typeof type, 'string'); + assert.strictEqual(typeof callback, 'function'); + + get(domain, function (error, result) { + if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error)); + + api(result.config.provider).get(result.config, result.zoneName, subdomain, type, function (error, values) { + if (error) return callback(error); + + callback(null, values); + }); + }); +} + +function upsertDNSRecords(subdomain, domain, type, values, callback) { + assert.strictEqual(typeof subdomain, 'string'); + assert.strictEqual(typeof domain, 'string'); + assert.strictEqual(typeof type, 'string'); + assert(util.isArray(values)); + assert.strictEqual(typeof callback, 'function'); + + debug('upsertDNSRecord: %s on %s type %s values', subdomain, domain, type, values); + + get(domain, function (error, result) { + if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error)); + + api(result.config.provider).upsert(result.config, result.zoneName, subdomain, type, values, function (error, changeId) { + if (error) return callback(error); + + callback(null, changeId); + }); + }); +} + +function removeDNSRecords(subdomain, domain, type, values, callback) { + assert.strictEqual(typeof subdomain, 'string'); + assert.strictEqual(typeof domain, 'string'); + assert.strictEqual(typeof type, 'string'); + assert(util.isArray(values)); + assert.strictEqual(typeof callback, 'function'); + + debug('removeDNSRecord: %s on %s type %s values', subdomain, domain, type, values); + + get(domain, function (error, result) { + if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error)); + + api(result.config.provider).del(result.config, result.zoneName, subdomain, type, values, function (error) { + if (error && error.reason !== DomainError.NOT_FOUND) return callback(error); + + callback(null); + }); + }); +} + +function waitForDNSRecord(fqdn, domain, value, type, options, callback) { + assert.strictEqual(typeof fqdn, 'string'); + assert.strictEqual(typeof domain, '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 && error.reason !== DomainError.NOT_FOUND) return callback(new DomainError(DomainError.INTERNAL_ERROR, error)); + + // if the domain is on another zone in case of external domain, use the correct zone + const zoneName = result ? result.zoneName : tld.getDomain(domain); + const provider = result ? result.config.provider : 'manual'; + + api(provider).waitForDns(fqdn, zoneName, value, type, options, callback); + }); +} diff --git a/src/email.js b/src/email.js index c109715f2..2532c70fe 100644 --- a/src/email.js +++ b/src/email.js @@ -12,10 +12,8 @@ var assert = require('assert'), async = require('async'), cloudron = require('./cloudron.js'), config = require('./config.js'), - constants = require('./constants.js'), debug = require('debug')('box:email'), dig = require('./dig.js'), - mailer = require('./mailer.js'), net = require('net'), nodemailer = require('nodemailer'), safe = require('safetydance'), @@ -25,8 +23,6 @@ var assert = require('assert'), util = require('util'), _ = require('underscore'); -var NOOP_CALLBACK = function (error) { if (error) console.error(error); }; - const digOptions = { server: '127.0.0.1', port: 53, timeout: 5000 }; function EmailError(reason, errorOrMessage) { @@ -113,7 +109,7 @@ function checkSmtpRelay(relay, callback) { return callback(error, result); } - callback(null, result); + callback(null, result); }); } @@ -266,59 +262,59 @@ function checkPtr(callback) { // 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': 'Barracuda', + 'dns': 'b.barracudacentral.org', + 'site': 'http://www.barracudacentral.org/rbl/removal-request' }, { - "name": "SpamCop", - "dns": "bl.spamcop.net", - "site": "http://spamcop.net" + '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 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': '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': 'Composite Blocking List', + 'dns': 'cbl.abuseat.org', + 'site': 'http://www.abuseat.org' }, { - "name": "SpamHaus Zen", - "dns": "zen.spamhaus.org", - "site": "http://spamhaus.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': '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': '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': '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': '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" + 'name': 'UCEPROTECT Network', + 'dns': 'dnsbl-1.uceprotect.net', + 'site': 'http://www.uceprotect.net/en' } ]; diff --git a/src/groupdb.js b/src/groupdb.js index 51b25361c..19b9e07ae 100644 --- a/src/groupdb.js +++ b/src/groupdb.js @@ -24,6 +24,7 @@ exports = module.exports = { var assert = require('assert'), constants = require('./constants.js'), + config = require('./config.js'), database = require('./database.js'), DatabaseError = require('./databaseerror'), mailboxdb = require('./mailboxdb.js'); @@ -88,10 +89,8 @@ function add(id, name, callback) { assert.strictEqual(typeof name, 'string'); assert.strictEqual(typeof callback, 'function'); - var data = [ id, name ]; - var queries = []; - queries.push({ query: 'INSERT INTO mailboxes (name, ownerId, ownerType) VALUES (?, ?, ?)', args: [ name, id, mailboxdb.TYPE_GROUP ] }); + 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) { diff --git a/src/ldap.js b/src/ldap.js index 22b876024..1a1313af9 100644 --- a/src/ldap.js +++ b/src/ldap.js @@ -265,7 +265,7 @@ function mailboxSearch(req, res, next) { name = parts[0]; } - mailboxdb.getMailbox(name, 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())); @@ -298,7 +298,7 @@ function mailAliasSearch(req, res, next) { if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString())); - mailboxdb.getAlias(req.dn.rdns[0].attrs.cn.value.toLowerCase(), 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())); @@ -331,7 +331,7 @@ function mailingListSearch(req, res, next) { if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString())); - mailboxdb.getGroup(req.dn.rdns[0].attrs.cn.value.toLowerCase(), 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())); @@ -419,7 +419,7 @@ function authenticateMailbox(req, res, next) { name = parts[0]; } - mailboxdb.getMailbox(name, 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.message)); @@ -479,13 +479,13 @@ function start(callback) { gServer.compare('cn=admins,ou=groups,dc=cloudron', groupAdminsCompare); // this is the bind for addons (after bind, they might search and authenticate) - gServer.bind('ou=addons,dc=cloudron', function(req, res, next) { + gServer.bind('ou=addons,dc=cloudron', function(req, res /*, next */) { debug('addons bind: %s', req.dn.toString()); // note: cn can be email or id res.end(); }); // this is the bind for apps (after bind, they might search and authenticate user) - gServer.bind('ou=apps,dc=cloudron', function(req, res, next) { + gServer.bind('ou=apps,dc=cloudron', function(req, res /*, next */) { // TODO: validate password debug('application bind: %s', req.dn.toString()); res.end(); diff --git a/src/locker.js b/src/locker.js index e3b219b01..ac679714f 100644 --- a/src/locker.js +++ b/src/locker.js @@ -15,6 +15,7 @@ util.inherits(Locker, EventEmitter); // these are mutually exclusive operations Locker.prototype.OP_BOX_UPDATE = 'box_update'; +Locker.prototype.OP_PLATFORM_START = 'platform_start'; Locker.prototype.OP_FULL_BACKUP = 'full_backup'; Locker.prototype.OP_APPTASK = 'apptask'; Locker.prototype.OP_MIGRATE = 'migrate'; diff --git a/src/mail_templates/feedback.ejs b/src/mail_templates/feedback.ejs deleted file mode 100644 index 901b9dd50..000000000 --- a/src/mail_templates/feedback.ejs +++ /dev/null @@ -1,14 +0,0 @@ -<%if (format === 'text') { %> - -New <%= type %> from <%= fqdn %>. - -Sender: <%= user.email %> -Sent at: <%= new Date().toUTCString() %> - -Subject: <%= subject %> ------------------------------------------------------------ -<%= description %> - -<% } else { %> - -<% } %> diff --git a/src/mailboxdb.js b/src/mailboxdb.js index 847f17b64..c5fb8448f 100644 --- a/src/mailboxdb.js +++ b/src/mailboxdb.js @@ -31,15 +31,16 @@ var assert = require('assert'), DatabaseError = require('./databaseerror.js'), util = require('util'); -var MAILBOX_FIELDS = [ 'name', 'ownerId', 'ownerType', 'aliasTarget', 'creationTime' ].join(','); +var MAILBOX_FIELDS = [ 'name', 'ownerId', 'ownerType', 'aliasTarget', 'creationTime', 'domain' ].join(','); -function add(name, ownerId, ownerType, callback) { +function add(name, domain, ownerId, ownerType, callback) { assert.strictEqual(typeof name, 'string'); + assert.strictEqual(typeof domain, 'string'); assert.strictEqual(typeof ownerId, 'string'); assert.strictEqual(typeof ownerType, 'string'); assert.strictEqual(typeof callback, 'function'); - database.query('INSERT INTO mailboxes (name, ownerId, ownerType) VALUES (?, ?, ?)', [ name, ownerId, ownerType ], function (error) { + database.query('INSERT INTO mailboxes (name, domain, ownerId, ownerType) VALUES (?, ?, ?, ?)', [ name, domain, ownerId, ownerType ], function (error) { if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, 'mailbox already exists')); if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error)); @@ -56,12 +57,13 @@ function clear(callback) { }); } -function del(name, callback) { +function del(name, domain, callback) { assert.strictEqual(typeof name, 'string'); + assert.strictEqual(typeof domain, 'string'); assert.strictEqual(typeof callback, 'function'); // deletes aliases as well - database.query('DELETE FROM mailboxes WHERE name=? OR aliasTarget = ?', [ name, name ], function (error, result) { + database.query('DELETE FROM mailboxes WHERE (name=? OR aliasTarget = ?) AND domain = ?', [ name, name, domain ], function (error, result) { if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error)); if (result.affectedRows === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND)); @@ -81,15 +83,17 @@ function delByOwnerId(id, callback) { }); } -function updateName(oldName, newName, callback) { +function updateName(oldName, oldDomain, newName, newDomain, callback) { assert.strictEqual(typeof oldName, 'string'); + assert.strictEqual(typeof oldDomain, 'string'); assert.strictEqual(typeof newName, 'string'); + assert.strictEqual(typeof newDomain, 'string'); assert.strictEqual(typeof callback, 'function'); // skip if no changes - if (oldName === newName) return callback(null); + if (oldName === newName && oldDomain === newDomain) return callback(null); - database.query('UPDATE mailboxes SET name=? WHERE name=?', [ newName, oldName ], function (error, result) { + database.query('UPDATE mailboxes SET name=?, domain=? WHERE name=? AND domain = ?', [ newName, newDomain, oldName, oldDomain ], function (error, result) { if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, 'mailbox already exists')); if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error)); if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND)); @@ -98,11 +102,12 @@ function updateName(oldName, newName, callback) { }); } -function getMailbox(name, callback) { +function getMailbox(name, domain, callback) { assert.strictEqual(typeof name, 'string'); + assert.strictEqual(typeof domain, 'string'); assert.strictEqual(typeof callback, 'function'); - database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE name = ? AND (ownerType = ? OR ownerType = ?) AND aliasTarget IS NULL', [ name, exports.TYPE_APP, exports.TYPE_USER ], function (error, results) { + database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE name = ? AND domain = ? AND (ownerType = ? OR ownerType = ?) AND aliasTarget IS NULL', [ name, domain, exports.TYPE_APP, exports.TYPE_USER ], function (error, results) { if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error)); if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND)); @@ -110,18 +115,20 @@ function getMailbox(name, callback) { }); } -function listMailboxes(callback) { +function listMailboxes(domain, callback) { + assert.strictEqual(typeof domain, 'string'); assert.strictEqual(typeof callback, 'function'); - database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE (ownerType = ? OR ownerType = ?) AND aliasTarget IS NULL ORDER BY name', [ exports.TYPE_APP, exports.TYPE_USER ], function (error, results) { + database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE domain = ? AND (ownerType = ? OR ownerType = ?) AND aliasTarget IS NULL ORDER BY name', [ domain, exports.TYPE_APP, exports.TYPE_USER ], function (error, results) { if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error)); callback(null, results); }); } -function getGroup(name, callback) { +function getGroup(name, domain, callback) { assert.strictEqual(typeof name, 'string'); + assert.strictEqual(typeof domain, 'string'); assert.strictEqual(typeof callback, 'function'); // This can be merged into a single query but cannot get 'not found' information @@ -130,7 +137,7 @@ function getGroup(name, callback) { // INNER JOIN users ON groupMembers.userId = users.id // WHERE mailboxes.name = - database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE name = ? AND ownerType = ? AND aliasTarget IS NULL', [ name, exports.TYPE_GROUP ], function (error, results) { + database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE name = ? AND domain = ? AND ownerType = ? AND aliasTarget IS NULL', [ name, 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)); @@ -157,20 +164,21 @@ function getByOwnerId(ownerId, callback) { }); } -function setAliasesForName(name, aliases, callback) { +function setAliasesForName(name, domain, aliases, callback) { assert.strictEqual(typeof name, 'string'); + assert.strictEqual(typeof domain, 'string'); assert(util.isArray(aliases)); assert.strictEqual(typeof callback, 'function'); - database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE name = ? ', [ name ], function (error, results) { + database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE name = ? AND domain = ?', [ name, 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)); var queries = []; - queries.push({ query: 'DELETE FROM mailboxes WHERE aliasTarget = ?', args: [ name ] }); + queries.push({ query: 'DELETE FROM mailboxes WHERE aliasTarget = ? AND domain = ?', args: [ name, domain ] }); aliases.forEach(function (alias) { - queries.push({ query: 'INSERT INTO mailboxes (name, aliasTarget, ownerId, ownerType) VALUES (?, ?, ?, ?)', - args: [ alias, name, results[0].ownerId, results[0].ownerType ] }); + queries.push({ query: 'INSERT INTO mailboxes (name, domain, aliasTarget, ownerId, ownerType) VALUES (?, ?, ?, ?, ?)', + args: [ alias, domain, name, results[0].ownerId, results[0].ownerType ] }); }); database.transaction(queries, function (error) { @@ -182,11 +190,12 @@ function setAliasesForName(name, aliases, callback) { }); } -function getAliasesForName(name, callback) { +function getAliasesForName(name, domain, callback) { assert.strictEqual(typeof name, 'string'); + assert.strictEqual(typeof domain, 'string'); assert.strictEqual(typeof callback, 'function'); - database.query('SELECT name FROM mailboxes WHERE aliasTarget=? ORDER BY name', [ name ], function (error, results) { + database.query('SELECT name FROM mailboxes WHERE aliasTarget = ? AND domain = ? ORDER BY name', [ name, domain ], function (error, results) { if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error)); results = results.map(function (r) { return r.name; }); @@ -194,21 +203,23 @@ function getAliasesForName(name, callback) { }); } -function listAliases(callback) { +function listAliases(domain, callback) { + assert.strictEqual(typeof domain, 'string'); assert.strictEqual(typeof callback, 'function'); - database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE aliasTarget IS NOT NULL ORDER BY name', function (error, results) { + database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE domain = ? AND aliasTarget IS NOT NULL ORDER BY name', [ domain ], function (error, results) { if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error)); callback(null, results); }); } -function getAlias(name, callback) { +function getAlias(name, domain, callback) { assert.strictEqual(typeof name, 'string'); + assert.strictEqual(typeof domain, 'string'); assert.strictEqual(typeof callback, 'function'); - database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE name = ? AND aliasTarget IS NOT NULL', [ name ], function (error, results) { + database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE name = ? AND domain = ? AND aliasTarget IS NOT NULL', [ name, 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)); diff --git a/src/mailer.js b/src/mailer.js index 2881db4c7..ee1ec44ec 100644 --- a/src/mailer.js +++ b/src/mailer.js @@ -1,9 +1,6 @@ 'use strict'; exports = module.exports = { - start: start, - stop: stop, - userAdded: userAdded, userRemoved: userRemoved, adminChanged: adminChanged, @@ -23,22 +20,13 @@ exports = module.exports = { certificateRenewalError: certificateRenewalError, - FEEDBACK_TYPE_FEEDBACK: 'feedback', - FEEDBACK_TYPE_TICKET: 'ticket', - FEEDBACK_TYPE_APP_MISSING: 'app_missing', - FEEDBACK_TYPE_APP_ERROR: 'app_error', - FEEDBACK_TYPE_UPGRADE_REQUEST: 'upgrade_request', - sendFeedback: sendFeedback, - sendTestMail: sendTestMail, _getMailQueue: _getMailQueue, _clearMailQueue: _clearMailQueue }; -var appstore = require('./appstore.js'), - AppstoreError = appstore.AppstoreError, - assert = require('assert'), +var assert = require('assert'), async = require('async'), config = require('./config.js'), debug = require('debug')('box:mailer'), @@ -58,8 +46,7 @@ var NOOP_CALLBACK = function (error) { if (error) debug(error); }; var MAIL_TEMPLATES_DIR = path.join(__dirname, 'mail_templates'); -var gMailQueue = [ ], - gPaused = false; +var gMailQueue = [ ]; function splatchError(error) { var result = { }; @@ -72,25 +59,6 @@ function splatchError(error) { return util.inspect(result, { depth: null, showHidden: true }); } -function start(callback) { - assert.strictEqual(typeof callback, 'function'); - - if (process.env.BOX_ENV === 'test') gPaused = true; - - callback(null); -} - -function stop(callback) { - assert.strictEqual(typeof callback, 'function'); - - // TODO: interrupt processQueue as well - - debug(gMailQueue.length + ' mail items dropped'); - gMailQueue = [ ]; - - callback(null); -} - function mailConfig() { return { from: '"Cloudron" ' @@ -98,8 +66,6 @@ function mailConfig() { } function processQueue() { - assert(!gPaused); - sendMails(gMailQueue); gMailQueue = [ ]; } @@ -146,7 +112,7 @@ function enqueue(mailOptions) { debug('Queued mail for ' + mailOptions.from + ' to ' + mailOptions.to); gMailQueue.push(mailOptions); - if (!gPaused) processQueue(); + if (process.env.BOX_ENV !== 'test') processQueue(); } function render(templateFile, params) { @@ -575,28 +541,6 @@ function unexpectedExit(program, context, callback) { sendMails([ mailOptions ], callback); } -function sendFeedback(user, type, subject, description) { - assert.strictEqual(typeof user, 'object'); - assert.strictEqual(typeof type, 'string'); - assert.strictEqual(typeof subject, 'string'); - assert.strictEqual(typeof description, 'string'); - - assert(type === exports.FEEDBACK_TYPE_TICKET || - type === exports.FEEDBACK_TYPE_FEEDBACK || - type === exports.FEEDBACK_TYPE_APP_MISSING || - type === exports.FEEDBACK_TYPE_UPGRADE_REQUEST || - type === exports.FEEDBACK_TYPE_APP_ERROR); - - var mailOptions = { - from: mailConfig().from, - to: 'support@cloudron.io', - subject: util.format('[%s] %s - %s', type, config.fqdn(), subject), - text: render('feedback.ejs', { fqdn: config.fqdn(), type: type, user: user, subject: subject, description: description, format: 'text'}) - }; - - enqueue(mailOptions); -} - function sendTestMail(email) { assert.strictEqual(typeof email, 'string'); diff --git a/src/nginx.js b/src/nginx.js index 7e8e88bb2..878c6d178 100644 --- a/src/nginx.js +++ b/src/nginx.js @@ -55,7 +55,7 @@ function configureApp(app, certFilePath, keyFilePath, callback) { var sourceDir = path.resolve(__dirname, '..'); var endpoint = 'app'; - var vhost = app.altDomain || config.appFqdn(app.location); + var vhost = app.altDomain || config.appFqdn(app); var data = { sourceDir: sourceDir, @@ -86,7 +86,7 @@ function unconfigureApp(app, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof callback, 'function'); - var vhost = app.altDomain || config.appFqdn(app.location); + var vhost = app.altDomain || config.appFqdn(app); var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, app.id + '.conf'); if (!safe.fs.unlinkSync(nginxConfigFilename)) { diff --git a/src/paths.js b/src/paths.js index 6dd863b21..c592f5a3a 100644 --- a/src/paths.js +++ b/src/paths.js @@ -31,6 +31,7 @@ exports = module.exports = { ACME_ACCOUNT_KEY_FILE: path.join(config.baseDir(), 'boxdata/acme/acme.key'), APP_CERTS_DIR: path.join(config.baseDir(), 'boxdata/certs'), CLOUDRON_AVATAR_FILE: path.join(config.baseDir(), 'boxdata/avatar.png'), - FIRST_RUN_FILE: path.join(config.baseDir(), 'boxdata/first_run'), - UPDATE_CHECKER_FILE: path.join(config.baseDir(), 'boxdata/updatechecker.json') + UPDATE_CHECKER_FILE: path.join(config.baseDir(), 'boxdata/updatechecker.json'), + + AUTO_PROVISION_FILE: path.join(config.baseDir(), 'configs/autoprovision.json') }; diff --git a/src/platform.js b/src/platform.js index b0853f10a..4ba17ed1d 100644 --- a/src/platform.js +++ b/src/platform.js @@ -13,9 +13,11 @@ var apps = require('./apps.js'), 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'), nginx = require('./nginx.js'), os = require('os'), paths = require('./paths.js'), @@ -23,7 +25,6 @@ var apps = require('./apps.js'), semver = require('semver'), settings = require('./settings.js'), shell = require('./shell.js'), - subdomains = require('./subdomains.js'), taskmanager = require('./taskmanager.js'), user = require('./user.js'), util = require('util'), @@ -63,6 +64,9 @@ function start(callback) { debug('Updating infrastructure from %s to %s', existingInfra.version, infra.version); + var error = locker.lock(locker.OP_PLATFORM_START); + if (error) return callback(error); + async.series([ stopContainers.bind(null, existingInfra), startAddons.bind(null, existingInfra), @@ -72,6 +76,8 @@ function start(callback) { ], function (error) { if (error) return callback(error); + locker.unlock(locker.OP_PLATFORM_START); + emitPlatformReady(); callback(); @@ -328,7 +334,7 @@ function startMail(callback) { ]; async.mapSeries(records, function (record, iteratorCallback) { - subdomains.upsert(record.subdomain, record.type, record.values, 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(); diff --git a/src/routes/apps.js b/src/routes/apps.js index 0b0234fe6..b0e890ece 100644 --- a/src/routes/apps.js +++ b/src/routes/apps.js @@ -51,8 +51,8 @@ function removeInternalAppFields(app) { runState: app.runState, health: app.health, location: app.location, + domain: app.domain, accessRestriction: app.accessRestriction, - lastBackupId: app.lastBackupId, manifest: app.manifest, portBindings: app.portBindings, iconUrl: app.iconUrl, @@ -64,7 +64,9 @@ function removeInternalAppFields(app) { sso: app.sso, debugMode: app.debugMode, robotsTxt: app.robotsTxt, - enableBackup: app.enableBackup + enableBackup: app.enableBackup, + creationTime: app.creationTime.toISOString(), + updateTime: app.updateTime.toISOString() }; } @@ -114,6 +116,7 @@ function installApp(req, res, next) { // required if (typeof data.location !== 'string') return next(new HttpError(400, 'location is required')); + if (typeof data.domain !== 'string') return next(new HttpError(400, 'domain is required')); if (typeof data.accessRestriction !== 'object') return next(new HttpError(400, 'accessRestriction is required')); // optional @@ -121,6 +124,7 @@ function installApp(req, res, next) { if ('icon' in data && typeof data.icon !== 'string') return next(new HttpError(400, 'icon is not a string')); if (data.backupId && typeof data.backupId !== 'string') return next(new HttpError(400, 'backupId must be string or null')); + if (data.backupFormat && typeof data.backupFormat !== 'string') return next(new HttpError(400, 'backupFormat must be string or null')); // falsy values in cert and key unset the cert if (data.key && typeof data.cert !== 'string') return next(new HttpError(400, 'cert must be a string')); @@ -165,6 +169,7 @@ function configureApp(req, res, next) { var data = req.body; if ('location' in data && typeof data.location !== 'string') return next(new HttpError(400, 'location must be string')); + if ('domain' in data && typeof data.domain !== 'string') return next(new HttpError(400, 'domain must be string')); if ('portBindings' in data && typeof data.portBindings !== 'object') return next(new HttpError(400, 'portBindings must be an object')); if ('accessRestriction' in data && typeof data.accessRestriction !== 'object') return next(new HttpError(400, 'accessRestriction must be an object')); @@ -232,12 +237,14 @@ function cloneApp(req, res, next) { if (typeof data.backupId !== 'string') return next(new HttpError(400, 'backupId must be a string')); if (typeof data.location !== 'string') return next(new HttpError(400, 'location is required')); + if (typeof data.domain !== 'string') return next(new HttpError(400, 'domain is required')); if (('portBindings' in data) && typeof data.portBindings !== 'object') return next(new HttpError(400, 'portBindings must be an object')); 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.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.EXTERNAL_ERROR) return next(new HttpError(424, error.message)); if (error) return next(new HttpError(500, error)); diff --git a/src/routes/cloudron.js b/src/routes/cloudron.js index ba1c11e0e..dc6b5a6ed 100644 --- a/src/routes/cloudron.js +++ b/src/routes/cloudron.js @@ -6,6 +6,7 @@ exports = module.exports = { setupTokenAuth: setupTokenAuth, providerTokenAuth: providerTokenAuth, getStatus: getStatus, + restore: restore, reboot: reboot, migrate: migrate, getProgress: getProgress, @@ -19,7 +20,9 @@ exports = module.exports = { sendTestMail: sendTestMail }; -var assert = require('assert'), +var appstore = require('../appstore.js'), + AppstoreError = require('../appstore.js').AppstoreError, + assert = require('assert'), async = require('async'), cloudron = require('../cloudron.js'), CloudronError = cloudron.CloudronError, @@ -66,13 +69,38 @@ function activate(req, res, next) { 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')); + 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)); - }); + 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)); }); } @@ -100,15 +128,15 @@ function setupTokenAuth(req, res, next) { 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')); + .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(); - }); + next(); + }); } else { next(); } @@ -224,17 +252,20 @@ function checkForUpdates(req, res, next) { function feedback(req, res, next) { assert.strictEqual(typeof req.user, 'object'); - if (req.body.type !== mailer.FEEDBACK_TYPE_FEEDBACK && - req.body.type !== mailer.FEEDBACK_TYPE_TICKET && - req.body.type !== mailer.FEEDBACK_TYPE_APP_MISSING && - req.body.type !== mailer.FEEDBACK_TYPE_UPGRADE_REQUEST && - req.body.type !== mailer.FEEDBACK_TYPE_APP_ERROR) return next(new HttpError(400, 'type must be either "ticket", "feedback", "app_missing", "app_error" or "upgrade_request"')); + const VALID_TYPES = [ 'feedback', 'ticket', 'app_missing', 'app_error', 'upgrade_request' ]; + + if (typeof req.body.type !== 'string' || !req.body.type) return next(new HttpError(400, 'type must be string')); + 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')); - mailer.sendFeedback(req.user, req.body.type, req.body.subject, req.body.description); + 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')); + + next(new HttpSuccess(201, {})); + }); - next(new HttpSuccess(201, {})); } function getLogs(req, res, next) { diff --git a/src/routes/developer.js b/src/routes/developer.js index 16dcca338..ccb727564 100644 --- a/src/routes/developer.js +++ b/src/routes/developer.js @@ -1,9 +1,6 @@ 'use strict'; exports = module.exports = { - enabled: enabled, - setEnabled: setEnabled, - status: status, login: login }; @@ -17,27 +14,6 @@ function auditSource(req) { return { ip: ip, username: req.user ? req.user.username : null, userId: req.user ? req.user.id : null }; } -function enabled(req, res, next) { - developer.isEnabled(function (error, enabled) { - if (enabled) return next(); - next(new HttpError(412, 'Developer mode not enabled')); - }); -} - -function setEnabled(req, res, next) { - if (typeof req.body.enabled !== 'boolean') return next(new HttpError(400, 'enabled must be boolean')); - - developer.setEnabled(req.body.enabled, auditSource(req), function (error) { - if (error) return next(new HttpError(500, error)); - - next(new HttpSuccess(200, {})); - }); -} - -function status(req, res, next) { - next(new HttpSuccess(200, {})); -} - function login(req, res, next) { passport.authenticate('local', function (error, user) { if (error) return next(new HttpError(500, error)); diff --git a/src/routes/domains.js b/src/routes/domains.js new file mode 100644 index 000000000..02401dd09 --- /dev/null +++ b/src/routes/domains.js @@ -0,0 +1,85 @@ +'use strict'; + +exports = module.exports = { + add: add, + get: get, + getAll: getAll, + update: update, + del: del +}; + +var assert = require('assert'), + domains = require('../domains.js'), + DomainError = domains.DomainError, + HttpError = require('connect-lastmile').HttpError, + HttpSuccess = require('connect-lastmile').HttpSuccess; + +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')); + if (typeof req.body.config !== 'object') return next(new HttpError(400, 'config must be an object')); + if ('zoneName' in req.body && typeof req.body.zoneName !== 'string') return next(new HttpError(400, 'zoneName must be a string')); + 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')); + + domains.add(req.body.domain, req.body.zoneName || req.body.domain, 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)); + if (error) return next(new HttpError(500, error)); + + next(new HttpSuccess(201, { domain: req.body.domain, config: req.body.config })); + }); +} + +function get(req, res, next) { + assert.strictEqual(typeof req.params.domain, 'string'); + + domains.get(req.params.domain, function (error, result) { + if (error && error.reason === DomainError.NOT_FOUND) return next(new HttpError(404, error.message)); + if (error) return next(new HttpError(500, error)); + + next(new HttpSuccess(200, result)); + }); +} + +function getAll(req, res, next) { + domains.getAll(function (error, result) { + if (error) return next(new HttpError(500, error)); + + next(new HttpSuccess(200, { domains: result })); + }); +} + +function update(req, res, next) { + assert.strictEqual(typeof req.params.domain, 'string'); + assert.strictEqual(typeof req.body, 'object'); + + if (typeof req.body.config !== 'object') return next(new HttpError(400, 'config must be an object')); + 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')); + + domains.update(req.params.domain, 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)); + if (error) return next(new HttpError(500, error)); + + next(new HttpSuccess(204, {})); + }); +} + +function del(req, res, next) { + assert.strictEqual(typeof req.params.domain, 'string'); + + 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')); + if (error) return next(new HttpError(500, error)); + + next(new HttpSuccess(204, {})); + }); +} diff --git a/src/routes/index.js b/src/routes/index.js index 58c1b64d0..5b86cf128 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -6,6 +6,7 @@ exports = module.exports = { clients: require('./clients.js'), cloudron: require('./cloudron.js'), developer: require('./developer.js'), + domains: require('./domains.js'), eventlog: require('./eventlog.js'), graphs: require('./graphs.js'), groups: require('./groups.js'), diff --git a/src/routes/settings.js b/src/routes/settings.js index 7a64b39e4..64273bb4d 100644 --- a/src/routes/settings.js +++ b/src/routes/settings.js @@ -12,9 +12,6 @@ exports = module.exports = { getEmailStatus: getEmailStatus, - getDnsConfig: getDnsConfig, - setDnsConfig: setDnsConfig, - getBackupConfig: getBackupConfig, setBackupConfig: setBackupConfig, @@ -36,7 +33,6 @@ exports = module.exports = { getAppstoreConfig: getAppstoreConfig, setAppstoreConfig: setAppstoreConfig, - setFallbackCertificate: setFallbackCertificate, setAdminCertificate: setAdminCertificate }; @@ -239,27 +235,6 @@ function getEmailStatus(req, res, next) { }); } -function getDnsConfig(req, res, next) { - settings.getDnsConfig(function (error, config) { - if (error) return next(new HttpError(500, error)); - - next(new HttpSuccess(200, config)); - }); -} - -function setDnsConfig(req, res, next) { - assert.strictEqual(typeof req.body, 'object'); - - if (typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider is required')); - - settings.setDnsConfig(req.body, config.fqdn(), config.zoneName(), 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 getBackupConfig(req, res, next) { settings.getBackupConfig(function (error, config) { if (error) return next(new HttpError(500, error)); @@ -318,21 +293,6 @@ function setAppstoreConfig(req, res, next) { }); } -// default fallback cert -function setFallbackCertificate(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.setFallbackCertificate(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, {})); - }); -} - // only webadmin cert, until it can be treated just like a normal app function setAdminCertificate(req, res, next) { assert.strictEqual(typeof req.body, 'object'); diff --git a/src/routes/test/apps-test.js b/src/routes/test/apps-test.js index 17388602a..612f721aa 100644 --- a/src/routes/test/apps-test.js +++ b/src/routes/test/apps-test.js @@ -149,7 +149,7 @@ 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('foobar.com'); + config.setFqdn('example-apps-test.com'); config.setZoneName('foobar.com'); awsHostedZones = { @@ -575,29 +575,25 @@ describe('App API', function () { }); it('app install succeeds without password but developer token', function (done) { - settings.setDeveloperMode(true, function (error) { - expect(error).to.be(null); + superagent.post(SERVER_URL + '/api/v1/developer/login') + .send({ username: USERNAME, password: PASSWORD }) + .end(function (error, result) { + expect(error).to.not.be.ok(); + expect(result.statusCode).to.equal(200); + expect(new Date(result.body.expiresAt).toString()).to.not.be('Invalid Date'); + expect(result.body.token).to.be.a('string'); - superagent.post(SERVER_URL + '/api/v1/developer/login') - .send({ username: USERNAME, password: PASSWORD }) - .end(function (error, result) { - expect(error).to.not.be.ok(); - expect(result.statusCode).to.equal(200); - expect(new Date(result.body.expiresAt).toString()).to.not.be('Invalid Date'); - expect(result.body.token).to.be.a('string'); + // overwrite non dev token + token = result.body.token; - // overwrite non dev token - token = result.body.token; - - superagent.post(SERVER_URL + '/api/v1/apps/install') - .query({ access_token: token }) - .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'); - APP_ID = res.body.id; - done(); - }); + superagent.post(SERVER_URL + '/api/v1/apps/install') + .query({ access_token: token }) + .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'); + APP_ID = res.body.id; + done(); }); }); }); diff --git a/src/routes/test/backups-test.js b/src/routes/test/backups-test.js index ef89a47b7..613c605fe 100644 --- a/src/routes/test/backups-test.js +++ b/src/routes/test/backups-test.js @@ -24,7 +24,7 @@ function setup(done) { nock.cleanAll(); config._reset(); config.setVersion('1.2.3'); - config.setFqdn('localhost'); + config.setFqdn('example-backups-test.com'); async.series([ server.start.bind(server), @@ -53,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', [ ] /* portBindings */, { }, callback); + appdb.add('appid', 'appStoreId', manifest, 'location', config.fqdn(), [ ] /* portBindings */, { }, callback); }, function createSettings(callback) { @@ -76,9 +76,6 @@ describe('Backups API', function () { before(setup); - after(function (done) { - done(); - }); after(cleanup); describe('create', function () { diff --git a/src/routes/test/clients-test.js b/src/routes/test/clients-test.js index 76649cc77..ad3eb8a03 100644 --- a/src/routes/test/clients-test.js +++ b/src/routes/test/clients-test.js @@ -1,6 +1,5 @@ 'use strict'; -/* jslint node:true */ /* global it:false */ /* global describe:false */ /* global before:false */ @@ -22,7 +21,38 @@ var async = require('async'), var SERVER_URL = 'http://localhost:' + config.get('port'); var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com'; -var token = null; // authentication token +var token = null; + +function setup(done) { + config._reset(); + config.setFqdn('example-clients-test.com'); + config.set('provider', 'caas'); + + async.series([ + server.start, + database._clear, + + function (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.equal(201); + expect(scope1.isDone()).to.be.ok(); + expect(scope2.isDone()).to.be.ok(); + + // stash token for further use + token = result.body.token; + + callback(); + }); + } + ], done); +} function cleanup(done) { database._clear(function (error) { @@ -34,170 +64,122 @@ function cleanup(done) { describe('OAuth Clients API', function () { describe('add', function () { - before(function (done) { - async.series([ - server.start.bind(null), - database._clear.bind(null), - - function (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.equal(201); - expect(scope1.isDone()).to.be.ok(); - expect(scope2.isDone()).to.be.ok(); - - // stash token for further use - token = result.body.token; - - callback(); - }); - }, - ], done); - }); - + before(setup), after(cleanup); - describe('without developer mode', function () { - before(function (done) { - settings.setDeveloperMode(false, done); - }); - - it('fails', function (done) { - superagent.post(SERVER_URL + '/api/v1/oauth/clients') - .query({ access_token: token }) - .send({ appId: 'someApp', redirectURI: 'http://foobar.com', scope: 'profile' }) - .end(function (error, result) { - expect(result.statusCode).to.equal(412); - done(); - }); + it('fails without token', function (done) { + superagent.post(SERVER_URL + '/api/v1/oauth/clients') + .send({ appId: 'someApp', redirectURI: 'http://foobar.com', scope: 'profile' }) + .end(function (error, result) { + expect(result.statusCode).to.equal(401); + done(); }); }); - describe('with developer mode', function () { - before(function (done) { - settings.setDeveloperMode(true, done); + it('fails without appId', function (done) { + superagent.post(SERVER_URL + '/api/v1/oauth/clients') + .query({ access_token: token }) + .send({ redirectURI: 'http://foobar.com', scope: 'profile' }) + .end(function (error, result) { + expect(result.statusCode).to.equal(400); + done(); }); + }); - it('fails without token', function (done) { - superagent.post(SERVER_URL + '/api/v1/oauth/clients') - .send({ appId: 'someApp', redirectURI: 'http://foobar.com', scope: 'profile' }) - .end(function (error, result) { - expect(result.statusCode).to.equal(401); - done(); - }); + it('fails with empty appId', function (done) { + superagent.post(SERVER_URL + '/api/v1/oauth/clients') + .query({ access_token: token }) + .send({ appId: '', redirectURI: 'http://foobar.com', scope: 'profile' }) + .end(function (error, result) { + expect(result.statusCode).to.equal(400); + done(); }); + }); - it('fails without appId', function (done) { - superagent.post(SERVER_URL + '/api/v1/oauth/clients') - .query({ access_token: token }) - .send({ redirectURI: 'http://foobar.com', scope: 'profile' }) - .end(function (error, result) { - expect(result.statusCode).to.equal(400); - done(); - }); + it('fails without scope', function (done) { + superagent.post(SERVER_URL + '/api/v1/oauth/clients') + .query({ access_token: token }) + .send({ appId: 'someApp', redirectURI: 'http://foobar.com' }) + .end(function (error, result) { + expect(result.statusCode).to.equal(400); + done(); }); + }); - it('fails with empty appId', function (done) { - superagent.post(SERVER_URL + '/api/v1/oauth/clients') - .query({ access_token: token }) - .send({ appId: '', redirectURI: 'http://foobar.com', scope: 'profile' }) - .end(function (error, result) { - expect(result.statusCode).to.equal(400); - done(); - }); + it('fails with empty scope', function (done) { + superagent.post(SERVER_URL + '/api/v1/oauth/clients') + .query({ access_token: token }) + .send({ appId: 'someApp', redirectURI: 'http://foobar.com', scope: '' }) + .end(function (error, result) { + expect(result.statusCode).to.equal(400); + done(); }); + }); - it('fails without scope', function (done) { - superagent.post(SERVER_URL + '/api/v1/oauth/clients') - .query({ access_token: token }) - .send({ appId: 'someApp', redirectURI: 'http://foobar.com' }) - .end(function (error, result) { - expect(result.statusCode).to.equal(400); - done(); - }); + it('fails without redirectURI', function (done) { + superagent.post(SERVER_URL + '/api/v1/oauth/clients') + .query({ access_token: token }) + .send({ appId: 'someApp', scope: 'profile' }) + .end(function (error, result) { + expect(result.statusCode).to.equal(400); + done(); }); + }); - it('fails with empty scope', function (done) { - superagent.post(SERVER_URL + '/api/v1/oauth/clients') - .query({ access_token: token }) - .send({ appId: 'someApp', redirectURI: 'http://foobar.com', scope: '' }) - .end(function (error, result) { - expect(result.statusCode).to.equal(400); - done(); - }); + it('fails with empty redirectURI', function (done) { + superagent.post(SERVER_URL + '/api/v1/oauth/clients') + .query({ access_token: token }) + .send({ appId: 'someApp', redirectURI: '', scope: 'profile' }) + .end(function (error, result) { + expect(result.statusCode).to.equal(400); + done(); }); + }); - it('fails without redirectURI', function (done) { - superagent.post(SERVER_URL + '/api/v1/oauth/clients') - .query({ access_token: token }) - .send({ appId: 'someApp', scope: 'profile' }) - .end(function (error, result) { - expect(result.statusCode).to.equal(400); - done(); - }); + it('fails with malformed redirectURI', function (done) { + superagent.post(SERVER_URL + '/api/v1/oauth/clients') + .query({ access_token: token }) + .send({ appId: 'someApp', redirectURI: 'foobar', scope: 'profile' }) + .end(function (error, result) { + expect(result.statusCode).to.equal(400); + done(); }); + }); - it('fails with empty redirectURI', function (done) { - superagent.post(SERVER_URL + '/api/v1/oauth/clients') - .query({ access_token: token }) - .send({ appId: 'someApp', redirectURI: '', scope: 'profile' }) - .end(function (error, result) { - expect(result.statusCode).to.equal(400); - done(); - }); + it('fails with invalid name', function (done) { + superagent.post(SERVER_URL + '/api/v1/oauth/clients') + .query({ access_token: token }) + .send({ appId: '$"$%^45asdfasdfadf.adf.', redirectURI: 'http://foobar.com', scope: 'profile' }) + .end(function (error, result) { + expect(result.statusCode).to.equal(400); + done(); }); + }); - it('fails with malformed redirectURI', function (done) { - superagent.post(SERVER_URL + '/api/v1/oauth/clients') - .query({ access_token: token }) - .send({ appId: 'someApp', redirectURI: 'foobar', scope: 'profile' }) - .end(function (error, result) { - expect(result.statusCode).to.equal(400); - done(); - }); + it('succeeds with dash', function (done) { + superagent.post(SERVER_URL + '/api/v1/oauth/clients') + .query({ access_token: token }) + .send({ appId: 'fo-1234-bar', redirectURI: 'http://foobar.com', scope: 'profile' }) + .end(function (error, result) { + expect(result.statusCode).to.equal(201); + done(); }); + }); - it('fails with invalid name', function (done) { - superagent.post(SERVER_URL + '/api/v1/oauth/clients') - .query({ access_token: token }) - .send({ appId: '$"$%^45asdfasdfadf.adf.', redirectURI: 'http://foobar.com', scope: 'profile' }) - .end(function (error, result) { - expect(result.statusCode).to.equal(400); - done(); - }); - }); + it('succeeds', function (done) { + superagent.post(SERVER_URL + '/api/v1/oauth/clients') + .query({ access_token: token }) + .send({ appId: 'someApp', redirectURI: 'http://foobar.com', scope: 'profile' }) + .end(function (error, result) { + expect(result.statusCode).to.equal(201); + expect(result.body.id).to.be.a('string'); + expect(result.body.appId).to.be.a('string'); + expect(result.body.redirectURI).to.be.a('string'); + expect(result.body.clientSecret).to.be.a('string'); + expect(result.body.scope).to.be.a('string'); + expect(result.body.type).to.equal(clients.TYPE_EXTERNAL); - it('succeeds with dash', function (done) { - superagent.post(SERVER_URL + '/api/v1/oauth/clients') - .query({ access_token: token }) - .send({ appId: 'fo-1234-bar', redirectURI: 'http://foobar.com', scope: 'profile' }) - .end(function (error, result) { - expect(result.statusCode).to.equal(201); - done(); - }); - }); - - it('succeeds', function (done) { - superagent.post(SERVER_URL + '/api/v1/oauth/clients') - .query({ access_token: token }) - .send({ appId: 'someApp', redirectURI: 'http://foobar.com', scope: 'profile' }) - .end(function (error, result) { - expect(result.statusCode).to.equal(201); - expect(result.body.id).to.be.a('string'); - expect(result.body.appId).to.be.a('string'); - expect(result.body.redirectURI).to.be.a('string'); - expect(result.body.clientSecret).to.be.a('string'); - expect(result.body.scope).to.be.a('string'); - expect(result.body.type).to.equal(clients.TYPE_EXTERNAL); - - done(); - }); + done(); }); }); }); @@ -212,29 +194,7 @@ describe('OAuth Clients API', function () { before(function (done) { async.series([ - server.start.bind(null), - database._clear.bind(null), - - function (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(scope1.isDone()).to.be.ok(); - expect(scope2.isDone()).to.be.ok(); - - // stash token for further use - token = result.body.token; - - callback(); - }); - }, - - settings.setDeveloperMode.bind(null, true), + setup, function (callback) { superagent.post(SERVER_URL + '/api/v1/oauth/clients') @@ -253,52 +213,31 @@ describe('OAuth Clients API', function () { after(cleanup); - describe('without developer mode', function () { - before(function (done) { - settings.setDeveloperMode(false, done); - }); - - it('fails', function (done) { - superagent.get(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id) - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(412); - done(); - }); + it('fails without token', function (done) { + superagent.get(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id) + .end(function (error, result) { + expect(result.statusCode).to.equal(401); + done(); }); }); - describe('with developer mode', function () { - before(function (done) { - settings.setDeveloperMode(true, done); + + it('fails with unknown id', function (done) { + superagent.get(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id.toUpperCase()) + .query({ access_token: token }) + .end(function (error, result) { + expect(result.statusCode).to.equal(404); + done(); }); + }); - it('fails without token', function (done) { - superagent.get(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id) - .end(function (error, result) { - expect(result.statusCode).to.equal(401); - done(); - }); - }); - - - it('fails with unknown id', function (done) { - superagent.get(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id.toUpperCase()) - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(404); - done(); - }); - }); - - it('succeeds', function (done) { - superagent.get(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id) - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(200); - expect(result.body).to.eql(CLIENT_0); - done(); - }); + it('succeeds', function (done) { + superagent.get(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id) + .query({ access_token: token }) + .end(function (error, result) { + expect(result.statusCode).to.equal(200); + expect(result.body).to.eql(CLIENT_0); + done(); }); }); }); @@ -321,29 +260,7 @@ describe('OAuth Clients API', function () { before(function (done) { async.series([ - server.start.bind(null), - database._clear.bind(null), - - function (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(scope1.isDone()).to.be.ok(); - expect(scope2.isDone()).to.be.ok(); - - // stash token for further use - token = result.body.token; - - callback(); - }); - }, - - settings.setDeveloperMode.bind(null, true), + setup, function (callback) { superagent.post(SERVER_URL + '/api/v1/oauth/clients') @@ -362,94 +279,73 @@ describe('OAuth Clients API', function () { after(cleanup); - describe('without developer mode', function () { - before(function (done) { - settings.setDeveloperMode(false, done); + it('fails without token', function (done) { + superagent.del(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id) + .end(function (error, result) { + expect(result.statusCode).to.equal(401); + done(); }); + }); + + + it('fails with unknown id', function (done) { + superagent.del(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id.toUpperCase()) + .query({ access_token: token }) + .end(function (error, result) { + expect(result.statusCode).to.equal(404); + done(); + }); + }); + + it('succeeds', function (done) { + superagent.del(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id) + .query({ access_token: token }) + .end(function (error, result) { + expect(result.statusCode).to.equal(204); + + superagent.get(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id) + .query({ access_token: token }) + .end(function (error, result) { + expect(result.statusCode).to.equal(404); - it('fails', function (done) { - superagent.del(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id) - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(412); done(); }); }); }); - describe('with developer mode', function () { - before(function (done) { - settings.setDeveloperMode(true, done); - }); + it('fails for cid-webadmin', function (done) { + superagent.del(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin') + .query({ access_token: token }) + .end(function (error, result) { + expect(result.statusCode).to.equal(405); + + superagent.get(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin') + .query({ access_token: token }) + .end(function (error, result) { + expect(result.statusCode).to.equal(200); - it('fails without token', function (done) { - superagent.del(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id) - .end(function (error, result) { - expect(result.statusCode).to.equal(401); done(); }); }); + }); + it('fails for addon auth client', function (done) { + clients.add(CLIENT_1.appId, CLIENT_1.type, CLIENT_1.redirectURI, CLIENT_1.scope, function (error, result) { + expect(error).to.equal(null); - it('fails with unknown id', function (done) { - superagent.del(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id.toUpperCase()) - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(404); - done(); - }); - }); + CLIENT_1.id = result.id; - it('succeeds', function (done) { - superagent.del(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id) - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(204); - - superagent.get(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id) - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(404); - - done(); - }); - }); - }); - - it('fails for cid-webadmin', function (done) { - superagent.del(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin') - .query({ access_token: token }) - .end(function (error, result) { + superagent.del(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_1.id) + .query({ access_token: token }) + .end(function (error, result) { expect(result.statusCode).to.equal(405); - superagent.get(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin') - .query({ access_token: token }) - .end(function (error, result) { + superagent.get(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_1.id) + .query({ access_token: token }) + .end(function (error, result) { expect(result.statusCode).to.equal(200); done(); - }); - }); - }); - - it('fails for addon auth client', function (done) { - clients.add(CLIENT_1.appId, CLIENT_1.type, CLIENT_1.redirectURI, CLIENT_1.scope, function (error, result) { - expect(error).to.equal(null); - - CLIENT_1.id = result.id; - - superagent.del(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_1.id) - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(405); - - superagent.get(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_1.id) - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(200); - - done(); - }); }); }); }); @@ -476,51 +372,27 @@ describe('Clients', function () { next(); }; - function setup(done) { + function setup2(done) { async.series([ - server.start.bind(server), - database._clear.bind(null), + setup, + function (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: USER_0.username, password: USER_0.password, email: USER_0.email }) - .end(function (error, result) { + superagent.get(SERVER_URL + '/api/v1/profile') + .query({ access_token: token }) + .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(); + expect(result.statusCode).to.eql(200); - // stash for further use - token = result.body.token; + USER_0.id = result.body.id; - superagent.get(SERVER_URL + '/api/v1/profile') - .query({ access_token: token }) - .end(function (error, result) { - expect(result).to.be.ok(); - expect(result.statusCode).to.eql(200); - - USER_0.id = result.body.id; - - callback(); - }); + callback(); }); } ], done); } - function cleanup(done) { - database._clear(function (error) { - expect(error).to.not.be.ok(); - - server.stop(done); - }); - } - describe('get', function () { - before(setup); + before(setup2); after(cleanup); it('fails due to missing token', function (done) { @@ -563,7 +435,7 @@ describe('Clients', function () { }); describe('get tokens by client', function () { - before(setup); + before(setup2); after(cleanup); it('fails due to missing token', function (done) { @@ -616,7 +488,7 @@ describe('Clients', function () { }); describe('delete tokens by client', function () { - before(setup); + before(setup2); after(cleanup); it('fails due to missing token', function (done) { diff --git a/src/routes/test/cloudron-test.js b/src/routes/test/cloudron-test.js index 918c8766a..7feac9762 100644 --- a/src/routes/test/cloudron-test.js +++ b/src/routes/test/cloudron-test.js @@ -28,13 +28,13 @@ var USERNAME_1 = 'userTheFirst', EMAIL_1 = 'taO@zen.mac', userId_1, token_1; function setup(done) { nock.cleanAll(); config._reset(); - config.set('version', '0.5.0'); - config.setFqdn('localhost'); + config.setFqdn('example-cloudron-test.com'); - server.start(function (error) { - if (error) return done(error); - settings.setBackupConfig({ provider: 'filesystem', backupFolder: '/tmp', format: 'tgz' }, done); - }); + async.series([ + server.start.bind(server), + database._clear, + settings.setBackupConfig.bind(null, { provider: 'filesystem', backupFolder: '/tmp', format: 'tgz' }) + ], done); } function cleanup(done) { @@ -189,8 +189,6 @@ describe('Cloudron', function () { 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, {}); - config._reset(); - superagent.post(SERVER_URL + '/api/v1/cloudron/activate') .query({ setupToken: 'somesetuptoken' }) .send({ username: USERNAME, password: PASSWORD, email: EMAIL }) @@ -246,7 +244,6 @@ describe('Cloudron', function () { expect(result.body.progress).to.be.an('object'); expect(result.body.update).to.be.an('object'); expect(result.body.version).to.eql(config.version()); - expect(result.body.developerMode).to.be.a('boolean'); expect(result.body.size).to.eql(null); expect(result.body.region).to.eql(null); expect(result.body.memory).to.eql(os.totalmem()); @@ -258,7 +255,7 @@ describe('Cloudron', function () { it('succeeds (admin)', function (done) { var scope = nock(config.apiServerOrigin()) - .get('/api/v1/boxes/localhost?token=' + config.token()) + .get(`/api/v1/boxes/${config.fqdn()}?token=${config.token()}`) .reply(200, { box: { region: 'sfo', size: '1gb' }, user: { }}); superagent.get(SERVER_URL + '/api/v1/cloudron/config') @@ -272,7 +269,6 @@ describe('Cloudron', function () { expect(result.body.progress).to.be.an('object'); expect(result.body.update).to.be.an('object'); expect(result.body.version).to.eql(config.version()); - expect(result.body.developerMode).to.be.a('boolean'); expect(result.body.size).to.eql('1gb'); expect(result.body.region).to.eql('sfo'); expect(result.body.memory).to.eql(os.totalmem()); @@ -484,8 +480,6 @@ describe('Cloudron', function () { 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, {}); - config._reset(); - superagent.post(SERVER_URL + '/api/v1/cloudron/activate') .query({ setupToken: 'somesetuptoken' }) .send({ username: USERNAME, password: PASSWORD, email: EMAIL }) @@ -544,26 +538,6 @@ describe('Cloudron', function () { }); }); - it('succeeds with ticket type', function (done) { - superagent.post(SERVER_URL + '/api/v1/feedback') - .send({ type: 'ticket', subject: 'some subject', description: 'some description' }) - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(201); - done(); - }); - }); - - it('succeeds with app type', function (done) { - superagent.post(SERVER_URL + '/api/v1/feedback') - .send({ type: 'app_missing', subject: 'some subject', description: 'some description' }) - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(201); - done(); - }); - }); - it('fails without description', function (done) { superagent.post(SERVER_URL + '/api/v1/feedback') .send({ type: 'ticket', subject: 'some subject' }) @@ -594,16 +568,6 @@ describe('Cloudron', function () { }); }); - it('succeeds with feedback type', function (done) { - superagent.post(SERVER_URL + '/api/v1/feedback') - .send({ type: 'feedback', subject: 'some subject', description: 'some description' }) - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(201); - done(); - }); - }); - it('fails without subject', function (done) { superagent.post(SERVER_URL + '/api/v1/feedback') .send({ type: 'ticket', description: 'some description' }) @@ -613,6 +577,42 @@ describe('Cloudron', function () { done(); }); }); + + it('succeeds with ticket type', function (done) { + var scope1 = nock(config.apiServerOrigin()).post('/api/v1/exchangeBoxTokenWithUserToken?token=APPSTORE_TOKEN').reply(201, { userId: 'USER_ID', cloudronId: 'CLOUDRON_ID', token: 'ACCESS_TOKEN' }); + var scope2 = nock(config.apiServerOrigin()) + .filteringRequestBody(function (/* unusedBody */) { return ''; }) // strip out body + .post('/api/v1/users/USER_ID/cloudrons/CLOUDRON_ID/feedback?accessToken=ACCESS_TOKEN') + .reply(201, { }); + + superagent.post(SERVER_URL + '/api/v1/feedback') + .send({ type: 'ticket', subject: 'some subject', description: 'some description' }) + .query({ access_token: token }) + .end(function (error, result) { + expect(result.statusCode).to.equal(201); + expect(scope1.isDone()).to.be.ok(); + expect(scope2.isDone()).to.be.ok(); + done(); + }); + }); + + it('succeeds with app type', function (done) { + var scope1 = nock(config.apiServerOrigin()).post('/api/v1/exchangeBoxTokenWithUserToken?token=APPSTORE_TOKEN').reply(201, { userId: 'USER_ID', cloudronId: 'CLOUDRON_ID', token: 'ACCESS_TOKEN' }); + var scope2 = nock(config.apiServerOrigin()) + .filteringRequestBody(function (/* unusedBody */) { return ''; }) // strip out body + .post('/api/v1/users/USER_ID/cloudrons/CLOUDRON_ID/feedback?accessToken=ACCESS_TOKEN') + .reply(201, { }); + + superagent.post(SERVER_URL + '/api/v1/feedback') + .send({ type: 'app_missing', subject: 'some subject', description: 'some description' }) + .query({ access_token: token }) + .end(function (error, result) { + expect(result.statusCode).to.equal(201); + expect(scope1.isDone()).to.be.ok(); + expect(scope2.isDone()).to.be.ok(); + done(); + }); + }); }); describe('logs', function () { @@ -624,8 +624,6 @@ describe('Cloudron', function () { 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, {}); - config._reset(); - superagent.post(SERVER_URL + '/api/v1/cloudron/activate') .query({ setupToken: 'somesetuptoken' }) .send({ username: USERNAME, password: PASSWORD, email: EMAIL }) diff --git a/src/routes/test/developer-test.js b/src/routes/test/developer-test.js index 42d25b8e9..bcdea6669 100644 --- a/src/routes/test/developer-test.js +++ b/src/routes/test/developer-test.js @@ -22,7 +22,13 @@ var token = null; // authentication token var server; function setup(done) { - server.start(done); + config._reset(); + config.setFqdn('example-developer-test.com'); + + async.series([ + server.start.bind(server), + database._clear + ], done); } function cleanup(done) { @@ -34,200 +40,10 @@ function cleanup(done) { } describe('Developer API', function () { - describe('isEnabled', function () { - before(function (done) { - async.series([ - setup, - - function (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(scope1.isDone()).to.be.ok(); - expect(scope2.isDone()).to.be.ok(); - - // stash token for further use - token = result.body.token; - - callback(); - }); - }, - ], done); - }); - - after(cleanup); - - it('fails without token', function (done) { - settings.setDeveloperMode(true, function (error) { - expect(error).to.be(null); - - superagent.get(SERVER_URL + '/api/v1/developer') - .end(function (error, result) { - expect(result.statusCode).to.equal(401); - done(); - }); - }); - }); - - it('succeeds (enabled)', function (done) { - settings.setDeveloperMode(true, function (error) { - expect(error).to.be(null); - - superagent.get(SERVER_URL + '/api/v1/developer') - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(200); - done(); - }); - }); - }); - - it('succeeds (not enabled)', function (done) { - settings.setDeveloperMode(false, function (error) { - expect(error).to.be(null); - - superagent.get(SERVER_URL + '/api/v1/developer') - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(412); - done(); - }); - }); - }); - }); - - describe('setEnabled', function () { - before(function (done) { - async.series([ - setup, - - function (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(scope1.isDone()).to.be.ok(); - expect(scope2.isDone()).to.be.ok(); - - // stash token for further use - token = result.body.token; - - callback(); - }); - }, - ], done); - }); - - after(cleanup); - - it('fails without token', function (done) { - superagent.post(SERVER_URL + '/api/v1/developer') - .send({ enabled: true }) - .end(function (error, result) { - expect(result.statusCode).to.equal(401); - done(); - }); - }); - - it('fails due to missing password', function (done) { - superagent.post(SERVER_URL + '/api/v1/developer') - .query({ access_token: token }) - .send({ enabled: true }) - .end(function (error, result) { - expect(result.statusCode).to.equal(400); - done(); - }); - }); - - it('fails due to empty password', function (done) { - superagent.post(SERVER_URL + '/api/v1/developer') - .query({ access_token: token }) - .send({ password: '', enabled: true }) - .end(function (error, result) { - expect(result.statusCode).to.equal(403); - done(); - }); - }); - - it('fails due to wrong password', function (done) { - superagent.post(SERVER_URL + '/api/v1/developer') - .query({ access_token: token }) - .send({ password: PASSWORD.toUpperCase(), enabled: true }) - .end(function (error, result) { - expect(result.statusCode).to.equal(403); - done(); - }); - }); - - it('fails due to missing enabled property', function (done) { - superagent.post(SERVER_URL + '/api/v1/developer') - .query({ access_token: token }) - .send({ password: PASSWORD }) - .end(function (error, result) { - expect(result.statusCode).to.equal(400); - done(); - }); - }); - - it('fails due to wrong enabled property type', function (done) { - superagent.post(SERVER_URL + '/api/v1/developer') - .query({ access_token: token }) - .send({ password: PASSWORD, enabled: 'true' }) - .end(function (error, result) { - expect(result.statusCode).to.equal(400); - done(); - }); - }); - - it('succeeds enabling', function (done) { - superagent.post(SERVER_URL + '/api/v1/developer') - .query({ access_token: token }) - .send({ password: PASSWORD, enabled: true }) - .end(function (error, result) { - expect(result.statusCode).to.equal(200); - - superagent.get(SERVER_URL + '/api/v1/developer') - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(200); - done(); - }); - }); - }); - - it('succeeds disabling', function (done) { - superagent.post(SERVER_URL + '/api/v1/developer') - .query({ access_token: token }) - .send({ password: PASSWORD, enabled: false }) - .end(function (error, result) { - expect(result.statusCode).to.equal(200); - - superagent.get(SERVER_URL + '/api/v1/developer') - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(412); - done(); - }); - }); - }); - }); - describe('login', function () { before(function (done) { async.series([ setup, - - settings.setDeveloperMode.bind(null, true), - function (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, {}); @@ -373,9 +189,6 @@ describe('Developer API', function () { before(function (done) { async.series([ setup, - - settings.setDeveloperMode.bind(null, true), - function (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, {}); diff --git a/src/routes/test/domains-test.js b/src/routes/test/domains-test.js new file mode 100644 index 000000000..1d608953d --- /dev/null +++ b/src/routes/test/domains-test.js @@ -0,0 +1,240 @@ +'use strict'; + +/* global it:false */ +/* global describe:false */ +/* global before:false */ +/* global after:false */ + +var async = require('async'), + config = require('../../config.js'), + database = require('../../database.js'), + expect = require('expect.js'), + nock = require('nock'), + superagent = require('superagent'), + server = require('../../server.js'); + +var SERVER_URL = 'http://localhost:' + config.get('port'); + +var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com'; +var token = null; + +var DOMAIN_0 = { + domain: 'cloudron.com', + zoneName: 'cloudron.com', + config: { provider: 'noop' } +}; + +var DOMAIN_1 = { + domain: 'foobar.com', + config: { provider: 'noop' } +}; + +describe('Domains API', function () { + this.timeout(10000); + + before(function (done) { + config._reset(); + config.set('provider', 'digitalocean'); + config.setFqdn('example-domains-test.com'); + + async.series([ + server.start.bind(null), + database._clear.bind(null), + + 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.equal(201); + + // stash token for further use + token = result.body.token; + + callback(); + }); + }, + ], done); + }); + + after(function (done) { + async.series([ + database._clear.bind(null), + server.stop.bind(null) + ], done); + }); + + describe('add', function () { + it('fails with missing domain', function (done) { + superagent.post(SERVER_URL + '/api/v1/domains') + .query({ access_token: token }) + .send({}) + .end(function (error, result) { + expect(result.statusCode).to.equal(400); + + done(); + }); + }); + + it('fails with invalid domain', function (done) { + superagent.post(SERVER_URL + '/api/v1/domains') + .query({ access_token: token }) + .send({ domain: 'abc' }) + .end(function (error, result) { + expect(result.statusCode).to.equal(400); + + done(); + }); + }); + + it('fails with unknown provider', function (done) { + var scope = nock(config.apiServerOrigin()).get('/api/v1/helper/public_ip').reply(200, { ip: '127.0.0.1' }); + + superagent.post(SERVER_URL + '/api/v1/domains') + .query({ access_token: token }) + .send({ domain: 'cloudron.com', config: { provider: 'doesnotexist' }}) + .end(function (error, result) { + expect(result.statusCode).to.equal(400); + expect(scope.isDone()).to.be.ok(); + + done(); + }); + }); + + it('succeeds', function (done) { + var scope = nock(config.apiServerOrigin()).get('/api/v1/helper/public_ip').reply(200, { ip: '127.0.0.1' }); + + superagent.post(SERVER_URL + '/api/v1/domains') + .query({ access_token: token }) + .send(DOMAIN_0) + .end(function (error, result) { + expect(result.statusCode).to.equal(201); + expect(scope.isDone()).to.be.ok(); + + done(); + }); + }); + + it('succeeds for second domain without zoneName', function (done) { + var scope = nock(config.apiServerOrigin()).get('/api/v1/helper/public_ip').reply(200, { ip: '127.0.0.1' }); + + superagent.post(SERVER_URL + '/api/v1/domains') + .query({ access_token: token }) + .send(DOMAIN_1) + .end(function (error, result) { + expect(result.statusCode).to.equal(201); + expect(scope.isDone()).to.be.ok(); + + done(); + }); + }); + + it('fails for already added domain', function (done) { + var scope = nock(config.apiServerOrigin()).get('/api/v1/helper/public_ip').reply(200, { ip: '127.0.0.1' }); + + superagent.post(SERVER_URL + '/api/v1/domains') + .query({ access_token: token }) + .send(DOMAIN_0) + .end(function (error, result) { + expect(result.statusCode).to.equal(409); + expect(scope.isDone()).to.be.ok(); + + done(); + }); + }); + }); + + describe('list', function () { + it('succeeds', function (done) { + superagent.get(SERVER_URL + '/api/v1/domains') + .query({ access_token: token }) + .end(function (error, result) { + expect(result.statusCode).to.equal(200); + expect(result.body.domains).to.be.an(Array); + // includes currently the implicitly added config.fqdn() + expect(result.body.domains.length).to.equal(3); + + expect(result.body.domains[0].domain).to.equal(DOMAIN_0.domain); + expect(result.body.domains[1].domain).to.equal(config.fqdn()); + expect(result.body.domains[2].domain).to.equal(DOMAIN_1.domain); + + done(); + }); + }); + }); + + describe('get', function () { + it('fails for non-existing domain', function (done) { + superagent.get(SERVER_URL + '/api/v1/domains/' + DOMAIN_0.domain + DOMAIN_0.domain) + .query({ access_token: token }) + .end(function (error, result) { + expect(result.statusCode).to.equal(404); + + done(); + }); + }); + + it('succeeds', function (done) { + superagent.get(SERVER_URL + '/api/v1/domains/' + DOMAIN_0.domain) + .query({ access_token: token }) + .end(function (error, result) { + expect(result.statusCode).to.equal(200); + expect(result.body.domain).to.equal(DOMAIN_0.domain); + + done(); + }); + }); + }); + + describe('delete', function () { + it('fails without password', function (done) { + superagent.delete(SERVER_URL + '/api/v1/domains/' + DOMAIN_0.domain + DOMAIN_0.domain) + .query({ access_token: token }) + .end(function (error, result) { + expect(result.statusCode).to.equal(400); + + done(); + }); + }); + + it('fails with wrong password', function (done) { + superagent.delete(SERVER_URL + '/api/v1/domains/' + DOMAIN_0.domain + DOMAIN_0.domain) + .query({ access_token: token }) + .send({ password: PASSWORD + PASSWORD }) + .end(function (error, result) { + expect(result.statusCode).to.equal(403); + + done(); + }); + }); + + it('fails for non-existing domain', function (done) { + superagent.delete(SERVER_URL + '/api/v1/domains/' + DOMAIN_0.domain + DOMAIN_0.domain) + .query({ access_token: token }) + .send({ password: PASSWORD }) + .end(function (error, result) { + expect(result.statusCode).to.equal(404); + + done(); + }); + }); + + it('succeeds', function (done) { + superagent.delete(SERVER_URL + '/api/v1/domains/' + DOMAIN_0.domain) + .query({ access_token: token }) + .send({ password: PASSWORD }) + .end(function (error, result) { + expect(result.statusCode).to.equal(204); + + superagent.get(SERVER_URL + '/api/v1/domains/' + DOMAIN_0.domain) + .query({ access_token: token }) + .end(function (error, result) { + expect(result.statusCode).to.equal(404); + + done(); + }); + }); + }); + }); +}); diff --git a/src/routes/test/eventlog-test.js b/src/routes/test/eventlog-test.js index 700ed5b0f..638314ee4 100644 --- a/src/routes/test/eventlog-test.js +++ b/src/routes/test/eventlog-test.js @@ -10,7 +10,6 @@ var async = require('async'), config = require('../../config.js'), database = require('../../database.js'), expect = require('expect.js'), - nock = require('nock'), superagent = require('superagent'), server = require('../../server.js'), tokendb = require('../../tokendb.js'); @@ -23,7 +22,9 @@ var token = null; var USER_1_ID = null, token_1; function setup(done) { - config.setVersion('1.2.3'); + config._reset(); + config.set('provider', 'notcaas'); + config.setFqdn('example-eventlog-test.com'); async.series([ server.start.bind(server), @@ -31,17 +32,12 @@ function setup(done) { database._clear, 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; diff --git a/src/routes/test/groups-test.js b/src/routes/test/groups-test.js index 677a41964..575b70df5 100644 --- a/src/routes/test/groups-test.js +++ b/src/routes/test/groups-test.js @@ -28,6 +28,10 @@ var groupObject; var server; function setup(done) { + config._reset(); + config.set('provider', 'caas'); + config.setFqdn('example-groups-test.com'); + async.series([ server.start.bind(server), @@ -223,7 +227,7 @@ describe('Groups API', function () { var group0Object, group1Object; before(function (done) { groups.create('group0', function (e, r) { - group0Object = r; + group0Object = r; groups.create('group1', function (e, r) { group1Object = r; done(); diff --git a/src/routes/test/oauth2-test.js b/src/routes/test/oauth2-test.js index 5a9f47401..e6bfb109b 100644 --- a/src/routes/test/oauth2-test.js +++ b/src/routes/test/oauth2-test.js @@ -154,6 +154,7 @@ describe('OAuth2', function () { appStoreId: '', manifest: { version: '0.1.0', addons: { } }, location: 'test', + domain: 'example.com', portBindings: {}, accessRestriction: null, memoryLimit: 0, @@ -165,6 +166,7 @@ describe('OAuth2', function () { appStoreId: '', manifest: { version: '0.1.0', addons: { } }, location: 'test1', + domain: 'example.com', portBindings: {}, accessRestriction: { users: [ 'foobar' ] }, memoryLimit: 0, @@ -176,6 +178,7 @@ describe('OAuth2', function () { appStoreId: '', manifest: { version: '0.1.0', addons: { } }, location: 'test2', + domain: 'example.com', portBindings: {}, accessRestriction: { users: [ USER_0.id ] }, memoryLimit: 0, @@ -187,6 +190,7 @@ describe('OAuth2', function () { appStoreId: '', manifest: { version: '0.1.0', addons: { } }, location: 'test3', + domain: 'example.com', portBindings: {}, accessRestriction: { groups: [ 'someothergroup', 'admin', 'anothergroup' ] }, memoryLimit: 0, @@ -290,6 +294,9 @@ describe('OAuth2', function () { }; function setup(done) { + config._reset(); + config.setFqdn(APP_0.domain); + async.series([ server.start, database._clear, @@ -302,10 +309,10 @@ describe('OAuth2', function () { clientdb.add.bind(null, CLIENT_6.id, CLIENT_6.appId, CLIENT_6.type, CLIENT_6.clientSecret, CLIENT_6.redirectURI, CLIENT_6.scope), clientdb.add.bind(null, CLIENT_7.id, CLIENT_7.appId, CLIENT_7.type, CLIENT_7.clientSecret, CLIENT_7.redirectURI, CLIENT_7.scope), clientdb.add.bind(null, CLIENT_9.id, CLIENT_9.appId, CLIENT_9.type, CLIENT_9.clientSecret, CLIENT_9.redirectURI, CLIENT_9.scope), - appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0), - appdb.add.bind(null, APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, APP_1.portBindings, APP_1), - appdb.add.bind(null, APP_2.id, APP_2.appStoreId, APP_2.manifest, APP_2.location, APP_2.portBindings, APP_2), - appdb.add.bind(null, APP_3.id, APP_3.appStoreId, APP_3.manifest, APP_3.location, APP_3.portBindings, APP_3), + appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, APP_0.portBindings, APP_0), + appdb.add.bind(null, APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, APP_1.domain, APP_1.portBindings, APP_1), + appdb.add.bind(null, APP_2.id, APP_2.appStoreId, APP_2.manifest, APP_2.location, APP_2.domain, APP_2.portBindings, APP_2), + appdb.add.bind(null, APP_3.id, APP_3.appStoreId, APP_3.manifest, APP_3.location, APP_3.domain, APP_3.portBindings, APP_3), function (callback) { user.create(USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, null /* source */, function (error, userObject) { expect(error).to.not.be.ok(); diff --git a/src/routes/test/profile-test.js b/src/routes/test/profile-test.js index 36f6d102b..2fac3d8e8 100644 --- a/src/routes/test/profile-test.js +++ b/src/routes/test/profile-test.js @@ -26,6 +26,9 @@ describe('Profile API', function () { var token_0; function setup(done) { + config._reset(); + config.setFqdn('example-profile-test.com'); + server.start(function (error) { expect(!error).to.be.ok(); diff --git a/src/routes/test/server-test.js b/src/routes/test/server-test.js index 297dc3073..fd4e8249a 100644 --- a/src/routes/test/server-test.js +++ b/src/routes/test/server-test.js @@ -22,6 +22,9 @@ var token = null; var server; function setup(done) { + config._reset(); + config.setFqdn('example-server-test.com'); + config.set('provider', 'caas'); config.setVersion('1.2.3'); async.series([ @@ -34,19 +37,19 @@ function setup(done) { 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(); + .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; + // stash token for further use + token = result.body.token; - callback(); - }); + callback(); + }); } ], done); } @@ -67,22 +70,24 @@ describe('REST API', function () { superagent.post(SERVER_URL + '/api/v1/users') .query({ access_token: token }) .set('content-type', 'application/json') - .send("some invalid non-strict json") - .end(function (error, result) { - expect(result.statusCode).to.equal(400); - expect(result.body.message).to.be('Bad JSON'); - done(); - }); + .send('some invalid non-strict json') + .end(function (error, result) { + expect(result.statusCode).to.equal(400); + expect(result.body.message).to.be('Failed to parse body'); + + done(); + }); }); it('does not crash with invalid string', function (done) { superagent.post(SERVER_URL + '/api/v1/users') .query({ access_token: token }) .set('content-type', 'application/x-www-form-urlencoded') - .send("some string") - .end(function (error, result) { - expect(result.statusCode).to.equal(400); - done(); - }); + .send('some string') + .end(function (error, result) { + expect(result.statusCode).to.equal(400); + + done(); + }); }); }); diff --git a/src/routes/test/settings-test.js b/src/routes/test/settings-test.js index a6357d2e7..2def15b40 100644 --- a/src/routes/test/settings-test.js +++ b/src/routes/test/settings-test.js @@ -1,40 +1,39 @@ +'use strict'; + /* global it:false */ /* global describe:false */ /* global before:false */ /* global after:false */ -'use strict'; - -var appdb = require('../../appdb.js'), - async = require('async'), +var async = require('async'), child_process = require('child_process'), cloudron = require('../../cloudron.js'), config = require('../../config.js'), constants = require('../../constants.js'), database = require('../../database.js'), expect = require('expect.js'), + fs = require('fs'), + nock = require('nock'), path = require('path'), paths = require('../../paths.js'), server = require('../../server.js'), settings = require('../../settings.js'), settingsdb = require('../../settingsdb.js'), - superagent = require('superagent'), - fs = require('fs'), - nock = require('nock'); + superagent = require('superagent'); var SERVER_URL = 'http://localhost:' + config.get('port'); var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com'; var token = null; -var server; function setup(done) { - config.setFqdn('foobar.com'); + config._reset(); + config.setFqdn('example-settings-test.com'); + config.set('provider', 'caas'); async.series([ - server.start.bind(server), - - database._clear, + server.start.bind(null), + database._clear.bind(null), function createAdmin(callback) { var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {}); @@ -54,11 +53,6 @@ function setup(done) { callback(); }); - }, - - function addApp(callback) { - var manifest = { version: '0.0.1', manifestVersion: 1, dockerImage: 'foo', healthCheckPath: '/', httpPort: 3, title: 'ok' }; - appdb.add('appid', 'appStoreId', manifest, 'location', [ ] /* portBindings */, { }, callback); } ], done); } @@ -72,8 +66,6 @@ function cleanup(done) { } describe('Settings API', function () { - this.timeout(10000); - before(setup); after(cleanup); @@ -234,47 +226,6 @@ describe('Settings API', function () { }); }); - describe('dns_config', function () { - it('get dns_config fails', function (done) { - superagent.get(SERVER_URL + '/api/v1/settings/dns_config') - .query({ access_token: token }) - .end(function (err, res) { - expect(res.statusCode).to.equal(200); - expect(res.body).to.eql({ provider: 'manual' }); - done(); - }); - }); - - it('cannot set without data', function (done) { - superagent.post(SERVER_URL + '/api/v1/settings/dns_config') - .query({ access_token: token }) - .end(function (err, res) { - expect(res.statusCode).to.equal(400); - done(); - }); - }); - - it('set succeeds', function (done) { - superagent.post(SERVER_URL + '/api/v1/settings/dns_config') - .query({ access_token: token }) - .send({ provider: 'route53', accessKeyId: 'accessKey', secretAccessKey: 'secretAccessKey' }) - .end(function (err, res) { - expect(res.statusCode).to.equal(200); - done(); - }); - }); - - it('get succeeds', function (done) { - superagent.get(SERVER_URL + '/api/v1/settings/dns_config') - .query({ access_token: token }) - .end(function (err, res) { - expect(res.statusCode).to.equal(200); - expect(res.body).to.eql({ provider: 'route53', accessKeyId: 'accessKey', secretAccessKey: 'secretAccessKey', region: 'us-east-1', endpoint: null }); - done(); - }); - }); - }); - describe('mail_config', function () { it('get mail_config succeeds', function (done) { superagent.get(SERVER_URL + '/api/v1/settings/mail_config') @@ -367,16 +318,16 @@ describe('Settings API', function () { }); }); - describe('Certificates API', function () { - var validCert0, validKey0, // foobar.com - validCert1, validKey1; // *.foobar.com + xdescribe('Certificates API', function () { + var validCert0, validKey0, // example.com + validCert1, validKey1; // *.example.com before(function () { - 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'); + child_process.execSync('openssl req -subj "/CN=example.com/O=My Company Name LTD./C=US" -new -newkey rsa:2048 -days 365 -nodes -x509 -keyout /tmp/server.key -out /tmp/server.crt'); validKey0 = fs.readFileSync('/tmp/server.key', 'utf8'); validCert0 = fs.readFileSync('/tmp/server.crt', 'utf8'); - 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'); + child_process.execSync('openssl req -subj "/CN=*.example.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'); }); diff --git a/src/routes/test/ssh-test.js b/src/routes/test/ssh-test.js index ebf33fe34..3e903841d 100644 --- a/src/routes/test/ssh-test.js +++ b/src/routes/test/ssh-test.js @@ -28,7 +28,8 @@ var token = null; var server; function setup(done) { - config.setFqdn('foobar.com'); + config._reset(); + config.setFqdn('example-ssh-test.com'); async.series([ server.start.bind(server), diff --git a/src/routes/test/sysadmin-test.js b/src/routes/test/sysadmin-test.js index d18561928..7c703cfbe 100644 --- a/src/routes/test/sysadmin-test.js +++ b/src/routes/test/sysadmin-test.js @@ -30,6 +30,9 @@ var SERVER_URL = 'http://localhost:' + config.get('port'); var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com'; function setup(done) { + config._reset(); + config.setFqdn('example-sysadmin-test.com'); + config.set('provider', 'caas'); config.setVersion('1.2.3'); async.series([ @@ -56,7 +59,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', [ ] /* portBindings */, { }, callback); + appdb.add('appid', 'appStoreId', manifest, 'location', config.fqdn(), [ ] /* portBindings */, { }, callback); }, function createSettings(callback) { @@ -65,7 +68,7 @@ function setup(done) { s3._mockInject(MockS3); safe.fs.mkdirSync('/tmp/box-sysadmin-test'); - settingsdb.set(settings.BACKUP_CONFIG_KEY, JSON.stringify({ provider: 'caas', token: 'BACKUP_TOKEN', key: 'key', prefix: 'boxid', format: 'tgz'}), callback); + settingsdb.set(settings.BACKUP_CONFIG_KEY, JSON.stringify({ provider: 'caas', token: 'BACKUP_TOKEN', fqdn: config.fqdn(), key: 'key', prefix: 'boxid', format: 'tgz'}), callback); } ], done); } diff --git a/src/routes/test/user-test.js b/src/routes/test/user-test.js index 096b555dc..0397ee0f9 100644 --- a/src/routes/test/user-test.js +++ b/src/routes/test/user-test.js @@ -26,6 +26,9 @@ var USERNAME_3 = 'ut', EMAIL_3 = 'user3@FOO.bar'; var groupObject; function setup(done) { + config._reset(); + config.setFqdn('example-user-test.com'); + server.start(function (error) { expect(!error).to.be.ok(); diff --git a/src/scripts/restart.sh b/src/scripts/restart.sh new file mode 100755 index 000000000..3a5caad06 --- /dev/null +++ b/src/scripts/restart.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +set -eu -o pipefail + +readonly INFRA_VERSION_FILE=/home/yellowtent/platformdata/INFRA_VERSION + +if [[ ${EUID} -ne 0 ]]; then + echo "This script should be run as root." > /dev/stderr + exit 1 +fi + +if [[ $# == 1 && "$1" == "--check" ]]; then + echo "OK" + exit 0 +fi + +if [[ "${BOX_ENV}" == "cloudron" ]]; then + systemctl restart box +fi + diff --git a/src/server.js b/src/server.js index be2fcb53f..45867e192 100644 --- a/src/server.js +++ b/src/server.js @@ -98,16 +98,16 @@ function initializeExpressSync() { var csrf = routes.oauth2.csrf; // public routes - router.post('/api/v1/cloudron/activate', routes.cloudron.setupTokenAuth, routes.cloudron.activate); router.post('/api/v1/cloudron/dns_setup', routes.cloudron.providerTokenAuth, routes.cloudron.dnsSetup); // only available until no-domain + router.post('/api/v1/cloudron/activate', routes.cloudron.setupTokenAuth, routes.cloudron.activate); + router.post('/api/v1/cloudron/restore', routes.cloudron.restore); // only available until activated + router.get ('/api/v1/cloudron/progress', routes.cloudron.getProgress); router.get ('/api/v1/cloudron/status', routes.cloudron.getStatus); router.get ('/api/v1/cloudron/avatar', routes.settings.getCloudronAvatar); // this is a public alias for /api/v1/settings/cloudron_avatar // developer routes - router.post('/api/v1/developer', developerScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.developer.setEnabled); - router.get ('/api/v1/developer', developerScope, routes.developer.enabled, routes.developer.status); - router.post('/api/v1/developer/login', routes.developer.enabled, routes.developer.login); + router.post('/api/v1/developer/login', routes.developer.login); // cloudron routes router.get ('/api/v1/cloudron/config', cloudronScope, routes.cloudron.getConfig); @@ -166,12 +166,12 @@ function initializeExpressSync() { router.get ('/api/v1/oauth/dialog/authorize', routes.oauth2.authorization); router.post('/api/v1/oauth/token', routes.oauth2.token); router.get ('/api/v1/oauth/clients', settingsScope, routes.clients.getAll); - router.post('/api/v1/oauth/clients', routes.developer.enabled, settingsScope, routes.clients.add); - router.get ('/api/v1/oauth/clients/:clientId', routes.developer.enabled, settingsScope, routes.clients.get); - router.post('/api/v1/oauth/clients/:clientId', routes.developer.enabled, settingsScope, routes.clients.add); - router.del ('/api/v1/oauth/clients/:clientId', routes.developer.enabled, settingsScope, routes.clients.del); + router.post('/api/v1/oauth/clients', settingsScope, routes.clients.add); + router.get ('/api/v1/oauth/clients/:clientId', settingsScope, routes.clients.get); + router.post('/api/v1/oauth/clients/:clientId', settingsScope, routes.clients.add); + router.del ('/api/v1/oauth/clients/:clientId', settingsScope, routes.clients.del); router.get ('/api/v1/oauth/clients/:clientId/tokens', settingsScope, routes.clients.getClientTokens); - router.post('/api/v1/oauth/clients/:clientId/tokens', routes.developer.enabled, settingsScope, routes.clients.addClientToken); + router.post('/api/v1/oauth/clients/:clientId/tokens', settingsScope, routes.clients.addClientToken); router.del ('/api/v1/oauth/clients/:clientId/tokens', settingsScope, routes.clients.delClientTokens); router.del ('/api/v1/oauth/clients/:clientId/tokens/:tokenId', settingsScope, routes.clients.delToken); @@ -191,7 +191,7 @@ function initializeExpressSync() { router.post('/api/v1/apps/:id/start', appsScope, routes.user.requireAdmin, routes.apps.startApp); router.get ('/api/v1/apps/:id/logstream', appsScope, routes.user.requireAdmin, routes.apps.getLogStream); router.get ('/api/v1/apps/:id/logs', appsScope, routes.user.requireAdmin, routes.apps.getLogs); - router.get ('/api/v1/apps/:id/exec', routes.developer.enabled, appsScope, routes.user.requireAdmin, routes.apps.exec); + router.get ('/api/v1/apps/:id/exec', appsScope, routes.user.requireAdmin, routes.apps.exec); // websocket cannot do bearer authentication router.get ('/api/v1/apps/:id/execws', routes.oauth2.websocketAuth.bind(null, [ clients.SCOPE_APPS ]), routes.user.requireAdmin, routes.apps.execWebSocket); router.post('/api/v1/apps/:id/clone', appsScope, routes.user.requireAdmin, routes.apps.cloneApp); @@ -206,11 +206,8 @@ function initializeExpressSync() { router.get ('/api/v1/settings/cloudron_avatar', settingsScope, routes.user.requireAdmin, routes.settings.getCloudronAvatar); router.post('/api/v1/settings/cloudron_avatar', settingsScope, routes.user.requireAdmin, multipart, routes.settings.setCloudronAvatar); router.get ('/api/v1/settings/email_status', settingsScope, routes.user.requireAdmin, routes.settings.getEmailStatus); - router.get ('/api/v1/settings/dns_config', settingsScope, routes.user.requireAdmin, routes.settings.getDnsConfig); - router.post('/api/v1/settings/dns_config', settingsScope, routes.user.requireAdmin, routes.settings.setDnsConfig); router.get ('/api/v1/settings/backup_config', settingsScope, routes.user.requireAdmin, routes.settings.getBackupConfig); router.post('/api/v1/settings/backup_config', settingsScope, routes.user.requireAdmin, routes.settings.setBackupConfig); - router.post('/api/v1/settings/certificate', settingsScope, routes.user.requireAdmin, routes.settings.setFallbackCertificate); router.post('/api/v1/settings/admin_certificate', settingsScope, routes.user.requireAdmin, routes.settings.setAdminCertificate); router.get ('/api/v1/settings/time_zone', settingsScope, routes.user.requireAdmin, routes.settings.getTimeZone); @@ -235,6 +232,13 @@ function initializeExpressSync() { router.get ('/api/v1/backups', settingsScope, routes.user.requireAdmin, routes.backups.get); router.post('/api/v1/backups', settingsScope, routes.user.requireAdmin, routes.backups.create); + // domain routes + router.post('/api/v1/domains', settingsScope, routes.user.requireAdmin, routes.domains.add); + router.get ('/api/v1/domains', settingsScope, routes.user.requireAdmin, routes.domains.getAll); + router.get ('/api/v1/domains/:domain', settingsScope, routes.user.requireAdmin, routes.domains.get); + router.put ('/api/v1/domains/:domain', settingsScope, routes.user.requireAdmin, routes.domains.update); + router.del ('/api/v1/domains/:domain', settingsScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.domains.del); + // disable server socket "idle" timeout. we use the timeout middleware to handle timeouts on a route level // we rely on nginx for timeouts on the TCP level (see client_header_timeout) httpServer.setTimeout(0); diff --git a/src/settings.js b/src/settings.js index e3f054bf9..d9093046c 100644 --- a/src/settings.js +++ b/src/settings.js @@ -18,12 +18,6 @@ exports = module.exports = { getCloudronAvatar: getCloudronAvatar, setCloudronAvatar: setCloudronAvatar, - getDeveloperMode: getDeveloperMode, - setDeveloperMode: setDeveloperMode, - - getDnsConfig: getDnsConfig, - setDnsConfig: setDnsConfig, - getDynamicDnsConfig: getDynamicDnsConfig, setDynamicDnsConfig: setDynamicDnsConfig, @@ -54,13 +48,11 @@ exports = module.exports = { getAll: getAll, // booleans. if you add an entry here, be sure to fix getAll - DEVELOPER_MODE_KEY: 'developer_mode', DYNAMIC_DNS_KEY: 'dynamic_dns', MAIL_FROM_VALIDATION_KEY: 'mail_from_validation', EMAIL_DIGEST: 'email_digest', // json. if you add an entry here, be sure to fix getAll - DNS_CONFIG_KEY: 'dns_config', BACKUP_CONFIG_KEY: 'backup_config', TLS_CONFIG_KEY: 'tls_config', UPDATE_CONFIG_KEY: 'update_config', @@ -85,7 +77,6 @@ var assert = require('assert'), CronJob = require('cron').CronJob, DatabaseError = require('./databaseerror.js'), debug = require('debug')('box:settings'), - cloudron = require('./cloudron.js'), moment = require('moment-timezone'), paths = require('./paths.js'), platform = require('./platform.js'), @@ -93,10 +84,7 @@ var assert = require('assert'), EmailError = email.EmailError, safe = require('safetydance'), settingsdb = require('./settingsdb.js'), - subdomains = require('./subdomains.js'), - SubdomainError = subdomains.SubdomainError, superagent = require('superagent'), - sysinfo = require('./sysinfo.js'), util = require('util'), _ = require('underscore'); @@ -105,9 +93,7 @@ var gDefaults = (function () { result[exports.AUTOUPDATE_PATTERN_KEY] = '00 00 1,3,5,23 * * *'; result[exports.TIME_ZONE_KEY] = 'America/Los_Angeles'; result[exports.CLOUDRON_NAME_KEY] = 'Cloudron'; - result[exports.DEVELOPER_MODE_KEY] = true; result[exports.DYNAMIC_DNS_KEY] = false; - result[exports.DNS_CONFIG_KEY] = { provider: 'manual' }; result[exports.BACKUP_CONFIG_KEY] = { provider: 'filesystem', key: '', @@ -273,72 +259,6 @@ function setCloudronAvatar(avatar, callback) { return callback(null); } -function getDeveloperMode(callback) { - assert.strictEqual(typeof callback, 'function'); - - settingsdb.get(exports.DEVELOPER_MODE_KEY, function (error, enabled) { - if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.DEVELOPER_MODE_KEY]); - if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error)); - - callback(null, !!enabled); // settingsdb holds string values only - }); -} - -function setDeveloperMode(enabled, callback) { - assert.strictEqual(typeof enabled, 'boolean'); - assert.strictEqual(typeof callback, 'function'); - - // settingsdb takes string values only - settingsdb.set(exports.DEVELOPER_MODE_KEY, enabled ? 'enabled' : '', function (error) { - if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error)); - - exports.events.emit(exports.DEVELOPER_MODE_KEY, enabled); - - return callback(null); - }); -} - -function getDnsConfig(callback) { - assert.strictEqual(typeof callback, 'function'); - - settingsdb.get(exports.DNS_CONFIG_KEY, function (error, value) { - if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.DNS_CONFIG_KEY]); - if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error)); - - callback(null, JSON.parse(value)); - }); -} - -function setDnsConfig(dnsConfig, domain, zoneName, callback) { - assert.strictEqual(typeof dnsConfig, 'object'); - assert.strictEqual(typeof domain, 'string'); - assert.strictEqual(typeof zoneName, 'string'); - assert.strictEqual(typeof callback, 'function'); - - sysinfo.getPublicIp(function (error, ip) { - if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, 'Error getting IP:' + error.message)); - - subdomains.verifyDnsConfig(dnsConfig, domain, zoneName, ip, function (error, result) { - if (error && error.reason === SubdomainError.ACCESS_DENIED) return callback(new SettingsError(SettingsError.BAD_FIELD, 'Error adding A record. Access denied')); - if (error && error.reason === SubdomainError.NOT_FOUND) return callback(new SettingsError(SettingsError.BAD_FIELD, 'Zone not found')); - if (error && error.reason === SubdomainError.EXTERNAL_ERROR) return callback(new SettingsError(SettingsError.BAD_FIELD, 'Error adding A record:' + error.message)); - if (error && error.reason === SubdomainError.BAD_FIELD) return callback(new SettingsError(SettingsError.BAD_FIELD, error.message)); - if (error && error.reason === SubdomainError.INVALID_PROVIDER) return callback(new SettingsError(SettingsError.BAD_FIELD, error.message)); - if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error)); - - settingsdb.set(exports.DNS_CONFIG_KEY, JSON.stringify(result), function (error) { - if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error)); - - exports.events.emit(exports.DNS_CONFIG_KEY, dnsConfig); - - cloudron.configureWebadmin(NOOP_CALLBACK); // do not block - - callback(null); - }); - }); - }); -} - function getDynamicDnsConfig(callback) { assert.strictEqual(typeof callback, 'function'); @@ -635,12 +555,11 @@ function getAll(callback) { settings.forEach(function (setting) { result[setting.name] = setting.value; }); // convert booleans - result[exports.DEVELOPER_MODE_KEY] = !!result[exports.DEVELOPER_MODE_KEY]; result[exports.DYNAMIC_DNS_KEY] = !!result[exports.DYNAMIC_DNS_KEY]; result[exports.MAIL_FROM_VALIDATION_KEY] = !!result[exports.MAIL_FROM_VALIDATION_KEY]; // convert JSON objects - [exports.DNS_CONFIG_KEY, exports.TLS_CONFIG_KEY, exports.BACKUP_CONFIG_KEY, exports.MAIL_CONFIG_KEY, + [exports.TLS_CONFIG_KEY, exports.BACKUP_CONFIG_KEY, exports.MAIL_CONFIG_KEY, exports.UPDATE_CONFIG_KEY, exports.APPSTORE_CONFIG_KEY, exports.MAIL_RELAY_KEY, exports.CATCH_ALL_ADDRESS_KEY].forEach(function (key) { result[key] = typeof result[key] === 'object' ? result[key] : safe.JSON.parse(result[key]); }); diff --git a/src/storage/s3.js b/src/storage/s3.js index 2a3a59edd..3c102dcd4 100644 --- a/src/storage/s3.js +++ b/src/storage/s3.js @@ -59,7 +59,7 @@ function getCaasConfig(apiConfig, callback) { debug('getCaasCredentials: getting new credentials'); - var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/awscredentials'; + var url = config.apiServerOrigin() + '/api/v1/boxes/' + apiConfig.fqdn + '/awscredentials'; superagent.post(url).query({ token: apiConfig.token }).timeout(30 * 1000).end(function (error, result) { if (error && !error.response) return callback(error); if (result.statusCode !== 201) return callback(new Error(result.text)); @@ -179,8 +179,8 @@ function download(apiConfig, backupFilePath, callback) { if (error.code === 'NoSuchKey' || error.code === 'ENOENT') { ps.emit('error', new BackupsError(BackupsError.NOT_FOUND)); } else { - debug('[%s] download: s3 stream error.', backupFilePath, error); - ps.emit('error', new BackupsError(BackupsError.EXTERNAL_ERROR, error.message)); + debug(`download: ${apiConfig.bucket}:${backupFilePath} s3 stream error.`, error); + ps.emit('error', new BackupsError(BackupsError.EXTERNAL_ERROR, error.message || error.code)); // DO sets 'code' } }); @@ -484,7 +484,7 @@ function testConfig(apiConfig, callback) { var s3 = new AWS.S3(credentials); s3.putObject(params, function (error) { - if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message)); + if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message || error.code)); // DO sets 'code' var params = { Bucket: apiConfig.bucket, @@ -492,7 +492,7 @@ function testConfig(apiConfig, callback) { }; s3.deleteObject(params, function (error) { - if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message)); + if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message || error.code)); // DO sets 'code' callback(); }); @@ -508,23 +508,18 @@ function backupDone(apiConfig, backupId, appBackupIds, callback) { if (apiConfig.provider !== 'caas') return callback(); - // CaaS expects filenames instead of backupIds, this means no prefix but a file type extension - var FILE_TYPE = '.tar.gz.enc'; - var boxBackupFilename = backupId + FILE_TYPE; - var appBackupFilenames = appBackupIds.map(function (id) { return id + FILE_TYPE; }); + debug('[%s] backupDone: %s apps %j', backupId, backupId, appBackupIds); - debug('[%s] backupDone: %s apps %j', backupId, boxBackupFilename, appBackupFilenames); - - var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/backupDone'; + var url = config.apiServerOrigin() + '/api/v1/boxes/' + apiConfig.fqdn + '/backupDone'; var data = { boxVersion: config.version(), - restoreKey: boxBackupFilename, + backupId: backupId, appId: null, // now unused appVersion: null, // now unused - appBackupIds: appBackupFilenames + appBackupIds: appBackupIds }; - superagent.post(url).send(data).query({ token: config.token() }).timeout(30 * 1000).end(function (error, result) { + superagent.post(url).send(data).query({ token: apiConfig.token }).timeout(30 * 1000).end(function (error, result) { if (error && !error.response) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error)); if (result.statusCode !== 200) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, result.text)); diff --git a/src/subdomains.js b/src/subdomains.js deleted file mode 100644 index 83b78a1df..000000000 --- a/src/subdomains.js +++ /dev/null @@ -1,155 +0,0 @@ -'use strict'; - -module.exports = exports = { - remove: remove, - upsert: upsert, - get: get, - waitForDns: waitForDns, - verifyDnsConfig: verifyDnsConfig, - - SubdomainError: SubdomainError -}; - -var assert = require('assert'), - config = require('./config.js'), - settings = require('./settings.js'), - tld = require('tldjs'), - util = require('util'); - -function SubdomainError(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(SubdomainError, Error); - -SubdomainError.NOT_FOUND = 'No such domain'; -SubdomainError.EXTERNAL_ERROR = 'External error'; -SubdomainError.BAD_FIELD = 'Bad Field'; -SubdomainError.STILL_BUSY = 'Still busy'; -SubdomainError.INTERNAL_ERROR = 'Internal error'; -SubdomainError.ACCESS_DENIED = 'Access denied'; -SubdomainError.INVALID_PROVIDER = 'provider must be route53, gcdns, digitalocean, cloudflare, noop, manual or caas'; - -// choose which subdomain backend we use for test purpose we use route53 -function api(provider) { - assert.strictEqual(typeof provider, 'string'); - - switch (provider) { - case 'caas': return require('./dns/caas.js'); - case 'cloudflare': return require('./dns/cloudflare.js'); - case 'route53': return require('./dns/route53.js'); - case 'gcdns': return require('./dns/gcdns.js'); - case 'digitalocean': return require('./dns/digitalocean.js'); - case 'noop': return require('./dns/noop.js'); - case 'manual': return require('./dns/manual.js'); - default: return null; - } -} - -function getName(subdomain) { - // support special caas domains - if (!config.isCustomDomain()) return subdomain; - - if (config.fqdn() === config.zoneName()) return subdomain; - - var part = config.fqdn().slice(0, -config.zoneName().length - 1); - - return subdomain === '' ? part : subdomain + '.' + part; -} - -function get(subdomain, type, callback) { - assert.strictEqual(typeof subdomain, 'string'); - assert.strictEqual(typeof type, 'string'); - assert.strictEqual(typeof callback, 'function'); - - settings.getDnsConfig(function (error, dnsConfig) { - if (error) return callback(new SubdomainError(SubdomainError.INTERNAL_ERROR, error)); - - api(dnsConfig.provider).get(dnsConfig, config.zoneName(), getName(subdomain), type, function (error, values) { - if (error) return callback(error); - - callback(null, values); - }); - }); -} - -function upsert(subdomain, type, values, callback) { - assert.strictEqual(typeof subdomain, 'string'); - assert.strictEqual(typeof type, 'string'); - assert(util.isArray(values)); - assert.strictEqual(typeof callback, 'function'); - - settings.getDnsConfig(function (error, dnsConfig) { - if (error) return callback(new SubdomainError(SubdomainError.INTERNAL_ERROR, error)); - - api(dnsConfig.provider).upsert(dnsConfig, config.zoneName(), getName(subdomain), type, values, function (error, changeId) { - if (error) return callback(error); - - callback(null, changeId); - }); - }); -} - -function remove(subdomain, type, values, callback) { - assert.strictEqual(typeof subdomain, 'string'); - assert.strictEqual(typeof type, 'string'); - assert(util.isArray(values)); - assert.strictEqual(typeof callback, 'function'); - - settings.getDnsConfig(function (error, dnsConfig) { - if (error) return callback(new SubdomainError(SubdomainError.INTERNAL_ERROR, error)); - - api(dnsConfig.provider).del(dnsConfig, config.zoneName(), getName(subdomain), type, values, function (error) { - if (error && error.reason !== SubdomainError.NOT_FOUND) return callback(error); - - callback(null); - }); - }); -} - -function waitForDns(domain, value, type, options, callback) { - assert.strictEqual(typeof domain, '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'); - - settings.getDnsConfig(function (error, dnsConfig) { - if (error) return callback(new SubdomainError(SubdomainError.INTERNAL_ERROR, error)); - - var zoneName = config.zoneName(); - - // if the domain is on another zone in case of external domain, use the correct zone - if (!domain.endsWith(zoneName)) zoneName = tld.getDomain(domain); - - api(dnsConfig.provider).waitForDns(domain, zoneName, value, type, options, callback); - }); -} - -function verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback) { - assert(dnsConfig && typeof dnsConfig === 'object'); // the dns config to test with - assert(typeof dnsConfig.provider === 'string'); - assert.strictEqual(typeof domain, 'string'); - assert.strictEqual(typeof zoneName, 'string'); - assert.strictEqual(typeof ip, 'string'); - assert.strictEqual(typeof callback, 'function'); - - var backend = api(dnsConfig.provider); - if (!backend) return callback(new SubdomainError(SubdomainError.INVALID_PROVIDER)); - - api(dnsConfig.provider).verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback); -} diff --git a/src/taskmanager.js b/src/taskmanager.js index a7b81f5df..fea215812 100644 --- a/src/taskmanager.js +++ b/src/taskmanager.js @@ -16,6 +16,7 @@ var appdb = require('./appdb.js'), assert = require('assert'), async = require('async'), child_process = require('child_process'), + config = require('./config.js'), debug = require('debug')('box:taskmanager'), locker = require('./locker.js'), sendFailureLogs = require('./logcollector.js').sendFailureLogs, @@ -47,7 +48,7 @@ function resumeTasks(callback) { if (app.installationState === appdb.ISTATE_ERROR) return; - debug('Creating process for %s (%s) with state %s', app.location, app.id, app.installationState); + debug('Creating process for %s (%s) with state %s', config.appFqdn(app), app.id, app.installationState); restartAppTask(app.id, NOOP_CALLBACK); // restart because the auto-installer could have queued up tasks already }); diff --git a/src/test/apps-test.js b/src/test/apps-test.js index fce0d87e3..97755e38a 100644 --- a/src/test/apps-test.js +++ b/src/test/apps-test.js @@ -12,6 +12,7 @@ var appdb = require('../appdb.js'), config = require('../config.js'), constants = require('../constants.js'), database = require('../database.js'), + domaindb = require('../domaindb.js'), expect = require('expect.js'), groupdb = require('../groupdb.js'), groups = require('../groups.js'), @@ -66,10 +67,23 @@ describe('Apps', function () { name: 'group1' }; + const DOMAIN_0 = { + domain: 'example.com', + zoneName: 'example.com', + config: { provider: 'manual' } + }; + + const DOMAIN_1 = { + domain: 'example2.com', + zoneName: 'example2.com', + config: { provider: 'manual' } + }; + var APP_0 = { id: 'appid-0', appStoreId: 'appStoreId-0', location: 'some-location-0', + domain: DOMAIN_0.domain, manifest: { version: '0.1', dockerImage: 'docker/app0', healthCheckPath: '/', httpPort: 80, title: 'app0', tcpPorts: { @@ -88,6 +102,7 @@ describe('Apps', function () { id: 'appid-1', appStoreId: 'appStoreId-1', location: 'some-location-1', + domain: DOMAIN_0.domain, manifest: { version: '0.1', dockerImage: 'docker/app1', healthCheckPath: '/', httpPort: 80, title: 'app1', tcpPorts: {} @@ -101,6 +116,7 @@ describe('Apps', function () { id: 'appid-2', appStoreId: 'appStoreId-2', location: 'some-location-2', + domain: DOMAIN_1.domain, manifest: { version: '0.1', dockerImage: 'docker/app2', healthCheckPath: '/', httpPort: 80, title: 'app2', tcpPorts: {} @@ -111,10 +127,15 @@ describe('Apps', function () { }; before(function (done) { + config._reset(); + + config.setFqdn(DOMAIN_0.domain); async.series([ database.initialize, database._clear, + // DOMAIN_0 already added for test through domaindb.addDefaultDomain() + domaindb.add.bind(null, DOMAIN_1.domain, DOMAIN_1.zoneName, DOMAIN_1.config), userdb.add.bind(null, ADMIN_0.id, ADMIN_0), userdb.add.bind(null, USER_0.id, USER_0), userdb.add.bind(null, USER_1.id, USER_1), @@ -122,48 +143,51 @@ describe('Apps', function () { groupdb.add.bind(null, GROUP_1.id, GROUP_1.name), groups.addMember.bind(null, constants.ADMIN_GROUP_ID, ADMIN_0.id), groups.addMember.bind(null, GROUP_0.id, USER_1.id), - appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0), - appdb.add.bind(null, APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, APP_1.portBindings, APP_1), - appdb.add.bind(null, APP_2.id, APP_2.appStoreId, APP_2.manifest, APP_2.location, APP_2.portBindings, APP_2), + appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, APP_0.portBindings, APP_0), + appdb.add.bind(null, APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, APP_1.domain, APP_1.portBindings, APP_1), + appdb.add.bind(null, APP_2.id, APP_2.appStoreId, APP_2.manifest, APP_2.location, APP_2.domain, APP_2.portBindings, APP_2), settingsdb.set.bind(null, settings.BACKUP_CONFIG_KEY, JSON.stringify({ provider: 'caas', token: 'BACKUP_TOKEN', bucket: 'Bucket', prefix: 'Prefix' })) ], done); }); after(function (done) { - database._clear(done); + async.series([ + database._clear, + database.uninitialize + ], done); }); describe('validateHostname', function () { it('does not allow admin subdomain', function () { - expect(apps._validateHostname('my', 'cloudron.us')).to.be.an(Error); + expect(apps._validateHostname('my', 'example.com')).to.be.an(Error); }); it('cannot have >63 length subdomains', function () { var s = ''; for (var i = 0; i < 64; i++) s += 's'; - expect(apps._validateHostname(s, 'cloudron.us')).to.be.an(Error); + expect(apps._validateHostname(s, 'example.com')).to.be.an(Error); }); it('allows only alphanumerics and hypen', function () { - expect(apps._validateHostname('#2r', 'cloudron.us')).to.be.an(Error); - expect(apps._validateHostname('a%b', 'cloudron.us')).to.be.an(Error); - expect(apps._validateHostname('ab_', 'cloudron.us')).to.be.an(Error); - expect(apps._validateHostname('a.b', 'cloudron.us')).to.be.an(Error); - expect(apps._validateHostname('-ab', 'cloudron.us')).to.be.an(Error); - expect(apps._validateHostname('ab-', 'cloudron.us')).to.be.an(Error); + expect(apps._validateHostname('#2r', 'example.com')).to.be.an(Error); + expect(apps._validateHostname('a%b', 'example.com')).to.be.an(Error); + expect(apps._validateHostname('ab_', 'example.com')).to.be.an(Error); + expect(apps._validateHostname('a.b', 'example.com')).to.be.an(Error); + expect(apps._validateHostname('-ab', 'example.com')).to.be.an(Error); + expect(apps._validateHostname('ab-', 'example.com')).to.be.an(Error); }); it('total length cannot exceed 255', function () { var s = ''; - for (var i = 0; i < (255 - 'cloudron.us'.length); i++) s += 's'; + for (var i = 0; i < (255 - 'example.com'.length); i++) s += 's'; - expect(apps._validateHostname(s, 'cloudron.us')).to.be.an(Error); + expect(apps._validateHostname(s, 'example.com')).to.be.an(Error); }); it('allow valid domains', function () { - expect(apps._validateHostname('a', 'cloudron.us')).to.be(null); - expect(apps._validateHostname('a0-x', 'cloudron.us')).to.be(null); - expect(apps._validateHostname('01', 'cloudron.us')).to.be(null); + expect(apps._validateHostname('a', 'example.com')).to.be(null); + expect(apps._validateHostname('a0-x', 'example.com')).to.be(null); + expect(apps._validateHostname('01', 'example.com')).to.be(null); }); }); @@ -325,11 +349,13 @@ describe('Apps', function () { }); }); - it('succeeds with admin not being special', function (done) { + it('returns all apps for admin', function (done) { apps.getAllByUser(ADMIN_0, function (error, result) { expect(error).to.equal(null); - expect(result.length).to.equal(1); + expect(result.length).to.equal(3); expect(result[0].id).to.equal(APP_0.id); + expect(result[1].id).to.equal(APP_1.id); + expect(result[2].id).to.equal(APP_2.id); done(); }); }); diff --git a/src/test/apptask-test.js b/src/test/apptask-test.js index 31bf55a26..2b8b060c4 100644 --- a/src/test/apptask-test.js +++ b/src/test/apptask-test.js @@ -11,6 +11,7 @@ var addons = require('../addons.js'), async = require('async'), config = require('../config.js'), database = require('../database.js'), + domains = require('../domains.js'), expect = require('expect.js'), fs = require('fs'), js2xml = require('js2xmlparser').parse, @@ -48,12 +49,24 @@ var MANIFEST = { } }; +const DOMAIN_0 = { + domain: 'example.com', + zoneName: 'example.com', + config: { + provider: 'route53', + accessKeyId: 'accessKeyId', + secretAccessKey: 'secretAccessKey', + endpoint: 'http://localhost:5353' + } +}; + var APP = { id: 'appid', appStoreId: 'appStoreId', installationState: appdb.ISTATE_PENDING_INSTALL, runState: null, location: 'applocation', + domain: DOMAIN_0.domain, manifest: MANIFEST, containerId: null, httpPort: 4567, @@ -63,13 +76,12 @@ var APP = { memoryLimit: 0 }; - var awsHostedZones; +var awsHostedZones; describe('apptask', function () { before(function (done) { - config.set('version', '0.5.0'); - config.setFqdn('foobar.com'); - config.setZoneName('foobar.com'); + config._reset(); + config.setFqdn(DOMAIN_0.domain); config.set('provider', 'caas'); awsHostedZones = { @@ -89,15 +101,18 @@ describe('apptask', function () { async.series([ database.initialize, - appdb.add.bind(null, APP.id, APP.appStoreId, APP.manifest, APP.location, APP.portBindings, APP), + domains.update.bind(null, DOMAIN_0.domain, DOMAIN_0.config, null), + appdb.add.bind(null, APP.id, APP.appStoreId, APP.manifest, APP.location, APP.domain, APP.portBindings, APP), settings.initialize, - settings.setDnsConfig.bind(null, { provider: 'route53', accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey', endpoint: 'http://localhost:5353' }, config.fqdn(), config.zoneName()), settings.setTlsConfig.bind(null, { provider: 'caas' }) ], done); }); after(function (done) { - database._clear(done); + async.series([ + database._clear, + database.uninitialize + ], done); }); it('initializes succesfully', function (done) { @@ -246,7 +261,7 @@ describe('apptask', function () { .post('/2013-04-01/hostedzone/ZONEID/rrset/') .reply(200, js2xml('ChangeResourceRecordSetsResponse', { ChangeInfo: { Id: 'RRID', Status: 'INSYNC' } })); - apptask._unregisterSubdomain(APP, APP.location, function (error) { + apptask._unregisterSubdomain(APP, APP.location, APP.domain, function (error) { expect(error).to.be(null); expect(awsScope.isDone()).to.be.ok(); done(); diff --git a/src/test/backups-test.js b/src/test/backups-test.js index 16e1f47b8..28640faec 100644 --- a/src/test/backups-test.js +++ b/src/test/backups-test.js @@ -92,14 +92,17 @@ function createBackup(callback) { describe('backups', function () { before(function (done) { + const BACKUP_DIR = path.join(os.tmpdir(), 'cloudron-backup-test'); + async.series([ + mkdirp.bind(null, BACKUP_DIR), database.initialize, database._clear, settings.initialize, settings.setBackupConfig.bind(null, { provider: 'filesystem', key: 'enckey', - backupFolder: '/var/backups', + backupFolder: BACKUP_DIR, retentionSecs: 1, format: 'tgz' }) @@ -121,7 +124,7 @@ describe('backups', function () { version: '1.0.0', type: backupdb.BACKUP_TYPE_BOX, dependsOn: [ 'backup-app-00', 'backup-app-01' ], - restoreConfig: null, + manifest: null, format: 'tgz' }; @@ -130,7 +133,7 @@ describe('backups', function () { version: '1.0.0', type: backupdb.BACKUP_TYPE_APP, dependsOn: [], - restoreConfig: null, + manifest: null, format: 'tgz' }; @@ -139,7 +142,7 @@ describe('backups', function () { version: '1.0.0', type: backupdb.BACKUP_TYPE_APP, dependsOn: [], - restoreConfig: null, + manifest: null, format: 'tgz' }; @@ -148,7 +151,7 @@ describe('backups', function () { version: '1.0.0', type: backupdb.BACKUP_TYPE_BOX, dependsOn: [ 'backup-app-10', 'backup-app-11' ], - restoreConfig: null, + manifest: null, format: 'tgz' }; @@ -157,7 +160,7 @@ describe('backups', function () { version: '1.0.0', type: backupdb.BACKUP_TYPE_APP, dependsOn: [], - restoreConfig: null, + manifest: null, format: 'tgz' }; @@ -166,7 +169,7 @@ describe('backups', function () { version: '1.0.0', type: backupdb.BACKUP_TYPE_APP, dependsOn: [], - restoreConfig: null, + manifest: null, format: 'tgz' }; diff --git a/src/test/certificates-test.js b/src/test/certificates-test.js index bd0dbccd3..fcc56a4a0 100644 --- a/src/test/certificates-test.js +++ b/src/test/certificates-test.js @@ -22,7 +22,10 @@ function setup(done) { } function cleanup(done) { - database._clear(done); + async.series([ + database._clear, + database.uninitialize + ], done); } describe('Certificates', function () { diff --git a/src/test/cloudron-test.js b/src/test/cloudron-test.js index 5c5d2baf3..373f15b3b 100644 --- a/src/test/cloudron-test.js +++ b/src/test/cloudron-test.js @@ -13,13 +13,16 @@ var async = require('async'), function setup(done) { async.series([ - database.initialize.bind(null), - database._clear.bind(null) + database.initialize, + database._clear ], done); } function cleanup(done) { - database._clear(done); + async.series([ + database._clear, + database.uninitialize + ], done); } describe('Cloudron', function () { diff --git a/src/test/config-test.js b/src/test/config-test.js index 2b99fd301..370b27696 100644 --- a/src/test/config-test.js +++ b/src/test/config-test.js @@ -6,7 +6,6 @@ 'use strict'; var config = require('../config.js'), - constants = require('../constants.js'), expect = require('expect.js'), fs = require('fs'), path = require('path'); @@ -25,11 +24,6 @@ describe('config', function () { done(); }); - it('cloudron.conf generated automatically', function (done) { - expect(fs.existsSync(path.join(config.baseDir(), 'configs/cloudron.conf'))).to.be.ok(); - done(); - }); - it('can get and set version', function (done) { config.setVersion('1.2.3'); expect(config.version()).to.be('1.2.3'); @@ -38,15 +32,20 @@ describe('config', function () { it('did set default values', function () { expect(config.isCustomDomain()).to.equal(true); - expect(config.fqdn()).to.equal('localhost'); - expect(config.adminOrigin()).to.equal('https://my.localhost'); - expect(config.appFqdn('app')).to.equal('app.localhost'); + expect(config.fqdn()).to.equal(''); expect(config.zoneName()).to.equal(''); + expect(config.adminLocation()).to.equal('my'); }); it('set saves value in file', function (done) { + config.set('fqdn', 'example.com'); + expect(JSON.parse(fs.readFileSync(path.join(config.baseDir(), 'configs/cloudron.conf'))).fqdn).to.eql('example.com'); + done(); + }); + + it('set does not save custom values in file', function (done) { config.set('foobar', 'somevalue'); - expect(JSON.parse(fs.readFileSync(path.join(config.baseDir(), 'configs/cloudron.conf'))).foobar).to.eql('somevalue'); + expect(JSON.parse(fs.readFileSync(path.join(config.baseDir(), 'configs/cloudron.conf'))).foobar).to.not.be.ok(); done(); }); @@ -69,7 +68,7 @@ describe('config', function () { expect(config.isCustomDomain()).to.equal(true); expect(config.fqdn()).to.equal('example.com'); expect(config.adminOrigin()).to.equal('https://my.example.com'); - expect(config.appFqdn('app')).to.equal('app.example.com'); + expect(config.appFqdn({ location: 'app', domain: config.fqdn() })).to.equal('app.example.com'); expect(config.zoneName()).to.equal('example.com'); }); @@ -80,7 +79,7 @@ describe('config', function () { expect(config.isCustomDomain()).to.equal(false); expect(config.fqdn()).to.equal('test.example.com'); expect(config.adminOrigin()).to.equal('https://my-test.example.com'); - expect(config.appFqdn('app')).to.equal('app-test.example.com'); + expect(config.appFqdn({ location: 'app', domain: config.fqdn() })).to.equal('app-test.example.com'); expect(config.zoneName()).to.equal('example.com'); }); diff --git a/src/test/database-test.js b/src/test/database-test.js index 6067d2b96..cd0f0fc84 100644 --- a/src/test/database-test.js +++ b/src/test/database-test.js @@ -10,13 +10,16 @@ var appdb = require('../appdb.js'), authcodedb = require('../authcodedb.js'), backupdb = require('../backupdb.js'), clientdb = require('../clientdb.js'), + config = require('../config.js'), database = require('../database'), DatabaseError = require('../databaseerror.js'), + domaindb = require('../domaindb'), eventlogdb = require('../eventlogdb.js'), expect = require('expect.js'), groupdb = require('../groupdb.js'), hat = require('hat'), mailboxdb = require('../mailboxdb.js'), + path = require('path'), settingsdb = require('../settingsdb.js'), tokendb = require('../tokendb.js'), userdb = require('../userdb.js'), @@ -58,8 +61,17 @@ var USER_2 = { displayName: 'Herbert 2' }; +const TEST_DOMAIN = { + domain: 'example.com', + zoneName: 'example.com', + config: {} +}; + describe('database', function () { before(function (done) { + config._reset(); + config.setFqdn(TEST_DOMAIN.domain); + async.series([ database.initialize, database._clear @@ -67,7 +79,169 @@ describe('database', function () { }); after(function (done) { - database._clear(done); + async.series([ + database._clear, + database.uninitialize + ], done); + }); + + describe('domains', function () { + const DOMAIN_0 = { + domain: 'foobar.com', + zoneName: 'foobar.com', + config: { provider: 'digitalocean', token: 'abcd' } + }; + + const DOMAIN_1 = { + domain: 'foo.cloudron.io', + zoneName: 'cloudron.io', + config: null + }; + + it('can add domain', function (done) { + domaindb.add(DOMAIN_0.domain, DOMAIN_0.zoneName, DOMAIN_0.config, done); + }); + + it('can add another domain', function (done) { + domaindb.add(DOMAIN_1.domain, DOMAIN_1.zoneName, DOMAIN_1.config, done); + }); + + it('cannot add same domain twice', function (done) { + domaindb.add(DOMAIN_0.domain, DOMAIN_0.zoneName, DOMAIN_0.config, function (error) { + expect(error).to.be.ok(); + expect(error.reason).to.be(DatabaseError.ALREADY_EXISTS); + done(); + }); + }); + + it('can get domain', function (done) { + domaindb.get(DOMAIN_0.domain, function (error, result) { + expect(error).to.equal(null); + expect(result).to.be.an('object'); + expect(result.domain).to.equal(DOMAIN_0.domain); + expect(result.zoneName).to.equal(DOMAIN_0.zoneName); + expect(result.config).to.eql(DOMAIN_0.config); + + done(); + }); + }); + + it('can get domain without provider set', function (done) { + domaindb.get(DOMAIN_1.domain, function (error, result) { + expect(error).to.equal(null); + expect(result).to.be.an('object'); + expect(result.domain).to.equal(DOMAIN_1.domain); + expect(result.zoneName).to.equal(DOMAIN_1.zoneName); + expect(result.config).to.eql(DOMAIN_1.config); + + done(); + }); + }); + + it('can update domain', function (done) { + const newConfig = { provider: 'manual' }; + + domaindb.update(DOMAIN_1.domain, newConfig, function (error) { + expect(error).to.equal(null); + + domaindb.get(DOMAIN_1.domain, function (error, result) { + expect(error).to.equal(null); + expect(result).to.be.an('object'); + expect(result.domain).to.equal(DOMAIN_1.domain); + expect(result.zoneName).to.equal(DOMAIN_1.zoneName); + expect(result.config).to.eql(newConfig); + + DOMAIN_1.config = newConfig; + + done(); + }); + }); + }); + + it('can get all domains', function (done) { + domaindb.getAll(function (error, result) { + expect(error).to.equal(null); + expect(result).to.be.an('array'); + expect(result.length).to.equal(3); // includes the TEST_DOMAIN + + // sorted by domain + expect(result[0].domain).to.equal(TEST_DOMAIN.domain); + expect(result[0].zoneName).to.equal(TEST_DOMAIN.zoneName); + expect(result[0].config).to.eql(TEST_DOMAIN.config); + + expect(result[1].domain).to.equal(DOMAIN_1.domain); + expect(result[1].zoneName).to.equal(DOMAIN_1.zoneName); + expect(result[1].config).to.eql(DOMAIN_1.config); + + expect(result[2].domain).to.equal(DOMAIN_0.domain); + expect(result[2].zoneName).to.equal(DOMAIN_0.zoneName); + expect(result[2].config).to.eql(DOMAIN_0.config); + + done(); + }); + }); + + it('cannot delete non-existing domain', function (done) { + domaindb.del('not.exists', function (error) { + expect(error).to.be.a(DatabaseError); + expect(error.reason).to.equal(DatabaseError.NOT_FOUND); + + done(); + }); + }); + + var APP_0 = { + id: 'appid-0', + appStoreId: 'appStoreId-0', + dnsRecordId: null, + installationState: appdb.ISTATE_PENDING_INSTALL, + installationProgress: null, + runState: null, + location: 'some-location-0', + domain: DOMAIN_0.domain, + manifest: { version: '0.1', dockerImage: 'docker/app0', healthCheckPath: '/', httpPort: 80, title: 'app0' }, + httpPort: null, + containerId: null, + portBindings: { port: 5678 }, + health: null, + accessRestriction: null, + lastBackupId: null, + oldConfig: null, + newConfig: null, + memoryLimit: 4294967296, + altDomain: null, + xFrameOptions: 'DENY', + sso: true, + debugMode: null, + robotsTxt: null, + enableBackup: true + }; + + it('cannot delete referenced domain', function (done) { + appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, APP_0.portBindings, APP_0, function (error) { + expect(error).to.be(null); + + domaindb.del(DOMAIN_0.domain, function (error) { + expect(error).to.be.a(DatabaseError); + expect(error.reason).to.equal(DatabaseError.IN_USE); + + appdb.del(APP_0.id, done); + }); + }); + }); + + it('can delete existing domain', function (done) { + domaindb.del(DOMAIN_0.domain, function (error) { + expect(error).to.be(null); + + domaindb.get(DOMAIN_0.domain, function (error) { + expect(error).to.be.a(DatabaseError); + expect(error.reason).to.equal(DatabaseError.NOT_FOUND); + + done(); + }); + }); + }); }); describe('user', function () { @@ -530,15 +704,16 @@ describe('database', function () { installationProgress: null, runState: null, location: 'some-location-0', + domain: 'example.com', manifest: { version: '0.1', dockerImage: 'docker/app0', healthCheckPath: '/', httpPort: 80, title: 'app0' }, httpPort: null, containerId: null, portBindings: { port: 5678 }, health: null, accessRestriction: null, - lastBackupId: null, + restoreConfig: null, oldConfig: null, - newConfig: null, + updateConfig: null, memoryLimit: 4294967296, altDomain: null, xFrameOptions: 'DENY', @@ -555,15 +730,16 @@ describe('database', function () { installationProgress: null, runState: null, location: 'some-location-1', + domain: 'example.com', manifest: { version: '0.2', dockerImage: 'docker/app1', healthCheckPath: '/', httpPort: 80, title: 'app1' }, httpPort: null, containerId: null, portBindings: { }, health: null, accessRestriction: { users: [ 'foobar' ] }, - lastBackupId: null, + restoreConfig: null, oldConfig: null, - newConfig: null, + updateConfig: null, memoryLimit: 0, altDomain: null, xFrameOptions: 'SAMEORIGIN', @@ -587,7 +763,7 @@ describe('database', function () { }); it('add succeeds', function (done) { - appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0, function (error) { + appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, APP_0.portBindings, APP_0, function (error) { expect(error).to.be(null); done(); }); @@ -611,7 +787,7 @@ describe('database', function () { }); it('add of same app fails', function (done) { - appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, [ ], APP_0, function (error) { + appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, [], APP_0, function (error) { expect(error).to.be.a(DatabaseError); expect(error.reason).to.be(DatabaseError.ALREADY_EXISTS); done(); @@ -622,7 +798,7 @@ describe('database', function () { appdb.get(APP_0.id, function (error, result) { expect(error).to.be(null); expect(result).to.be.an('object'); - expect(result).to.be.eql(APP_0); + expect(_.omit(result, ['creationTime', 'updateTime'])).to.be.eql(APP_0); done(); }); }); @@ -659,7 +835,7 @@ describe('database', function () { appdb.get(APP_0.id, function (error, result) { expect(error).to.be(null); expect(result).to.be.an('object'); - expect(result).to.be.eql(APP_0); + expect(_.omit(result, ['creationTime', 'updateTime'])).to.be.eql(APP_0); done(); }); }); @@ -669,7 +845,7 @@ describe('database', function () { appdb.getByHttpPort(APP_0.httpPort, function (error, result) { expect(error).to.be(null); expect(result).to.be.an('object'); - expect(result).to.be.eql(APP_0); + expect(_.omit(result, ['creationTime', 'updateTime'])).to.be.eql(APP_0); done(); }); }); @@ -683,7 +859,7 @@ describe('database', function () { }); it('add second app succeeds', function (done) { - appdb.add(APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, [ ], APP_1, function (error) { + appdb.add(APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, APP_1.domain, [], APP_1, function (error) { expect(error).to.be(null); done(); }); @@ -694,8 +870,8 @@ describe('database', function () { expect(error).to.be(null); expect(result).to.be.an(Array); expect(result.length).to.be(2); - expect(result[0]).to.be.eql(APP_0); - expect(result[1]).to.be.eql(APP_1); + expect(_.omit(result[0], ['creationTime', 'updateTime'])).to.be.eql(APP_0); + expect(_.omit(result[1], ['creationTime', 'updateTime'])).to.be.eql(APP_1); done(); }); }); @@ -1027,7 +1203,7 @@ describe('database', function () { version: '1.0.0', type: backupdb.BACKUP_TYPE_BOX, dependsOn: [ 'dep1' ], - restoreConfig: null, + manifest: null, format: 'tgz' }; @@ -1044,7 +1220,7 @@ describe('database', function () { expect(result.type).to.be(backupdb.BACKUP_TYPE_BOX); expect(result.creationTime).to.be.a(Date); expect(result.dependsOn).to.eql(['dep1']); - expect(result.restoreConfig).to.eql(null); + expect(result.manifest).to.eql(null); done(); }); }); @@ -1067,7 +1243,7 @@ describe('database', function () { expect(results[0].id).to.be('backup-box'); expect(results[0].version).to.be('1.0.0'); expect(results[0].dependsOn).to.eql(['dep1']); - expect(results[0].restoreConfig).to.eql(null); + expect(results[0].manifest).to.eql(null); done(); }); @@ -1093,7 +1269,7 @@ describe('database', function () { version: '1.0.0', type: backupdb.BACKUP_TYPE_APP, dependsOn: [ ], - restoreConfig: { manifest: { foo: 'bar' } }, + manifest: { foo: 'bar' }, format: 'tgz' }; @@ -1110,7 +1286,7 @@ describe('database', function () { expect(result.type).to.be(backupdb.BACKUP_TYPE_APP); expect(result.creationTime).to.be.a(Date); expect(result.dependsOn).to.eql([]); - expect(result.restoreConfig).to.eql({ manifest: { foo: 'bar' } }); + expect(result.manifest).to.eql({ foo: 'bar' }); done(); }); }); @@ -1124,7 +1300,7 @@ describe('database', function () { expect(results[0].id).to.be('app_appid_123'); expect(results[0].version).to.be('1.0.0'); expect(results[0].dependsOn).to.eql([]); - expect(results[0].restoreConfig).to.eql({ manifest: { foo: 'bar' } }); + expect(results[0].manifest).to.eql({ foo: 'bar' }); done(); }); @@ -1250,6 +1426,9 @@ describe('database', function () { describe('groups', function () { before(function (done) { + config._reset(); + config.setFqdn(TEST_DOMAIN.domain); + async.series([ database.initialize, database._clear, @@ -1368,8 +1547,45 @@ describe('database', function () { }); }); + describe('importFromFile', function () { + before(function (done) { + config._reset(); + config.setFqdn(TEST_DOMAIN.domain); + + async.series([ + database.initialize, + database._clear + ], done); + }); + + it('cannot import from non-existent file', function (done) { + database.importFromFile('/does/not/exist', function (error) { + expect(error).to.be.ok(); + done(); + }); + }); + + it('can export to file', function (done) { + database.exportToFile('/tmp/box.mysqldump', function (error) { + expect(error).to.be(null); + done(); + }); + }); + + it('can import from file', function (done) { + database.importFromFile('/tmp/box.mysqldump', function (error) { + expect(error).to.be(null); + done(); + }); + }); + + }); + describe('mailboxes', function () { before(function (done) { + config._reset(); + config.setFqdn(TEST_DOMAIN.domain); + async.series([ database.initialize, database._clear @@ -1377,31 +1593,32 @@ describe('database', function () { }); it('add user mailbox succeeds', function (done) { - mailboxdb.add('girish', 'uid-0', mailboxdb.TYPE_USER, function (error, mailbox) { + mailboxdb.add('girish', TEST_DOMAIN.domain, 'uid-0', mailboxdb.TYPE_USER, function (error, mailbox) { expect(error).to.be(null); done(); }); }); it('cannot add dup entry', function (done) { - mailboxdb.add('girish', 'uid-1', mailboxdb.TYPE_APP, function (error, mailbox) { + mailboxdb.add('girish', TEST_DOMAIN.domain, 'uid-1', mailboxdb.TYPE_APP, function (error, mailbox) { expect(error.reason).to.be(DatabaseError.ALREADY_EXISTS); done(); }); }); it('add app mailbox succeeds', function (done) { - mailboxdb.add('support', 'osticket', mailboxdb.TYPE_APP, function (error, mailbox) { + mailboxdb.add('support', TEST_DOMAIN.domain, 'osticket', mailboxdb.TYPE_APP, function (error, mailbox) { expect(error).to.be(null); done(); }); }); it('get succeeds', function (done) { - mailboxdb.getMailbox('support', function (error, mailbox) { + mailboxdb.getMailbox('support', TEST_DOMAIN.domain, function (error, mailbox) { expect(error).to.be(null); - expect(mailbox.name).to.be('support'); - expect(mailbox.ownerId).to.be('osticket'); + expect(mailbox.name).to.equal('support'); + expect(mailbox.ownerId).to.equal('osticket'); + expect(mailbox.domain).to.equal(TEST_DOMAIN.domain); expect(mailbox.creationTime).to.be.a(Date); done(); @@ -1409,7 +1626,7 @@ describe('database', function () { }); it('list mailboxes succeeds', function (done) { - mailboxdb.listMailboxes(function (error, mailboxes) { + mailboxdb.listMailboxes(TEST_DOMAIN.domain, function (error, mailboxes) { expect(error).to.be(null); expect(mailboxes.length).to.be(2); expect(mailboxes[0].name).to.be('girish'); @@ -1421,14 +1638,14 @@ describe('database', function () { }); it('can set alias', function (done) { - mailboxdb.setAliasesForName('support', [ 'support2', 'help' ], function (error) { + mailboxdb.setAliasesForName('support', TEST_DOMAIN.domain, [ 'support2', 'help' ], function (error) { expect(error).to.be(null); done(); }); }); it('can get aliases of name', function (done) { - mailboxdb.getAliasesForName('support', function (error, results) { + mailboxdb.getAliasesForName('support', TEST_DOMAIN.domain, function (error, results) { expect(error).to.be(null); expect(results.length).to.be(2); expect(results[0]).to.be('help'); @@ -1438,7 +1655,7 @@ describe('database', function () { }); it('can get alias', function (done) { - mailboxdb.getAlias('support2', function (error, result) { + mailboxdb.getAlias('support2', TEST_DOMAIN.domain, function (error, result) { expect(error).to.be(null); expect(result.name).to.be('support2'); expect(result.aliasTarget).to.be('support'); @@ -1447,7 +1664,7 @@ describe('database', function () { }); it('can list aliases', function (done) { - mailboxdb.listAliases(function (error, results) { + mailboxdb.listAliases(TEST_DOMAIN.domain, function (error, results) { expect(error).to.be(null); expect(results.length).to.be(2); expect(results[0].name).to.be('help'); @@ -1469,22 +1686,22 @@ describe('database', function () { }); it('cannot get non-existing group', function (done) { - mailboxdb.getGroup('random', function (error) { + mailboxdb.getGroup('random', TEST_DOMAIN.domain, function (error) { expect(error.reason).to.be(DatabaseError.NOT_FOUND); done(); }); }); it('can change name', function (done) { - mailboxdb.updateName('support', 'support3', function (error) { + mailboxdb.updateName('support', TEST_DOMAIN.domain, 'support3', TEST_DOMAIN.domain, function (error) { expect(error).to.be(null); - mailboxdb.updateName('support3', 'support', done); + mailboxdb.updateName('support3', TEST_DOMAIN.domain, 'support', TEST_DOMAIN.domain, done); }); }); it('cannot change name to existing one', function (done) { - mailboxdb.updateName('support', 'support2', function (error) { + mailboxdb.updateName('support', TEST_DOMAIN.domain, 'support2', TEST_DOMAIN.domain, function (error) { expect(error).to.be.ok(); expect(error.reason).to.eql(DatabaseError.ALREADY_EXISTS); @@ -1493,10 +1710,10 @@ describe('database', function () { }); it('unset aliases', function (done) { - mailboxdb.setAliasesForName('support', [ ], function (error) { + mailboxdb.setAliasesForName('support', TEST_DOMAIN.domain, [], function (error) { expect(error).to.be(null); - mailboxdb.getAliasesForName('support', function (error, results) { + mailboxdb.getAliasesForName('support', TEST_DOMAIN.domain, function (error, results) { expect(error).to.be(null); expect(results.length).to.be(0); done(); @@ -1505,7 +1722,7 @@ describe('database', function () { }); it('del succeeds', function (done) { - mailboxdb.del('girish', function (error) { + mailboxdb.del('girish', TEST_DOMAIN.domain, function (error) { expect(error).to.be(null); done(); }); diff --git a/src/test/digest-test.js b/src/test/digest-test.js index 07e627b08..7987b7d6a 100644 --- a/src/test/digest-test.js +++ b/src/test/digest-test.js @@ -9,10 +9,10 @@ var async = require('async'), config = require('../config.js'), database = require('../database.js'), digest = require('../digest.js'), + domaindb = require('../domaindb.js'), eventlog = require('../eventlog.js'), expect = require('expect.js'), mailer = require('../mailer.js'), - nock = require('nock'), paths = require('../paths.js'), safe = require('safetydance'), settings = require('../settings.js'), @@ -28,6 +28,12 @@ var USER_0 = { displayName: 'User 0' }; +const DOMAIN_0 = { + domain: 'example.com', + zoneName: 'example.com', + config: { provider: 'manual' } +}; + var AUDIT_SOURCE = { ip: '1.2.3.4' }; @@ -37,8 +43,8 @@ function checkMails(number, email, done) { setTimeout(function () { expect(mailer._getMailQueue().length).to.equal(number); - if (number && email) { - expect(mailer._getMailQueue()[0].to.indexOf(email)).to.not.equal(-1); + if (number) { + expect(mailer._getMailQueue()[0].to).to.equal(email); } mailer._clearMailQueue(); @@ -47,21 +53,12 @@ function checkMails(number, email, done) { } describe('digest', function () { - function cleanup(done) { - mailer._clearMailQueue(); - safe.fs.unlinkSync(paths.UPDATE_CHECKER_FILE); - - async.series([ - settings.uninitialize, - database._clear - ], done); - } - before(function (done) { config._reset(); - config.set('version', '1.0.0'); + config.set('fqdn', 'domain.com'); config.set('apiServerOrigin', 'http://localhost:4444'); config.set('provider', 'notcaas'); + config.setFqdn(DOMAIN_0.domain); safe.fs.unlinkSync(paths.UPDATE_CHECKER_FILE); async.series([ @@ -70,12 +67,21 @@ describe('digest', function () { settings.initialize, user.createOwner.bind(null, USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, AUDIT_SOURCE), eventlog.add.bind(null, eventlog.ACTION_UPDATE, AUDIT_SOURCE, { boxUpdateInfo: { sourceTarballUrl: 'xx', version: '1.2.3', changelog: [ 'good stuff' ] } }), - mailer.start, + settingsdb.set.bind(null, settings.MAIL_CONFIG_KEY, JSON.stringify({ enabled: true })), mailer._clearMailQueue ], done); }); - after(cleanup); + after(function (done) { + mailer._clearMailQueue(); + safe.fs.unlinkSync(paths.UPDATE_CHECKER_FILE); + + async.series([ + settings.uninitialize, + database._clear, + database.uninitialize + ], done); + }); describe('disabled', function () { before(function (done) { @@ -85,7 +91,7 @@ describe('digest', function () { it('does not send mail with digest disabled', function (done) { digest.maybeSend(function (error) { if (error) return done(error); - checkMails(0, '', done); + checkMails(0, null, done); }); }); @@ -100,7 +106,7 @@ describe('digest', function () { digest.maybeSend(function (error) { if (error) return done(error); - checkMails(1, '', done); + checkMails(1, `${USER_0.email}, ${USER_0.username}@${config.fqdn()}`, done); }); }); @@ -110,19 +116,11 @@ describe('digest', function () { digest.maybeSend(function (error) { if (error) return done(error); - checkMails(1, '', done); + checkMails(1, `${USER_0.email}, ${USER_0.username}@${config.fqdn()}`, done); }); }); it('sends mail for pending update to owner account email', function (done) { - var subscription = { - id: 'caas', - created: 0, - canceled_at: 0, - status: 'active', - plan: { id: 'caas' } - }; - updatechecker._setUpdateInfo({ box: null, apps: { 'appid': { manifest: { version: '1.2.5', changelog: 'noop\nreally' } } } }); settingsdb.set(settings.MAIL_CONFIG_KEY, JSON.stringify({ enabled: true }), function (error) { @@ -131,11 +129,7 @@ describe('digest', function () { digest.maybeSend(function (error) { if (error) return done(error); - checkMails(1, [ 'user0@email.com, username0@localhost' ], function (error) { - if (error) return done(error); - - done(); - }); + checkMails(1, `${USER_0.email}, ${USER_0.username}@${DOMAIN_0.domain}`, done); }); }); }); diff --git a/src/test/dns-test.js b/src/test/dns-test.js index d098c3d83..6f10ae36c 100644 --- a/src/test/dns-test.js +++ b/src/test/dns-test.js @@ -11,40 +11,48 @@ var async = require('async'), GCDNS = require('@google-cloud/dns'), config = require('../config.js'), database = require('../database.js'), + domains = require('../domains.js'), expect = require('expect.js'), nock = require('nock'), settings = require('../settings.js'), - subdomains = require('../subdomains.js'), util = require('util'); +var DOMAIN_0 = { + domain: 'example-dns-test.com', + zoneName: 'example-dns-test.com', + config: {} +}; + describe('dns provider', function () { before(function (done) { config._reset(); + config.setFqdn(DOMAIN_0.domain); async.series([ database.initialize, - settings.initialize + settings.initialize, + database._clear ], done); }); after(function (done) { - database._clear(done); + async.series([ + database._clear, + database.uninitialize + ], done); }); describe('noop', function () { before(function (done) { - var data = { + DOMAIN_0.config = { provider: 'noop' }; - config.setFqdn('example.com'); - config.setZoneName('example.com'); - - settings.setDnsConfig(data, config.fqdn(), config.zoneName(), done); + domains.update(DOMAIN_0.domain, DOMAIN_0.config, null, done); }); it('upsert succeeds', function (done) { - subdomains.upsert('test', 'A', [ '1.2.3.4' ], function (error, result) { + domains.upsertDNSRecords('test', DOMAIN_0.domain, 'A', [ '1.2.3.4' ], function (error, result) { expect(error).to.eql(null); expect(result).to.eql('noop-record-id'); @@ -53,7 +61,7 @@ describe('dns provider', function () { }); it('get succeeds', function (done) { - subdomains.get('test', 'A', function (error, result) { + domains.getDNSRecords('test', DOMAIN_0.domain, 'A', function (error, result) { expect(error).to.eql(null); expect(result).to.be.an(Array); expect(result.length).to.eql(0); @@ -63,7 +71,7 @@ describe('dns provider', function () { }); it('del succeeds', function (done) { - subdomains.remove('test', 'A', [ '1.2.3.4' ], function (error) { + domains.removeDNSRecords('test', DOMAIN_0.domain, 'A', [ '1.2.3.4' ], function (error) { expect(error).to.eql(null); done(); @@ -76,15 +84,12 @@ describe('dns provider', function () { var DIGITALOCEAN_ENDPOINT = 'https://api.digitalocean.com'; before(function (done) { - var data = { + DOMAIN_0.config = { provider: 'digitalocean', token: TOKEN }; - config.setFqdn('example.com'); - config.setZoneName('example.com'); - - settings.setDnsConfig(data, config.fqdn(), config.zoneName(), done); + domains.update(DOMAIN_0.domain, DOMAIN_0.config, null, done); }); it('upsert non-existing record succeeds', function (done) { @@ -107,7 +112,7 @@ describe('dns provider', function () { .post('/v2/domains/' + config.zoneName() + '/records') .reply(201, { domain_record: DOMAIN_RECORD_0 }); - subdomains.upsert('test', 'A', [ '1.2.3.4' ], function (error, result) { + domains.upsertDNSRecords('test', DOMAIN_0.domain, 'A', [ '1.2.3.4' ], function (error, result) { expect(error).to.eql(null); expect(result).to.eql('3352892'); expect(req1.isDone()).to.be.ok(); @@ -157,7 +162,7 @@ describe('dns provider', function () { .put('/v2/domains/' + config.zoneName() + '/records/' + DOMAIN_RECORD_1.id) .reply(200, { domain_record: DOMAIN_RECORD_1_NEW }); - subdomains.upsert('test', 'A', [ DOMAIN_RECORD_1_NEW.data ], function (error, result) { + domains.upsertDNSRecords('test', DOMAIN_0.domain, 'A', [ DOMAIN_RECORD_1_NEW.data ], function (error, result) { expect(error).to.eql(null); expect(result).to.eql('3352893'); expect(req1.isDone()).to.be.ok(); @@ -243,7 +248,7 @@ describe('dns provider', function () { .post('/v2/domains/' + config.zoneName() + '/records') .reply(201, { domain_record: DOMAIN_RECORD_2_NEW }); - subdomains.upsert('', 'TXT', [ DOMAIN_RECORD_2_NEW.data, DOMAIN_RECORD_1_NEW.data, DOMAIN_RECORD_3_NEW.data ], function (error, result) { + domains.upsertDNSRecords('', config.fqdn(), 'TXT', [ DOMAIN_RECORD_2_NEW.data, DOMAIN_RECORD_1_NEW.data, DOMAIN_RECORD_3_NEW.data ], function (error, result) { expect(error).to.eql(null); expect(result).to.eql('3352893'); expect(req1.isDone()).to.be.ok(); @@ -282,7 +287,7 @@ describe('dns provider', function () { .get('/v2/domains/' + config.zoneName() + '/records') .reply(200, { domain_records: [ DOMAIN_RECORD_0, DOMAIN_RECORD_1 ] }); - subdomains.get('test', 'A', function (error, result) { + domains.getDNSRecords('test', DOMAIN_0.domain, 'A', function (error, result) { expect(error).to.eql(null); expect(result).to.be.an(Array); expect(result.length).to.eql(1); @@ -323,7 +328,7 @@ describe('dns provider', function () { .delete('/v2/domains/' + config.zoneName() + '/records/' + DOMAIN_RECORD_1.id) .reply(204, {}); - subdomains.remove('test', 'A', ['1.2.3.4'], function (error) { + domains.removeDNSRecords('test', DOMAIN_0.domain, 'A', ['1.2.3.4'], function (error) { expect(error).to.eql(null); expect(req1.isDone()).to.be.ok(); expect(req2.isDone()).to.be.ok(); @@ -334,44 +339,43 @@ describe('dns provider', function () { }); describe('route53', function () { - config.setFqdn('example.com'); - config.setZoneName('example.com'); - // do not clear this with [] but .length = 0 so we don't loose the reference in mockery var awsAnswerQueue = []; - var AWS_HOSTED_ZONES = { - HostedZones: [{ - Id: '/hostedzone/Z34G16B38TNZ9L', - Name: config.zoneName() + '.', - CallerReference: '305AFD59-9D73-4502-B020-F4E6F889CB30', - ResourceRecordSetCount: 2, - ChangeInfo: { - Id: '/change/CKRTFJA0ANHXB', - Status: 'INSYNC' - } - }, { - Id: '/hostedzone/Z3OFC3B6E8YTA7', - Name: 'cloudron.us.', - CallerReference: '0B37F2DE-21A4-E678-BA32-3FC8AF0CF635', - Config: {}, - ResourceRecordSetCount: 2, - ChangeInfo: { - Id: '/change/C2682N5HXP0BZ5', - Status: 'INSYNC' - } - }], - IsTruncated: false, - MaxItems: '100' - }; + var AWS_HOSTED_ZONES = null; before(function (done) { - var data = { + DOMAIN_0.config = { provider: 'route53', accessKeyId: 'unused', secretAccessKey: 'unused' }; + AWS_HOSTED_ZONES = { + HostedZones: [{ + Id: '/hostedzone/Z34G16B38TNZ9L', + Name: config.zoneName() + '.', + CallerReference: '305AFD59-9D73-4502-B020-F4E6F889CB30', + ResourceRecordSetCount: 2, + ChangeInfo: { + Id: '/change/CKRTFJA0ANHXB', + Status: 'INSYNC' + } + }, { + Id: '/hostedzone/Z3OFC3B6E8YTA7', + Name: 'cloudron.us.', + CallerReference: '0B37F2DE-21A4-E678-BA32-3FC8AF0CF635', + Config: {}, + ResourceRecordSetCount: 2, + ChangeInfo: { + Id: '/change/C2682N5HXP0BZ5', + Status: 'INSYNC' + } + }], + IsTruncated: false, + MaxItems: '100' + }; + function mockery (queue) { return function(options, callback) { expect(options).to.be.an(Object); @@ -396,8 +400,8 @@ describe('dns provider', function () { function Route53Mock(cfg) { expect(cfg).to.eql({ - accessKeyId: data.accessKeyId, - secretAccessKey: data.secretAccessKey, + accessKeyId: DOMAIN_0.config.accessKeyId, + secretAccessKey: DOMAIN_0.config.secretAccessKey, region: 'us-east-1' }); } @@ -412,7 +416,7 @@ describe('dns provider', function () { AWS._originalRoute53 = AWS.Route53; AWS.Route53 = Route53Mock; - settings.setDnsConfig(data, config.fqdn(), config.zoneName(), done); + domains.update(DOMAIN_0.domain, DOMAIN_0.config, null, done); }); after(function () { @@ -430,7 +434,7 @@ describe('dns provider', function () { } }]); - subdomains.upsert('test', 'A', [ '1.2.3.4' ], function (error, result) { + domains.upsertDNSRecords('test', DOMAIN_0.domain, 'A', [ '1.2.3.4' ], function (error, result) { expect(error).to.eql(null); expect(result).to.eql('/change/C2QLKQIWEI0BZF'); expect(awsAnswerQueue.length).to.eql(0); @@ -449,7 +453,7 @@ describe('dns provider', function () { } }]); - subdomains.upsert('test', 'A', [ '1.2.3.4' ], function (error, result) { + domains.upsertDNSRecords('test', DOMAIN_0.domain, 'A', [ '1.2.3.4' ], function (error, result) { expect(error).to.eql(null); expect(result).to.eql('/change/C2QLKQIWEI0BZF'); expect(awsAnswerQueue.length).to.eql(0); @@ -468,7 +472,7 @@ describe('dns provider', function () { } }]); - subdomains.upsert('', 'TXT', [ 'first', 'second', 'third' ], function (error, result) { + domains.upsertDNSRecords('', config.fqdn(), 'TXT', [ 'first', 'second', 'third' ], function (error, result) { expect(error).to.eql(null); expect(result).to.eql('/change/C2QLKQIWEI0BZF'); expect(awsAnswerQueue.length).to.eql(0); @@ -489,7 +493,7 @@ describe('dns provider', function () { }] }]); - subdomains.get('test', 'A', function (error, result) { + domains.getDNSRecords('test', DOMAIN_0.domain, 'A', function (error, result) { expect(error).to.eql(null); expect(result).to.be.an(Array); expect(result.length).to.eql(1); @@ -510,7 +514,7 @@ describe('dns provider', function () { } }]); - subdomains.remove('test', 'A', ['1.2.3.4'], function (error) { + domains.removeDNSRecords('test', DOMAIN_0.domain, 'A', ['1.2.3.4'], function (error) { expect(error).to.eql(null); expect(awsAnswerQueue.length).to.eql(0); @@ -525,10 +529,7 @@ describe('dns provider', function () { var _OriginalGCDNS; before(function (done) { - var domain = 'example.com'; - config.setFqdn(domain); - config.setZoneName(domain); - var dnsConfig = { + DOMAIN_0.config = { provider: 'gcdns', projectId: 'my-dns-proj', keyFilename: __dirname + '/syn-im-1ec6f9f870bf.json' @@ -566,12 +567,12 @@ describe('dns provider', function () { zone.deleteRecords = mockery(recordQueue || zoneQueue); return zone; } - HOSTED_ZONES = [fakeZone(domain), fakeZone('cloudron.us')]; + HOSTED_ZONES = [ fakeZone(DOMAIN_0.domain), fakeZone('cloudron.us') ]; _OriginalGCDNS = GCDNS.prototype.getZones; GCDNS.prototype.getZones = mockery(zoneQueue); - settings.setDnsConfig(dnsConfig, config.fqdn(), config.zoneName(), done); + domains.update(DOMAIN_0.domain, DOMAIN_0.config, null, done); }); after(function () { @@ -583,7 +584,8 @@ describe('dns provider', function () { zoneQueue.push([null, HOSTED_ZONES]); // getZone zoneQueue.push([null, [ ]]); // getRecords zoneQueue.push([null, {id: '1'}]); - subdomains.upsert('test', 'A', [ '1.2.3.4' ], function (error, result) { + + domains.upsertDNSRecords('test', DOMAIN_0.domain, 'A', [ '1.2.3.4' ], function (error, result) { expect(error).to.eql(null); expect(result).to.eql('1'); expect(zoneQueue.length).to.eql(0); @@ -597,7 +599,7 @@ describe('dns provider', function () { zoneQueue.push([null, [GCDNS().zone('test').record('A', {'name': 'test', data:['5.6.7.8'], ttl: 1})]]); zoneQueue.push([null, {id: '2'}]); - subdomains.upsert('test', 'A', [ '1.2.3.4' ], function (error, result) { + domains.upsertDNSRecords('test', DOMAIN_0.domain, 'A', [ '1.2.3.4' ], function (error, result) { expect(error).to.eql(null); expect(result).to.eql('2'); expect(zoneQueue.length).to.eql(0); @@ -611,7 +613,7 @@ describe('dns provider', function () { zoneQueue.push([null, [ ]]); // getRecords zoneQueue.push([null, {id: '3'}]); - subdomains.upsert('', 'TXT', [ 'first', 'second', 'third' ], function (error, result) { + domains.upsertDNSRecords('', config.fqdn(), 'TXT', [ 'first', 'second', 'third' ], function (error, result) { expect(error).to.eql(null); expect(result).to.eql('3'); expect(zoneQueue.length).to.eql(0); @@ -624,7 +626,7 @@ describe('dns provider', function () { zoneQueue.push([null, HOSTED_ZONES]); zoneQueue.push([null, [GCDNS().zone('test').record('A', {'name': 'test', data:['1.2.3.4', '5.6.7.8'], ttl: 1})]]); - subdomains.get('test', 'A', function (error, result) { + domains.getDNSRecords('test', DOMAIN_0.domain, 'A', function (error, result) { expect(error).to.eql(null); expect(result).to.be.an(Array); expect(result.length).to.eql(2); @@ -640,7 +642,7 @@ describe('dns provider', function () { zoneQueue.push([null, [GCDNS().zone('test').record('A', {'name': 'test', data:['5.6.7.8'], ttl: 1})]]); zoneQueue.push([null, {id: '5'}]); - subdomains.remove('test', 'A', ['1.2.3.4'], function (error) { + domains.removeDNSRecords('test', DOMAIN_0.domain, 'A', ['1.2.3.4'], function (error) { expect(error).to.eql(null); expect(zoneQueue.length).to.eql(0); diff --git a/src/test/eventlog-test.js b/src/test/eventlog-test.js index 0aa6ff698..aaae967a6 100644 --- a/src/test/eventlog-test.js +++ b/src/test/eventlog-test.js @@ -6,10 +6,11 @@ 'use strict'; -var database = require('../database.js'), - expect = require('expect.js'), +var async = require('async'), + database = require('../database.js'), eventlog = require('../eventlog.js'), - EventLogError = eventlog.EventLogError; + EventLogError = eventlog.EventLogError, + expect = require('expect.js'); function setup(done) { // ensure data/config/mount paths @@ -20,7 +21,10 @@ function setup(done) { } function cleanup(done) { - database._clear(done); + async.series([ + database._clear, + database.uninitialize + ], done); } describe('Eventlog', function () { diff --git a/src/test/groups-test.js b/src/test/groups-test.js index 052fc078d..e4be4f41e 100644 --- a/src/test/groups-test.js +++ b/src/test/groups-test.js @@ -7,6 +7,7 @@ 'use strict'; var async = require('async'), + config = require('../config.js'), constants = require('../constants.js'), database = require('../database.js'), DatabaseError = require('../databaseerror.js'), @@ -23,6 +24,12 @@ var GROUP0_NAME = 'administrators', var GROUP1_NAME = 'externs', group1Object; +const DOMAIN_0 = { + domain: 'example.com', + zoneName: 'example.com', + config: { provider: 'manual' } +}; + var USER_0 = { id: 'uuid213', username: 'uuid213', @@ -50,16 +57,20 @@ var USER_1 = { // this user has not signed up yet }; function setup(done) { - // ensure data/config/mount paths - database.initialize(function (error) { - expect(error).to.be(null); + config.setFqdn(DOMAIN_0.domain); - database._clear(done); - }); + // ensure data/config/mount paths + async.series([ + database.initialize, + database._clear + ], done); } function cleanup(done) { - database._clear(done); + async.series([ + database._clear, + database.uninitialize + ], done); } describe('Groups', function () { @@ -118,7 +129,7 @@ describe('Groups', function () { }); it('did create mailbox', function (done) { - mailboxdb.getGroup(GROUP0_NAME.toLowerCase(), function (error, mailbox) { + mailboxdb.getGroup(GROUP0_NAME.toLowerCase(), DOMAIN_0.domain, function (error, mailbox) { expect(error).to.be(null); expect(mailbox.ownerType).to.be(mailboxdb.TYPE_GROUP); done(); @@ -162,7 +173,7 @@ describe('Groups', function () { }); it('did delete mailbox', function (done) { - mailboxdb.getGroup(GROUP0_NAME.toLowerCase(), function (error) { + mailboxdb.getGroup(GROUP0_NAME.toLowerCase(), DOMAIN_0.domain, function (error) { expect(error.reason).to.be(DatabaseError.NOT_FOUND); done(); }); @@ -241,7 +252,7 @@ describe('Group membership', function () { }); it('can get list members', function (done) { - mailboxdb.getGroup(GROUP0_NAME.toLowerCase(), function (error, result) { + mailboxdb.getGroup(GROUP0_NAME.toLowerCase(), DOMAIN_0.domain, function (error, result) { expect(error).to.be(null); expect(result.name).to.be(GROUP0_NAME.toLowerCase()); expect(result.ownerType).to.be(mailboxdb.TYPE_GROUP); diff --git a/src/test/janitor-test.js b/src/test/janitor-test.js index f06155970..91fa2ffde 100644 --- a/src/test/janitor-test.js +++ b/src/test/janitor-test.js @@ -55,7 +55,10 @@ describe('janitor', function () { }); after(function (done) { - database._clear(done); + async.series([ + database._clear, + database.uninitialize + ], done); }); it('can cleanupTokens', function (done) { diff --git a/src/test/ldap-test.js b/src/test/ldap-test.js index 9075a5425..60dde96b0 100644 --- a/src/test/ldap-test.js +++ b/src/test/ldap-test.js @@ -1,6 +1,6 @@ /* jslint node:true */ /* global it:false */ -/* global describe:false */ +/* global xdescribe:false */ /* global before:false */ /* global after:false */ @@ -60,13 +60,14 @@ var APP_0 = { installationProgress: null, runState: appdb.RSTATE_RUNNING, location: 'some-location-0', + domain: 'example.com', manifest: { version: '0.1', dockerImage: 'docker/app0', healthCheckPath: '/', httpPort: 80, title: 'app0' }, httpPort: null, containerId: 'someContainerId', portBindings: { port: 5678 }, health: null, accessRestriction: null, - lastBackupId: null, + restoreConfig: null, oldConfig: null, memoryLimit: 4294967296 }; @@ -81,15 +82,18 @@ function startDockerProxy(interceptor, callback) { } function setup(done) { + config._reset(); + config.set('fqdn', 'example.com'); + async.series([ database.initialize.bind(null), database._clear.bind(null), ldapServer.start.bind(null), - appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0), + appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, APP_0.portBindings, APP_0), appdb.update.bind(null, APP_0.id, { containerId: APP_0.containerId }), appdb.setAddonConfig.bind(null, APP_0.id, 'sendmail', [{ name: 'MAIL_SMTP_PASSWORD', value : 'sendmailpassword' }]), appdb.setAddonConfig.bind(null, APP_0.id, 'recvmail', [{ name: 'MAIL_IMAP_PASSWORD', value : 'recvmailpassword' }]), - mailboxdb.add.bind(null, APP_0.location + '.app', APP_0.id, mailboxdb.TYPE_APP), + mailboxdb.add.bind(null, APP_0.location + '.app', APP_0.domain, APP_0.id, mailboxdb.TYPE_APP), function (callback) { user.createOwner(USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, AUDIT_SOURCE, function (error, result) { @@ -143,28 +147,28 @@ function setup(done) { if (req.method === 'GET' && req.url === '/networks/cloudron') { answer = { - Name: "cloudron", - Id: "f2de39df4171b0dc801e8002d1d999b77256983dfc63041c0f34030aa3977566", - Scope: "local", - Driver: "bridge", + Name: 'cloudron', + Id: 'f2de39df4171b0dc801e8002d1d999b77256983dfc63041c0f34030aa3977566', + Scope: 'local', + Driver: 'bridge', IPAM: { - Driver: "default", + Driver: 'default', Config: [{ - Subnet: "172.18.0.0/16" + Subnet: '172.18.0.0/16' }] }, - "Containers": { + 'Containers': { someOtherContainerId: { - "EndpointID": "ed2419a97c1d9954d05b46e462e7002ea552f216e9b136b80a7db8d98b442eda", - "MacAddress": "02:42:ac:11:00:02", - "IPv4Address": "127.0.0.2/16", - "IPv6Address": "" + 'EndpointID': 'ed2419a97c1d9954d05b46e462e7002ea552f216e9b136b80a7db8d98b442eda', + 'MacAddress': '02:42:ac:11:00:02', + 'IPv4Address': '127.0.0.2/16', + 'IPv6Address': '' }, someContainerId: { - "EndpointID": "ed2419a97c1d9954d05b46e462e7002ea552f216e9b136b80a7db8d98b442eda", - "MacAddress": "02:42:ac:11:00:02", - "IPv4Address": "127.0.0.1/16", - "IPv6Address": "" + 'EndpointID': 'ed2419a97c1d9954d05b46e462e7002ea552f216e9b136b80a7db8d98b442eda', + 'MacAddress': '02:42:ac:11:00:02', + 'IPv4Address': '127.0.0.1/16', + 'IPv6Address': '' } } }; @@ -181,7 +185,8 @@ function setup(done) { function cleanup(done) { async.series([ ldapServer.stop, - database._clear + database._clear, + database.uninitialize ], function () { dockerProxy.close(function () { done(); }); // some strange error }); @@ -199,7 +204,7 @@ describe('Ldap', function () { client.bind('cn=doesnotexist,ou=users,dc=cloudron', 'password', function (error) { expect(error).to.be.a(ldap.NoSuchObjectError); - done(); + client.unbind(done); }); }); @@ -208,7 +213,7 @@ describe('Ldap', function () { client.bind('cn=' + USER_0.id + ',ou=users,dc=cloudron', 'wrongpassword', function (error) { expect(error).to.be.a(ldap.InvalidCredentialsError); - done(); + client.unbind(done); }); }); @@ -217,7 +222,7 @@ describe('Ldap', function () { client.bind('cn=' + USER_0.id + ',ou=users,dc=cloudron', USER_0.password, function (error) { expect(error).to.be(null); - done(); + client.unbind(done); }); }); @@ -226,7 +231,7 @@ describe('Ldap', function () { client.bind('cn=' + USER_0.username + ',ou=users,dc=cloudron', USER_0.password, function (error) { expect(error).to.be(null); - done(); + client.unbind(done); }); }); @@ -235,7 +240,7 @@ describe('Ldap', function () { client.bind('cn=' + USER_0.email + ',ou=users,dc=cloudron', USER_0.password, function (error) { expect(error).to.be(null); - done(); + client.unbind(done); }); }); @@ -249,6 +254,8 @@ describe('Ldap', function () { client.bind('cn=' + USER_0.username.toLowerCase() + '@' + config.fqdn() + ',ou=users,dc=cloudron', USER_0.password, function (error) { expect(error).to.be(null); + client.unbind(); + settingsdb.set(settings.MAIL_CONFIG_KEY, JSON.stringify({ enabled: false }), done); }); }); @@ -259,19 +266,19 @@ describe('Ldap', function () { client.bind('mail=' + USER_0.username + ',ou=users,dc=cloudron', USER_0.password, function (error) { expect(error).to.be.a(ldap.NoSuchObjectError); - done(); + client.unbind(done); }); }); it('fails with accessRestriction denied', function (done) { var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') }); - appdb.update(APP_0.id, { accessRestriction: { users: [ USER_1.id ], groups: [] }}, function (error) { + appdb.update(APP_0.id, { accessRestriction: { users: [ USER_0.id ], groups: [] }}, function (error) { expect(error).to.eql(null); - client.bind('cn=' + USER_0.id + ',ou=users,dc=cloudron', USER_0.password, function (error) { + client.bind('cn=' + USER_1.id + ',ou=users,dc=cloudron', USER_1.password, function (error) { expect(error).to.be.a(ldap.NoSuchObjectError); - done(); + client.unbind(done); }); }); }); @@ -284,7 +291,7 @@ describe('Ldap', function () { client.bind('cn=' + USER_0.id + ',ou=users,dc=cloudron', USER_0.password, function (error) { expect(error).to.be(null); - done(); + client.unbind(done); }); }); }); @@ -304,7 +311,7 @@ describe('Ldap', function () { result.on('error', function (error) { expect(error).to.be.a(ldap.NoSuchObjectError); - done(); + client.unbind(done); }); result.on('end', function (result) { done(new Error('Should not succeed. Status ' + result.status)); @@ -335,7 +342,7 @@ describe('Ldap', function () { expect(entries[0].mail).to.equal(USER_0.email.toLowerCase()); expect(entries[1].username).to.equal(USER_1.username.toLowerCase()); expect(entries[1].mail).to.equal(USER_1.email.toLowerCase()); - done(); + client.unbind(done); }); }); }); @@ -364,7 +371,7 @@ describe('Ldap', function () { expect(entries[0].mail).to.equal(USER_0.email.toLowerCase()); expect(entries[1].username).to.equal(USER_1.username.toLowerCase()); expect(entries[1].mail).to.equal(USER_1.email.toLowerCase()); - done(); + client.unbind(done); }); }); }); @@ -400,6 +407,8 @@ describe('Ldap', function () { expect(entries[1].mailAlternateAddress).to.equal(USER_1.email.toLowerCase()); expect(entries[1].mail).to.equal(USER_1.username.toLowerCase() + '@' + config.fqdn()); + client.unbind(); + settingsdb.set(settings.MAIL_CONFIG_KEY, JSON.stringify({ enabled: false }), done); }); }); @@ -427,7 +436,7 @@ describe('Ldap', function () { entries.sort(function (a, b) { return a.username > b.username; }); expect(entries[0].username).to.equal(USER_0.username.toLowerCase()); expect(entries[1].username).to.equal(USER_1.username.toLowerCase()); - done(); + client.unbind(done); }); }); }); @@ -452,12 +461,12 @@ describe('Ldap', function () { expect(entries.length).to.equal(1); expect(entries[0].username).to.equal(USER_0.username.toLowerCase()); expect(entries[0].memberof.length).to.equal(2); - done(); + client.unbind(done); }); }); }); - it ('does not list users who have no access', function (done) { + it ('always lists admins', function (done) { appdb.update(APP_0.id, { accessRestriction: { users: [], groups: [] } }, function (error) { expect(error).to.be(null); @@ -477,7 +486,11 @@ describe('Ldap', function () { result.on('error', done); result.on('end', function (result) { expect(result.status).to.equal(0); - expect(entries.length).to.equal(0); + expect(entries.length).to.equal(1); + expect(entries[0].username).to.equal(USER_0.username.toLowerCase()); + expect(entries[0].memberof.length).to.equal(2); + + client.unbind(); appdb.update(APP_0.id, { accessRestriction: null }, done); }); @@ -511,6 +524,8 @@ describe('Ldap', function () { expect(entries[0].username).to.equal(USER_0.username.toLowerCase()); expect(entries[1].username).to.equal(USER_1.username.toLowerCase()); + client.unbind(); + appdb.update(APP_0.id, { accessRestriction: null }, done); }); }); @@ -549,7 +564,7 @@ describe('Ldap', function () { expect(entries[1].cn).to.equal('admins'); // if only one entry, the array becomes a string :-/ expect(entries[1].memberuid).to.equal(USER_0.id); - done(); + client.unbind(done); }); }); }); @@ -580,7 +595,7 @@ describe('Ldap', function () { expect(entries[1].cn).to.equal('admins'); // if only one entry, the array becomes a string :-/ expect(entries[1].memberuid).to.equal(USER_0.id); - done(); + client.unbind(done); }); }); }); @@ -605,7 +620,7 @@ describe('Ldap', function () { expect(entries.length).to.equal(1); expect(entries[0].cn).to.equal('users'); expect(entries[0].memberuid.length).to.equal(3); - done(); + client.unbind(done); }); }); }); @@ -639,6 +654,8 @@ describe('Ldap', function () { // if only one entry, the array becomes a string :-/ expect(entries[1].memberuid).to.equal(USER_0.id); + client.unbind(); + appdb.update(APP_0.id, { accessRestriction: null }, done); }); }); @@ -676,7 +693,7 @@ describe('Ldap', function () { expect(entries[1].cn).to.equal('admins'); // if only one entry, the array becomes a string :-/ expect(entries[1].memberuid).to.equal(USER_0.id); - done(); + client.unbind(done); }); }); }); @@ -690,6 +707,12 @@ describe('Ldap', function () { paged: true }; + function done(error, entries) { + client.unbind(function () { + callback(error, entries); + }); + } + client.search(dn, opts, function (error, result) { expect(error).to.be(null); expect(result).to.be.an(EventEmitter); @@ -697,10 +720,10 @@ describe('Ldap', function () { var entries = []; result.on('searchEntry', function (entry) { entries.push(entry.object); }); - result.on('error', callback); + result.on('error', done); result.on('end', function (result) { expect(result.status).to.equal(0); - callback(null, entries); + done(null, entries); }); }); } @@ -725,7 +748,7 @@ describe('Ldap', function () { }); it('cannot get alias as a mailbox', function (done) { - ldapSearch('cn=' + USER_0_ALIAS + ',ou=mailboxes,dc=cloudron', 'objectclass=mailbox', function (error, entries) { + ldapSearch('cn=' + USER_0_ALIAS + ',ou=mailboxes,dc=cloudron', 'objectclass=mailbox', function (error) { expect(error).to.be.a(ldap.NoSuchObjectError); done(); }); @@ -751,7 +774,7 @@ describe('Ldap', function () { }); it('cannot get mailbox as alias', function (done) { - ldapSearch('cn=' + USER_0.username + ',ou=mailaliases,dc=cloudron', 'objectclass=nismailalias', function (error, entries) { + ldapSearch('cn=' + USER_0.username + ',ou=mailaliases,dc=cloudron', 'objectclass=nismailalias', function (error) { expect(error).to.be.a(ldap.NoSuchObjectError); done(); }); @@ -809,7 +832,7 @@ describe('Ldap', function () { client.bind('cn=' + USER_0.username + ',ou=sendmail,dc=cloudron', USER_0.password + 'nope', function (error) { expect(error).to.be.a(ldap.InvalidCredentialsError); - done(); + client.unbind(done); }); }); @@ -817,6 +840,7 @@ describe('Ldap', function () { var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') }); client.bind('cn=' + USER_0.username + ',ou=sendmail,dc=cloudron', USER_0.password, function (error) { + client.unbind(); done(error); }); }); @@ -831,6 +855,8 @@ describe('Ldap', function () { client.bind('cn=' + USER_0.username + '@' + config.fqdn() + ',ou=sendmail,dc=cloudron', USER_0.password, function (error) { expect(error).not.to.be.ok(); + client.unbind(); + settingsdb.set(settings.MAIL_CONFIG_KEY, JSON.stringify({ enabled: false }), done); }); }); @@ -843,7 +869,7 @@ describe('Ldap', function () { client.bind('cn=hacker.app,ou=sendmail,dc=cloudron', 'nope', function (error) { expect(error).to.be.a(ldap.NoSuchObjectError); - done(); + client.unbind(done); }); }); @@ -852,7 +878,7 @@ describe('Ldap', function () { client.bind('cn=' + APP_0.location + '.app,ou=sendmail,dc=cloudron', 'nope', function (error) { expect(error).to.be.a(ldap.InvalidCredentialsError); - done(); + client.unbind(done); }); }); @@ -860,6 +886,7 @@ describe('Ldap', function () { var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') }); client.bind('cn=' + APP_0.location + '.app,ou=sendmail,dc=cloudron', 'sendmailpassword', function (error) { + client.unbind(); done(error); }); }); @@ -871,7 +898,7 @@ describe('Ldap', function () { client.bind('cn=' + USER_0.username + ',ou=recvmail,dc=cloudron', USER_0.password + 'nope', function (error) { expect(error).to.be.a(ldap.InvalidCredentialsError); - done(); + client.unbind(done); }); }); @@ -879,6 +906,8 @@ describe('Ldap', function () { var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') }); client.bind('cn=' + USER_0.username + ',ou=recvmail,dc=cloudron', USER_0.password, function (error) { + client.unbind(); + done(error); }); }); @@ -893,6 +922,8 @@ describe('Ldap', function () { client.bind('cn=' + USER_0.username + '@' + config.fqdn() + ',ou=recvmail,dc=cloudron', USER_0.password, function (error) { expect(error).not.to.be.ok(); + client.unbind(); + settingsdb.set(settings.MAIL_CONFIG_KEY, JSON.stringify({ enabled: false }), done); }); }); @@ -905,7 +936,7 @@ describe('Ldap', function () { client.bind('cn=hacker.app,ou=recvmail,dc=cloudron', 'nope', function (error) { expect(error).to.be.a(ldap.NoSuchObjectError); - done(); + client.unbind(done); }); }); @@ -914,7 +945,7 @@ describe('Ldap', function () { client.bind('cn=' + APP_0.location + '.app,ou=recvmail,dc=cloudron', 'nope', function (error) { expect(error).to.be.a(ldap.InvalidCredentialsError); - done(); + client.unbind(done); }); }); @@ -922,9 +953,10 @@ describe('Ldap', function () { var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') }); client.bind('cn=' + APP_0.location + '.app,ou=recvmail,dc=cloudron', 'recvmailpassword', function (error) { + client.unbind(); + done(error); }); }); }); - }); diff --git a/src/test/server-test.js b/src/test/server-test.js index 922e2b3b2..9fa2e53a0 100644 --- a/src/test/server-test.js +++ b/src/test/server-test.js @@ -24,7 +24,9 @@ describe('Server', function () { this.timeout(5000); before(function () { - config.set('version', '0.5.0'); + config._reset(); + config.setFqdn('example-server-test.com'); + config.set('provider', 'notcaas'); // otherwise, cron sets a caas timer for heartbeat causing the test to not quit }); after(cleanup); @@ -91,22 +93,22 @@ describe('Server', function () { superagent.get(SERVER_URL + '/api/v1/cloudron/status', function (err, res) { expect(err).to.not.be.ok(); expect(res.statusCode).to.equal(200); - expect(res.body.version).to.equal('0.5.0'); + expect(res.body.version).to.equal('1.1.1-test'); done(); }); }); it('status route is GET', function (done) { superagent.post(SERVER_URL + '/api/v1/cloudron/status') - .end(function (err, res) { - expect(res.statusCode).to.equal(404); + .end(function (err, res) { + expect(res.statusCode).to.equal(404); - superagent.get(SERVER_URL + '/api/v1/cloudron/status') - .end(function (err, res) { - expect(res.statusCode).to.equal(200); - done(); + superagent.get(SERVER_URL + '/api/v1/cloudron/status') + .end(function (err, res) { + expect(res.statusCode).to.equal(200); + done(); + }); }); - }); }); }); @@ -227,21 +229,21 @@ describe('Server', function () { .set('Access-Control-Request-Headers', 'accept, origin, x-superagented-with') .set('Origin', 'http://localhost') .end(function (error, res) { - expect(res.headers['access-control-allow-methods']).to.be('GET, PUT, DELETE, POST, OPTIONS'); - expect(res.headers['access-control-allow-credentials']).to.be('false'); - expect(res.headers['access-control-allow-headers']).to.be('accept, origin, x-superagented-with'); // mirrored from superagent - expect(res.headers['access-control-allow-origin']).to.be('http://localhost'); // mirrors from superagent - done(); - }); + expect(res.headers['access-control-allow-methods']).to.be('GET, PUT, DELETE, POST, OPTIONS'); + expect(res.headers['access-control-allow-credentials']).to.be('false'); + expect(res.headers['access-control-allow-headers']).to.be('accept, origin, x-superagented-with'); // mirrored from superagent + expect(res.headers['access-control-allow-origin']).to.be('http://localhost'); // mirrors from superagent + done(); + }); }); it('does not crash for malformed origin', function (done) { superagent('OPTIONS', SERVER_URL + '/api/v1/cloudron/status') .set('Origin', 'foobar') .end(function (error, res) { - expect(res.statusCode).to.be(405); - done(); - }); + expect(res.statusCode).to.be(405); + done(); + }); }); after(function (done) { diff --git a/src/test/settings-test.js b/src/test/settings-test.js index 28a3e1114..acb6467ce 100644 --- a/src/test/settings-test.js +++ b/src/test/settings-test.js @@ -19,6 +19,8 @@ var async = require('async'), settingsdb = require('../settingsdb.js'); function setup(done) { + config._reset(); + config.set('fqdn', 'example.com'); config.set('provider', 'caas'); nock.cleanAll(); @@ -45,7 +47,8 @@ function cleanup(done) { async.series([ settings.uninitialize, - database._clear + database._clear, + database.uninitialize ], done); } @@ -86,47 +89,6 @@ describe('Settings', function () { }); }); - it('can get default developer mode', function (done) { - settings.getDeveloperMode(function (error, enabled) { - expect(error).to.be(null); - expect(enabled).to.equal(true); - done(); - }); - }); - - it('can set developer mode', function (done) { - settings.setDeveloperMode(true, function (error) { - expect(error).to.be(null); - done(); - }); - }); - - it('can get developer mode', function (done) { - settings.getDeveloperMode(function (error, enabled) { - expect(error).to.be(null); - expect(enabled).to.equal(true); - done(); - }); - }); - - it('can set dns config', function (done) { - settings.setDnsConfig({ provider: 'route53', accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey' }, config.fqdn(), config.zoneName(), function (error) { - expect(error).to.be(null); - done(); - }); - }); - - it('can get dns config', function (done) { - settings.getDnsConfig(function (error, dnsConfig) { - expect(error).to.be(null); - expect(dnsConfig.provider).to.be('route53'); - expect(dnsConfig.accessKeyId).to.be('accessKeyId'); - expect(dnsConfig.secretAccessKey).to.be('secretAccessKey'); - expect(dnsConfig.region).to.be('us-east-1'); - done(); - }); - }); - it('can set tls config', function (done) { settings.setTlsConfig({ provider: 'caas' }, function (error) { expect(error).to.be(null); @@ -147,7 +109,7 @@ describe('Settings', function () { .post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=TOKEN') .reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } }); - settings.setBackupConfig({ provider: 'caas', token: 'TOKEN', format: 'tgz', prefix: 'boxid', bucket: 'bucket' }, function (error) { + settings.setBackupConfig({ provider: 'caas', fqdn: config.fqdn(), token: 'TOKEN', format: 'tgz', prefix: 'boxid', bucket: 'bucket' }, function (error) { expect(error).to.be(null); done(); }); diff --git a/src/test/updatechecker-test.js b/src/test/updatechecker-test.js index 5c5ff2814..36f970e87 100644 --- a/src/test/updatechecker-test.js +++ b/src/test/updatechecker-test.js @@ -28,6 +28,12 @@ var USER_0 = { displayName: 'User 0' }; +const DOMAIN_0 = { + domain: 'example.com', + zoneName: 'example.com', + config: { provider: 'manual' } +}; + var AUDIT_SOURCE = { ip: '1.2.3.4' }; @@ -47,14 +53,15 @@ function cleanup(done) { async.series([ settings.uninitialize, - database._clear + database._clear, + database.uninitialize ], done); } describe('updatechecker - box - manual (email)', function () { before(function (done) { config._reset(); - config.set('version', '1.0.0'); + config.setFqdn(DOMAIN_0.domain); config.set('apiServerOrigin', 'http://localhost:4444'); config.set('provider', 'notcaas'); safe.fs.unlinkSync(paths.UPDATE_CHECKER_FILE); @@ -66,8 +73,7 @@ describe('updatechecker - box - manual (email)', function () { user.createOwner.bind(null, USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, AUDIT_SOURCE), settings.setAutoupdatePattern.bind(null, constants.AUTOUPDATE_PATTERN_NEVER), settingsdb.set.bind(null, settings.APPSTORE_CONFIG_KEY, JSON.stringify({ userId: 'uid', cloudronId: 'cid', token: 'token' })), - mailer._clearMailQueue, - mailer.start + mailer._clearMailQueue ], done); }); @@ -157,14 +163,14 @@ describe('updatechecker - box - manual (email)', function () { describe('updatechecker - box - automatic (no email)', function () { before(function (done) { - config.set('version', '1.0.0'); + config.setFqdn(DOMAIN_0.domain); config.set('apiServerOrigin', 'http://localhost:4444'); config.set('provider', 'notcaas'); + async.series([ database.initialize, settings.initialize, mailer._clearMailQueue, - mailer.start, user.createOwner.bind(null, USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, AUDIT_SOURCE), settingsdb.set.bind(null, settings.APPSTORE_CONFIG_KEY, JSON.stringify({ userId: 'uid', cloudronId: 'cid', token: 'token' })) ], done); @@ -198,9 +204,11 @@ describe('updatechecker - box - automatic (no email)', function () { describe('updatechecker - box - automatic free (email)', function () { before(function (done) { + config.setFqdn(DOMAIN_0.domain); config.set('version', '1.0.0'); config.set('apiServerOrigin', 'http://localhost:4444'); config.set('provider', 'notcaas'); + async.series([ database.initialize, settings.initialize, @@ -244,6 +252,7 @@ describe('updatechecker - app - manual (email)', function () { installationProgress: null, runState: null, location: 'some-location-0', + domain: DOMAIN_0.domain, manifest: { version: '1.0.0', dockerImage: 'docker/app0', healthCheckPath: '/', httpPort: 80, title: 'app0', tcpPorts: { @@ -262,6 +271,7 @@ describe('updatechecker - app - manual (email)', function () { }; before(function (done) { + config.setFqdn(DOMAIN_0.domain); config.set('version', '1.0.0'); config.set('apiServerOrigin', 'http://localhost:4444'); config.set('provider', 'notcaas'); @@ -271,7 +281,7 @@ describe('updatechecker - app - manual (email)', function () { database._clear, settings.initialize, mailer._clearMailQueue, - appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0), + appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, APP_0.portBindings, APP_0), user.createOwner.bind(null, USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, AUDIT_SOURCE), settings.setAutoupdatePattern.bind(null, constants.AUTOUPDATE_PATTERN_NEVER), settingsdb.set.bind(null, settings.APPSTORE_CONFIG_KEY, JSON.stringify({ userId: 'uid', cloudronId: 'cid', token: 'token' })) @@ -356,6 +366,7 @@ describe('updatechecker - app - automatic (no email)', function () { installationProgress: null, runState: null, location: 'some-location-0', + domain: DOMAIN_0.domain, manifest: { version: '1.0.0', dockerImage: 'docker/app0', healthCheckPath: '/', httpPort: 80, title: 'app0', tcpPorts: { @@ -374,6 +385,7 @@ describe('updatechecker - app - automatic (no email)', function () { }; before(function (done) { + config.setFqdn(DOMAIN_0.domain); config.set('version', '1.0.0'); config.set('apiServerOrigin', 'http://localhost:4444'); config.set('provider', 'notcaas'); @@ -383,7 +395,7 @@ describe('updatechecker - app - automatic (no email)', function () { database._clear, settings.initialize, mailer._clearMailQueue, - appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0), + appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, APP_0.portBindings, APP_0), user.createOwner.bind(null, USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, AUDIT_SOURCE), settingsdb.set.bind(null, settings.APPSTORE_CONFIG_KEY, JSON.stringify({ userId: 'uid', cloudronId: 'cid', token: 'token' })) ], done); @@ -417,6 +429,7 @@ describe('updatechecker - app - automatic free (email)', function () { installationProgress: null, runState: null, location: 'some-location-0', + domain: DOMAIN_0.domain, manifest: { version: '1.0.0', dockerImage: 'docker/app0', healthCheckPath: '/', httpPort: 80, title: 'app0', tcpPorts: { @@ -435,6 +448,7 @@ describe('updatechecker - app - automatic free (email)', function () { }; before(function (done) { + config.setFqdn(DOMAIN_0.domain); config.set('version', '1.0.0'); config.set('apiServerOrigin', 'http://localhost:4444'); config.set('provider', 'notcaas'); @@ -444,7 +458,7 @@ describe('updatechecker - app - automatic free (email)', function () { database._clear, settings.initialize, mailer._clearMailQueue, - appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0), + appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, APP_0.portBindings, APP_0), user.createOwner.bind(null, USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, AUDIT_SOURCE), settingsdb.set.bind(null, settings.APPSTORE_CONFIG_KEY, JSON.stringify({ userId: 'uid', cloudronId: 'cid', token: 'token' })) ], done); diff --git a/src/test/user-test.js b/src/test/user-test.js index 37f6643d6..a7f7a3b2f 100644 --- a/src/test/user-test.js +++ b/src/test/user-test.js @@ -39,6 +39,12 @@ var EMAIL_1 = 'second@user.com'; var PASSWORD_1 = 'Sup2345$@strong'; var DISPLAY_NAME_1 = 'Second User'; +const DOMAIN_0 = { + domain: 'example.com', + zoneName: 'example.com', + config: { provider: 'manual' } +}; + function cleanupUsers(done) { async.series([ groupdb._clear, @@ -62,6 +68,9 @@ function createOwner(done) { } function setup(done) { + config._reset(); + config.setFqdn(DOMAIN_0.domain); + async.series([ database.initialize, database._clear, @@ -72,7 +81,10 @@ function setup(done) { function cleanup(done) { mailer._clearMailQueue(); - database._clear(done); + async.series([ + database._clear, + database.uninitialize + ], done); } function checkMails(number, options, callback) { @@ -205,7 +217,7 @@ describe('User', function () { }); it('did create mailbox', function (done) { - mailboxdb.getMailbox(USERNAME.toLowerCase(), function (error, mailbox) { + mailboxdb.getMailbox(USERNAME.toLowerCase(), DOMAIN_0.domain, function (error, mailbox) { expect(error).to.be(null); expect(mailbox.ownerType).to.be(mailboxdb.TYPE_USER); done(); @@ -712,10 +724,10 @@ describe('User', function () { }); it('updated the mailbox', function (done) { - mailboxdb.getMailbox(USERNAME, function (error) { + mailboxdb.getMailbox(USERNAME, DOMAIN_0.domain, function (error) { expect(error.reason).to.be(DatabaseError.NOT_FOUND); - mailboxdb.getMailbox(USERNAME_NEW.toLowerCase(), function (error, mailbox) { + mailboxdb.getMailbox(USERNAME_NEW.toLowerCase(), DOMAIN_0.domain, function (error, mailbox) { expect(error).to.be(null); expect(mailbox.ownerId).to.be(userObject.id); done(); @@ -998,7 +1010,7 @@ describe('User', function () { user.setAliases(userObject.id, [ 'everything', 'is', 'awesome' ], function (error) { expect(error).to.be(null); - mailboxdb.getAliasesForName(USERNAME.toLowerCase(), function (error, results) { + mailboxdb.getAliasesForName(USERNAME.toLowerCase(), DOMAIN_0.domain, function (error, results) { expect(error).to.be(null); expect(results.length).to.be(3); done(); @@ -1014,10 +1026,10 @@ describe('User', function () { }); it('did delete mailbox and aliases', function (done) { - mailboxdb.getMailbox(userObject.username.toLowerCase(), function (error, mailbox) { + mailboxdb.getMailbox(userObject.username.toLowerCase(), DOMAIN_0.domain, function (error, mailbox) { expect(error.reason).to.be(DatabaseError.NOT_FOUND); - mailboxdb.getAliasesForName(USERNAME.toLowerCase(), function (error, results) { + mailboxdb.getAliasesForName(USERNAME.toLowerCase(), DOMAIN_0.domain, function (error, results) { expect(error).to.be(null); expect(results.length).to.be(0); done(); diff --git a/src/user.js b/src/user.js index 6089a13e8..2d66f48ee 100644 --- a/src/user.js +++ b/src/user.js @@ -32,6 +32,7 @@ var assert = require('assert'), debug = require('debug')('box:user'), DatabaseError = require('./databaseerror.js'), eventlog = require('./eventlog.js'), + groupdb = require('./groupdb.js'), groups = require('./groups.js'), GroupError = groups.GroupError, hat = require('hat'), @@ -551,13 +552,19 @@ function createOwner(username, password, email, displayName, auditSource, callba if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error)); if (count !== 0) return callback(new UserError(UserError.ALREADY_EXISTS, 'Owner already exists')); - createUser(username, password, email, displayName, auditSource, { owner: true }, function (error, user) { - if (error) return callback(error); + // have to provide the group id explicitly so using db layer directly + groupdb.add(constants.ADMIN_GROUP_ID, constants.ADMIN_GROUP_NAME, function (error) { + // we proceed if it already exists so we can re-create the owner if need be + if (error && error.reason !== DatabaseError.ALREADY_EXISTS) return callback(new UserError(UserError.INTERNAL_ERROR, error)); - groups.addMember(constants.ADMIN_GROUP_ID, user.id, function (error) { - if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error)); + createUser(username, password, email, displayName, auditSource, { owner: true }, function (error, user) { + if (error) return callback(error); - callback(null, user); + groups.addMember(constants.ADMIN_GROUP_ID, user.id, function (error) { + if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error)); + + callback(null, user); + }); }); }); }); @@ -623,7 +630,7 @@ function setAliases(userId, aliases, callback) { if (!user.username) return new UserError(UserError.BAD_FIELD, 'Username must be set before settings aliases'); - mailboxdb.setAliasesForName(user.username, aliases, function (error) { + mailboxdb.setAliasesForName(user.username, config.fqdn(), aliases, function (error) { if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new UserError(UserError.ALREADY_EXISTS, error.message)); if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new UserError(UserError.NOT_FOUND)); if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error)); @@ -643,7 +650,7 @@ function getAliases(userId, callback) { if (!user.username) return callback(null, [ ]); - mailboxdb.getAliasesForName(user.username, function (error, aliases) { + mailboxdb.getAliasesForName(user.username, config.fqdn(), function (error, aliases) { if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new UserError(UserError.NOT_FOUND)); if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error)); diff --git a/src/userdb.js b/src/userdb.js index 7e0ad714e..19d1702de 100644 --- a/src/userdb.js +++ b/src/userdb.js @@ -19,6 +19,7 @@ exports = module.exports = { var assert = require('assert'), constants = require('./constants.js'), + config = require('./config.js'), database = require('./database.js'), debug = require('debug')('box:userdb'), DatabaseError = require('./databaseerror'), @@ -145,8 +146,8 @@ function add(userId, user, callback) { }); if (user.username) { queries.push({ - query: 'INSERT INTO mailboxes (name, ownerId, ownerType) VALUES (?, ?, ?)', - args: [ user.username, userId, mailboxdb.TYPE_USER ] + query: 'INSERT INTO mailboxes (name, domain, ownerId, ownerType) VALUES (?, ?, ?, ?)', + args: [ user.username, config.fqdn(), userId, mailboxdb.TYPE_USER ] }); } @@ -239,8 +240,8 @@ function update(userId, user, callback) { queries.push({ query: 'DELETE FROM mailboxes WHERE ownerId = ? AND aliasTarget IS NULL', args: [ userId ] }); // add new mailbox queries.push({ - query: 'INSERT INTO mailboxes (name, ownerId, ownerType) VALUES (?, ?, ?)', - args: [ user.username, userId, mailboxdb.TYPE_USER ] + query: 'INSERT INTO mailboxes (name, domain, ownerId, ownerType) VALUES (?, ?, ?, ?)', + args: [ user.username, config.fqdn(), userId, mailboxdb.TYPE_USER ] }); } diff --git a/webadmin/src/index.html b/webadmin/src/index.html index 01b3a0387..0baf2fc77 100644 --- a/webadmin/src/index.html +++ b/webadmin/src/index.html @@ -215,7 +215,7 @@
  • Account
  • Activity
  • API Access
  • -
  • Domain & Certs
  • +
  • Domains
  • Email
  • Graphs
  • Settings
  • diff --git a/webadmin/src/js/client.js b/webadmin/src/js/client.js index e27efd20d..8c8bbd4c4 100644 --- a/webadmin/src/js/client.js +++ b/webadmin/src/js/client.js @@ -306,6 +306,7 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification', var data = { appStoreId: id + '@' + manifest.version, location: config.location, + domain: config.domain, portBindings: config.portBindings, accessRestriction: config.accessRestriction, cert: config.cert, @@ -349,6 +350,7 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification', var data = { appId: id, location: config.location, + domain: config.domain, portBindings: config.portBindings, accessRestriction: config.accessRestriction, cert: config.cert, @@ -466,20 +468,6 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification', }).error(defaultErrorHandler(callback)); }; - Client.prototype.setDnsConfig = function (dnsConfig, callback) { - post('/api/v1/settings/dns_config', dnsConfig).success(function(data, status) { - if (status !== 200) return callback(new ClientError(status, data)); - callback(null); - }).error(defaultErrorHandler(callback)); - }; - - Client.prototype.getDnsConfig = function (callback) { - get('/api/v1/settings/dns_config').success(function(data, status) { - if (status !== 200) return callback(new ClientError(status, data)); - callback(null, data); - }).error(defaultErrorHandler(callback)); - }; - Client.prototype.setAutoupdatePattern = function (pattern, callback) { post('/api/v1/settings/autoupdate_pattern', { pattern: pattern }).success(function(data, status) { if (status !== 200) return callback(new ClientError(status, data)); @@ -587,6 +575,19 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification', }).error(defaultErrorHandler(callback)); }; + Client.prototype.restore = function (backupConfig, backupId, version, callback) { + var data = { + backupConfig: backupConfig, + backupId: backupId, + version: version + }; + + post('/api/v1/cloudron/restore', data).success(function(data, status) { + if (status !== 200) return callback(new ClientError(status)); + callback(null); + }).error(defaultErrorHandler(callback)); + }; + Client.prototype.getEventLogs = function (action, search, page, perPage, callback) { var config = { params: { @@ -1160,6 +1161,64 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification', }).error(defaultErrorHandler(callback)); }; + // Domains + Client.prototype.getDomains = function (callback) { + get('/api/v1/domains').success(function (data, status) { + if (status !== 200 || typeof data !== 'object') return callback(new ClientError(status, data)); + callback(null, data.domains); + }).error(defaultErrorHandler(callback)); + }; + + Client.prototype.getDomain = function (domain, callback) { + get('/api/v1/domains/' + domain).success(function (data, status) { + if (status !== 200 || typeof data !== 'object') return callback(new ClientError(status, data)); + callback(null, data); + }).error(defaultErrorHandler(callback)); + }; + + Client.prototype.addDomain = function (domain, config, fallbackCertificate, callback) { + var data = { + domain: domain, + config: config + }; + + if (fallbackCertificate) data.fallbackCertificate = fallbackCertificate; + + post('/api/v1/domains', data).success(function (data, status) { + if (status !== 201 || typeof data !== 'object') return callback(new ClientError(status, data)); + callback(null, data); + }).error(defaultErrorHandler(callback)); + }; + + Client.prototype.updateDomain = function (domain, config, fallbackCertificate, callback) { + var data = { + config: config + }; + + if (fallbackCertificate) data.fallbackCertificate = fallbackCertificate; + + put('/api/v1/domains/' + domain, data).success(function (data, status) { + if (status !== 204) return callback(new ClientError(status, data)); + callback(null); + }).error(defaultErrorHandler(callback)); + }; + + Client.prototype.removeDomain = function (domain, password, callback) { + var config = { + data: { + password: password + }, + headers: { + 'Content-Type': 'application/json' + } + }; + + del('/api/v1/domains/' + domain, config).success(function (data, status) { + if (status !== 204) return callback(new ClientError(status, data)); + callback(null); + }).error(defaultErrorHandler(callback)); + }; + client = new Client(); return client; }]); diff --git a/webadmin/src/js/index.js b/webadmin/src/js/index.js index 6e9d7f5b3..dd1470158 100644 --- a/webadmin/src/js/index.js +++ b/webadmin/src/js/index.js @@ -55,9 +55,9 @@ app.config(['$routeProvider', function ($routeProvider) { }).when('/debug', { controller: 'DebugController', templateUrl: 'views/debug.html' - }).when('/certs', { - controller: 'CertsController', - templateUrl: 'views/certs.html' + }).when('/domains', { + controller: 'DomainsController', + templateUrl: 'views/domains.html' }).when('/email', { controller: 'EmailController', templateUrl: 'views/email.html' diff --git a/webadmin/src/js/main.js b/webadmin/src/js/main.js index 199310138..5d66ba357 100644 --- a/webadmin/src/js/main.js +++ b/webadmin/src/js/main.js @@ -114,47 +114,41 @@ angular.module('Application').controller('MainController', ['$scope', '$route', }; function runConfigurationChecks() { - Client.getDnsConfig(function (error, result) { + var actionScope; + + // warn user if dns config is not working (the 'configuring' flag detects if configureWebadmin is 'active') + if (!$scope.status.webadminStatus.configuring && !$scope.status.webadminStatus.dns) { + actionScope = $scope.$new(true); + actionScope.action = '/#/certs'; + Client.notify('Invalid Domain Config', 'Unable to update DNS. Click here to update it.', true, 'error', actionScope); + } + + Client.getBackupConfig(function (error, backupConfig) { if (error) return console.error(error); - var actionScope; + if (backupConfig.provider === 'noop') { + var actionScope = $scope.$new(true); + actionScope.action = '/#/settings'; - // warn user if dns config is not working (the 'configuring' flag detects if configureWebadmin is 'active') - if (!$scope.status.webadminStatus.configuring && !$scope.status.webadminStatus.dns) { - actionScope = $scope.$new(true); - actionScope.action = '/#/certs'; - Client.notify('Invalid Domain Config', 'Unable to update DNS. Click here to update it.', true, 'error', actionScope); + Client.notify('Backup Configuration', 'Cloudron backups are disabled. Ensure the server is backed up using alternate means.', false, 'info', actionScope); } - if (result.provider === 'caas') return; - - Client.getBackupConfig(function (error, backupConfig) { + Client.getMailRelay(function (error, result) { if (error) return console.error(error); - if (backupConfig.provider === 'noop') { - var actionScope = $scope.$new(true); - actionScope.action = '/#/settings'; + // the email status checks are currently only useful when using Cloudron itself for relaying + if (result.provider !== 'cloudron-smtp') return; - Client.notify('Backup Configuration', 'Cloudron backups are disabled. Ensure the server is backed up using alternate means.', false, 'info', actionScope); - } - - Client.getMailRelay(function (error, result) { + // Check if all email DNS records are set up properly only for non external DNS API + Client.getEmailStatus(function (error, result) { if (error) return console.error(error); - // the email status checks are currently only useful when using Cloudron itself for relaying - if (result.provider !== 'cloudron-smtp') return; + if (!result.dns.spf.status || !result.dns.dkim.status || !result.dns.ptr.status || !result.relay.status) { + var actionScope = $scope.$new(true); + actionScope.action = '/#/email'; - // Check if all email DNS records are set up properly only for non external DNS API - Client.getEmailStatus(function (error, result) { - if (error) return console.error(error); - - if (!result.dns.spf.status || !result.dns.dkim.status || !result.dns.ptr.status || !result.relay.status) { - var actionScope = $scope.$new(true); - actionScope.action = '/#/email'; - - Client.notify('DNS Configuration', 'Please setup all required DNS records to guarantee correct mail delivery', false, 'info', actionScope); - } - }); + Client.notify('DNS Configuration', 'Please setup all required DNS records to guarantee correct mail delivery', false, 'info', actionScope); + } }); }); }); diff --git a/webadmin/src/js/restore.js b/webadmin/src/js/restore.js new file mode 100644 index 000000000..5fe2fe541 --- /dev/null +++ b/webadmin/src/js/restore.js @@ -0,0 +1,184 @@ +'use strict'; + +/* global tld */ + +// create main application module +var app = angular.module('Application', ['angular-md5', 'ui-notification']); + +app.filter('zoneName', function () { + return function (domain) { + return tld.getDomain(domain); + }; +}); + +app.controller('RestoreController', ['$scope', '$http', 'Client', function ($scope, $http, Client) { + var search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {}); + + $scope.busy = false; + $scope.error = {}; + $scope.provider = ''; + $scope.bucket = ''; + $scope.prefix = ''; + $scope.accessKeyId = ''; + $scope.secretAccessKey = ''; + $scope.region = ''; + $scope.endpoint = ''; + $scope.backupFolder = ''; + $scope.backupId = ''; + $scope.instanceId = ''; + $scope.acceptSelfSignedCerts = false; + $scope.format = 'tgz'; + + // List is from http://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region + $scope.s3Regions = [ + { name: 'Asia Pacific (Mumbai)', value: 'ap-south-1' }, + { name: 'Asia Pacific (Seoul)', value: 'ap-northeast-2' }, + { name: 'Asia Pacific (Singapore)', value: 'ap-southeast-1' }, + { name: 'Asia Pacific (Sydney)', value: 'ap-southeast-2' }, + { name: 'Asia Pacific (Tokyo)', value: 'ap-northeast-1' }, + { name: 'Canada (Central)', value: 'ca-central-1' }, + { name: 'EU (Frankfurt)', value: 'eu-central-1' }, + { name: 'EU (Ireland)', value: 'eu-west-1' }, + { name: 'EU (London)', value: 'eu-west-2' }, + { name: 'South America (São Paulo)', value: 'sa-east-1' }, + { name: 'US East (N. Virginia)', value: 'us-east-1' }, + { name: 'US East (Ohio)', value: 'us-east-2' }, + { name: 'US West (N. California)', value: 'us-west-1' }, + { name: 'US West (Oregon)', value: 'us-west-2' }, + ]; + + $scope.doSpacesRegions = [ + { name: 'AMS3', value: 'https://ams3.digitaloceanspaces.com' }, + { name: 'NYC3', value: 'https://nyc3.digitaloceanspaces.com' } + ]; + + $scope.storageProvider = [ + { name: 'Amazon S3', value: 's3' }, + { name: 'DigitalOcean Spaces', value: 'digitalocean-spaces' }, + { name: 'Exoscale SOS', value: 'exoscale-sos' }, + { name: 'Filesystem', value: 'filesystem' }, + { name: 'Minio', value: 'minio' }, + { name: 'S3 API Compatible (v4)', value: 's3-v4-compat' }, + ]; + + $scope.formats = [ + { name: 'Tarball (zipped)', value: 'tgz' }, + { name: 'rsync', value: 'rsync' } + ]; + + $scope.s3like = function (provider) { + return provider === 's3' || provider === 'minio' || provider === 's3-v4-compat' || provider === 'exoscale-sos' || provider === 'digitalocean-spaces'; + }; + + $scope.restore = function () { + $scope.error = {}; + $scope.busy = true; + + var backupConfig = { + provider: $scope.provider, + key: $scope.key, + format: $scope.format + }; + + // only set provider specific fields, this will clear them in the db + if ($scope.s3like(backupConfig.provider)) { + backupConfig.bucket = $scope.bucket; + backupConfig.prefix = $scope.prefix; + backupConfig.accessKeyId = $scope.accessKeyId; + backupConfig.secretAccessKey = $scope.secretAccessKey; + + if ($scope.endpoint) backupConfig.endpoint = $scope.endpoint; + + if (backupConfig.provider === 's3') { + if ($scope.region) backupConfig.region = $scope.region; + } else if (backupConfig.provider === 'minio' || backupConfig.provider === 's3-v4-compat') { + backupConfig.region = 'us-east-1'; + backupConfig.acceptSelfSignedCerts = $scope.acceptSelfSignedCerts; + } else if (backupConfig.provider === 'exoscale-sos') { + backupConfig.endpoint = 'https://sos.exo.io'; + backupConfig.region = 'us-east-1'; + backupConfig.signatureVersion = 'v2'; + } else if (backupConfig.provider === 'digitalocean-spaces') { + backupConfig.region = 'us-east-1'; + } + } else if (backupConfig.provider === 'filesystem') { + backupConfig.backupFolder = $scope.backupFolder; + } + + var version = $scope.backupId.match(/_v(\d+.\d+.\d+)/); + + Client.restore(backupConfig, $scope.backupId.replace(/\.tar\.gz(\.enc)?$/, ''), version ? version[1] : '', function (error) { + $scope.busy = false; + + if (error) { + if (error.statusCode === 402) { + $scope.error.generic = error.message; + + if (error.message.indexOf('AWS Access Key Id') !== -1) { + $scope.error.accessKeyId = true; + $scope.accessKeyId = ''; + $scope.configureBackupForm.accessKeyId.$setPristine(); + $('#inputConfigureBackupAccessKeyId').focus(); + } else if (error.message.indexOf('not match the signature') !== -1 ) { + $scope.error.secretAccessKey = true; + $scope.secretAccessKey = ''; + $scope.configureBackupForm.secretAccessKey.$setPristine(); + $('#inputConfigureBackupSecretAccessKey').focus(); + } else if (error.message.toLowerCase() === 'access denied') { + $scope.error.bucket = true; + $scope.bucket = ''; + $scope.configureBackupForm.bucket.$setPristine(); + $('#inputConfigureBackupBucket').focus(); + } else if (error.message.indexOf('ECONNREFUSED') !== -1) { + $scope.error.generic = 'Unknown region'; + $scope.error.region = true; + $scope.configureBackupForm.region.$setPristine(); + $('#inputConfigureBackupRegion').focus(); + } else if (error.message.toLowerCase() === 'wrong region') { + $scope.error.generic = 'Wrong S3 Region'; + $scope.error.region = true; + $scope.configureBackupForm.region.$setPristine(); + $('#inputConfigureBackupRegion').focus(); + } else { + $('#inputConfigureBackupBucket').focus(); + } + } else { + $scope.error.generic = error.message; + } + + return; + } + + waitForRestore(); + }); + } + + function waitForRestore() { + $scope.busy = true; + + Client.getStatus(function (error, status) { + if (!error && !status.webadminStatus.restoring) { + window.location.href = '/'; + } + + setTimeout(waitForRestore, 5000); + }); + } + + Client.getStatus(function (error, status) { + if (error) { + window.location.href = '/error.html'; + return; + } + + if (status.restoring) return waitForRestore(); + + if (status.activated) { + window.location.href = '/'; + return; + } + + $scope.instanceId = search.instanceId; + $scope.initialized = true; + }); +}]); diff --git a/webadmin/src/js/setupdns.js b/webadmin/src/js/setupdns.js index ebc48c9bd..cd13ad309 100644 --- a/webadmin/src/js/setupdns.js +++ b/webadmin/src/js/setupdns.js @@ -38,7 +38,7 @@ app.controller('SetupDNSController', ['$scope', '$http', 'Client', function ($sc } }); - // keep in sync with certs.js + // keep in sync with domains.js $scope.dnsProvider = [ { name: 'AWS Route53', value: 'route53' }, { name: 'Cloudflare (DNS only)', value: 'cloudflare' }, @@ -108,10 +108,10 @@ app.controller('SetupDNSController', ['$scope', '$http', 'Client', function ($sc }; if (!data.projectId || !data.credentials || !data.credentials.client_email || !data.credentials.private_key) { - throw "fields_missing"; + throw 'fields_missing'; } } catch(e) { - $scope.dnsCredentials.error = "Cannot parse Google Service Account Key"; + $scope.dnsCredentials.error = 'Cannot parse Google Service Account Key'; $scope.dnsCredentials.busy = false; return; } @@ -134,7 +134,7 @@ app.controller('SetupDNSController', ['$scope', '$http', 'Client', function ($sc } waitForDnsSetup(); - }); + }); }; function waitForDnsSetup() { diff --git a/webadmin/src/restore.html b/webadmin/src/restore.html new file mode 100644 index 000000000..2d64b0ae4 --- /dev/null +++ b/webadmin/src/restore.html @@ -0,0 +1,159 @@ + + + + + + + + Cloudron Restore + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    +
    +

    Downloading backup

    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    +

    Cloudron Restore

    +

    Provide the backup to restore from

    +
    +
    +
    + +
    +
    +

    {{ error.generic }}

    + +
    + + +
    + + +
    + + +
    + + +
    + + +
    + +
    + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + + +
    + + +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    +
    + + + + + diff --git a/webadmin/src/setup.html b/webadmin/src/setup.html index ecd2dbdfa..78bb30c37 100644 --- a/webadmin/src/setup.html +++ b/webadmin/src/setup.html @@ -84,11 +84,15 @@ +
    +
    +
    Looking to restore?
    +
    diff --git a/webadmin/src/setupdns.html b/webadmin/src/setupdns.html index 6792e0db9..5f1e5269f 100644 --- a/webadmin/src/setupdns.html +++ b/webadmin/src/setupdns.html @@ -53,8 +53,8 @@

    Cloudron Setup

    -

    Provide a domain for your Cloudron

    -

    Apps will be installed on subdomains of this domain.

    +

    Provide the first domain for your Cloudron

    +

    Apps will be installed on subdomains.

    @@ -63,7 +63,7 @@
    -

    Choose how the domain is managed

    +

    Choose how this domain is managed

    {{ dnsCredentials.error }}

    @@ -140,6 +140,12 @@
    +
    +
    +
    + You can setup a new Cloudron or restore from a backup in the next step +
    +
    diff --git a/webadmin/src/theme.scss b/webadmin/src/theme.scss index 1a1f91396..232c9ffaa 100644 --- a/webadmin/src/theme.scss +++ b/webadmin/src/theme.scss @@ -213,7 +213,7 @@ h1, h2, h3 { .grid-item { padding: 10px; - min-width: 205px; + min-width: 225px; .col-xs-12 { padding-left: 5px; @@ -714,7 +714,6 @@ multiselect.stretch { color: $brand-danger; a { - text-decoration: underline; color: $brand-danger; } } diff --git a/webadmin/src/views/apps.html b/webadmin/src/views/apps.html index c15f8b92a..fdd810ac2 100644 --- a/webadmin/src/views/apps.html +++ b/webadmin/src/views/apps.html @@ -8,7 +8,7 @@ @@ -230,9 +230,14 @@
    - +
    - +
    diff --git a/webadmin/src/views/apps.js b/webadmin/src/views/apps.js index 00fca473b..b7d2f78c7 100644 --- a/webadmin/src/views/apps.js +++ b/webadmin/src/views/apps.js @@ -7,7 +7,7 @@ angular.module('Application').controller('AppsController', ['$scope', '$location $scope.installedApps = Client.getInstalledApps(); $scope.config = Client.getConfig(); $scope.user = Client.getUserInfo(); - $scope.dnsConfig = {}; + $scope.domains = []; $scope.groups = []; $scope.users = []; $scope.mailConfig = {}; @@ -17,6 +17,7 @@ angular.module('Application').controller('AppsController', ['$scope', '$location busy: false, error: {}, app: {}, + domain: '', location: '', usingAltDomain: false, advancedVisible: false, @@ -51,6 +52,129 @@ angular.module('Application').controller('AppsController', ['$scope', '$location isAltDomainNaked: function () { return ngTld.isNakedDomain($scope.appConfigure.location); + }, + + show: function (app) { + $scope.reset(); + + // fill relevant info from the app + $scope.appConfigure.app = app; + $scope.appConfigure.location = app.altDomain || app.location; + $scope.appConfigure.domain = app.domain; + $scope.appConfigure.usingAltDomain = !!app.altDomain; + $scope.appConfigure.portBindingsInfo = app.manifest.tcpPorts || {}; // Portbinding map only for information + $scope. Option = app.accessRestriction ? 'groups' : 'any'; + $scope.appConfigure.memoryLimit = app.memoryLimit || app.manifest.memoryLimit || (256 * 1024 * 1024); + $scope.appConfigure.xFrameOptions = app.xFrameOptions.indexOf('ALLOW-FROM') === 0 ? app.xFrameOptions.split(' ')[1] : ''; + $scope.appConfigure.customAuth = !(app.manifest.addons['ldap'] || app.manifest.addons['oauth']); + $scope.appConfigure.robotsTxt = app.robotsTxt; + $scope.appConfigure.enableBackup = app.enableBackup; + + // create ticks starting from manifest memory limit. the memory limit here is currently split into ram+swap (and thus *2 below) + // TODO: the *2 will overallocate since 4GB is max swap that cloudron itself allocates + $scope.appConfigure.memoryTicks = [ ]; + var npow2 = Math.pow(2, Math.ceil(Math.log($scope.config.memory)/Math.log(2))); + for (var i = 256; i <= (npow2*2/1024/1024); i *= 2) { + if (i >= (app.manifest.memoryLimit/1024/1024 || 0)) $scope.appConfigure.memoryTicks.push(i * 1024 * 1024); + } + if (app.manifest.memoryLimit && $scope.appConfigure.memoryTicks[0] !== app.manifest.memoryLimit) { + $scope.appConfigure.memoryTicks.unshift(app.manifest.memoryLimit); + } + + $scope.appConfigure.accessRestrictionOption = app.accessRestriction ? 'groups' : 'any'; + $scope.appConfigure.accessRestriction = { users: [], groups: [] }; + + if (app.accessRestriction) { + var userSet = { }; + app.accessRestriction.users.forEach(function (uid) { userSet[uid] = true; }); + $scope.users.forEach(function (u) { if (userSet[u.id] === true) $scope.appConfigure.accessRestriction.users.push(u); }); + + var groupSet = { }; + app.accessRestriction.groups.forEach(function (gid) { groupSet[gid] = true; }); + $scope.groups.forEach(function (g) { if (groupSet[g.id] === true) $scope.appConfigure.accessRestriction.groups.push(g); }); + } + + // fill the portBinding structures. There might be holes in the app.portBindings, which signalizes a disabled port + for (var env in $scope.appConfigure.portBindingsInfo) { + if (app.portBindings && app.portBindings[env]) { + $scope.appConfigure.portBindings[env] = app.portBindings[env]; + $scope.appConfigure.portBindingsEnabled[env] = true; + } else { + $scope.appConfigure.portBindings[env] = $scope.appConfigure.portBindingsInfo[env].defaultValue || 0; + $scope.appConfigure.portBindingsEnabled[env] = false; + } + } + + $('#appConfigureModal').modal('show'); + }, + + submit: function () { + $scope.appConfigure.busy = true; + $scope.appConfigure.error.other = null; + $scope.appConfigure.error.location = null; + $scope.appConfigure.error.xFrameOptions = null; + + // only use enabled ports from portBindings + var finalPortBindings = {}; + for (var env in $scope.appConfigure.portBindings) { + if ($scope.appConfigure.portBindingsEnabled[env]) { + finalPortBindings[env] = $scope.appConfigure.portBindings[env]; + } + } + + var finalAccessRestriction = null; + if ($scope.appConfigure.accessRestrictionOption === 'groups') { + finalAccessRestriction = { users: [], groups: [] }; + finalAccessRestriction.users = $scope.appConfigure.accessRestriction.users.map(function (u) { return u.id; }); + finalAccessRestriction.groups = $scope.appConfigure.accessRestriction.groups.map(function (g) { return g.id; }); + } + + var data = { + location: $scope.appConfigure.usingAltDomain ? $scope.appConfigure.app.location : $scope.appConfigure.location, + altDomain: $scope.appConfigure.usingAltDomain ? $scope.appConfigure.location : null, + domain: $scope.appConfigure.usingAltDomain ? undefined : $scope.appConfigure.domain, + portBindings: finalPortBindings, + accessRestriction: finalAccessRestriction, + cert: $scope.appConfigure.certificateFile, + key: $scope.appConfigure.keyFile, + xFrameOptions: $scope.appConfigure.xFrameOptions ? ('ALLOW-FROM ' + $scope.appConfigure.xFrameOptions) : 'SAMEORIGIN', + memoryLimit: $scope.appConfigure.memoryLimit === $scope.appConfigure.memoryTicks[0] ? 0 : $scope.appConfigure.memoryLimit, + robotsTxt: $scope.appConfigure.robotsTxt, + enableBackup: $scope.appConfigure.enableBackup + }; + + Client.configureApp($scope.appConfigure.app.id, data, function (error) { + if (error) { + if (error.statusCode === 409 && (error.message.indexOf('is reserved') !== -1 || error.message.indexOf('is already in use') !== -1)) { + $scope.appConfigure.error.port = error.message; + } else if (error.statusCode === 409) { + $scope.appConfigure.error.location = 'This name is already taken.'; + $scope.appConfigureForm.location.$setPristine(); + $('#appConfigureLocationInput').focus(); + } else if (error.statusCode === 400 && error.message.indexOf('cert') !== -1 ) { + $scope.appConfigure.error.cert = error.message; + $scope.appConfigure.certificateFileName = ''; + $scope.appConfigure.certificateFile = null; + $scope.appConfigure.keyFileName = ''; + $scope.appConfigure.keyFile = null; + } else if (error.statusCode === 400 && error.message.indexOf('xFrameOptions') !== -1 ) { + $scope.appConfigure.error.xFrameOptions = error.message; + $scope.appConfigureForm.xFrameOptions.$setPristine(); + $('#appConfigureXFrameOptionsInput').focus(); + } else { + $scope.appConfigure.error.other = error.message; + } + + $scope.appConfigure.busy = false; + return; + } + + $scope.appConfigure.busy = false; + + $('#appConfigureModal').modal('hide'); + + $scope.reset(); + }); } }; @@ -145,6 +269,7 @@ angular.module('Application').controller('AppsController', ['$scope', '$location // reset configure dialog $scope.appConfigure.error = {}; $scope.appConfigure.app = {}; + $scope.appConfigure.domain = ''; $scope.appConfigure.location = ''; $scope.appConfigure.advancedVisible = false; $scope.appConfigure.usingAltDomain = false; @@ -218,8 +343,9 @@ angular.module('Application').controller('AppsController', ['$scope', '$location }); }; - $scope.useAltDomain = function (use) { + $scope.useAltDomain = function (use, domain) { $scope.appConfigure.usingAltDomain = use; + $scope.appConfigure.domain = domain; if (use) { $scope.appConfigure.location = ''; @@ -228,127 +354,6 @@ angular.module('Application').controller('AppsController', ['$scope', '$location } }; - $scope.showConfigure = function (app) { - $scope.reset(); - - // fill relevant info from the app - $scope.appConfigure.app = app; - $scope.appConfigure.location = app.altDomain || app.location; - $scope.appConfigure.usingAltDomain = !!app.altDomain; - $scope.appConfigure.portBindingsInfo = app.manifest.tcpPorts || {}; // Portbinding map only for information - $scope. Option = app.accessRestriction ? 'groups' : 'any'; - $scope.appConfigure.memoryLimit = app.memoryLimit || app.manifest.memoryLimit || (256 * 1024 * 1024); - $scope.appConfigure.xFrameOptions = app.xFrameOptions.indexOf('ALLOW-FROM') === 0 ? app.xFrameOptions.split(' ')[1] : ''; - $scope.appConfigure.customAuth = !(app.manifest.addons['ldap'] || app.manifest.addons['oauth']); - $scope.appConfigure.robotsTxt = app.robotsTxt; - $scope.appConfigure.enableBackup = app.enableBackup; - - // create ticks starting from manifest memory limit. the memory limit here is currently split into ram+swap (and thus *2 below) - // TODO: the *2 will overallocate since 4GB is max swap that cloudron itself allocates - $scope.appConfigure.memoryTicks = [ ]; - var npow2 = Math.pow(2, Math.ceil(Math.log($scope.config.memory)/Math.log(2))); - for (var i = 256; i <= (npow2*2/1024/1024); i *= 2) { - if (i >= (app.manifest.memoryLimit/1024/1024 || 0)) $scope.appConfigure.memoryTicks.push(i * 1024 * 1024); - } - if (app.manifest.memoryLimit && $scope.appConfigure.memoryTicks[0] !== app.manifest.memoryLimit) { - $scope.appConfigure.memoryTicks.unshift(app.manifest.memoryLimit); - } - - $scope.appConfigure.accessRestrictionOption = app.accessRestriction ? 'groups' : 'any'; - $scope.appConfigure.accessRestriction = { users: [], groups: [] }; - - if (app.accessRestriction) { - var userSet = { }; - app.accessRestriction.users.forEach(function (uid) { userSet[uid] = true; }); - $scope.users.forEach(function (u) { if (userSet[u.id] === true) $scope.appConfigure.accessRestriction.users.push(u); }); - - var groupSet = { }; - app.accessRestriction.groups.forEach(function (gid) { groupSet[gid] = true; }); - $scope.groups.forEach(function (g) { if (groupSet[g.id] === true) $scope.appConfigure.accessRestriction.groups.push(g); }); - } - - // fill the portBinding structures. There might be holes in the app.portBindings, which signalizes a disabled port - for (var env in $scope.appConfigure.portBindingsInfo) { - if (app.portBindings && app.portBindings[env]) { - $scope.appConfigure.portBindings[env] = app.portBindings[env]; - $scope.appConfigure.portBindingsEnabled[env] = true; - } else { - $scope.appConfigure.portBindings[env] = $scope.appConfigure.portBindingsInfo[env].defaultValue || 0; - $scope.appConfigure.portBindingsEnabled[env] = false; - } - } - - $('#appConfigureModal').modal('show'); - }; - - $scope.doConfigure = function () { - $scope.appConfigure.busy = true; - $scope.appConfigure.error.other = null; - $scope.appConfigure.error.location = null; - $scope.appConfigure.error.xFrameOptions = null; - - // only use enabled ports from portBindings - var finalPortBindings = {}; - for (var env in $scope.appConfigure.portBindings) { - if ($scope.appConfigure.portBindingsEnabled[env]) { - finalPortBindings[env] = $scope.appConfigure.portBindings[env]; - } - } - - var finalAccessRestriction = null; - if ($scope.appConfigure.accessRestrictionOption === 'groups') { - finalAccessRestriction = { users: [], groups: [] }; - finalAccessRestriction.users = $scope.appConfigure.accessRestriction.users.map(function (u) { return u.id; }); - finalAccessRestriction.groups = $scope.appConfigure.accessRestriction.groups.map(function (g) { return g.id; }); - } - - var data = { - location: $scope.appConfigure.usingAltDomain ? $scope.appConfigure.app.location : $scope.appConfigure.location, - altDomain: $scope.appConfigure.usingAltDomain ? $scope.appConfigure.location : null, - portBindings: finalPortBindings, - accessRestriction: finalAccessRestriction, - cert: $scope.appConfigure.certificateFile, - key: $scope.appConfigure.keyFile, - xFrameOptions: $scope.appConfigure.xFrameOptions ? ('ALLOW-FROM ' + $scope.appConfigure.xFrameOptions) : 'SAMEORIGIN', - memoryLimit: $scope.appConfigure.memoryLimit === $scope.appConfigure.memoryTicks[0] ? 0 : $scope.appConfigure.memoryLimit, - robotsTxt: $scope.appConfigure.robotsTxt, - enableBackup: $scope.appConfigure.enableBackup - }; - - Client.configureApp($scope.appConfigure.app.id, data, function (error) { - if (error) { - if (error.statusCode === 409 && (error.message.indexOf('is reserved') !== -1 || error.message.indexOf('is already in use') !== -1)) { - $scope.appConfigure.error.port = error.message; - } else if (error.statusCode === 409) { - $scope.appConfigure.error.location = 'This name is already taken.'; - $scope.appConfigureForm.location.$setPristine(); - $('#appConfigureLocationInput').focus(); - } else if (error.statusCode === 400 && error.message.indexOf('cert') !== -1 ) { - $scope.appConfigure.error.cert = error.message; - $scope.appConfigure.certificateFileName = ''; - $scope.appConfigure.certificateFile = null; - $scope.appConfigure.keyFileName = ''; - $scope.appConfigure.keyFile = null; - } else if (error.statusCode === 400 && error.message.indexOf('xFrameOptions') !== -1 ) { - $scope.appConfigure.error.xFrameOptions = error.message; - $scope.appConfigureForm.xFrameOptions.$setPristine(); - $('#appConfigureXFrameOptionsInput').focus(); - } else { - $scope.appConfigure.error.other = error.message; - } - - $scope.appConfigure.busy = false; - return; - } - - $scope.appConfigure.busy = false; - - $('#appConfigureModal').modal('hide'); - - $scope.reset(); - }); - }; - $scope.showInformation = function (app) { $scope.reset(); @@ -465,14 +470,14 @@ angular.module('Application').controller('AppsController', ['$scope', '$location }); } - function fetchDnsConfig() { - Client.getDnsConfig(function (error, result) { + function getDomains() { + Client.getDomains(function (error, result) { if (error) { console.error(error); - return $timeout(fetchDnsConfig, 5000); + return $timeout(getDomains, 5000); } - $scope.dnsConfig = result; + $scope.domains = result.map(function (d) { return d.domain; }); }); } @@ -499,7 +504,7 @@ angular.module('Application').controller('AppsController', ['$scope', '$location if ($scope.user.admin) { fetchUsers(); fetchGroups(); - fetchDnsConfig(); + getDomains(); getMailConfig(); getBackupConfig(); } diff --git a/webadmin/src/views/appstore.html b/webadmin/src/views/appstore.html index fdd23b166..0af12f191 100644 --- a/webadmin/src/views/appstore.html +++ b/webadmin/src/views/appstore.html @@ -21,8 +21,16 @@
    -
    - {{ !appInstall.location ? '' : (config.isCustomDomain ? '.' : '-') }}{{ config.fqdn }} +
    + +
    @@ -278,7 +286,6 @@ Project Management Wiki
    -


    diff --git a/webadmin/src/views/appstore.js b/webadmin/src/views/appstore.js index fae104b59..3d0d10770 100644 --- a/webadmin/src/views/appstore.js +++ b/webadmin/src/views/appstore.js @@ -12,7 +12,7 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca $scope.user = Client.getUserInfo(); $scope.users = []; $scope.groups = []; - $scope.dnsConfig = {}; + $scope.domains = []; $scope.category = ''; $scope.cachedCategory = ''; // used to cache the selected category while searching $scope.searchString = ''; @@ -37,6 +37,7 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca error: {}, app: {}, location: '', + domain: '', portBindings: {}, mediaLinks: [], certificateFile: null, @@ -57,6 +58,7 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca $scope.appInstall.app = {}; $scope.appInstall.error = {}; $scope.appInstall.location = ''; + $scope.appInstall.domain = ''; $scope.appInstall.portBindings = {}; $scope.appInstall.state = 'appInfo'; $scope.appInstall.mediaLinks = []; @@ -102,6 +104,7 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca $scope.appInstall.mediaLinks = $scope.appInstall.app.manifest.mediaLinks || []; $scope.appInstall.location = app.location; + $scope.appInstall.domain = $scope.config.fqdn; // FIXME needs to come from domains dropdown $scope.appInstall.portBindingsInfo = $scope.appInstall.app.manifest.tcpPorts || {}; // Portbinding map only for information $scope.appInstall.portBindings = {}; // This is the actual model holding the env:port pair $scope.appInstall.portBindingsEnabled = {}; // This is the actual model holding the enabled/disabled flag @@ -145,6 +148,7 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca var data = { location: $scope.appInstall.location || '', + domain: $scope.appInstall.domain, portBindings: finalPortBindings, accessRestriction: finalAccessRestriction, cert: $scope.appInstall.certificateFile, @@ -492,14 +496,14 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca }); } - function fetchDnsConfig() { - Client.getDnsConfig(function (error, result) { + function getDomains() { + Client.getDomains(function (error, result) { if (error) { console.error(error); - return $timeout(fetchDnsConfig, 5000); + return $timeout(getDomains, 5000); } - $scope.dnsConfig = result; + $scope.domains = result.map(function (d) { return d.domain; }); }); } @@ -557,7 +561,7 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca fetchUsers(); fetchGroups(); - fetchDnsConfig(); + getDomains(); getMailConfig(); fetchAppstoreConfig(function (error) { diff --git a/webadmin/src/views/certs.html b/webadmin/src/views/certs.html deleted file mode 100644 index 3f6376f66..000000000 --- a/webadmin/src/views/certs.html +++ /dev/null @@ -1,260 +0,0 @@ - - -
    -
    -

    Domain & Certificates

    -
    - -
    -

    Domain

    -
    - -
    -
    -
    -

    To use a custom domain, configure your domain to use Route53. Moving to a custom domain will retain all your apps and data and will take around 15 minutes.

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Domain name{{ config.fqdn }}
    DNS provider{{ dnsConfig.provider }}
    -
    - No DNS provider is configured. All DNS records need to be setup manually. - To avoid manual setup for each installed app, set a DNS API provider. -
    -
    - Wildcard DNS provider is configured. Always ensure there is a wildcard DNS record for this server's IP. -
    -
    - No DNS provider configured. All DNS records need to be setup manually and all DNS checks are skipped. -
    Access key id{{ dnsConfig.accessKeyId || 'unset' }}
    Secret access keyhidden
    DigitalOcean tokenhidden

    -
    -
    -
    - -
    -

    SSL Certificates

    -
    - -
    -
    -
    - Certificates can only by set for custom domains. -
    -
    - -
    -
    -
    -
    -

    Certificates are automatically obtained and renewed from Let’s Encrypt. See the current rate limit here.

    -
    - -

    This wildcard certificate will be used for apps, should getting a Let’s Encrypt certificate fail.

    -
    {{ defaultCert.error }}
    -
    Upload successful
    -
    -
    - - - - - -
    -
    -
    -
    - - - - - -
    -
    - -
    -
    -
    -
    -
    -
    -
    -
    - -

    This certificate will be used for this Settings application.

    -
    {{ adminCert.error }}
    -
    Upload successful
    -
    -
    - - - - - -
    -
    -
    -
    - - - - - -
    -
    - -
    -
    -
    -
    -
    -
    diff --git a/webadmin/src/views/certs.js b/webadmin/src/views/certs.js deleted file mode 100644 index 58aab16dd..000000000 --- a/webadmin/src/views/certs.js +++ /dev/null @@ -1,258 +0,0 @@ -'use strict'; - -angular.module('Application').controller('CertsController', ['$scope', '$location', 'Client', 'ngTld', function ($scope, $location, Client, ngTld) { - Client.onReady(function () { if (!Client.getUserInfo().admin) $location.path('/'); }); - - $scope.config = Client.getConfig(); - $scope.dnsConfig = null; - - // keep in sync with setupdns.js - $scope.dnsProvider = [ - { name: 'AWS Route53', value: 'route53' }, - { name: 'Cloudflare (DNS only)', value: 'cloudflare' }, - { name: 'Digital Ocean', value: 'digitalocean' }, - { name: 'Google Cloud DNS', value: 'gcdns' }, - { name: 'Wildcard', value: 'wildcard' }, - { name: 'Manual (not recommended)', value: 'manual' }, - { name: 'No-op (only for development)', value: 'noop' } - ]; - - $scope.defaultCert = { - error: null, - success: false, - busy: false, - certificateFile: null, - certificateFileName: '', - keyFile: null, - keyFileName: '' - }; - - $scope.adminCert = { - error: null, - success: false, - busy: false, - certificateFile: null, - certificateFileName: '', - keyFile: null, - keyFileName: '' - }; - - $scope.dnsCredentials = { - error: null, - success: false, - busy: false, - customDomain: '', - accessKeyId: '', - secretAccessKey: '', - gcdnsKey: { keyFileName: '', content: '' }, - digitalOceanToken: '', - cloudflareToken: '', - cloudflareEmail: '', - provider: 'route53', - password: '' - }; - - function readFileLocally(obj, file, fileName) { - return function (event) { - $scope.$apply(function () { - obj[file] = null; - obj[fileName] = event.target.files[0].name; - - var reader = new FileReader(); - reader.onload = function (result) { - if (!result.target || !result.target.result) return console.error('Unable to read local file'); - obj[file] = result.target.result; - }; - reader.readAsText(event.target.files[0]); - }); - }; - } - - document.getElementById('defaultCertFileInput').onchange = readFileLocally($scope.defaultCert, 'certificateFile', 'certificateFileName'); - document.getElementById('defaultKeyFileInput').onchange = readFileLocally($scope.defaultCert, 'keyFile', 'keyFileName'); - document.getElementById('adminCertFileInput').onchange = readFileLocally($scope.adminCert, 'certificateFile', 'certificateFileName'); - document.getElementById('adminKeyFileInput').onchange = readFileLocally($scope.adminCert, 'keyFile', 'keyFileName'); - - document.getElementById('gcdnsKeyFileInput').onchange = readFileLocally($scope.dnsCredentials.gcdnsKey, 'content', 'keyFileName'); - - $scope.setDefaultCert = function () { - $scope.defaultCert.busy = true; - $scope.defaultCert.error = null; - $scope.defaultCert.success = false; - - Client.setCertificate($scope.defaultCert.certificateFile, $scope.defaultCert.keyFile, function (error) { - if (error) { - $scope.defaultCert.error = error.message; - } else { - $scope.defaultCert.success = true; - $scope.defaultCert.certificateFileName = ''; - $scope.defaultCert.keyFileName = ''; - } - - $scope.defaultCert.busy = false; - }); - }; - - $scope.setAdminCert = function () { - $scope.adminCert.busy = true; - $scope.adminCert.error = null; - $scope.adminCert.success = false; - - Client.setAdminCertificate($scope.adminCert.certificateFile, $scope.adminCert.keyFile, function (error) { - if (error) { - $scope.adminCert.error = error.message; - } else { - $scope.adminCert.success = true; - $scope.adminCert.certificateFileName = ''; - $scope.adminCert.keyFileName = ''; - } - - $scope.adminCert.busy = false; - - // attempt to reload to make the browser get the new certs - window.location.reload(true); - }); - }; - - $scope.setDnsCredentials = function () { - $scope.dnsCredentials.busy = true; - $scope.dnsCredentials.error = null; - $scope.dnsCredentials.success = false; - - var migrateDomain = $scope.dnsCredentials.customDomain !== $scope.config.fqdn; - - var data = { - provider: $scope.dnsCredentials.provider - }; - - // special case the wildcard provider - if (data.provider === 'wildcard') { - data.provider = 'manual'; - data.wildcard = true; - } - - if (data.provider === 'route53') { - data.accessKeyId = $scope.dnsCredentials.accessKeyId; - data.secretAccessKey = $scope.dnsCredentials.secretAccessKey; - } else if (data.provider === 'gcdns'){ - try { - var serviceAccountKey = JSON.parse($scope.dnsCredentials.gcdnsKey.content); - data.projectId = serviceAccountKey.project_id; - data.credentials = { - client_email: serviceAccountKey.client_email, - private_key: serviceAccountKey.private_key - }; - - if (!data.projectId || !data.credentials || !data.credentials.client_email || !data.credentials.private_key) { - throw 'fields_missing'; - } - } catch (e) { - $scope.dnsCredentials.error = 'Cannot parse Google Service Account Key: ' + e.message; - $scope.dnsCredentials.busy = false; - return; - } - } else if (data.provider === 'digitalocean') { - data.token = $scope.dnsCredentials.digitalOceanToken; - } else if (data.provider === 'cloudflare') { - data.token = $scope.dnsCredentials.cloudflareToken; - data.email = $scope.dnsCredentials.cloudflareEmail; - } - - var func; - if (migrateDomain) { - data.domain = $scope.dnsCredentials.customDomain; - func = Client.migrate.bind(Client, data, $scope.dnsCredentials.password); - } else { - func = Client.setDnsConfig.bind(Client, data); - } - - func(function (error) { - if (error) { - $scope.dnsCredentials.error = error.message; - } else { - $scope.dnsCredentials.success = true; - - $('#dnsCredentialsModal').modal('hide'); - - dnsCredentialsReset(); - - if (migrateDomain) window.location.href = '/update.html'; - } - - $scope.dnsCredentials.busy = false; - - // reload the dns config - Client.getDnsConfig(function (error, result) { - if (error) return console.error(error); - - $scope.dnsConfig = result; - }); - }); - }; - - function dnsCredentialsReset() { - $scope.dnsCredentials.busy = false; - $scope.dnsCredentials.success = false; - $scope.dnsCredentials.error = null; - - $scope.dnsCredentials.provider = ''; - $scope.dnsCredentials.customDomain = ''; - $scope.dnsCredentials.accessKeyId = ''; - $scope.dnsCredentials.secretAccessKey = ''; - $scope.dnsCredentials.gcdnsKey.keyFileName = ''; - $scope.dnsCredentials.gcdnsKey.content = ''; - $scope.dnsCredentials.digitalOceanToken = ''; - $scope.dnsCredentials.cloudflareToken = ''; - $scope.dnsCredentials.cloudflareEmail = ''; - $scope.dnsCredentials.password = ''; - - $scope.dnsCredentialsForm.$setPristine(); - $scope.dnsCredentialsForm.$setUntouched(); - - $('#customDomainId').focus(); - } - - $scope.showChangeDnsCredentials = function () { - dnsCredentialsReset(); - - // clear the input box for non-custom domain - $scope.dnsCredentials.customDomain = $scope.config.isCustomDomain ? $scope.config.fqdn : ''; - $scope.dnsCredentials.accessKeyId = $scope.dnsConfig.accessKeyId; - $scope.dnsCredentials.secretAccessKey = $scope.dnsConfig.secretAccessKey; - - $scope.dnsCredentials.gcdnsKey.keyFileName = ''; - $scope.dnsCredentials.gcdnsKey.content = ''; - if ($scope.dnsConfig.provider === 'gcdns') { - $scope.dnsCredentials.gcdnsKey.keyFileName = $scope.dnsConfig.credentials && $scope.dnsConfig.credentials.client_email; - $scope.dnsCredentials.gcdnsKey.content = JSON.stringify({ - "project_id": $scope.dnsConfig.projectId, - "credentials": $scope.dnsConfig.credentials - }); - } - $scope.dnsCredentials.digitalOceanToken = $scope.dnsConfig.provider === 'digitalocean' ? $scope.dnsConfig.token : ''; - $scope.dnsCredentials.cloudflareToken = $scope.dnsConfig.provider === 'cloudflare' ? $scope.dnsConfig.token : ''; - $scope.dnsCredentials.cloudflareEmail = $scope.dnsConfig.email; - - $scope.dnsCredentials.provider = $scope.dnsConfig.provider === 'caas' ? 'route53' : $scope.dnsConfig.provider; - $scope.dnsCredentials.provider = ($scope.dnsCredentials.provider === 'manual' && $scope.dnsConfig.wildcard) ? 'wildcard' : $scope.dnsCredentials.provider; - - $('#dnsCredentialsModal').modal('show'); - }; - - Client.onReady(function () { - Client.getDnsConfig(function (error, result) { - if (error) return console.error(error); - - $scope.dnsConfig = result; - }); - }); - - // setup all the dialog focus handling - ['dnsCredentialsModal'].forEach(function (id) { - $('#' + id).on('shown.bs.modal', function () { - $(this).find("[autofocus]:first").focus(); - }); - }); - - $('.modal-backdrop').remove(); -}]); diff --git a/webadmin/src/views/debug.html b/webadmin/src/views/debug.html index 197fe78ec..b814f6b10 100644 --- a/webadmin/src/views/debug.html +++ b/webadmin/src/views/debug.html @@ -68,8 +68,8 @@
    - + diff --git a/webadmin/src/views/domains.html b/webadmin/src/views/domains.html new file mode 100644 index 000000000..8d5250636 --- /dev/null +++ b/webadmin/src/views/domains.html @@ -0,0 +1,197 @@ + + + + + +
    +
    +

    Domains

    +
    + +
    +
    +
    +
    +

    +
    +
    +
    +
    + + + + + + + + + + + + + + + +
    DomainActions
    + {{ domain.domain }} + + + +
    +
    +
    +
    +
    +
    diff --git a/webadmin/src/views/domains.js b/webadmin/src/views/domains.js new file mode 100644 index 000000000..1334f1463 --- /dev/null +++ b/webadmin/src/views/domains.js @@ -0,0 +1,261 @@ +'use strict'; + +angular.module('Application').controller('DomainsController', ['$scope', '$location', 'Client', 'ngTld', function ($scope, $location, Client, ngTld) { + Client.onReady(function () { if (!Client.getUserInfo().admin) $location.path('/'); }); + + $scope.config = Client.getConfig(); + $scope.dnsConfig = null; + $scope.domains = []; + $scope.ready = false; + + // keep in sync with setupdns.js + $scope.dnsProvider = [ + { name: 'AWS Route53', value: 'route53' }, + { name: 'Cloudflare (DNS only)', value: 'cloudflare' }, + { name: 'Digital Ocean', value: 'digitalocean' }, + { name: 'Google Cloud DNS', value: 'gcdns' }, + { name: 'Wildcard', value: 'wildcard' }, + { name: 'Manual (not recommended)', value: 'manual' }, + { name: 'No-op (only for development)', value: 'noop' } + ]; + + function readFileLocally(obj, file, fileName) { + return function (event) { + $scope.$apply(function () { + obj[file] = null; + obj[fileName] = event.target.files[0].name; + + var reader = new FileReader(); + reader.onload = function (result) { + if (!result.target || !result.target.result) return console.error('Unable to read local file'); + obj[file] = result.target.result; + }; + reader.readAsText(event.target.files[0]); + }); + }; + } + + // We reused configure also for adding domains to avoid much code duplication + $scope.domainConfigure = { + adding: false, + error: null, + busy: false, + domain: null, + + // form model + newDomain: '', + accessKeyId: '', + secretAccessKey: '', + gcdnsKey: { keyFileName: '', content: '' }, + digitalOceanToken: '', + cloudflareToken: '', + cloudflareEmail: '', + provider: 'route53', + + fallbackCert: { + certificateFile: null, + certificateFileName: '', + keyFile: null, + keyFileName: '' + }, + + show: function (domain) { + $scope.domainConfigure.reset(); + + if (domain) { + $scope.domainConfigure.domain = domain; + $scope.domainConfigure.accessKeyId = domain.config.accessKeyId; + $scope.domainConfigure.secretAccessKey = domain.config.secretAccessKey; + + $scope.domainConfigure.gcdnsKey.keyFileName = ''; + $scope.domainConfigure.gcdnsKey.content = ''; + if ($scope.domainConfigure.provider === 'gcdns') { + $scope.domainConfigure.gcdnsKey.keyFileName = domain.config.credentials && domain.config.credentials.client_email; + $scope.domainConfigure.gcdnsKey.content = JSON.stringify({ + "project_id": domain.config.projectId, + "credentials": domain.config.credentials + }); + } + $scope.domainConfigure.digitalOceanToken = domain.config.provider === 'digitalocean' ? domain.config.token : ''; + $scope.domainConfigure.cloudflareToken = domain.config.provider === 'cloudflare' ? domain.config.token : ''; + $scope.domainConfigure.cloudflareEmail = domain.config.email; + + $scope.domainConfigure.provider = domain.config.provider; + $scope.domainConfigure.provider = ($scope.domainConfigure.provider === 'manual' && domain.config.wildcard) ? 'wildcard' : domain.config.provider; + } else { + $scope.domainConfigure.adding = true; + } + + $('#domainConfigureModal').modal('show'); + }, + + submit: function () { + $scope.domainConfigure.busy = true; + $scope.domainConfigure.error = null; + + var data = { + provider: $scope.domainConfigure.provider + }; + + // special case the wildcard provider + if (data.provider === 'wildcard') { + data.provider = 'manual'; + data.wildcard = true; + } + + if (data.provider === 'route53') { + data.accessKeyId = $scope.domainConfigure.accessKeyId; + data.secretAccessKey = $scope.domainConfigure.secretAccessKey; + } else if (data.provider === 'gcdns'){ + try { + var serviceAccountKey = JSON.parse($scope.domainConfigure.gcdnsKey.content); + data.projectId = serviceAccountKey.project_id; + data.credentials = { + client_email: serviceAccountKey.client_email, + private_key: serviceAccountKey.private_key + }; + + if (!data.projectId || !data.credentials || !data.credentials.client_email || !data.credentials.private_key) { + throw 'fields_missing'; + } + } catch (e) { + $scope.domainConfigure.error = 'Cannot parse Google Service Account Key: ' + e.message; + $scope.domainConfigure.busy = false; + return; + } + } else if (data.provider === 'digitalocean') { + data.token = $scope.domainConfigure.digitalOceanToken; + } else if (data.provider === 'cloudflare') { + data.token = $scope.domainConfigure.cloudflareToken; + data.email = $scope.domainConfigure.cloudflareEmail; + } + + var fallbackCertificate = null; + if ($scope.domainConfigure.fallbackCert.certificateFile && $scope.domainConfigure.fallbackCert.keyFile) { + fallbackCertificate = { + cert: $scope.domainConfigure.fallbackCert.certificateFile, + key: $scope.domainConfigure.fallbackCert.keyFile + }; + } + + // choose the right api, since we reuse this for adding and configuring domains + var func; + if ($scope.domainConfigure.adding) func = Client.addDomain.bind(Client, $scope.domainConfigure.newDomain, data, fallbackCertificate); + else func = Client.updateDomain.bind(Client, $scope.domainConfigure.domain.domain, data, fallbackCertificate); + + func(function (error) { + $scope.domainConfigure.busy = false; + if (error) { + $scope.domainConfigure.error = error.message; + return; + } + + $('#domainConfigureModal').modal('hide'); + $scope.domainConfigure.reset(); + + // reload the domains + Client.getDomains(function (error, result) { + if (error) return console.error(error); + + $scope.domains = result; + }); + }); + }, + + reset: function () { + $scope.domainConfigure.adding = false; + $scope.domainConfigure.newDomain = ''; + + $scope.domainConfigure.busy = false; + $scope.domainConfigure.error = null; + + $scope.domainConfigure.provider = ''; + $scope.domainConfigure.accessKeyId = ''; + $scope.domainConfigure.secretAccessKey = ''; + $scope.domainConfigure.gcdnsKey.keyFileName = ''; + $scope.domainConfigure.gcdnsKey.content = ''; + $scope.domainConfigure.digitalOceanToken = ''; + $scope.domainConfigure.cloudflareToken = ''; + $scope.domainConfigure.cloudflareEmail = ''; + + $scope.domainConfigureForm.$setPristine(); + $scope.domainConfigureForm.$setUntouched(); + } + }; + + $scope.domainRemove = { + busy: false, + error: null, + domain: null, + password: '', + + show: function (domain) { + $scope.domainRemove.reset(); + + $scope.domainRemove.domain = domain; + + $('#domainRemoveModal').modal('show'); + }, + + submit: function () { + $scope.domainRemove.busy = true; + $scope.domainRemove.error = null; + + Client.removeDomain($scope.domainRemove.domain.domain, $scope.domainRemove.password, function (error) { + if (error && (error.statusCode === 403 || error.statusCode === 409)) { + $scope.domainRemove.password = ''; + $scope.domainRemove.error = error.message; + $scope.domainRemoveForm.password.$setPristine(); + $('#domainRemovePasswordInput').focus(); + } else if (error) { + Client.error(error); + } else { + $('#domainRemoveModal').modal('hide'); + $scope.domainRemove.reset(); + + // reload the domains + Client.getDomains(function (error, result) { + if (error) return console.error(error); + + $scope.domains = result; + }); + } + + $scope.domainRemove.busy = false; + }); + }, + + reset: function () { + $scope.domainRemove.busy = false; + $scope.domainRemove.error = null; + $scope.domainRemove.domain = null; + $scope.domainRemove.password = ''; + + $scope.domainRemoveForm.$setPristine(); + $scope.domainRemoveForm.$setUntouched(); + } + }; + + Client.onReady(function () { + Client.getDomains(function (error, result) { + if (error) return console.error(error); + + $scope.domains = result; + $scope.ready = true; + }); + }); + + document.getElementById('gcdnsKeyFileInput').onchange = readFileLocally($scope.domainConfigure.gcdnsKey, 'content', 'keyFileName'); + document.getElementById('fallbackCertFileInput').onchange = readFileLocally($scope.domainConfigure.fallbackCert, 'certificateFile', 'certificateFileName'); + document.getElementById('fallbackKeyFileInput').onchange = readFileLocally($scope.domainConfigure.fallbackCert, 'keyFile', 'keyFileName'); + + + // setup all the dialog focus handling + ['domainConfigureModal', 'domainRemoveModal'].forEach(function (id) { + $('#' + id).on('shown.bs.modal', function () { + $(this).find("[autofocus]:first").focus(); + }); + }); + + $('.modal-backdrop').remove(); +}]); diff --git a/webadmin/src/views/email.html b/webadmin/src/views/email.html index e7e477971..a0a7d2be4 100644 --- a/webadmin/src/views/email.html +++ b/webadmin/src/views/email.html @@ -27,18 +27,32 @@
    -
    -
    +

    DNS Records

    -
    +
    +
    Set the following DNS records to guarantee email delivery: @@ -196,7 +211,7 @@

    -   +   {{ record.name }} record

    @@ -211,8 +226,10 @@
    +
    +
    @@ -226,13 +243,13 @@

    -   - +   + Outbound SMTP

    -
    +

    {{ relay.value }}

    @@ -240,10 +257,10 @@
    -
    +

    -   +   IP Address Blacklist Check @@ -262,7 +279,7 @@

    - +
    diff --git a/webadmin/src/views/email.js b/webadmin/src/views/email.js index af120c72f..de43d3844 100644 --- a/webadmin/src/views/email.js +++ b/webadmin/src/views/email.js @@ -7,9 +7,16 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio $scope.user = Client.getUserInfo(); $scope.config = Client.getConfig(); $scope.dnsConfig = {}; + $scope.currentRelay = {}; $scope.relay = {}; $scope.rbl = null; - $scope.expectedDnsRecords = {}; + $scope.expectedDnsRecords = { + mx: { }, + dkim: { }, + spf: { }, + dmarc: { }, + ptr: { } + }; $scope.expectedDnsRecordsTypes = [ { name: 'MX', value: 'mx' }, { name: 'DKIM', value: 'dkim' }, @@ -81,6 +88,8 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio refresh: function () { $scope.email.refreshBusy = true; + collapseDnsRecords(); + showExpectedDnsRecords(function (error) { if (error) console.error(error); @@ -150,20 +159,56 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio } Client.setMailRelay(data, function (error) { - if (error) $scope.mailRelay.error = error.message; - else $scope.mailRelay.success = true; - $scope.mailRelay.busy = false; + + if (error) { + $scope.mailRelay.error = error.message; + return; + } + + $scope.currentRelay = data; + $scope.mailRelay.success = true; + $scope.email.refresh(); }); } }; - $scope.sendTestEmail = function () { - Client.sentTestMail($scope.user.email, function (error) { - if (error) return console.error(error); + $scope.testEmail = { + busy: false, + error: {}, - $('#testEmailSent').modal('show'); - }); + mailTo: '', + + clearForm: function () { + $scope.testEmail.mailTo = ''; + }, + + show: function () { + $scope.testEmail.error = {}; + $scope.testEmail.busy = false; + + $scope.testEmail.mailTo = $scope.user.email; + + $('#testEmailModal').modal('show'); + }, + + submit: function () { + $scope.testEmail.error = {}; + $scope.testEmail.busy = true; + + Client.sentTestMail($scope.testEmail.mailTo, function (error) { + $scope.testEmail.busy = false; + + if (error) { + $scope.testEmail.error.generic = error.message; + console.error(error); + $('#inputTestMailTo').focus(); + return; + } + + $('#testEmailModal').modal('hide'); + }); + } }; function getMailConfig() { @@ -185,6 +230,8 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio $scope.mailRelay.relay.password = ''; $scope.mailRelay.relay.serverApiToken = ''; + $scope.currentRelay = relay; + if (relay.provider === 'postmark-smtp') { $scope.mailRelay.relay.serverApiToken = relay.username; } else if (relay.provider === 'sendgrid-smtp') { @@ -203,33 +250,50 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio }); } + // TODO this currently assumes the config.fqdn is the mail domain function getDnsConfig() { - Client.getDnsConfig(function (error, dnsConfig) { + Client.getDomain($scope.config.fqdn, function (error, result) { if (error) return console.error(error); - $scope.dnsConfig = dnsConfig; + $scope.dnsConfig = result.config; }); } + function collapseDnsRecords() { + $scope.expectedDnsRecordsTypes.forEach(function (record) { + var type = record.value; + $('#collapse_dns_' + type).collapse('hide'); + }); + + $('#collapse_outbound_smtp').collapse('hide'); + $('#collapse_rbl').collapse('hide'); + } + function showExpectedDnsRecords(callback) { callback = callback || function (error) { if (error) console.error(error); }; Client.getEmailStatus(function (error, result) { if (error) return callback(error); - $scope.expectedDnsRecords = result.dns; $scope.relay = result.relay; $scope.rbl = result.rbl; // open the record details if they are not correct - for (var type in $scope.expectedDnsRecords) { + $scope.expectedDnsRecordsTypes.forEach(function (record) { + var type = record.value; + $scope.expectedDnsRecords[type] = result.dns[type] || {}; + if (!$scope.expectedDnsRecords[type].status) { $('#collapse_dns_' + type).collapse('show'); } - } + }); if (!$scope.relay.status) { - $('#collapse_dns_port').collapse('show'); + $('#collapse_outbound_smtp').collapse('show'); + } + + if (!$scope.rbl.status) { + $('#collapse_rbl').collapse('show'); } callback(null); @@ -285,5 +349,12 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio $scope.email.refresh(); }); + // setup all the dialog focus handling + ['testEmailModal'].forEach(function (id) { + $('#' + id).on('shown.bs.modal', function () { + $(this).find("[autofocus]:first").focus(); + }); + }); + $('.modal-backdrop').remove(); }]); diff --git a/webadmin/src/views/settings.html b/webadmin/src/views/settings.html index 9651eb6d9..2ea9163ed 100644 --- a/webadmin/src/views/settings.html +++ b/webadmin/src/views/settings.html @@ -91,7 +91,7 @@
    - +
    A password is required Wrong password @@ -125,7 +125,7 @@

    {{ configureBackup.error.generic }}

    - +
    @@ -183,6 +183,11 @@
    +
    + + +
    +
    @@ -403,7 +408,7 @@
    -
    +

    @@ -412,7 +417,7 @@
    -
    +

    {{ createBackup.detail || 'Syncing ...' }} @@ -420,14 +425,14 @@

    -
    +

    {{ createBackup.message }}

    {{ createBackup.result }}

    -
    +
    diff --git a/webadmin/src/views/settings.js b/webadmin/src/views/settings.js index 658277e55..8172efa0b 100644 --- a/webadmin/src/views/settings.js +++ b/webadmin/src/views/settings.js @@ -40,10 +40,15 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca { name: 'US West (Oregon)', value: 'us-west-2' }, ]; + $scope.doSpacesRegions = [ + { name: 'AMS3', value: 'https://ams3.digitaloceanspaces.com' }, + { name: 'NYC3', value: 'https://nyc3.digitaloceanspaces.com' } + ]; + $scope.storageProvider = [ { name: 'Amazon S3', value: 's3' }, + { name: 'DigitalOcean Spaces', value: 'digitalocean-spaces' }, { name: 'Google Cloud Storage', value: 'gcs' }, - { name: 'DigitalOcean Spaces NYC3', value: 'digitalocean-spaces' }, { name: 'Exoscale SOS', value: 'exoscale-sos' }, { name: 'Filesystem', value: 'filesystem' }, { name: 'Minio', value: 'minio' }, @@ -422,7 +427,6 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca backupConfig.region = 'us-east-1'; backupConfig.signatureVersion = 'v2'; } else if (backupConfig.provider === 'digitalocean-spaces') { - backupConfig.endpoint = 'https://nyc3.digitaloceanspaces.com'; backupConfig.region = 'us-east-1'; } } else if (backupConfig.provider === 'gcs'){ diff --git a/webadmin/src/views/support.js b/webadmin/src/views/support.js index 4e2da77b8..f4f43147d 100644 --- a/webadmin/src/views/support.js +++ b/webadmin/src/views/support.js @@ -31,7 +31,7 @@ angular.module('Application').controller('SupportController', ['$scope', '$locat Client.feedback($scope.feedback.type, $scope.feedback.subject, $scope.feedback.description, function (error) { if (error) { - $scope.feedback.error = error; + $scope.feedback.error = error.message; } else { $scope.feedback.success = true; resetFeedback();