Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ca60d4c8b8 | |||
| 2ce00ca0d7 | |||
| a57db15b63 | |||
| f14a8b0ab0 | |||
| 5f207716e5 | |||
| 010d48035b |
@@ -1,6 +1,7 @@
|
||||
node_modules/
|
||||
coverage/
|
||||
webadmin/dist/
|
||||
setup/splash/website/
|
||||
installer/src/certs/server.key
|
||||
|
||||
# vim swap files
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"node": true,
|
||||
"browser": true,
|
||||
"unused": true,
|
||||
"multistr": true,
|
||||
"globalstrict": true,
|
||||
"predef": [ "angular", "$" ],
|
||||
"esnext": true
|
||||
|
||||
@@ -1138,88 +1138,3 @@
|
||||
* Fix issue where internal email was not delivered when email relay is enabled
|
||||
* Fix display of DNS records when email relay is enabled
|
||||
|
||||
[1.8.5]
|
||||
* Fix issues where unused addons were not cleaned on an app update causing uninstall to fail
|
||||
* Change UI text from 'Waiting' to 'Pending'
|
||||
|
||||
[1.9.0]
|
||||
* Prepare Cloudron for supporting multiple domains
|
||||
* Add Cloudron restore UI
|
||||
* Do not put app in errored state if backup fails
|
||||
* Display backup progress in CaaS
|
||||
* Add Google Cloud Storage backend for backups
|
||||
* Update node to 8.9.3 LTS
|
||||
* Set max email recepient limit (in outgoing emails) to 500
|
||||
|
||||
[1.9.1]
|
||||
* Prepare Cloudron for supporting multiple domains
|
||||
* Add Cloudron restore UI
|
||||
* Do not put app in errored state if backup fails
|
||||
* Display backup progress in CaaS
|
||||
* Add Google Cloud Storage backend for backups
|
||||
* Update node to 8.9.3 LTS
|
||||
* Set max email recepient limit (in outgoing emails) to 500
|
||||
* Put terminal and app logs viewer to separate window
|
||||
|
||||
[1.9.2]
|
||||
* Prepare Cloudron for supporting multiple domains
|
||||
* Add Cloudron restore UI
|
||||
* Do not put app in errored state if backup fails
|
||||
* Display backup progress in CaaS
|
||||
* Add Google Cloud Storage backend for backups
|
||||
* Update node to 8.9.3 LTS
|
||||
* Set max email recepient limit (in outgoing emails) to 500
|
||||
* Put terminal and app logs viewer to separate window
|
||||
|
||||
[1.9.3]
|
||||
* Prepare Cloudron for supporting multiple domains
|
||||
* Add Cloudron restore UI
|
||||
* Do not put app in errored state if backup fails
|
||||
* Display backup progress in CaaS
|
||||
* Add Google Cloud Storage backend for backups
|
||||
* Update node to 8.9.3 LTS
|
||||
* Set max email recepient limit (in outgoing emails) to 500
|
||||
* Put terminal and app logs viewer to separate window
|
||||
|
||||
[1.9.4]
|
||||
* Fix typo causing LE cert renewals to fail
|
||||
|
||||
[1.10.0]
|
||||
* Migrate mailboxes to support multiple domains
|
||||
* Update addon containers to latest versions
|
||||
* Add DigitalOcean Spaces region Singapore 1 (SGP1)
|
||||
* Configure Exoscale SOS to use new SOS NG endpoint
|
||||
* Fix S3 storage backend CopySource encoding rules
|
||||
|
||||
[1.10.1]
|
||||
* Migrate mailboxes to support multiple domains
|
||||
* Update addon containers to latest versions
|
||||
* Add DigitalOcean Spaces region Singapore 1 (SGP1)
|
||||
* Configure Exoscale SOS to use new SOS NG endpoint
|
||||
* Fix S3 storage backend CopySource encoding rules
|
||||
|
||||
[1.10.2]
|
||||
* Migrate mailboxes to support multiple domains
|
||||
* Update addon containers to latest versions
|
||||
* Add DigitalOcean Spaces region Singapore 1 (SGP1)
|
||||
* Configure Exoscale SOS to use new SOS NG endpoint
|
||||
* Fix S3 storage backend CopySource encoding rules
|
||||
|
||||
[1.11.0]
|
||||
* Update Haraka to 2.8.17 to fix various crashes
|
||||
* Report dependency error for clone if backup or domain was not found
|
||||
* Enable auto-updates for major versions
|
||||
|
||||
[2.0.0]
|
||||
* Multi-domain support
|
||||
* Update Haraka to 2.8.18
|
||||
* Split box and app autoupdate pattern settings
|
||||
* Stop and disable any pre-installed postfix server
|
||||
* Migrate altDomain as a manual DNS provider
|
||||
* Use node's native dns resolve instead of dig
|
||||
* DNS records can now be a A record or a CNAME record
|
||||
* Fix generation of fallback certificates to include naked domain
|
||||
* Merge multi-string DKIM records
|
||||
* scheduler: do not start cron jobs all at once
|
||||
* scheduler: give cron jobs a grace period of 30 minutes to complete
|
||||
|
||||
|
||||
@@ -9,6 +9,10 @@ a complex task.
|
||||
We are building the ultimate platform for self-hosting web apps. The Cloudron allows
|
||||
anyone to effortlessly host web applications on their server on their own terms.
|
||||
|
||||
Support us on
|
||||
[](https://flattr.com/submit/auto?user_id=cloudron&url=https://cloudron.io&title=Cloudron&tags=opensource&category=software)
|
||||
or [pay us a coffee](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=8982CKNM46D8U)
|
||||
|
||||
## Features
|
||||
|
||||
* Single click install for apps. Check out the [App Store](https://cloudron.io/appstore.html).
|
||||
@@ -29,9 +33,9 @@ anyone to effortlessly host web applications on their server on their own terms.
|
||||
* 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/documentation/developer/api/).
|
||||
* Comprehensive [REST API](https://cloudron.io/references/api.html).
|
||||
|
||||
* [CLI](https://cloudron.io/documentation/cli/) to configure apps.
|
||||
* [CLI](https://git.cloudron.io/cloudron/cloudron-cli) to configure apps.
|
||||
|
||||
* Alerts, audit logs, graphs, dns management ... and much more
|
||||
|
||||
@@ -45,20 +49,35 @@ 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/documentation/installation/) - [Pricing](https://cloudron.io/pricing.html)
|
||||
* [Selfhosting](https://cloudron.io/references/selfhosting.html) - [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
|
||||
|
||||
* [Documentation](https://cloudron.io/documentation/)
|
||||
* [User manual](https://cloudron.io/references/usermanual.html)
|
||||
* [Developer docs](https://cloudron.io/documentation.html)
|
||||
* [Architecture](https://cloudron.io/references/architecture.html)
|
||||
|
||||
## Related repos
|
||||
|
||||
The [base image repo](https://git.cloudron.io/cloudron/docker-base-image) is the parent image of all
|
||||
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
|
||||
* [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
|
||||
|
||||
* [Forum](https://forum.cloudron.io/)
|
||||
* [Chat](https://chat.cloudron.io/)
|
||||
* [Support](mailto:support@cloudron.io)
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ function create_droplet() {
|
||||
local ssh_key_id="$1"
|
||||
local box_name="$2"
|
||||
|
||||
local image_region="sfo2"
|
||||
local image_region="sfo1"
|
||||
local ubuntu_image_slug="ubuntu-16-04-x64"
|
||||
local box_size="1gb"
|
||||
|
||||
|
||||
@@ -47,10 +47,10 @@ apt-get -y install \
|
||||
cp /usr/share/unattended-upgrades/20auto-upgrades /etc/apt/apt.conf.d/20auto-upgrades
|
||||
|
||||
echo "==> Installing node.js"
|
||||
mkdir -p /usr/local/node-8.9.3
|
||||
curl -sL https://nodejs.org/dist/v8.9.3/node-v8.9.3-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-8.9.3
|
||||
ln -sf /usr/local/node-8.9.3/bin/node /usr/bin/node
|
||||
ln -sf /usr/local/node-8.9.3/bin/npm /usr/bin/npm
|
||||
mkdir -p /usr/local/node-6.11.5
|
||||
curl -sL https://nodejs.org/dist/v6.11.5/node-v6.11.5-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-6.11.5
|
||||
ln -sf /usr/local/node-6.11.5/bin/node /usr/bin/node
|
||||
ln -sf /usr/local/node-6.11.5/bin/npm /usr/bin/npm
|
||||
apt-get install -y python # Install python which is required for npm rebuild
|
||||
[[ "$(python --version 2>&1)" == "Python 2.7."* ]] || die "Expecting python version to be 2.7.x"
|
||||
|
||||
@@ -105,7 +105,3 @@ systemctl disable bind9 || true
|
||||
systemctl stop dnsmasq || true
|
||||
systemctl disable dnsmasq || true
|
||||
|
||||
# on ssdnodes postfix seems to run by default
|
||||
systemctl stop postfix || true
|
||||
systemctl disable postfix || true
|
||||
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
var database = require('./src/database.js');
|
||||
|
||||
var sendFailureLogs = require('./src/logcollector').sendFailureLogs;
|
||||
|
||||
function main() {
|
||||
@@ -12,12 +10,7 @@ function main() {
|
||||
var processName = process.argv[2];
|
||||
console.log('Started crash notifier for', processName);
|
||||
|
||||
// mailer needs the db
|
||||
database.initialize(function (error) {
|
||||
if (error) return console.error('Cannot connect to database. Unable to send crash log.', error);
|
||||
|
||||
sendFailureLogs(processName, { unit: processName });
|
||||
});
|
||||
sendFailureLogs(processName, { unit: processName });
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
+205
@@ -0,0 +1,205 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
var argv = require('yargs').argv,
|
||||
autoprefixer = require('gulp-autoprefixer'),
|
||||
concat = require('gulp-concat'),
|
||||
cssnano = require('gulp-cssnano'),
|
||||
ejs = require('gulp-ejs'),
|
||||
gulp = require('gulp'),
|
||||
rimraf = require('rimraf'),
|
||||
sass = require('gulp-sass'),
|
||||
serve = require('gulp-serve'),
|
||||
sourcemaps = require('gulp-sourcemaps'),
|
||||
uglify = require('gulp-uglify'),
|
||||
url = require('url');
|
||||
|
||||
gulp.task('3rdparty', function () {
|
||||
gulp.src([
|
||||
'webadmin/src/3rdparty/**/*.js',
|
||||
'webadmin/src/3rdparty/**/*.map',
|
||||
'webadmin/src/3rdparty/**/*.css',
|
||||
'webadmin/src/3rdparty/**/*.otf',
|
||||
'webadmin/src/3rdparty/**/*.eot',
|
||||
'webadmin/src/3rdparty/**/*.svg',
|
||||
'webadmin/src/3rdparty/**/*.gif',
|
||||
'webadmin/src/3rdparty/**/*.ttf',
|
||||
'webadmin/src/3rdparty/**/*.woff',
|
||||
'webadmin/src/3rdparty/**/*.woff2'
|
||||
])
|
||||
.pipe(gulp.dest('webadmin/dist/3rdparty/'))
|
||||
.pipe(gulp.dest('setup/splash/website/3rdparty'));
|
||||
|
||||
gulp.src('node_modules/bootstrap-sass/assets/javascripts/bootstrap.min.js')
|
||||
.pipe(gulp.dest('webadmin/dist/3rdparty/js'))
|
||||
.pipe(gulp.dest('setup/splash/website/3rdparty/js'));
|
||||
});
|
||||
|
||||
|
||||
// --------------
|
||||
// JavaScript
|
||||
// --------------
|
||||
|
||||
if (argv.help || argv.h) {
|
||||
console.log('Supported arguments for "gulp develop":');
|
||||
console.log(' --client-id <clientId>');
|
||||
console.log(' --client-secret <clientSecret>');
|
||||
console.log(' --api-origin <cloudron api uri>');
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
gulp.task('js', ['js-index', 'js-setup', 'js-setupdns', 'js-update'], function () {});
|
||||
|
||||
var oauth = {
|
||||
clientId: argv.clientId || 'cid-webadmin',
|
||||
clientSecret: argv.clientSecret || 'unused',
|
||||
apiOrigin: argv.apiOrigin || '',
|
||||
apiOriginHostname: argv.apiOrigin ? url.parse(argv.apiOrigin).hostname : ''
|
||||
};
|
||||
|
||||
console.log();
|
||||
console.log('Using OAuth credentials:');
|
||||
console.log(' ClientId: %s', oauth.clientId);
|
||||
console.log(' ClientSecret: %s', oauth.clientSecret);
|
||||
console.log(' Cloudron API: %s', oauth.apiOrigin || 'default');
|
||||
console.log(' Cloudron Host: %s', oauth.apiOriginHostname);
|
||||
console.log();
|
||||
|
||||
|
||||
gulp.task('js-index', function () {
|
||||
// needs special treatment for error handling
|
||||
var uglifyer = uglify();
|
||||
uglifyer.on('error', function (error) {
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
gulp.src([
|
||||
'webadmin/src/js/index.js',
|
||||
'webadmin/src/js/client.js',
|
||||
'webadmin/src/js/appstore.js',
|
||||
'webadmin/src/js/main.js',
|
||||
'webadmin/src/views/*.js'
|
||||
])
|
||||
.pipe(ejs({ oauth: oauth }, { ext: '.js' }))
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(concat('index.js', { newLine: ';' }))
|
||||
.pipe(uglifyer)
|
||||
.pipe(sourcemaps.write())
|
||||
.pipe(gulp.dest('webadmin/dist/js'));
|
||||
});
|
||||
|
||||
gulp.task('js-setup', function () {
|
||||
// needs special treatment for error handling
|
||||
var uglifyer = uglify();
|
||||
uglifyer.on('error', function (error) {
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
gulp.src(['webadmin/src/js/setup.js', 'webadmin/src/js/client.js'])
|
||||
.pipe(ejs({ oauth: oauth }, { ext: '.js' }))
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(concat('setup.js', { newLine: ';' }))
|
||||
.pipe(uglifyer)
|
||||
.pipe(sourcemaps.write())
|
||||
.pipe(gulp.dest('webadmin/dist/js'));
|
||||
});
|
||||
|
||||
gulp.task('js-setupdns', function () {
|
||||
// needs special treatment for error handling
|
||||
var uglifyer = uglify();
|
||||
uglifyer.on('error', function (error) {
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
gulp.src(['webadmin/src/js/setupdns.js', 'webadmin/src/js/client.js'])
|
||||
.pipe(ejs({ oauth: oauth }, { ext: '.js' }))
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(concat('setupdns.js', { newLine: ';' }))
|
||||
.pipe(uglifyer)
|
||||
.pipe(sourcemaps.write())
|
||||
.pipe(gulp.dest('webadmin/dist/js'));
|
||||
});
|
||||
|
||||
gulp.task('js-update', function () {
|
||||
// needs special treatment for error handling
|
||||
var uglifyer = uglify();
|
||||
uglifyer.on('error', function (error) {
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
gulp.src(['webadmin/src/js/update.js'])
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(uglifyer)
|
||||
.pipe(sourcemaps.write())
|
||||
.pipe(gulp.dest('webadmin/dist/js'))
|
||||
.pipe(gulp.dest('setup/splash/website/js'));
|
||||
});
|
||||
|
||||
|
||||
// --------------
|
||||
// HTML
|
||||
// --------------
|
||||
|
||||
gulp.task('html', ['html-views', 'html-update', 'html-templates'], function () {
|
||||
return gulp.src('webadmin/src/*.html').pipe(ejs({ apiOriginHostname: oauth.apiOriginHostname }, { ext: '.html' })).pipe(gulp.dest('webadmin/dist'));
|
||||
});
|
||||
|
||||
gulp.task('html-update', function () {
|
||||
return gulp.src(['webadmin/src/update.html']).pipe(gulp.dest('setup/splash/website'));
|
||||
});
|
||||
|
||||
gulp.task('html-views', function () {
|
||||
return gulp.src('webadmin/src/views/**/*.html').pipe(gulp.dest('webadmin/dist/views'));
|
||||
});
|
||||
|
||||
gulp.task('html-templates', function () {
|
||||
return gulp.src('webadmin/src/templates/**/*.html').pipe(gulp.dest('webadmin/dist/templates'));
|
||||
});
|
||||
|
||||
// --------------
|
||||
// CSS
|
||||
// --------------
|
||||
|
||||
gulp.task('css', function () {
|
||||
return gulp.src('webadmin/src/*.scss')
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(sass({ includePaths: ['node_modules/bootstrap-sass/assets/stylesheets/'] }).on('error', sass.logError))
|
||||
.pipe(autoprefixer())
|
||||
.pipe(cssnano())
|
||||
.pipe(sourcemaps.write())
|
||||
.pipe(gulp.dest('webadmin/dist'))
|
||||
.pipe(gulp.dest('setup/splash/website'));
|
||||
});
|
||||
|
||||
gulp.task('images', function () {
|
||||
return gulp.src('webadmin/src/img/**')
|
||||
.pipe(gulp.dest('webadmin/dist/img'));
|
||||
});
|
||||
|
||||
// --------------
|
||||
// Utilities
|
||||
// --------------
|
||||
|
||||
gulp.task('watch', ['default'], function () {
|
||||
gulp.watch(['webadmin/src/*.scss'], ['css']);
|
||||
gulp.watch(['webadmin/src/img/*'], ['images']);
|
||||
gulp.watch(['webadmin/src/**/*.html'], ['html']);
|
||||
gulp.watch(['webadmin/src/views/*.html'], ['html-views']);
|
||||
gulp.watch(['webadmin/src/templates/*.html'], ['html-templates']);
|
||||
gulp.watch(['webadmin/src/js/update.js'], ['js-update']);
|
||||
gulp.watch(['webadmin/src/js/setup.js', 'webadmin/src/js/client.js'], ['js-setup']);
|
||||
gulp.watch(['webadmin/src/js/setupdns.js', 'webadmin/src/js/client.js'], ['js-setupdns']);
|
||||
gulp.watch(['webadmin/src/js/index.js', 'webadmin/src/js/client.js', 'webadmin/src/js/appstore.js', 'webadmin/src/js/main.js', 'webadmin/src/views/*.js'], ['js-index']);
|
||||
gulp.watch(['webadmin/src/3rdparty/**/*'], ['3rdparty']);
|
||||
});
|
||||
|
||||
gulp.task('clean', function () {
|
||||
rimraf.sync('webadmin/dist');
|
||||
rimraf.sync('setup/splash/website');
|
||||
});
|
||||
|
||||
gulp.task('default', ['clean', 'html', 'js', '3rdparty', 'images', 'css'], function () {});
|
||||
|
||||
gulp.task('develop', ['watch'], serve({ root: 'webadmin/dist', port: 4000 }));
|
||||
Executable
+32
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
'use strict';
|
||||
|
||||
var tar = require('tar-fs'),
|
||||
fs = require('fs'),
|
||||
path = require('path'),
|
||||
zlib = require('zlib');
|
||||
|
||||
if (process.argv.length < 4) {
|
||||
console.error('Usage: tarjs <cwd> <dir>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
var dir = process.argv[3];
|
||||
var cwd = process.argv[2];
|
||||
|
||||
console.error('Packing directory "'+ dir +'" from within "' + cwd + '" and stream to stdout');
|
||||
|
||||
process.chdir(cwd);
|
||||
|
||||
var stat = fs.statSync(dir);
|
||||
if (!stat.isDirectory()) throw(dir + ' is not a directory');
|
||||
|
||||
var gzipStream = zlib.createGzip({});
|
||||
|
||||
tar.pack(path.resolve(dir), {
|
||||
ignore: function (name) {
|
||||
if (name === '.') return true;
|
||||
return false;
|
||||
}
|
||||
}).pipe(gzipStream).pipe(process.stdout);
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
var async = require('async');
|
||||
|
||||
var ADMIN_GROUP_ID = 'admin'; // see constants.js
|
||||
var ADMIN_GROUP_ID = 'admin'; // see groups.js
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
async.series([
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
// WARNING!!
|
||||
// At this point the default db collation is utf8mb4_unicode_ci however we already have foreign key constraits
|
||||
// already with tables on utf8_bin charset, so we cannot convert all tables here to utf8mb4 collation without
|
||||
// a reimport from a sql dump, as foreign keys across different collations are not supported
|
||||
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
async.series([
|
||||
db.runSql.bind(db, 'ALTER TABLE appPortBindings CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
|
||||
db.runSql.bind(db, 'ALTER TABLE apps CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
|
||||
db.runSql.bind(db, 'ALTER TABLE authcodes CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
|
||||
db.runSql.bind(db, 'ALTER TABLE backups CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
|
||||
db.runSql.bind(db, 'ALTER TABLE clients CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
|
||||
db.runSql.bind(db, 'ALTER TABLE eventlog CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
|
||||
db.runSql.bind(db, 'ALTER TABLE groupMembers CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
|
||||
db.runSql.bind(db, 'ALTER TABLE groups CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
|
||||
db.runSql.bind(db, 'ALTER TABLE mailboxes CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
|
||||
db.runSql.bind(db, 'ALTER TABLE migrations CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
|
||||
db.runSql.bind(db, 'ALTER TABLE settings CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
|
||||
db.runSql.bind(db, 'ALTER TABLE tokens CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
|
||||
db.runSql.bind(db, 'ALTER TABLE users CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin')
|
||||
], callback);
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
// nothing to be done here
|
||||
callback();
|
||||
};
|
||||
@@ -1,70 +0,0 @@
|
||||
'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);
|
||||
};
|
||||
@@ -1,61 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async'),
|
||||
safe = require('safetydance'),
|
||||
tld = require('tldjs');
|
||||
|
||||
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 || ''; // will be null pre-setup
|
||||
zoneName = domain.zoneName || tld.getDomain(fqdn) || 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);
|
||||
};
|
||||
@@ -1,15 +0,0 @@
|
||||
'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);
|
||||
});
|
||||
};
|
||||
@@ -1,15 +0,0 @@
|
||||
'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);
|
||||
});
|
||||
};
|
||||
@@ -1,15 +0,0 @@
|
||||
'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);
|
||||
});
|
||||
};
|
||||
@@ -1,15 +0,0 @@
|
||||
'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);
|
||||
});
|
||||
};
|
||||
@@ -1,15 +0,0 @@
|
||||
'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);
|
||||
});
|
||||
};
|
||||
@@ -1,15 +0,0 @@
|
||||
'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);
|
||||
});
|
||||
};
|
||||
@@ -1,28 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
// NOTE: This migration is incorrect because 'caas' domain is not guaranteed to be present in all Caas cloudrons
|
||||
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();
|
||||
};
|
||||
@@ -1,23 +0,0 @@
|
||||
'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);
|
||||
});
|
||||
};
|
||||
@@ -1,33 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
// first check precondtion of domain entry in settings
|
||||
db.all('SELECT * FROM domains', [ ], function (error, domains) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.series([
|
||||
db.runSql.bind(db, 'START TRANSACTION;'),
|
||||
db.runSql.bind(db, 'ALTER TABLE domains ADD COLUMN provider VARCHAR(16) DEFAULT ""'),
|
||||
function setProvider(done) {
|
||||
async.eachSeries(domains, function (domain, iteratorCallback) {
|
||||
var config = JSON.parse(domain.configJson);
|
||||
var provider = config.provider;
|
||||
delete config.provider;
|
||||
|
||||
db.runSql('UPDATE domains SET provider = ?, configJson = ? WHERE domain = ?', [ provider, JSON.stringify(config), domain.domain ], iteratorCallback);
|
||||
}, done);
|
||||
},
|
||||
db.runSql.bind(db, 'ALTER TABLE domains MODIFY provider VARCHAR(16) NOT NULL'),
|
||||
db.runSql.bind(db, 'COMMIT')
|
||||
], callback);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE domains DROP COLUMN provider', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -1,24 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
var cmd = 'CREATE TABLE IF NOT EXISTS mail(' +
|
||||
'domain VARCHAR(128) NOT NULL UNIQUE,' +
|
||||
'enabled BOOLEAN DEFAULT 0,' +
|
||||
'mailFromValidation BOOLEAN DEFAULT 1,' +
|
||||
'catchAllJson TEXT,' +
|
||||
'relayJson TEXT,' +
|
||||
'FOREIGN KEY(domain) REFERENCES domains(domain),' +
|
||||
'PRIMARY KEY(domain)) CHARACTER SET utf8 COLLATE utf8_bin';
|
||||
|
||||
db.runSql(cmd, function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('DROP TABLE mail', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -1,34 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.all('SELECT * FROM domains', function (error, domains) {
|
||||
if (error) return callback(error);
|
||||
if (domains.length === 0) return callback();
|
||||
|
||||
db.all('SELECT * FROM settings', function (error, allSettings) {
|
||||
if (error) return callback(error);
|
||||
|
||||
// defaults
|
||||
var mailFromValidation = true;
|
||||
var catchAll = [ ];
|
||||
var relay = { provider: 'cloudron-smtp' };
|
||||
var mailEnabled = false;
|
||||
|
||||
allSettings.forEach(function (setting) {
|
||||
switch (setting.name) {
|
||||
case 'mail_from_validation': mailFromValidation = !!setting.value; break;
|
||||
case 'catch_all_address': catchAll = JSON.parse(setting.value); break;
|
||||
case 'mail_relay': relay = JSON.parse(setting.value); break;
|
||||
case 'mail_config': mailEnabled = JSON.parse(setting.value).enabled; break;
|
||||
}
|
||||
});
|
||||
|
||||
db.runSql('INSERT INTO mail (domain, enabled, mailFromValidation, catchAllJson, relayJson) VALUES (?, ?, ?, ?, ?)',
|
||||
[ domains[0].domain, mailEnabled, mailFromValidation, JSON.stringify(catchAll), JSON.stringify(relay) ], callback);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
@@ -1,44 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.all('SELECT * FROM users', [ ], function (error, users) {
|
||||
if (error) return callback(error);
|
||||
|
||||
db.all('SELECT * FROM mail WHERE enabled=1', [ ], function (error, mailDomains) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.series([
|
||||
db.runSql.bind(db, 'START TRANSACTION;'),
|
||||
db.runSql.bind(db, 'ALTER TABLE users DROP INDEX users_email'),
|
||||
db.runSql.bind(db, 'ALTER TABLE users ADD COLUMN fallbackEmail VARCHAR(512) DEFAULT ""'),
|
||||
function setDefaults(done) {
|
||||
async.eachSeries(users, function (user, iteratorCallback) {
|
||||
var defaultEmail = '';
|
||||
var fallbackEmail = '';
|
||||
|
||||
if (mailDomains.length === 0) {
|
||||
defaultEmail = user.email;
|
||||
fallbackEmail = user.email;
|
||||
} else {
|
||||
defaultEmail = user.username ? (user.username + '@' + mailDomains[0].domain) : user.email;
|
||||
fallbackEmail = user.email;
|
||||
}
|
||||
|
||||
db.runSql('UPDATE users SET email = ?, fallbackEmail = ? WHERE id = ?', [ defaultEmail, fallbackEmail, user.id ], iteratorCallback);
|
||||
}, done);
|
||||
},
|
||||
db.runSql.bind(db, 'ALTER TABLE users ADD UNIQUE users_email (email)'),
|
||||
db.runSql.bind(db, 'COMMIT')
|
||||
], callback);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE users DROP COLUMN fallbackEmail', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -1,26 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.all('SELECT * FROM settings WHERE name = ?', [ 'tls_config' ], function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var tlsConfig = (result[0] && result[0].value) ? JSON.parse(result[0].value) : { provider: 'letsencrypt-prod'};
|
||||
tlsConfig.provider = tlsConfig.provider.replace(/$le\-/, 'letsencrypt-'); // old cloudrons had le-prod/le-staging
|
||||
|
||||
async.series([
|
||||
db.runSql.bind(db, 'START TRANSACTION;'),
|
||||
db.runSql.bind(db, 'ALTER TABLE domains ADD COLUMN tlsConfigJson TEXT'),
|
||||
db.runSql.bind(db, 'UPDATE domains SET tlsConfigJson = ?', [ JSON.stringify(tlsConfig) ]),
|
||||
db.runSql.bind(db, 'COMMIT')
|
||||
], callback);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE domains DROP COLUMN tlsConfigJson', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -1,47 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async'),
|
||||
fs = require('fs'),
|
||||
superagent = require('superagent');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
if (!fs.existsSync('/home/yellowtent/configs/cloudron.conf')) {
|
||||
console.log('Unable to locate cloudron.conf');
|
||||
return callback();
|
||||
}
|
||||
|
||||
var config = JSON.parse(fs.readFileSync('/home/yellowtent/configs/cloudron.conf', 'utf8'));
|
||||
|
||||
if (config.provider !== 'caas' || !config.fqdn) {
|
||||
console.log('Not caas (%s) or no fqdn', config.provider, config.fqdn);
|
||||
return callback();
|
||||
}
|
||||
|
||||
db.runSql('SELECT COUNT(*) AS total FROM users', function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (result[0].total === 0) {
|
||||
console.log('This cloudron is not activated. It will automatically get appstore and caas configs from autoprovision logic');
|
||||
return callback();
|
||||
}
|
||||
|
||||
console.log('Downloading appstore and caas config');
|
||||
|
||||
superagent.get(config.apiServerOrigin + `/api/v1/boxes/${config.fqdn}/config`)
|
||||
.query({ token: config.token })
|
||||
.timeout(30 * 1000).end(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
console.log('Adding %j config', result.body);
|
||||
|
||||
async.series([
|
||||
db.runSql.bind(db, 'INSERT settings (name, value) VALUES(?, ?)', [ 'appstore_config', JSON.stringify(result.body.appstoreConfig) ]),
|
||||
db.runSql.bind(db, 'INSERT settings (name, value) VALUES(?, ?)', [ 'caas_config', JSON.stringify(result.body.caasConfig) ])
|
||||
], callback);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
@@ -1,24 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('SELECT * FROM settings WHERE name=?', ['autoupdate_pattern'], function (error, results) {
|
||||
if (error || results.length === 0) return callback(error); // will use defaults from box code
|
||||
|
||||
// migrate the 'daily' update pattern
|
||||
var appUpdatePattern = results[0].value;
|
||||
if (appUpdatePattern === '00 00 1,3,5,23 * * *') appUpdatePattern = '00 30 1,3,5,23 * * *';
|
||||
|
||||
async.series([
|
||||
db.runSql.bind(db, 'START TRANSACTION;'),
|
||||
db.runSql.bind(db, 'DELETE FROM settings WHERE name=?', ['autoupdate_pattern']),
|
||||
db.runSql.bind(db, 'INSERT settings (name, value) VALUES(?, ?)', ['app_autoupdate_pattern', appUpdatePattern]),
|
||||
db.runSql.bind(db, 'COMMIT')
|
||||
], callback);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
@@ -1,121 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async'),
|
||||
crypto = require('crypto'),
|
||||
fs = require('fs'),
|
||||
os = require('os'),
|
||||
path = require('path'),
|
||||
safe = require('safetydance'),
|
||||
tldjs = require('tldjs');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.all('SELECT * FROM apps', function (error, apps) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.eachSeries(apps, function (app, callback) {
|
||||
if (!app.altDomain) {
|
||||
console.log('App %s does not use altDomain, skip', app.id);
|
||||
return callback();
|
||||
}
|
||||
|
||||
const domain = tldjs.getDomain(app.altDomain);
|
||||
const subdomain = tldjs.getSubdomain(app.altDomain);
|
||||
const mailboxName = (subdomain ? subdomain : JSON.parse(app.manifestJson).title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app';
|
||||
|
||||
console.log('App %s is on domain %s and subdomain %s with mailbox', app.id, domain, subdomain, mailboxName);
|
||||
|
||||
async.series([
|
||||
// Add domain if not exists
|
||||
function (callback) {
|
||||
const query = 'INSERT INTO domains (domain, zoneName, provider, configJson, tlsConfigJson) VALUES (?, ?, ?, ?, ?)';
|
||||
const args = [ domain, domain, 'manual', JSON.stringify({}), JSON.stringify({ provider: 'letsencrypt-prod' }) ];
|
||||
|
||||
db.runSql(query, args, function (error) {
|
||||
if (error && error.code !== 'ER_DUP_ENTRY') return callback(error);
|
||||
|
||||
console.log('Added domain %s', domain);
|
||||
|
||||
// ensure we have a fallback cert for the newly added domain. This is the same as in reverseproxy.js
|
||||
// WARNING this will only work on the cloudron itself not during local testing!
|
||||
const certFilePath = `/home/yellowtent/boxdata/certs/${domain}.host.cert`;
|
||||
const keyFilePath = `/home/yellowtent/boxdata/certs/${domain}.host.key`;
|
||||
|
||||
if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) { // generate it
|
||||
let opensslConf = safe.fs.readFileSync('/etc/ssl/openssl.cnf', 'utf8');
|
||||
let opensslConfWithSan = `${opensslConf}\n[SAN]\nsubjectAltName=DNS:${domain}\n`;
|
||||
let configFile = path.join(os.tmpdir(), 'openssl-' + crypto.randomBytes(4).readUInt32LE(0) + '.conf');
|
||||
let certCommand = `openssl req -x509 -newkey rsa:2048 -keyout ${keyFilePath} -out ${certFilePath} -days 3650 -subj /CN=*.${domain} -extensions SAN -config ${configFile} -nodes`;
|
||||
|
||||
safe.fs.writeFileSync(configFile, opensslConfWithSan, 'utf8');
|
||||
if (!safe.child_process.execSync(certCommand)) return callback(safe.error.message);
|
||||
safe.fs.unlinkSync(configFile);
|
||||
}
|
||||
|
||||
callback();
|
||||
});
|
||||
},
|
||||
// Add domain to mail table if not exists
|
||||
function (callback) {
|
||||
const query = 'INSERT INTO mail (domain, enabled, mailFromValidation, catchAllJson, relayJson) VALUES (?, ?, ?, ?, ?)';
|
||||
const args = [ domain, 0, 1, '[]', JSON.stringify({ provider: 'cloudron-smtp' }) ];
|
||||
|
||||
db.runSql(query, args, function (error) {
|
||||
if (error && error.code !== 'ER_DUP_ENTRY') return callback(error);
|
||||
|
||||
console.log('Added domain %s to mail table', domain);
|
||||
|
||||
callback();
|
||||
});
|
||||
},
|
||||
// Remove old mailbox record if any
|
||||
function (callback) {
|
||||
const query = 'DELETE FROM mailboxes WHERE ownerId=?';
|
||||
const args = [ app.id ];
|
||||
|
||||
db.runSql(query, args, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
console.log('Cleaned up mailbox record for app %s', app.id);
|
||||
|
||||
callback();
|
||||
});
|
||||
},
|
||||
// Add new mailbox record
|
||||
function (callback) {
|
||||
const query = 'INSERT INTO mailboxes (name, domain, ownerId, ownerType) VALUES (?, ?, ?, ?)';
|
||||
const args = [ mailboxName, domain, app.id, 'app' /* mailboxdb.TYPE_APP */ ];
|
||||
|
||||
db.runSql(query, args, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
console.log('Added mailbox record for app %s', app.id);
|
||||
|
||||
callback();
|
||||
});
|
||||
},
|
||||
// Update app record
|
||||
function (callback) {
|
||||
const query = 'UPDATE apps SET location=?, domain=?, altDomain=? WHERE id=?';
|
||||
const args = [ subdomain, domain, '', app.id ];
|
||||
|
||||
db.runSql(query, args, function (error) {
|
||||
if (error) return error;
|
||||
|
||||
console.log('Updated app %s with new domain', app.id);
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
], callback);
|
||||
}, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
// finally drop the altDomain db field
|
||||
db.runSql('ALTER TABLE apps DROP COLUMN altDomain', [], callback);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps ADD COLUMN altDomain VARCHAR(256)', [], callback);
|
||||
};
|
||||
@@ -1,19 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
async.series([
|
||||
db.runSql.bind(db, 'START TRANSACTION;'),
|
||||
db.runSql.bind(db, 'ALTER TABLE mailboxes DROP FOREIGN KEY mailboxes_domain_constraint'),
|
||||
db.runSql.bind(db, 'ALTER TABLE mailboxes ADD CONSTRAINT mailboxes_domain_constraint FOREIGN KEY(domain) REFERENCES mail(domain)'),
|
||||
db.runSql.bind(db, 'COMMIT')
|
||||
], callback);
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE mailboxes DROP FOREIGN KEY mailboxes_domain_constraint', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
+6
-42
@@ -9,10 +9,6 @@
|
||||
#### 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,
|
||||
@@ -23,8 +19,6 @@ CREATE TABLE IF NOT EXISTS users(
|
||||
modifiedAt VARCHAR(512) NOT NULL,
|
||||
admin INTEGER NOT NULL,
|
||||
displayName VARCHAR(512) DEFAULT '',
|
||||
fallbackEmail VARCHAR(512) DEFAULT ""
|
||||
|
||||
PRIMARY KEY(id));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS groups(
|
||||
@@ -65,25 +59,23 @@ 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,
|
||||
domain VARCHAR(128) NOT NULL,
|
||||
location VARCHAR(128) NOT NULL UNIQUE,
|
||||
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, // misnomer: controls automatic daily backups
|
||||
enableBackup BOOLEAN DEFAULT 1,
|
||||
|
||||
// the following fields do not belong here, they can be removed when we use a queue for apptask
|
||||
restoreConfigJson VARCHAR(256), // used to pass backupId to restore from to apptask
|
||||
oldConfigJson TEXT, // used to pass old config to apptask (configure, restore)
|
||||
updateConfigJson TEXT, // used to pass new config to apptask (update)
|
||||
oldConfigJson TEXT, // used to pass old config for apptask (configure, restore)
|
||||
updateConfigJson TEXT, // used to pass new config for apptask (update)
|
||||
|
||||
FOREIGN KEY(domain) REFERENCES domains(domain),
|
||||
PRIMARY KEY(id));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS appPortBindings(
|
||||
@@ -133,31 +125,6 @@ CREATE TABLE IF NOT EXISTS eventlog(
|
||||
|
||||
PRIMARY KEY (id));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS domains(
|
||||
domain VARCHAR(128) NOT NULL UNIQUE, /* if this needs to be larger, InnoDB has a limit of 767 bytes for PRIMARY KEY values! */
|
||||
zoneName VARCHAR(128) NOT NULL, /* this mostly contains the domain itself again */
|
||||
provider VARCHAR(16) NOT NULL,
|
||||
configJson TEXT, /* JSON containing the dns backend provider config */
|
||||
tlsConfigJson TEXT, /* JSON containing the tls provider config */
|
||||
|
||||
PRIMARY KEY (domain))
|
||||
|
||||
/* the default db collation is utf8mb4_unicode_ci but for the app table domain constraint we have to use the old one */
|
||||
CHARACTER SET utf8 COLLATE utf8_bin;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS mail(
|
||||
domain VARCHAR(128) NOT NULL UNIQUE,
|
||||
|
||||
enabled BOOLEAN DEFAULT 0, /* MDA enabled */
|
||||
mailFromValidation BOOLEAN DEFAULT 1,
|
||||
catchAllJson TEXT,
|
||||
relayJson TEXT,
|
||||
|
||||
FOREIGN KEY(domain) REFERENCES domains(domain),
|
||||
PRIMARY KEY(domain))
|
||||
|
||||
CHARACTER SET utf8 COLLATE utf8_bin;
|
||||
|
||||
/* Future fields:
|
||||
* accessRestriction - to determine who can access it. So this has foreign keys
|
||||
* quota - per mailbox quota
|
||||
@@ -168,8 +135,5 @@ 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 mail(domain),
|
||||
UNIQUE (name, domain));
|
||||
|
||||
PRIMARY KEY (name));
|
||||
|
||||
Generated
+5447
File diff suppressed because it is too large
Load Diff
Generated
-8941
File diff suppressed because it is too large
Load Diff
+22
-21
@@ -14,13 +14,12 @@
|
||||
"node": ">=4.0.0 <=4.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google-cloud/dns": "^0.7.1",
|
||||
"@google-cloud/storage": "^1.6.0",
|
||||
"@google-cloud/dns": "^0.7.0",
|
||||
"@sindresorhus/df": "^2.1.0",
|
||||
"async": "^2.6.0",
|
||||
"aws-sdk": "^2.201.0",
|
||||
"aws-sdk": "^2.151.0",
|
||||
"body-parser": "^1.18.2",
|
||||
"cloudron-manifestformat": "^2.11.0",
|
||||
"cloudron-manifestformat": "^2.10.0",
|
||||
"connect-ensure-login": "^0.1.1",
|
||||
"connect-lastmile": "^1.0.2",
|
||||
"connect-timeout": "^1.9.0",
|
||||
@@ -28,24 +27,26 @@
|
||||
"cookie-session": "^1.3.2",
|
||||
"cron": "^1.3.0",
|
||||
"csurf": "^1.6.6",
|
||||
"db-migrate": "^0.10.5",
|
||||
"db-migrate": "^0.10.0-beta.24",
|
||||
"db-migrate-mysql": "^1.1.10",
|
||||
"debug": "^3.1.0",
|
||||
"dockerode": "^2.5.4",
|
||||
"dockerode": "^2.5.3",
|
||||
"ejs": "^2.5.7",
|
||||
"ejs-cli": "^2.0.0",
|
||||
"express": "^4.16.2",
|
||||
"express-session": "^1.15.6",
|
||||
"gulp-sass": "^3.0.0",
|
||||
"hat": "0.0.3",
|
||||
"hock": "https://registry.npmjs.org/hock/-/hock-1.3.2.tgz",
|
||||
"json": "^9.0.3",
|
||||
"ldapjs": "^1.0.2",
|
||||
"ldapjs": "^1.0.0",
|
||||
"lodash.chunk": "^4.2.0",
|
||||
"mime": "^2.2.0",
|
||||
"mime": "^2.0.3",
|
||||
"moment-timezone": "^0.5.14",
|
||||
"morgan": "^1.9.0",
|
||||
"multiparty": "^4.1.2",
|
||||
"mysql": "^2.15.0",
|
||||
"nodemailer": "^4.6.0",
|
||||
"nodemailer": "^4.4.0",
|
||||
"nodemailer-smtp-transport": "^2.7.4",
|
||||
"oauth2orize": "^1.11.0",
|
||||
"once": "^1.3.2",
|
||||
@@ -58,23 +59,22 @@
|
||||
"password-generator": "^2.2.0",
|
||||
"progress-stream": "^2.0.0",
|
||||
"proxy-middleware": "^0.15.0",
|
||||
"recursive-readdir": "^2.2.1",
|
||||
"request": "^2.83.0",
|
||||
"s3-block-read-stream": "^0.2.0",
|
||||
"safetydance": "^0.7.1",
|
||||
"semver": "^5.5.0",
|
||||
"semver": "^5.4.1",
|
||||
"showdown": "^1.8.2",
|
||||
"split": "^1.0.0",
|
||||
"superagent": "^3.8.1",
|
||||
"supererror": "^0.7.1",
|
||||
"tar-fs": "^1.16.0",
|
||||
"tar-stream": "^1.5.5",
|
||||
"tldjs": "^2.3.1",
|
||||
"tldjs": "^2.2.0",
|
||||
"underscore": "^1.7.0",
|
||||
"uuid": "^3.2.1",
|
||||
"uuid": "^3.1.0",
|
||||
"valid-url": "^1.0.9",
|
||||
"validator": "^9.4.1",
|
||||
"ws": "^3.3.3"
|
||||
"validator": "^9.1.1",
|
||||
"ws": "^3.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"bootstrap-sass": "^3.3.3",
|
||||
@@ -84,25 +84,26 @@
|
||||
"gulp-concat": "^2.4.3",
|
||||
"gulp-cssnano": "^2.1.0",
|
||||
"gulp-ejs": "^3.1.0",
|
||||
"gulp-sass": "^3.1.0",
|
||||
"gulp-sass": "^3.0.0",
|
||||
"gulp-serve": "^1.0.0",
|
||||
"gulp-sourcemaps": "^2.6.1",
|
||||
"gulp-uglify": "^3.0.0",
|
||||
"hock": "^1.3.2",
|
||||
"hock": "~1.2.0",
|
||||
"istanbul": "*",
|
||||
"js2xmlparser": "^3.0.0",
|
||||
"mocha": "^5.0.1",
|
||||
"mocha": "*",
|
||||
"mock-aws-s3": "git+https://github.com/cloudron-io/mock-aws-s3.git",
|
||||
"nock": "^9.0.14",
|
||||
"node-sass": "^4.6.1",
|
||||
"readdirp": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz",
|
||||
"yargs": "^10.1.2"
|
||||
"request": "^2.65.0",
|
||||
"yargs": "^10.0.3"
|
||||
},
|
||||
"scripts": {
|
||||
"migrate_local": "DATABASE_URL=mysql://root:@localhost/box node_modules/.bin/db-migrate up",
|
||||
"migrate_test": "BOX_ENV=test DATABASE_URL=mysql://root:@localhost/boxtest node_modules/.bin/db-migrate up",
|
||||
"test": "npm run migrate_test && src/test/setupTest && BOX_ENV=test ./node_modules/istanbul/lib/cli.js test $1 ./node_modules/mocha/bin/_mocha -- --exit -R spec ./src/test ./src/routes/test/[^a]*",
|
||||
"test_all": "npm run migrate_test && src/test/setupTest && BOX_ENV=test ./node_modules/istanbul/lib/cli.js test $1 ./node_modules/mocha/bin/_mocha -- --exit -R spec ./src/test ./src/routes/test",
|
||||
"test": "npm run migrate_test && src/test/setupTest && BOX_ENV=test ./node_modules/istanbul/lib/cli.js test $1 ./node_modules/mocha/bin/_mocha -- -R spec ./src/test ./src/routes/test/[^a]*",
|
||||
"test_all": "npm run migrate_test && src/test/setupTest && BOX_ENV=test ./node_modules/istanbul/lib/cli.js test $1 ./node_modules/mocha/bin/_mocha -- -R spec ./src/test ./src/routes/test",
|
||||
"postmerge": "/bin/true",
|
||||
"precommit": "/bin/true",
|
||||
"prepush": "npm test",
|
||||
|
||||
+137
-57
@@ -26,10 +26,6 @@ readonly physical_memory=$(LC_ALL=C free -m | awk '/Mem:/ { print $2 }')
|
||||
readonly disk_size_bytes=$(LC_ALL=C df --output=size / | tail -n1)
|
||||
readonly disk_size_gb=$((${disk_size_bytes}/1024/1024))
|
||||
|
||||
readonly RED='\033[31m'
|
||||
readonly GREEN='\033[32m'
|
||||
readonly DONE='\033[m'
|
||||
|
||||
# verify the system has minimum requirements met
|
||||
if [[ "${rootfs_type}" != "ext4" ]]; then
|
||||
echo "Error: Cloudron requires '/' to be ext4" # see #364
|
||||
@@ -48,37 +44,60 @@ fi
|
||||
|
||||
initBaseImage="true"
|
||||
# provisioning data
|
||||
domain=""
|
||||
adminLocation="my"
|
||||
zoneName=""
|
||||
provider=""
|
||||
encryptionKey=""
|
||||
restoreUrl=""
|
||||
dnsProvider="manual"
|
||||
tlsProvider="le-prod"
|
||||
requestedVersion=""
|
||||
apiServerOrigin="https://api.cloudron.io"
|
||||
webServerOrigin="https://cloudron.io"
|
||||
dataJson=""
|
||||
prerelease="false"
|
||||
sourceTarballUrl=""
|
||||
rebootServer="true"
|
||||
baseDataDir=""
|
||||
|
||||
args=$(getopt -o "" -l "help,skip-baseimage-init,data-dir:,provider:,version:,env:,prerelease,skip-reboot" -n "$0" -- "$@")
|
||||
# TODO this is still there for the restore case, see other occasions below
|
||||
versionsUrl="https://s3.amazonaws.com/prod-cloudron-releases/versions.json"
|
||||
|
||||
args=$(getopt -o "" -l "domain:,help,skip-baseimage-init,data:,data-dir:,provider:,encryption-key:,restore-url:,tls-provider:,version:,dns-provider:,env:,admin-location:,prerelease,skip-reboot,source-url:" -n "$0" -- "$@")
|
||||
eval set -- "${args}"
|
||||
|
||||
while true; do
|
||||
case "$1" in
|
||||
--domain) domain="$2"; shift 2;;
|
||||
--admin-location) adminLocation="$2"; shift 2;;
|
||||
--help) echo "See https://cloudron.io/documentation/installation/ on how to install Cloudron"; exit 0;;
|
||||
--provider) provider="$2"; shift 2;;
|
||||
--encryption-key) encryptionKey="$2"; shift 2;;
|
||||
--restore-url) restoreUrl="$2"; shift 2;;
|
||||
--tls-provider) tlsProvider="$2"; shift 2;;
|
||||
--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"
|
||||
prerelease="true"
|
||||
fi
|
||||
shift 2;;
|
||||
--skip-baseimage-init) initBaseImage="false"; shift;;
|
||||
--skip-reboot) rebootServer="false"; shift;;
|
||||
--data) dataJson="$2"; shift 2;;
|
||||
--prerelease) prerelease="true"; shift;;
|
||||
--source-url) sourceTarballUrl="$2"; version="0.0.1+custom"; shift 2;;
|
||||
--data-dir) baseDataDir=$(realpath "$2"); shift 2;;
|
||||
--) break;;
|
||||
*) echo "Unknown option $1"; exit 1;;
|
||||
@@ -86,34 +105,47 @@ while true; do
|
||||
done
|
||||
|
||||
# validate arguments in the absence of data
|
||||
if [[ -z "${provider}" ]]; then
|
||||
echo "--provider is required (azure, cloudscale, digitalocean, ec2, exoscale, hetzner, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic)"
|
||||
exit 1
|
||||
elif [[ \
|
||||
"${provider}" != "ami" && \
|
||||
"${provider}" != "azure" && \
|
||||
"${provider}" != "caas" && \
|
||||
"${provider}" != "cloudscale" && \
|
||||
"${provider}" != "digitalocean" && \
|
||||
"${provider}" != "ec2" && \
|
||||
"${provider}" != "exoscale" && \
|
||||
"${provider}" != "gce" && \
|
||||
"${provider}" != "hetzner" && \
|
||||
"${provider}" != "lightsail" && \
|
||||
"${provider}" != "linode" && \
|
||||
"${provider}" != "ovh" && \
|
||||
"${provider}" != "rosehosting" && \
|
||||
"${provider}" != "scaleway" && \
|
||||
"${provider}" != "vultr" && \
|
||||
"${provider}" != "generic" \
|
||||
]]; then
|
||||
echo "--provider must be one of: azure, cloudscale.ch, digitalocean, ec2, exoscale, gce, hetzner, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic"
|
||||
exit 1
|
||||
fi
|
||||
if [[ -z "${dataJson}" ]]; then
|
||||
if [[ -z "${provider}" ]]; then
|
||||
echo "--provider is required (azure, cloudscale.ch, digitalocean, ec2, exoscale, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic)"
|
||||
exit 1
|
||||
elif [[ \
|
||||
"${provider}" != "ami" && \
|
||||
"${provider}" != "azure" && \
|
||||
"${provider}" != "cloudscale.ch" && \
|
||||
"${provider}" != "digitalocean" && \
|
||||
"${provider}" != "ec2" && \
|
||||
"${provider}" != "exoscale" && \
|
||||
"${provider}" != "gce" && \
|
||||
"${provider}" != "lightsail" && \
|
||||
"${provider}" != "linode" && \
|
||||
"${provider}" != "ovh" && \
|
||||
"${provider}" != "rosehosting" && \
|
||||
"${provider}" != "scaleway" && \
|
||||
"${provider}" != "vultr" && \
|
||||
"${provider}" != "generic" \
|
||||
]]; then
|
||||
echo "--provider must be one of: azure, cloudscale.ch, digitalocean, ec2, exoscale, gce, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -n "${baseDataDir}" && ! -d "${baseDataDir}" ]]; then
|
||||
echo "${baseDataDir} does not exist"
|
||||
exit 1
|
||||
if [[ "${tlsProvider}" != "fallback" && "${tlsProvider}" != "le-prod" && "${tlsProvider}" != "le-staging" ]]; then
|
||||
echo "--tls-provider must be one of: le-prod, le-staging, fallback"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -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
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
@@ -124,7 +156,7 @@ echo ""
|
||||
echo " Follow setup logs in a second terminal with:"
|
||||
echo " $ tail -f ${LOG_FILE}"
|
||||
echo ""
|
||||
echo " Join us at https://forum.cloudron.io for any questions."
|
||||
echo " Join us at https://chat.cloudron.io for any questions."
|
||||
echo ""
|
||||
|
||||
if [[ "${initBaseImage}" == "true" ]]; then
|
||||
@@ -141,33 +173,76 @@ if [[ "${initBaseImage}" == "true" ]]; then
|
||||
fi
|
||||
|
||||
echo "=> Checking version"
|
||||
if ! releaseJson=$($curl -s "${apiServerOrigin}/api/v1/releases?prerelease=${prerelease}&boxVersion=${requestedVersion}"); then
|
||||
echo "Failed to get release information"
|
||||
exit 1
|
||||
fi
|
||||
if [[ "${sourceTarballUrl}" == "" ]]; then
|
||||
if ! releaseJson=$($curl -s "${apiServerOrigin}/api/v1/releases?prerelease=${prerelease}&boxVersion=${requestedVersion}"); then
|
||||
echo "Failed to get release information"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$requestedVersion" == "" ]]; then
|
||||
version=$(echo "${releaseJson}" | python3 -c 'import json,sys;obj=json.load(sys.stdin);print(obj["version"])')
|
||||
else
|
||||
version="${requestedVersion}"
|
||||
fi
|
||||
if [[ "$requestedVersion" == "" ]]; then
|
||||
version=$(echo "${releaseJson}" | python3 -c 'import json,sys;obj=json.load(sys.stdin);print(obj["version"])')
|
||||
else
|
||||
version="${requestedVersion}"
|
||||
fi
|
||||
|
||||
if ! sourceTarballUrl=$(echo "${releaseJson}" | python3 -c 'import json,sys;obj=json.load(sys.stdin);print(obj["info"]["sourceTarballUrl"])'); then
|
||||
echo "No source code for version '${requestedVersion:-latest}'"
|
||||
exit 1
|
||||
if ! sourceTarballUrl=$(echo "${releaseJson}" | python3 -c 'import json,sys;obj=json.load(sys.stdin);print(obj["info"]["sourceTarballUrl"])'); then
|
||||
echo "No source code for version '${requestedVersion:-latest}'"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Build data
|
||||
# from 1.9, we use autoprovision.json
|
||||
data=$(cat <<EOF
|
||||
{
|
||||
"provider": "${provider}",
|
||||
"apiServerOrigin": "${apiServerOrigin}",
|
||||
"webServerOrigin": "${webServerOrigin}",
|
||||
"version": "${version}"
|
||||
}
|
||||
# TODO versionsUrl is still there for the cloudron restore case
|
||||
if [[ -z "${dataJson}" ]]; then
|
||||
if [[ -z "${restoreUrl}" ]]; then
|
||||
data=$(cat <<EOF
|
||||
{
|
||||
"boxVersionsUrl": "${versionsUrl}",
|
||||
"fqdn": "${domain}",
|
||||
"adminLocation": "${adminLocation}",
|
||||
"zoneName": "${zoneName}",
|
||||
"provider": "${provider}",
|
||||
"apiServerOrigin": "${apiServerOrigin}",
|
||||
"webServerOrigin": "${webServerOrigin}",
|
||||
"tlsConfig": {
|
||||
"provider": "${tlsProvider}"
|
||||
},
|
||||
"dnsConfig": {
|
||||
"provider": "${dnsProvider}"
|
||||
},
|
||||
"backupConfig" : {
|
||||
"provider": "filesystem",
|
||||
"backupFolder": "/var/backups",
|
||||
"key": "${encryptionKey}",
|
||||
"format": "tgz",
|
||||
"retentionSecs": 172800
|
||||
},
|
||||
"version": "${version}"
|
||||
}
|
||||
EOF
|
||||
)
|
||||
)
|
||||
else
|
||||
data=$(cat <<EOF
|
||||
{
|
||||
"boxVersionsUrl": "${versionsUrl}",
|
||||
"fqdn": "${domain}",
|
||||
"adminLocation": "${adminLocation}",
|
||||
"zoneName": "${zoneName}",
|
||||
"provider": "${provider}",
|
||||
"apiServerOrigin": "${apiServerOrigin}",
|
||||
"webServerOrigin": "${webServerOrigin}",
|
||||
"restore": {
|
||||
"url": "${restoreUrl}",
|
||||
"key": "${encryptionKey}"
|
||||
},
|
||||
"version": "${version}"
|
||||
}
|
||||
EOF
|
||||
)
|
||||
fi
|
||||
else
|
||||
data="${dataJson}"
|
||||
fi
|
||||
|
||||
echo "=> Downloading version ${version} ..."
|
||||
box_src_tmp_dir=$(mktemp -dt box-src-XXXXXX)
|
||||
@@ -198,15 +273,20 @@ echo -n "=> Waiting for cloudron to be ready (this takes some time) ..."
|
||||
while true; do
|
||||
echo -n "."
|
||||
if status=$($curl -q -f "http://localhost:3000/api/v1/cloudron/status" 2>/dev/null); then
|
||||
break # we are up and running
|
||||
[[ -z "$domain" ]] && break # with no domain, we are up and running
|
||||
[[ "$status" == *"\"tls\": true"* ]] && break # with a domain, wait for the cert
|
||||
fi
|
||||
sleep 10
|
||||
done
|
||||
|
||||
echo -e "\n\n${GREEN}Visit https://<IP> to finish setup once the server has rebooted.${DONE}"
|
||||
if [[ -n "${domain}" ]]; then
|
||||
echo -e "\n\nVisit https://my.${domain} to finish setup once the server has rebooted.\n"
|
||||
else
|
||||
echo -e "\n\nVisit https://<IP> to finish setup once the server has rebooted.\n"
|
||||
fi
|
||||
|
||||
if [[ "${rebootServer}" == "true" ]]; then
|
||||
echo -e "\nRebooting this server now to let changes take effect.\n"
|
||||
echo -e "\n\nRebooting this server now to let bootloader changes take effect.\n"
|
||||
systemctl stop mysql # sometimes mysql ends up having corrupt privilege tables
|
||||
systemctl reboot
|
||||
fi
|
||||
|
||||
@@ -7,15 +7,17 @@ set -eu
|
||||
[[ $(uname -s) == "Darwin" ]] && GNU_GETOPT="/usr/local/opt/gnu-getopt/bin/getopt" || GNU_GETOPT="getopt"
|
||||
readonly GNU_GETOPT
|
||||
|
||||
args=$(${GNU_GETOPT} -o "" -l "output:" -n "$0" -- "$@")
|
||||
args=$(${GNU_GETOPT} -o "" -l "revision:,output:" -n "$0" -- "$@")
|
||||
eval set -- "${args}"
|
||||
|
||||
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
commitish="HEAD"
|
||||
bundle_file=""
|
||||
|
||||
while true; do
|
||||
case "$1" in
|
||||
--revision) commitish="$2"; shift 2;;
|
||||
--output) bundle_file="$2"; shift 2;;
|
||||
--) break;;
|
||||
*) echo "Unknown option $1"; exit 1;;
|
||||
@@ -25,37 +27,24 @@ done
|
||||
readonly TMPDIR=${TMPDIR:-/tmp} # why is this not set on mint?
|
||||
|
||||
if ! $(cd "${SOURCE_DIR}" && git diff --exit-code >/dev/null); then
|
||||
echo "You have local changes in box, stash or commit them to proceed"
|
||||
echo "You have local changes, stash or commit them to proceed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! $(cd "${SOURCE_DIR}/../webadmin" && git diff --exit-code >/dev/null); then
|
||||
echo "You have local changes in webadmin, stash or commit them to proceed"
|
||||
if [[ "$(node --version)" != "v6.11.5" ]]; then
|
||||
echo "This script requires node 6.11.5"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$(node --version)" != "v8.9.3" ]]; then
|
||||
echo "This script requires node 8.9.3"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
box_version=$(cd "${SOURCE_DIR}" && git rev-parse "HEAD")
|
||||
branch=$(git rev-parse --abbrev-ref HEAD)
|
||||
if [[ "${branch}" == "master" ]]; then
|
||||
webadmin_version=$(cd "${SOURCE_DIR}/../webadmin" && git rev-parse "${branch}")
|
||||
else
|
||||
webadmin_version=$(cd "${SOURCE_DIR}/../webadmin" && git fetch && git rev-parse "origin/${branch}")
|
||||
fi
|
||||
version=$(cd "${SOURCE_DIR}" && git rev-parse "${commitish}")
|
||||
bundle_dir=$(mktemp -d -t box 2>/dev/null || mktemp -d box-XXXXXXXXXX --tmpdir=$TMPDIR)
|
||||
[[ -z "$bundle_file" ]] && bundle_file="${TMPDIR}/box-${box_version:0:10}-${webadmin_version:0:10}.tar.gz"
|
||||
[[ -z "$bundle_file" ]] && bundle_file="${TMPDIR}/box-${version}.tar.gz"
|
||||
|
||||
chmod "o+rx,g+rx" "${bundle_dir}" # otherwise extracted tarball director won't be readable by others/group
|
||||
echo "Checking out code box version [${box_version}] and webadmin version [${webadmin_version}] into ${bundle_dir}"
|
||||
(cd "${SOURCE_DIR}" && git archive --format=tar ${box_version} | (cd "${bundle_dir}" && tar xf -))
|
||||
(cd "${SOURCE_DIR}/../webadmin" && git archive --format=tar ${webadmin_version} | (cd "${bundle_dir}" && tar xf -))
|
||||
(cp "${SOURCE_DIR}/../webadmin/LICENSE" "${bundle_dir}")
|
||||
echo "Checking out code [${version}] into ${bundle_dir}"
|
||||
(cd "${SOURCE_DIR}" && git archive --format=tar ${version} | (cd "${bundle_dir}" && tar xf -))
|
||||
|
||||
if diff "${TMPDIR}/boxtarball.cache/package-lock.json.all" "${bundle_dir}/package-lock.json" >/dev/null 2>&1; then
|
||||
if diff "${TMPDIR}/boxtarball.cache/npm-shrinkwrap.json.all" "${bundle_dir}/npm-shrinkwrap.json" >/dev/null 2>&1; then
|
||||
echo "Reusing dev modules from cache"
|
||||
cp -r "${TMPDIR}/boxtarball.cache/node_modules-all/." "${bundle_dir}/node_modules"
|
||||
else
|
||||
@@ -65,7 +54,7 @@ else
|
||||
echo "Caching dev dependencies"
|
||||
mkdir -p "${TMPDIR}/boxtarball.cache/node_modules-all"
|
||||
rsync -a --delete "${bundle_dir}/node_modules/" "${TMPDIR}/boxtarball.cache/node_modules-all/"
|
||||
cp "${bundle_dir}/package-lock.json" "${TMPDIR}/boxtarball.cache/package-lock.json.all"
|
||||
cp "${bundle_dir}/npm-shrinkwrap.json" "${TMPDIR}/boxtarball.cache/npm-shrinkwrap.json.all"
|
||||
fi
|
||||
|
||||
echo "Building webadmin assets"
|
||||
@@ -76,7 +65,7 @@ rm -rf "${bundle_dir}/node_modules/"
|
||||
rm -rf "${bundle_dir}/webadmin/src"
|
||||
rm -rf "${bundle_dir}/gulpfile.js"
|
||||
|
||||
if diff "${TMPDIR}/boxtarball.cache/package-lock.json.prod" "${bundle_dir}/package-lock.json" >/dev/null 2>&1; then
|
||||
if diff "${TMPDIR}/boxtarball.cache/npm-shrinkwrap.json.prod" "${bundle_dir}/npm-shrinkwrap.json" >/dev/null 2>&1; then
|
||||
echo "Reusing prod modules from cache"
|
||||
cp -r "${TMPDIR}/boxtarball.cache/node_modules-prod/." "${bundle_dir}/node_modules"
|
||||
else
|
||||
@@ -86,7 +75,7 @@ else
|
||||
echo "Caching prod dependencies"
|
||||
mkdir -p "${TMPDIR}/boxtarball.cache/node_modules-prod"
|
||||
rsync -a --delete "${bundle_dir}/node_modules/" "${TMPDIR}/boxtarball.cache/node_modules-prod/"
|
||||
cp "${bundle_dir}/package-lock.json" "${TMPDIR}/boxtarball.cache/package-lock.json.prod"
|
||||
cp "${bundle_dir}/npm-shrinkwrap.json" "${TMPDIR}/boxtarball.cache/npm-shrinkwrap.json.prod"
|
||||
fi
|
||||
|
||||
echo "Create final tarball"
|
||||
|
||||
+15
-14
@@ -40,22 +40,22 @@ if [[ $(docker version --format {{.Client.Version}}) != "17.09.0-ce" ]]; then
|
||||
|
||||
# https://download.docker.com/linux/ubuntu/dists/xenial/stable/binary-amd64/Packages
|
||||
if [[ $(sha256sum /tmp/docker.deb | cut -d' ' -f1) != "d33f6eb134f0ab0876148bd96de95ea47d583d7f2cddfdc6757979453f9bd9bf" ]]; then
|
||||
echo "==> installer: docker binary download is corrupt"
|
||||
echo "docker binary download is corrupt"
|
||||
exit 5
|
||||
fi
|
||||
|
||||
echo "==> installer: Waiting for all dpkg tasks to finish..."
|
||||
echo "Waiting for all dpkg tasks to finish..."
|
||||
while fuser /var/lib/dpkg/lock; do
|
||||
sleep 1
|
||||
done
|
||||
|
||||
while ! dpkg --force-confold --configure -a; do
|
||||
echo "==> installer: Failed to fix packages. Retry"
|
||||
echo "Failed to fix packages. Retry"
|
||||
sleep 1
|
||||
done
|
||||
|
||||
while ! apt install -y /tmp/docker.deb; do
|
||||
echo "==> installer: Failed to install docker. Retry"
|
||||
echo "Failed to install docker. Retry"
|
||||
sleep 1
|
||||
done
|
||||
|
||||
@@ -63,12 +63,12 @@ if [[ $(docker version --format {{.Client.Version}}) != "17.09.0-ce" ]]; then
|
||||
fi
|
||||
|
||||
echo "==> installer: updating node"
|
||||
if [[ "$(node --version)" != "v8.9.3" ]]; then
|
||||
mkdir -p /usr/local/node-8.9.3
|
||||
$curl -sL https://nodejs.org/dist/v8.9.3/node-v8.9.3-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-8.9.3
|
||||
ln -sf /usr/local/node-8.9.3/bin/node /usr/bin/node
|
||||
ln -sf /usr/local/node-8.9.3/bin/npm /usr/bin/npm
|
||||
rm -rf /usr/local/node-6.11.5
|
||||
if [[ "$(node --version)" != "v6.11.5" ]]; then
|
||||
mkdir -p /usr/local/node-6.11.5
|
||||
$curl -sL https://nodejs.org/dist/v6.11.5/node-v6.11.5-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-6.11.5
|
||||
ln -sf /usr/local/node-6.11.5/bin/node /usr/bin/node
|
||||
ln -sf /usr/local/node-6.11.5/bin/npm /usr/bin/npm
|
||||
rm -rf /usr/local/node-6.11.3
|
||||
fi
|
||||
|
||||
for try in `seq 1 10`; do
|
||||
@@ -78,12 +78,12 @@ for try in `seq 1 10`; do
|
||||
# however by default npm drops privileges for npm rebuild
|
||||
# https://docs.npmjs.com/misc/config#unsafe-perm
|
||||
if cd "${box_src_tmp_dir}" && npm rebuild --unsafe-perm; then break; fi
|
||||
echo "==> installer: Failed to rebuild, trying again"
|
||||
echo "Failed to rebuild, trying again"
|
||||
sleep 5
|
||||
done
|
||||
|
||||
if [[ ${try} -eq 10 ]]; then
|
||||
echo "==> installer: npm rebuild failed, giving up"
|
||||
echo "npm rebuild failed"
|
||||
exit 4
|
||||
fi
|
||||
|
||||
@@ -92,8 +92,9 @@ if ! id "${USER}" 2>/dev/null; then
|
||||
fi
|
||||
|
||||
if [[ "${is_update}" == "yes" ]]; then
|
||||
echo "==> installer: stop cloudron.target service for update"
|
||||
${BOX_SRC_DIR}/setup/stop.sh
|
||||
echo "Setting up update splash screen"
|
||||
"${box_src_tmp_dir}/setup/splashpage.sh" --data "${arg_data}" || true # show splash from new code
|
||||
${BOX_SRC_DIR}/setup/stop.sh # stop the old code
|
||||
fi
|
||||
|
||||
# setup links to data directory
|
||||
|
||||
+45
-8
@@ -3,15 +3,24 @@
|
||||
source_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
json="${source_dir}/../node_modules/.bin/json"
|
||||
|
||||
# IMPORTANT: Fix cloudron.js:doUpdate if you add/remove any arg. keep these sorted for readability
|
||||
arg_api_server_origin=""
|
||||
arg_fqdn="" # remove after 1.10
|
||||
arg_admin_domain=""
|
||||
arg_fqdn=""
|
||||
arg_admin_location=""
|
||||
arg_admin_fqdn=""
|
||||
arg_zone_name=""
|
||||
arg_is_custom_domain="false"
|
||||
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_is_demo="false"
|
||||
|
||||
@@ -31,14 +40,14 @@ while true; do
|
||||
--data)
|
||||
# these params must be valid in all cases
|
||||
arg_fqdn=$(echo "$2" | $json fqdn)
|
||||
arg_admin_fqdn=$(echo "$2" | $json adminFqdn)
|
||||
arg_zone_name=$(echo "$2" | $json zoneName)
|
||||
|
||||
arg_is_custom_domain=$(echo "$2" | $json isCustomDomain)
|
||||
[[ "${arg_is_custom_domain}" == "" ]] && arg_is_custom_domain="true"
|
||||
|
||||
arg_admin_location=$(echo "$2" | $json adminLocation)
|
||||
[[ "${arg_admin_location}" == "" ]] && arg_admin_location="my"
|
||||
|
||||
arg_admin_domain=$(echo "$2" | $json adminDomain)
|
||||
[[ "${arg_admin_domain}" == "" ]] && arg_admin_domain="${arg_fqdn}"
|
||||
|
||||
# only update/restore have this valid (but not migrate)
|
||||
arg_api_server_origin=$(echo "$2" | $json apiServerOrigin)
|
||||
[[ "${arg_api_server_origin}" == "" ]] && arg_api_server_origin="https://api.cloudron.io"
|
||||
@@ -52,9 +61,30 @@ while true; do
|
||||
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;;
|
||||
@@ -64,8 +94,15 @@ done
|
||||
|
||||
echo "Parsed arguments:"
|
||||
echo "api server: ${arg_api_server_origin}"
|
||||
echo "admin fqdn: ${arg_admin_fqdn}"
|
||||
echo "fqdn: ${arg_fqdn}"
|
||||
echo "custom domain: ${arg_is_custom_domain}"
|
||||
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}"
|
||||
|
||||
Executable
+49
@@ -0,0 +1,49 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
readonly SETUP_WEBSITE_DIR="/home/yellowtent/setup/website"
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
readonly box_src_dir="$(realpath ${script_dir}/..)"
|
||||
readonly PLATFORM_DATA_DIR="/home/yellowtent/platformdata"
|
||||
|
||||
echo "Setting up nginx update page"
|
||||
|
||||
if [[ ! -f "${PLATFORM_DATA_DIR}/nginx/applications/admin.conf" ]]; then
|
||||
echo "No admin.conf found. This Cloudron has no domain yet. Skip splash setup"
|
||||
exit
|
||||
fi
|
||||
|
||||
source "${script_dir}/argparser.sh" "$@" # this injects the arg_* variables used below
|
||||
|
||||
# keep this is sync with config.js appFqdn()
|
||||
admin_fqdn=$([[ "${arg_is_custom_domain}" == "true" ]] && echo "${arg_admin_location}.${arg_fqdn}" || echo "${arg_admin_location}-${arg_fqdn}")
|
||||
admin_origin="https://${admin_fqdn}"
|
||||
|
||||
# copy the website
|
||||
rm -rf "${SETUP_WEBSITE_DIR}" && mkdir -p "${SETUP_WEBSITE_DIR}"
|
||||
cp -r "${script_dir}/splash/website/"* "${SETUP_WEBSITE_DIR}"
|
||||
|
||||
# create nginx config
|
||||
readonly current_infra=$(node -e "console.log(require('${script_dir}/../src/infra_version.js').version);")
|
||||
existing_infra="none"
|
||||
[[ -f "${PLATFORM_DATA_DIR}/INFRA_VERSION" ]] && existing_infra=$(node -e "console.log(JSON.parse(require('fs').readFileSync('${PLATFORM_DATA_DIR}/INFRA_VERSION', 'utf8')).version);")
|
||||
if [[ "${arg_retire_reason}" != "" || "${existing_infra}" != "${current_infra}" ]]; then
|
||||
echo "Showing progress bar on all subdomains in retired mode or infra update. retire: ${arg_retire_reason} existing: ${existing_infra} current: ${current_infra}"
|
||||
rm -f ${PLATFORM_DATA_DIR}/nginx/applications/*
|
||||
${box_src_dir}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/appconfig.ejs" \
|
||||
-O "{ \"vhost\": \"~^(.+)\$\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"splash\", \"sourceDir\": \"${SETUP_WEBSITE_DIR}\", \"certFilePath\": \"cert/host.cert\", \"keyFilePath\": \"cert/host.key\", \"xFrameOptions\": \"SAMEORIGIN\", \"robotsTxtQuoted\": null, \"hasIPv6\": false }" > "${PLATFORM_DATA_DIR}/nginx/applications/admin.conf"
|
||||
else
|
||||
echo "Show progress bar only on admin domain for normal update"
|
||||
${box_src_dir}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/appconfig.ejs" \
|
||||
-O "{ \"vhost\": \"${admin_fqdn}\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"splash\", \"sourceDir\": \"${SETUP_WEBSITE_DIR}\", \"certFilePath\": \"cert/host.cert\", \"keyFilePath\": \"cert/host.key\", \"xFrameOptions\": \"SAMEORIGIN\", \"robotsTxtQuoted\": null, \"hasIPv6\": false }" > "${PLATFORM_DATA_DIR}/nginx/applications/admin.conf"
|
||||
fi
|
||||
|
||||
if [[ "${arg_retire_reason}" == "migrate" ]]; then
|
||||
echo "{ \"migrate\": { \"percent\": \"10\", \"message\": \"Migrating cloudron. This could take up to 15 minutes.\", \"info\": ${arg_retire_info} }, \"backup\": null, \"apiServerOrigin\": \"${arg_api_server_origin}\" }" > "${SETUP_WEBSITE_DIR}/progress.json"
|
||||
else
|
||||
echo '{ "update": { "percent": "10", "message": "Updating cloudron software" }, "backup": null }' > "${SETUP_WEBSITE_DIR}/progress.json"
|
||||
fi
|
||||
|
||||
nginx -s reload
|
||||
+78
-12
@@ -11,6 +11,7 @@ readonly PLATFORM_DATA_DIR="${HOME_DIR}/platformdata" # platform data
|
||||
readonly APPS_DATA_DIR="${HOME_DIR}/appsdata" # app data
|
||||
readonly BOX_DATA_DIR="${HOME_DIR}/boxdata" # box data
|
||||
readonly CONFIG_DIR="${HOME_DIR}/configs"
|
||||
readonly SETUP_PROGRESS_JSON="${HOME_DIR}/setup/website/progress.json"
|
||||
|
||||
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400"
|
||||
|
||||
@@ -18,11 +19,19 @@ readonly script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
source "${script_dir}/argparser.sh" "$@" # this injects the arg_* variables used below
|
||||
|
||||
echo "==> Configuring host"
|
||||
set_progress() {
|
||||
local percent="$1"
|
||||
local message="$2"
|
||||
|
||||
echo "==> ${percent} - ${message}"
|
||||
(echo "{ \"update\": { \"percent\": \"${percent}\", \"message\": \"${message}\" }, \"backup\": {} }" > "${SETUP_PROGRESS_JSON}") 2> /dev/null || true # as this will fail in non-update mode
|
||||
}
|
||||
|
||||
set_progress "20" "Configuring host"
|
||||
sed -e 's/^#NTP=/NTP=0.ubuntu.pool.ntp.org 1.ubuntu.pool.ntp.org 2.ubuntu.pool.ntp.org 3.ubuntu.pool.ntp.org/' -i /etc/systemd/timesyncd.conf
|
||||
timedatectl set-ntp 1
|
||||
timedatectl set-timezone UTC
|
||||
hostnamectl set-hostname "${arg_admin_fqdn}"
|
||||
hostnamectl set-hostname "${arg_fqdn}"
|
||||
|
||||
echo "==> Configuring docker"
|
||||
cp "${script_dir}/start/docker-cloudron-app.apparmor" /etc/apparmor.d/docker-cloudron-app
|
||||
@@ -105,7 +114,7 @@ systemctl restart systemd-journald
|
||||
setfacl -n -m u:${USER}:r /var/log/journal/*/system.journal
|
||||
|
||||
echo "==> Creating config directory"
|
||||
mkdir -p "${CONFIG_DIR}"
|
||||
rm -rf "${CONFIG_DIR}" && mkdir "${CONFIG_DIR}"
|
||||
|
||||
echo "==> Setting up unbound"
|
||||
# DO uses Google nameservers by default. This causes RBL queries to fail (host 2.0.0.127.zen.spamhaus.org)
|
||||
@@ -163,9 +172,6 @@ if ! grep -q "^Restart=" /etc/systemd/system/multi-user.target.wants/nginx.servi
|
||||
echo -e "\n[Service]\nRestart=always\n" >> /etc/systemd/system/multi-user.target.wants/nginx.service
|
||||
systemctl daemon-reload
|
||||
fi
|
||||
# remove this migration after 1.10
|
||||
[[ -f /etc/nginx/cert/host.cert ]] && cp /etc/nginx/cert/host.cert "/etc/nginx/cert/${arg_admin_domain}.host.cert"
|
||||
[[ -f /etc/nginx/cert/host.key ]] && cp /etc/nginx/cert/host.key "/etc/nginx/cert/${arg_admin_domain}.host.key"
|
||||
systemctl start nginx
|
||||
|
||||
# bookkeep the version as part of data
|
||||
@@ -193,26 +199,87 @@ readonly mysql_root_password="password"
|
||||
mysqladmin -u root -ppassword password password # reset default root password
|
||||
mysql -u root -p${mysql_root_password} -e 'CREATE DATABASE IF NOT EXISTS box'
|
||||
|
||||
echo "==> Migrating data"
|
||||
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 <<EOF
|
||||
set -eu
|
||||
cd "${BOX_SRC_DIR}"
|
||||
BOX_ENV=cloudron DATABASE_URL=mysql://root:${mysql_root_password}@127.0.0.1/box "${BOX_SRC_DIR}/node_modules/.bin/db-migrate" up
|
||||
EOF
|
||||
|
||||
echo "==> 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" <<CONF_END
|
||||
{
|
||||
"version": "${arg_version}",
|
||||
"token": "${arg_token}",
|
||||
"apiServerOrigin": "${arg_api_server_origin}",
|
||||
"webServerOrigin": "${arg_web_server_origin}",
|
||||
"adminDomain": "${arg_admin_domain}",
|
||||
"adminFqdn": "${arg_admin_fqdn}",
|
||||
"fqdn": "${arg_fqdn}",
|
||||
"adminLocation": "${arg_admin_location}",
|
||||
"zoneName": "${arg_zone_name}",
|
||||
"isCustomDomain": ${arg_is_custom_domain},
|
||||
"provider": "${arg_provider}",
|
||||
"isDemo": ${arg_is_demo}
|
||||
}
|
||||
CONF_END
|
||||
# pass these out-of-band because they have new lines which interfere with json
|
||||
if [[ -n "${arg_tls_cert}" && -n "${arg_tls_key}" ]]; then
|
||||
echo "${arg_tls_cert}" > "${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" <<CONF_END
|
||||
@@ -238,12 +305,11 @@ chown "${USER}:${USER}" "${PLATFORM_DATA_DIR}"
|
||||
# do not chown the boxdata/mail directory; dovecot gets upset
|
||||
chown "${USER}:${USER}" "${BOX_DATA_DIR}"
|
||||
find "${BOX_DATA_DIR}" -mindepth 1 -maxdepth 1 -not -path "${BOX_DATA_DIR}/mail" -exec chown -R "${USER}:${USER}" {} \;
|
||||
chown "${USER}:${USER}" "${BOX_DATA_DIR}/mail"
|
||||
chown "${USER}:${USER}" -R "${BOX_DATA_DIR}/mail/dkim" # this is owned by box currently since it generates the keys
|
||||
|
||||
echo "==> Starting Cloudron"
|
||||
set_progress "60" "Starting Cloudron"
|
||||
systemctl start cloudron.target
|
||||
|
||||
sleep 2 # give systemd sometime to start the processes
|
||||
|
||||
echo "==> Almost done"
|
||||
set_progress "90" "Almost done"
|
||||
|
||||
@@ -165,6 +165,40 @@ server {
|
||||
}
|
||||
<% } else if ( endpoint === 'app' ) { %>
|
||||
proxy_pass http://127.0.0.1:<%= port %>;
|
||||
<% } else if ( endpoint === 'splash' ) { %>
|
||||
root <%= sourceDir %>;
|
||||
|
||||
error_page 503 /update.html;
|
||||
|
||||
location /update.html {
|
||||
add_header Cache-Control no-cache;
|
||||
}
|
||||
|
||||
location /theme.css {
|
||||
add_header Cache-Control no-cache;
|
||||
}
|
||||
|
||||
location /3rdparty/ {
|
||||
add_header Cache-Control no-cache;
|
||||
}
|
||||
|
||||
location /js/ {
|
||||
add_header Cache-Control no-cache;
|
||||
}
|
||||
|
||||
location /progress.json {
|
||||
add_header Cache-Control no-cache;
|
||||
}
|
||||
|
||||
location /api/v1/cloudron/progress {
|
||||
add_header Cache-Control no-cache;
|
||||
default_type application/json;
|
||||
alias <%= sourceDir %>/progress.json;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 503;
|
||||
}
|
||||
<% } %>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,3 @@ yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/configurelogrot
|
||||
Defaults!/home/yellowtent/box/src/backuptask.js env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD:SETENV: /home/yellowtent/box/src/backuptask.js
|
||||
|
||||
Defaults!/home/yellowtent/box/src/scripts/restart.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/restart.sh
|
||||
|
||||
|
||||
+35
-73
@@ -28,7 +28,6 @@ var appdb = require('./appdb.js'),
|
||||
generatePassword = require('password-generator'),
|
||||
hat = require('hat'),
|
||||
infra = require('./infra_version.js'),
|
||||
mail = require('./mail.js'),
|
||||
mailboxdb = require('./mailboxdb.js'),
|
||||
once = require('once'),
|
||||
path = require('path'),
|
||||
@@ -113,9 +112,10 @@ var KNOWN_ADDONS = {
|
||||
var RMAPPDIR_CMD = path.join(__dirname, 'scripts/rmappdir.sh');
|
||||
|
||||
function debugApp(app, args) {
|
||||
assert(typeof app === 'object');
|
||||
assert(!app || typeof app === 'object');
|
||||
|
||||
debug(app.fqdn + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
|
||||
var prefix = app ? (app.location || 'naked_domain') : '(no app)';
|
||||
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
|
||||
}
|
||||
|
||||
function setupAddons(app, addons, callback) {
|
||||
@@ -125,7 +125,7 @@ function setupAddons(app, addons, callback) {
|
||||
|
||||
if (!addons) return callback(null);
|
||||
|
||||
debugApp(app, 'setupAddons: Setting up %j', Object.keys(addons));
|
||||
debugApp(app, 'setupAddons: Settings up %j', Object.keys(addons));
|
||||
|
||||
async.eachSeries(Object.keys(addons), function iterator(addon, iteratorCallback) {
|
||||
if (!(addon in KNOWN_ADDONS)) return iteratorCallback(new Error('No such addon:' + addon));
|
||||
@@ -245,12 +245,10 @@ function setupOauth(app, options, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debugApp(app, 'setupOauth');
|
||||
|
||||
if (!app.sso) return callback(null);
|
||||
|
||||
var appId = app.id;
|
||||
var redirectURI = 'https://' + app.fqdn;
|
||||
var redirectURI = 'https://' + (app.altDomain || config.appFqdn(app.location));
|
||||
var scope = 'profile';
|
||||
|
||||
clients.delByAppIdAndType(appId, clients.TYPE_OAUTH, function (error) { // remove existing creds
|
||||
@@ -291,27 +289,20 @@ function setupEmail(app, options, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
mail.getAll(function (error, mailDomains) {
|
||||
if (error) return callback(error);
|
||||
// note that "external" access info can be derived from MAIL_DOMAIN (since it's part of user documentation)
|
||||
var env = [
|
||||
{ name: 'MAIL_SMTP_SERVER', value: 'mail' },
|
||||
{ name: 'MAIL_SMTP_PORT', value: '2525' },
|
||||
{ name: 'MAIL_IMAP_SERVER', value: 'mail' },
|
||||
{ name: 'MAIL_IMAP_PORT', value: '9993' },
|
||||
{ name: 'MAIL_SIEVE_SERVER', value: 'mail' },
|
||||
{ name: 'MAIL_SIEVE_PORT', value: '4190' },
|
||||
{ name: 'MAIL_DOMAIN', value: config.fqdn() }
|
||||
];
|
||||
|
||||
const mailInDomains = mailDomains.filter(function (d) { return d.enabled; }).map(function (d) { return d.domain; }).join(',');
|
||||
debugApp(app, 'Setting up Email');
|
||||
|
||||
// note that "external" access info can be derived from MAIL_DOMAIN (since it's part of user documentation)
|
||||
var env = [
|
||||
{ name: 'MAIL_SMTP_SERVER', value: 'mail' },
|
||||
{ name: 'MAIL_SMTP_PORT', value: '2525' },
|
||||
{ name: 'MAIL_IMAP_SERVER', value: 'mail' },
|
||||
{ name: 'MAIL_IMAP_PORT', value: '9993' },
|
||||
{ name: 'MAIL_SIEVE_SERVER', value: 'mail' },
|
||||
{ name: 'MAIL_SIEVE_PORT', value: '4190' },
|
||||
{ name: 'MAIL_DOMAIN', value: app.domain },
|
||||
{ name: 'MAIL_DOMAINS', value: mailInDomains }
|
||||
];
|
||||
|
||||
debugApp(app, 'Setting up Email');
|
||||
|
||||
appdb.setAddonConfig(app.id, 'email', env, callback);
|
||||
});
|
||||
appdb.setAddonConfig(app.id, 'email', env, callback);
|
||||
}
|
||||
|
||||
function teardownEmail(app, options, callback) {
|
||||
@@ -372,11 +363,11 @@ function setupSendMail(app, options, callback) {
|
||||
var env = [
|
||||
{ name: 'MAIL_SMTP_SERVER', value: 'mail' },
|
||||
{ name: 'MAIL_SMTP_PORT', value: '2525' },
|
||||
{ name: 'MAIL_SMTPS_PORT', value: '2465' },
|
||||
{ name: 'MAIL_SMTP_USERNAME', value: mailbox.name + '@' + app.domain },
|
||||
{ name: 'MAIL_SMTPS_PORT', value: '4650' },
|
||||
{ name: 'MAIL_SMTP_USERNAME', value: mailbox.name },
|
||||
{ name: 'MAIL_SMTP_PASSWORD', value: password },
|
||||
{ name: 'MAIL_FROM', value: mailbox.name + '@' + app.domain },
|
||||
{ name: 'MAIL_DOMAIN', value: app.domain }
|
||||
{ name: 'MAIL_FROM', value: mailbox.name + '@' + config.fqdn() },
|
||||
{ name: 'MAIL_DOMAIN', value: config.fqdn() }
|
||||
];
|
||||
debugApp(app, 'Setting sendmail addon config to %j', env);
|
||||
appdb.setAddonConfig(app.id, 'sendmail', env, callback);
|
||||
@@ -409,10 +400,10 @@ function setupRecvMail(app, options, callback) {
|
||||
var env = [
|
||||
{ name: 'MAIL_IMAP_SERVER', value: 'mail' },
|
||||
{ name: 'MAIL_IMAP_PORT', value: '9993' },
|
||||
{ name: 'MAIL_IMAP_USERNAME', value: mailbox.name + '@' + app.domain },
|
||||
{ name: 'MAIL_IMAP_USERNAME', value: mailbox.name },
|
||||
{ name: 'MAIL_IMAP_PASSWORD', value: password },
|
||||
{ name: 'MAIL_TO', value: mailbox.name + '@' + app.domain },
|
||||
{ name: 'MAIL_DOMAIN', value: app.domain }
|
||||
{ name: 'MAIL_TO', value: mailbox.name + '@' + config.fqdn() },
|
||||
{ name: 'MAIL_DOMAIN', value: config.fqdn() }
|
||||
];
|
||||
|
||||
debugApp(app, 'Setting sendmail addon config to %j', env);
|
||||
@@ -467,10 +458,6 @@ function teardownMySql(app, options, callback) {
|
||||
}
|
||||
|
||||
function backupMySql(app, options, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debugApp(app, 'Backing up mysql');
|
||||
|
||||
callback = once(callback); // ChildProcess exit may or may not be called after error
|
||||
@@ -484,10 +471,6 @@ function backupMySql(app, options, callback) {
|
||||
}
|
||||
|
||||
function restoreMySql(app, options, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
callback = once(callback); // ChildProcess exit may or may not be called after error
|
||||
|
||||
setupMySql(app, options, function (error) {
|
||||
@@ -540,10 +523,6 @@ function teardownPostgreSql(app, options, callback) {
|
||||
}
|
||||
|
||||
function backupPostgreSql(app, options, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debugApp(app, 'Backing up postgresql');
|
||||
|
||||
callback = once(callback); // ChildProcess exit may or may not be called after error
|
||||
@@ -557,10 +536,6 @@ function backupPostgreSql(app, options, callback) {
|
||||
}
|
||||
|
||||
function restorePostgreSql(app, options, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
callback = once(callback);
|
||||
|
||||
setupPostgreSql(app, options, function (error) {
|
||||
@@ -614,10 +589,6 @@ function teardownMongoDb(app, options, callback) {
|
||||
}
|
||||
|
||||
function backupMongoDb(app, options, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debugApp(app, 'Backing up mongodb');
|
||||
|
||||
callback = once(callback); // ChildProcess exit may or may not be called after error
|
||||
@@ -631,10 +602,6 @@ function backupMongoDb(app, options, callback) {
|
||||
}
|
||||
|
||||
function restoreMongoDb(app, options, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
callback = once(callback); // ChildProcess exit may or may not be called after error
|
||||
|
||||
setupMongoDb(app, options, function (error) {
|
||||
@@ -678,10 +645,9 @@ function setupRedis(app, options, callback) {
|
||||
}
|
||||
|
||||
const tag = infra.images.redis.tag, redisName = 'redis-' + app.id;
|
||||
const label = app.fqdn;
|
||||
// note that we do not add appId label because this interferes with the stop/start app logic
|
||||
const cmd = `docker run --restart=always -d --name=${redisName} \
|
||||
--label=location=${label} \
|
||||
--label=location=${app.location} \
|
||||
--net cloudron \
|
||||
--net-alias ${redisName} \
|
||||
-m ${memoryLimit/2} \
|
||||
@@ -716,31 +682,27 @@ function teardownRedis(app, options, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var container = dockerConnection.getContainer('redis-' + app.id);
|
||||
var container = dockerConnection.getContainer('redis-' + app.id);
|
||||
|
||||
var removeOptions = {
|
||||
force: true, // kill container if it's running
|
||||
v: true // removes volumes associated with the container
|
||||
};
|
||||
var removeOptions = {
|
||||
force: true, // kill container if it's running
|
||||
v: true // removes volumes associated with the container
|
||||
};
|
||||
|
||||
container.remove(removeOptions, function (error) {
|
||||
if (error && error.statusCode !== 404) return callback(new Error('Error removing container:' + error));
|
||||
container.remove(removeOptions, function (error) {
|
||||
if (error && error.statusCode !== 404) return callback(new Error('Error removing container:' + error));
|
||||
|
||||
safe.fs.unlinkSync(paths.ADDON_CONFIG_DIR, 'redis-' + app.id + '_vars.sh');
|
||||
safe.fs.unlinkSync(paths.ADDON_CONFIG_DIR, 'redis-' + app.id + '_vars.sh');
|
||||
|
||||
shell.sudo('teardownRedis', [ RMAPPDIR_CMD, app.id + '/redis', true /* delete directory */ ], function (error /* ,stdout , stderr*/) {
|
||||
shell.sudo('teardownRedis', [ RMAPPDIR_CMD, app.id + '/redis', true /* delete directory */ ], function (error, stdout, stderr) {
|
||||
if (error) return callback(new Error('Error removing redis data:' + error));
|
||||
|
||||
appdb.unsetAddonConfig(app.id, 'redis', callback);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function backupRedis(app, options, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debugApp(app, 'Backing up redis');
|
||||
|
||||
var cmd = [ '/addons/redis/service.sh', 'backup' ]; // the redis dir is volume mounted
|
||||
|
||||
+9
-13
@@ -59,10 +59,9 @@ var assert = require('assert'),
|
||||
util = require('util');
|
||||
|
||||
var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationState', 'apps.installationProgress', 'apps.runState',
|
||||
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.httpPort', 'apps.location', 'apps.domain', 'apps.dnsRecordId',
|
||||
'apps.accessRestrictionJson', 'apps.restoreConfigJson', 'apps.oldConfigJson', 'apps.updateConfigJson', 'apps.memoryLimit',
|
||||
'apps.xFrameOptions', 'apps.sso', 'apps.debugModeJson', 'apps.robotsTxt', 'apps.enableBackup',
|
||||
'apps.creationTime', 'apps.updateTime' ].join(',');
|
||||
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.httpPort', 'apps.location', 'apps.dnsRecordId',
|
||||
'apps.accessRestrictionJson', 'apps.restoreConfigJson', 'apps.oldConfigJson', 'apps.updateConfigJson', 'apps.memoryLimit', 'apps.altDomain',
|
||||
'apps.xFrameOptions', 'apps.sso', 'apps.debugModeJson', 'apps.robotsTxt', 'apps.enableBackup' ].join(',');
|
||||
|
||||
var PORT_BINDINGS_FIELDS = [ 'hostPort', 'environmentVariable', 'appId' ].join(',');
|
||||
|
||||
@@ -178,13 +177,12 @@ function getAll(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function add(id, appStoreId, manifest, location, domain, portBindings, data, callback) {
|
||||
function add(id, appStoreId, manifest, location, portBindings, data, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof appStoreId, 'string');
|
||||
assert(manifest && typeof manifest === 'object');
|
||||
assert.strictEqual(typeof manifest.version, 'string');
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof portBindings, 'object');
|
||||
assert(data && typeof data === 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -196,18 +194,17 @@ function add(id, appStoreId, manifest, location, domain, portBindings, data, cal
|
||||
var accessRestriction = data.accessRestriction || null;
|
||||
var accessRestrictionJson = JSON.stringify(accessRestriction);
|
||||
var memoryLimit = data.memoryLimit || 0;
|
||||
var altDomain = data.altDomain || null;
|
||||
var xFrameOptions = data.xFrameOptions || '';
|
||||
var installationState = data.installationState || exports.ISTATE_PENDING_INSTALL;
|
||||
var restoreConfigJson = data.restoreConfig ? JSON.stringify(data.restoreConfig) : null; // used when cloning
|
||||
var sso = 'sso' in data ? data.sso : null;
|
||||
var robotsTxt = 'robotsTxt' in data ? data.robotsTxt : null;
|
||||
var debugModeJson = data.debugMode ? JSON.stringify(data.debugMode) : null;
|
||||
|
||||
var queries = [];
|
||||
queries.push({
|
||||
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, location, domain, accessRestrictionJson, memoryLimit, xFrameOptions, restoreConfigJson, sso, debugModeJson, robotsTxt) ' +
|
||||
' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
args: [ id, appStoreId, manifestJson, installationState, location, domain, accessRestrictionJson, memoryLimit, xFrameOptions, restoreConfigJson, sso, debugModeJson, robotsTxt ]
|
||||
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, location, accessRestrictionJson, memoryLimit, altDomain, xFrameOptions, restoreConfigJson, sso, debugModeJson) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
args: [ id, appStoreId, manifestJson, installationState, location, accessRestrictionJson, memoryLimit, altDomain, xFrameOptions, restoreConfigJson, sso, debugModeJson ]
|
||||
});
|
||||
|
||||
Object.keys(portBindings).forEach(function (env) {
|
||||
@@ -220,14 +217,13 @@ function add(id, appStoreId, manifest, location, domain, portBindings, data, cal
|
||||
// only allocate a mailbox if mailboxName is set
|
||||
if (data.mailboxName) {
|
||||
queries.push({
|
||||
query: 'INSERT INTO mailboxes (name, domain, ownerId, ownerType) VALUES (?, ?, ?, ?)',
|
||||
args: [ data.mailboxName, domain, id, mailboxdb.TYPE_APP ]
|
||||
query: 'INSERT INTO mailboxes (name, ownerId, ownerType) VALUES (?, ?, ?)',
|
||||
args: [ data.mailboxName, id, mailboxdb.TYPE_APP ]
|
||||
});
|
||||
}
|
||||
|
||||
database.transaction(queries, function (error) {
|
||||
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error.message));
|
||||
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND, 'no such domain'));
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
|
||||
@@ -23,9 +23,13 @@ var gRunTimeout = null;
|
||||
var gDockerEventStream = null;
|
||||
|
||||
function debugApp(app) {
|
||||
assert(typeof app === 'object');
|
||||
assert(!app || typeof app === 'object');
|
||||
|
||||
debug(app.fqdn + ' ' + app.manifest.id + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)) + ' - ' + app.id);
|
||||
var prefix = app ? (app.location || 'naked_domain') : '(no app)';
|
||||
var manifestAppId = app ? app.manifest.id : '';
|
||||
var id = app ? app.id : '';
|
||||
|
||||
debug(prefix + ' ' + manifestAppId + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)) + ' - ' + id);
|
||||
}
|
||||
|
||||
function setHealth(app, health, callback) {
|
||||
@@ -66,9 +70,6 @@ function setHealth(app, health, callback) {
|
||||
|
||||
// callback is called with error for fatal errors and not if health check failed
|
||||
function checkAppHealth(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (app.installationState !== appdb.ISTATE_INSTALLED || app.runState !== appdb.RSTATE_RUNNING) {
|
||||
debugApp(app, 'skipped. istate:%s rstate:%s', app.installationState, app.runState);
|
||||
return callback(null);
|
||||
|
||||
+156
-219
@@ -54,14 +54,12 @@ var addons = require('./addons.js'),
|
||||
async = require('async'),
|
||||
backups = require('./backups.js'),
|
||||
BackupsError = backups.BackupsError,
|
||||
certificates = require('./certificates.js'),
|
||||
config = require('./config.js'),
|
||||
constants = require('./constants.js'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
debug = require('debug')('box:apps'),
|
||||
docker = require('./docker.js'),
|
||||
domaindb = require('./domaindb.js'),
|
||||
domains = require('./domains.js'),
|
||||
DomainError = require('./domains.js').DomainError,
|
||||
eventlog = require('./eventlog.js'),
|
||||
fs = require('fs'),
|
||||
groups = require('./groups.js'),
|
||||
@@ -70,14 +68,12 @@ var addons = require('./addons.js'),
|
||||
os = require('os'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
reverseProxy = require('./reverseproxy.js'),
|
||||
safe = require('safetydance'),
|
||||
semver = require('semver'),
|
||||
spawn = require('child_process').spawn,
|
||||
split = require('split'),
|
||||
superagent = require('superagent'),
|
||||
taskmanager = require('./taskmanager.js'),
|
||||
tld = require('tldjs'),
|
||||
TransformStream = require('stream').Transform,
|
||||
updateChecker = require('./updatechecker.js'),
|
||||
url = require('url'),
|
||||
@@ -121,33 +117,18 @@ AppsError.BAD_CERTIFICATE = 'Invalid certificate';
|
||||
// Hostname validation comes from RFC 1123 (section 2.1)
|
||||
// Domain name validation comes from RFC 2181 (Name syntax)
|
||||
// https://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_host_names
|
||||
// We are validating the validity of the location-fqdn as host name (and not dns name)
|
||||
function validateHostname(location, domain, hostname) {
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof hostname, 'string');
|
||||
// We are validating the validity of the location-fqdn as host name
|
||||
function validateHostname(location, fqdn) {
|
||||
var RESERVED_LOCATIONS = [ config.adminLocation(), constants.API_LOCATION, constants.SMTP_LOCATION, constants.IMAP_LOCATION, config.mailLocation(), constants.POSTMAN_LOCATION ];
|
||||
|
||||
const RESERVED_LOCATIONS = [
|
||||
constants.API_LOCATION,
|
||||
constants.SMTP_LOCATION,
|
||||
constants.IMAP_LOCATION
|
||||
];
|
||||
if (RESERVED_LOCATIONS.indexOf(location) !== -1) return new AppsError(AppsError.BAD_FIELD, location + ' is reserved');
|
||||
|
||||
if (hostname === config.adminFqdn()) return new AppsError(AppsError.BAD_FIELD, location + ' is reserved');
|
||||
if (location === '') return null; // bare location
|
||||
|
||||
// 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) {
|
||||
// label validation
|
||||
if (location.length > 63) return new AppsError(AppsError.BAD_FIELD, 'Subdomain exceeds 63 characters');
|
||||
if (location.match(/^[A-Za-z0-9-]+$/) === null) return new AppsError(AppsError.BAD_FIELD, 'Subdomain can only contain alphanumerics and hyphen');
|
||||
if (location.startsWith('-') || location.endsWith('-')) return new AppsError(AppsError.BAD_FIELD, 'Subdomain cannot start or end with hyphen');
|
||||
}
|
||||
if ((location.length + 1 /*+ hyphen */ + fqdn.indexOf('.')) > 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');
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -306,19 +287,15 @@ function getDuplicateErrorDetails(location, portBindings, error) {
|
||||
return new AppsError(AppsError.ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
// app configs that is useful for 'archival' into the app backup config.json
|
||||
function getAppConfig(app) {
|
||||
return {
|
||||
manifest: app.manifest,
|
||||
location: app.location,
|
||||
domain: app.domain,
|
||||
fqdn: app.fqdn,
|
||||
accessRestriction: app.accessRestriction,
|
||||
portBindings: app.portBindings,
|
||||
memoryLimit: app.memoryLimit,
|
||||
xFrameOptions: app.xFrameOptions || 'SAMEORIGIN',
|
||||
robotsTxt: app.robotsTxt,
|
||||
sso: app.sso
|
||||
altDomain: app.altDomain
|
||||
};
|
||||
}
|
||||
|
||||
@@ -361,14 +338,11 @@ function get(appId, 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));
|
||||
|
||||
domaindb.get(app.domain, function (error, result) {
|
||||
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.iconUrl = getIconUrlSync(app);
|
||||
app.fqdn = domains.fqdn(app.location, app.domain, result.provider);
|
||||
|
||||
callback(null, app);
|
||||
});
|
||||
callback(null, app);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -383,14 +357,11 @@ function getByIpAddress(ip, 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));
|
||||
|
||||
domaindb.get(app.domain, function (error, result) {
|
||||
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.iconUrl = getIconUrlSync(app);
|
||||
app.fqdn = domains.fqdn(app.location, app.domain, result.provider);
|
||||
|
||||
callback(null, app);
|
||||
});
|
||||
callback(null, app);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -401,20 +372,13 @@ function getAll(callback) {
|
||||
appdb.getAll(function (error, apps) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
async.eachSeries(apps, function (app, iteratorDone) {
|
||||
domaindb.get(app.domain, function (error, result) {
|
||||
if (error) return iteratorDone(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
app.iconUrl = getIconUrlSync(app);
|
||||
app.fqdn = domains.fqdn(app.location, app.domain, result.provider);
|
||||
|
||||
iteratorDone();
|
||||
});
|
||||
}, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, apps);
|
||||
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;
|
||||
});
|
||||
|
||||
callback(null, apps);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -445,7 +409,7 @@ function downloadManifest(appStoreId, manifest, callback) {
|
||||
superagent.get(url).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppsError(AppsError.EXTERNAL_ERROR, 'Network error downloading manifest:' + error.message));
|
||||
|
||||
if (result.statusCode !== 200) return callback(new AppsError(AppsError.NOT_FOUND, util.format('Failed to get app info from store.', result.statusCode, result.text)));
|
||||
if (result.statusCode !== 200) return callback(new AppsError(AppsError.BAD_FIELD, util.format('Failed to get app info from store.', result.statusCode, result.text)));
|
||||
|
||||
callback(null, parts[0], result.body.manifest);
|
||||
});
|
||||
@@ -457,13 +421,13 @@ 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,
|
||||
cert = data.cert || null,
|
||||
key = data.key || null,
|
||||
memoryLimit = data.memoryLimit || 0,
|
||||
altDomain = data.altDomain || null,
|
||||
xFrameOptions = data.xFrameOptions || 'SAMEORIGIN',
|
||||
sso = 'sso' in data ? data.sso : null,
|
||||
debugMode = data.debugMode || null,
|
||||
@@ -483,6 +447,9 @@ function install(data, auditSource, callback) {
|
||||
error = checkManifestConstraints(manifest);
|
||||
if (error) return callback(error);
|
||||
|
||||
error = validateHostname(location, config.fqdn());
|
||||
if (error) return callback(error);
|
||||
|
||||
error = validatePortBindings(portBindings, manifest.tcpPorts);
|
||||
if (error) return callback(error);
|
||||
|
||||
@@ -508,6 +475,8 @@ function install(data, auditSource, callback) {
|
||||
// if sso was unspecified, enable it by default if possible
|
||||
if (sso === null) sso = !!manifest.addons['ldap'] || !!manifest.addons['oauth'];
|
||||
|
||||
if (altDomain !== null && !validator.isFQDN(altDomain)) return callback(new AppsError(AppsError.BAD_FIELD, 'Invalid external domain'));
|
||||
|
||||
var appId = uuid.v4();
|
||||
|
||||
if (icon) {
|
||||
@@ -518,62 +487,45 @@ function install(data, auditSource, callback) {
|
||||
}
|
||||
}
|
||||
|
||||
domains.get(domain, function (error, domainObject) {
|
||||
if (error && error.reason === DomainError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such domain'));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Could not get domain info:' + error.message));
|
||||
error = certificates.validateCertificate(cert, key, config.appFqdn(location));
|
||||
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
|
||||
|
||||
var fqdn = domains.fqdn(location, domain, domainObject.provider);
|
||||
debug('Will install app with id : ' + appId);
|
||||
|
||||
error = validateHostname(location, domain, fqdn);
|
||||
if (error) return callback(error);
|
||||
appstore.purchase(appId, 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));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
if (cert && key) {
|
||||
error = reverseProxy.validateCertificate(fqdn, cert, key);
|
||||
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
|
||||
}
|
||||
var data = {
|
||||
accessRestriction: accessRestriction,
|
||||
memoryLimit: memoryLimit,
|
||||
altDomain: altDomain,
|
||||
xFrameOptions: xFrameOptions,
|
||||
sso: sso,
|
||||
debugMode: debugMode,
|
||||
mailboxName: (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app',
|
||||
restoreConfig: backupId ? { backupId: backupId, backupFormat: backupFormat } : null,
|
||||
enableBackup: enableBackup,
|
||||
robotsTxt: robotsTxt
|
||||
};
|
||||
|
||||
debug('Will install app with id : ' + appId);
|
||||
|
||||
appstore.purchase(appId, 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));
|
||||
appdb.add(appId, appStoreId, manifest, location, 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));
|
||||
|
||||
var data = {
|
||||
accessRestriction: accessRestriction,
|
||||
memoryLimit: memoryLimit,
|
||||
xFrameOptions: xFrameOptions,
|
||||
sso: sso,
|
||||
debugMode: debugMode,
|
||||
mailboxName: (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app',
|
||||
restoreConfig: backupId ? { backupId: backupId, backupFormat: backupFormat } : null,
|
||||
enableBackup: enableBackup,
|
||||
robotsTxt: robotsTxt
|
||||
};
|
||||
// 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));
|
||||
}
|
||||
|
||||
appdb.add(appId, appStoreId, manifest, location, domain, portBindings, data, function (error) {
|
||||
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location, portBindings, error));
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, error.message));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
taskmanager.restartAppTask(appId);
|
||||
|
||||
// save cert to boxdata/certs
|
||||
if (cert && key) {
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, fqdn + '.user.cert'), cert)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving cert: ' + safe.error.message));
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, fqdn + '.user.key'), key)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving key: ' + safe.error.message));
|
||||
}
|
||||
eventlog.add(eventlog.ACTION_APP_INSTALL, auditSource, { appId: appId, location: location, manifest: manifest, backupId: backupId });
|
||||
|
||||
taskmanager.restartAppTask(appId);
|
||||
|
||||
// fetch fresh app object for eventlog
|
||||
get(appId, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
eventlog.add(eventlog.ACTION_APP_INSTALL, auditSource, { appId: appId, app: result });
|
||||
|
||||
callback(null, { id : appId });
|
||||
});
|
||||
});
|
||||
callback(null, { id : appId });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -585,15 +537,18 @@ function configure(appId, data, auditSource, callback) {
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
get(appId, function (error, app) {
|
||||
if (error) return callback(error);
|
||||
appdb.get(appId, function (error, app) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
var domain, location, portBindings, values = { };
|
||||
if ('location' in data) location = values.location = data.location.toLowerCase();
|
||||
else location = app.location;
|
||||
|
||||
if ('domain' in data) domain = values.domain = data.domain.toLowerCase();
|
||||
else domain = app.domain;
|
||||
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;
|
||||
}
|
||||
|
||||
if ('accessRestriction' in data) {
|
||||
values.accessRestriction = data.accessRestriction;
|
||||
@@ -601,6 +556,11 @@ function configure(appId, data, auditSource, callback) {
|
||||
if (error) return callback(error);
|
||||
}
|
||||
|
||||
if ('altDomain' in data) {
|
||||
values.altDomain = data.altDomain;
|
||||
if (values.altDomain !== null && !validator.isFQDN(values.altDomain)) return callback(new AppsError(AppsError.BAD_FIELD, 'Invalid external domain'));
|
||||
}
|
||||
|
||||
if ('portBindings' in data) {
|
||||
portBindings = values.portBindings = data.portBindings;
|
||||
error = validatePortBindings(values.portBindings, app.manifest.tcpPorts);
|
||||
@@ -633,58 +593,43 @@ function configure(appId, data, auditSource, callback) {
|
||||
if (error) return callback(error);
|
||||
}
|
||||
|
||||
domains.get(domain, function (error, domainObject) {
|
||||
if (error && error.reason === DomainError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such domain'));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Could not get domain info:' + error.message));
|
||||
// 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));
|
||||
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
|
||||
|
||||
var fqdn = domains.fqdn(location, domain, domainObject.provider);
|
||||
|
||||
error = validateHostname(location, domain, fqdn);
|
||||
if (error) return callback(error);
|
||||
|
||||
// save cert to boxdata/certs. TODO: move this to apptask when we have a real task queue
|
||||
if ('cert' in data && 'key' in data) {
|
||||
if (data.cert && data.key) {
|
||||
error = reverseProxy.validateCertificate(fqdn, data.cert, data.key);
|
||||
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
|
||||
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${fqdn}.user.cert`), data.cert)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving cert: ' + safe.error.message));
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${fqdn}.user.key`), data.key)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving key: ' + safe.error.message));
|
||||
} else { // remove existing cert/key
|
||||
if (!safe.fs.unlinkSync(path.join(paths.APP_CERTS_DIR, `${fqdn}.user.cert`))) debug('Error removing cert: ' + safe.error.message);
|
||||
if (!safe.fs.unlinkSync(path.join(paths.APP_CERTS_DIR, `${fqdn}..user.key`))) debug('Error removing key: ' + safe.error.message);
|
||||
}
|
||||
if (!safe.fs.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));
|
||||
} 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 ('enableBackup' in data) values.enableBackup = data.enableBackup;
|
||||
if ('enableBackup' in data) values.enableBackup = data.enableBackup;
|
||||
|
||||
values.oldConfig = getAppConfig(app);
|
||||
values.oldConfig = getAppConfig(app);
|
||||
|
||||
debug('Will configure app with id:%s values:%j', appId, values);
|
||||
debug('Will configure app with id:%s values:%j', appId, values);
|
||||
|
||||
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, 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'));
|
||||
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) {
|
||||
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));
|
||||
|
||||
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_CONFIGURE, values, function (error) {
|
||||
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location, portBindings, error));
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_CONFIGURE, values, function (error) {
|
||||
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location, portBindings, error));
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
taskmanager.restartAppTask(appId);
|
||||
|
||||
taskmanager.restartAppTask(appId);
|
||||
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: appId });
|
||||
|
||||
// fetch fresh app object for eventlog
|
||||
get(appId, function (error, result) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: appId, app: result });
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -723,8 +668,9 @@ function update(appId, data, auditSource, callback) {
|
||||
}
|
||||
}
|
||||
|
||||
get(appId, function (error, app) {
|
||||
if (error) return callback(error);
|
||||
appdb.get(appId, function (error, app) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
// prevent user from installing a app with different manifest id over an existing app
|
||||
// this allows cloudron install -f --app <appid> for an app installed from the appStore
|
||||
@@ -749,7 +695,7 @@ function update(appId, data, auditSource, callback) {
|
||||
|
||||
taskmanager.restartAppTask(appId);
|
||||
|
||||
eventlog.add(eventlog.ACTION_APP_UPDATE, auditSource, { appId: appId, toManifest: manifest, fromManifest: app.manifest, force: data.force, app: app });
|
||||
eventlog.add(eventlog.ACTION_APP_UPDATE, auditSource, { appId: appId, toManifest: manifest, fromManifest: app.manifest, force: data.force });
|
||||
|
||||
// clear update indicator, if update fails, it will come back through the update checker
|
||||
updateChecker.resetAppUpdateInfo(appId);
|
||||
@@ -773,8 +719,10 @@ function getLogs(appId, options, callback) {
|
||||
|
||||
debug('Getting logs for %s', appId);
|
||||
|
||||
get(appId, function (error, app) {
|
||||
if (error) return callback(error);
|
||||
appdb.get(appId, function (error, app) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
|
||||
var lines = options.lines || 100,
|
||||
follow = !!options.follow,
|
||||
@@ -818,8 +766,9 @@ function restore(appId, data, auditSource, callback) {
|
||||
|
||||
debug('Will restore app with id:%s', appId);
|
||||
|
||||
get(appId, function (error, app) {
|
||||
if (error) return callback(error);
|
||||
appdb.get(appId, function (error, app) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
// for empty or null backupId, use existing manifest to mimic a reinstall
|
||||
var func = data.backupId ? backups.get.bind(null, data.backupId) : function (next) { return next(null, { manifest: app.manifest }); };
|
||||
@@ -848,7 +797,7 @@ function restore(appId, data, auditSource, callback) {
|
||||
|
||||
taskmanager.restartAppTask(appId);
|
||||
|
||||
eventlog.add(eventlog.ACTION_APP_RESTORE, auditSource, { appId: appId, app: app });
|
||||
eventlog.add(eventlog.ACTION_APP_RESTORE, auditSource, { appId: appId });
|
||||
|
||||
callback(null);
|
||||
});
|
||||
@@ -865,21 +814,20 @@ 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');
|
||||
|
||||
get(appId, function (error, app) {
|
||||
if (error) return callback(error);
|
||||
appdb.get(appId, function (error, app) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
backups.get(backupId, function (error, backupInfo) {
|
||||
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
|
||||
if (error && error.reason === BackupsError.NOT_FOUND) return callback(new AppsError(AppsError.EXTERNAL_ERROR, 'Backup not found'));
|
||||
if (error && error.reason === BackupsError.NOT_FOUND) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
if (!backupInfo.manifest) callback(new AppsError(AppsError.EXTERNAL_ERROR, 'Could not get restore config'));
|
||||
@@ -888,51 +836,39 @@ function clone(appId, data, auditSource, callback) {
|
||||
error = checkManifestConstraints(backupInfo.manifest);
|
||||
if (error) return callback(error);
|
||||
|
||||
error = validateHostname(location, config.fqdn());
|
||||
if (error) return callback(error);
|
||||
|
||||
error = validatePortBindings(portBindings, backupInfo.manifest.tcpPorts);
|
||||
if (error) return callback(error);
|
||||
|
||||
domains.get(domain, function (error, domainObject) {
|
||||
if (error && error.reason === DomainError.NOT_FOUND) return callback(new AppsError(AppsError.EXTERNAL_ERROR, 'No such domain'));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Could not get domain info:' + error.message));
|
||||
var newAppId = uuid.v4(), appStoreId = app.appStoreId, manifest = backupInfo.manifest;
|
||||
|
||||
error = validateHostname(location, domain, domains.fqdn(location, domain, domainObject.provider));
|
||||
if (error) return callback(error);
|
||||
appstore.purchase(newAppId, 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));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
var newAppId = uuid.v4(), manifest = backupInfo.manifest;
|
||||
var data = {
|
||||
installationState: appdb.ISTATE_PENDING_CLONE,
|
||||
memoryLimit: app.memoryLimit,
|
||||
accessRestriction: app.accessRestriction,
|
||||
xFrameOptions: app.xFrameOptions,
|
||||
restoreConfig: { backupId: backupId, backupFormat: backupInfo.format },
|
||||
sso: !!app.sso,
|
||||
mailboxName: (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app'
|
||||
};
|
||||
|
||||
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));
|
||||
appdb.add(newAppId, appStoreId, manifest, location, 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));
|
||||
|
||||
var data = {
|
||||
installationState: appdb.ISTATE_PENDING_CLONE,
|
||||
memoryLimit: app.memoryLimit,
|
||||
accessRestriction: app.accessRestriction,
|
||||
xFrameOptions: app.xFrameOptions,
|
||||
restoreConfig: { backupId: backupId, backupFormat: backupInfo.format },
|
||||
sso: !!app.sso,
|
||||
mailboxName: (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app',
|
||||
enableBackup: app.enableBackup,
|
||||
robotsTxt: app.robotsTxt
|
||||
};
|
||||
taskmanager.restartAppTask(newAppId);
|
||||
|
||||
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));
|
||||
eventlog.add(eventlog.ACTION_APP_CLONE, auditSource, { appId: newAppId, oldAppId: appId, backupId: backupId, location: location, manifest: manifest });
|
||||
|
||||
taskmanager.restartAppTask(newAppId);
|
||||
|
||||
// fetch fresh app object for eventlog
|
||||
get(appId, function (error, result) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
eventlog.add(eventlog.ACTION_APP_CLONE, auditSource, { appId: newAppId, oldAppId: appId, backupId: backupId, oldApp: app, newApp: result });
|
||||
|
||||
callback(null, { id : newAppId });
|
||||
});
|
||||
});
|
||||
callback(null, { id : newAppId });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -946,10 +882,10 @@ function uninstall(appId, auditSource, callback) {
|
||||
|
||||
debug('Will uninstall app with id:%s', appId);
|
||||
|
||||
get(appId, function (error, app) {
|
||||
get(appId, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
appstore.unpurchase(appId, app.appStoreId, function (error) {
|
||||
appstore.unpurchase(appId, result.appStoreId, function (error) {
|
||||
if (error && error.reason === AppstoreError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
|
||||
if (error && error.reason === AppstoreError.BILLING_REQUIRED) return callback(new AppsError(AppsError.BILLING_REQUIRED, error.message));
|
||||
if (error && error.reason === AppstoreError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
|
||||
@@ -960,7 +896,7 @@ function uninstall(appId, auditSource, callback) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
eventlog.add(eventlog.ACTION_APP_UNINSTALL, auditSource, { appId: appId, app: app });
|
||||
eventlog.add(eventlog.ACTION_APP_UNINSTALL, auditSource, { appId: appId });
|
||||
|
||||
taskmanager.startAppTask(appId, callback);
|
||||
});
|
||||
@@ -1011,7 +947,7 @@ function checkManifestConstraints(manifest) {
|
||||
}
|
||||
|
||||
if (semver.valid(manifest.minBoxVersion) && semver.gt(manifest.minBoxVersion, config.version())) {
|
||||
return new AppsError(AppsError.BAD_FIELD, 'App version requires a new platform version');
|
||||
return new AppsError(AppsError.BAD_FIELD, 'minBoxVersion exceeds Box version');
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -1025,8 +961,9 @@ function exec(appId, options, callback) {
|
||||
var cmd = options.cmd || [ '/bin/bash' ];
|
||||
assert(util.isArray(cmd) && cmd.length > 0);
|
||||
|
||||
get(appId, function (error, app) {
|
||||
if (error) return callback(error);
|
||||
appdb.get(appId, function (error, app) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
if (app.installationState !== appdb.ISTATE_INSTALLED || app.runState !== appdb.RSTATE_RUNNING) {
|
||||
return callback(new AppsError(AppsError.BAD_STATE, 'App not installed or running'));
|
||||
@@ -1165,17 +1102,17 @@ function listBackups(page, perPage, appId, callback) {
|
||||
function restoreInstalledApps(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getAll(function (error, apps) {
|
||||
appdb.getAll(function (error, apps) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
async.map(apps, function (app, iteratorDone) {
|
||||
debug('marking %s for restore', app.location || app.id);
|
||||
|
||||
backups.getByAppIdPaged(1, 1, app.id, function (error, results) {
|
||||
var restoreConfig = !error && results.length ? { backupId: results[0].id, backupFormat: results[0].format } : null;
|
||||
|
||||
debug(`marking ${app.fqdn} for restore using restore config ${JSON.stringify(restoreConfig)}`);
|
||||
|
||||
appdb.setInstallationCommand(app.id, appdb.ISTATE_PENDING_RESTORE, { restoreConfig: restoreConfig, oldConfig: null }, function (error) {
|
||||
if (error) debug(`Error marking ${app.fqdn} for restore: ${JSON.stringify(error)}`);
|
||||
if (error) debug('did not mark %s for restore', app.location || app.id, error);
|
||||
|
||||
iteratorDone(); // always succeed
|
||||
});
|
||||
@@ -1187,14 +1124,14 @@ function restoreInstalledApps(callback) {
|
||||
function configureInstalledApps(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getAll(function (error, apps) {
|
||||
appdb.getAll(function (error, apps) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
async.map(apps, function (app, iteratorDone) {
|
||||
debug(`marking ${app.fqdn} for reconfigure`);
|
||||
debug('marking %s for reconfigure', app.location || app.id);
|
||||
|
||||
appdb.setInstallationCommand(app.id, appdb.ISTATE_PENDING_CONFIGURE, { oldConfig: null }, function (error) {
|
||||
if (error) debug(`Error marking ${app.fqdn} for reconfigure: ${JSON.stringify(error)}`);
|
||||
if (error) debug('did not mark %s for reconfigure', app.location || app.id, error);
|
||||
|
||||
iteratorDone(); // always succeed
|
||||
});
|
||||
|
||||
+79
-122
@@ -18,17 +18,11 @@ exports = module.exports = {
|
||||
AppstoreError: AppstoreError
|
||||
};
|
||||
|
||||
var apps = require('./apps.js'),
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
var assert = require('assert'),
|
||||
config = require('./config.js'),
|
||||
debug = require('debug')('box:appstore'),
|
||||
domains = require('./domains.js'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
mail = require('./mail.js'),
|
||||
os = require('os'),
|
||||
safe = require('safetydance'),
|
||||
semver = require('semver'),
|
||||
settings = require('./settings.js'),
|
||||
superagent = require('superagent'),
|
||||
util = require('util');
|
||||
@@ -62,12 +56,23 @@ var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
function getAppstoreConfig(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settings.getAppstoreConfig(function (error, result) {
|
||||
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
|
||||
if (!result.token) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED));
|
||||
// Caas Cloudrons do not store appstore credentials in their local database
|
||||
if (config.provider() === 'caas') {
|
||||
var url = config.apiServerOrigin() + '/api/v1/exchangeBoxTokenWithUserToken';
|
||||
superagent.post(url).query({ token: config.token() }).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
|
||||
if (result.statusCode !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App unpurchase failed. %s %j', result.status, result.body)));
|
||||
|
||||
callback(null, result);
|
||||
});
|
||||
callback(null, result.body);
|
||||
});
|
||||
} else {
|
||||
settings.getAppstoreConfig(function (error, result) {
|
||||
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
|
||||
if (!result.token) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED));
|
||||
|
||||
callback(null, result);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getSubscription(callback) {
|
||||
@@ -130,7 +135,7 @@ function unpurchase(appId, appstoreId, callback) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED));
|
||||
if (result.statusCode === 404) return callback(null); // was never purchased
|
||||
if (result.statusCode !== 201 && result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App unpurchase failed. %s %j', result.status, result.body)));
|
||||
if (result.statusCode !== 201 && result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App purchase failed. %s %j', result.status, result.body)));
|
||||
|
||||
superagent.del(url).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
|
||||
@@ -143,87 +148,66 @@ function unpurchase(appId, appstoreId, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function sendAliveStatus(callback) {
|
||||
function sendAliveStatus(data, callback) {
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
|
||||
var allSettings, allDomains, mailDomains, loginEvents;
|
||||
settings.getAll(function (error, result) {
|
||||
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
|
||||
|
||||
async.series([
|
||||
function (callback) {
|
||||
settings.getAll(function (error, result) {
|
||||
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
|
||||
allSettings = result;
|
||||
callback();
|
||||
});
|
||||
},
|
||||
function (callback) {
|
||||
domains.getAll(function (error, result) {
|
||||
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
|
||||
allDomains = result;
|
||||
callback();
|
||||
});
|
||||
},
|
||||
function (callback) {
|
||||
mail.getAll(function (error, result) {
|
||||
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
|
||||
mailDomains = result;
|
||||
callback();
|
||||
});
|
||||
},
|
||||
function (callback) {
|
||||
eventlog.getAllPaged([ eventlog.ACTION_USER_LOGIN ], null, 1, 1, function (error, result) {
|
||||
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
|
||||
loginEvents = result;
|
||||
callback();
|
||||
});
|
||||
}
|
||||
], function (error) {
|
||||
if (error) return callback(error);
|
||||
eventlog.getAllPaged(eventlog.ACTION_USER_LOGIN, null, 1, 1, function (error, loginEvents) {
|
||||
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
|
||||
|
||||
var backendSettings = {
|
||||
backupConfig: {
|
||||
provider: allSettings[settings.BACKUP_CONFIG_KEY].provider,
|
||||
hardlinks: !allSettings[settings.BACKUP_CONFIG_KEY].noHardlinks
|
||||
},
|
||||
domainConfig: {
|
||||
count: allDomains.length,
|
||||
domains: Array.from(new Set(allDomains.map(function (d) { return { domain: d.domain, provider: d.provider }; })))
|
||||
},
|
||||
mailConfig: {
|
||||
outboundCount: mailDomains.length,
|
||||
inboundCount: mailDomains.filter(function (d) { return d.enabled; }).length,
|
||||
catchAllCount: mailDomains.filter(function (d) { return d.catchAll.length !== 0; }).length,
|
||||
relayProviders: Array.from(new Set(mailDomains.map(function (d) { return d.relay.provider; })))
|
||||
},
|
||||
appAutoupdatePattern: allSettings[settings.APP_AUTOUPDATE_PATTERN_KEY],
|
||||
boxAutoupdatePattern: allSettings[settings.BOX_AUTOUPDATE_PATTERN_KEY],
|
||||
timeZone: allSettings[settings.TIME_ZONE_KEY],
|
||||
};
|
||||
var backendSettings = {
|
||||
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
|
||||
},
|
||||
backupConfig: {
|
||||
provider: result[settings.BACKUP_CONFIG_KEY].provider,
|
||||
hardlinks: !result[settings.BACKUP_CONFIG_KEY].noHardlinks
|
||||
},
|
||||
mailConfig: {
|
||||
enabled: result[settings.MAIL_CONFIG_KEY].enabled
|
||||
},
|
||||
mailRelay: {
|
||||
provider: result[settings.MAIL_RELAY_KEY].provider
|
||||
},
|
||||
mailCatchAll: {
|
||||
count: result[settings.CATCH_ALL_ADDRESS_KEY].length
|
||||
},
|
||||
autoupdatePattern: result[settings.AUTOUPDATE_PATTERN_KEY],
|
||||
timeZone: result[settings.TIME_ZONE_KEY],
|
||||
};
|
||||
|
||||
var data = {
|
||||
version: config.version(),
|
||||
adminFqdn: config.adminFqdn(),
|
||||
provider: config.provider(),
|
||||
backendSettings: backendSettings,
|
||||
machine: {
|
||||
cpus: os.cpus(),
|
||||
totalmem: os.totalmem()
|
||||
},
|
||||
events: {
|
||||
lastLogin: loginEvents[0] ? (new Date(loginEvents[0].creationTime).getTime()) : 0
|
||||
}
|
||||
};
|
||||
var data = {
|
||||
domain: config.fqdn(),
|
||||
version: config.version(),
|
||||
adminFqdn: config.adminFqdn(),
|
||||
provider: config.provider(),
|
||||
backendSettings: backendSettings,
|
||||
machine: {
|
||||
cpus: os.cpus(),
|
||||
totalmem: os.totalmem()
|
||||
},
|
||||
events: {
|
||||
lastLogin: loginEvents[0] ? (new Date(loginEvents[0].creationTime).getTime()) : 0
|
||||
}
|
||||
};
|
||||
|
||||
getAppstoreConfig(function (error, appstoreConfig) {
|
||||
if (error) return callback(error);
|
||||
getAppstoreConfig(function (error, appstoreConfig) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/alive';
|
||||
superagent.post(url).send(data).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
|
||||
if (result.statusCode === 404) return callback(new AppstoreError(AppstoreError.NOT_FOUND));
|
||||
if (result.statusCode !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Sending alive status failed. %s %j', result.status, result.body)));
|
||||
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/alive';
|
||||
superagent.post(url).send(data).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
|
||||
if (result.statusCode === 404) return callback(new AppstoreError(AppstoreError.NOT_FOUND));
|
||||
if (result.statusCode !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Sending alive status failed. %s %j', result.status, result.body)));
|
||||
|
||||
callback(null);
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -240,16 +224,10 @@ function getBoxUpdate(callback) {
|
||||
superagent.get(url).query({ accessToken: appstoreConfig.token, boxVersion: config.version() }).timeout(10 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
|
||||
if (result.statusCode === 204) return callback(null); // no update
|
||||
if (result.statusCode !== 200 || !result.body) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
|
||||
|
||||
var updateInfo = result.body;
|
||||
|
||||
if (!semver.valid(updateInfo.version) || semver.gt(config.version(), updateInfo.version)) {
|
||||
return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
|
||||
}
|
||||
if (result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
|
||||
|
||||
// { version, changelog, upgrade, sourceTarballUrl}
|
||||
callback(null, updateInfo);
|
||||
callback(null, result.body);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -266,21 +244,10 @@ function getAppUpdate(app, callback) {
|
||||
superagent.get(url).query({ accessToken: appstoreConfig.token, boxVersion: config.version(), appId: app.appStoreId, appVersion: app.manifest.version }).timeout(10 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
|
||||
if (result.statusCode === 204) return callback(null); // no update
|
||||
if (result.statusCode !== 200 || !result.body) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
|
||||
|
||||
const updateInfo = result.body;
|
||||
|
||||
// for the appstore, x.y.z is the same as x.y.z-0 but in semver, x.y.z > x.y.z-0
|
||||
const curAppVersion = semver.prerelease(app.manifest.version) ? app.manifest.version : `${app.manifest.version}-0`;
|
||||
|
||||
// do some sanity checks
|
||||
if (!safe.query(updateInfo, 'manifest.version') || semver.gt(curAppVersion, safe.query(updateInfo, 'manifest.version'))) {
|
||||
debug('Skipping malformed update of app %s version: %s. got %j', app.id, curAppVersion, updateInfo);
|
||||
return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Malformed update: %s %s', result.statusCode, result.text)));
|
||||
}
|
||||
if (result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
|
||||
|
||||
// { id, creationDate, manifest }
|
||||
callback(null, updateInfo);
|
||||
callback(null, result.body);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -312,26 +279,16 @@ function sendFeedback(info, callback) {
|
||||
assert.strictEqual(typeof info.description, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
function collectAppInfoIfNeeded(callback) {
|
||||
if (!info.appId) return callback();
|
||||
apps.get(info.appId, callback);
|
||||
}
|
||||
|
||||
getAppstoreConfig(function (error, appstoreConfig) {
|
||||
if (error) return callback(error);
|
||||
|
||||
collectAppInfoIfNeeded(function (error, result) {
|
||||
if (error) console.error('Unable to get app info', error);
|
||||
if (result) info.app = result;
|
||||
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/feedback';
|
||||
|
||||
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/feedback';
|
||||
superagent.post(url).query({ accessToken: appstoreConfig.token }).send(info).timeout(10 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
|
||||
if (result.statusCode !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
|
||||
|
||||
superagent.post(url).query({ accessToken: appstoreConfig.token }).send(info).timeout(10 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
|
||||
if (result.statusCode !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+97
-70
@@ -8,14 +8,15 @@ exports = module.exports = {
|
||||
|
||||
// exported for testing
|
||||
_reserveHttpPort: reserveHttpPort,
|
||||
_configureReverseProxy: configureReverseProxy,
|
||||
_unconfigureReverseProxy: unconfigureReverseProxy,
|
||||
_configureNginx: configureNginx,
|
||||
_unconfigureNginx: unconfigureNginx,
|
||||
_createVolume: createVolume,
|
||||
_deleteVolume: deleteVolume,
|
||||
_verifyManifest: verifyManifest,
|
||||
_registerSubdomain: registerSubdomain,
|
||||
_unregisterSubdomain: unregisterSubdomain,
|
||||
_waitForDnsPropagation: waitForDnsPropagation
|
||||
_waitForDnsPropagation: waitForDnsPropagation,
|
||||
_waitForAltDomainDnsPropagation: waitForAltDomainDnsPropagation
|
||||
};
|
||||
|
||||
require('supererror')({ splatchError: true });
|
||||
@@ -31,23 +32,24 @@ var addons = require('./addons.js'),
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
backups = require('./backups.js'),
|
||||
certificates = require('./certificates.js'),
|
||||
config = require('./config.js'),
|
||||
database = require('./database.js'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
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'),
|
||||
net = require('net'),
|
||||
nginx = require('./nginx.js'),
|
||||
os = require('os'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
reverseProxy = require('./reverseproxy.js'),
|
||||
safe = require('safetydance'),
|
||||
shell = require('./shell.js'),
|
||||
SubdomainError = require('./subdomains.js').SubdomainError,
|
||||
subdomains = require('./subdomains.js'),
|
||||
superagent = require('superagent'),
|
||||
sysinfo = require('./sysinfo.js'),
|
||||
tld = require('tldjs'),
|
||||
@@ -70,26 +72,8 @@ function initialize(callback) {
|
||||
function debugApp(app) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
|
||||
debug(app.fqdn + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
|
||||
}
|
||||
|
||||
// updates the app object and the database
|
||||
function updateApp(app, values, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof values, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debugApp(app, 'updating app with values: %j', values);
|
||||
|
||||
appdb.update(app.id, values, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
for (var value in values) {
|
||||
app[value] = values[value];
|
||||
}
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
var prefix = app ? (app.location || '(bare)') : '(no app)';
|
||||
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
|
||||
}
|
||||
|
||||
function reserveHttpPort(app, callback) {
|
||||
@@ -110,19 +94,23 @@ function reserveHttpPort(app, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function configureReverseProxy(app, callback) {
|
||||
function configureNginx(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
reverseProxy.configureApp(app, { userId: null, username: 'apptask' }, callback);
|
||||
certificates.ensureCertificate(app, function (error, certFilePath, keyFilePath) {
|
||||
if (error) return callback(error);
|
||||
|
||||
nginx.configureApp(app, certFilePath, keyFilePath, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function unconfigureReverseProxy(app, callback) {
|
||||
function unconfigureNginx(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// TODO: maybe revoke the cert
|
||||
reverseProxy.unconfigureApp(app, callback);
|
||||
nginx.unconfigureApp(app, callback);
|
||||
}
|
||||
|
||||
function createContainer(app, callback) {
|
||||
@@ -264,17 +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.fqdn, overwrite);
|
||||
debugApp(app, 'Registering subdomain location [%s] overwrite: %s', app.location, overwrite);
|
||||
|
||||
// get the current record before updating it
|
||||
domains.getDnsRecords(app.location, app.domain, 'A', function (error, values) {
|
||||
subdomains.get(app.location, 'A', function (error, values) {
|
||||
if (error) return retryCallback(error);
|
||||
|
||||
// refuse to update any existing DNS record for custom domains that we did not create
|
||||
if (values.length !== 0 && !overwrite) return retryCallback(null, new Error('DNS Record already exists'));
|
||||
// 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'));
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
retryCallback(null, error || changeId);
|
||||
});
|
||||
@@ -288,12 +277,17 @@ function registerSubdomain(app, overwrite, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function unregisterSubdomain(app, location, domain, callback) {
|
||||
function unregisterSubdomain(app, location, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// 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');
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
if (!app.dnsRecordId) {
|
||||
debugApp(app, 'Skip unregister of record not created by cloudron');
|
||||
return callback(null);
|
||||
@@ -303,11 +297,10 @@ function unregisterSubdomain(app, location, domain, callback) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.retry({ times: 30, interval: 5000 }, function (retryCallback) {
|
||||
debugApp(app, 'Unregistering subdomain: %s', app.fqdn);
|
||||
debugApp(app, 'Unregistering subdomain: %s', location);
|
||||
|
||||
domains.removeDnsRecords(location, domain, 'A', [ ip ], function (error) {
|
||||
if (error && error.reason === DomainError.NOT_FOUND) return retryCallback(null, null); // domain can be not found if oldConfig.domain or restoreConfig.domain was removed
|
||||
if (error && (error.reason === DomainError.STILL_BUSY || error.reason === DomainError.EXTERNAL_ERROR)) return retryCallback(error); // try again
|
||||
subdomains.remove(location, 'A', [ ip ], function (error) {
|
||||
if (error && (error.reason === SubdomainError.STILL_BUSY || error.reason === SubdomainError.EXTERNAL_ERROR)) return retryCallback(error); // try again
|
||||
|
||||
retryCallback(null, error);
|
||||
});
|
||||
@@ -341,7 +334,43 @@ function waitForDnsPropagation(app, callback) {
|
||||
sysinfo.getPublicIp(function (error, ip) {
|
||||
if (error) return callback(error);
|
||||
|
||||
domains.waitForDnsRecord(app.fqdn, app.domain, ip, { interval: 5000, times: 120 }, callback);
|
||||
subdomains.waitForDns(config.appFqdn(app.location), ip, 'A', { interval: 5000, times: 120 }, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function waitForAltDomainDnsPropagation(app, callback) {
|
||||
if (!app.altDomain) return callback(null);
|
||||
|
||||
// try for 10 minutes before giving up. this allows the user to "reconfigure" the app in the case where
|
||||
// an app has an external domain and cloudron is migrated to custom domain.
|
||||
var isNakedDomain = tld.getDomain(app.altDomain) === app.altDomain;
|
||||
if (isNakedDomain) { // check naked domains with A record since CNAME records don't work there
|
||||
sysinfo.getPublicIp(function (error, ip) {
|
||||
if (error) return callback(error);
|
||||
|
||||
subdomains.waitForDns(app.altDomain, ip, 'A', { interval: 10000, times: 60 }, callback);
|
||||
});
|
||||
} else {
|
||||
subdomains.waitForDns(app.altDomain, config.appFqdn(app.location) + '.', 'CNAME', { interval: 10000, times: 60 }, callback);
|
||||
}
|
||||
}
|
||||
|
||||
// updates the app object and the database
|
||||
function updateApp(app, values, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof values, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debugApp(app, 'updating app with values: %j', values);
|
||||
|
||||
appdb.update(app.id, values, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
for (var value in values) {
|
||||
app[value] = values[value];
|
||||
}
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -369,7 +398,7 @@ function install(app, callback) {
|
||||
|
||||
// teardown for re-installs
|
||||
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
|
||||
unconfigureReverseProxy.bind(null, app),
|
||||
unconfigureNginx.bind(null, app),
|
||||
removeCollectdProfile.bind(null, app),
|
||||
removeLogrotateConfig.bind(null, app),
|
||||
stopApp.bind(null, app),
|
||||
@@ -427,8 +456,11 @@ function install(app, callback) {
|
||||
updateApp.bind(null, app, { installationProgress: '85, Waiting for DNS propagation' }),
|
||||
exports._waitForDnsPropagation.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '95, Configuring reverse proxy' }),
|
||||
configureReverseProxy.bind(null, app),
|
||||
updateApp.bind(null, app, { installationProgress: '90, Waiting for External Domain setup' }),
|
||||
exports._waitForAltDomainDnsPropagation.bind(null, app), // required when restoring and !restoreConfig
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '95, Configure nginx' }),
|
||||
configureNginx.bind(null, app),
|
||||
|
||||
// done!
|
||||
function (callback) {
|
||||
@@ -450,7 +482,7 @@ function backup(app, callback) {
|
||||
|
||||
async.series([
|
||||
updateApp.bind(null, app, { installationProgress: '10, Backing up' }),
|
||||
backups.backupApp.bind(null, app),
|
||||
backups.backupApp.bind(null, app, app.manifest),
|
||||
|
||||
// done!
|
||||
function (callback) {
|
||||
@@ -472,19 +504,18 @@ function configure(app, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// oldConfig can be null during an infra update
|
||||
var locationChanged = app.oldConfig && (app.oldConfig.fqdn !== app.fqdn);
|
||||
var locationChanged = app.oldConfig && app.oldConfig.location !== app.location;
|
||||
|
||||
async.series([
|
||||
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
|
||||
unconfigureReverseProxy.bind(null, app),
|
||||
unconfigureNginx.bind(null, app),
|
||||
removeCollectdProfile.bind(null, app),
|
||||
removeLogrotateConfig.bind(null, app),
|
||||
stopApp.bind(null, app),
|
||||
deleteContainers.bind(null, app),
|
||||
function (next) {
|
||||
if (!locationChanged) return next();
|
||||
|
||||
unregisterSubdomain(app, app.oldConfig.location, app.oldConfig.domain, next);
|
||||
unregisterSubdomain(app, app.oldConfig.location, next);
|
||||
},
|
||||
|
||||
reserveHttpPort.bind(null, app),
|
||||
@@ -519,8 +550,11 @@ function configure(app, callback) {
|
||||
updateApp.bind(null, app, { installationProgress: '80, Waiting for DNS propagation' }),
|
||||
exports._waitForDnsPropagation.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '90, Configuring reverse proxy' }),
|
||||
configureReverseProxy.bind(null, app),
|
||||
updateApp.bind(null, app, { installationProgress: '85, Waiting for External Domain setup' }),
|
||||
exports._waitForAltDomainDnsPropagation.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '90, Configuring Nginx' }),
|
||||
configureNginx.bind(null, app),
|
||||
|
||||
// done!
|
||||
function (callback) {
|
||||
@@ -558,11 +592,8 @@ function update(app, callback) {
|
||||
|
||||
async.series([
|
||||
updateApp.bind(null, app, { installationProgress: '15, Backing up app' }),
|
||||
backups.backupApp.bind(null, app)
|
||||
], function (error) {
|
||||
if (error) error.backupError = true;
|
||||
next(error);
|
||||
});
|
||||
backups.backupApp.bind(null, app, app.manifest)
|
||||
], next);
|
||||
},
|
||||
|
||||
// download new image before app is stopped. this is so we can reduce downtime
|
||||
@@ -614,7 +645,7 @@ function update(app, callback) {
|
||||
downloadIcon.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '70, Updating addons' }),
|
||||
addons.setupAddons.bind(null, app, app.updateConfig.manifest.addons),
|
||||
addons.setupAddons.bind(null, app, app.manifest.addons),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '80, Creating container' }),
|
||||
createContainer.bind(null, app),
|
||||
@@ -630,18 +661,14 @@ function update(app, callback) {
|
||||
// done!
|
||||
function (callback) {
|
||||
debugApp(app, 'updated');
|
||||
updateApp(app, { installationState: appdb.ISTATE_INSTALLED, installationProgress: '', health: null, updateConfig: null, updateTime: new Date() }, callback);
|
||||
updateApp(app, { installationState: appdb.ISTATE_INSTALLED, installationProgress: '', health: null, updateConfig: null }, callback);
|
||||
}
|
||||
], function seriesDone(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) {
|
||||
if (error) {
|
||||
debugApp(app, 'Error updating app: %s', error);
|
||||
updateApp(app, { installationState: appdb.ISTATE_ERROR, installationProgress: error.message, updateTime: new Date() }, callback.bind(null, error));
|
||||
} else {
|
||||
callback(null);
|
||||
return updateApp(app, { installationState: appdb.ISTATE_ERROR, installationProgress: error.message }, callback.bind(null, error));
|
||||
}
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -674,13 +701,13 @@ function uninstall(app, callback) {
|
||||
docker.deleteImage.bind(null, app.manifest),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '60, Unregistering subdomain' }),
|
||||
unregisterSubdomain.bind(null, app, app.location, app.domain),
|
||||
unregisterSubdomain.bind(null, app, app.location),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '80, Cleanup icon' }),
|
||||
removeIcon.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '90, Unconfiguring reverse proxy' }),
|
||||
unconfigureReverseProxy.bind(null, app),
|
||||
updateApp.bind(null, app, { installationProgress: '90, Unconfiguring Nginx' }),
|
||||
unconfigureNginx.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '95, Remove app from database' }),
|
||||
appdb.del.bind(null, app.id)
|
||||
@@ -738,7 +765,7 @@ function startTask(appId, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// determine what to do
|
||||
apps.get(appId, function (error, app) {
|
||||
appdb.get(appId, function (error, app) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debugApp(app, 'startTask installationState: %s runState: %s', app.installationState, app.runState);
|
||||
|
||||
+14
-14
@@ -25,22 +25,22 @@ var assert = require('assert'),
|
||||
|
||||
function initialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
|
||||
passport.serializeUser(function (user, callback) {
|
||||
callback(null, user.id);
|
||||
});
|
||||
|
||||
|
||||
passport.deserializeUser(function(userId, callback) {
|
||||
user.get(userId, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var md5 = crypto.createHash('md5').update(result.email).digest('hex');
|
||||
|
||||
var md5 = crypto.createHash('md5').update(result.alternateEmail || result.email).digest('hex');
|
||||
result.gravatar = 'https://www.gravatar.com/avatar/' + md5 + '.jpg?s=24&d=mm';
|
||||
|
||||
|
||||
callback(null, result);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
passport.use(new LocalStrategy(function (username, password, callback) {
|
||||
if (username.indexOf('@') === -1) {
|
||||
user.verifyWithUsername(username, password, function (error, result) {
|
||||
@@ -60,7 +60,7 @@ function initialize(callback) {
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
passport.use(new BasicStrategy(function (username, password, callback) {
|
||||
if (username.indexOf('cid-') === 0) {
|
||||
debug('BasicStrategy: detected client id %s instead of username:password', username);
|
||||
@@ -82,7 +82,7 @@ function initialize(callback) {
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
passport.use(new ClientPasswordStrategy(function (clientId, clientSecret, callback) {
|
||||
clients.get(clientId, function(error, client) {
|
||||
if (error && error.reason === ClientsError.NOT_FOUND) return callback(null, false);
|
||||
@@ -91,15 +91,15 @@ function initialize(callback) {
|
||||
return callback(null, client);
|
||||
});
|
||||
}));
|
||||
|
||||
|
||||
passport.use(new BearerStrategy(accessTokenAuth));
|
||||
|
||||
|
||||
callback(null);
|
||||
}
|
||||
|
||||
function uninitialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
|
||||
callback(null);
|
||||
}
|
||||
|
||||
@@ -110,15 +110,15 @@ function accessTokenAuth(accessToken, callback) {
|
||||
tokendb.get(accessToken, function (error, token) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, false);
|
||||
if (error) return callback(error);
|
||||
|
||||
|
||||
// scopes here can define what capabilities that token carries
|
||||
// passport put the 'info' object into req.authInfo, where we can further validate the scopes
|
||||
var info = { scope: token.scope };
|
||||
|
||||
|
||||
user.get(token.identifier, function (error, user) {
|
||||
if (error && error.reason === UserError.NOT_FOUND) return callback(null, false);
|
||||
if (error) return callback(error);
|
||||
|
||||
|
||||
callback(null, user, info);
|
||||
});
|
||||
});
|
||||
|
||||
+63
-79
@@ -13,14 +13,13 @@ exports = module.exports = {
|
||||
ensureBackup: ensureBackup,
|
||||
|
||||
backup: backup,
|
||||
restore: restore,
|
||||
|
||||
backupApp: backupApp,
|
||||
restoreApp: restoreApp,
|
||||
|
||||
backupBoxAndApps: backupBoxAndApps,
|
||||
|
||||
upload: upload,
|
||||
download: download,
|
||||
|
||||
cleanup: cleanup,
|
||||
cleanupCacheFilesSync: cleanupCacheFilesSync,
|
||||
@@ -42,7 +41,6 @@ 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,9 +66,10 @@ var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
var BACKUPTASK_CMD = path.join(__dirname, 'backuptask.js');
|
||||
|
||||
function debugApp(app) {
|
||||
assert(typeof app === 'object');
|
||||
assert(!app || typeof app === 'object');
|
||||
|
||||
debug(app.fqdn + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
|
||||
var prefix = app ? app.location : '(no app)';
|
||||
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
|
||||
}
|
||||
|
||||
function BackupsError(reason, errorOrMessage) {
|
||||
@@ -103,7 +102,6 @@ function api(provider) {
|
||||
switch (provider) {
|
||||
case 'caas': return require('./storage/s3.js');
|
||||
case 's3': return require('./storage/s3.js');
|
||||
case 'gcs': return require('./storage/gcs.js');
|
||||
case 'filesystem': return require('./storage/filesystem.js');
|
||||
case 'minio': return require('./storage/s3.js');
|
||||
case 's3-v4-compat': return require('./storage/s3.js');
|
||||
@@ -231,11 +229,6 @@ function sync(backupConfig, backupId, dataDir, callback) {
|
||||
assert.strictEqual(typeof dataDir, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
function setBackupProgress(message) {
|
||||
debug(message);
|
||||
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, message);
|
||||
}
|
||||
|
||||
syncer.sync(dataDir, function processTask(task, iteratorCallback) {
|
||||
debug('sync: processing task: %j', task);
|
||||
var backupFilePath = path.join(getBackupFilePath(backupConfig, backupId, backupConfig.format), task.path);
|
||||
@@ -243,23 +236,24 @@ function sync(backupConfig, backupId, dataDir, callback) {
|
||||
if (task.operation === 'removedir') {
|
||||
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, `Removing directory ${task.path}`);
|
||||
return api(backupConfig.provider).removeDir(backupConfig, backupFilePath)
|
||||
.on('progress', setBackupProgress)
|
||||
.on('progress', function (detail) {
|
||||
debug(`sync: ${detail}`);
|
||||
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, detail);
|
||||
})
|
||||
.on('done', iteratorCallback);
|
||||
} else if (task.operation === 'remove') {
|
||||
setBackupProgress(`Removing ${task.path}`);
|
||||
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, `Removing ${task.path}`);
|
||||
return api(backupConfig.provider).remove(backupConfig, backupFilePath, iteratorCallback);
|
||||
}
|
||||
|
||||
var retryCount = 0;
|
||||
async.retry({ times: 5, interval: 20000 }, function (retryCallback) {
|
||||
retryCallback = once(retryCallback); // protect again upload() erroring much later after read stream error
|
||||
|
||||
++retryCount;
|
||||
debug(`${task.operation} ${task.path} try ${retryCount}`);
|
||||
if (task.operation === 'add') {
|
||||
setBackupProgress(`Adding ${task.path}`);
|
||||
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, `Adding ${task.path}`);
|
||||
var stream = fs.createReadStream(path.join(dataDir, task.path));
|
||||
stream.on('error', function (error) { setBackupProgress(`read stream error for ${task.path}: ${error.message}`); retryCallback(); }); // ignore error if file disappears
|
||||
stream.on('error', function () { return retryCallback(); }); // ignore error if file disappears
|
||||
api(backupConfig.provider).upload(backupConfig, backupFilePath, stream, retryCallback);
|
||||
}
|
||||
}, iteratorCallback);
|
||||
@@ -297,6 +291,8 @@ function upload(backupId, format, dataDir, callback) {
|
||||
assert.strictEqual(typeof dataDir, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
callback = once(callback);
|
||||
|
||||
debug('upload: id %s format %s dataDir %s', backupId, format, dataDir);
|
||||
|
||||
settings.getBackupConfig(function (error, backupConfig) {
|
||||
@@ -304,8 +300,6 @@ function upload(backupId, format, dataDir, callback) {
|
||||
|
||||
if (format === 'tgz') {
|
||||
async.retry({ times: 5, interval: 20000 }, function (retryCallback) {
|
||||
retryCallback = once(retryCallback); // protect again upload() erroring much later after tar stream error
|
||||
|
||||
var tarStream = createTarPackStream(dataDir, backupConfig.key || null);
|
||||
tarStream.on('error', retryCallback); // already returns BackupsError
|
||||
|
||||
@@ -374,10 +368,8 @@ function restoreFsMetadata(appDataDir, callback) {
|
||||
|
||||
log('Recreating empty directories');
|
||||
|
||||
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));
|
||||
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));
|
||||
|
||||
async.eachSeries(metadata.emptyDirs, function createPath(emptyDir, iteratorDone) {
|
||||
mkdirp(path.join(appDataDir, emptyDir), iteratorDone);
|
||||
@@ -394,8 +386,7 @@ function restoreFsMetadata(appDataDir, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function download(backupConfig, backupId, format, dataDir, callback) {
|
||||
assert.strictEqual(typeof backupConfig, 'object');
|
||||
function download(backupId, format, dataDir, callback) {
|
||||
assert.strictEqual(typeof backupId, 'string');
|
||||
assert.strictEqual(typeof format, 'string');
|
||||
assert.strictEqual(typeof dataDir, 'string');
|
||||
@@ -405,36 +396,24 @@ function download(backupConfig, backupId, format, dataDir, callback) {
|
||||
|
||||
log(`Downloading ${backupId} of format ${format} to ${dataDir}`);
|
||||
|
||||
if (format === 'tgz') {
|
||||
api(backupConfig.provider).download(backupConfig, getBackupFilePath(backupConfig, backupId, format), function (error, sourceStream) {
|
||||
if (error) return callback(error);
|
||||
settings.getBackupConfig(function (error, backupConfig) {
|
||||
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, 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);
|
||||
if (format === 'tgz') {
|
||||
api(backupConfig.provider).download(backupConfig, getBackupFilePath(backupConfig, backupId, format), function (error, sourceStream) {
|
||||
if (error) return callback(error);
|
||||
|
||||
restoreFsMetadata(dataDir, callback);
|
||||
});
|
||||
}
|
||||
}
|
||||
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);
|
||||
|
||||
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();
|
||||
});
|
||||
restoreFsMetadata(dataDir, callback);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -448,17 +427,13 @@ function restoreApp(app, addonsToRestore, restoreConfig, callback) {
|
||||
|
||||
var startTime = new Date();
|
||||
|
||||
settings.getBackupConfig(function (error, backupConfig) {
|
||||
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||
async.series([
|
||||
download.bind(null, restoreConfig.backupId, restoreConfig.backupFormat, appDataDir),
|
||||
addons.restoreAddons.bind(null, app, addonsToRestore)
|
||||
], function (error) {
|
||||
debug('restoreApp: time: %s', (new Date() - startTime)/1000);
|
||||
|
||||
async.series([
|
||||
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);
|
||||
|
||||
callback(error);
|
||||
});
|
||||
callback(error);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -530,7 +505,13 @@ function snapshotBox(callback) {
|
||||
|
||||
log('Snapshotting box');
|
||||
|
||||
database.exportToFile(`${paths.BOX_DATA_DIR}/box.mysqldump`, function (error) {
|
||||
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) {
|
||||
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||
|
||||
return callback();
|
||||
@@ -621,8 +602,9 @@ function canBackupApp(app) {
|
||||
app.installationState === appdb.ISTATE_PENDING_UPDATE; // called from apptask
|
||||
}
|
||||
|
||||
function snapshotApp(app, callback) {
|
||||
function snapshotApp(app, manifest, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert(manifest && typeof manifest === 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
log(`Snapshotting app ${app.id}`);
|
||||
@@ -631,7 +613,7 @@ function snapshotApp(app, callback) {
|
||||
return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, 'Error creating config.json: ' + safe.error.message));
|
||||
}
|
||||
|
||||
addons.backupAddons(app, app.manifest.addons, function (error) {
|
||||
addons.backupAddons(app, manifest.addons, function (error) {
|
||||
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
|
||||
|
||||
return callback(null);
|
||||
@@ -674,16 +656,17 @@ function rotateAppBackup(backupConfig, app, timestamp, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function uploadAppSnapshot(backupConfig, app, callback) {
|
||||
function uploadAppSnapshot(backupConfig, app, manifest, 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, function (error) {
|
||||
snapshotApp(app, manifest, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var backupId = util.format('snapshot/app_%s', app.id);
|
||||
@@ -693,13 +676,14 @@ function uploadAppSnapshot(backupConfig, app, callback) {
|
||||
|
||||
debugApp(app, 'uploadAppSnapshot: %s done time: %s secs', backupId, (new Date() - startTime)/1000);
|
||||
|
||||
setSnapshotInfo(app.id, { timestamp: new Date().toISOString(), manifest: app.manifest, format: backupConfig.format }, callback);
|
||||
setSnapshotInfo(app.id, { timestamp: new Date().toISOString(), manifest: manifest, format: backupConfig.format }, callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function backupAppWithTimestamp(app, timestamp, callback) {
|
||||
function backupAppWithTimestamp(app, manifest, timestamp, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert(manifest && typeof manifest === 'object');
|
||||
assert.strictEqual(typeof timestamp, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -708,7 +692,7 @@ function backupAppWithTimestamp(app, timestamp, callback) {
|
||||
settings.getBackupConfig(function (error, backupConfig) {
|
||||
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||
|
||||
uploadAppSnapshot(backupConfig, app, function (error) {
|
||||
uploadAppSnapshot(backupConfig, app, manifest, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
rotateAppBackup(backupConfig, app, timestamp, callback);
|
||||
@@ -716,16 +700,17 @@ function backupAppWithTimestamp(app, timestamp, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function backupApp(app, callback) {
|
||||
function backupApp(app, manifest, 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.fqdn);
|
||||
progress.set(progress.BACKUP, 10, 'Backing up ' + (app.altDomain || config.appFqdn(app.location)));
|
||||
|
||||
backupAppWithTimestamp(app, timestamp, function (error) {
|
||||
backupAppWithTimestamp(app, manifest, timestamp, function (error) {
|
||||
progress.set(progress.BACKUP, 100, error ? error.message : '');
|
||||
|
||||
callback(error);
|
||||
@@ -750,22 +735,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.fqdn);
|
||||
progress.set(progress.BACKUP, step * processed, 'Backing up ' + (app.altDomain || config.appFqdn(app.location)));
|
||||
|
||||
++processed;
|
||||
|
||||
if (!app.enableBackup) {
|
||||
progress.set(progress.BACKUP, step * processed, 'Skipped backup ' + app.fqdn);
|
||||
progress.set(progress.BACKUP, step * processed, 'Skipped backup ' + (app.altDomain || config.appFqdn(app.location)));
|
||||
return iteratorCallback(null, null); // nothing to backup
|
||||
}
|
||||
|
||||
backupAppWithTimestamp(app, timestamp, function (error, backupId) {
|
||||
backupAppWithTimestamp(app, app.manifest, 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.fqdn);
|
||||
progress.set(progress.BACKUP, step * processed, 'Backed up ' + (app.altDomain || config.appFqdn(app.location)));
|
||||
|
||||
iteratorCallback(null, backupId || null); // clear backupId if is in BAD_STATE and never backed up
|
||||
});
|
||||
@@ -844,7 +829,7 @@ function cleanupBackup(backupConfig, backup, callback) {
|
||||
function done(error) {
|
||||
if (error) {
|
||||
debug('cleanupBackup: error removing backup %j : %s', backup, error.message);
|
||||
return callback();
|
||||
callback();
|
||||
}
|
||||
|
||||
// prune empty directory if possible
|
||||
@@ -1016,4 +1001,3 @@ function cleanup(auditSource, callback) {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
-266
@@ -1,266 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
verifySetupToken: verifySetupToken,
|
||||
setupDone: setupDone,
|
||||
|
||||
changePlan: changePlan,
|
||||
upgrade: upgrade,
|
||||
sendHeartbeat: sendHeartbeat,
|
||||
getBoxAndUserDetails: getBoxAndUserDetails,
|
||||
setPtrRecord: setPtrRecord,
|
||||
|
||||
CaasError: CaasError
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
backups = require('./backups.js'),
|
||||
config = require('./config.js'),
|
||||
debug = require('debug')('box:caas'),
|
||||
locker = require('./locker.js'),
|
||||
path = require('path'),
|
||||
progress = require('./progress.js'),
|
||||
settings = require('./settings.js'),
|
||||
shell = require('./shell.js'),
|
||||
superagent = require('superagent'),
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
|
||||
const RETIRE_CMD = path.join(__dirname, 'scripts/retire.sh');
|
||||
|
||||
function CaasError(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(CaasError, Error);
|
||||
CaasError.BAD_FIELD = 'Field error';
|
||||
CaasError.BAD_STATE = 'Bad state';
|
||||
CaasError.INVALID_TOKEN = 'Invalid Token';
|
||||
CaasError.INTERNAL_ERROR = 'Internal Error';
|
||||
CaasError.EXTERNAL_ERROR = 'External Error';
|
||||
|
||||
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
|
||||
function retire(reason, info, callback) {
|
||||
assert(reason === 'migrate' || reason === 'upgrade');
|
||||
info = info || { };
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
|
||||
var data = {
|
||||
apiServerOrigin: config.apiServerOrigin(),
|
||||
adminFqdn: config.adminFqdn()
|
||||
};
|
||||
shell.sudo('retire', [ RETIRE_CMD, reason, JSON.stringify(info), JSON.stringify(data) ], callback);
|
||||
}
|
||||
|
||||
function getCaasConfig(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settings.getCaasConfig(function (error, result) {
|
||||
if (error) return callback(new CaasError(CaasError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, result);
|
||||
});
|
||||
}
|
||||
|
||||
function verifySetupToken(setupToken, callback) {
|
||||
assert.strictEqual(typeof setupToken, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settings.getCaasConfig(function (error, caasConfig) {
|
||||
if (error) return callback(new CaasError(CaasError.INTERNAL_ERROR, error));
|
||||
|
||||
superagent.get(config.apiServerOrigin() + '/api/v1/boxes/' + caasConfig.boxId + '/setup/verify').query({ setupToken: setupToken })
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new CaasError(CaasError.EXTERNAL_ERROR, error));
|
||||
if (result.statusCode === 403) return callback(new CaasError(CaasError.INVALID_TOKEN));
|
||||
if (result.statusCode === 409) return callback(new CaasError(CaasError.BAD_STATE, 'Already setup'));
|
||||
if (result.statusCode !== 200) return callback(new CaasError(CaasError.EXTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setupDone(setupToken, callback) {
|
||||
assert.strictEqual(typeof setupToken, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settings.getCaasConfig(function (error, caasConfig) {
|
||||
if (error) return callback(new CaasError(CaasError.INTERNAL_ERROR, error));
|
||||
|
||||
// Now let the api server know we got activated
|
||||
superagent.post(config.apiServerOrigin() + '/api/v1/boxes/' + caasConfig.boxId + '/setup/done').query({ setupToken: setupToken })
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new CaasError(CaasError.EXTERNAL_ERROR, error));
|
||||
if (result.statusCode === 403) return callback(new CaasError(CaasError.INVALID_TOKEN));
|
||||
if (result.statusCode === 409) return callback(new CaasError(CaasError.BAD_STATE, 'Already setup'));
|
||||
if (result.statusCode !== 201) return callback(new CaasError(CaasError.EXTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
function doMigrate(options, caasConfig, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof caasConfig, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var error = locker.lock(locker.OP_MIGRATE);
|
||||
if (error) return callback(new CaasError(CaasError.BAD_STATE, error.message));
|
||||
|
||||
function unlock(error) {
|
||||
debug('Failed to migrate', error);
|
||||
locker.unlock(locker.OP_MIGRATE);
|
||||
progress.set(progress.MIGRATE, -1, 'Backup failed: ' + error.message);
|
||||
}
|
||||
|
||||
progress.set(progress.MIGRATE, 10, 'Backing up for migration');
|
||||
|
||||
// initiate the migration in the background
|
||||
backups.backupBoxAndApps({ userId: null, username: 'migrator' }, function (error) {
|
||||
if (error) return unlock(error);
|
||||
|
||||
debug('migrate: domain: %s size %s region %s', options.domain, options.size, options.region);
|
||||
|
||||
superagent
|
||||
.post(config.apiServerOrigin() + '/api/v1/boxes/' + caasConfig.boxId + '/migrate')
|
||||
.query({ token: caasConfig.token })
|
||||
.send(options)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return unlock(error); // network error
|
||||
if (result.statusCode === 409) return unlock(new CaasError(CaasError.BAD_STATE));
|
||||
if (result.statusCode === 404) return unlock(new CaasError(CaasError.NOT_FOUND));
|
||||
if (result.statusCode !== 202) return unlock(new CaasError(CaasError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
|
||||
|
||||
progress.set(progress.MIGRATE, 10, 'Migrating');
|
||||
|
||||
retire('migrate', _.pick(options, 'domain', 'size', 'region'));
|
||||
});
|
||||
});
|
||||
|
||||
callback(null);
|
||||
}
|
||||
|
||||
function changePlan(options, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (config.isDemo()) return callback(new CaasError(CaasError.BAD_FIELD, 'Not allowed in demo mode'));
|
||||
|
||||
getCaasConfig(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
doMigrate(options, result, callback);
|
||||
});
|
||||
}
|
||||
|
||||
// this function expects a lock
|
||||
function upgrade(boxUpdateInfo, callback) {
|
||||
assert(boxUpdateInfo !== null && typeof boxUpdateInfo === 'object');
|
||||
|
||||
function upgradeError(e) {
|
||||
progress.set(progress.UPDATE, -1, e.message);
|
||||
callback(e);
|
||||
}
|
||||
|
||||
progress.set(progress.UPDATE, 5, 'Backing up for upgrade');
|
||||
|
||||
backups.backupBoxAndApps({ userId: null, username: 'upgrader' }, function (error) {
|
||||
if (error) return upgradeError(error);
|
||||
|
||||
getCaasConfig(function (error, result) {
|
||||
if (error) return upgradeError(error);
|
||||
|
||||
superagent.post(config.apiServerOrigin() + '/api/v1/boxes/' + result.boxId + '/upgrade')
|
||||
.query({ token: result.token })
|
||||
.send({ version: boxUpdateInfo.version })
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return upgradeError(new Error('Network error making upgrade request: ' + error));
|
||||
if (result.statusCode !== 202) return upgradeError(new Error(util.format('Server not ready to upgrade. statusCode: %s body: %j', result.status, result.body)));
|
||||
|
||||
progress.set(progress.UPDATE, 10, 'Updating base system');
|
||||
|
||||
// no need to unlock since this is the last thing we ever do on this box
|
||||
callback();
|
||||
|
||||
retire('upgrade');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function sendHeartbeat() {
|
||||
assert(config.provider() === 'caas', 'Heartbeat is only sent for managed cloudrons');
|
||||
|
||||
getCaasConfig(function (error, result) {
|
||||
if (error) return debug('Caas config missing', error);
|
||||
|
||||
var url = config.apiServerOrigin() + '/api/v1/boxes/' + result.boxId + '/heartbeat';
|
||||
superagent.post(url).query({ token: result.token, version: config.version() }).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) debug('Network error sending heartbeat.', error);
|
||||
else if (result.statusCode !== 200) debug('Server responded to heartbeat with %s %s', result.statusCode, result.text);
|
||||
else debug('Heartbeat sent to %s', url);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getBoxAndUserDetails(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (config.provider() !== 'caas') return callback(null, {});
|
||||
|
||||
getCaasConfig(function (error, caasConfig) {
|
||||
if (error) return callback(error);
|
||||
|
||||
superagent
|
||||
.get(config.apiServerOrigin() + '/api/v1/boxes/' + caasConfig.boxId)
|
||||
.query({ token: caasConfig.token })
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new CaasError(CaasError.EXTERNAL_ERROR, 'Cannot reach appstore'));
|
||||
if (result.statusCode !== 200) return callback(new CaasError(CaasError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
|
||||
|
||||
return callback(null, result.body);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setPtrRecord(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getCaasConfig(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
superagent
|
||||
.post(config.apiServerOrigin() + '/api/v1/boxes/' + result.boxId + '/ptr')
|
||||
.query({ token: result.token })
|
||||
.send({ domain: domain })
|
||||
.timeout(5 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new CaasError(CaasError.EXTERNAL_ERROR, 'Cannot reach appstore'));
|
||||
if (result.statusCode !== 202) return callback(new CaasError(CaasError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
+4
-4
@@ -10,12 +10,12 @@ exports = module.exports = {
|
||||
var assert = require('assert'),
|
||||
debug = require('debug')('box:cert/caas.js');
|
||||
|
||||
function getCertificate(vhost, options, callback) {
|
||||
assert.strictEqual(typeof vhost, 'string');
|
||||
function getCertificate(domain, options, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('getCertificate: using fallback certificate', vhost);
|
||||
debug('getCertificate: using fallback certificate', domain);
|
||||
|
||||
return callback(null, '', '');
|
||||
return callback(null, 'cert/host.cert', 'cert/host.key');
|
||||
}
|
||||
|
||||
@@ -10,12 +10,12 @@ exports = module.exports = {
|
||||
var assert = require('assert'),
|
||||
debug = require('debug')('box:cert/fallback.js');
|
||||
|
||||
function getCertificate(vhost, options, callback) {
|
||||
assert.strictEqual(typeof vhost, 'string');
|
||||
function getCertificate(domain, options, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('getCertificate: using fallback certificate', vhost);
|
||||
debug('getCertificate: using fallback certificate', domain);
|
||||
|
||||
return callback(null, '', '');
|
||||
return callback(null, 'cert/host.cert', 'cert/host.key');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,433 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
CertificatesError: CertificatesError,
|
||||
|
||||
ensureFallbackCertificate: ensureFallbackCertificate,
|
||||
setFallbackCertificate: setFallbackCertificate,
|
||||
|
||||
validateCertificate: validateCertificate,
|
||||
ensureCertificate: ensureCertificate,
|
||||
|
||||
setAdminCertificate: setAdminCertificate,
|
||||
getAdminCertificate: getAdminCertificate,
|
||||
|
||||
renewAll: renewAll,
|
||||
|
||||
initialize: initialize,
|
||||
uninitialize: uninitialize,
|
||||
|
||||
events: null,
|
||||
|
||||
EVENT_CERT_CHANGED: 'cert_changed',
|
||||
|
||||
// exported for testing
|
||||
_getApi: getApi
|
||||
};
|
||||
|
||||
var acme = require('./cert/acme.js'),
|
||||
apps = require('./apps.js'),
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
caas = require('./cert/caas.js'),
|
||||
config = require('./config.js'),
|
||||
constants = require('./constants.js'),
|
||||
debug = require('debug')('box:certificates'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
fallback = require('./cert/fallback.js'),
|
||||
fs = require('fs'),
|
||||
mailer = require('./mailer.js'),
|
||||
nginx = require('./nginx.js'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance'),
|
||||
settings = require('./settings.js'),
|
||||
user = require('./user.js'),
|
||||
util = require('util');
|
||||
|
||||
function CertificatesError(reason, errorOrMessage) {
|
||||
assert.strictEqual(typeof reason, 'string');
|
||||
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
|
||||
|
||||
Error.call(this);
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
|
||||
this.name = this.constructor.name;
|
||||
this.reason = reason;
|
||||
if (typeof errorOrMessage === 'undefined') {
|
||||
this.message = reason;
|
||||
} else if (typeof errorOrMessage === 'string') {
|
||||
this.message = errorOrMessage;
|
||||
} else {
|
||||
this.message = 'Internal error';
|
||||
this.nestedError = errorOrMessage;
|
||||
}
|
||||
}
|
||||
util.inherits(CertificatesError, Error);
|
||||
CertificatesError.INTERNAL_ERROR = 'Internal Error';
|
||||
CertificatesError.INVALID_CERT = 'Invalid certificate';
|
||||
CertificatesError.NOT_FOUND = 'Not Found';
|
||||
|
||||
function initialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
exports.events = new (require('events').EventEmitter)();
|
||||
callback();
|
||||
}
|
||||
|
||||
function uninitialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
exports.events = null;
|
||||
callback();
|
||||
}
|
||||
|
||||
function getApi(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settings.getTlsConfig(function (error, tlsConfig) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (tlsConfig.provider === 'fallback') return callback(null, fallback, {});
|
||||
|
||||
// use acme if we have altDomain or the tlsConfig is not caas
|
||||
var api = (app.altDomain || tlsConfig.provider) !== 'caas' ? acme : caas;
|
||||
|
||||
var options = { };
|
||||
if (tlsConfig.provider === 'caas') {
|
||||
options.prod = true; // with altDomain, we will choose acme setting based on this
|
||||
} else { // acme
|
||||
options.prod = tlsConfig.provider.match(/.*-prod/) !== null;
|
||||
}
|
||||
|
||||
// registering user with an email requires A or MX record (https://github.com/letsencrypt/boulder/issues/1197)
|
||||
// we cannot use admin@fqdn because the user might not have set it up.
|
||||
// we simply update the account with the latest email we have each time when getting letsencrypt certs
|
||||
// https://github.com/ietf-wg-acme/acme/issues/30
|
||||
user.getOwner(function (error, owner) {
|
||||
options.email = error ? 'support@cloudron.io' : (owner.alternateEmail || owner.email); // can error if not activated yet
|
||||
|
||||
callback(null, api, options);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function ensureFallbackCertificate(callback) {
|
||||
// ensure a fallback certificate that much of our code requires
|
||||
var certFilePath = path.join(paths.APP_CERTS_DIR, 'host.cert');
|
||||
var keyFilePath = path.join(paths.APP_CERTS_DIR, 'host.key');
|
||||
|
||||
var fallbackCertPath = path.join(paths.NGINX_CERT_DIR, 'host.cert');
|
||||
var fallbackKeyPath = path.join(paths.NGINX_CERT_DIR, 'host.key');
|
||||
|
||||
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) { // existing custom fallback certs (when restarting, restoring, updating)
|
||||
debug('ensureFallbackCertificate: using fallback certs provided by user');
|
||||
if (!safe.child_process.execSync('cp ' + certFilePath + ' ' + fallbackCertPath)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
|
||||
if (!safe.child_process.execSync('cp ' + keyFilePath + ' ' + fallbackKeyPath)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
|
||||
|
||||
return callback();
|
||||
}
|
||||
|
||||
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()) {
|
||||
debug('ensureFallbackCertificate: generating self-signed certificate');
|
||||
var certCommand = util.format('openssl req -x509 -newkey rsa:2048 -keyout %s -out %s -days 3650 -subj /CN=*.%s -nodes', keyFilePath, certFilePath, config.fqdn());
|
||||
safe.child_process.execSync(certCommand);
|
||||
|
||||
if (!safe.child_process.execSync('cp ' + certFilePath + ' ' + fallbackCertPath)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
|
||||
if (!safe.child_process.execSync('cp ' + keyFilePath + ' ' + fallbackKeyPath)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
|
||||
|
||||
return callback();
|
||||
} else {
|
||||
debug('ensureFallbackCertificate: cannot generate fallback certificate without domain');
|
||||
return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, 'No domain set'));
|
||||
}
|
||||
}
|
||||
|
||||
function isExpiringSync(certFilePath, hours) {
|
||||
assert.strictEqual(typeof certFilePath, 'string');
|
||||
assert.strictEqual(typeof hours, 'number');
|
||||
|
||||
if (!fs.existsSync(certFilePath)) return 2; // not found
|
||||
|
||||
var result = safe.child_process.spawnSync('/usr/bin/openssl', [ 'x509', '-checkend', String(60 * 60 * hours), '-in', certFilePath ]);
|
||||
|
||||
debug('isExpiringSync: %s %s %s', certFilePath, result.stdout.toString('utf8').trim(), result.status);
|
||||
|
||||
return result.status === 1; // 1 - expired 0 - not expired
|
||||
}
|
||||
|
||||
function renewAll(auditSource, callback) {
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('renewAll: Checking certificates for renewal');
|
||||
|
||||
apps.getAll(function (error, allApps) {
|
||||
if (error) return callback(error);
|
||||
|
||||
allApps.push({ location: config.adminLocation() }); // 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 certFilePath = path.join(paths.APP_CERTS_DIR, appDomain + '.user.cert');
|
||||
var keyFilePath = path.join(paths.APP_CERTS_DIR, appDomain + '.user.key');
|
||||
|
||||
if (safe.fs.existsSync(certFilePath) && safe.fs.existsSync(keyFilePath)) {
|
||||
debug('renewAll: existing user key file for %s. skipping', appDomain);
|
||||
continue;
|
||||
}
|
||||
|
||||
// check if we have an auto cert to be renewed
|
||||
certFilePath = path.join(paths.APP_CERTS_DIR, appDomain + '.cert');
|
||||
keyFilePath = path.join(paths.APP_CERTS_DIR, appDomain + '.key');
|
||||
|
||||
if (!safe.fs.existsSync(keyFilePath)) {
|
||||
debug('renewAll: no existing key file for %s. skipping', appDomain);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isExpiringSync(certFilePath, 24 * 30)) { // expired or not found
|
||||
expiringApps.push(allApps[i]);
|
||||
}
|
||||
}
|
||||
|
||||
debug('renewAll: %j needs to be renewed', expiringApps.map(function (a) { return a.altDomain || config.appFqdn(a.location); }));
|
||||
|
||||
async.eachSeries(expiringApps, function iterator(app, iteratorCallback) {
|
||||
var domain = app.altDomain || config.appFqdn(app.location);
|
||||
|
||||
getApi(app, function (error, api, apiOptions) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('renewAll: renewing cert for %s with options %j', domain, apiOptions);
|
||||
|
||||
api.getCertificate(domain, apiOptions, function (error) {
|
||||
var certFilePath = path.join(paths.APP_CERTS_DIR, domain + '.cert');
|
||||
var keyFilePath = path.join(paths.APP_CERTS_DIR, domain + '.key');
|
||||
|
||||
var errorMessage = error ? error.message : '';
|
||||
eventlog.add(eventlog.ACTION_CERTIFICATE_RENEWAL, auditSource, { domain: domain, errorMessage: errorMessage });
|
||||
|
||||
if (error) {
|
||||
debug('renewAll: could not renew cert for %s because %s', domain, error);
|
||||
|
||||
mailer.certificateRenewalError(domain, errorMessage);
|
||||
|
||||
// check if we should fallback if we expire in the coming day
|
||||
if (!isExpiringSync(certFilePath, 24 * 1)) return iteratorCallback();
|
||||
|
||||
debug('renewAll: using fallback certs for %s since it expires soon', domain, error);
|
||||
|
||||
certFilePath = 'cert/host.cert';
|
||||
keyFilePath = '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() ?
|
||||
nginx.configureAdmin.bind(null, certFilePath, keyFilePath, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn())
|
||||
: nginx.configureApp.bind(null, app, certFilePath, keyFilePath);
|
||||
|
||||
configureFunc(function (ignoredError) {
|
||||
if (ignoredError) debug('fallbackExpiredCertificates: error reconfiguring app', ignoredError);
|
||||
|
||||
exports.events.emit(exports.EVENT_CERT_CHANGED, domain);
|
||||
|
||||
iteratorCallback(); // move to next app
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// note: https://tools.ietf.org/html/rfc4346#section-7.4.2 (certificate_list) requires that the
|
||||
// servers certificate appears first (and not the intermediate cert)
|
||||
function validateCertificate(cert, key, fqdn) {
|
||||
assert(cert === null || typeof cert === 'string');
|
||||
assert(key === null || typeof key === 'string');
|
||||
assert.strictEqual(typeof fqdn, 'string');
|
||||
|
||||
function matchesDomain(domain) {
|
||||
if (typeof domain !== 'string') return false;
|
||||
if (domain === fqdn) return true;
|
||||
if (domain.indexOf('*') === 0 && domain.slice(2) === fqdn.slice(fqdn.indexOf('.') + 1)) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (cert === null && key === null) return null;
|
||||
if (!cert && key) return new Error('missing cert');
|
||||
if (cert && !key) return new Error('missing key');
|
||||
|
||||
var result = safe.child_process.execSync('openssl x509 -noout -checkhost "' + fqdn + '"', { encoding: 'utf8', input: cert });
|
||||
if (!result) return new Error(util.format('could not get cert subject'));
|
||||
|
||||
// if no match, check alt names
|
||||
if (result.indexOf('does match certificate') === -1) {
|
||||
// https://github.com/drwetter/testssl.sh/pull/383
|
||||
var cmd = `openssl x509 -noout -text | grep -A3 "Subject Alternative Name" | \
|
||||
grep "DNS:" | \
|
||||
sed -e "s/DNS://g" -e "s/ //g" -e "s/,/ /g" -e "s/othername:<unsupported>//g"`;
|
||||
result = safe.child_process.execSync(cmd, { encoding: 'utf8', input: cert });
|
||||
var altNames = result ? [ ] : result.trim().split(' '); // might fail if cert has no SAN
|
||||
debug('validateCertificate: detected altNames as %j', altNames);
|
||||
|
||||
// check altNames
|
||||
if (!altNames.some(matchesDomain)) return new Error(util.format('cert 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');
|
||||
|
||||
// check expiration
|
||||
result = safe.child_process.execSync('openssl x509 -checkend 0', { encoding: 'utf8', input: cert });
|
||||
if (!result) return new Error('cert expired');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function setFallbackCertificate(cert, key, callback) {
|
||||
assert.strictEqual(typeof cert, 'string');
|
||||
assert.strictEqual(typeof key, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var error = validateCertificate(cert, key, '*.' + config.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));
|
||||
|
||||
// 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));
|
||||
|
||||
exports.events.emit(exports.EVENT_CERT_CHANGED, '*.' + config.fqdn());
|
||||
|
||||
nginx.reload(function (error) {
|
||||
if (error) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, error));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function getFallbackCertificatePath(callback) {
|
||||
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'));
|
||||
}
|
||||
|
||||
function setAdminCertificate(cert, key, callback) {
|
||||
assert.strictEqual(typeof cert, 'string');
|
||||
assert.strictEqual(typeof key, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var vhost = config.adminFqdn();
|
||||
var certFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.user.cert');
|
||||
var keyFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.user.key');
|
||||
|
||||
var error = validateCertificate(cert, key, vhost);
|
||||
if (error) return callback(new CertificatesError(CertificatesError.INVALID_CERT, error.message));
|
||||
|
||||
// backup the cert
|
||||
if (!safe.fs.writeFileSync(certFilePath, cert)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
|
||||
if (!safe.fs.writeFileSync(keyFilePath, key)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
|
||||
|
||||
exports.events.emit(exports.EVENT_CERT_CHANGED, vhost);
|
||||
|
||||
nginx.configureAdmin(certFilePath, keyFilePath, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn(), callback);
|
||||
}
|
||||
|
||||
function getAdminCertificatePath(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var vhost = config.adminFqdn();
|
||||
var certFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.user.cert');
|
||||
var keyFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.user.key');
|
||||
|
||||
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, certFilePath, keyFilePath);
|
||||
|
||||
certFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.cert');
|
||||
keyFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.key');
|
||||
|
||||
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, certFilePath, keyFilePath);
|
||||
|
||||
getFallbackCertificatePath(callback);
|
||||
}
|
||||
|
||||
function getAdminCertificate(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getAdminCertificatePath(function (error, certFilePath, keyFilePath) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var cert = safe.fs.readFileSync(certFilePath);
|
||||
if (!cert) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error));
|
||||
|
||||
var key = safe.fs.readFileSync(keyFilePath);
|
||||
if (!cert) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error));
|
||||
|
||||
return callback(null, cert, key);
|
||||
});
|
||||
}
|
||||
|
||||
function ensureCertificate(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var domain = app.altDomain || config.appFqdn(app.location);
|
||||
|
||||
var certFilePath = path.join(paths.APP_CERTS_DIR, domain + '.user.cert');
|
||||
var keyFilePath = path.join(paths.APP_CERTS_DIR, domain + '.user.key');
|
||||
|
||||
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) {
|
||||
debug('ensureCertificate: %s. user certificate already exists at %s', domain, keyFilePath);
|
||||
return callback(null, certFilePath, keyFilePath);
|
||||
}
|
||||
|
||||
certFilePath = path.join(paths.APP_CERTS_DIR, domain + '.cert');
|
||||
keyFilePath = path.join(paths.APP_CERTS_DIR, domain + '.key');
|
||||
|
||||
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) {
|
||||
debug('ensureCertificate: %s. certificate already exists at %s', domain, keyFilePath);
|
||||
|
||||
if (!isExpiringSync(certFilePath, 24 * 1)) return callback(null, certFilePath, keyFilePath);
|
||||
debug('ensureCertificate: %s cert require renewal', domain);
|
||||
} else {
|
||||
debug('ensureCertificate: %s cert does not exist', domain);
|
||||
}
|
||||
|
||||
getApi(app, function (error, api, apiOptions) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('ensureCertificate: getting certificate for %s with options %j', domain, apiOptions);
|
||||
|
||||
api.getCertificate(domain, apiOptions, function (error, certFilePath, keyFilePath) {
|
||||
if (error) {
|
||||
debug('ensureCertificate: could not get certificate. using fallback certs', error);
|
||||
return callback(null, 'cert/host.cert', 'cert/host.key'); // use fallback certs
|
||||
}
|
||||
|
||||
callback(null, certFilePath, keyFilePath);
|
||||
});
|
||||
});
|
||||
}
|
||||
+11
-11
@@ -18,7 +18,7 @@ exports = module.exports = {
|
||||
|
||||
// keep this in sync with start.sh ADMIN_SCOPES that generates the cid-webadmin
|
||||
SCOPE_APPS: 'apps',
|
||||
SCOPE_DEVELOPER: 'developer', // obsolete
|
||||
SCOPE_DEVELOPER: 'developer',
|
||||
SCOPE_PROFILE: 'profile',
|
||||
SCOPE_CLOUDRON: 'cloudron',
|
||||
SCOPE_SETTINGS: 'settings',
|
||||
@@ -35,10 +35,11 @@ exports = module.exports = {
|
||||
TYPE_PROXY: 'addon-proxy'
|
||||
};
|
||||
|
||||
var apps = require('./apps.js'),
|
||||
var appdb = require('./appdb.js'),
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
clientdb = require('./clientdb.js'),
|
||||
config = require('./config.js'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
debug = require('debug')('box:clients'),
|
||||
hat = require('hat'),
|
||||
@@ -79,7 +80,7 @@ function validateName(name) {
|
||||
if (name.length < 1) return new ClientsError(ClientsError.BAD_FIELD, 'Name must be atleast 1 character');
|
||||
if (name.length > 128) return new ClientsError(ClientsError.BAD_FIELD, 'Name too long');
|
||||
|
||||
if (/[^a-zA-Z0-9-]/.test(name)) return new ClientsError(ClientsError.BAD_FIELD, 'Username can only contain alphanumerals and dash');
|
||||
if (/[^a-zA-Z0-9\-]/.test(name)) return new ClientsError(ClientsError.BAD_FIELD, 'Username can only contain alphanumerals and dash');
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -182,7 +183,7 @@ function getAll(callback) {
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
apps.get(record.appId, function (error, result) {
|
||||
appdb.get(record.appId, function (error, result) {
|
||||
if (error) {
|
||||
console.error('Failed to get app details for oauth client', record.appId, error);
|
||||
return callback(null); // ignore error so we continue listing clients
|
||||
@@ -191,7 +192,7 @@ function getAll(callback) {
|
||||
if (record.type === exports.TYPE_PROXY) record.name = result.manifest.title + ' Website Proxy';
|
||||
if (record.type === exports.TYPE_OAUTH) record.name = result.manifest.title + ' OAuth';
|
||||
|
||||
record.domain = result.fqdn;
|
||||
record.location = result.location;
|
||||
|
||||
tmp.push(record);
|
||||
|
||||
@@ -317,20 +318,19 @@ function delToken(clientId, tokenId, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function addDefaultClients(origin, callback) {
|
||||
assert.strictEqual(typeof origin, 'string');
|
||||
function addDefaultClients(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('Adding default clients');
|
||||
|
||||
// The domain might have changed, therefor we have to update the record
|
||||
// !!! This needs to be in sync with the webadmin, specifically login_callback.js
|
||||
const ADMIN_SCOPES = 'cloudron,developer,profile,users,apps,settings';
|
||||
const ADMIN_SCOPES="cloudron,developer,profile,users,apps,settings";
|
||||
|
||||
// id, appId, type, clientSecret, redirectURI, scope
|
||||
async.series([
|
||||
clientdb.upsert.bind(null, 'cid-webadmin', 'Settings', 'built-in', 'secret-webadmin', origin, ADMIN_SCOPES),
|
||||
clientdb.upsert.bind(null, 'cid-sdk', 'SDK', 'built-in', 'secret-sdk', origin, '*,roleSdk'),
|
||||
clientdb.upsert.bind(null, 'cid-cli', 'Cloudron Tool', 'built-in', 'secret-cli', origin, '*, roleSdk')
|
||||
clientdb.upsert.bind(null, 'cid-webadmin', 'Settings', 'built-in', 'secret-webadmin', config.adminOrigin(), ADMIN_SCOPES),
|
||||
clientdb.upsert.bind(null, 'cid-sdk', 'SDK', 'built-in', 'secret-sdk', config.adminOrigin(), '*,roleSdk'),
|
||||
clientdb.upsert.bind(null, 'cid-cli', 'Cloudron Tool', 'built-in', 'secret-cli', config.adminOrigin(), '*, roleSdk')
|
||||
], callback);
|
||||
}
|
||||
|
||||
+603
-53
@@ -5,50 +5,90 @@ exports = module.exports = {
|
||||
|
||||
initialize: initialize,
|
||||
uninitialize: uninitialize,
|
||||
activate: activate,
|
||||
getConfig: getConfig,
|
||||
getStatus: getStatus,
|
||||
getDisks: getDisks,
|
||||
dnsSetup: dnsSetup,
|
||||
getLogs: getLogs,
|
||||
|
||||
sendCaasHeartbeat: sendCaasHeartbeat,
|
||||
|
||||
updateToLatest: updateToLatest,
|
||||
reboot: reboot,
|
||||
retire: retire,
|
||||
migrate: migrate,
|
||||
|
||||
onActivated: onActivated,
|
||||
checkDiskSpace: checkDiskSpace,
|
||||
|
||||
checkDiskSpace: checkDiskSpace
|
||||
readDkimPublicKeySync: readDkimPublicKeySync,
|
||||
refreshDNS: refreshDNS,
|
||||
configureWebadmin: configureWebadmin
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
var appdb = require('./appdb.js'),
|
||||
apps = require('./apps.js'),
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
backups = require('./backups.js'),
|
||||
caas = require('./caas.js'),
|
||||
certificates = require('./certificates.js'),
|
||||
child_process = require('child_process'),
|
||||
clients = require('./clients.js'),
|
||||
config = require('./config.js'),
|
||||
constants = require('./constants.js'),
|
||||
cron = require('./cron.js'),
|
||||
debug = require('debug')('box:cloudron'),
|
||||
df = require('@sindresorhus/df'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
fs = require('fs'),
|
||||
locker = require('./locker.js'),
|
||||
mailer = require('./mailer.js'),
|
||||
nginx = require('./nginx.js'),
|
||||
os = require('os'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
platform = require('./platform.js'),
|
||||
progress = require('./progress.js'),
|
||||
reverseProxy = require('./reverseproxy.js'),
|
||||
safe = require('safetydance'),
|
||||
settings = require('./settings.js'),
|
||||
SettingsError = settings.SettingsError,
|
||||
settingsdb = require('./settingsdb.js'),
|
||||
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'),
|
||||
tokendb = require('./tokendb.js'),
|
||||
updateChecker = require('./updatechecker.js'),
|
||||
user = require('./user.js'),
|
||||
UserError = user.UserError,
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
|
||||
var REBOOT_CMD = path.join(__dirname, 'scripts/reboot.sh'),
|
||||
UPDATE_CMD = path.join(__dirname, 'scripts/update.sh');
|
||||
UPDATE_CMD = path.join(__dirname, 'scripts/update.sh'),
|
||||
RETIRE_CMD = path.join(__dirname, 'scripts/retire.sh');
|
||||
|
||||
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
|
||||
// result to not depend on the appstore
|
||||
const BOX_AND_USER_TEMPLATE = {
|
||||
box: {
|
||||
region: null,
|
||||
size: null,
|
||||
plan: 'Custom Plan'
|
||||
},
|
||||
user: {
|
||||
billing: false,
|
||||
currency: ''
|
||||
}
|
||||
};
|
||||
|
||||
var gBoxAndUserDetails = null, // cached cloudron details like region,size...
|
||||
gWebadminStatus = { dns: false, tls: false, configuring: false };
|
||||
|
||||
function CloudronError(reason, errorOrMessage) {
|
||||
assert.strictEqual(typeof reason, 'string');
|
||||
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
|
||||
@@ -71,6 +111,8 @@ util.inherits(CloudronError, Error);
|
||||
CloudronError.BAD_FIELD = 'Field error';
|
||||
CloudronError.INTERNAL_ERROR = 'Internal Error';
|
||||
CloudronError.EXTERNAL_ERROR = 'External Error';
|
||||
CloudronError.ALREADY_PROVISIONED = 'Already Provisioned';
|
||||
CloudronError.ALREADY_SETUP = 'Already Setup';
|
||||
CloudronError.BAD_STATE = 'Bad state';
|
||||
CloudronError.ALREADY_UPTODATE = 'No Update Available';
|
||||
CloudronError.NOT_FOUND = 'Not found';
|
||||
@@ -79,12 +121,21 @@ CloudronError.SELF_UPGRADE_NOT_SUPPORTED = 'Self upgrade not supported';
|
||||
function initialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
gWebadminStatus = { dns: false, tls: false, configuring: false };
|
||||
gBoxAndUserDetails = null;
|
||||
|
||||
async.series([
|
||||
certificates.initialize,
|
||||
settings.initialize,
|
||||
reverseProxy.configureDefaultServer,
|
||||
cron.initialize, // required for caas heartbeat before activation
|
||||
onActivated
|
||||
], callback);
|
||||
configureDefaultServer,
|
||||
onDomainConfigured
|
||||
], function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
configureWebadmin(NOOP_CALLBACK); // for restore() and caas initial setup. do not block
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
function uninitialize(callback) {
|
||||
@@ -92,22 +143,217 @@ function uninitialize(callback) {
|
||||
|
||||
async.series([
|
||||
cron.uninitialize,
|
||||
mailer.stop,
|
||||
platform.stop,
|
||||
certificates.uninitialize,
|
||||
settings.uninitialize
|
||||
], callback);
|
||||
}
|
||||
|
||||
function onActivated(callback) {
|
||||
function onDomainConfigured(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)
|
||||
if (!config.fqdn()) return callback();
|
||||
|
||||
async.series([
|
||||
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
|
||||
], callback);
|
||||
}
|
||||
|
||||
function dnsSetup(dnsConfig, domain, zoneName, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (config.fqdn()) return callback(new CloudronError(CloudronError.ALREADY_SETUP));
|
||||
|
||||
if (!zoneName) zoneName = tld.getDomain(domain) || '';
|
||||
|
||||
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));
|
||||
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);
|
||||
|
||||
// upsert the temporary domain record in settings db
|
||||
// This can be removed after this release
|
||||
settingsdb.set('domain', JSON.stringify({ fqdn: domain, zoneName: zoneName }), function (error) {
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
|
||||
async.series([ // do not block
|
||||
onDomainConfigured,
|
||||
configureWebadmin
|
||||
], NOOP_CALLBACK);
|
||||
|
||||
callback();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function configureDefaultServer(callback) {
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
|
||||
debug('configureDefaultServer: domain %s', config.fqdn());
|
||||
|
||||
if (process.env.BOX_ENV === 'test') return callback();
|
||||
|
||||
var certFilePath = path.join(paths.NGINX_CERT_DIR, 'default.cert');
|
||||
var keyFilePath = path.join(paths.NGINX_CERT_DIR, 'default.key');
|
||||
|
||||
if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) {
|
||||
debug('configureDefaultServer: create new cert');
|
||||
|
||||
var cn = 'cloudron-' + (new Date()).toISOString(); // randomize date a bit to keep firefox happy
|
||||
var certCommand = util.format('openssl req -x509 -newkey rsa:2048 -keyout %s -out %s -days 3650 -subj /CN=%s -nodes', keyFilePath, certFilePath, cn);
|
||||
safe.child_process.execSync(certCommand);
|
||||
}
|
||||
|
||||
nginx.configureAdmin(certFilePath, keyFilePath, 'default.conf', '', function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('configureDefaultServer: done');
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function configureWebadmin(callback) {
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
|
||||
debug('configureWebadmin: fqdn:%s status:%j', config.fqdn(), gWebadminStatus);
|
||||
|
||||
if (process.env.BOX_ENV === 'test' || !config.fqdn() || gWebadminStatus.configuring) return callback();
|
||||
|
||||
gWebadminStatus.configuring = true; // re-entracy guard
|
||||
|
||||
function done(error) {
|
||||
gWebadminStatus.configuring = false;
|
||||
debug('configureWebadmin: done error:%j', error);
|
||||
callback(error);
|
||||
}
|
||||
|
||||
function configureNginx(error) {
|
||||
debug('configureNginx: dns update:%j', error);
|
||||
|
||||
certificates.ensureCertificate({ location: config.adminLocation() }, function (error, certFilePath, keyFilePath) {
|
||||
if (error) return done(error);
|
||||
|
||||
gWebadminStatus.tls = true;
|
||||
|
||||
nginx.configureAdmin(certFilePath, keyFilePath, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn(), done);
|
||||
});
|
||||
}
|
||||
|
||||
// update the DNS. configure nginx regardless of whether it succeeded so that
|
||||
// box is accessible even if dns creds are invalid
|
||||
sysinfo.getPublicIp(function (error, ip) {
|
||||
if (error) return configureNginx(error);
|
||||
|
||||
addDnsRecords(ip, function (error) {
|
||||
if (error) return configureNginx(error);
|
||||
|
||||
subdomains.waitForDns(config.adminFqdn(), ip, 'A', { interval: 30000, times: 50000 }, function (error) {
|
||||
if (error) return configureNginx(error);
|
||||
|
||||
gWebadminStatus.dns = true;
|
||||
|
||||
configureNginx();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setTimeZone(ip, callback) {
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('setTimeZone ip:%s', ip);
|
||||
|
||||
superagent.get('https://geolocation.cloudron.io/json').query({ ip: ip }).timeout(10 * 1000).end(function (error, result) {
|
||||
if ((error && !error.response) || result.statusCode !== 200) {
|
||||
debug('Failed to get geo location: %s', error.message);
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
var timezone = safe.query(result.body, 'location.time_zone');
|
||||
|
||||
if (!timezone || typeof timezone !== 'string') {
|
||||
debug('No timezone in geoip response : %j', result.body);
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
debug('Setting timezone to ', timezone);
|
||||
|
||||
settings.setTimeZone(timezone, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function activate(username, password, email, displayName, ip, auditSource, callback) {
|
||||
assert.strictEqual(typeof username, 'string');
|
||||
assert.strictEqual(typeof password, 'string');
|
||||
assert.strictEqual(typeof email, 'string');
|
||||
assert.strictEqual(typeof displayName, 'string');
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('activating user:%s email:%s', username, email);
|
||||
|
||||
setTimeZone(ip, function () { }); // TODO: get this from user. note that timezone is detected based on the browser location and not the cloudron region
|
||||
|
||||
user.createOwner(username, password, email, displayName, auditSource, function (error, userObject) {
|
||||
if (error && error.reason === UserError.ALREADY_EXISTS) return callback(new CloudronError(CloudronError.ALREADY_PROVISIONED));
|
||||
if (error && error.reason === UserError.BAD_FIELD) return callback(new CloudronError(CloudronError.BAD_FIELD, error.message));
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
|
||||
clients.get('cid-webadmin', function (error, result) {
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
|
||||
// Also generate a token so the admin creation can also act as a login
|
||||
var token = tokendb.generateToken();
|
||||
var expires = Date.now() + constants.DEFAULT_TOKEN_EXPIRATION;
|
||||
|
||||
tokendb.add(token, userObject.id, result.id, expires, '*', function (error) {
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
|
||||
eventlog.add(eventlog.ACTION_ACTIVATE, auditSource, { });
|
||||
|
||||
platform.createMailConfig(NOOP_CALLBACK); // bounces can now be sent to the cloudron owner
|
||||
|
||||
callback(null, { token: token, expires: expires });
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getStatus(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
user.count(function (error, count) {
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
if (!count) return callback(); // not activated
|
||||
|
||||
platform.start(callback);
|
||||
settings.getCloudronName(function (error, cloudronName) {
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, {
|
||||
activated: count !== 0,
|
||||
version: config.version(),
|
||||
apiServerOrigin: config.apiServerOrigin(), // used by CaaS tool
|
||||
provider: config.provider(),
|
||||
cloudronName: cloudronName,
|
||||
adminFqdn: config.fqdn() ? config.adminFqdn() : null,
|
||||
webadminStatus: gWebadminStatus
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -137,23 +383,32 @@ function getDisks(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getBoxAndUserDetails(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (gBoxAndUserDetails) return callback(null, gBoxAndUserDetails);
|
||||
|
||||
// only supported for caas
|
||||
if (config.provider() !== 'caas') return callback(null, {});
|
||||
|
||||
superagent
|
||||
.get(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn())
|
||||
.query({ token: config.token() })
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new CloudronError(CloudronError.EXTERNAL_ERROR, 'Cannot reach appstore'));
|
||||
if (result.statusCode !== 200) return callback(new CloudronError(CloudronError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
|
||||
|
||||
gBoxAndUserDetails = result.body;
|
||||
|
||||
return callback(null, gBoxAndUserDetails);
|
||||
});
|
||||
}
|
||||
|
||||
function getConfig(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// result to not depend on the appstore
|
||||
const BOX_AND_USER_TEMPLATE = {
|
||||
box: {
|
||||
region: null,
|
||||
size: null,
|
||||
plan: 'Custom Plan'
|
||||
},
|
||||
user: {
|
||||
billing: false,
|
||||
currency: ''
|
||||
}
|
||||
};
|
||||
|
||||
caas.getBoxAndUserDetails(function (error, result) {
|
||||
getBoxAndUserDetails(function (error, result) {
|
||||
if (error) debug('Failed to fetch cloudron details.', error.reason, error.message);
|
||||
|
||||
result = _.extend(BOX_AND_USER_TEMPLATE, result || {});
|
||||
@@ -161,30 +416,181 @@ function getConfig(callback) {
|
||||
settings.getCloudronName(function (error, cloudronName) {
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, {
|
||||
apiServerOrigin: config.apiServerOrigin(),
|
||||
webServerOrigin: config.webServerOrigin(),
|
||||
adminDomain: config.adminDomain(),
|
||||
adminLocation: config.adminLocation(),
|
||||
adminFqdn: config.adminFqdn(),
|
||||
mailFqdn: config.mailFqdn(),
|
||||
version: config.version(),
|
||||
update: updateChecker.getUpdateInfo(),
|
||||
progress: progress.getAll(),
|
||||
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
|
||||
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
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function sendCaasHeartbeat() {
|
||||
assert(config.provider() === 'caas', 'Heartbeat is only sent for managed cloudrons');
|
||||
|
||||
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/heartbeat';
|
||||
superagent.post(url).query({ token: config.token(), version: config.version() }).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) debug('Network error sending heartbeat.', error);
|
||||
else if (result.statusCode !== 200) debug('Server responded to heartbeat with %s %s', result.statusCode, result.text);
|
||||
else debug('Heartbeat sent to %s', url);
|
||||
});
|
||||
}
|
||||
|
||||
function ensureDkimKey(callback) {
|
||||
assert(config.fqdn(), 'fqdn is not set');
|
||||
|
||||
var dkimPath = path.join(paths.MAIL_DATA_DIR, 'dkim/' + config.fqdn());
|
||||
var dkimPrivateKeyFile = path.join(dkimPath, 'private');
|
||||
var dkimPublicKeyFile = path.join(dkimPath, 'public');
|
||||
|
||||
if (!fs.existsSync(dkimPrivateKeyFile) || !fs.existsSync(dkimPublicKeyFile)) {
|
||||
debug('Generating new DKIM keys');
|
||||
|
||||
if (!safe.fs.mkdirSync(dkimPath) && safe.error.code !== 'EEXIST') {
|
||||
debug('Error creating dkim.', safe.error);
|
||||
return null;
|
||||
}
|
||||
|
||||
child_process.execSync('openssl genrsa -out ' + dkimPrivateKeyFile + ' 1024');
|
||||
child_process.execSync('openssl rsa -in ' + dkimPrivateKeyFile + ' -out ' + dkimPublicKeyFile + ' -pubout -outform PEM');
|
||||
} else {
|
||||
debug('DKIM keys already present');
|
||||
}
|
||||
|
||||
callback();
|
||||
}
|
||||
|
||||
function readDkimPublicKeySync() {
|
||||
if (!config.fqdn()) {
|
||||
debug('Cannot read dkim public key without a domain.', safe.error);
|
||||
return null;
|
||||
}
|
||||
|
||||
var dkimPath = path.join(paths.MAIL_DATA_DIR, 'dkim/' + config.fqdn());
|
||||
var dkimPublicKeyFile = path.join(dkimPath, 'public');
|
||||
|
||||
var publicKey = safe.fs.readFileSync(dkimPublicKeyFile, 'utf8');
|
||||
|
||||
if (publicKey === null) {
|
||||
debug('Error reading dkim public key.', safe.error);
|
||||
return null;
|
||||
}
|
||||
|
||||
// remove header, footer and new lines
|
||||
publicKey = publicKey.split('\n').slice(1, -2).join('');
|
||||
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
// NOTE: if you change the SPF record here, be sure the wait check in mailer.js
|
||||
// https://agari.zendesk.com/hc/en-us/articles/202952749-How-long-can-my-SPF-record-be-
|
||||
function txtRecordsWithSpf(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
subdomains.get('', 'TXT', function (error, txtRecords) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('txtRecordsWithSpf: current txt records - %j', txtRecords);
|
||||
|
||||
var i, matches, validSpf;
|
||||
|
||||
for (i = 0; i < txtRecords.length; i++) {
|
||||
matches = txtRecords[i].match(/^("?v=spf1) /); // DO backend may return without quotes
|
||||
if (matches === null) continue;
|
||||
|
||||
// this won't work if the entry is arbitrarily "split" across quoted strings
|
||||
validSpf = txtRecords[i].indexOf('a:' + config.adminFqdn()) !== -1;
|
||||
break; // there can only be one SPF record
|
||||
}
|
||||
|
||||
if (validSpf) return callback(null, null);
|
||||
|
||||
if (!matches) { // no spf record was found, create one
|
||||
txtRecords.push('"v=spf1 a:' + config.adminFqdn() + ' ~all"');
|
||||
debug('txtRecordsWithSpf: adding txt record');
|
||||
} else { // just add ourself
|
||||
txtRecords[i] = matches[1] + ' a:' + config.adminFqdn() + txtRecords[i].slice(matches[1].length);
|
||||
debug('txtRecordsWithSpf: inserting txt record');
|
||||
}
|
||||
|
||||
return callback(null, txtRecords);
|
||||
});
|
||||
}
|
||||
|
||||
function addDnsRecords(ip, callback) {
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
|
||||
if (process.env.BOX_ENV === 'test') return callback();
|
||||
|
||||
var dkimKey = readDkimPublicKeySync();
|
||||
if (!dkimKey) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, new Error('Failed to read dkim public key')));
|
||||
|
||||
var webadminRecord = { subdomain: config.adminLocation(), 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 records = [ ];
|
||||
if (config.isCustomDomain()) {
|
||||
records.push(webadminRecord);
|
||||
records.push(dkimRecord);
|
||||
} else {
|
||||
// for non-custom domains, we show a noapp.html page
|
||||
var nakedDomainRecord = { subdomain: '', type: 'A', values: [ ip ] };
|
||||
|
||||
records.push(nakedDomainRecord);
|
||||
records.push(webadminRecord);
|
||||
records.push(dkimRecord);
|
||||
}
|
||||
|
||||
debug('addDnsRecords: %j', records);
|
||||
|
||||
async.retry({ times: 10, interval: 20000 }, function (retryCallback) {
|
||||
txtRecordsWithSpf(function (error, txtRecords) {
|
||||
if (error) return retryCallback(error);
|
||||
|
||||
if (txtRecords) records.push({ subdomain: '', 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);
|
||||
}, function (error, changeIds) {
|
||||
if (error) debug('addDnsRecords: failed to update : %s. will retry', error);
|
||||
else debug('addDnsRecords: records %j added with changeIds %j', records, changeIds);
|
||||
|
||||
retryCallback(error);
|
||||
});
|
||||
});
|
||||
}, function (error) {
|
||||
if (error) debug('addDnsRecords: done updating records with error:', error);
|
||||
else debug('addDnsRecords: done');
|
||||
|
||||
callback(error);
|
||||
});
|
||||
}
|
||||
|
||||
function reboot(callback) {
|
||||
shell.sudo('reboot', [ REBOOT_CMD ], callback);
|
||||
}
|
||||
@@ -207,7 +613,7 @@ function update(boxUpdateInfo, auditSource, callback) {
|
||||
// initiate the update/upgrade but do not wait for it
|
||||
if (boxUpdateInfo.upgrade) {
|
||||
debug('Starting upgrade');
|
||||
caas.upgrade(boxUpdateInfo, function (error) {
|
||||
doUpgrade(boxUpdateInfo, function (error) {
|
||||
if (error) {
|
||||
debug('Upgrade failed with error:', error);
|
||||
locker.unlock(locker.OP_BOX_UPDATE);
|
||||
@@ -226,6 +632,7 @@ function update(boxUpdateInfo, auditSource, callback) {
|
||||
callback(null);
|
||||
}
|
||||
|
||||
|
||||
function updateToLatest(auditSource, callback) {
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -258,6 +665,36 @@ function doShortCircuitUpdate(boxUpdateInfo, callback) {
|
||||
callback();
|
||||
}
|
||||
|
||||
function doUpgrade(boxUpdateInfo, callback) {
|
||||
assert(boxUpdateInfo !== null && typeof boxUpdateInfo === 'object');
|
||||
|
||||
function upgradeError(e) {
|
||||
progress.set(progress.UPDATE, -1, e.message);
|
||||
callback(e);
|
||||
}
|
||||
|
||||
progress.set(progress.UPDATE, 5, 'Backing up for upgrade');
|
||||
|
||||
backups.backupBoxAndApps({ userId: null, username: 'upgrader' }, function (error) {
|
||||
if (error) return upgradeError(error);
|
||||
|
||||
superagent.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/upgrade')
|
||||
.query({ token: config.token() })
|
||||
.send({ version: boxUpdateInfo.version })
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return upgradeError(new Error('Network error making upgrade request: ' + error));
|
||||
if (result.statusCode !== 202) return upgradeError(new Error(util.format('Server not ready to upgrade. statusCode: %s body: %j', result.status, result.body)));
|
||||
|
||||
progress.set(progress.UPDATE, 10, 'Updating base system');
|
||||
|
||||
// no need to unlock since this is the last thing we ever do on this box
|
||||
callback();
|
||||
retire('upgrade');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function doUpdate(boxUpdateInfo, callback) {
|
||||
assert(boxUpdateInfo && typeof boxUpdateInfo === 'object');
|
||||
|
||||
@@ -274,17 +711,23 @@ function doUpdate(boxUpdateInfo, callback) {
|
||||
// NOTE: this data is opaque and will be passed through the installer.sh
|
||||
var data= {
|
||||
provider: config.provider(),
|
||||
token: config.token(),
|
||||
apiServerOrigin: config.apiServerOrigin(),
|
||||
webServerOrigin: config.webServerOrigin(),
|
||||
adminDomain: config.adminDomain(),
|
||||
adminFqdn: config.adminFqdn(),
|
||||
fqdn: config.fqdn(),
|
||||
adminLocation: config.adminLocation(),
|
||||
tlsCert: config.tlsCert(),
|
||||
tlsKey: config.tlsKey(),
|
||||
isCustomDomain: config.isCustomDomain(),
|
||||
isDemo: config.isDemo(),
|
||||
zoneName: config.zoneName(),
|
||||
|
||||
appstore: {
|
||||
token: config.token(),
|
||||
apiServerOrigin: config.apiServerOrigin()
|
||||
},
|
||||
caas: {
|
||||
token: config.token(),
|
||||
apiServerOrigin: config.apiServerOrigin(),
|
||||
webServerOrigin: config.webServerOrigin()
|
||||
},
|
||||
@@ -294,7 +737,7 @@ function doUpdate(boxUpdateInfo, callback) {
|
||||
|
||||
debug('updating box %s %j', boxUpdateInfo.sourceTarballUrl, _.omit(data, 'tlsCert', 'tlsKey', 'token', 'appstore', 'caas'));
|
||||
|
||||
progress.set(progress.UPDATE, 5, 'Downloading and installing new version');
|
||||
progress.set(progress.UPDATE, 5, 'Downloading and extracting new version');
|
||||
|
||||
shell.sudo('update', [ UPDATE_CMD, boxUpdateInfo.sourceTarballUrl, JSON.stringify(data) ], function (error) {
|
||||
if (error) return updateError(error);
|
||||
@@ -346,6 +789,113 @@ function checkDiskSpace(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function retire(reason, info, callback) {
|
||||
assert(reason === 'migrate' || reason === 'upgrade');
|
||||
info = info || { };
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
|
||||
var data = {
|
||||
apiServerOrigin: config.apiServerOrigin(),
|
||||
isCustomDomain: config.isCustomDomain(),
|
||||
fqdn: config.fqdn()
|
||||
};
|
||||
shell.sudo('retire', [ RETIRE_CMD, reason, JSON.stringify(info), JSON.stringify(data) ], callback);
|
||||
}
|
||||
|
||||
function doMigrate(options, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var error = locker.lock(locker.OP_MIGRATE);
|
||||
if (error) return callback(new CloudronError(CloudronError.BAD_STATE, error.message));
|
||||
|
||||
function unlock(error) {
|
||||
debug('Failed to migrate', error);
|
||||
locker.unlock(locker.OP_MIGRATE);
|
||||
progress.set(progress.MIGRATE, -1, 'Backup failed: ' + error.message);
|
||||
}
|
||||
|
||||
progress.set(progress.MIGRATE, 10, 'Backing up for migration');
|
||||
|
||||
// initiate the migration in the background
|
||||
backups.backupBoxAndApps({ userId: null, username: 'migrator' }, function (error) {
|
||||
if (error) return unlock(error);
|
||||
|
||||
debug('migrate: domain: %s size %s region %s', options.domain, options.size, options.region);
|
||||
|
||||
superagent
|
||||
.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/migrate')
|
||||
.query({ token: config.token() })
|
||||
.send(options)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return unlock(error); // network error
|
||||
if (result.statusCode === 409) return unlock(new CloudronError(CloudronError.BAD_STATE));
|
||||
if (result.statusCode === 404) return unlock(new CloudronError(CloudronError.NOT_FOUND));
|
||||
if (result.statusCode !== 202) return unlock(new CloudronError(CloudronError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
|
||||
|
||||
progress.set(progress.MIGRATE, 10, 'Migrating');
|
||||
|
||||
retire('migrate', _.pick(options, 'domain', 'size', 'region'));
|
||||
});
|
||||
});
|
||||
|
||||
callback(null);
|
||||
}
|
||||
|
||||
function migrate(options, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (config.isDemo()) return callback(new CloudronError(CloudronError.BAD_FIELD, 'Not allowed in demo mode'));
|
||||
|
||||
if (!options.domain) return doMigrate(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));
|
||||
|
||||
// TODO: should probably rollback dns config if migrate fails
|
||||
doMigrate(options, callback);
|
||||
});
|
||||
}
|
||||
|
||||
// called for dynamic dns setups where we have to update the IP
|
||||
function refreshDNS(callback) {
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
|
||||
sysinfo.getPublicIp(function (error, ip) {
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
|
||||
debug('refreshDNS: current ip %s', ip);
|
||||
|
||||
addDnsRecords(ip, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('refreshDNS: done for system records');
|
||||
|
||||
apps.getAll(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.each(result, function (app, callback) {
|
||||
// do not change state of installing apps since apptask will error if dns record already exists
|
||||
if (app.installationState !== appdb.ISTATE_INSTALLED) return callback();
|
||||
|
||||
subdomains.upsert(app.location, 'A', [ ip ], callback);
|
||||
}, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('refreshDNS: done for apps');
|
||||
|
||||
callback();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getLogs(options, callback) {
|
||||
assert(options && typeof options === 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
+68
-31
@@ -16,13 +16,13 @@ exports = module.exports = {
|
||||
provider: provider,
|
||||
apiServerOrigin: apiServerOrigin,
|
||||
webServerOrigin: webServerOrigin,
|
||||
adminDomain: adminDomain,
|
||||
setFqdn: setAdminDomain,
|
||||
setAdminDomain: setAdminDomain,
|
||||
setAdminFqdn: setAdminFqdn,
|
||||
setAdminLocation: setAdminLocation,
|
||||
fqdn: fqdn,
|
||||
zoneName: zoneName,
|
||||
setFqdn: setFqdn,
|
||||
token: token,
|
||||
version: version,
|
||||
setVersion: setVersion,
|
||||
isCustomDomain: isCustomDomain,
|
||||
database: database,
|
||||
|
||||
// these values are derived
|
||||
@@ -33,19 +33,26 @@ exports = module.exports = {
|
||||
adminFqdn: adminFqdn,
|
||||
mailLocation: mailLocation,
|
||||
mailFqdn: mailFqdn,
|
||||
appFqdn: appFqdn,
|
||||
setZoneName: setZoneName,
|
||||
hasIPv6: hasIPv6,
|
||||
dkimSelector: dkimSelector,
|
||||
|
||||
isDemo: isDemo,
|
||||
|
||||
tlsCert: tlsCert,
|
||||
tlsKey: tlsKey,
|
||||
|
||||
// for testing resets to defaults
|
||||
_reset: _reset
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
debug = require('debug')('box:config.js'),
|
||||
fs = require('fs'),
|
||||
path = require('path'),
|
||||
safe = require('safetydance'),
|
||||
tld = require('tldjs'),
|
||||
_ = require('underscore');
|
||||
|
||||
|
||||
@@ -70,11 +77,13 @@ function saveSync() {
|
||||
// only save values we want to have in the cloudron.conf, see start.sh
|
||||
var conf = {
|
||||
version: data.version,
|
||||
token: data.token,
|
||||
apiServerOrigin: data.apiServerOrigin,
|
||||
webServerOrigin: data.webServerOrigin,
|
||||
adminDomain: data.adminDomain,
|
||||
adminFqdn: data.adminFqdn,
|
||||
fqdn: data.fqdn,
|
||||
zoneName: data.zoneName,
|
||||
adminLocation: data.adminLocation,
|
||||
isCustomDomain: data.isCustomDomain,
|
||||
provider: data.provider,
|
||||
isDemo: data.isDemo
|
||||
};
|
||||
@@ -92,14 +101,16 @@ function _reset(callback) {
|
||||
|
||||
function initConfig() {
|
||||
// setup defaults
|
||||
data.adminFqdn = '';
|
||||
data.adminDomain = '';
|
||||
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.provider = 'generic';
|
||||
data.provider = 'caas';
|
||||
data.smtpPort = 2525; // this value comes from mail container
|
||||
data.sysadminPort = 3001;
|
||||
data.ldapPort = 3002;
|
||||
@@ -115,8 +126,8 @@ function initConfig() {
|
||||
|
||||
// overrides for local testings
|
||||
if (exports.TEST) {
|
||||
data.version = '1.1.1-test';
|
||||
data.port = 5454;
|
||||
data.token = 'APPSTORE_TOKEN';
|
||||
data.apiServerOrigin = 'http://localhost:6060'; // hock doesn't support https
|
||||
data.database.password = '';
|
||||
data.database.name = 'boxtest';
|
||||
@@ -140,7 +151,6 @@ function set(key, value) {
|
||||
} else {
|
||||
data = safe.set(data, key, value);
|
||||
}
|
||||
|
||||
saveSync();
|
||||
}
|
||||
|
||||
@@ -158,41 +168,52 @@ function webServerOrigin() {
|
||||
return get('webServerOrigin');
|
||||
}
|
||||
|
||||
function setAdminDomain(domain) {
|
||||
set('adminDomain', domain);
|
||||
function setFqdn(fqdn) {
|
||||
set('fqdn', fqdn);
|
||||
}
|
||||
|
||||
function adminDomain() {
|
||||
return get('adminDomain');
|
||||
function fqdn() {
|
||||
return get('fqdn');
|
||||
}
|
||||
|
||||
function setZoneName(zone) {
|
||||
set('zoneName', zone);
|
||||
}
|
||||
|
||||
function zoneName() {
|
||||
var zone = get('zoneName');
|
||||
if (zone) return zone;
|
||||
|
||||
// TODO: move this to migration code path instead
|
||||
return tld.getDomain(fqdn()) || '';
|
||||
}
|
||||
|
||||
// keep this in sync with start.sh admin.conf generation code
|
||||
function appFqdn(location) {
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
|
||||
if (location === '') return fqdn();
|
||||
return isCustomDomain() ? location + '.' + fqdn() : location + '-' + fqdn();
|
||||
}
|
||||
|
||||
function mailLocation() {
|
||||
return get('adminLocation'); // not a typo! should be same as admin location until we figure out certificates
|
||||
}
|
||||
|
||||
function setAdminLocation(location) {
|
||||
set('adminLocation', location);
|
||||
function mailFqdn() {
|
||||
return appFqdn(mailLocation());
|
||||
}
|
||||
|
||||
|
||||
function adminLocation() {
|
||||
return get('adminLocation');
|
||||
}
|
||||
|
||||
function setAdminFqdn(adminFqdn) {
|
||||
set('adminFqdn', adminFqdn);
|
||||
}
|
||||
|
||||
function adminFqdn() {
|
||||
return get('adminFqdn');
|
||||
}
|
||||
|
||||
function mailFqdn() {
|
||||
return adminFqdn();
|
||||
return appFqdn(adminLocation());
|
||||
}
|
||||
|
||||
function adminOrigin() {
|
||||
return 'https://' + adminFqdn();
|
||||
return 'https://' + appFqdn(adminLocation());
|
||||
}
|
||||
|
||||
function internalAdminOrigin() {
|
||||
@@ -203,6 +224,10 @@ function sysadminOrigin() {
|
||||
return 'http://127.0.0.1:' + get('sysadminPort');
|
||||
}
|
||||
|
||||
function token() {
|
||||
return get('token');
|
||||
}
|
||||
|
||||
function version() {
|
||||
return get('version');
|
||||
}
|
||||
@@ -211,6 +236,10 @@ function setVersion(version) {
|
||||
set('version', version);
|
||||
}
|
||||
|
||||
function isCustomDomain() {
|
||||
return get('isCustomDomain');
|
||||
}
|
||||
|
||||
function database() {
|
||||
return get('database');
|
||||
}
|
||||
@@ -223,13 +252,21 @@ 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);
|
||||
}
|
||||
|
||||
// it has to change with the adminLocation so that multiple cloudrons
|
||||
// can send out emails at the same time.
|
||||
function dkimSelector() {
|
||||
var loc = adminLocation();
|
||||
return loc === 'my' ? 'cloudron' : `cloudron-${loc.replace(/\./g, '')}`;
|
||||
|
||||
+3
-4
@@ -4,14 +4,13 @@ exports = module.exports = {
|
||||
API_LOCATION: 'api', // this is unused but reserved for future use (#403)
|
||||
SMTP_LOCATION: 'smtp',
|
||||
IMAP_LOCATION: 'imap',
|
||||
POSTMAN_LOCATION: 'postman', // used in dovecot bounces
|
||||
|
||||
// These are combined into one array because users and groups become mailboxes
|
||||
RESERVED_NAMES: [
|
||||
// Reserved usernames
|
||||
// https://github.com/gogits/gogs/blob/52c8f691630548fe091d30bcfe8164545a05d3d5/models/repo.go#L393
|
||||
// apps like wordpress, gogs don't like these
|
||||
// postmaster is used in dovecot and haraka
|
||||
'admin', 'no-reply', 'postmaster', 'mailer-daemon',
|
||||
'admin', 'no-reply', 'postmaster', 'mailer-daemon', // apps like wordpress, gogs don't like these
|
||||
|
||||
// Reserved groups
|
||||
'admins', 'users' // ldap code uses 'users' pseudo group
|
||||
@@ -20,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',
|
||||
|
||||
+118
-107
@@ -9,39 +9,35 @@ var apps = require('./apps.js'),
|
||||
appstore = require('./appstore.js'),
|
||||
assert = require('assert'),
|
||||
backups = require('./backups.js'),
|
||||
caas = require('./caas.js'),
|
||||
certificates = require('./certificates.js'),
|
||||
cloudron = require('./cloudron.js'),
|
||||
config = require('./config.js'),
|
||||
constants = require('./constants.js'),
|
||||
CronJob = require('cron').CronJob,
|
||||
debug = require('debug')('box:cron'),
|
||||
digest = require('./digest.js'),
|
||||
dyndns = require('./dyndns.js'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
janitor = require('./janitor.js'),
|
||||
reverseProxy = require('./reverseproxy.js'),
|
||||
scheduler = require('./scheduler.js'),
|
||||
settings = require('./settings.js'),
|
||||
semver = require('semver'),
|
||||
updateChecker = require('./updatechecker.js');
|
||||
|
||||
var gJobs = {
|
||||
alive: null, // send periodic stats
|
||||
appAutoUpdater: null,
|
||||
boxAutoUpdater: null,
|
||||
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 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 NOOP_CALLBACK = function (error) { if (error) console.error(error); };
|
||||
var AUDIT_SOURCE = { userId: null, username: 'cron' };
|
||||
@@ -58,37 +54,37 @@ 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
|
||||
var seconds = (new Date()).getSeconds() - 1;
|
||||
if (seconds === -1) seconds = 59;
|
||||
|
||||
gJobs.caasHeartbeat = new CronJob({
|
||||
cronTime: `${seconds} */1 * * * *`, // every minute
|
||||
onTick: caas.sendHeartbeat,
|
||||
start: true
|
||||
});
|
||||
setTimeout(function () {
|
||||
if (!gCaasHeartbeatJob) return; // already uninitalized
|
||||
gCaasHeartbeatJob.start();
|
||||
cloudron.sendCaasHeartbeat();
|
||||
}, 1000 * 60);
|
||||
}
|
||||
|
||||
var randomHourMinute = Math.floor(60*Math.random());
|
||||
gJobs.alive = new CronJob({
|
||||
gAliveJob = new CronJob({
|
||||
cronTime: '00 ' + randomHourMinute + ' * * * *', // every hour on a random minute
|
||||
onTick: appstore.sendAliveStatus,
|
||||
start: true
|
||||
});
|
||||
|
||||
settings.events.on(settings.TIME_ZONE_KEY, recreateJobs);
|
||||
settings.events.on(settings.APP_AUTOUPDATE_PATTERN_KEY, appAutoupdatePatternChanged);
|
||||
settings.events.on(settings.BOX_AUTOUPDATE_PATTERN_KEY, boxAutoupdatePatternChanged);
|
||||
settings.events.on(settings.DYNAMIC_DNS_KEY, dynamicDnsChanged);
|
||||
settings.events.on(settings.AUTOUPDATE_PATTERN_KEY, autoupdatePatternChanged);
|
||||
settings.events.on(settings.DYNAMIC_DNS_KEY, dynamicDNSChanged);
|
||||
|
||||
settings.getAll(function (error, allSettings) {
|
||||
if (error) return callback(error);
|
||||
|
||||
recreateJobs(allSettings[settings.TIME_ZONE_KEY]);
|
||||
appAutoupdatePatternChanged(allSettings[settings.APP_AUTOUPDATE_PATTERN_KEY]);
|
||||
boxAutoupdatePatternChanged(allSettings[settings.BOX_AUTOUPDATE_PATTERN_KEY]);
|
||||
dynamicDnsChanged(allSettings[settings.DYNAMIC_DNS_KEY]);
|
||||
autoupdatePatternChanged(allSettings[settings.AUTOUPDATE_PATTERN_KEY]);
|
||||
dynamicDNSChanged(allSettings[settings.DYNAMIC_DNS_KEY]);
|
||||
|
||||
callback();
|
||||
});
|
||||
@@ -99,16 +95,16 @@ function recreateJobs(tz) {
|
||||
|
||||
debug('Creating jobs with timezone %s', tz);
|
||||
|
||||
if (gJobs.backup) gJobs.backup.stop();
|
||||
gJobs.backup = new CronJob({
|
||||
if (gBackupJob) gBackupJob.stop();
|
||||
gBackupJob = 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 (gJobs.checkDiskSpace) gJobs.checkDiskSpace.stop();
|
||||
gJobs.checkDiskSpace = new CronJob({
|
||||
if (gCheckDiskSpaceJob) gCheckDiskSpaceJob.stop();
|
||||
gCheckDiskSpaceJob = new CronJob({
|
||||
cronTime: '00 30 */4 * * *', // every 4 hours
|
||||
onTick: cloudron.checkDiskSpace,
|
||||
start: true,
|
||||
@@ -118,72 +114,72 @@ function recreateJobs(tz) {
|
||||
// randomized pattern per cloudron every hour
|
||||
var randomMinute = Math.floor(60*Math.random());
|
||||
|
||||
if (gJobs.boxUpdateCheckerJob) gJobs.boxUpdateCheckerJob.stop();
|
||||
gJobs.boxUpdateCheckerJob = new CronJob({
|
||||
if (gBoxUpdateCheckerJob) gBoxUpdateCheckerJob.stop();
|
||||
gBoxUpdateCheckerJob = new CronJob({
|
||||
cronTime: '00 ' + randomMinute + ' * * * *', // once an hour
|
||||
onTick: updateChecker.checkBoxUpdates,
|
||||
start: true,
|
||||
timeZone: tz
|
||||
});
|
||||
|
||||
if (gJobs.appUpdateChecker) gJobs.appUpdateChecker.stop();
|
||||
gJobs.appUpdateChecker = new CronJob({
|
||||
if (gAppUpdateCheckerJob) gAppUpdateCheckerJob.stop();
|
||||
gAppUpdateCheckerJob = new CronJob({
|
||||
cronTime: '00 ' + randomMinute + ' * * * *', // once an hour
|
||||
onTick: updateChecker.checkAppUpdates,
|
||||
start: true,
|
||||
timeZone: tz
|
||||
});
|
||||
|
||||
if (gJobs.cleanupTokens) gJobs.cleanupTokens.stop();
|
||||
gJobs.cleanupTokens = new CronJob({
|
||||
if (gCleanupTokensJob) gCleanupTokensJob.stop();
|
||||
gCleanupTokensJob = new CronJob({
|
||||
cronTime: '00 */30 * * * *', // every 30 minutes
|
||||
onTick: janitor.cleanupTokens,
|
||||
start: true,
|
||||
timeZone: tz
|
||||
});
|
||||
|
||||
if (gJobs.cleanupBackups) gJobs.cleanupBackups.stop();
|
||||
gJobs.cleanupBackups = new CronJob({
|
||||
if (gCleanupBackupsJob) gCleanupBackupsJob.stop();
|
||||
gCleanupBackupsJob = 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 (gJobs.cleanupEventlog) gJobs.cleanupEventlog.stop();
|
||||
gJobs.cleanupEventlog = new CronJob({
|
||||
if (gCleanupEventlogJob) gCleanupEventlogJob.stop();
|
||||
gCleanupEventlogJob = new CronJob({
|
||||
cronTime: '00 */30 * * * *', // every 30 minutes
|
||||
onTick: eventlog.cleanup,
|
||||
start: true,
|
||||
timeZone: tz
|
||||
});
|
||||
|
||||
if (gJobs.dockerVolumeCleaner) gJobs.dockerVolumeCleaner.stop();
|
||||
gJobs.dockerVolumeCleaner = new CronJob({
|
||||
if (gDockerVolumeCleanerJob) gDockerVolumeCleanerJob.stop();
|
||||
gDockerVolumeCleanerJob = new CronJob({
|
||||
cronTime: '00 00 */12 * * *', // every 12 hours
|
||||
onTick: janitor.cleanupDockerVolumes,
|
||||
start: true,
|
||||
timeZone: tz
|
||||
});
|
||||
|
||||
if (gJobs.schedulerSync) gJobs.schedulerSync.stop();
|
||||
gJobs.schedulerSync = new CronJob({
|
||||
if (gSchedulerSyncJob) gSchedulerSyncJob.stop();
|
||||
gSchedulerSyncJob = new CronJob({
|
||||
cronTime: config.TEST ? '*/10 * * * * *' : '00 */1 * * * *', // every minute
|
||||
onTick: scheduler.sync,
|
||||
start: true,
|
||||
timeZone: tz
|
||||
});
|
||||
|
||||
if (gJobs.certificateRenew) gJobs.certificateRenew.stop();
|
||||
gJobs.certificateRenew = new CronJob({
|
||||
if (gCertificateRenewJob) gCertificateRenewJob.stop();
|
||||
gCertificateRenewJob = new CronJob({
|
||||
cronTime: '00 00 */12 * * *', // every 12 hours
|
||||
onTick: reverseProxy.renewAll.bind(null, AUDIT_SOURCE, NOOP_CALLBACK),
|
||||
onTick: certificates.renewAll.bind(null, AUDIT_SOURCE, NOOP_CALLBACK),
|
||||
start: true,
|
||||
timeZone: tz
|
||||
});
|
||||
|
||||
if (gJobs.digestEmail) gJobs.digestEmail.stop();
|
||||
gJobs.digestEmail = new CronJob({
|
||||
if (gDigestEmailJob) gDigestEmailJob.stop();
|
||||
gDigestEmailJob = new CronJob({
|
||||
cronTime: '00 00 00 * * 3', // every wednesday
|
||||
onTick: digest.maybeSend,
|
||||
start: true,
|
||||
@@ -191,74 +187,55 @@ function recreateJobs(tz) {
|
||||
});
|
||||
}
|
||||
|
||||
function boxAutoupdatePatternChanged(pattern) {
|
||||
function autoupdatePatternChanged(pattern) {
|
||||
assert.strictEqual(typeof pattern, 'string');
|
||||
assert(gJobs.boxUpdateCheckerJob);
|
||||
assert(gBoxUpdateCheckerJob);
|
||||
|
||||
debug('Box auto update pattern changed to %s', pattern);
|
||||
debug('Auto update pattern changed to %s', pattern);
|
||||
|
||||
if (gJobs.boxAutoUpdater) gJobs.boxAutoUpdater.stop();
|
||||
if (gAutoupdaterJob) gAutoupdaterJob.stop();
|
||||
|
||||
if (pattern === constants.AUTOUPDATE_PATTERN_NEVER) return;
|
||||
|
||||
gJobs.boxAutoUpdater = new CronJob({
|
||||
gAutoupdaterJob = new CronJob({
|
||||
cronTime: pattern,
|
||||
onTick: function() {
|
||||
var updateInfo = updateChecker.getUpdateInfo();
|
||||
if (updateInfo.box) {
|
||||
debug('Starting autoupdate to %j', updateInfo.box);
|
||||
cloudron.updateToLatest(AUDIT_SOURCE, NOOP_CALLBACK);
|
||||
} else {
|
||||
debug('No box auto updates available');
|
||||
}
|
||||
},
|
||||
start: true,
|
||||
timeZone: gJobs.boxUpdateCheckerJob.cronTime.zone // hack
|
||||
});
|
||||
}
|
||||
|
||||
function appAutoupdatePatternChanged(pattern) {
|
||||
assert.strictEqual(typeof pattern, 'string');
|
||||
assert(gJobs.boxUpdateCheckerJob);
|
||||
|
||||
debug('Apps auto update pattern changed to %s', pattern);
|
||||
|
||||
if (gJobs.appAutoUpdater) gJobs.appAutoUpdater.stop();
|
||||
|
||||
if (pattern === constants.AUTOUPDATE_PATTERN_NEVER) return;
|
||||
|
||||
gJobs.appAutoUpdater = new CronJob({
|
||||
cronTime: pattern,
|
||||
onTick: function() {
|
||||
var updateInfo = updateChecker.getUpdateInfo();
|
||||
if (updateInfo.apps) {
|
||||
if (semver.major(updateInfo.box.version) === semver.major(config.version())) {
|
||||
debug('Starting autoupdate to %j', updateInfo.box);
|
||||
cloudron.updateToLatest(AUDIT_SOURCE, NOOP_CALLBACK);
|
||||
} else {
|
||||
debug('Block automatic update for major version');
|
||||
}
|
||||
} else if (updateInfo.apps) {
|
||||
debug('Starting app update to %j', updateInfo.apps);
|
||||
apps.autoupdateApps(updateInfo.apps, AUDIT_SOURCE, NOOP_CALLBACK);
|
||||
} else {
|
||||
debug('No app auto updates available');
|
||||
debug('No auto updates available');
|
||||
}
|
||||
},
|
||||
start: true,
|
||||
timeZone: gJobs.boxUpdateCheckerJob.cronTime.zone // hack
|
||||
timeZone: gBoxUpdateCheckerJob.cronTime.zone // hack
|
||||
});
|
||||
}
|
||||
|
||||
function dynamicDnsChanged(enabled) {
|
||||
function dynamicDNSChanged(enabled) {
|
||||
assert.strictEqual(typeof enabled, 'boolean');
|
||||
assert(gJobs.boxUpdateCheckerJob);
|
||||
assert(gBoxUpdateCheckerJob);
|
||||
|
||||
debug('Dynamic DNS setting changed to %s', enabled);
|
||||
|
||||
if (enabled) {
|
||||
gJobs.dynamicDNS = new CronJob({
|
||||
gDynamicDNSJob = new CronJob({
|
||||
cronTime: '00 */10 * * * *',
|
||||
onTick: dyndns.sync,
|
||||
onTick: cloudron.refreshDNS,
|
||||
start: true,
|
||||
timeZone: gJobs.boxUpdateCheckerJob.cronTime.zone // hack
|
||||
timeZone: gBoxUpdateCheckerJob.cronTime.zone // hack
|
||||
});
|
||||
} else {
|
||||
if (gJobs.dynamicDNS) gJobs.dynamicDNS.stop();
|
||||
gJobs.dynamicDNS = null;
|
||||
if (gDynamicDNSJob) gDynamicDNSJob.stop();
|
||||
gDynamicDNSJob = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,15 +243,49 @@ function uninitialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settings.events.removeListener(settings.TIME_ZONE_KEY, recreateJobs);
|
||||
settings.events.removeListener(settings.APP_AUTOUPDATE_PATTERN_KEY, appAutoupdatePatternChanged);
|
||||
settings.events.removeListener(settings.BOX_AUTOUPDATE_PATTERN_KEY, boxAutoupdatePatternChanged);
|
||||
settings.events.removeListener(settings.DYNAMIC_DNS_KEY, dynamicDnsChanged);
|
||||
settings.events.removeListener(settings.AUTOUPDATE_PATTERN_KEY, autoupdatePatternChanged);
|
||||
|
||||
for (var job in gJobs) {
|
||||
if (!gJobs[job]) continue;
|
||||
gJobs[job].stop();
|
||||
gJobs[job] = null;
|
||||
}
|
||||
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;
|
||||
|
||||
callback();
|
||||
}
|
||||
|
||||
@@ -10,9 +10,6 @@ exports = module.exports = {
|
||||
rollback: rollback,
|
||||
commit: commit,
|
||||
|
||||
importFromFile: importFromFile,
|
||||
exportToFile: exportToFile,
|
||||
|
||||
_clear: clear
|
||||
};
|
||||
|
||||
@@ -186,27 +183,3 @@ 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);
|
||||
}
|
||||
|
||||
+31
-6
@@ -5,6 +5,8 @@
|
||||
exports = module.exports = {
|
||||
DeveloperError: DeveloperError,
|
||||
|
||||
isEnabled: isEnabled,
|
||||
setEnabled: setEnabled,
|
||||
issueDeveloperToken: issueDeveloperToken
|
||||
};
|
||||
|
||||
@@ -13,7 +15,7 @@ var assert = require('assert'),
|
||||
constants = require('./constants.js'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
tokendb = require('./tokendb.js'),
|
||||
user = require('./user.js'),
|
||||
settings = require('./settings.js'),
|
||||
util = require('util');
|
||||
|
||||
function DeveloperError(reason, errorOrMessage) {
|
||||
@@ -38,19 +40,42 @@ util.inherits(DeveloperError, Error);
|
||||
DeveloperError.INTERNAL_ERROR = 'Internal Error';
|
||||
DeveloperError.EXTERNAL_ERROR = 'External Error';
|
||||
|
||||
function issueDeveloperToken(userObject, ip, callback) {
|
||||
assert.strictEqual(typeof userObject, 'object');
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
function 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');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var token = tokendb.generateToken();
|
||||
var expiresAt = Date.now() + constants.DEFAULT_TOKEN_EXPIRATION;
|
||||
var scopes = '*,' + clients.SCOPE_ROLE_SDK;
|
||||
|
||||
tokendb.add(token, userObject.id, 'cid-cli', expiresAt, scopes, function (error) {
|
||||
tokendb.add(token, user.id, 'cid-cli', expiresAt, scopes, function (error) {
|
||||
if (error) return callback(new DeveloperError(DeveloperError.INTERNAL_ERROR, error));
|
||||
|
||||
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'cli', ip: ip }, { userId: userObject.id, user: user.removePrivateFields(userObject) });
|
||||
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource, { authType: 'cli', userId: user.id, username: user.username });
|
||||
|
||||
callback(null, { token: token, expiresAt: new Date(expiresAt).toISOString() });
|
||||
});
|
||||
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
resolve: resolve
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
child_process = require('child_process'),
|
||||
debug = require('debug')('box:dig');
|
||||
|
||||
function resolve(domain, type, options, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// dig @server cloudron.io TXT +short
|
||||
var args = [ ];
|
||||
if (options.server) args.push('@' + options.server);
|
||||
if (type === 'PTR') {
|
||||
args.push('-x', domain);
|
||||
} else {
|
||||
args.push(domain, type);
|
||||
}
|
||||
args.push('+short');
|
||||
|
||||
child_process.execFile('/usr/bin/dig', args, { encoding: 'utf8', killSignal: 'SIGKILL', timeout: options.timeout || 0 }, function (error, stdout, stderr) {
|
||||
if (error && error.killed) error.code = 'ETIMEDOUT';
|
||||
|
||||
if (error || stderr) debug('resolve error (%j): %j %s %s', args, error, stdout, stderr);
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('resolve (%j): %s', args, stdout);
|
||||
|
||||
if (!stdout) return callback(); // timeout or no result
|
||||
|
||||
var lines = stdout.trim().split('\n');
|
||||
if (type === 'MX') {
|
||||
lines = lines.map(function (line) {
|
||||
var parts = line.split(' ');
|
||||
return { priority: parts[0], exchange: parts[1] };
|
||||
});
|
||||
}
|
||||
return callback(null, lines);
|
||||
});
|
||||
}
|
||||
+19
-27
@@ -11,17 +11,10 @@ exports = module.exports = {
|
||||
var assert = require('assert'),
|
||||
config = require('../config.js'),
|
||||
debug = require('debug')('box:dns/caas'),
|
||||
DomainError = require('../domains.js').DomainError,
|
||||
SubdomainError = require('../subdomains.js').SubdomainError,
|
||||
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');
|
||||
@@ -30,9 +23,9 @@ function add(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
assert(util.isArray(values));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var fqdn = subdomain !== '' && type === 'TXT' ? subdomain + '.' + dnsConfig.fqdn : getFqdn(subdomain, dnsConfig.fqdn);
|
||||
var fqdn = subdomain !== '' && type === 'TXT' ? subdomain + '.' + config.fqdn() : config.appFqdn(subdomain);
|
||||
|
||||
debug('add: %s for zone %s of type %s with values %j', subdomain, dnsConfig.fqdn, type, values);
|
||||
debug('add: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
|
||||
|
||||
var data = {
|
||||
type: type,
|
||||
@@ -45,10 +38,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 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)));
|
||||
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)));
|
||||
|
||||
return callback(null, result.body.changeId);
|
||||
});
|
||||
@@ -61,17 +54,17 @@ function get(dnsConfig, zoneName, subdomain, type, callback) {
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var fqdn = subdomain !== '' && type === 'TXT' ? subdomain + '.' + dnsConfig.fqdn : getFqdn(subdomain, dnsConfig.fqdn);
|
||||
var fqdn = subdomain !== '' && type === 'TXT' ? subdomain + '.' + config.fqdn() : config.appFqdn(subdomain);
|
||||
|
||||
debug('get: zoneName: %s subdomain: %s type: %s fqdn: %s', dnsConfig.fqdn, subdomain, type, fqdn);
|
||||
debug('get: zoneName: %s subdomain: %s type: %s fqdn: %s', zoneName, 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 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)));
|
||||
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)));
|
||||
|
||||
return callback(null, result.body.values);
|
||||
});
|
||||
@@ -96,7 +89,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, dnsConfig.fqdn, type, values);
|
||||
debug('del: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
|
||||
|
||||
var data = {
|
||||
type: type,
|
||||
@@ -104,16 +97,16 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
};
|
||||
|
||||
superagent
|
||||
.del(config.apiServerOrigin() + '/api/v1/domains/' + getFqdn(subdomain, dnsConfig.fqdn))
|
||||
.del(config.apiServerOrigin() + '/api/v1/domains/' + config.appFqdn(subdomain))
|
||||
.query({ token: dnsConfig.token })
|
||||
.send(data)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
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)));
|
||||
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)));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
@@ -127,8 +120,7 @@ function verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var credentials = {
|
||||
token: dnsConfig.token,
|
||||
fqdn: domain
|
||||
provider: dnsConfig.provider
|
||||
};
|
||||
|
||||
return callback(null, credentials);
|
||||
|
||||
+70
-77
@@ -10,12 +10,12 @@ exports = module.exports = {
|
||||
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
debug = require('debug')('box:dns/cloudflare'),
|
||||
dns = require('../native-dns.js'),
|
||||
DomainError = require('../domains.js').DomainError,
|
||||
dns = require('dns'),
|
||||
_ = require('underscore'),
|
||||
SubdomainError = require('../subdomains.js').SubdomainError,
|
||||
superagent = require('superagent'),
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
debug = require('debug')('box:dns/cloudflare'),
|
||||
util = require('util');
|
||||
|
||||
// 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 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 === 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 === 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 DomainError(DomainError.ACCESS_DENIED, message));
|
||||
return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, message));
|
||||
}
|
||||
|
||||
callback(new DomainError(DomainError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
|
||||
callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
|
||||
}
|
||||
|
||||
function getZoneByName(dnsConfig, zoneName, callback) {
|
||||
@@ -46,19 +46,19 @@ function getZoneByName(dnsConfig, zoneName, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
superagent.get(CLOUDFLARE_ENDPOINT + '/zones?name=' + zoneName + '&status=active')
|
||||
.set('X-Auth-Key', dnsConfig.token)
|
||||
.set('X-Auth-Email', dnsConfig.email)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(error);
|
||||
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
|
||||
if (!result.body.result.length) return callback(new DomainError(DomainError.NOT_FOUND, util.format('%s %j', result.statusCode, result.body)));
|
||||
.set('X-Auth-Key', dnsConfig.token)
|
||||
.set('X-Auth-Email', dnsConfig.email)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(error);
|
||||
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
|
||||
if (!result.body.result.length) return callback(new SubdomainError(SubdomainError.NOT_FOUND, util.format('%s %j', result.statusCode, result.body)));
|
||||
|
||||
callback(null, result.body.result[0]);
|
||||
});
|
||||
callback(null, result.body.result[0]);
|
||||
});
|
||||
}
|
||||
|
||||
function getDnsRecordsByZoneId(dnsConfig, zoneId, zoneName, subdomain, type, callback) {
|
||||
function getDNSRecordsByZoneId(dnsConfig, zoneId, zoneName, subdomain, type, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneId, 'string');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
@@ -69,18 +69,18 @@ function getDnsRecordsByZoneId(dnsConfig, zoneId, zoneName, subdomain, type, cal
|
||||
var fqdn = subdomain === '' ? zoneName : subdomain + '.' + zoneName;
|
||||
|
||||
superagent.get(CLOUDFLARE_ENDPOINT + '/zones/' + zoneId + '/dns_records')
|
||||
.set('X-Auth-Key',dnsConfig.token)
|
||||
.set('X-Auth-Email',dnsConfig.email)
|
||||
.query({ type: type, name: fqdn })
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(error);
|
||||
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
|
||||
.set('X-Auth-Key',dnsConfig.token)
|
||||
.set('X-Auth-Email',dnsConfig.email)
|
||||
.query({ type: type, name: fqdn })
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(error);
|
||||
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
|
||||
|
||||
var tmp = result.body.result;
|
||||
var tmp = result.body.result;
|
||||
|
||||
return callback(null, tmp);
|
||||
});
|
||||
return callback(null, tmp);
|
||||
});
|
||||
}
|
||||
|
||||
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
@@ -100,7 +100,7 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
|
||||
var zoneId = result.id;
|
||||
|
||||
getDnsRecordsByZoneId(dnsConfig, zoneId, zoneName, subdomain, type, function (error, result) {
|
||||
getDNSRecordsByZoneId(dnsConfig, zoneId, zoneName, subdomain, type, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var dnsRecords = result;
|
||||
@@ -126,31 +126,31 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
|
||||
if (i >= dnsRecords.length) {
|
||||
superagent.post(CLOUDFLARE_ENDPOINT + '/zones/'+ zoneId + '/dns_records')
|
||||
.set('X-Auth-Key',dnsConfig.token)
|
||||
.set('X-Auth-Email',dnsConfig.email)
|
||||
.send(data)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(error);
|
||||
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
|
||||
.set('X-Auth-Key',dnsConfig.token)
|
||||
.set('X-Auth-Email',dnsConfig.email)
|
||||
.send(data)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(error);
|
||||
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
callback(null);
|
||||
});
|
||||
} else {
|
||||
superagent.put(CLOUDFLARE_ENDPOINT + '/zones/'+ zoneId + '/dns_records/' + dnsRecords[i].id)
|
||||
.set('X-Auth-Key',dnsConfig.token)
|
||||
.set('X-Auth-Email',dnsConfig.email)
|
||||
.send(data)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
.set('X-Auth-Key',dnsConfig.token)
|
||||
.set('X-Auth-Email',dnsConfig.email)
|
||||
.send(data)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
// increment, as we have consumed the record
|
||||
++i;
|
||||
++i;
|
||||
|
||||
if (error && !error.response) return callback(error);
|
||||
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
|
||||
if (error && !error.response) return callback(error);
|
||||
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
}, function (error) {
|
||||
if (error) return callback(error);
|
||||
@@ -171,7 +171,7 @@ function get(dnsConfig, zoneName, subdomain, type, callback) {
|
||||
getZoneByName(dnsConfig, zoneName, function(error, result){
|
||||
if (error) return callback(error);
|
||||
|
||||
getDnsRecordsByZoneId(dnsConfig, result.id, zoneName, subdomain, type, function(error, result) {
|
||||
getDNSRecordsByZoneId(dnsConfig, result.id, zoneName, subdomain, type, function(error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var tmp = result.map(function (record) { return record.content; });
|
||||
@@ -193,7 +193,7 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
getZoneByName(dnsConfig, zoneName, function(error, result){
|
||||
if (error) return callback(error);
|
||||
|
||||
getDnsRecordsByZoneId(dnsConfig, result.id, zoneName, subdomain, type, function(error, result) {
|
||||
getDNSRecordsByZoneId(dnsConfig, result.id, zoneName, subdomain, type, function(error, result) {
|
||||
if (error) return callback(error);
|
||||
if (result.length === 0) return callback(null);
|
||||
|
||||
@@ -206,17 +206,17 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
|
||||
async.eachSeries(tmp, function (record, callback) {
|
||||
superagent.del(CLOUDFLARE_ENDPOINT + '/zones/'+ zoneId + '/dns_records/' + record.id)
|
||||
.set('X-Auth-Key',dnsConfig.token)
|
||||
.set('X-Auth-Email',dnsConfig.email)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(error);
|
||||
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
|
||||
.set('X-Auth-Key',dnsConfig.token)
|
||||
.set('X-Auth-Email',dnsConfig.email)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(error);
|
||||
if (result.statusCode !== 204 || result.body.success !== true) return translateRequestError(result, callback);
|
||||
|
||||
debug('del: done');
|
||||
debug('del: done');
|
||||
|
||||
callback(null);
|
||||
});
|
||||
callback(null);
|
||||
});
|
||||
}, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
@@ -233,42 +233,35 @@ 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 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'));
|
||||
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'));
|
||||
|
||||
var credentials = {
|
||||
provider: dnsConfig.provider,
|
||||
token: dnsConfig.token,
|
||||
email: dnsConfig.email
|
||||
};
|
||||
|
||||
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
|
||||
|
||||
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
|
||||
if (error && error.code === 'ENOTFOUND') return callback(new DomainError(DomainError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
|
||||
if (error || !nameservers) return callback(new DomainError(DomainError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
|
||||
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'));
|
||||
|
||||
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 DomainError(DomainError.BAD_FIELD, 'Domain nameservers are not set to Cloudflare'));
|
||||
return callback(new SubdomainError(SubdomainError.BAD_FIELD, 'Domain nameservers are not set to Cloudflare'));
|
||||
}
|
||||
|
||||
const testSubdomain = 'cloudrontestdns';
|
||||
|
||||
upsert(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error, changeId) {
|
||||
upsert(credentials, zoneName, 'my', 'A', [ ip ], function (error, changeId) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('verifyDnsConfig: Test A record added with change id %s', changeId);
|
||||
debug('verifyDnsConfig: A record added with change id %s', changeId);
|
||||
|
||||
del(dnsConfig, zoneName, testSubdomain, 'A', [ ip ], function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('verifyDnsConfig: Test A record removed again');
|
||||
|
||||
callback(null, credentials);
|
||||
});
|
||||
callback(null, credentials);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+31
-35
@@ -10,10 +10,11 @@ exports = module.exports = {
|
||||
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
config = require('../config.js'),
|
||||
debug = require('debug')('box:dns/digitalocean'),
|
||||
dns = require('../native-dns.js'),
|
||||
DomainError = require('../domains.js').DomainError,
|
||||
dns = require('dns'),
|
||||
safe = require('safetydance'),
|
||||
SubdomainError = require('../subdomains.js').SubdomainError,
|
||||
superagent = require('superagent'),
|
||||
util = require('util');
|
||||
|
||||
@@ -39,10 +40,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 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)));
|
||||
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)));
|
||||
|
||||
matchingRecords = matchingRecords.concat(result.body.domain_records.filter(function (record) {
|
||||
return (record.type === type && record.name === subdomain);
|
||||
@@ -79,7 +80,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, iteratorCallback) {
|
||||
async.eachSeries(values, function (value, callback) {
|
||||
var priority = null;
|
||||
|
||||
if (type === 'MX') {
|
||||
@@ -101,14 +102,14 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
.send(data)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, 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)));
|
||||
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)));
|
||||
|
||||
recordIds.push(safe.query(result.body, 'domain_record.id'));
|
||||
|
||||
return iteratorCallback(null);
|
||||
return callback(null);
|
||||
});
|
||||
} else {
|
||||
superagent.put(DIGITALOCEAN_ENDPOINT + '/v2/domains/' + zoneName + '/records/' + result[i].id)
|
||||
@@ -119,17 +120,17 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
// increment, as we have consumed the record
|
||||
++i;
|
||||
|
||||
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)));
|
||||
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)));
|
||||
|
||||
recordIds.push(safe.query(result.body, 'domain_record.id'));
|
||||
|
||||
return iteratorCallback(null);
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
}, function (error) {
|
||||
}, function (error, id) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, '' + recordIds[0]); // DO ids are integers
|
||||
@@ -185,10 +186,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 DomainError(DomainError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
|
||||
if (error && !error.response) return callback(new SubdomainError(SubdomainError.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 DomainError(DomainError.ACCESS_DENIED, formatError(result)));
|
||||
if (result.statusCode !== 204) return callback(new DomainError(DomainError.EXTERNAL_ERROR, formatError(result)));
|
||||
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)));
|
||||
|
||||
debug('del: done');
|
||||
|
||||
@@ -205,34 +206,29 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var credentials = {
|
||||
provider: dnsConfig.provider,
|
||||
token: dnsConfig.token
|
||||
};
|
||||
|
||||
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
|
||||
|
||||
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
|
||||
if (error && error.code === 'ENOTFOUND') return callback(new DomainError(DomainError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
|
||||
if (error || !nameservers) return callback(new DomainError(DomainError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
|
||||
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 (nameservers.map(function (n) { return n.toLowerCase(); }).indexOf('ns1.digitalocean.com') === -1) {
|
||||
debug('verifyDnsConfig: %j does not contains DO NS', nameservers);
|
||||
return callback(new DomainError(DomainError.BAD_FIELD, 'Domain nameservers are not set to Digital Ocean'));
|
||||
return callback(new SubdomainError(SubdomainError.BAD_FIELD, 'Domain nameservers are not set to Digital Ocean'));
|
||||
}
|
||||
|
||||
const testSubdomain = 'cloudrontestdns';
|
||||
const name = config.adminLocation() + (fqdn === zoneName ? '' : '.' + fqdn.slice(0, - zoneName.length - 1));
|
||||
|
||||
upsert(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error, changeId) {
|
||||
upsert(credentials, zoneName, name, 'A', [ ip ], function (error, changeId) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('verifyDnsConfig: Test A record added with change id %s', changeId);
|
||||
debug('verifyDnsConfig: A record added with change id %s', changeId);
|
||||
|
||||
del(dnsConfig, zoneName, testSubdomain, 'A', [ ip ], function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('verifyDnsConfig: Test A record removed again');
|
||||
|
||||
callback(null, credentials);
|
||||
});
|
||||
callback(null, credentials);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+30
-34
@@ -9,10 +9,11 @@ exports = module.exports = {
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
config = require('../config.js'),
|
||||
debug = require('debug')('box:dns/gcdns'),
|
||||
dns = require('../native-dns.js'),
|
||||
DomainError = require('../domains.js').DomainError,
|
||||
dns = require('dns'),
|
||||
GCDNS = require('@google-cloud/dns'),
|
||||
SubdomainError = require('../subdomains.js').SubdomainError,
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
|
||||
@@ -20,6 +21,7 @@ function getDnsCredentials(dnsConfig) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
|
||||
var config = {
|
||||
provider: dnsConfig.provider,
|
||||
projectId: dnsConfig.projectId,
|
||||
keyFilename: dnsConfig.keyFilename,
|
||||
email: dnsConfig.email
|
||||
@@ -42,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 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 && 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) {
|
||||
debug('gcdns.getZones', error);
|
||||
return callback(new DomainError(DomainError.EXTERNAL_ERROR, error));
|
||||
return callback(new SubdomainError(SubdomainError.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 DomainError(DomainError.NOT_FOUND, 'no such zone'));
|
||||
if (!zone) return callback(new SubdomainError(SubdomainError.NOT_FOUND, 'no such zone'));
|
||||
|
||||
callback(null, zone); //zone.metadata ~= {name="", dnsName="", nameServers:[]}
|
||||
});
|
||||
@@ -77,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 DomainError(DomainError.ACCESS_DENIED, error.message));
|
||||
if (error && error.code === 403) return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, error.message));
|
||||
if (error) {
|
||||
debug('upsert->zone.getRecords', error);
|
||||
return callback(new DomainError(DomainError.EXTERNAL_ERROR, error.message));
|
||||
return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error.message));
|
||||
}
|
||||
|
||||
var newRecord = zone.record(type, {
|
||||
@@ -90,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 DomainError(DomainError.ACCESS_DENIED, error.message));
|
||||
if (error && error.code === 412) return callback(new DomainError(DomainError.STILL_BUSY, error.message));
|
||||
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) {
|
||||
debug('upsert->zone.createChange', error);
|
||||
return callback(new DomainError(DomainError.EXTERNAL_ERROR, error.message));
|
||||
return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error.message));
|
||||
}
|
||||
|
||||
callback(null, change.id);
|
||||
@@ -119,8 +121,8 @@ function get(dnsConfig, zoneName, subdomain, type, callback) {
|
||||
};
|
||||
|
||||
zone.getRecords(params, function (error, records) {
|
||||
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 (error && error.code === 403) return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, error.message));
|
||||
if (error) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error));
|
||||
if (records.length === 0) return callback(null, [ ]);
|
||||
|
||||
return callback(null, records[0].data);
|
||||
@@ -142,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 DomainError(DomainError.ACCESS_DENIED, error.message));
|
||||
if (error && error.code === 403) return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, error.message));
|
||||
if (error) {
|
||||
debug('del->zone.getRecords', error);
|
||||
return callback(new DomainError(DomainError.EXTERNAL_ERROR, error.message));
|
||||
return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error.message));
|
||||
}
|
||||
|
||||
zone.deleteRecords(oldRecords, function (error, change) {
|
||||
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 && 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) {
|
||||
debug('del->zone.createChange', error);
|
||||
return callback(new DomainError(DomainError.EXTERNAL_ERROR, error.message));
|
||||
return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error.message));
|
||||
}
|
||||
|
||||
callback(null, change.id);
|
||||
@@ -172,9 +174,9 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
|
||||
var credentials = getDnsCredentials(dnsConfig);
|
||||
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
|
||||
|
||||
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
|
||||
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'));
|
||||
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'));
|
||||
|
||||
getZoneByName(credentials, zoneName, function (error, zone) {
|
||||
if (error) return callback(error);
|
||||
@@ -182,23 +184,17 @@ 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 DomainError(DomainError.BAD_FIELD, 'Domain nameservers are not set to Google Cloud DNS'));
|
||||
return callback(new SubdomainError(SubdomainError.BAD_FIELD, 'Domain nameservers are not set to Google Cloud DNS'));
|
||||
}
|
||||
|
||||
const testSubdomain = 'cloudrontestdns';
|
||||
const name = config.adminLocation() + (fqdn === zoneName ? '' : '.' + fqdn.slice(0, - zoneName.length - 1));
|
||||
|
||||
upsert(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error, changeId) {
|
||||
upsert(credentials, zoneName, name, 'A', [ ip ], function (error, changeId) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('verifyDnsConfig: Test A record added with change id %s', changeId);
|
||||
debug('verifyDnsConfig: A record added with change id %s', changeId);
|
||||
|
||||
del(dnsConfig, zoneName, testSubdomain, 'A', [ ip ], function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('verifyDnsConfig: Test A record removed again');
|
||||
|
||||
callback(null, credentials);
|
||||
});
|
||||
callback(null, credentials);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@ exports = module.exports = {
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
DomainError = require('../domains.js').DomainError,
|
||||
SubdomainError = require('../subdomains.js').SubdomainError,
|
||||
util = require('util');
|
||||
|
||||
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
|
||||
+49
-6
@@ -9,9 +9,12 @@ exports = module.exports = {
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
config = require('../config.js'),
|
||||
debug = require('debug')('box:dns/manual'),
|
||||
dns = require('../native-dns.js'),
|
||||
DomainError = require('../domains.js').DomainError,
|
||||
dig = require('../dig.js'),
|
||||
dns = require('dns'),
|
||||
SubdomainError = require('../subdomains.js').SubdomainError,
|
||||
util = require('util');
|
||||
|
||||
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
@@ -55,10 +58,50 @@ function verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback) {
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// Very basic check if the nameservers can be fetched
|
||||
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
|
||||
if (error || !nameservers) return callback(new DomainError(DomainError.BAD_FIELD, 'Unable to get nameservers'));
|
||||
var adminDomain = config.adminLocation() + '.' + domain;
|
||||
|
||||
callback(null, { wildcard: !!dnsConfig.wildcard });
|
||||
dns.resolveNs(zoneName, function (error, nameservers) {
|
||||
if (error || !nameservers) return callback(new SubdomainError(SubdomainError.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 });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+8
-3
@@ -46,10 +46,11 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
function waitForDns(domain, zoneName, value, options, callback) {
|
||||
function waitForDns(domain, zoneName, value, type, options, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof value, 'string');
|
||||
assert(typeof value === 'string' || util.isRegExp(value));
|
||||
assert(type === 'A' || type === 'CNAME' || type === 'TXT');
|
||||
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -63,5 +64,9 @@ function verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback) {
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
return callback(null, { });
|
||||
var credentials = {
|
||||
provider: dnsConfig.provider
|
||||
};
|
||||
|
||||
return callback(null, credentials);
|
||||
}
|
||||
|
||||
+35
-39
@@ -13,9 +13,10 @@ exports = module.exports = {
|
||||
|
||||
var assert = require('assert'),
|
||||
AWS = require('aws-sdk'),
|
||||
config = require('../config.js'),
|
||||
debug = require('debug')('box:dns/route53'),
|
||||
dns = require('../native-dns.js'),
|
||||
DomainError = require('../domains.js').DomainError,
|
||||
dns = require('dns'),
|
||||
SubdomainError = require('../subdomains.js').SubdomainError,
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
|
||||
@@ -40,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 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 (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));
|
||||
|
||||
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 DomainError(DomainError.NOT_FOUND, 'no such zone'));
|
||||
if (!zone) return callback(new SubdomainError(SubdomainError.NOT_FOUND, 'no such zone'));
|
||||
|
||||
callback(null, zone);
|
||||
});
|
||||
@@ -64,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 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 (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));
|
||||
|
||||
callback(null, result);
|
||||
});
|
||||
@@ -106,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 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));
|
||||
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));
|
||||
|
||||
callback(null, result.ChangeInfo.Id);
|
||||
});
|
||||
@@ -147,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 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 (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 (result.ResourceRecordSets.length === 0) return callback(null, [ ]);
|
||||
if (result.ResourceRecordSets[0].Name !== params.StartRecordName || result.ResourceRecordSets[0].Type !== params.StartRecordType) return callback(null, [ ]);
|
||||
|
||||
@@ -192,24 +193,24 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
};
|
||||
|
||||
var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
|
||||
route53.changeResourceRecordSets(params, function(error) {
|
||||
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));
|
||||
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.message && error.message.indexOf('it was not found') !== -1) {
|
||||
debug('del: resource record set not found.', error);
|
||||
return callback(new DomainError(DomainError.NOT_FOUND, error.message));
|
||||
return callback(new SubdomainError(SubdomainError.NOT_FOUND, error.message));
|
||||
} else if (error && error.code === 'NoSuchHostedZone') {
|
||||
debug('del: hosted zone not found.', error);
|
||||
return callback(new DomainError(DomainError.NOT_FOUND, error.message));
|
||||
return callback(new SubdomainError(SubdomainError.NOT_FOUND, error.message));
|
||||
} else if (error && error.code === 'PriorRequestNotComplete') {
|
||||
debug('del: resource is still busy', error);
|
||||
return callback(new DomainError(DomainError.STILL_BUSY, error.message));
|
||||
return callback(new SubdomainError(SubdomainError.STILL_BUSY, error.message));
|
||||
} else if (error && error.code === 'InvalidChangeBatch') {
|
||||
debug('del: invalid change batch. No such record to be deleted.');
|
||||
return callback(new DomainError(DomainError.NOT_FOUND, error.message));
|
||||
return callback(new SubdomainError(SubdomainError.NOT_FOUND, error.message));
|
||||
} else if (error) {
|
||||
debug('del: error', error);
|
||||
return callback(new DomainError(DomainError.EXTERNAL_ERROR, error.message));
|
||||
return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error.message));
|
||||
}
|
||||
|
||||
callback(null);
|
||||
@@ -225,6 +226,7 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var credentials = {
|
||||
provider: dnsConfig.provider,
|
||||
accessKeyId: dnsConfig.accessKeyId,
|
||||
secretAccessKey: dnsConfig.secretAccessKey,
|
||||
region: dnsConfig.region || 'us-east-1',
|
||||
@@ -233,32 +235,26 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
|
||||
|
||||
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
|
||||
|
||||
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
|
||||
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'));
|
||||
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'));
|
||||
|
||||
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 DomainError(DomainError.BAD_FIELD, 'Domain nameservers are not set to Route53'));
|
||||
return callback(new SubdomainError(SubdomainError.BAD_FIELD, 'Domain nameservers are not set to Route53'));
|
||||
}
|
||||
|
||||
const testSubdomain = 'cloudrontestdns';
|
||||
const name = config.adminLocation() + (fqdn === zoneName ? '' : '.' + fqdn.slice(0, - zoneName.length - 1));
|
||||
|
||||
upsert(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error, changeId) {
|
||||
upsert(credentials, zoneName, name, 'A', [ ip ], function (error, changeId) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('verifyDnsConfig: Test A record added with change id %s', changeId);
|
||||
debug('verifyDnsConfig: A record added with change id %s', changeId);
|
||||
|
||||
del(dnsConfig, zoneName, testSubdomain, 'A', [ ip ], function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('verifyDnsConfig: Test A record removed again');
|
||||
|
||||
callback(null, credentials);
|
||||
});
|
||||
callback(null, credentials);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+49
-50
@@ -5,94 +5,93 @@ exports = module.exports = waitForDns;
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
debug = require('debug')('box:dns/waitfordns'),
|
||||
dns = require('../native-dns.js'),
|
||||
DomainError = require('../domains.js').DomainError;
|
||||
dig = require('../dig.js'),
|
||||
dns = require('dns'),
|
||||
SubdomainError = require('../subdomains.js').SubdomainError,
|
||||
util = require('util');
|
||||
|
||||
function resolveIp(hostname, options, callback) {
|
||||
assert.strictEqual(typeof hostname, 'string');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// try A record at authoritative server
|
||||
debug(`resolveIp: Checking if ${hostname} has A record at ${options.server}`);
|
||||
dns.resolve(hostname, 'A', options, function (error, results) {
|
||||
if (!error && results.length !== 0) return callback(null, results);
|
||||
|
||||
// try CNAME record at authoritative server
|
||||
debug(`resolveIp: Checking if ${hostname} has CNAME record at ${options.server}`);
|
||||
dns.resolve(hostname, 'CNAME', options, function (error, results) {
|
||||
if (error || results.length === 0) return callback(error, results);
|
||||
|
||||
// recurse lookup the CNAME record
|
||||
debug(`resolveIp: Resolving ${hostname}'s CNAME record ${results[0]}`);
|
||||
dns.resolve(results[0], 'A', { server: '127.0.0.1', timeout: options.timeout }, callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function isChangeSynced(domain, value, nameserver, callback) {
|
||||
function isChangeSynced(domain, value, type, nameserver, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof value, 'string');
|
||||
assert(util.isRegExp(value));
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert.strictEqual(typeof nameserver, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// ns records cannot have cname
|
||||
dns.resolve(nameserver, 'A', { timeout: 5000 }, function (error, nsIps) {
|
||||
dns.resolve4(nameserver, function (error, nsIps) {
|
||||
if (error || !nsIps || nsIps.length === 0) {
|
||||
debug(`isChangeSynced: cannot resolve NS ${nameserver}`); // it's fine if one or more ns are dead
|
||||
return callback(null, true);
|
||||
debug('nameserver %s does not resolve. assuming it stays bad.', nameserver); // it's fine if one or more ns are dead
|
||||
return callback(true);
|
||||
}
|
||||
|
||||
async.every(nsIps, function (nsIp, iteratorCallback) {
|
||||
resolveIp(domain, { server: nsIp, timeout: 5000 }, function (error, answer) {
|
||||
if (error && error.code === 'TIMEOUT') {
|
||||
debug(`isChangeSynced: NS ${nameserver} (${nsIp}) timed out when resolving ${domain}`);
|
||||
dig.resolve(domain, type, { server: nsIp, timeout: 5000 }, function (error, answer) {
|
||||
if (error && error.code === 'ETIMEDOUT') {
|
||||
debug('nameserver %s (%s) timed out when trying to resolve %s', nameserver, nsIp, domain);
|
||||
return iteratorCallback(null, true); // should be ok if dns server is down
|
||||
}
|
||||
|
||||
if (error) {
|
||||
debug(`isChangeSynced: NS ${nameserver} (${nsIp}) errored when resolve ${domain}: ${error}`);
|
||||
debug('nameserver %s (%s) returned error trying to resolve %s: %s', nameserver, nsIp, domain, error);
|
||||
return iteratorCallback(null, false);
|
||||
}
|
||||
|
||||
debug(`isChangeSynced: ${domain} was resolved to ${answer} at NS ${nameserver} (${nsIp}). Expecting ${value}`);
|
||||
if (!answer || answer.length === 0) {
|
||||
debug('bad answer from nameserver %s (%s) resolving %s (%s)', nameserver, nsIp, domain, type);
|
||||
return iteratorCallback(null, false);
|
||||
}
|
||||
|
||||
iteratorCallback(null, answer.length === 1 && answer[0] === value);
|
||||
debug('isChangeSynced: ns: %s (%s), name:%s Actual:%j Expecting:%s', nameserver, nsIp, domain, answer, value);
|
||||
|
||||
var match = answer.some(function (a) {
|
||||
return ((type === 'A' && value.test(a)) ||
|
||||
(type === 'CNAME' && value.test(a)) ||
|
||||
(type === 'TXT' && value.test(a)));
|
||||
});
|
||||
|
||||
if (match) return iteratorCallback(null, true); // done!
|
||||
|
||||
iteratorCallback(null, false);
|
||||
});
|
||||
}, callback);
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// check if IP change has propagated to every nameserver
|
||||
function waitForDns(domain, zoneName, value, options, callback) {
|
||||
function waitForDns(domain, zoneName, value, type, options, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof value, 'string');
|
||||
assert(typeof value === 'string' || util.isRegExp(value));
|
||||
assert(type === 'A' || type === 'CNAME' || type === 'TXT');
|
||||
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('waitForDns: domain %s to be %s in zone %s.', domain, value, zoneName);
|
||||
if (typeof value === 'string') {
|
||||
// http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript
|
||||
value = new RegExp('^' + value.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') + '$');
|
||||
}
|
||||
|
||||
var attempt = 0;
|
||||
debug('waitForIp: domain %s to be %s in zone %s.', domain, value, zoneName);
|
||||
|
||||
var attempt = 1;
|
||||
async.retry(options, function (retryCallback) {
|
||||
++attempt;
|
||||
debug(`waitForDns (try ${attempt}): ${domain} to be ${value} in zone ${zoneName}`);
|
||||
debug('waitForDNS: %s attempt %s.', domain, attempt++);
|
||||
|
||||
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
|
||||
if (error || !nameservers) return retryCallback(error || new DomainError(DomainError.EXTERNAL_ERROR, 'Unable to get nameservers'));
|
||||
dns.resolveNs(zoneName, function (error, nameservers) {
|
||||
if (error || !nameservers) return retryCallback(error || new SubdomainError(SubdomainError.EXTERNAL_ERROR, 'Unable to get nameservers'));
|
||||
|
||||
async.every(nameservers, isChangeSynced.bind(null, domain, value), function (error, synced) {
|
||||
debug('waitForDns: %s %s ns: %j', domain, synced ? 'done' : 'not done', nameservers);
|
||||
async.every(nameservers, isChangeSynced.bind(null, domain, value, type), function (error, synced) {
|
||||
debug('waitForIp: %s %s ns: %j', domain, synced ? 'done' : 'not done', nameservers);
|
||||
|
||||
retryCallback(synced ? null : new DomainError(DomainError.EXTERNAL_ERROR, 'ETRYAGAIN'));
|
||||
retryCallback(synced ? null : new SubdomainError(SubdomainError.EXTERNAL_ERROR, 'ETRYAGAIN'));
|
||||
});
|
||||
});
|
||||
}, function retryDone(error) {
|
||||
if (error) return callback(error);
|
||||
if (error) return callback(error);
|
||||
|
||||
debug(`waitForDns: ${domain} has propagated`);
|
||||
debug('waitForDNS: %s done.', domain);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+5
-9
@@ -15,7 +15,6 @@ exports = module.exports = {
|
||||
createSubcontainer: createSubcontainer,
|
||||
getContainerIdByIp: getContainerIdByIp,
|
||||
inspect: inspect,
|
||||
inspectByName: inspect,
|
||||
execContainer: execContainer
|
||||
};
|
||||
|
||||
@@ -50,9 +49,10 @@ var addons = require('./addons.js'),
|
||||
_ = require('underscore');
|
||||
|
||||
function debugApp(app, args) {
|
||||
assert(typeof app === 'object');
|
||||
assert(!app || typeof app === 'object');
|
||||
|
||||
debug(app.fqdn + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
|
||||
var prefix = app ? (app.location || '(bare)') : '(no app)';
|
||||
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
|
||||
}
|
||||
|
||||
function pullImage(manifest, callback) {
|
||||
@@ -129,7 +129,7 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
||||
|
||||
var manifest = app.manifest;
|
||||
var exposedPorts = {}, dockerPortBindings = { };
|
||||
var domain = app.fqdn;
|
||||
var domain = app.altDomain || config.appFqdn(app.location);
|
||||
var stdEnv = [
|
||||
'CLOUDRON=1',
|
||||
'WEBADMIN_ORIGIN=' + config.adminOrigin(),
|
||||
@@ -163,10 +163,6 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
||||
memoryLimit = constants.DEFAULT_MEMORY_LIMIT;
|
||||
}
|
||||
|
||||
// give scheduler tasks twice the memory limit since background jobs take more memory
|
||||
// if required, we can make this a manifest and runtime argument later
|
||||
if (!isAppContainer) memoryLimit *= 2;
|
||||
|
||||
// apparmor is disabled on few servers
|
||||
var enableSecurityOpt = config.CLOUDRON && safe(function () { return child_process.spawnSync('aa-enabled').status === 0; }, false);
|
||||
|
||||
@@ -190,7 +186,7 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
||||
'/run': {}
|
||||
},
|
||||
Labels: {
|
||||
'fqdn': app.fqdn,
|
||||
'location': app.location,
|
||||
'appId': app.id,
|
||||
'isSubcontainer': String(!isAppContainer)
|
||||
},
|
||||
|
||||
-119
@@ -1,119 +0,0 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
add: add,
|
||||
get: get,
|
||||
getAll: getAll,
|
||||
update: update,
|
||||
del: del,
|
||||
|
||||
_clear: clear
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
database = require('./database.js'),
|
||||
DatabaseError = require('./databaseerror'),
|
||||
safe = require('safetydance');
|
||||
|
||||
var DOMAINS_FIELDS = [ 'domain', 'zoneName', 'provider', 'configJson', 'tlsConfigJson' ].join(',');
|
||||
|
||||
function postProcess(data) {
|
||||
data.config = safe.JSON.parse(data.configJson);
|
||||
data.tlsConfig = safe.JSON.parse(data.tlsConfigJson);
|
||||
delete data.configJson;
|
||||
delete data.tlsConfigJson;
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function get(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query(`SELECT ${DOMAINS_FIELDS} FROM domains WHERE domain=?`, [ domain ], function (error, result) {
|
||||
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 ${DOMAINS_FIELDS} 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(name, domain, callback) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof domain, 'object');
|
||||
assert.strictEqual(typeof domain.zoneName, 'string');
|
||||
assert.strictEqual(typeof domain.provider, 'string');
|
||||
assert.strictEqual(typeof domain.config, 'object');
|
||||
assert.strictEqual(typeof domain.tlsConfig, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('INSERT INTO domains (domain, zoneName, provider, configJson, tlsConfigJson) VALUES (?, ?, ?, ?, ?)', [ name, domain.zoneName, domain.provider, JSON.stringify(domain.config), JSON.stringify(domain.tlsConfig) ], function (error) {
|
||||
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 update(name, domain, callback) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof domain, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var args = [ ], fields = [ ];
|
||||
for (var k in domain) {
|
||||
if (k === 'config') {
|
||||
fields.push('configJson = ?');
|
||||
args.push(JSON.stringify(domain[k]));
|
||||
} else if (k === 'tlsConfig') {
|
||||
fields.push('tlsConfigJson = ?');
|
||||
args.push(JSON.stringify(domain[k]));
|
||||
} else {
|
||||
fields.push(k + ' = ?');
|
||||
args.push(domain[k]);
|
||||
}
|
||||
}
|
||||
args.push(name);
|
||||
|
||||
database.query('UPDATE domains SET ' + fields.join(', ') + ' WHERE domain=?', args, 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 === 0) 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);
|
||||
});
|
||||
}
|
||||
-359
@@ -1,359 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = exports = {
|
||||
add: add,
|
||||
get: get,
|
||||
getAll: getAll,
|
||||
update: update,
|
||||
del: del,
|
||||
|
||||
fqdn: fqdn,
|
||||
setAdmin: setAdmin,
|
||||
|
||||
getDnsRecords: getDnsRecords,
|
||||
upsertDnsRecords: upsertDnsRecords,
|
||||
removeDnsRecords: removeDnsRecords,
|
||||
|
||||
waitForDnsRecord: waitForDnsRecord,
|
||||
|
||||
DomainError: DomainError
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
caas = require('./caas.js'),
|
||||
config = require('./config.js'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
debug = require('debug')('box:domains'),
|
||||
domaindb = require('./domaindb.js'),
|
||||
path = require('path'),
|
||||
reverseProxy = require('./reverseproxy.js'),
|
||||
ReverseProxyError = reverseProxy.ReverseProxyError,
|
||||
safe = require('safetydance'),
|
||||
shell = require('./shell.js'),
|
||||
sysinfo = require('./sysinfo.js'),
|
||||
tld = require('tldjs'),
|
||||
util = require('util');
|
||||
|
||||
var RESTART_CMD = path.join(__dirname, 'scripts/restart.sh');
|
||||
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
function verifyDnsConfig(config, domain, zoneName, provider, ip, callback) {
|
||||
assert(config && typeof config === 'object'); // the dns config to test with
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof provider, 'string');
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var backend = api(provider);
|
||||
if (!backend) return callback(new DomainError(DomainError.INVALID_PROVIDER));
|
||||
|
||||
api(provider).verifyDnsConfig(config, domain, zoneName, ip, callback);
|
||||
}
|
||||
|
||||
|
||||
function add(domain, zoneName, provider, config, fallbackCertificate, tlsConfig, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof provider, 'string');
|
||||
assert.strictEqual(typeof config, 'object');
|
||||
assert.strictEqual(typeof fallbackCertificate, 'object');
|
||||
assert.strictEqual(typeof tlsConfig, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!tld.isValid(domain)) return callback(new DomainError(DomainError.BAD_FIELD, 'Invalid domain'));
|
||||
|
||||
if (zoneName) {
|
||||
if (!tld.isValid(zoneName)) return callback(new DomainError(DomainError.BAD_FIELD, 'Invalid zoneName'));
|
||||
} else {
|
||||
zoneName = tld.getDomain(domain) || domain;
|
||||
}
|
||||
|
||||
if (fallbackCertificate) {
|
||||
let error = reverseProxy.validateCertificate(`test.${domain}`, fallbackCertificate.cert, fallbackCertificate.key);
|
||||
if (error) return callback(new DomainError(DomainError.BAD_FIELD, error.message));
|
||||
}
|
||||
|
||||
if (tlsConfig.provider !== 'fallback' && tlsConfig.provider !== 'caas' && tlsConfig.provider.indexOf('letsencrypt-') !== 0) {
|
||||
return callback(new DomainError(DomainError.BAD_FIELD, 'tlsConfig.provider must be caas, fallback or le-*'));
|
||||
}
|
||||
|
||||
sysinfo.getPublicIp(function (error, ip) {
|
||||
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, 'Error getting IP:' + error.message));
|
||||
|
||||
verifyDnsConfig(config, domain, zoneName, provider, 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: zoneName, provider: provider, config: result, tlsConfig: tlsConfig }, function (error) {
|
||||
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new DomainError(DomainError.ALREADY_EXISTS));
|
||||
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
|
||||
|
||||
reverseProxy.setFallbackCertificate(domain, fallbackCertificate, function (error) {
|
||||
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
|
||||
|
||||
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));
|
||||
|
||||
reverseProxy.getFallbackCertificate(domain, function (error, bundle) {
|
||||
if (error && error.reason !== ReverseProxyError.NOT_FOUND) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
|
||||
|
||||
var cert = safe.fs.readFileSync(bundle.certFilePath, 'utf-8');
|
||||
var key = safe.fs.readFileSync(bundle.keyFilePath, 'utf-8');
|
||||
|
||||
if (!cert || !key) return callback(new DomainError(DomainError.INTERNAL_ERROR, 'unable to read certificates from disk'));
|
||||
|
||||
result.fallbackCertificate = { cert: cert, key: key };
|
||||
|
||||
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, provider, config, fallbackCertificate, tlsConfig, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof provider, 'string');
|
||||
assert.strictEqual(typeof config, 'object');
|
||||
assert.strictEqual(typeof fallbackCertificate, 'object');
|
||||
assert.strictEqual(typeof tlsConfig, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
domaindb.get(domain, function (error, result) {
|
||||
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 = reverseProxy.validateCertificate(`test.${domain}`, fallbackCertificate.cert, fallbackCertificate.key);
|
||||
if (error) return callback(new DomainError(DomainError.BAD_FIELD, error.message));
|
||||
}
|
||||
|
||||
if (tlsConfig.provider !== 'fallback' && tlsConfig.provider !== 'caas' && tlsConfig.provider.indexOf('letsencrypt-') !== 0) {
|
||||
return callback(new DomainError(DomainError.BAD_FIELD, 'tlsConfig.provider must be caas, fallback or letsencrypt-*'));
|
||||
}
|
||||
|
||||
sysinfo.getPublicIp(function (error, ip) {
|
||||
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, 'Error getting IP:' + error.message));
|
||||
|
||||
verifyDnsConfig(config, domain, result.zoneName, provider, 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, { provider: provider, config: result, tlsConfig: tlsConfig }, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DomainError(DomainError.NOT_FOUND));
|
||||
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
|
||||
|
||||
if (!fallbackCertificate) return callback();
|
||||
|
||||
reverseProxy.setFallbackCertificate(domain, fallbackCertificate, function (error) {
|
||||
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 getName(domain, subdomain) {
|
||||
// support special caas domains
|
||||
if (domain.provider === 'caas') return subdomain;
|
||||
|
||||
if (domain.domain === domain.zoneName) return subdomain;
|
||||
|
||||
var part = domain.domain.slice(0, -domain.zoneName.length - 1);
|
||||
|
||||
return subdomain === '' ? part : subdomain + '.' + part;
|
||||
}
|
||||
|
||||
function getDnsRecords(subdomain, domain, type, callback) {
|
||||
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.provider).get(result.config, result.zoneName, getName(result, 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.provider).upsert(result.config, result.zoneName, getName(result, 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(error);
|
||||
|
||||
api(result.provider).del(result.config, result.zoneName, getName(result, subdomain), type, values, function (error) {
|
||||
if (error && error.reason !== DomainError.NOT_FOUND) return callback(error);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// only wait for A record
|
||||
function waitForDnsRecord(fqdn, domain, value, options, callback) {
|
||||
assert.strictEqual(typeof fqdn, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof value, 'string');
|
||||
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
get(domain, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
api(result.provider).waitForDns(fqdn, result ? result.zoneName : domain, value, options, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function setAdmin(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('setAdmin domain:%s', domain);
|
||||
|
||||
get(domain, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var setPtrRecord = config.provider() === 'caas' ? caas.setPtrRecord : function (d, next) { next(); };
|
||||
|
||||
setPtrRecord(domain, function (error) {
|
||||
if (error) return callback(new DomainError(DomainError.EXTERNAL_ERROR, 'Error setting PTR record:' + error.message));
|
||||
|
||||
config.setAdminDomain(result.domain);
|
||||
config.setAdminLocation('my');
|
||||
config.setAdminFqdn('my' + (result.provider === 'caas' ? '-' : '.') + result.domain);
|
||||
|
||||
callback();
|
||||
|
||||
shell.sudo('restart', [ RESTART_CMD ], NOOP_CALLBACK);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function fqdn(location, domain, provider) {
|
||||
return location + (location ? (provider !== 'caas' ? '.' : '-') : '') + domain;
|
||||
}
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
sync: sync
|
||||
};
|
||||
|
||||
var appdb = require('./appdb.js'),
|
||||
apps = require('./apps.js'),
|
||||
async = require('async'),
|
||||
config = require('./config.js'),
|
||||
debug = require('debug')('box:dyndns'),
|
||||
domains = require('./domains.js'),
|
||||
sysinfo = require('./sysinfo.js');
|
||||
|
||||
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
|
||||
// called for dynamic dns setups where we have to update the IP
|
||||
function sync(callback) {
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
|
||||
sysinfo.getPublicIp(function (error, ip) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('refreshDNS: current ip %s', ip);
|
||||
|
||||
domains.upsertDnsRecords(config.adminLocation(), config.adminDomain(), 'A', [ ip ], function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('refreshDNS: done for admin location');
|
||||
|
||||
apps.getAll(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.each(result, function (app, callback) {
|
||||
// do not change state of installing apps since apptask will error if dns record already exists
|
||||
if (app.installationState !== appdb.ISTATE_INSTALLED) return callback();
|
||||
|
||||
domains.upsertDnsRecords(app.location, app.domain, 'A', [ ip ], callback);
|
||||
}, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('refreshDNS: done for apps');
|
||||
|
||||
callback();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
+402
@@ -0,0 +1,402 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
verifyRelay: verifyRelay,
|
||||
getStatus: getStatus,
|
||||
checkRblStatus: checkRblStatus,
|
||||
|
||||
EmailError: EmailError
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
cloudron = require('./cloudron.js'),
|
||||
config = require('./config.js'),
|
||||
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'),
|
||||
settings = require('./settings.js'),
|
||||
smtpTransport = require('nodemailer-smtp-transport'),
|
||||
sysinfo = require('./sysinfo.js'),
|
||||
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) {
|
||||
assert.strictEqual(typeof reason, 'string');
|
||||
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
|
||||
|
||||
Error.call(this);
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
|
||||
this.name = this.constructor.name;
|
||||
this.reason = reason;
|
||||
if (typeof errorOrMessage === 'undefined') {
|
||||
this.message = reason;
|
||||
} else if (typeof errorOrMessage === 'string') {
|
||||
this.message = errorOrMessage;
|
||||
} else {
|
||||
this.message = 'Internal error';
|
||||
this.nestedError = errorOrMessage;
|
||||
}
|
||||
}
|
||||
util.inherits(EmailError, Error);
|
||||
EmailError.INTERNAL_ERROR = 'Internal Error';
|
||||
EmailError.BAD_FIELD = 'Bad Field';
|
||||
|
||||
function checkOutboundPort25(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var smtpServer = _.sample([
|
||||
'smtp.gmail.com',
|
||||
'smtp.live.com',
|
||||
'smtp.mail.yahoo.com',
|
||||
'smtp.o2.ie',
|
||||
'smtp.comcast.net',
|
||||
'outgoing.verizon.net'
|
||||
]);
|
||||
|
||||
var relay = {
|
||||
value: 'OK',
|
||||
status: false
|
||||
};
|
||||
|
||||
var client = new net.Socket();
|
||||
client.setTimeout(5000);
|
||||
client.connect(25, smtpServer);
|
||||
client.on('connect', function () {
|
||||
relay.status = true;
|
||||
relay.value = 'OK';
|
||||
client.destroy(); // do not use end() because it still triggers timeout
|
||||
callback(null, relay);
|
||||
});
|
||||
client.on('timeout', function () {
|
||||
relay.status = false;
|
||||
relay.value = 'Connect to ' + smtpServer + ' timed out';
|
||||
client.destroy();
|
||||
callback(new Error('Timeout'), relay);
|
||||
});
|
||||
client.on('error', function (error) {
|
||||
relay.status = false;
|
||||
relay.value = 'Connect to ' + smtpServer + ' failed: ' + error.message;
|
||||
client.destroy();
|
||||
callback(error, relay);
|
||||
});
|
||||
}
|
||||
|
||||
function checkSmtpRelay(relay, callback) {
|
||||
var result = {
|
||||
value: 'OK',
|
||||
status: false
|
||||
};
|
||||
|
||||
var transporter = nodemailer.createTransport(smtpTransport({
|
||||
host: relay.host,
|
||||
port: relay.port,
|
||||
auth: {
|
||||
user: relay.username,
|
||||
pass: relay.password
|
||||
}
|
||||
}));
|
||||
|
||||
transporter.verify(function(error) {
|
||||
result.status = !error;
|
||||
if (error) {
|
||||
result.value = error.message;
|
||||
return callback(error, result);
|
||||
}
|
||||
|
||||
callback(null, result);
|
||||
});
|
||||
}
|
||||
|
||||
function verifyRelay(relay, callback) {
|
||||
assert.strictEqual(typeof relay, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var verifier = relay.provider === 'cloudron-smtp' ? checkOutboundPort25 : checkSmtpRelay.bind(null, relay);
|
||||
|
||||
verifier(function (error) {
|
||||
if (error) return callback(new EmailError(EmailError.BAD_FIELD, error.message));
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
function checkDkim(callback) {
|
||||
var dkim = {
|
||||
domain: config.dkimSelector() + '._domainkey.' + config.fqdn(),
|
||||
type: 'TXT',
|
||||
expected: null,
|
||||
value: null,
|
||||
status: false
|
||||
};
|
||||
|
||||
var dkimKey = cloudron.readDkimPublicKeySync();
|
||||
if (!dkimKey) return callback(new Error('Failed to read dkim public key'), dkim);
|
||||
|
||||
dkim.expected = '"v=DKIM1; t=s; p=' + dkimKey + '"';
|
||||
|
||||
dig.resolve(dkim.domain, dkim.type, digOptions, function (error, txtRecords) {
|
||||
if (error && error.code === 'ENOTFOUND') return callback(null, dkim); // not setup
|
||||
if (error) return callback(error, dkim);
|
||||
|
||||
if (Array.isArray(txtRecords) && txtRecords.length !== 0) {
|
||||
dkim.value = txtRecords[0];
|
||||
dkim.status = (dkim.value === dkim.expected);
|
||||
}
|
||||
|
||||
callback(null, dkim);
|
||||
});
|
||||
}
|
||||
|
||||
function checkSpf(callback) {
|
||||
var spf = {
|
||||
domain: config.fqdn(),
|
||||
type: 'TXT',
|
||||
value: null,
|
||||
expected: '"v=spf1 a:' + config.adminFqdn() + ' ~all"',
|
||||
status: false
|
||||
};
|
||||
|
||||
// https://agari.zendesk.com/hc/en-us/articles/202952749-How-long-can-my-SPF-record-be-
|
||||
dig.resolve(spf.domain, spf.type, digOptions, function (error, txtRecords) {
|
||||
if (error && error.code === 'ENOTFOUND') return callback(null, spf); // not setup
|
||||
if (error) return callback(error, spf);
|
||||
|
||||
if (!Array.isArray(txtRecords)) return callback(null, spf);
|
||||
|
||||
var i;
|
||||
for (i = 0; i < txtRecords.length; i++) {
|
||||
if (txtRecords[i].indexOf('"v=spf1 ') !== 0) continue; // not SPF
|
||||
spf.value = txtRecords[i];
|
||||
spf.status = spf.value.indexOf(' a:' + config.adminFqdn()) !== -1;
|
||||
break;
|
||||
}
|
||||
|
||||
if (spf.status) {
|
||||
spf.expected = spf.value;
|
||||
} else if (i !== txtRecords.length) {
|
||||
spf.expected = '"v=spf1 a:' + config.adminFqdn() + ' ' + spf.value.slice('"v=spf1 '.length);
|
||||
}
|
||||
|
||||
callback(null, spf);
|
||||
});
|
||||
}
|
||||
|
||||
function checkMx(callback) {
|
||||
var mx = {
|
||||
domain: config.fqdn(),
|
||||
type: 'MX',
|
||||
value: null,
|
||||
expected: '10 ' + config.mailFqdn() + '.',
|
||||
status: false
|
||||
};
|
||||
|
||||
dig.resolve(mx.domain, mx.type, digOptions, function (error, mxRecords) {
|
||||
if (error && error.code === 'ENOTFOUND') return callback(null, mx); // not setup
|
||||
if (error) return callback(error, mx);
|
||||
|
||||
if (Array.isArray(mxRecords) && mxRecords.length !== 0) {
|
||||
mx.status = mxRecords.length == 1 && mxRecords[0].exchange === (config.mailFqdn() + '.');
|
||||
mx.value = mxRecords.map(function (r) { return r.priority + ' ' + r.exchange; }).join(' ');
|
||||
}
|
||||
|
||||
callback(null, mx);
|
||||
});
|
||||
}
|
||||
|
||||
function checkDmarc(callback) {
|
||||
var dmarc = {
|
||||
domain: '_dmarc.' + config.fqdn(),
|
||||
type: 'TXT',
|
||||
value: null,
|
||||
expected: '"v=DMARC1; p=reject; pct=100"',
|
||||
status: false
|
||||
};
|
||||
|
||||
dig.resolve(dmarc.domain, dmarc.type, digOptions, function (error, txtRecords) {
|
||||
if (error && error.code === 'ENOTFOUND') return callback(null, dmarc); // not setup
|
||||
if (error) return callback(error, dmarc);
|
||||
|
||||
if (Array.isArray(txtRecords) && txtRecords.length !== 0) {
|
||||
dmarc.value = txtRecords[0];
|
||||
dmarc.status = (dmarc.value === dmarc.expected);
|
||||
}
|
||||
|
||||
callback(null, dmarc);
|
||||
});
|
||||
}
|
||||
|
||||
function checkPtr(callback) {
|
||||
var ptr = {
|
||||
domain: null,
|
||||
type: 'PTR',
|
||||
value: null,
|
||||
expected: config.mailFqdn() + '.',
|
||||
status: false
|
||||
};
|
||||
|
||||
sysinfo.getPublicIp(function (error, ip) {
|
||||
if (error) return callback(error, ptr);
|
||||
|
||||
ptr.domain = ip.split('.').reverse().join('.') + '.in-addr.arpa';
|
||||
|
||||
dig.resolve(ip, 'PTR', digOptions, function (error, ptrRecords) {
|
||||
if (error && error.code === 'ENOTFOUND') return callback(null, ptr); // not setup
|
||||
if (error) return callback(error, ptr);
|
||||
|
||||
if (Array.isArray(ptrRecords) && ptrRecords.length !== 0) {
|
||||
ptr.value = ptrRecords.join(' ');
|
||||
ptr.status = ptrRecords.some(function (v) { return v === ptr.expected; });
|
||||
}
|
||||
|
||||
return callback(null, ptr);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// https://raw.githubusercontent.com/jawsome/node-dnsbl/master/list.json
|
||||
const RBL_LIST = [
|
||||
{
|
||||
"name": "Barracuda",
|
||||
"dns": "b.barracudacentral.org",
|
||||
"site": "http://www.barracudacentral.org/rbl/removal-request"
|
||||
},
|
||||
{
|
||||
"name": "SpamCop",
|
||||
"dns": "bl.spamcop.net",
|
||||
"site": "http://spamcop.net"
|
||||
},
|
||||
{
|
||||
"name": "Sorbs Aggregate Zone",
|
||||
"dns": "dnsbl.sorbs.net",
|
||||
"site": "http://dnsbl.sorbs.net/"
|
||||
},
|
||||
{
|
||||
"name": "Sorbs spam.dnsbl Zone",
|
||||
"dns": "spam.dnsbl.sorbs.net",
|
||||
"site": "http://sorbs.net"
|
||||
},
|
||||
{
|
||||
"name": "Composite Blocking List",
|
||||
"dns": "cbl.abuseat.org",
|
||||
"site": "http://www.abuseat.org"
|
||||
},
|
||||
{
|
||||
"name": "SpamHaus Zen",
|
||||
"dns": "zen.spamhaus.org",
|
||||
"site": "http://spamhaus.org"
|
||||
},
|
||||
{
|
||||
"name": "Multi SURBL",
|
||||
"dns": "multi.surbl.org",
|
||||
"site": "http://www.surbl.org"
|
||||
},
|
||||
{
|
||||
"name": "Spam Cannibal",
|
||||
"dns": "bl.spamcannibal.org",
|
||||
"site": "http://www.spamcannibal.org/cannibal.cgi"
|
||||
},
|
||||
{
|
||||
"name": "dnsbl.abuse.ch",
|
||||
"dns": "spam.abuse.ch",
|
||||
"site": "http://dnsbl.abuse.ch/"
|
||||
},
|
||||
{
|
||||
"name": "The Unsubscribe Blacklist(UBL)",
|
||||
"dns": "ubl.unsubscore.com ",
|
||||
"site": "http://www.lashback.com/blacklist/"
|
||||
},
|
||||
{
|
||||
"name": "UCEPROTECT Network",
|
||||
"dns": "dnsbl-1.uceprotect.net",
|
||||
"site": "http://www.uceprotect.net/en"
|
||||
}
|
||||
];
|
||||
|
||||
function checkRblStatus(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
sysinfo.getPublicIp(function (error, ip) {
|
||||
if (error) return callback(error, ip);
|
||||
|
||||
var flippedIp = ip.split('.').reverse().join('.');
|
||||
|
||||
// https://tools.ietf.org/html/rfc5782
|
||||
async.map(RBL_LIST, function (rblServer, iteratorDone) {
|
||||
dig.resolve(flippedIp + '.' + rblServer.dns, 'A', digOptions, function (error, records) {
|
||||
if (error || !records) return iteratorDone(null, null); // not listed
|
||||
|
||||
debug('checkRblStatus: %s (ip: %s) is in the blacklist of %j', config.fqdn(), flippedIp, rblServer);
|
||||
|
||||
var result = _.extend({ }, rblServer);
|
||||
|
||||
dig.resolve(flippedIp + '.' + rblServer.dns, 'TXT', digOptions, function (error, txtRecords) {
|
||||
result.txtRecords = error || !txtRecords ? 'No txt record' : txtRecords;
|
||||
|
||||
debug('checkRblStatus: %s (error: %s) (txtRecords: %j)', config.fqdn(), error, txtRecords);
|
||||
|
||||
return iteratorDone(null, result);
|
||||
});
|
||||
});
|
||||
}, function (ignoredError, blacklistedServers) {
|
||||
blacklistedServers = blacklistedServers.filter(function(b) { return b !== null; });
|
||||
|
||||
debug('checkRblStatus: %s (ip: %s) servers: %j', config.fqdn(), ip, blacklistedServers);
|
||||
|
||||
return callback(null, { status: blacklistedServers.length === 0, ip: ip, servers: blacklistedServers });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getStatus(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var results = {};
|
||||
|
||||
function recordResult(what, func) {
|
||||
return function (callback) {
|
||||
func(function (error, result) {
|
||||
if (error) debug('Ignored error - ' + what + ':', error);
|
||||
|
||||
safe.set(results, what, result);
|
||||
|
||||
callback();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
settings.getMailRelay(function (error, relay) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var checks = [
|
||||
recordResult('dns.mx', checkMx),
|
||||
recordResult('dns.dmarc', checkDmarc)
|
||||
];
|
||||
|
||||
if (relay.provider === 'cloudron-smtp') {
|
||||
// these tests currently only make sense when using Cloudron's SMTP server at this point
|
||||
checks.push(
|
||||
recordResult('dns.spf', checkSpf),
|
||||
recordResult('dns.dkim', checkDkim),
|
||||
recordResult('dns.ptr', checkPtr),
|
||||
recordResult('relay', checkOutboundPort25),
|
||||
recordResult('rbl', checkRblStatus)
|
||||
);
|
||||
} else {
|
||||
checks.push(recordResult('relay', checkSmtpRelay.bind(null, relay)));
|
||||
}
|
||||
|
||||
async.parallel(checks, function () {
|
||||
callback(null, results);
|
||||
});
|
||||
});
|
||||
}
|
||||
+4
-3
@@ -22,6 +22,7 @@ exports = module.exports = {
|
||||
ACTION_BACKUP_START: 'backup.start',
|
||||
ACTION_BACKUP_CLEANUP: 'backup.cleanup',
|
||||
ACTION_CERTIFICATE_RENEWAL: 'certificate.renew',
|
||||
ACTION_CLI_MODE: 'settings.climode',
|
||||
ACTION_START: 'cloudron.start',
|
||||
ACTION_UPDATE: 'cloudron.update',
|
||||
ACTION_USER_ADD: 'user.add',
|
||||
@@ -90,14 +91,14 @@ function get(id, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getAllPaged(actions, search, page, perPage, callback) {
|
||||
assert(Array.isArray(actions));
|
||||
function getAllPaged(action, search, page, perPage, callback) {
|
||||
assert(typeof action === 'string' || action === null);
|
||||
assert(typeof search === 'string' || search === null);
|
||||
assert.strictEqual(typeof page, 'number');
|
||||
assert.strictEqual(typeof perPage, 'number');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
eventlogdb.getAllPaged(actions, search, page, perPage, function (error, events) {
|
||||
eventlogdb.getAllPaged(action, search, page, perPage, function (error, events) {
|
||||
if (error) return callback(new EventLogError(EventLogError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, events);
|
||||
|
||||
+8
-9
@@ -40,8 +40,8 @@ function get(eventId, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getAllPaged(actions, search, page, perPage, callback) {
|
||||
assert(Array.isArray(actions));
|
||||
function getAllPaged(action, search, page, perPage, callback) {
|
||||
assert(typeof action === 'string' || action === null);
|
||||
assert(typeof search === 'string' || search === null);
|
||||
assert.strictEqual(typeof page, 'number');
|
||||
assert.strictEqual(typeof perPage, 'number');
|
||||
@@ -50,15 +50,14 @@ function getAllPaged(actions, search, page, perPage, callback) {
|
||||
var data = [];
|
||||
var query = 'SELECT ' + EVENTLOGS_FIELDS + ' FROM eventlog';
|
||||
|
||||
if (actions.length || search) query += ' WHERE';
|
||||
if (action || search) query += ' WHERE';
|
||||
if (search) query += ' (source LIKE ' + mysql.escape('%' + search + '%') + ' OR data LIKE ' + mysql.escape('%' + search + '%') + ')';
|
||||
if (action && search) query += ' AND ';
|
||||
|
||||
if (actions.length && search) query += ' AND ( ';
|
||||
actions.forEach(function (action, i) {
|
||||
query += ' (action LIKE ' + mysql.escape(`%${action}%`) + ') ';
|
||||
if (i < actions.length-1) query += ' OR ';
|
||||
});
|
||||
if (actions.length && search) query += ' ) ';
|
||||
if (action) {
|
||||
query += ' action=?';
|
||||
data.push(action);
|
||||
}
|
||||
|
||||
query += ' ORDER BY creationTime DESC LIMIT ?,?';
|
||||
|
||||
|
||||
+11
-3
@@ -25,7 +25,8 @@ exports = module.exports = {
|
||||
var assert = require('assert'),
|
||||
constants = require('./constants.js'),
|
||||
database = require('./database.js'),
|
||||
DatabaseError = require('./databaseerror');
|
||||
DatabaseError = require('./databaseerror'),
|
||||
mailboxdb = require('./mailboxdb.js');
|
||||
|
||||
var GROUPS_FIELDS = [ 'id', 'name' ].join(',');
|
||||
|
||||
@@ -87,9 +88,15 @@ function add(id, name, callback) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('INSERT INTO groups (id, name) VALUES (?, ?)', [ id, name ], function (error, result) {
|
||||
var 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 groups (id, name) VALUES (?, ?)', args: [ id, name ] });
|
||||
|
||||
database.transaction(queries, function (error, result) {
|
||||
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error));
|
||||
if (error || result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (error || result[1].affectedRows !== 1) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
@@ -103,6 +110,7 @@ function del(id, callback) {
|
||||
var queries = [];
|
||||
queries.push({ query: 'DELETE FROM groupMembers WHERE groupId = ?', args: [ id ] });
|
||||
queries.push({ query: 'DELETE FROM groups WHERE id = ?', args: [ id ] });
|
||||
queries.push({ query: 'DELETE FROM mailboxes WHERE ownerId=?', args: [ id ] });
|
||||
|
||||
database.transaction(queries, function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
+3
-8
@@ -24,7 +24,6 @@ var assert = require('assert'),
|
||||
constants = require('./constants.js'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
groupdb = require('./groupdb.js'),
|
||||
mailboxdb = require('./mailboxdb.js'),
|
||||
util = require('util'),
|
||||
uuid = require('uuid');
|
||||
|
||||
@@ -100,15 +99,11 @@ function remove(id, callback) {
|
||||
// never allow admin group to be deleted
|
||||
if (id === constants.ADMIN_GROUP_ID) return callback(new GroupError(GroupError.NOT_ALLOWED));
|
||||
|
||||
mailboxdb.delByOwnerId(id, function (error) {
|
||||
groupdb.del(id, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupError(GroupError.NOT_FOUND));
|
||||
if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, error));
|
||||
|
||||
groupdb.del(id, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupError(GroupError.NOT_FOUND));
|
||||
if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -7,18 +7,18 @@
|
||||
exports = module.exports = {
|
||||
// a major version makes all apps restore from backup. #451 must be fixed before we do this.
|
||||
// a minor version makes all apps re-configure themselves
|
||||
'version': '48.9.0',
|
||||
'version': '48.8.0',
|
||||
|
||||
'baseImages': [ 'cloudron/base:0.10.0' ],
|
||||
|
||||
// Note that if any of the databases include an upgrade, bump the infra version above
|
||||
// This is because we upgrade using dumps instead of mysql_upgrade, pg_upgrade etc
|
||||
'images': {
|
||||
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:1.0.0' },
|
||||
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:1.0.0' },
|
||||
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:1.0.1' },
|
||||
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:1.0.0' },
|
||||
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:1.2.0' },
|
||||
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:1.0.0' }
|
||||
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:0.18.0' },
|
||||
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:0.17.1' },
|
||||
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:0.13.0' },
|
||||
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:0.11.0' },
|
||||
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:0.39.1' },
|
||||
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:0.12.0' }
|
||||
}
|
||||
};
|
||||
|
||||
+48
-65
@@ -16,8 +16,6 @@ var assert = require('assert'),
|
||||
user = require('./user.js'),
|
||||
UserError = user.UserError,
|
||||
ldap = require('ldapjs'),
|
||||
mail = require('./mail.js'),
|
||||
MailError = mail.MailError,
|
||||
mailboxdb = require('./mailboxdb.js'),
|
||||
safe = require('safetydance');
|
||||
|
||||
@@ -154,7 +152,7 @@ function userSearch(req, res, next) {
|
||||
cn: entry.id,
|
||||
uid: entry.id,
|
||||
mail: entry.email,
|
||||
mailAlternateAddress: entry.fallbackEmail,
|
||||
mailAlternateAddress: entry.alternateEmail,
|
||||
displayname: displayName,
|
||||
givenName: firstName,
|
||||
username: entry.username,
|
||||
@@ -260,11 +258,14 @@ function mailboxSearch(req, res, next) {
|
||||
|
||||
if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
|
||||
var email = req.dn.rdns[0].attrs.cn.value.toLowerCase();
|
||||
var parts = email.split('@');
|
||||
if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
var name = req.dn.rdns[0].attrs.cn.value.toLowerCase();
|
||||
// allow login via email
|
||||
var parts = name.split('@');
|
||||
if (parts[1] === config.fqdn()) {
|
||||
name = parts[0];
|
||||
}
|
||||
|
||||
mailboxdb.getMailbox(parts[0], parts[1], function (error, mailbox) {
|
||||
mailboxdb.getMailbox(name, function (error, mailbox) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
if (error) return next(new ldap.OperationsError(error.toString()));
|
||||
|
||||
@@ -273,9 +274,9 @@ function mailboxSearch(req, res, next) {
|
||||
attributes: {
|
||||
objectclass: ['mailbox'],
|
||||
objectcategory: 'mailbox',
|
||||
cn: `${mailbox.name}@${mailbox.domain}`,
|
||||
uid: `${mailbox.name}@${mailbox.domain}`,
|
||||
mail: `${mailbox.name}@${mailbox.domain}`,
|
||||
cn: mailbox.name,
|
||||
uid: mailbox.name,
|
||||
mail: mailbox.name + '@' + config.fqdn(),
|
||||
ownerType: mailbox.ownerType
|
||||
}
|
||||
};
|
||||
@@ -297,24 +298,19 @@ function mailAliasSearch(req, res, next) {
|
||||
|
||||
if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
|
||||
var email = req.dn.rdns[0].attrs.cn.value.toLowerCase();
|
||||
var parts = email.split('@');
|
||||
if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
|
||||
mailboxdb.getAlias(parts[0], parts[1], function (error, alias) {
|
||||
mailboxdb.getAlias(req.dn.rdns[0].attrs.cn.value.toLowerCase(), function (error, alias) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
if (error) return next(new ldap.OperationsError(error.toString()));
|
||||
|
||||
// https://wiki.debian.org/LDAP/MigrationTools/Examples
|
||||
// https://docs.oracle.com/cd/E19455-01/806-5580/6jej518pp/index.html
|
||||
// member is fully qualified - https://docs.oracle.com/cd/E19957-01/816-6082-10/chap4.doc.html#43314
|
||||
var obj = {
|
||||
dn: req.dn.toString(),
|
||||
attributes: {
|
||||
objectclass: ['nisMailAlias'],
|
||||
objectcategory: 'nisMailAlias',
|
||||
cn: `${alias.name}@${alias.domain}`,
|
||||
rfc822MailMember: `${alias.aliasTarget}@${alias.domain}`
|
||||
cn: alias.name,
|
||||
rfc822MailMember: alias.aliasTarget
|
||||
}
|
||||
};
|
||||
|
||||
@@ -335,24 +331,19 @@ function mailingListSearch(req, res, next) {
|
||||
|
||||
if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
|
||||
var email = req.dn.rdns[0].attrs.cn.value.toLowerCase();
|
||||
var parts = email.split('@');
|
||||
if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
|
||||
mailboxdb.getGroup(parts[0], parts[1], function (error, group) {
|
||||
mailboxdb.getGroup(req.dn.rdns[0].attrs.cn.value.toLowerCase(), function (error, group) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
if (error) return next(new ldap.OperationsError(error.toString()));
|
||||
|
||||
// http://ldapwiki.willeke.com/wiki/Original%20Mailgroup%20Schema%20From%20Netscape
|
||||
// members are fully qualified (https://docs.oracle.com/cd/E19444-01/816-6018-10/groups.htm#13356)
|
||||
var obj = {
|
||||
dn: req.dn.toString(),
|
||||
attributes: {
|
||||
objectclass: ['mailGroup'],
|
||||
objectcategory: 'mailGroup',
|
||||
cn: `${group.name}@${group.domain}`, // fully qualified
|
||||
mail: `${group.name}@${group.domain}`,
|
||||
mgrpRFC822MailMember: group.members.map(function (m) { return `${m}@${group.domain}`; })
|
||||
cn: group.name,
|
||||
mail: group.name + '@' + config.fqdn(),
|
||||
mgrpRFC822MailMember: group.members
|
||||
}
|
||||
};
|
||||
|
||||
@@ -410,7 +401,7 @@ function authorizeUserForApp(req, res, next) {
|
||||
// we return no such object, to avoid leakage of a users existence
|
||||
if (!result) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
|
||||
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', appId: app.id, app: app }, { userId: req.user.id, user: user.removePrivateFields(req.user) });
|
||||
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', appId: app.id }, { userId: req.user.id });
|
||||
|
||||
res.end();
|
||||
});
|
||||
@@ -418,51 +409,43 @@ function authorizeUserForApp(req, res, next) {
|
||||
}
|
||||
|
||||
function authenticateMailbox(req, res, next) {
|
||||
debug('mailbox auth: %s (from %s)', req.dn.toString(), req.connection.ldap.id);
|
||||
|
||||
if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
|
||||
var email = req.dn.rdns[0].attrs.cn.value.toLowerCase();
|
||||
var parts = email.split('@');
|
||||
if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
var name = req.dn.rdns[0].attrs.cn.value.toLowerCase();
|
||||
|
||||
mailboxdb.getMailbox(parts[0], parts[1], function (error, mailbox) {
|
||||
// allow login via email
|
||||
var parts = name.split('@');
|
||||
if (parts[1] === config.fqdn()) {
|
||||
name = parts[0];
|
||||
}
|
||||
|
||||
mailboxdb.getMailbox(name, function (error, mailbox) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
if (error) return next(new ldap.OperationsError(error.message));
|
||||
|
||||
mail.get(parts[1], function (error, domain) {
|
||||
if (error && error.reason === MailError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
if (error) return next(new ldap.OperationsError(error.message));
|
||||
if (mailbox.ownerType === mailboxdb.TYPE_APP) {
|
||||
var addonId = req.dn.rdns[1].attrs.ou.value.toLowerCase(); // 'sendmail' or 'recvmail'
|
||||
var name;
|
||||
if (addonId === 'sendmail') name = 'MAIL_SMTP_PASSWORD';
|
||||
else if (addonId === 'recvmail') name = 'MAIL_IMAP_PASSWORD';
|
||||
else return next(new ldap.OperationsError('Invalid DN'));
|
||||
|
||||
if (mailbox.ownerType === mailboxdb.TYPE_APP) {
|
||||
var addonId = req.dn.rdns[1].attrs.ou.value.toLowerCase(); // 'sendmail' or 'recvmail'
|
||||
var name;
|
||||
if (addonId === 'sendmail') name = 'MAIL_SMTP_PASSWORD';
|
||||
else if (addonId === 'recvmail') name = 'MAIL_IMAP_PASSWORD';
|
||||
else return next(new ldap.OperationsError('Invalid DN'));
|
||||
appdb.getAddonConfigByName(mailbox.ownerId, addonId, name, function (error, value) {
|
||||
if (error) return next(new ldap.OperationsError(error.message));
|
||||
if (req.credentials !== value) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
|
||||
|
||||
appdb.getAddonConfigByName(mailbox.ownerId, addonId, name, function (error, value) {
|
||||
if (error) return next(new ldap.OperationsError(error.message));
|
||||
if (req.credentials !== value) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
|
||||
|
||||
eventlog.add(eventlog.ACTION_APP_LOGIN, { authType: 'ldap', mailboxId: name }, { appId: mailbox.ownerId, addonId: addonId });
|
||||
return res.end();
|
||||
});
|
||||
} else if (mailbox.ownerType === mailboxdb.TYPE_USER) {
|
||||
if (!domain.enabled) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
|
||||
user.verifyWithUsername(parts[0], req.credentials || '', function (error, result) {
|
||||
if (error && error.reason === UserError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
if (error && error.reason === UserError.WRONG_PASSWORD) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
|
||||
if (error) return next(new ldap.OperationsError(error.message));
|
||||
|
||||
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: email }, { userId: result.id, user: user.removePrivateFields(result) });
|
||||
res.end();
|
||||
});
|
||||
} else {
|
||||
return next(new ldap.OperationsError('Unknown ownerType for mailbox'));
|
||||
}
|
||||
});
|
||||
eventlog.add(eventlog.ACTION_APP_LOGIN, { authType: 'ldap', mailboxId: name }, { appId: mailbox.ownerId, addonId: addonId });
|
||||
return res.end();
|
||||
});
|
||||
} else if (mailbox.ownerType === mailboxdb.TYPE_USER) {
|
||||
authenticateUser(req, res, function (error) {
|
||||
if (error) return next(error);
|
||||
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: name }, { userId: req.user.username });
|
||||
res.end();
|
||||
});
|
||||
} else {
|
||||
return next(new ldap.OperationsError('Unknown ownerType for mailbox'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ 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';
|
||||
|
||||
-1053
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,7 @@ This is most likely a problem in the application.
|
||||
To resolve this, you can try the following:
|
||||
* Restart the app in the app configuration dialog
|
||||
* Restore the app to the latest backup
|
||||
* Contact us via support@cloudron.io or https://forum.cloudron.io
|
||||
* Contact us via support@cloudron.io or https://chat.cloudron.io
|
||||
|
||||
|
||||
Powered by https://cloudron.io
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<%if (format === 'text') { %>
|
||||
|
||||
Dear <%= cloudronName %> Admin,
|
||||
Dear Cloudron Admin,
|
||||
|
||||
creating a backup has failed.
|
||||
creating a backup of <%= fqdn %> has failed.
|
||||
|
||||
-------------------------------------
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Dear <%= cloudronName %> Admin,
|
||||
|
||||
Version <%= newBoxVersion %> is now available!
|
||||
Version <%= newBoxVersion %> for Cloudron <%= fqdn %> is now available!
|
||||
|
||||
Changelog:
|
||||
<% for (var i = 0; i < changelog.length; i++) { %>
|
||||
@@ -27,7 +27,7 @@ Sent at: <%= new Date().toUTCString() %>
|
||||
|
||||
<div style="width: 650px; text-align: left;">
|
||||
<p>
|
||||
Version <b><%= newBoxVersion %></b> is now available!
|
||||
Version <b><%= newBoxVersion %></b> for Cloudron <%= fqdn %> is now available!
|
||||
</p>
|
||||
|
||||
<h5>Changelog:</h5>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Dear <%= cloudronName %> Admin,
|
||||
|
||||
This is a summary of the activities on your Cloudron.
|
||||
This is a summary of the activities on your Cloudron <%= fqdn %>.
|
||||
<% if (info.usersAdded.length) { -%>
|
||||
|
||||
The following users were added:
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<%if (format === 'text') { %>
|
||||
|
||||
Dear <%= cloudronName %> Admin,
|
||||
Dear Cloudron Admin,
|
||||
|
||||
<%= program %> exited unexpectedly using too much memory!
|
||||
<%= program %> on <%= fqdn %> exited unexpectedly using too much memory!
|
||||
|
||||
The app has been restarted now. Should this message appear repeatedly or
|
||||
undefined behavior is observed, give the app more memory.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<%if (format === 'text') { %>
|
||||
|
||||
Dear <%= cloudronName %> Admin,
|
||||
Dear Cloudron Admin,
|
||||
|
||||
your server is running out of disk space.
|
||||
<%= fqdn %> is running out of disk space.
|
||||
|
||||
Disk space logs are attached.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<%if (format === 'text') { %>
|
||||
|
||||
Hi <%= user.displayName || user.username || user.email %>,
|
||||
Hi <%= user.displayName || user.username || user.alternateEmail || user.email %>,
|
||||
|
||||
Someone, hopefully you, has requested your account's password
|
||||
be reset. If you did not request this reset, please ignore this message.
|
||||
@@ -18,7 +18,7 @@ Powered by https://cloudron.io
|
||||
|
||||
<img src="<%= cloudronAvatarUrl %>" width="128px" height="128px"/>
|
||||
|
||||
<h3>Hi <%= user.displayName || user.username || user.email %>,</h3>
|
||||
<h3>Hi <%= user.displayName || user.username || user.alternateEmail || user.email %>,</h3>
|
||||
|
||||
<p>
|
||||
Someone, hopefully you, has requested your account's password be reset.<br/>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<%if (format === 'text') { %>
|
||||
|
||||
Test email from <%= cloudronName %>,
|
||||
Test email from <%= fqdn %>,
|
||||
|
||||
If you can read this, your Cloudron email settings are good.
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"format": "html",
|
||||
"webadminUrl": "https://my.cloudron.io",
|
||||
"fqdn": "my.cloudron.io",
|
||||
"cloudronName": "Smartserver",
|
||||
"cloudronAvatarUrl": "https://cloudron.io/img/logo.png",
|
||||
"info": {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<%if (format === 'text') { %>
|
||||
|
||||
Dear <%= cloudronName %> Admin,
|
||||
Dear Cloudron Admin,
|
||||
|
||||
Unfortunately <%= program %> exited unexpectedly!
|
||||
Unfortunately <%= program %> on <%= fqdn %> exited unexpectedly!
|
||||
|
||||
Please see some excerpt of the logs below:
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Dear <%= cloudronName %> Admin,
|
||||
|
||||
A new user with email <%= user.email %> was added to <%= cloudronName %>.
|
||||
A new user with email <%= user.alternateEmail || user.email %> was added to <%= cloudronName %>.
|
||||
|
||||
<% if (inviteLink) { %>
|
||||
As requested, this user has not been sent an invitation email.
|
||||
@@ -24,7 +24,7 @@ Powered by https://cloudron.io
|
||||
<h3>Dear <%= cloudronName %> Admin,</h3>
|
||||
|
||||
<p>
|
||||
A new user with email <%= user.email %> was added to <%= cloudronName %>.
|
||||
A new user with email <%= user.alternateEmail || user.email %> was added to <%= cloudronName %>.
|
||||
</p>
|
||||
|
||||
<% if (inviteLink) { %>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Dear Cloudron Admin,
|
||||
|
||||
User <%= user.username || user.email %> <%= event %>.
|
||||
User <%= user.username || user.alternateEmail || user.email %> <%= event %>.
|
||||
|
||||
|
||||
Powered by https://cloudron.io
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<%if (format === 'text') { %>
|
||||
|
||||
Dear <%= user.displayName || user.username || user.email %>,
|
||||
Dear <%= user.displayName || user.username || user.alternateEmail || user.email %>,
|
||||
|
||||
Welcome to <%= cloudronName %>!
|
||||
|
||||
@@ -20,7 +20,7 @@ Powered by https://cloudron.io
|
||||
|
||||
<img src="<%= cloudronAvatarUrl %>" width="128px" height="128px"/>
|
||||
|
||||
<h3>Hi <%= user.displayName || user.username || user.email %>,</h3>
|
||||
<h3>Hi <%= user.displayName || user.username || user.alternateEmail || user.email %>,</h3>
|
||||
|
||||
<h2>Welcome to <%= cloudronName %>!</h2>
|
||||
|
||||
|
||||
+25
-62
@@ -6,7 +6,6 @@ exports = module.exports = {
|
||||
|
||||
listAliases: listAliases,
|
||||
listMailboxes: listMailboxes,
|
||||
listGroups: listGroups,
|
||||
|
||||
getMailbox: getMailbox,
|
||||
getGroup: getGroup,
|
||||
@@ -17,7 +16,6 @@ exports = module.exports = {
|
||||
|
||||
getByOwnerId: getByOwnerId,
|
||||
delByOwnerId: delByOwnerId,
|
||||
delByDomain: delByDomain,
|
||||
|
||||
updateName: updateName,
|
||||
|
||||
@@ -33,16 +31,15 @@ var assert = require('assert'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
util = require('util');
|
||||
|
||||
var MAILBOX_FIELDS = [ 'name', 'ownerId', 'ownerType', 'aliasTarget', 'creationTime', 'domain' ].join(',');
|
||||
var MAILBOX_FIELDS = [ 'name', 'ownerId', 'ownerType', 'aliasTarget', 'creationTime' ].join(',');
|
||||
|
||||
function add(name, domain, ownerId, ownerType, callback) {
|
||||
function add(name, 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, domain, ownerId, ownerType) VALUES (?, ?, ?, ?)', [ name, domain, ownerId, ownerType ], function (error) {
|
||||
database.query('INSERT INTO mailboxes (name, ownerId, ownerType) VALUES (?, ?, ?)', [ name, 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));
|
||||
|
||||
@@ -59,13 +56,12 @@ function clear(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function del(name, domain, callback) {
|
||||
function del(name, 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 = ?) AND domain = ?', [ name, name, domain ], function (error, result) {
|
||||
database.query('DELETE FROM mailboxes WHERE name=? OR aliasTarget = ?', [ name, name ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.affectedRows === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
@@ -73,18 +69,6 @@ function del(name, domain, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function delByDomain(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// deletes aliases as well
|
||||
database.query('DELETE FROM mailboxes WHERE domain = ?', [ domain ], function (error) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function delByOwnerId(id, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -97,17 +81,15 @@ function delByOwnerId(id, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function updateName(oldName, oldDomain, newName, newDomain, callback) {
|
||||
function updateName(oldName, newName, 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 && oldDomain === newDomain) return callback(null);
|
||||
if (oldName === newName) return callback(null);
|
||||
|
||||
database.query('UPDATE mailboxes SET name=?, domain=? WHERE name=? AND domain = ?', [ newName, newDomain, oldName, oldDomain ], function (error, result) {
|
||||
database.query('UPDATE mailboxes SET name=? WHERE name=?', [ newName, oldName ], 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));
|
||||
@@ -116,12 +98,11 @@ function updateName(oldName, oldDomain, newName, newDomain, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getMailbox(name, domain, callback) {
|
||||
function getMailbox(name, 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 domain = ? AND (ownerType = ? OR ownerType = ?) AND aliasTarget IS NULL', [ name, domain, exports.TYPE_APP, exports.TYPE_USER ], function (error, results) {
|
||||
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) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
@@ -129,32 +110,18 @@ function getMailbox(name, domain, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function listMailboxes(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
function listMailboxes(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
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) {
|
||||
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) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, results);
|
||||
});
|
||||
}
|
||||
|
||||
function listGroups(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE domain = ? AND ownerType = ? AND aliasTarget IS NULL', [ domain, exports.TYPE_GROUP ], function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
callback(null, results);
|
||||
});
|
||||
}
|
||||
|
||||
function getGroup(name, domain, callback) {
|
||||
function getGroup(name, 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
|
||||
@@ -163,7 +130,7 @@ function getGroup(name, domain, callback) {
|
||||
// INNER JOIN users ON groupMembers.userId = users.id
|
||||
// WHERE mailboxes.name = <name>
|
||||
|
||||
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) {
|
||||
database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE name = ? AND ownerType = ? AND aliasTarget IS NULL', [ name, 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));
|
||||
|
||||
@@ -190,21 +157,20 @@ function getByOwnerId(ownerId, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function setAliasesForName(name, domain, aliases, callback) {
|
||||
function setAliasesForName(name, 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 = ? AND domain = ?', [ name, domain ], function (error, results) {
|
||||
database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE name = ? ', [ name ], 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 = ? AND domain = ?', args: [ name, domain ] });
|
||||
queries.push({ query: 'DELETE FROM mailboxes WHERE aliasTarget = ?', args: [ name ] });
|
||||
aliases.forEach(function (alias) {
|
||||
queries.push({ query: 'INSERT INTO mailboxes (name, domain, aliasTarget, ownerId, ownerType) VALUES (?, ?, ?, ?, ?)',
|
||||
args: [ alias, domain, name, results[0].ownerId, results[0].ownerType ] });
|
||||
queries.push({ query: 'INSERT INTO mailboxes (name, aliasTarget, ownerId, ownerType) VALUES (?, ?, ?, ?)',
|
||||
args: [ alias, name, results[0].ownerId, results[0].ownerType ] });
|
||||
});
|
||||
|
||||
database.transaction(queries, function (error) {
|
||||
@@ -216,12 +182,11 @@ function setAliasesForName(name, domain, aliases, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getAliasesForName(name, domain, callback) {
|
||||
function getAliasesForName(name, callback) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT name FROM mailboxes WHERE aliasTarget = ? AND domain = ? ORDER BY name', [ name, domain ], function (error, results) {
|
||||
database.query('SELECT name FROM mailboxes WHERE aliasTarget=? ORDER BY name', [ name ], function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
results = results.map(function (r) { return r.name; });
|
||||
@@ -229,23 +194,21 @@ function getAliasesForName(name, domain, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function listAliases(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
function listAliases(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE domain = ? AND aliasTarget IS NOT NULL ORDER BY name', [ domain ], function (error, results) {
|
||||
database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE aliasTarget IS NOT NULL ORDER BY name', function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, results);
|
||||
});
|
||||
}
|
||||
|
||||
function getAlias(name, domain, callback) {
|
||||
function getAlias(name, 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 domain = ? AND aliasTarget IS NOT NULL', [ name, domain ], function (error, results) {
|
||||
database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE name = ? AND aliasTarget IS NOT NULL', [ name ], function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
|
||||
-123
@@ -1,123 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
add: add,
|
||||
del: del,
|
||||
get: get,
|
||||
getAll: getAll,
|
||||
update: update,
|
||||
|
||||
_clear: clear,
|
||||
|
||||
TYPE_USER: 'user',
|
||||
TYPE_APP: 'app',
|
||||
TYPE_GROUP: 'group'
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
database = require('./database.js'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
safe = require('safetydance');
|
||||
|
||||
var MAILDB_FIELDS = [ 'domain', 'enabled', 'mailFromValidation', 'catchAllJson', 'relayJson' ].join(',');
|
||||
|
||||
function postProcess(data) {
|
||||
data.enabled = !!data.enabled; // int to boolean
|
||||
data.mailFromValidation = !!data.mailFromValidation; // int to boolean
|
||||
|
||||
data.catchAll = safe.JSON.parse(data.catchAllJson) || [ ];
|
||||
delete data.catchAllJson;
|
||||
|
||||
data.relay = safe.JSON.parse(data.relayJson) || { provider: 'cloudron-smtp' };
|
||||
delete data.relayJson;
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function add(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
|
||||
database.query('INSERT INTO mail (domain) VALUES (?)', [ domain ], function (error) {
|
||||
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, 'mail domain already exists'));
|
||||
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND), 'no such domain');
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function clear(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('TRUNCATE TABLE mail', [], function (error) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function del(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// deletes aliases as well
|
||||
database.query('DELETE FROM mail WHERE domain=?', [ domain ], function (error, result) {
|
||||
if (error && error.code === 'ER_ROW_IS_REFERENCED_2') return callback(new DatabaseError(DatabaseError.IN_USE));
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.affectedRows === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function get(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + MAILDB_FIELDS + ' FROM mail WHERE domain = ?', [ domain ], function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
callback(null, postProcess(results[0]));
|
||||
});
|
||||
}
|
||||
|
||||
function getAll(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + MAILDB_FIELDS + ' FROM mail ORDER BY domain', function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
results.forEach(function (result) { postProcess(result); });
|
||||
|
||||
callback(null, results);
|
||||
});
|
||||
}
|
||||
|
||||
function update(domain, data, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof data, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var args = [ ];
|
||||
var fields = [ ];
|
||||
for (var k in data) {
|
||||
if (k === 'catchAll') {
|
||||
fields.push('catchAllJson = ?');
|
||||
args.push(JSON.stringify(data[k]));
|
||||
} else if (k === 'relay') {
|
||||
fields.push('relayJson = ?');
|
||||
args.push(JSON.stringify(data[k]));
|
||||
} else {
|
||||
fields.push(k + ' = ?');
|
||||
args.push(data[k]);
|
||||
}
|
||||
}
|
||||
args.push(domain);
|
||||
|
||||
database.query('UPDATE mail SET ' + fields.join(', ') + ' WHERE domain=?', args, function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
+219
-190
@@ -1,6 +1,9 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
start: start,
|
||||
stop: stop,
|
||||
|
||||
userAdded: userAdded,
|
||||
userRemoved: userRemoved,
|
||||
adminChanged: adminChanged,
|
||||
@@ -32,7 +35,6 @@ var assert = require('assert'),
|
||||
debug = require('debug')('box:mailer'),
|
||||
docker = require('./docker.js').connection,
|
||||
ejs = require('ejs'),
|
||||
mail = require('./mail.js'),
|
||||
nodemailer = require('nodemailer'),
|
||||
path = require('path'),
|
||||
safe = require('safetydance'),
|
||||
@@ -47,7 +49,8 @@ var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
|
||||
var MAIL_TEMPLATES_DIR = path.join(__dirname, 'mail_templates');
|
||||
|
||||
var gMailQueue = [ ];
|
||||
var gMailQueue = [ ],
|
||||
gPaused = false;
|
||||
|
||||
function splatchError(error) {
|
||||
var result = { };
|
||||
@@ -60,39 +63,34 @@ function splatchError(error) {
|
||||
return util.inspect(result, { depth: null, showHidden: true });
|
||||
}
|
||||
|
||||
// This will collect the most common details required for notification emails
|
||||
function getMailConfig(callback) {
|
||||
function start(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getAdminEmails(function (error, adminEmails) {
|
||||
if (error) return callback(error);
|
||||
if (process.env.BOX_ENV === 'test') gPaused = true;
|
||||
|
||||
settings.getCloudronName(function (error, cloudronName) {
|
||||
// this is not fatal
|
||||
if (error) {
|
||||
debug(error);
|
||||
cloudronName = 'Cloudron';
|
||||
}
|
||||
|
||||
mail.getAll(function (error, domains) {
|
||||
if (error) return callback(error);
|
||||
if (domains.length === 0) return callback('No domains configured');
|
||||
|
||||
const defaultDomain = domains[0];
|
||||
|
||||
callback(null, {
|
||||
adminEmails: adminEmails,
|
||||
cloudronName: cloudronName,
|
||||
notificationDomain: defaultDomain.domain,
|
||||
notificationFrom: `"${cloudronName}" <no-reply@${defaultDomain.domain}>`
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
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" <no-reply@' + config.fqdn() + '>'
|
||||
};
|
||||
}
|
||||
|
||||
function processQueue() {
|
||||
assert(!gPaused);
|
||||
|
||||
sendMails(gMailQueue);
|
||||
gMailQueue = [ ];
|
||||
}
|
||||
@@ -139,7 +137,7 @@ function enqueue(mailOptions) {
|
||||
debug('Queued mail for ' + mailOptions.from + ' to ' + mailOptions.to);
|
||||
gMailQueue.push(mailOptions);
|
||||
|
||||
if (process.env.BOX_ENV !== 'test') processQueue();
|
||||
if (!gPaused) processQueue();
|
||||
}
|
||||
|
||||
function render(templateFile, params) {
|
||||
@@ -164,7 +162,7 @@ function getAdminEmails(callback) {
|
||||
if (admins.length === 0) return callback(new Error('No admins on this cloudron')); // box not activated yet
|
||||
|
||||
var adminEmails = [ ];
|
||||
adminEmails.push(admins[0].fallbackEmail);
|
||||
if (admins[0].alternateEmail) adminEmails.push(admins[0].alternateEmail);
|
||||
admins.forEach(function (admin) { adminEmails.push(admin.email); });
|
||||
|
||||
callback(null, adminEmails);
|
||||
@@ -175,16 +173,16 @@ function mailUserEventToAdmins(user, event) {
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
assert.strictEqual(typeof event, 'string');
|
||||
|
||||
getMailConfig(function (error, mailConfig) {
|
||||
if (error) return debug('Error getting mail details:', error);
|
||||
getAdminEmails(function (error, adminEmails) {
|
||||
if (error) return debug('Error getting admins', error);
|
||||
|
||||
var adminEmails = _.difference(mailConfig.adminEmails, [ user.email ]);
|
||||
adminEmails = _.difference(adminEmails, [ user.email ]);
|
||||
|
||||
var mailOptions = {
|
||||
from: mailConfig.notificationFrom,
|
||||
from: mailConfig().from,
|
||||
to: adminEmails.join(', '),
|
||||
subject: util.format('[%s] %s %s', mailConfig.cloudronName, user.username || user.fallbackEmail || user.email, event),
|
||||
text: render('user_event.ejs', { user: user, event: event, format: 'text' }),
|
||||
subject: util.format('[%s] %s %s', config.fqdn(), user.username || user.alternateEmail || user.email, event),
|
||||
text: render('user_event.ejs', { fqdn: config.fqdn(), user: user, event: event, format: 'text' }),
|
||||
};
|
||||
|
||||
enqueue(mailOptions);
|
||||
@@ -197,15 +195,19 @@ function sendInvite(user, invitor) {
|
||||
|
||||
debug('Sending invite mail');
|
||||
|
||||
getMailConfig(function (error, mailConfig) {
|
||||
if (error) return debug('Error getting mail details:', error);
|
||||
settings.getCloudronName(function (error, cloudronName) {
|
||||
if (error) {
|
||||
debug(error);
|
||||
cloudronName = 'Cloudron';
|
||||
}
|
||||
|
||||
var templateData = {
|
||||
user: user,
|
||||
webadminUrl: config.adminOrigin(),
|
||||
setupLink: config.adminOrigin() + '/api/v1/session/account/setup.html?reset_token=' + user.resetToken,
|
||||
fqdn: config.fqdn(),
|
||||
invitor: invitor,
|
||||
cloudronName: mailConfig.cloudronName,
|
||||
cloudronName: cloudronName,
|
||||
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
|
||||
};
|
||||
|
||||
@@ -216,9 +218,9 @@ function sendInvite(user, invitor) {
|
||||
templateDataHTML.format = 'html';
|
||||
|
||||
var mailOptions = {
|
||||
from: mailConfig.notificationFrom,
|
||||
to: user.fallbackEmail,
|
||||
subject: util.format('Welcome to %s', mailConfig.cloudronName),
|
||||
from: mailConfig().from,
|
||||
to: user.alternateEmail || user.email,
|
||||
subject: util.format('Welcome to %s', cloudronName),
|
||||
text: render('welcome_user.ejs', templateDataText),
|
||||
html: render('welcome_user.ejs', templateDataHTML)
|
||||
};
|
||||
@@ -233,33 +235,41 @@ function userAdded(user, inviteSent) {
|
||||
|
||||
debug('Sending mail for userAdded %s including invite link', inviteSent ? 'not' : '');
|
||||
|
||||
getMailConfig(function (error, mailConfig) {
|
||||
if (error) return debug('Error getting mail details:', error);
|
||||
getAdminEmails(function (error, adminEmails) {
|
||||
if (error) return debug('Error getting admins', error);
|
||||
|
||||
var adminEmails = _.difference(mailConfig.adminEmails, [ user.email ]);
|
||||
adminEmails = _.difference(adminEmails, [ user.email ]);
|
||||
|
||||
var templateData = {
|
||||
user: user,
|
||||
inviteLink: inviteSent ? null : config.adminOrigin() + '/api/v1/session/account/setup.html?reset_token=' + user.resetToken,
|
||||
cloudronName: mailConfig.cloudronName,
|
||||
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
|
||||
};
|
||||
settings.getCloudronName(function (error, cloudronName) {
|
||||
if (error) {
|
||||
debug(error);
|
||||
cloudronName = 'Cloudron';
|
||||
}
|
||||
|
||||
var templateDataText = JSON.parse(JSON.stringify(templateData));
|
||||
templateDataText.format = 'text';
|
||||
var templateData = {
|
||||
fqdn: config.fqdn(),
|
||||
user: user,
|
||||
inviteLink: inviteSent ? null : config.adminOrigin() + '/api/v1/session/account/setup.html?reset_token=' + user.resetToken,
|
||||
cloudronName: cloudronName,
|
||||
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
|
||||
};
|
||||
|
||||
var templateDataHTML = JSON.parse(JSON.stringify(templateData));
|
||||
templateDataHTML.format = 'html';
|
||||
var templateDataText = JSON.parse(JSON.stringify(templateData));
|
||||
templateDataText.format = 'text';
|
||||
|
||||
var mailOptions = {
|
||||
from: mailConfig.notificationFrom,
|
||||
to: adminEmails.join(', '),
|
||||
subject: util.format('[%s] User %s added', mailConfig.cloudronName, user.fallbackEmail),
|
||||
text: render('user_added.ejs', templateDataText),
|
||||
html: render('user_added.ejs', templateDataHTML)
|
||||
};
|
||||
var templateDataHTML = JSON.parse(JSON.stringify(templateData));
|
||||
templateDataHTML.format = 'html';
|
||||
|
||||
enqueue(mailOptions);
|
||||
var mailOptions = {
|
||||
from: mailConfig().from,
|
||||
to: adminEmails.join(', '),
|
||||
subject: util.format('[%s] User %s added', config.fqdn(), user.alternateEmail || user.email),
|
||||
text: render('user_added.ejs', templateDataText),
|
||||
html: render('user_added.ejs', templateDataHTML)
|
||||
};
|
||||
|
||||
enqueue(mailOptions);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -285,13 +295,17 @@ function passwordReset(user) {
|
||||
|
||||
debug('Sending mail for password reset for user %s.', user.email, user.id);
|
||||
|
||||
getMailConfig(function (error, mailConfig) {
|
||||
if (error) return debug('Error getting mail details:', error);
|
||||
settings.getCloudronName(function (error, cloudronName) {
|
||||
if (error) {
|
||||
debug(error);
|
||||
cloudronName = 'Cloudron';
|
||||
}
|
||||
|
||||
var templateData = {
|
||||
fqdn: config.fqdn(),
|
||||
user: user,
|
||||
resetLink: config.adminOrigin() + '/api/v1/session/password/reset.html?reset_token=' + user.resetToken,
|
||||
cloudronName: mailConfig.cloudronName,
|
||||
cloudronName: cloudronName,
|
||||
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
|
||||
};
|
||||
|
||||
@@ -302,9 +316,9 @@ function passwordReset(user) {
|
||||
templateDataHTML.format = 'html';
|
||||
|
||||
var mailOptions = {
|
||||
from: mailConfig.notificationFrom,
|
||||
to: user.fallbackEmail,
|
||||
subject: util.format('[%s] Password Reset', mailConfig.cloudronName),
|
||||
from: mailConfig().from,
|
||||
to: user.alternateEmail || user.email,
|
||||
subject: util.format('[%s] Password Reset', config.fqdn()),
|
||||
text: render('password_reset.ejs', templateDataText),
|
||||
html: render('password_reset.ejs', templateDataHTML)
|
||||
};
|
||||
@@ -318,14 +332,14 @@ function appDied(app) {
|
||||
|
||||
debug('Sending mail for app %s @ %s died', app.id, app.fqdn);
|
||||
|
||||
getMailConfig(function (error, mailConfig) {
|
||||
if (error) return debug('Error getting mail details:', error);
|
||||
getAdminEmails(function (error, adminEmails) {
|
||||
if (error) return debug('Error getting admins', error);
|
||||
|
||||
var mailOptions = {
|
||||
from: mailConfig.notificationFrom,
|
||||
to: config.provider() === 'caas' ? 'support@cloudron.io' : mailConfig.adminEmails.join(', '),
|
||||
subject: util.format('[%s] App %s is down', mailConfig.cloudronName, app.fqdn),
|
||||
text: render('app_down.ejs', { title: app.manifest.title, appFqdn: app.fqdn, format: 'text' })
|
||||
from: mailConfig().from,
|
||||
to: config.provider() === 'caas' ? 'support@cloudron.io' : adminEmails.join(', '),
|
||||
subject: util.format('[%s] App %s is down', config.fqdn(), app.fqdn),
|
||||
text: render('app_down.ejs', { fqdn: config.fqdn(), title: app.manifest.title, appFqdn: app.fqdn, format: 'text' })
|
||||
};
|
||||
|
||||
enqueue(mailOptions);
|
||||
@@ -337,36 +351,44 @@ function boxUpdateAvailable(hasSubscription, newBoxVersion, changelog) {
|
||||
assert.strictEqual(typeof newBoxVersion, 'string');
|
||||
assert(util.isArray(changelog));
|
||||
|
||||
getMailConfig(function (error, mailConfig) {
|
||||
if (error) return debug('Error getting mail details:', error);
|
||||
getAdminEmails(function (error, adminEmails) {
|
||||
if (error) return debug('Error getting admins', error);
|
||||
|
||||
var converter = new showdown.Converter();
|
||||
settings.getCloudronName(function (error, cloudronName) {
|
||||
if (error) {
|
||||
debug(error);
|
||||
cloudronName = 'Cloudron';
|
||||
}
|
||||
|
||||
var templateData = {
|
||||
webadminUrl: config.adminOrigin(),
|
||||
newBoxVersion: newBoxVersion,
|
||||
hasSubscription: hasSubscription,
|
||||
changelog: changelog,
|
||||
changelogHTML: changelog.map(function (e) { return converter.makeHtml(e); }),
|
||||
cloudronName: mailConfig.cloudronName,
|
||||
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
|
||||
};
|
||||
var converter = new showdown.Converter();
|
||||
|
||||
var templateDataText = JSON.parse(JSON.stringify(templateData));
|
||||
templateDataText.format = 'text';
|
||||
var templateData = {
|
||||
fqdn: config.fqdn(),
|
||||
webadminUrl: config.adminOrigin(),
|
||||
newBoxVersion: newBoxVersion,
|
||||
hasSubscription: hasSubscription,
|
||||
changelog: changelog,
|
||||
changelogHTML: changelog.map(function (e) { return converter.makeHtml(e); }),
|
||||
cloudronName: cloudronName,
|
||||
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
|
||||
};
|
||||
|
||||
var templateDataHTML = JSON.parse(JSON.stringify(templateData));
|
||||
templateDataHTML.format = 'html';
|
||||
var templateDataText = JSON.parse(JSON.stringify(templateData));
|
||||
templateDataText.format = 'text';
|
||||
|
||||
var mailOptions = {
|
||||
from: mailConfig.notificationFrom,
|
||||
to: mailConfig.adminEmails.join(', '),
|
||||
subject: util.format('%s has a new update available', mailConfig.cloudronName),
|
||||
text: render('box_update_available.ejs', templateDataText),
|
||||
html: render('box_update_available.ejs', templateDataHTML)
|
||||
};
|
||||
var templateDataHTML = JSON.parse(JSON.stringify(templateData));
|
||||
templateDataHTML.format = 'html';
|
||||
|
||||
enqueue(mailOptions);
|
||||
var mailOptions = {
|
||||
from: mailConfig().from,
|
||||
to: adminEmails.join(', '),
|
||||
subject: util.format('%s has a new update available', config.fqdn()),
|
||||
text: render('box_update_available.ejs', templateDataText),
|
||||
html: render('box_update_available.ejs', templateDataHTML)
|
||||
};
|
||||
|
||||
enqueue(mailOptions);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -375,81 +397,97 @@ function appUpdateAvailable(app, hasSubscription, info) {
|
||||
assert.strictEqual(typeof hasSubscription, 'boolean');
|
||||
assert.strictEqual(typeof info, 'object');
|
||||
|
||||
getMailConfig(function (error, mailConfig) {
|
||||
if (error) return debug('Error getting mail details:', error);
|
||||
getAdminEmails(function (error, adminEmails) {
|
||||
if (error) return debug('Error getting admins', error);
|
||||
|
||||
var converter = new showdown.Converter();
|
||||
settings.getCloudronName(function (error, cloudronName) {
|
||||
if (error) {
|
||||
debug(error);
|
||||
cloudronName = 'Cloudron';
|
||||
}
|
||||
|
||||
var templateData = {
|
||||
webadminUrl: config.adminOrigin(),
|
||||
hasSubscription: hasSubscription,
|
||||
app: app,
|
||||
updateInfo: info,
|
||||
changelogHTML: converter.makeHtml(info.manifest.changelog),
|
||||
cloudronName: mailConfig.cloudronName,
|
||||
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
|
||||
};
|
||||
var converter = new showdown.Converter();
|
||||
|
||||
var templateDataText = JSON.parse(JSON.stringify(templateData));
|
||||
templateDataText.format = 'text';
|
||||
var templateData = {
|
||||
fqdn: config.fqdn(),
|
||||
webadminUrl: config.adminOrigin(),
|
||||
hasSubscription: hasSubscription,
|
||||
app: app,
|
||||
updateInfo: info,
|
||||
changelogHTML: converter.makeHtml(info.manifest.changelog),
|
||||
cloudronName: cloudronName,
|
||||
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
|
||||
};
|
||||
|
||||
var templateDataHTML = JSON.parse(JSON.stringify(templateData));
|
||||
templateDataHTML.format = 'html';
|
||||
var templateDataText = JSON.parse(JSON.stringify(templateData));
|
||||
templateDataText.format = 'text';
|
||||
|
||||
var mailOptions = {
|
||||
from: mailConfig.notificationFrom,
|
||||
to: mailConfig.adminEmails.join(', '),
|
||||
subject: util.format('App %s has a new update available', app.fqdn),
|
||||
text: render('app_update_available.ejs', templateDataText),
|
||||
html: render('app_update_available.ejs', templateDataHTML)
|
||||
};
|
||||
var templateDataHTML = JSON.parse(JSON.stringify(templateData));
|
||||
templateDataHTML.format = 'html';
|
||||
|
||||
enqueue(mailOptions);
|
||||
var mailOptions = {
|
||||
from: mailConfig().from,
|
||||
to: adminEmails.join(', '),
|
||||
subject: util.format('App %s has a new update available', app.fqdn),
|
||||
text: render('app_update_available.ejs', templateDataText),
|
||||
html: render('app_update_available.ejs', templateDataHTML)
|
||||
};
|
||||
|
||||
enqueue(mailOptions);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function sendDigest(info) {
|
||||
assert.strictEqual(typeof info, 'object');
|
||||
|
||||
getMailConfig(function (error, mailConfig) {
|
||||
if (error) return debug('Error getting mail details:', error);
|
||||
getAdminEmails(function (error, adminEmails) {
|
||||
if (error) return debug('Error getting admins', error);
|
||||
|
||||
var templateData = {
|
||||
webadminUrl: config.adminOrigin(),
|
||||
cloudronName: mailConfig.cloudronName,
|
||||
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar',
|
||||
info: info
|
||||
};
|
||||
settings.getCloudronName(function (error, cloudronName) {
|
||||
if (error) {
|
||||
debug(error);
|
||||
cloudronName = 'Cloudron';
|
||||
}
|
||||
|
||||
var templateDataText = JSON.parse(JSON.stringify(templateData));
|
||||
templateDataText.format = 'text';
|
||||
var templateData = {
|
||||
fqdn: config.fqdn(),
|
||||
webadminUrl: config.adminOrigin(),
|
||||
cloudronName: cloudronName,
|
||||
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar',
|
||||
info: info
|
||||
};
|
||||
|
||||
var templateDataHTML = JSON.parse(JSON.stringify(templateData));
|
||||
templateDataHTML.format = 'html';
|
||||
var templateDataText = JSON.parse(JSON.stringify(templateData));
|
||||
templateDataText.format = 'text';
|
||||
|
||||
var mailOptions = {
|
||||
from: mailConfig.notificationFrom,
|
||||
to: mailConfig.adminEmails.join(', '),
|
||||
subject: util.format('[%s] Cloudron - Weekly activity digest', mailConfig.cloudronName),
|
||||
text: render('digest.ejs', templateDataText),
|
||||
html: render('digest.ejs', templateDataHTML)
|
||||
};
|
||||
var templateDataHTML = JSON.parse(JSON.stringify(templateData));
|
||||
templateDataHTML.format = 'html';
|
||||
|
||||
enqueue(mailOptions);
|
||||
var mailOptions = {
|
||||
from: mailConfig().from,
|
||||
to: adminEmails.join(', '),
|
||||
subject: util.format('[%s] Cloudron - Weekly activity digest', config.fqdn()),
|
||||
text: render('digest.ejs', templateDataText),
|
||||
html: render('digest.ejs', templateDataHTML)
|
||||
};
|
||||
|
||||
enqueue(mailOptions);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function outOfDiskSpace(message) {
|
||||
assert.strictEqual(typeof message, 'string');
|
||||
|
||||
getMailConfig(function (error, mailConfig) {
|
||||
if (error) return debug('Error getting mail details:', error);
|
||||
getAdminEmails(function (error, adminEmails) {
|
||||
if (error) return debug('Error getting admins', error);
|
||||
|
||||
var mailOptions = {
|
||||
from: mailConfig.notificationFrom,
|
||||
to: config.provider() === 'caas' ? 'support@cloudron.io' : mailConfig.adminEmails.join(', '),
|
||||
subject: util.format('[%s] Out of disk space alert', mailConfig.cloudronName),
|
||||
text: render('out_of_disk_space.ejs', { cloudronName: mailConfig.cloudronName, message: message, format: 'text' })
|
||||
from: mailConfig().from,
|
||||
to: config.provider() === 'caas' ? 'support@cloudron.io' : adminEmails.join(', '),
|
||||
subject: util.format('[%s] Out of disk space alert', config.fqdn()),
|
||||
text: render('out_of_disk_space.ejs', { fqdn: config.fqdn(), message: message, format: 'text' })
|
||||
};
|
||||
|
||||
sendMails([ mailOptions ]);
|
||||
@@ -459,14 +497,14 @@ function outOfDiskSpace(message) {
|
||||
function backupFailed(error) {
|
||||
var message = splatchError(error);
|
||||
|
||||
getMailConfig(function (error, mailConfig) {
|
||||
if (error) return debug('Error getting mail details:', error);
|
||||
getAdminEmails(function (error, adminEmails) {
|
||||
if (error) return debug('Error getting admins', error);
|
||||
|
||||
var mailOptions = {
|
||||
from: mailConfig.notificationFrom,
|
||||
to: config.provider() === 'caas' ? 'support@cloudron.io' : mailConfig.adminEmails.join(', '),
|
||||
subject: util.format('[%s] Failed to backup', mailConfig.cloudronName),
|
||||
text: render('backup_failed.ejs', { cloudronName: mailConfig.cloudronName, message: message, format: 'text' })
|
||||
from: mailConfig().from,
|
||||
to: config.provider() === 'caas' ? 'support@cloudron.io' : adminEmails.join(', '),
|
||||
subject: util.format('[%s] Failed to backup', config.fqdn()),
|
||||
text: render('backup_failed.ejs', { fqdn: config.fqdn(), message: message, format: 'text' })
|
||||
};
|
||||
|
||||
enqueue(mailOptions);
|
||||
@@ -477,12 +515,12 @@ function certificateRenewalError(domain, message) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof message, 'string');
|
||||
|
||||
getMailConfig(function (error, mailConfig) {
|
||||
if (error) return debug('Error getting mail details:', error);
|
||||
getAdminEmails(function (error, adminEmails) {
|
||||
if (error) return debug('Error getting admins', error);
|
||||
|
||||
var mailOptions = {
|
||||
from: mailConfig.notificationFrom,
|
||||
to: config.provider() === 'caas' ? 'support@cloudron.io' : mailConfig.adminEmails.join(', '),
|
||||
from: mailConfig().from,
|
||||
to: config.provider() === 'caas' ? 'support@cloudron.io' : adminEmails.join(', '),
|
||||
subject: util.format('[%s] Certificate renewal error', domain),
|
||||
text: render('certificate_renewal_error.ejs', { domain: domain, message: message, format: 'text' })
|
||||
};
|
||||
@@ -495,14 +533,14 @@ function oomEvent(program, context) {
|
||||
assert.strictEqual(typeof program, 'string');
|
||||
assert.strictEqual(typeof context, 'string');
|
||||
|
||||
getMailConfig(function (error, mailConfig) {
|
||||
if (error) return debug('Error getting mail details:', error);
|
||||
getAdminEmails(function (error, adminEmails) {
|
||||
if (error) return debug('Error getting admins', error);
|
||||
|
||||
var mailOptions = {
|
||||
from: mailConfig.notificationFrom,
|
||||
to: config.provider() === 'caas' ? 'support@cloudron.io' : mailConfig.adminEmails.join(', '),
|
||||
subject: util.format('[%s] %s exited unexpectedly', mailConfig.cloudronName, program),
|
||||
text: render('oom_event.ejs', { cloudronName: mailConfig.cloudronName, program: program, context: context, format: 'text' })
|
||||
from: mailConfig().from,
|
||||
to: config.provider() === 'caas' ? 'support@cloudron.io' : adminEmails.join(', '),
|
||||
subject: util.format('[%s] %s exited unexpectedly', config.fqdn(), program),
|
||||
text: render('oom_event.ejs', { fqdn: config.fqdn(), program: program, context: context, format: 'text' })
|
||||
};
|
||||
|
||||
sendMails([ mailOptions ]);
|
||||
@@ -510,7 +548,7 @@ function oomEvent(program, context) {
|
||||
}
|
||||
|
||||
// this function bypasses the queue intentionally. it is also expected to work without the mailer module initialized
|
||||
// NOTE: crashnotifier should ideally be able to send mail when there is no db, however we need the 'from' address domain from the db
|
||||
// NOTE: crashnotifier should be able to send mail when there is no db
|
||||
function unexpectedExit(program, context, callback) {
|
||||
assert.strictEqual(typeof program, 'string');
|
||||
assert.strictEqual(typeof context, 'string');
|
||||
@@ -518,36 +556,27 @@ function unexpectedExit(program, context, callback) {
|
||||
|
||||
if (config.provider() !== 'caas') return callback(); // no way to get admins without db access
|
||||
|
||||
getMailConfig(function (error, mailConfig) {
|
||||
if (error) return debug('Error getting mail details:', error);
|
||||
var mailOptions = {
|
||||
from: mailConfig().from,
|
||||
to: 'support@cloudron.io',
|
||||
subject: util.format('[%s] %s exited unexpectedly', config.fqdn(), program),
|
||||
text: render('unexpected_exit.ejs', { fqdn: config.fqdn(), program: program, context: context, format: 'text' })
|
||||
};
|
||||
|
||||
var mailOptions = {
|
||||
from: mailConfig.notificationFrom,
|
||||
to: 'support@cloudron.io',
|
||||
subject: util.format('[%s] %s exited unexpectedly', mailConfig.cloudronName, program),
|
||||
text: render('unexpected_exit.ejs', { cloudronName: mailConfig.cloudronName, program: program, context: context, format: 'text' })
|
||||
};
|
||||
|
||||
sendMails([ mailOptions ], callback);
|
||||
});
|
||||
sendMails([ mailOptions ], callback);
|
||||
}
|
||||
|
||||
function sendTestMail(domain, email) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
function sendTestMail(email) {
|
||||
assert.strictEqual(typeof email, 'string');
|
||||
|
||||
getMailConfig(function (error, mailConfig) {
|
||||
if (error) return debug('Error getting mail details:', error);
|
||||
var mailOptions = {
|
||||
from: mailConfig().from,
|
||||
to: email,
|
||||
subject: util.format('Test Email from %s', config.fqdn()),
|
||||
text: render('test.ejs', { fqdn: config.fqdn(), format: 'text'})
|
||||
};
|
||||
|
||||
var mailOptions = {
|
||||
from: `"${mailConfig.cloudronName}" <no-reply@${domain}>`,
|
||||
to: email,
|
||||
subject: util.format('Test Email from %s', mailConfig.cloudronName),
|
||||
text: render('test.ejs', { cloudronName: mailConfig.cloudronName, format: 'text'})
|
||||
};
|
||||
|
||||
enqueue(mailOptions);
|
||||
});
|
||||
enqueue(mailOptions);
|
||||
}
|
||||
|
||||
function _getMailQueue() {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user