Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7a63512d21 | |||
| 7ed18b3016 | |||
| 0d3d7d2b88 | |||
| 457f76367c | |||
| 74b3d06cd7 | |||
| d22f385402 | |||
| 08f920afee |
+1
-2
@@ -1,7 +1,6 @@
|
||||
# following files are skipped when exporting using git archive
|
||||
test export-ignore
|
||||
.jshintrc export-ignore
|
||||
.gitlab export-ignore
|
||||
docs export-ignore
|
||||
.gitattributes export-ignore
|
||||
.gitignore export-ignore
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
node_modules/
|
||||
coverage/
|
||||
webadmin/dist/
|
||||
setup/splash/website/
|
||||
installer/src/certs/server.key
|
||||
|
||||
# vim swap files
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
Please do not use this issue tracker for support requests and bug reports.
|
||||
This issue tracker is used by the Cloudron development team to track actual
|
||||
bugs in the code.
|
||||
|
||||
Please use the forum at https://forum.cloudron.io to report bugs. For
|
||||
confidential issues, please email us at support@cloudron.io.
|
||||
@@ -1,7 +0,0 @@
|
||||
Please do not use this issue tracker for support requests and feature reports.
|
||||
This issue tracker is used by the Cloudron development team to track issues in
|
||||
the code.
|
||||
|
||||
Please use the forum at https://forum.cloudron.io to report bugs. For
|
||||
confidential issues, please email us at support@cloudron.io.
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"node": true,
|
||||
"browser": true,
|
||||
"unused": true,
|
||||
"multistr": true,
|
||||
"globalstrict": true,
|
||||
"predef": [ "angular", "$" ],
|
||||
"esnext": true
|
||||
|
||||
@@ -1184,199 +1184,3 @@
|
||||
[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
|
||||
|
||||
[2.0.1]
|
||||
* 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
|
||||
* Rework the eventlog view
|
||||
* App clone now clones the robotsTxt and backup settings
|
||||
|
||||
[2.1.0]
|
||||
* Make S3 backend work reliably with slow internet connections
|
||||
* Update docker to 18.03.0-ce
|
||||
* Finalize the Email and Mailbox API
|
||||
* Move mailbox settings from users to email view
|
||||
* mail: fix issue where hosts with valid SPF for a Cloudron domain are unable to send mail to Cloudron
|
||||
* mail: fix crash when bounce emails have a null sender
|
||||
* Add CSP header for dashboard
|
||||
* Add support for installing private docker images
|
||||
|
||||
[2.1.1]
|
||||
* Make S3 backend work reliably with slow internet connections
|
||||
* Update docker to 18.03.0-ce
|
||||
* Finalize the Email and Mailbox API
|
||||
* Move mailbox settings from users to email view
|
||||
* mail: fix issue where hosts with valid SPF for a Cloudron domain are unable to send mail to Cloudron
|
||||
* mail: fix crash when bounce emails have a null sender
|
||||
* Add CSP header for dashboard
|
||||
* Add support for installing private docker images
|
||||
|
||||
[2.2.0]
|
||||
* Add 2FA support for the admin dashboard
|
||||
* Cleanup scope management in REST API
|
||||
* Enhance user creation API to take a password
|
||||
* Relax restriction on mailbox names now that it is decoupled from user management
|
||||
|
||||
[2.2.1]
|
||||
* Add 2FA support for the admin dashboard
|
||||
* Add Gandi & GoDaddy DNS providers
|
||||
* Fix zone detection logic on Route53 accounts with more than 100 zones
|
||||
* Warn using when disabling email
|
||||
* Cleanup scope management in REST API
|
||||
* Enhance user creation API to take a password
|
||||
* Relax restriction on mailbox names now that it is decoupled from user management
|
||||
* Fix issue where mail container incorrectly advertised CRAM-MD5 support
|
||||
|
||||
[2.3.0]
|
||||
* Add Name.com DNS provider
|
||||
* Fix issue where account setup page was crashing
|
||||
* Add advanced DNS configuration UI
|
||||
* Preserve addon/database configuration across app updates and restores
|
||||
* ManageSieve port now offers STARTTLS
|
||||
|
||||
[2.3.1]
|
||||
* Add Name.com DNS provider
|
||||
* Fix issue where account setup page was crashing
|
||||
* Add advanced DNS configuration UI
|
||||
* Preserve addon/database configuration across app updates and restores
|
||||
* ManageSieve port now offers STARTTLS
|
||||
* Allow mailbox name to be set for apps
|
||||
* Rework the Email server UI
|
||||
* Add the ability to manually trigger a backup of an application
|
||||
* Enable/disable mail from validation within UI
|
||||
* Allow setting app visibility for non-SSO apps
|
||||
* Add Clone UI
|
||||
|
||||
[2.3.2]
|
||||
* Fix issue where multi-db apps were not provisioned correctly
|
||||
* Improve setup, restore views to have field labels
|
||||
|
||||
[2.4.0]
|
||||
* Use custom logging backend to have more control over log rotation
|
||||
* Make user explicitly confirm that fs backup dir is on external storage
|
||||
* Update node to 8.11.2
|
||||
* Update docker to 18.03.1
|
||||
* Fix docker exec terminal resize issue
|
||||
* Make the mailbox name follow the apps new location, if the user did not set it explicitly
|
||||
* Add backups view
|
||||
|
||||
[2.4.1]
|
||||
* Use custom logging backend to have more control over log rotation
|
||||
* Mail logs and box logs UI
|
||||
* Make user explicitly confirm that fs backup dir is on external storage
|
||||
* Update node to 8.11.2
|
||||
* Update docker to 18.03.1
|
||||
* Fix docker exec terminal resize issue
|
||||
* Make the mailbox name follow the apps new location, if the user did not set it explicitly
|
||||
* Add backups view
|
||||
|
||||
[3.0.0]
|
||||
* Support alternate app domains with redirects
|
||||
* Allow hyphen in mailbox names
|
||||
* Fix issue where the UI timesout when relay server is not reachable
|
||||
* Add support for personal spaces
|
||||
* Add UI to edit users in the groups dialog
|
||||
* Add UI to set groups when creating a user
|
||||
* Open logs and terminal in a new tab instead of a window
|
||||
* Add button to view backup logs
|
||||
* Add Mailjet mail relay support
|
||||
* Encryption support for incremental backups
|
||||
* Display restore errors in the UI
|
||||
* Update Haraka to 2.8.19
|
||||
* GPG verify releases
|
||||
* Allow subdomains in location field
|
||||
|
||||
[3.0.1]
|
||||
* Support alternate app domains with redirects
|
||||
* Allow hyphen in mailbox names
|
||||
* Fix issue where the UI timesout when relay server is not reachable
|
||||
* Add support for personal spaces
|
||||
* Add UI to edit users in the groups dialog
|
||||
* Add UI to set groups when creating a user
|
||||
* Open logs and terminal in a new tab instead of a window
|
||||
* Add button to view backup logs
|
||||
* Add Mailjet mail relay support
|
||||
* Encryption support for incremental backups
|
||||
* Display restore errors in the UI
|
||||
* Update Haraka to 2.8.19
|
||||
* GPG verify releases
|
||||
* Allow subdomains in location field
|
||||
|
||||
[3.0.2]
|
||||
* Fix issue where normal users are shown apps they don't have access to
|
||||
* Re-configure email apps when email is enabled/disabled
|
||||
|
||||
[3.1.0]
|
||||
* Add UDP support
|
||||
* Clicking invite button does not send an invite immediately
|
||||
* Implement docker addon
|
||||
* Automatically login after password reset and account setup
|
||||
* Make backup interval configurable
|
||||
* Fix alternate domain certificate renewal
|
||||
|
||||
[3.1.1]
|
||||
* Fix caas domain migration
|
||||
|
||||
[3.1.2]
|
||||
* Add UDP support
|
||||
* Clicking invite button does not send an invite immediately
|
||||
* Implement docker addon
|
||||
* Automatically login after password reset and account setup
|
||||
* Make backup interval configurable
|
||||
* Fix alternate domain certificate renewal
|
||||
* API token can now have a name
|
||||
|
||||
[3.1.3]
|
||||
* Prevent dashboard domain from being deleted
|
||||
* Add alternateDomains to app install route
|
||||
|
||||
[3.1.4]
|
||||
* Fix issue where support tab was redirecting
|
||||
|
||||
|
||||
@@ -630,7 +630,7 @@ state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
box
|
||||
Copyright (C) 2016,2017,2018 Cloudron UG
|
||||
Copyright (C) 2016,2017 Cloudron UG
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
|
||||
@@ -48,11 +48,6 @@ apps up-to-date and secure.
|
||||
* [Selfhosting](https://cloudron.io/documentation/installation/) - [Pricing](https://cloudron.io/pricing.html)
|
||||
* [Managed Hosting](https://cloudron.io/managed.html)
|
||||
|
||||
**Note:** This repo is a small part of what gets installed on your server - there is
|
||||
the dashboard, database addons, graph container, base image etc. Cloudron also relies
|
||||
on external services such as the App Store for apps to be installed. As such, don't
|
||||
clone this repo and npm install and expect something to work.
|
||||
|
||||
## Documentation
|
||||
|
||||
* [Documentation](https://cloudron.io/documentation/)
|
||||
@@ -64,6 +59,6 @@ the containers in the Cloudron.
|
||||
|
||||
## 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"
|
||||
|
||||
|
||||
@@ -14,11 +14,8 @@ function die {
|
||||
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# hold grub since updating it breaks on some VPS providers. also, dist-upgrade will trigger it
|
||||
apt-mark hold grub* >/dev/null
|
||||
apt-get -o Dpkg::Options::="--force-confdef" update -y
|
||||
apt-get -o Dpkg::Options::="--force-confdef" upgrade -y
|
||||
apt-mark unhold grub* >/dev/null
|
||||
apt-get -o Dpkg::Options::="--force-confdef" dist-upgrade -y
|
||||
|
||||
echo "==> Installing required packages"
|
||||
|
||||
@@ -64,7 +61,7 @@ echo "==> Installing Docker"
|
||||
mkdir -p /etc/systemd/system/docker.service.d
|
||||
echo -e "[Service]\nExecStart=\nExecStart=/usr/bin/dockerd -H fd:// --log-driver=journald --exec-opt native.cgroupdriver=cgroupfs --storage-driver=overlay2" > /etc/systemd/system/docker.service.d/cloudron.conf
|
||||
|
||||
curl -sL https://download.docker.com/linux/ubuntu/dists/xenial/pool/stable/amd64/docker-ce_18.03.1~ce-0~ubuntu_amd64.deb -o /tmp/docker.deb
|
||||
curl -sL https://download.docker.com/linux/ubuntu/dists/xenial/pool/stable/amd64/docker-ce_17.09.0~ce-0~ubuntu_amd64.deb -o /tmp/docker.deb
|
||||
# apt install with install deps (as opposed to dpkg -i)
|
||||
apt install -y /tmp/docker.deb
|
||||
rm /tmp/docker.deb
|
||||
@@ -75,9 +72,8 @@ if [[ "${storage_driver}" != "overlay2" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# do not upgrade grub because it might prompt user and break this script
|
||||
echo "==> Enable memory accounting"
|
||||
apt-get -y --no-upgrade install grub2-common
|
||||
apt-get -y install grub2
|
||||
sed -e 's/^GRUB_CMDLINE_LINUX="\(.*\)"$/GRUB_CMDLINE_LINUX="\1 cgroup_enable=memory swapaccount=1 panic_on_oops=1 panic=5"/' -i /etc/default/grub
|
||||
update-grub
|
||||
|
||||
@@ -109,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
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ var appHealthMonitor = require('./src/apphealthmonitor.js'),
|
||||
async = require('async'),
|
||||
config = require('./src/config.js'),
|
||||
ldap = require('./src/ldap.js'),
|
||||
dockerProxy = require('./src/dockerproxy.js'),
|
||||
server = require('./src/server.js');
|
||||
|
||||
console.log();
|
||||
@@ -26,9 +25,6 @@ console.log(' Version: ', config.version());
|
||||
console.log(' Admin Origin: ', config.adminOrigin());
|
||||
console.log(' Appstore API server origin: ', config.apiServerOrigin());
|
||||
console.log(' Appstore Web server origin: ', config.webServerOrigin());
|
||||
console.log(' SysAdmin Port: ', config.get('sysadminPort'));
|
||||
console.log(' LDAP Server Port: ', config.get('ldapPort'));
|
||||
console.log(' Docker Proxy Port: ', config.get('dockerProxyPort'));
|
||||
console.log();
|
||||
console.log('==========================================');
|
||||
console.log();
|
||||
@@ -36,7 +32,6 @@ console.log();
|
||||
async.series([
|
||||
server.start,
|
||||
ldap.start,
|
||||
dockerProxy.start,
|
||||
appHealthMonitor.start,
|
||||
], function (error) {
|
||||
if (error) {
|
||||
@@ -51,13 +46,11 @@ var NOOP_CALLBACK = function () { };
|
||||
process.on('SIGINT', function () {
|
||||
server.stop(NOOP_CALLBACK);
|
||||
ldap.stop(NOOP_CALLBACK);
|
||||
dockerProxy.stop(NOOP_CALLBACK);
|
||||
setTimeout(process.exit.bind(process), 3000);
|
||||
});
|
||||
|
||||
process.on('SIGTERM', function () {
|
||||
server.stop(NOOP_CALLBACK);
|
||||
ldap.stop(NOOP_CALLBACK);
|
||||
dockerProxy.stop(NOOP_CALLBACK);
|
||||
setTimeout(process.exit.bind(process), 3000);
|
||||
});
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
var database = require('./src/database.js');
|
||||
|
||||
var sendFailureLogs = require('./src/logcollector').sendFailureLogs;
|
||||
|
||||
function main() {
|
||||
@@ -12,12 +10,7 @@ function main() {
|
||||
var processName = process.argv[2];
|
||||
console.log('Started crash notifier for', processName);
|
||||
|
||||
// mailer needs the db
|
||||
database.initialize(function (error) {
|
||||
if (error) return console.error('Cannot connect to database. Unable to send crash log.', error);
|
||||
|
||||
sendFailureLogs(processName, { unit: processName });
|
||||
});
|
||||
sendFailureLogs(processName, { unit: processName });
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
+257
@@ -0,0 +1,257 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
var argv = require('yargs').argv,
|
||||
autoprefixer = require('gulp-autoprefixer'),
|
||||
concat = require('gulp-concat'),
|
||||
cssnano = require('gulp-cssnano'),
|
||||
ejs = require('gulp-ejs'),
|
||||
gulp = require('gulp'),
|
||||
rimraf = require('rimraf'),
|
||||
sass = require('gulp-sass'),
|
||||
serve = require('gulp-serve'),
|
||||
sourcemaps = require('gulp-sourcemaps'),
|
||||
uglify = require('gulp-uglify'),
|
||||
url = require('url');
|
||||
|
||||
gulp.task('3rdparty', function () {
|
||||
gulp.src([
|
||||
'webadmin/src/3rdparty/**/*.js',
|
||||
'webadmin/src/3rdparty/**/*.map',
|
||||
'webadmin/src/3rdparty/**/*.css',
|
||||
'webadmin/src/3rdparty/**/*.otf',
|
||||
'webadmin/src/3rdparty/**/*.eot',
|
||||
'webadmin/src/3rdparty/**/*.svg',
|
||||
'webadmin/src/3rdparty/**/*.gif',
|
||||
'webadmin/src/3rdparty/**/*.ttf',
|
||||
'webadmin/src/3rdparty/**/*.woff',
|
||||
'webadmin/src/3rdparty/**/*.woff2'
|
||||
])
|
||||
.pipe(gulp.dest('webadmin/dist/3rdparty/'))
|
||||
.pipe(gulp.dest('setup/splash/website/3rdparty'));
|
||||
|
||||
gulp.src('node_modules/bootstrap-sass/assets/javascripts/bootstrap.min.js')
|
||||
.pipe(gulp.dest('webadmin/dist/3rdparty/js'))
|
||||
.pipe(gulp.dest('setup/splash/website/3rdparty/js'));
|
||||
});
|
||||
|
||||
|
||||
// --------------
|
||||
// JavaScript
|
||||
// --------------
|
||||
|
||||
if (argv.help || argv.h) {
|
||||
console.log('Supported arguments for "gulp develop":');
|
||||
console.log(' --client-id <clientId>');
|
||||
console.log(' --client-secret <clientSecret>');
|
||||
console.log(' --api-origin <cloudron api uri>');
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
gulp.task('js', ['js-index', 'js-logs', 'js-terminal', 'js-setup', 'js-setupdns', 'js-restore', 'js-update'], function () {});
|
||||
|
||||
var oauth = {
|
||||
clientId: argv.clientId || 'cid-webadmin',
|
||||
clientSecret: argv.clientSecret || 'unused',
|
||||
apiOrigin: argv.apiOrigin || '',
|
||||
apiOriginHostname: argv.apiOrigin ? url.parse(argv.apiOrigin).hostname : ''
|
||||
};
|
||||
|
||||
console.log();
|
||||
console.log('Using OAuth credentials:');
|
||||
console.log(' ClientId: %s', oauth.clientId);
|
||||
console.log(' ClientSecret: %s', oauth.clientSecret);
|
||||
console.log(' Cloudron API: %s', oauth.apiOrigin || 'default');
|
||||
console.log(' Cloudron Host: %s', oauth.apiOriginHostname);
|
||||
console.log();
|
||||
|
||||
|
||||
gulp.task('js-index', function () {
|
||||
// needs special treatment for error handling
|
||||
var uglifyer = uglify();
|
||||
uglifyer.on('error', function (error) {
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
gulp.src([
|
||||
'webadmin/src/js/index.js',
|
||||
'webadmin/src/js/client.js',
|
||||
'webadmin/src/js/appstore.js',
|
||||
'webadmin/src/js/main.js',
|
||||
'webadmin/src/views/*.js'
|
||||
])
|
||||
.pipe(ejs({ oauth: oauth }, {}, { ext: '.js' }))
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(concat('index.js', { newLine: ';' }))
|
||||
.pipe(uglifyer)
|
||||
.pipe(sourcemaps.write())
|
||||
.pipe(gulp.dest('webadmin/dist/js'));
|
||||
});
|
||||
|
||||
gulp.task('js-logs', function () {
|
||||
// needs special treatment for error handling
|
||||
var uglifyer = uglify();
|
||||
uglifyer.on('error', function (error) {
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
gulp.src(['webadmin/src/js/logs.js', 'webadmin/src/js/client.js'])
|
||||
.pipe(ejs({ oauth: oauth }, {}, { ext: '.js' }))
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(concat('logs.js', { newLine: ';' }))
|
||||
.pipe(uglifyer)
|
||||
.pipe(sourcemaps.write())
|
||||
.pipe(gulp.dest('webadmin/dist/js'));
|
||||
});
|
||||
|
||||
gulp.task('js-terminal', function () {
|
||||
// needs special treatment for error handling
|
||||
var uglifyer = uglify();
|
||||
uglifyer.on('error', function (error) {
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
gulp.src(['webadmin/src/js/terminal.js', 'webadmin/src/js/client.js'])
|
||||
.pipe(ejs({ oauth: oauth }, {}, { ext: '.js' }))
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(concat('terminal.js', { newLine: ';' }))
|
||||
.pipe(uglifyer)
|
||||
.pipe(sourcemaps.write())
|
||||
.pipe(gulp.dest('webadmin/dist/js'));
|
||||
});
|
||||
|
||||
gulp.task('js-setup', function () {
|
||||
// needs special treatment for error handling
|
||||
var uglifyer = uglify();
|
||||
uglifyer.on('error', function (error) {
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
gulp.src(['webadmin/src/js/setup.js', 'webadmin/src/js/client.js'])
|
||||
.pipe(ejs({ oauth: oauth }, {}, { ext: '.js' }))
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(concat('setup.js', { newLine: ';' }))
|
||||
.pipe(uglifyer)
|
||||
.pipe(sourcemaps.write())
|
||||
.pipe(gulp.dest('webadmin/dist/js'));
|
||||
});
|
||||
|
||||
gulp.task('js-setupdns', function () {
|
||||
// needs special treatment for error handling
|
||||
var uglifyer = uglify();
|
||||
uglifyer.on('error', function (error) {
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
gulp.src(['webadmin/src/js/setupdns.js', 'webadmin/src/js/client.js'])
|
||||
.pipe(ejs({ oauth: oauth }, {}, { ext: '.js' }))
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(concat('setupdns.js', { newLine: ';' }))
|
||||
.pipe(uglifyer)
|
||||
.pipe(sourcemaps.write())
|
||||
.pipe(gulp.dest('webadmin/dist/js'));
|
||||
});
|
||||
|
||||
gulp.task('js-restore', function () {
|
||||
// needs special treatment for error handling
|
||||
var uglifyer = uglify();
|
||||
uglifyer.on('error', function (error) {
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
gulp.src(['webadmin/src/js/restore.js', 'webadmin/src/js/client.js'])
|
||||
.pipe(ejs({ oauth: oauth }, {}, { ext: '.js' }))
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(concat('restore.js', { newLine: ';' }))
|
||||
.pipe(uglifyer)
|
||||
.pipe(sourcemaps.write())
|
||||
.pipe(gulp.dest('webadmin/dist/js'));
|
||||
});
|
||||
|
||||
|
||||
gulp.task('js-update', function () {
|
||||
// needs special treatment for error handling
|
||||
var uglifyer = uglify();
|
||||
uglifyer.on('error', function (error) {
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
gulp.src(['webadmin/src/js/update.js'])
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(uglifyer)
|
||||
.pipe(sourcemaps.write())
|
||||
.pipe(gulp.dest('webadmin/dist/js'))
|
||||
.pipe(gulp.dest('setup/splash/website/js'));
|
||||
});
|
||||
|
||||
|
||||
// --------------
|
||||
// HTML
|
||||
// --------------
|
||||
|
||||
gulp.task('html', ['html-views', 'html-update', 'html-templates'], function () {
|
||||
return gulp.src('webadmin/src/*.html').pipe(ejs({ apiOriginHostname: oauth.apiOriginHostname }, {}, { ext: '.html' })).pipe(gulp.dest('webadmin/dist'));
|
||||
});
|
||||
|
||||
gulp.task('html-update', function () {
|
||||
return gulp.src(['webadmin/src/update.html']).pipe(gulp.dest('setup/splash/website'));
|
||||
});
|
||||
|
||||
gulp.task('html-views', function () {
|
||||
return gulp.src('webadmin/src/views/**/*.html').pipe(gulp.dest('webadmin/dist/views'));
|
||||
});
|
||||
|
||||
gulp.task('html-templates', function () {
|
||||
return gulp.src('webadmin/src/templates/**/*.html').pipe(gulp.dest('webadmin/dist/templates'));
|
||||
});
|
||||
|
||||
// --------------
|
||||
// CSS
|
||||
// --------------
|
||||
|
||||
gulp.task('css', function () {
|
||||
return gulp.src('webadmin/src/*.scss')
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(sass({ includePaths: ['node_modules/bootstrap-sass/assets/stylesheets/'] }).on('error', sass.logError))
|
||||
.pipe(autoprefixer())
|
||||
.pipe(cssnano())
|
||||
.pipe(sourcemaps.write())
|
||||
.pipe(gulp.dest('webadmin/dist'))
|
||||
.pipe(gulp.dest('setup/splash/website'));
|
||||
});
|
||||
|
||||
gulp.task('images', function () {
|
||||
return gulp.src('webadmin/src/img/**')
|
||||
.pipe(gulp.dest('webadmin/dist/img'));
|
||||
});
|
||||
|
||||
// --------------
|
||||
// Utilities
|
||||
// --------------
|
||||
|
||||
gulp.task('watch', ['default'], function () {
|
||||
gulp.watch(['webadmin/src/*.scss'], ['css']);
|
||||
gulp.watch(['webadmin/src/img/*'], ['images']);
|
||||
gulp.watch(['webadmin/src/**/*.html'], ['html']);
|
||||
gulp.watch(['webadmin/src/views/*.html'], ['html-views']);
|
||||
gulp.watch(['webadmin/src/templates/*.html'], ['html-templates']);
|
||||
gulp.watch(['webadmin/src/js/update.js'], ['js-update']);
|
||||
gulp.watch(['webadmin/src/js/setup.js', 'webadmin/src/js/client.js'], ['js-setup']);
|
||||
gulp.watch(['webadmin/src/js/setupdns.js', 'webadmin/src/js/client.js'], ['js-setupdns']);
|
||||
gulp.watch(['webadmin/src/js/restore.js', 'webadmin/src/js/client.js'], ['js-restore']);
|
||||
gulp.watch(['webadmin/src/js/logs.js', 'webadmin/src/js/client.js'], ['js-logs']);
|
||||
gulp.watch(['webadmin/src/js/terminal.js', 'webadmin/src/js/client.js'], ['js-terminal']);
|
||||
gulp.watch(['webadmin/src/js/index.js', 'webadmin/src/js/client.js', 'webadmin/src/js/appstore.js', 'webadmin/src/js/main.js', 'webadmin/src/views/*.js'], ['js-index']);
|
||||
gulp.watch(['webadmin/src/3rdparty/**/*'], ['3rdparty']);
|
||||
});
|
||||
|
||||
gulp.task('clean', function () {
|
||||
rimraf.sync('webadmin/dist');
|
||||
rimraf.sync('setup/splash/website');
|
||||
});
|
||||
|
||||
gulp.task('default', ['clean', 'html', 'js', '3rdparty', 'images', 'css'], function () {});
|
||||
|
||||
gulp.task('develop', ['watch'], serve({ root: 'webadmin/dist', port: 4000 }));
|
||||
@@ -1,24 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
var cmd = 'CREATE TABLE IF NOT EXISTS mail(' +
|
||||
'domain VARCHAR(128) NOT NULL UNIQUE,' +
|
||||
'enabled BOOLEAN DEFAULT 0,' +
|
||||
'mailFromValidation BOOLEAN DEFAULT 1,' +
|
||||
'catchAllJson TEXT,' +
|
||||
'relayJson TEXT,' +
|
||||
'FOREIGN KEY(domain) REFERENCES domains(domain),' +
|
||||
'PRIMARY KEY(domain)) CHARACTER SET utf8 COLLATE utf8_bin';
|
||||
|
||||
db.runSql(cmd, function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('DROP TABLE mail', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -1,34 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.all('SELECT * FROM domains', function (error, domains) {
|
||||
if (error) return callback(error);
|
||||
if (domains.length === 0) return callback();
|
||||
|
||||
db.all('SELECT * FROM settings', function (error, allSettings) {
|
||||
if (error) return callback(error);
|
||||
|
||||
// defaults
|
||||
var mailFromValidation = true;
|
||||
var catchAll = [ ];
|
||||
var relay = { provider: 'cloudron-smtp' };
|
||||
var mailEnabled = false;
|
||||
|
||||
allSettings.forEach(function (setting) {
|
||||
switch (setting.name) {
|
||||
case 'mail_from_validation': mailFromValidation = !!setting.value; break;
|
||||
case 'catch_all_address': catchAll = JSON.parse(setting.value); break;
|
||||
case 'mail_relay': relay = JSON.parse(setting.value); break;
|
||||
case 'mail_config': mailEnabled = JSON.parse(setting.value).enabled; break;
|
||||
}
|
||||
});
|
||||
|
||||
db.runSql('INSERT INTO mail (domain, enabled, mailFromValidation, catchAllJson, relayJson) VALUES (?, ?, ?, ?, ?)',
|
||||
[ domains[0].domain, mailEnabled, mailFromValidation, JSON.stringify(catchAll), JSON.stringify(relay) ], callback);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
@@ -1,44 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.all('SELECT * FROM users', [ ], function (error, users) {
|
||||
if (error) return callback(error);
|
||||
|
||||
db.all('SELECT * FROM mail WHERE enabled=1', [ ], function (error, mailDomains) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.series([
|
||||
db.runSql.bind(db, 'START TRANSACTION;'),
|
||||
db.runSql.bind(db, 'ALTER TABLE users DROP INDEX users_email'),
|
||||
db.runSql.bind(db, 'ALTER TABLE users ADD COLUMN fallbackEmail VARCHAR(512) DEFAULT ""'),
|
||||
function setDefaults(done) {
|
||||
async.eachSeries(users, function (user, iteratorCallback) {
|
||||
var defaultEmail = '';
|
||||
var fallbackEmail = '';
|
||||
|
||||
if (mailDomains.length === 0) {
|
||||
defaultEmail = user.email;
|
||||
fallbackEmail = user.email;
|
||||
} else {
|
||||
defaultEmail = user.username ? (user.username + '@' + mailDomains[0].domain) : user.email;
|
||||
fallbackEmail = user.email;
|
||||
}
|
||||
|
||||
db.runSql('UPDATE users SET email = ?, fallbackEmail = ? WHERE id = ?', [ defaultEmail, fallbackEmail, user.id ], iteratorCallback);
|
||||
}, done);
|
||||
},
|
||||
db.runSql.bind(db, 'ALTER TABLE users ADD UNIQUE users_email (email)'),
|
||||
db.runSql.bind(db, 'COMMIT')
|
||||
], callback);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE users DROP COLUMN fallbackEmail', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -1,26 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.all('SELECT * FROM settings WHERE name = ?', [ 'tls_config' ], function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var tlsConfig = (result[0] && result[0].value) ? JSON.parse(result[0].value) : { provider: 'letsencrypt-prod'};
|
||||
tlsConfig.provider = tlsConfig.provider.replace(/$le\-/, 'letsencrypt-'); // old cloudrons had le-prod/le-staging
|
||||
|
||||
async.series([
|
||||
db.runSql.bind(db, 'START TRANSACTION;'),
|
||||
db.runSql.bind(db, 'ALTER TABLE domains ADD COLUMN tlsConfigJson TEXT'),
|
||||
db.runSql.bind(db, 'UPDATE domains SET tlsConfigJson = ?', [ JSON.stringify(tlsConfig) ]),
|
||||
db.runSql.bind(db, 'COMMIT')
|
||||
], callback);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE domains DROP COLUMN tlsConfigJson', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -1,47 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async'),
|
||||
fs = require('fs'),
|
||||
superagent = require('superagent');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
if (!fs.existsSync('/home/yellowtent/configs/cloudron.conf')) {
|
||||
console.log('Unable to locate cloudron.conf');
|
||||
return callback();
|
||||
}
|
||||
|
||||
var config = JSON.parse(fs.readFileSync('/home/yellowtent/configs/cloudron.conf', 'utf8'));
|
||||
|
||||
if (config.provider !== 'caas' || !config.fqdn) {
|
||||
console.log('Not caas (%s) or no fqdn', config.provider, config.fqdn);
|
||||
return callback();
|
||||
}
|
||||
|
||||
db.runSql('SELECT COUNT(*) AS total FROM users', function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (result[0].total === 0) {
|
||||
console.log('This cloudron is not activated. It will automatically get appstore and caas configs from autoprovision logic');
|
||||
return callback();
|
||||
}
|
||||
|
||||
console.log('Downloading appstore and caas config');
|
||||
|
||||
superagent.get(config.apiServerOrigin + `/api/v1/boxes/${config.fqdn}/config`)
|
||||
.query({ token: config.token })
|
||||
.timeout(30 * 1000).end(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
console.log('Adding %j config', result.body);
|
||||
|
||||
async.series([
|
||||
db.runSql.bind(db, 'INSERT settings (name, value) VALUES(?, ?)', [ 'appstore_config', JSON.stringify(result.body.appstoreConfig) ]),
|
||||
db.runSql.bind(db, 'INSERT settings (name, value) VALUES(?, ?)', [ 'caas_config', JSON.stringify(result.body.caasConfig) ])
|
||||
], callback);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
@@ -1,24 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('SELECT * FROM settings WHERE name=?', ['autoupdate_pattern'], function (error, results) {
|
||||
if (error || results.length === 0) return callback(error); // will use defaults from box code
|
||||
|
||||
// migrate the 'daily' update pattern
|
||||
var appUpdatePattern = results[0].value;
|
||||
if (appUpdatePattern === '00 00 1,3,5,23 * * *') appUpdatePattern = '00 30 1,3,5,23 * * *';
|
||||
|
||||
async.series([
|
||||
db.runSql.bind(db, 'START TRANSACTION;'),
|
||||
db.runSql.bind(db, 'DELETE FROM settings WHERE name=?', ['autoupdate_pattern']),
|
||||
db.runSql.bind(db, 'INSERT settings (name, value) VALUES(?, ?)', ['app_autoupdate_pattern', appUpdatePattern]),
|
||||
db.runSql.bind(db, 'COMMIT')
|
||||
], callback);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
@@ -1,121 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async'),
|
||||
crypto = require('crypto'),
|
||||
fs = require('fs'),
|
||||
os = require('os'),
|
||||
path = require('path'),
|
||||
safe = require('safetydance'),
|
||||
tldjs = require('tldjs');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.all('SELECT * FROM apps', function (error, apps) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.eachSeries(apps, function (app, callback) {
|
||||
if (!app.altDomain) {
|
||||
console.log('App %s does not use altDomain, skip', app.id);
|
||||
return callback();
|
||||
}
|
||||
|
||||
const domain = tldjs.getDomain(app.altDomain);
|
||||
const subdomain = tldjs.getSubdomain(app.altDomain);
|
||||
const mailboxName = (subdomain ? subdomain : JSON.parse(app.manifestJson).title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app';
|
||||
|
||||
console.log('App %s is on domain %s and subdomain %s with mailbox', app.id, domain, subdomain, mailboxName);
|
||||
|
||||
async.series([
|
||||
// Add domain if not exists
|
||||
function (callback) {
|
||||
const query = 'INSERT INTO domains (domain, zoneName, provider, configJson, tlsConfigJson) VALUES (?, ?, ?, ?, ?)';
|
||||
const args = [ domain, domain, 'manual', JSON.stringify({}), JSON.stringify({ provider: 'letsencrypt-prod' }) ];
|
||||
|
||||
db.runSql(query, args, function (error) {
|
||||
if (error && error.code !== 'ER_DUP_ENTRY') return callback(error);
|
||||
|
||||
console.log('Added domain %s', domain);
|
||||
|
||||
// ensure we have a fallback cert for the newly added domain. This is the same as in reverseproxy.js
|
||||
// WARNING this will only work on the cloudron itself not during local testing!
|
||||
const certFilePath = `/home/yellowtent/boxdata/certs/${domain}.host.cert`;
|
||||
const keyFilePath = `/home/yellowtent/boxdata/certs/${domain}.host.key`;
|
||||
|
||||
if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) { // generate it
|
||||
let opensslConf = safe.fs.readFileSync('/etc/ssl/openssl.cnf', 'utf8');
|
||||
let opensslConfWithSan = `${opensslConf}\n[SAN]\nsubjectAltName=DNS:${domain}\n`;
|
||||
let configFile = path.join(os.tmpdir(), 'openssl-' + crypto.randomBytes(4).readUInt32LE(0) + '.conf');
|
||||
let certCommand = `openssl req -x509 -newkey rsa:2048 -keyout ${keyFilePath} -out ${certFilePath} -days 3650 -subj /CN=*.${domain} -extensions SAN -config ${configFile} -nodes`;
|
||||
|
||||
safe.fs.writeFileSync(configFile, opensslConfWithSan, 'utf8');
|
||||
if (!safe.child_process.execSync(certCommand)) return callback(safe.error.message);
|
||||
safe.fs.unlinkSync(configFile);
|
||||
}
|
||||
|
||||
callback();
|
||||
});
|
||||
},
|
||||
// Add domain to mail table if not exists
|
||||
function (callback) {
|
||||
const query = 'INSERT INTO mail (domain, enabled, mailFromValidation, catchAllJson, relayJson) VALUES (?, ?, ?, ?, ?)';
|
||||
const args = [ domain, 0, 1, '[]', JSON.stringify({ provider: 'cloudron-smtp' }) ];
|
||||
|
||||
db.runSql(query, args, function (error) {
|
||||
if (error && error.code !== 'ER_DUP_ENTRY') return callback(error);
|
||||
|
||||
console.log('Added domain %s to mail table', domain);
|
||||
|
||||
callback();
|
||||
});
|
||||
},
|
||||
// Remove old mailbox record if any
|
||||
function (callback) {
|
||||
const query = 'DELETE FROM mailboxes WHERE ownerId=?';
|
||||
const args = [ app.id ];
|
||||
|
||||
db.runSql(query, args, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
console.log('Cleaned up mailbox record for app %s', app.id);
|
||||
|
||||
callback();
|
||||
});
|
||||
},
|
||||
// Add new mailbox record
|
||||
function (callback) {
|
||||
const query = 'INSERT INTO mailboxes (name, domain, ownerId, ownerType) VALUES (?, ?, ?, ?)';
|
||||
const args = [ mailboxName, domain, app.id, 'app' /* mailboxdb.TYPE_APP */ ];
|
||||
|
||||
db.runSql(query, args, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
console.log('Added mailbox record for app %s', app.id);
|
||||
|
||||
callback();
|
||||
});
|
||||
},
|
||||
// Update app record
|
||||
function (callback) {
|
||||
const query = 'UPDATE apps SET location=?, domain=?, altDomain=? WHERE id=?';
|
||||
const args = [ subdomain, domain, '', app.id ];
|
||||
|
||||
db.runSql(query, args, function (error) {
|
||||
if (error) return error;
|
||||
|
||||
console.log('Updated app %s with new domain', app.id);
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
], callback);
|
||||
}, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
// finally drop the altDomain db field
|
||||
db.runSql('ALTER TABLE apps DROP COLUMN altDomain', [], callback);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps ADD COLUMN altDomain VARCHAR(256)', [], callback);
|
||||
};
|
||||
@@ -1,19 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
async.series([
|
||||
db.runSql.bind(db, 'START TRANSACTION;'),
|
||||
db.runSql.bind(db, 'ALTER TABLE mailboxes DROP FOREIGN KEY mailboxes_domain_constraint'),
|
||||
db.runSql.bind(db, 'ALTER TABLE mailboxes ADD CONSTRAINT mailboxes_domain_constraint FOREIGN KEY(domain) REFERENCES mail(domain)'),
|
||||
db.runSql.bind(db, 'COMMIT')
|
||||
], callback);
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE mailboxes DROP FOREIGN KEY mailboxes_domain_constraint', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -1,51 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
var users = { }, groupMembers = { };
|
||||
|
||||
async.series([
|
||||
db.runSql.bind(db, 'START TRANSACTION;'),
|
||||
db.runSql.bind(db, 'ALTER TABLE mailboxes ADD COLUMN membersJson TEXT'),
|
||||
function getUsers(done) {
|
||||
db.all('SELECT * from users', [ ], function (error, results) {
|
||||
if (error) return done(error);
|
||||
|
||||
results.forEach(function (result) { users[result.id] = result; });
|
||||
|
||||
done();
|
||||
});
|
||||
},
|
||||
function getGroups(done) {
|
||||
db.all('SELECT id, name, GROUP_CONCAT(groupMembers.userId) AS userIds ' +
|
||||
' FROM groups LEFT OUTER JOIN groupMembers ON groups.id = groupMembers.groupId ' +
|
||||
' GROUP BY groups.id', [ ], function (error, results) {
|
||||
if (error) return done(error);
|
||||
|
||||
results.forEach(function (result) {
|
||||
var userIds = result.userIds ? result.userIds.split(',') : [];
|
||||
var members = userIds.map(function (id) { return users[id].username; });
|
||||
groupMembers[result.id] = members;
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
},
|
||||
function removeGroupIdAndSetMembers(done) {
|
||||
async.eachSeries(Object.keys(groupMembers), function (gid, iteratorDone) {
|
||||
console.log(`Migrating group id ${gid} to ${JSON.stringify(groupMembers[gid])}`);
|
||||
|
||||
db.runSql('UPDATE mailboxes SET membersJson = ?, ownerId = ? WHERE ownerId = ?', [ JSON.stringify(groupMembers[gid]), 'admin', gid ], iteratorDone);
|
||||
}, done);
|
||||
},
|
||||
db.runSql.bind(db, 'COMMIT')
|
||||
], callback);
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE mailboxes DROP COLUMN membersJson', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -1,34 +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 ADD COLUMN type VARCHAR(16)'),
|
||||
function addMailboxType(done) {
|
||||
db.all('SELECT * from mailboxes', [ ], function (error, results) {
|
||||
if (error) return done(error);
|
||||
|
||||
async.eachSeries(results, function (mailbox, iteratorCallback) {
|
||||
let type = 'mailbox';
|
||||
if (mailbox.aliasTarget) {
|
||||
type = 'alias';
|
||||
} else if (mailbox.membersJson) {
|
||||
type = 'list';
|
||||
}
|
||||
db.runSql('UPDATE mailboxes SET type = ? WHERE name = ? AND domain = ?', [ type, mailbox.name, mailbox.domain ], iteratorCallback);
|
||||
}, done);
|
||||
});
|
||||
},
|
||||
db.runSql.bind(db, 'ALTER TABLE mailboxes MODIFY type VARCHAR(16) NOT NULL'),
|
||||
db.runSql.bind(db, 'COMMIT')
|
||||
], callback);
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE mailboxes DROP COLUMN membersJson', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -1,15 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE users ADD COLUMN twoFactorAuthenticationSecret VARCHAR(128) DEFAULT "", ADD COLUMN twoFactorAuthenticationEnabled BOOLEAN DEFAULT false', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE users DROP twoFactorAuthenticationSecret, DROP twoFactorAuthenticationEnabled', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('UPDATE clients SET scope=? WHERE id=? OR id=? OR id=?', ['*', 'cid-webadmin', 'cid-sdk', 'cid-cli'], function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
db.runSql('UPDATE tokens SET scope=? WHERE scope LIKE ?', ['*', '%*%'], function (error) { // remove the roleSdk
|
||||
if (error) console.error(error);
|
||||
|
||||
db.runSql('UPDATE tokens SET expires=? WHERE clientId=?', [ 1525636734905, 'cid-webadmin' ], function (error) { // force webadmin to get a new token
|
||||
if (error) console.error(error);
|
||||
|
||||
callback(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
@@ -1,28 +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 apps ADD COLUMN ownerId VARCHAR(128)'),
|
||||
function (next) {
|
||||
db.all('SELECT id FROM users ORDER BY createdAt LIMIT 1', [ ], function (error, results) {
|
||||
if (error || results.length === 0) return next(error);
|
||||
|
||||
var ownerId = results[0].id;
|
||||
db.runSql('UPDATE apps SET ownerId=?', [ ownerId ], next);
|
||||
});
|
||||
},
|
||||
db.runSql.bind(db, 'ALTER TABLE apps MODIFY ownerId VARCHAR(128) NOT NULL'),
|
||||
db.runSql.bind(db, 'ALTER TABLE apps ADD CONSTRAINT apps_owner_constraint FOREIGN KEY(ownerId) REFERENCES users(id)'),
|
||||
db.runSql.bind(db, 'COMMIT'),
|
||||
], callback);
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps DROP COLUMN ownerId', 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 ts TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps DROP COLUMN ts ', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -1,25 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
var cmd = 'CREATE TABLE IF NOT EXISTS subdomains(' +
|
||||
'appId VARCHAR(128) NOT NULL,' +
|
||||
'domain VARCHAR(128) NOT NULL,' +
|
||||
'subdomain VARCHAR(128) NOT NULL,' +
|
||||
'type VARCHAR(128) NOT NULL,' +
|
||||
'dnsRecordId VARCHAR(512),' +
|
||||
'FOREIGN KEY(domain) REFERENCES domains(domain),' +
|
||||
'FOREIGN KEY(appId) REFERENCES apps(id),' +
|
||||
'UNIQUE (subdomain, 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 subdomains', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -1,28 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.all('SELECT * from apps', [ ], function (error, results) {
|
||||
if (error) return done(error);
|
||||
|
||||
var queries = [
|
||||
db.runSql.bind(db, 'START TRANSACTION;')
|
||||
];
|
||||
|
||||
results.forEach(function (app) {
|
||||
queries.push(db.runSql.bind(db, 'INSERT INTO subdomains (appId, domain, subdomain, type, dnsRecordId) VALUES (?, ?, ?, ?, ?)', [ app.id, app.domain, app.location, 'primary', app.dnsRecordId ]));
|
||||
});
|
||||
|
||||
queries.push(db.runSql.bind(db, 'COMMIT'));
|
||||
|
||||
async.series(queries, callback);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('DELETE FROM subdomains', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -1,41 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps DROP INDEX location_domain_unique_index, DROP FOREIGN KEY apps_domain_constraint, DROP COLUMN domain, DROP COLUMN location, DROP COLUMN dnsRecordId', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.all('SELECT * from subdomains WHERE type = ?', [ 'primary' ], function (error, results) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var cmd = 'ALTER TABLE apps'
|
||||
+ ' ADD COLUMN location VARCHAR(128),'
|
||||
+ ' ADD COLUMN domain VARCHAR(128),'
|
||||
+ ' ADD COLUMN dnsRecordId VARCHAR(512)';
|
||||
|
||||
db.runSql(cmd, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var queries = [ db.runSql.bind(db, 'START TRANSACTION;') ];
|
||||
results.forEach(function (d) {
|
||||
queries.push(db.runSql.bind(db, 'UPDATE apps SET domain = ?, location = ?, dnsRecordId = ? WHERE id = ?', [ d.domain, d.subdomain, d.appId, d.dnsRecordId ]));
|
||||
});
|
||||
queries.push(db.runSql.bind(db, 'COMMIT'));
|
||||
|
||||
async.series(queries, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var cmd = 'ALTER TABLE apps'
|
||||
+ ' ADD CONSTRAINT apps_domain_constraint FOREIGN KEY(domain) REFERENCES domains(domain),'
|
||||
+ ' ADD UNIQUE location_domain_unique_index (location, domain)';
|
||||
|
||||
db.runSql(cmd, callback);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -1,15 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE subdomains DROP COLUMN dnsRecordId', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE subdomains ADD COLUMN dnsRecordId VARCHAR(512)', function (error) {
|
||||
if (error) return callback(error);
|
||||
callback();
|
||||
});
|
||||
};
|
||||
@@ -1,34 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE users ADD COLUMN admin BOOLEAN DEFAULT 0', function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
db.all('SELECT userId FROM groupMembers WHERE groupId=?', [ 'admin' ], function (error, results) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (results.length === 0) return callback();
|
||||
|
||||
async.eachSeries(results, function (result, iteratorDone) {
|
||||
db.runSql('UPDATE users SET admin=1 WHERE id=?', [ result.userId ], iteratorDone);
|
||||
}, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.series([
|
||||
db.runSql.bind(db, 'DELETE FROM groupMembers WHERE groupId=?', [ 'admin' ]),
|
||||
db.runSql.bind(db, 'DELETE FROM groups WHERE id=?', [ 'admin' ])
|
||||
], callback);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE users DROP COLUMN admin', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('UPDATE tokens SET expires=? WHERE clientId=?', [ 1525636734905, 'cid-webadmin' ], function (error) { // force webadmin to get a new token
|
||||
if (error) console.error(error);
|
||||
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
async.series([
|
||||
db.runSql.bind(db, 'ALTER TABLE appPortBindings ADD COLUMN type VARCHAR(8) NOT NULL DEFAULT "tcp"'),
|
||||
db.runSql.bind(db, 'ALTER TABLE appPortBindings DROP INDEX hostPort'), // this drops the unique constraint
|
||||
db.runSql.bind(db, 'ALTER TABLE appPortBindings DROP PRIMARY KEY, ADD PRIMARY KEY(hostPort, type)')
|
||||
], callback);
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
async.series([
|
||||
db.runSql.bind(db, 'ALTER TABLE appPortBindings DROP PRIMARY KEY, ADD PRIMARY KEY(hostPort)'),
|
||||
db.runSql.bind(db, 'ALTER TABLE appPortBindings DROP COLUMN type')
|
||||
], callback);
|
||||
};
|
||||
@@ -1,16 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.all('SELECT value FROM settings WHERE name="backup_config"', function (error, results) {
|
||||
if (error || results.length === 0) return callback(error);
|
||||
|
||||
var backupConfig = JSON.parse(results[0].value);
|
||||
backupConfig.intervalSecs = 24 * 60 * 60;
|
||||
db.runSql('UPDATE settings SET value=? WHERE name="backup_config"', [ JSON.stringify(backupConfig) ], callback);
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
@@ -1,23 +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);
|
||||
|
||||
let caasDomains = domains.filter(function (d) { return d.provider === 'caas'; });
|
||||
|
||||
async.eachSeries(caasDomains, function (domain, iteratorCallback) {
|
||||
let config = JSON.parse(domain.configJson);
|
||||
config.hyphenatedSubdomains = true;
|
||||
|
||||
db.runSql('UPDATE domains SET configJson = ? WHERE domain = ?', [ JSON.stringify(config), domain.domain ], iteratorCallback);
|
||||
}, callback);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
@@ -1,12 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE tokens ADD COLUMN name VARCHAR(64) DEFAULT ""', [], callback);
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE tokens DROP COLUMN name', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
+21
-56
@@ -21,12 +21,8 @@ CREATE TABLE IF NOT EXISTS users(
|
||||
salt VARCHAR(512) NOT NULL,
|
||||
createdAt VARCHAR(512) NOT NULL,
|
||||
modifiedAt VARCHAR(512) NOT NULL,
|
||||
displayName VARCHAR(512) DEFAULT "",
|
||||
fallbackEmail VARCHAR(512) DEFAULT "",
|
||||
twoFactorAuthenticationSecret VARCHAR(128) DEFAULT "",
|
||||
twoFactorAuthenticationEnabled BOOLEAN DEFAULT false,
|
||||
admin BOOLEAN DEFAULT false,
|
||||
|
||||
admin INTEGER NOT NULL,
|
||||
displayName VARCHAR(512) DEFAULT '',
|
||||
PRIMARY KEY(id));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS groups(
|
||||
@@ -41,7 +37,6 @@ CREATE TABLE IF NOT EXISTS groupMembers(
|
||||
FOREIGN KEY(userId) REFERENCES users(id));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tokens(
|
||||
name VARCHAR(64) DEFAULT "", // description
|
||||
accessToken VARCHAR(128) NOT NULL UNIQUE,
|
||||
identifier VARCHAR(128) NOT NULL,
|
||||
clientId VARCHAR(128),
|
||||
@@ -51,7 +46,7 @@ CREATE TABLE IF NOT EXISTS tokens(
|
||||
|
||||
CREATE TABLE IF NOT EXISTS clients(
|
||||
id VARCHAR(128) NOT NULL UNIQUE, // prefixed with cid- to identify token easily in auth routes
|
||||
appId VARCHAR(128) NOT NULL, // name of the client (for external apps) or id of app (for built-in apps)
|
||||
appId VARCHAR(128) NOT NULL,
|
||||
type VARCHAR(16) NOT NULL,
|
||||
clientSecret VARCHAR(512) NOT NULL,
|
||||
redirectURI VARCHAR(512) NOT NULL,
|
||||
@@ -70,11 +65,12 @@ CREATE TABLE IF NOT EXISTS apps(
|
||||
httpPort INTEGER, // this is the nginx proxy port and not manifest.httpPort
|
||||
location VARCHAR(128) NOT NULL,
|
||||
domain VARCHAR(128) NOT NULL,
|
||||
dnsRecordId VARCHAR(512), // tracks any id that we got back to track dns updates
|
||||
accessRestrictionJson TEXT, // { users: [ ], groups: [ ] }
|
||||
creationTime TIMESTAMP(2) NOT NULL DEFAULT CURRENT_TIMESTAMP, // when the app was installed
|
||||
updateTime TIMESTAMP(2) NOT NULL DEFAULT CURRENT_TIMESTAMP, // when the last app update was done
|
||||
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, // when this db record was updated (useful for UI caching)
|
||||
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
|
||||
@@ -83,17 +79,14 @@ CREATE TABLE IF NOT EXISTS apps(
|
||||
|
||||
// the following fields do not belong here, they can be removed when we use a queue for apptask
|
||||
restoreConfigJson VARCHAR(256), // used to pass backupId to restore from to apptask
|
||||
oldConfigJson TEXT, // used to pass old config to apptask (configure, restore)
|
||||
updateConfigJson TEXT, // used to pass new config to apptask (update)
|
||||
oldConfigJson TEXT, // used to pass old config for apptask (configure, restore)
|
||||
updateConfigJson TEXT, // used to pass new config for apptask (update)
|
||||
|
||||
ownerId VARCHAR(128),
|
||||
|
||||
FOREIGN KEY(ownerId) REFERENCES users(id),
|
||||
FOREIGN KEY(domain) REFERENCES domains(domain),
|
||||
PRIMARY KEY(id));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS appPortBindings(
|
||||
hostPort INTEGER NOT NULL UNIQUE,
|
||||
type VARCHAR(8) NOT NULL DEFAULT "tcp",
|
||||
environmentVariable VARCHAR(128) NOT NULL,
|
||||
appId VARCHAR(128) NOT NULL,
|
||||
FOREIGN KEY(appId) REFERENCES apps(id),
|
||||
@@ -135,60 +128,32 @@ CREATE TABLE IF NOT EXISTS eventlog(
|
||||
action VARCHAR(128) NOT NULL,
|
||||
source TEXT, /* { userId, username, ip }. userId can be null for cron,sysadmin */
|
||||
data TEXT, /* free flowing json based on action */
|
||||
createdAt TIMESTAMP(2) NOT NULL,
|
||||
creationTime TIMESTAMP, /* FIXME: precision must be TIMESTAMP(2) */
|
||||
|
||||
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
|
||||
*/
|
||||
CREATE TABLE IF NOT EXISTS mailboxes(
|
||||
name VARCHAR(128) NOT NULL,
|
||||
type VARCHAR(16) NOT NULL, /* 'mailbox', 'alias', 'list' */
|
||||
ownerId VARCHAR(128) NOT NULL, /* app id or user id or group id */
|
||||
ownerType VARCHAR(16) NOT NULL, /* 'app' or 'user' or 'group' */
|
||||
aliasTarget VARCHAR(128), /* the target name type is an alias */
|
||||
membersJson TEXT, /* members of a group */
|
||||
creationTime TIMESTAMP,
|
||||
domain VARCHAR(128),
|
||||
|
||||
FOREIGN KEY(domain) REFERENCES mail(domain),
|
||||
UNIQUE (name, domain));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS subdomains(
|
||||
appId VARCHAR(128) NOT NULL,
|
||||
domain VARCHAR(128) NOT NULL,
|
||||
subdomain VARCHAR(128) NOT NULL,
|
||||
type VARCHAR(128) NOT NULL,
|
||||
|
||||
FOREIGN KEY(domain) REFERENCES domains(domain),
|
||||
FOREIGN KEY(appId) REFERENCES apps(id),
|
||||
UNIQUE (subdomain, domain))
|
||||
PRIMARY KEY (name));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS domains(
|
||||
domain VARCHAR(128) NOT NULL UNIQUE, /* if this needs to be larger, InnoDB has a limit of 767 bytes for PRIMARY KEY values! */
|
||||
zoneName VARCHAR(128) NOT NULL, /* this mostly contains the domain itself again */
|
||||
provider VARCHAR(16) NOT NULL,
|
||||
configJson TEXT, /* JSON containing the dns backend provider config */
|
||||
|
||||
PRIMARY KEY (domain))
|
||||
|
||||
/* the default db collation is utf8mb4_unicode_ci but for the app table domain constraint we have to use the old one */
|
||||
CHARACTER SET utf8 COLLATE utf8_bin;
|
||||
|
||||
Generated
+5933
-5995
File diff suppressed because it is too large
Load Diff
+48
-38
@@ -14,14 +14,13 @@
|
||||
"node": ">=4.0.0 <=4.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google-cloud/dns": "^0.7.2",
|
||||
"@google-cloud/storage": "^1.7.0",
|
||||
"@google-cloud/dns": "^0.7.0",
|
||||
"@google-cloud/storage": "^1.2.1",
|
||||
"@sindresorhus/df": "^2.1.0",
|
||||
"async": "^2.6.1",
|
||||
"aws-sdk": "^2.253.1",
|
||||
"body-parser": "^1.18.3",
|
||||
"cloudron-manifestformat": "^2.13.1",
|
||||
"connect": "^3.6.6",
|
||||
"async": "^2.6.0",
|
||||
"aws-sdk": "^2.151.0",
|
||||
"body-parser": "^1.18.2",
|
||||
"cloudron-manifestformat": "^2.11.0",
|
||||
"connect-ensure-login": "^0.1.1",
|
||||
"connect-lastmile": "^1.0.2",
|
||||
"connect-timeout": "^1.9.0",
|
||||
@@ -29,23 +28,24 @@
|
||||
"cookie-session": "^1.3.2",
|
||||
"cron": "^1.3.0",
|
||||
"csurf": "^1.6.6",
|
||||
"db-migrate": "^0.11.1",
|
||||
"db-migrate": "^0.10.0-beta.24",
|
||||
"db-migrate-mysql": "^1.1.10",
|
||||
"debug": "^3.1.0",
|
||||
"dockerode": "^2.5.5",
|
||||
"ejs": "^2.6.1",
|
||||
"ejs-cli": "^2.0.1",
|
||||
"express": "^4.16.3",
|
||||
"dockerode": "^2.5.3",
|
||||
"ejs": "^2.5.7",
|
||||
"ejs-cli": "^2.0.0",
|
||||
"express": "^4.16.2",
|
||||
"express-session": "^1.15.6",
|
||||
"hat": "0.0.3",
|
||||
"json": "^9.0.3",
|
||||
"ldapjs": "^1.0.2",
|
||||
"ldapjs": "^1.0.0",
|
||||
"lodash.chunk": "^4.2.0",
|
||||
"mime": "^2.3.1",
|
||||
"moment-timezone": "^0.5.17",
|
||||
"mime": "^2.0.3",
|
||||
"moment-timezone": "^0.5.14",
|
||||
"morgan": "^1.9.0",
|
||||
"multiparty": "^4.1.4",
|
||||
"multiparty": "^4.1.2",
|
||||
"mysql": "^2.15.0",
|
||||
"nodemailer": "^4.6.5",
|
||||
"nodemailer": "^4.4.0",
|
||||
"nodemailer-smtp-transport": "^2.7.4",
|
||||
"oauth2orize": "^1.11.0",
|
||||
"once": "^1.3.2",
|
||||
@@ -55,47 +55,57 @@
|
||||
"passport-http-bearer": "^1.0.1",
|
||||
"passport-local": "^1.0.0",
|
||||
"passport-oauth2-client-password": "^0.1.2",
|
||||
"password-generator": "^2.2.0",
|
||||
"progress-stream": "^2.0.0",
|
||||
"proxy-middleware": "^0.15.0",
|
||||
"qrcode": "^1.2.0",
|
||||
"readdirp": "^2.1.0",
|
||||
"request": "^2.87.0",
|
||||
"rimraf": "^2.6.2",
|
||||
"s3-block-read-stream": "^0.5.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",
|
||||
"showdown": "^1.8.6",
|
||||
"speakeasy": "^2.0.0",
|
||||
"semver": "^5.4.1",
|
||||
"showdown": "^1.8.2",
|
||||
"split": "^1.0.0",
|
||||
"superagent": "^3.8.3",
|
||||
"supererror": "^0.7.2",
|
||||
"tar-fs": "^1.16.2",
|
||||
"tar-stream": "^1.6.1",
|
||||
"tldjs": "^2.3.1",
|
||||
"underscore": "^1.9.1",
|
||||
"uuid": "^3.2.1",
|
||||
"superagent": "^3.8.1",
|
||||
"supererror": "^0.7.1",
|
||||
"tar-fs": "^1.16.0",
|
||||
"tar-stream": "^1.5.5",
|
||||
"tldjs": "^2.2.0",
|
||||
"underscore": "^1.7.0",
|
||||
"uuid": "^3.1.0",
|
||||
"valid-url": "^1.0.9",
|
||||
"validator": "^10.3.0",
|
||||
"ws": "^5.2.0"
|
||||
"validator": "^9.1.1",
|
||||
"ws": "^3.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"bootstrap-sass": "^3.3.3",
|
||||
"expect.js": "*",
|
||||
"gulp": "^3.9.1",
|
||||
"gulp-autoprefixer": "^4.0.0",
|
||||
"gulp-concat": "^2.4.3",
|
||||
"gulp-cssnano": "^2.1.0",
|
||||
"gulp-ejs": "^3.1.0",
|
||||
"gulp-sass": "^3.1.0",
|
||||
"gulp-serve": "^1.0.0",
|
||||
"gulp-sourcemaps": "^2.6.1",
|
||||
"gulp-uglify": "^3.0.0",
|
||||
"hock": "^1.3.2",
|
||||
"istanbul": "*",
|
||||
"js2xmlparser": "^3.0.0",
|
||||
"mocha": "^5.2.0",
|
||||
"mocha": "*",
|
||||
"mock-aws-s3": "git+https://github.com/cloudron-io/mock-aws-s3.git",
|
||||
"nock": "^9.0.14",
|
||||
"node-sass": "^4.6.1",
|
||||
"recursive-readdir": "^2.2.2"
|
||||
"readdirp": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz",
|
||||
"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",
|
||||
"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",
|
||||
"postmerge": "/bin/true",
|
||||
"precommit": "/bin/true",
|
||||
"prepush": "npm test",
|
||||
"dashboard": "node_modules/.bin/gulp"
|
||||
"webadmin": "node_modules/.bin/gulp"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400"
|
||||
|
||||
function get_status() {
|
||||
key="$1"
|
||||
if status=$($curl -q -f "http://localhost:3000/api/v1/cloudron/status" 2>/dev/null); then
|
||||
currentValue=$(echo "${status}" | python3 -c 'import sys, json; print(json.dumps(json.load(sys.stdin)[sys.argv[1]]))' "${key}")
|
||||
echo "${currentValue}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
function wait_for_status() {
|
||||
key="$1"
|
||||
expectedValue="$2"
|
||||
|
||||
echo "wait_for_status: $key to be $expectedValue"
|
||||
while true; do
|
||||
if currentValue=$(get_status "${key}"); then
|
||||
echo "wait_for_status: $key is current: $currentValue expecting: $expectedValue"
|
||||
if [[ "${currentValue}" == $expectedValue ]]; then
|
||||
break
|
||||
fi
|
||||
fi
|
||||
sleep 3
|
||||
done
|
||||
}
|
||||
|
||||
domain=""
|
||||
domainProvider=""
|
||||
domainConfigJson="{}"
|
||||
domainTlsProvider="letsencrypt-prod"
|
||||
adminUsername="superadmin"
|
||||
adminPassword="Secret123#"
|
||||
adminEmail="admin@server.local"
|
||||
appstoreUserId=""
|
||||
appstoreToken=""
|
||||
backupDir="/var/backups"
|
||||
|
||||
args=$(getopt -o "" -l "domain:,domain-provider:,domain-tls-provider:,admin-username:,admin-password:,admin-email:,appstore-user:,appstore-token:,backup-dir:" -n "$0" -- "$@")
|
||||
eval set -- "${args}"
|
||||
|
||||
while true; do
|
||||
case "$1" in
|
||||
--domain) domain="$2"; shift 2;;
|
||||
--domain-provider) domainProvider="$2"; shift 2;;
|
||||
--domain-tls-provider) domainTlsProvider="$2"; shift 2;;
|
||||
--admin-username) adminUsername="$2"; shift 2;;
|
||||
--admin-password) adminPassword="$2"; shift 2;;
|
||||
--admin-email) adminEmail="$2"; shift 2;;
|
||||
--appstore-user) appstoreUser="$2"; shift 2;;
|
||||
--appstore-token) appstoreToken="$2"; shift 2;;
|
||||
--backup-dir) backupDir="$2"; shift 2;;
|
||||
--) break;;
|
||||
*) echo "Unknown option $1"; exit 1;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo "=> Waiting for cloudron to be ready"
|
||||
wait_for_status "version" '*'
|
||||
|
||||
if [[ $(get_status "webadminStatus") != *'"tls": true'* ]]; then
|
||||
echo "=> Domain setup"
|
||||
dnsSetupData=$(printf '{ "domain": "%s", "adminFqdn": "%s", "provider": "%s", "config": %s, "tlsConfig": { "provider": "%s" } }' "${domain}" "my.${domain}" "${domainProvider}" "$domainConfigJson" "${domainTlsProvider}")
|
||||
|
||||
if ! $curl -X POST -H "Content-Type: application/json" -d "${dnsSetupData}" http://localhost:3000/api/v1/cloudron/dns_setup; then
|
||||
echo "DNS Setup Failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
wait_for_status "webadminStatus" '*"tls": true*'
|
||||
else
|
||||
echo "=> Skipping Domain setup"
|
||||
fi
|
||||
|
||||
activationData=$(printf '{"username": "%s", "password":"%s", "email": "%s" }' "${adminUsername}" "${adminPassword}" "${adminEmail}")
|
||||
if [[ $(get_status "activated") == "false" ]]; then
|
||||
echo "=> Activating"
|
||||
|
||||
if ! activationResult=$($curl -X POST -H "Content-Type: application/json" -d "${activationData}" http://localhost:3000/api/v1/cloudron/activate); then
|
||||
echo "Failed to activate with ${activationData}: ${activationResult}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
wait_for_status "activated" "true"
|
||||
else
|
||||
echo "=> Skipping Activation"
|
||||
fi
|
||||
|
||||
echo "=> Getting token"
|
||||
if ! activationResult=$($curl -X POST -H "Content-Type: application/json" -d "${activationData}" http://localhost:3000/api/v1/developer/login); then
|
||||
echo "Failed to login with ${activationData}: ${activationResult}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
accessToken=$(echo "${activationResult}" | python3 -c 'import sys, json; print(json.load(sys.stdin)[sys.argv[1]])' "accessToken")
|
||||
|
||||
echo "=> Setting up App Store account with accessToken ${accessToken}"
|
||||
appstoreData=$(printf '{"userId":"%s", "token":"%s" }' "${appstoreUser}" "${appstoreToken}")
|
||||
|
||||
if ! appstoreResult=$($curl -X POST -H "Content-Type: application/json" -d "${appstoreData}" "http://localhost:3000/api/v1/settings/appstore_config?access_token=${accessToken}"); then
|
||||
echo "Failed to setup Appstore account with ${appstoreData}: ${appstoreResult}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=> Setting up Backup Directory with accessToken ${accessToken}"
|
||||
backupData=$(printf '{"provider":"filesystem", "key":"", "backupFolder":"%s", "retentionSecs": 864000, "format": "tgz", "externalDisk": true}' "${backupDir}")
|
||||
|
||||
chown -R yellowtent:yellowtent "${backupDir}"
|
||||
|
||||
if ! backupResult=$($curl -X POST -H "Content-Type: application/json" -d "${backupData}" "http://localhost:3000/api/v1/settings/backup_config?access_token=${accessToken}"); then
|
||||
echo "Failed to setup backup configuration with ${backupDir}: ${backupResult}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=> Done!"
|
||||
|
||||
+143
-81
@@ -2,6 +2,16 @@
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
if [[ ${EUID} -ne 0 ]]; then
|
||||
echo "This script should be run as root." > /dev/stderr
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $(lsb_release -rs) != "16.04" ]]; then
|
||||
echo "Cloudron requires Ubuntu 16.04" > /dev/stderr
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# change this to a hash when we make a upgrade release
|
||||
readonly LOG_FILE="/var/log/cloudron-setup.log"
|
||||
readonly DATA_FILE="/root/cloudron-install-data.json"
|
||||
@@ -16,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
|
||||
@@ -36,97 +42,97 @@ if [[ "${disk_size_gb}" -lt "${MINIMUM_DISK_SIZE_GB}" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if systemctl -q is-active box; then
|
||||
echo "Error: Cloudron is already installed. To reinstall, start afresh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
initBaseImage="true"
|
||||
# provisioning data
|
||||
domain=""
|
||||
adminLocation="my"
|
||||
zoneName=""
|
||||
provider=""
|
||||
edition=""
|
||||
tlsProvider="le-prod"
|
||||
requestedVersion=""
|
||||
apiServerOrigin="https://api.cloudron.io"
|
||||
webServerOrigin="https://cloudron.io"
|
||||
dataJson=""
|
||||
prerelease="false"
|
||||
sourceTarballUrl=""
|
||||
rebootServer="true"
|
||||
baseDataDir=""
|
||||
|
||||
echo "Running cloudron-setup with args : $@" > "${LOG_FILE}"
|
||||
# these are here for pre-1.9 compat
|
||||
encryptionKey=""
|
||||
restoreUrl=""
|
||||
|
||||
args=$(getopt -o "" -l "help,skip-baseimage-init,data-dir:,provider:,version:,env:,prerelease,edition:,skip-reboot" -n "$0" -- "$@")
|
||||
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;;
|
||||
--edition) edition="$2"; shift 2;;
|
||||
--encryption-key) encryptionKey="$2"; shift 2;;
|
||||
--restore-url) restoreUrl="$2"; shift 2;;
|
||||
--tls-provider) tlsProvider="$2"; shift 2;;
|
||||
--version) requestedVersion="$2"; shift 2;;
|
||||
--env)
|
||||
if [[ "$2" == "dev" ]]; then
|
||||
apiServerOrigin="https://api.dev.cloudron.io"
|
||||
webServerOrigin="https://dev.cloudron.io"
|
||||
tlsProvider="le-staging"
|
||||
prerelease="true"
|
||||
elif [[ "$2" == "staging" ]]; then
|
||||
apiServerOrigin="https://api.staging.cloudron.io"
|
||||
webServerOrigin="https://staging.cloudron.io"
|
||||
tlsProvider="le-staging"
|
||||
prerelease="true"
|
||||
fi
|
||||
shift 2;;
|
||||
--skip-baseimage-init) initBaseImage="false"; shift;;
|
||||
--skip-reboot) rebootServer="false"; shift;;
|
||||
--data) dataJson="$2"; shift 2;;
|
||||
--prerelease) prerelease="true"; shift;;
|
||||
--source-url) sourceTarballUrl="$2"; version="0.0.1+custom"; shift 2;;
|
||||
--data-dir) baseDataDir=$(realpath "$2"); shift 2;;
|
||||
--) break;;
|
||||
*) echo "Unknown option $1"; exit 1;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Only --help works as non-root
|
||||
if [[ ${EUID} -ne 0 ]]; then
|
||||
echo "This script should be run as root." > /dev/stderr
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Only --help works with mismatched ubuntu
|
||||
if [[ $(lsb_release -rs) != "16.04" ]]; then
|
||||
echo "Cloudron requires Ubuntu 16.04" > /dev/stderr
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# validate arguments in the absence of data
|
||||
if [[ -z "${provider}" ]]; then
|
||||
echo "--provider is required (azure, digitalocean, ec2, exoscale, gce, 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}" != "galaxygate" && \
|
||||
"${provider}" != "digitalocean" && \
|
||||
"${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, galaxygate, gce, hetzner, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic"
|
||||
exit 1
|
||||
fi
|
||||
if [[ -z "${dataJson}" ]]; then
|
||||
if [[ -z "${provider}" ]]; then
|
||||
echo "--provider is required (azure, cloudscale.ch, digitalocean, ec2, exoscale, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic)"
|
||||
exit 1
|
||||
elif [[ \
|
||||
"${provider}" != "ami" && \
|
||||
"${provider}" != "azure" && \
|
||||
"${provider}" != "cloudscale.ch" && \
|
||||
"${provider}" != "digitalocean" && \
|
||||
"${provider}" != "ec2" && \
|
||||
"${provider}" != "exoscale" && \
|
||||
"${provider}" != "gce" && \
|
||||
"${provider}" != "lightsail" && \
|
||||
"${provider}" != "linode" && \
|
||||
"${provider}" != "ovh" && \
|
||||
"${provider}" != "rosehosting" && \
|
||||
"${provider}" != "scaleway" && \
|
||||
"${provider}" != "vultr" && \
|
||||
"${provider}" != "generic" \
|
||||
]]; then
|
||||
echo "--provider must be one of: azure, cloudscale.ch, digitalocean, ec2, exoscale, gce, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -n "${baseDataDir}" && ! -d "${baseDataDir}" ]]; then
|
||||
echo "${baseDataDir} does not exist"
|
||||
exit 1
|
||||
if [[ "${tlsProvider}" != "fallback" && "${tlsProvider}" != "le-prod" && "${tlsProvider}" != "le-staging" ]]; then
|
||||
echo "--tls-provider must be one of: le-prod, le-staging, fallback"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -n "${baseDataDir}" && ! -d "${baseDataDir}" ]]; then
|
||||
echo "${baseDataDir} does not exist"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
@@ -137,51 +143,91 @@ 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
|
||||
echo "=> Updating apt and installing script dependencies"
|
||||
if ! apt-get update &>> "${LOG_FILE}"; then
|
||||
echo "Could not update package repositories. See ${LOG_FILE}"
|
||||
echo "Could not update package repositories"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! apt-get install curl python3 ubuntu-standard -y &>> "${LOG_FILE}"; then
|
||||
echo "Could not install setup dependencies (curl). See ${LOG_FILE}"
|
||||
echo "Could not install setup dependencies (curl)"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "=> Checking version"
|
||||
if ! releaseJson=$($curl -s "${apiServerOrigin}/api/v1/releases?prerelease=${prerelease}&boxVersion=${requestedVersion}"); then
|
||||
echo "Failed to get release information"
|
||||
exit 1
|
||||
fi
|
||||
if [[ "${sourceTarballUrl}" == "" ]]; then
|
||||
if ! releaseJson=$($curl -s "${apiServerOrigin}/api/v1/releases?prerelease=${prerelease}&boxVersion=${requestedVersion}"); then
|
||||
echo "Failed to get release information"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$requestedVersion" == "" ]]; then
|
||||
version=$(echo "${releaseJson}" | python3 -c 'import json,sys;obj=json.load(sys.stdin);print(obj["version"])')
|
||||
else
|
||||
version="${requestedVersion}"
|
||||
fi
|
||||
if [[ "$requestedVersion" == "" ]]; then
|
||||
version=$(echo "${releaseJson}" | python3 -c 'import json,sys;obj=json.load(sys.stdin);print(obj["version"])')
|
||||
else
|
||||
version="${requestedVersion}"
|
||||
fi
|
||||
|
||||
if ! sourceTarballUrl=$(echo "${releaseJson}" | python3 -c 'import json,sys;obj=json.load(sys.stdin);print(obj["info"]["sourceTarballUrl"])'); then
|
||||
echo "No source code for version '${requestedVersion:-latest}'"
|
||||
exit 1
|
||||
if ! sourceTarballUrl=$(echo "${releaseJson}" | python3 -c 'import json,sys;obj=json.load(sys.stdin);print(obj["info"]["sourceTarballUrl"])'); then
|
||||
echo "No source code for version '${requestedVersion:-latest}'"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Build data
|
||||
# tlsConfig, dnsConfig, backupConfig are here for backward compat with < 1.9
|
||||
# from 1.9, we use autoprovision.json
|
||||
data=$(cat <<EOF
|
||||
{
|
||||
"provider": "${provider}",
|
||||
"edition": "${edition}",
|
||||
"apiServerOrigin": "${apiServerOrigin}",
|
||||
"webServerOrigin": "${webServerOrigin}",
|
||||
"version": "${version}"
|
||||
}
|
||||
if [[ -z "${dataJson}" ]]; then
|
||||
if [[ -z "${restoreUrl}" ]]; then
|
||||
data=$(cat <<EOF
|
||||
{
|
||||
"fqdn": "${domain}",
|
||||
"adminLocation": "${adminLocation}",
|
||||
"adminFqdn": "${adminLocation}.${domain}",
|
||||
"zoneName": "${zoneName}",
|
||||
"provider": "${provider}",
|
||||
"apiServerOrigin": "${apiServerOrigin}",
|
||||
"webServerOrigin": "${webServerOrigin}",
|
||||
"version": "${version}",
|
||||
"tlsConfig": {
|
||||
"provider": "${tlsProvider}"
|
||||
},
|
||||
"backupConfig" : {
|
||||
"provider": "filesystem",
|
||||
"backupFolder": "/var/backups",
|
||||
"key": "${encryptionKey}",
|
||||
"format": "tgz",
|
||||
"retentionSecs": 172800
|
||||
}
|
||||
}
|
||||
EOF
|
||||
)
|
||||
)
|
||||
else
|
||||
data=$(cat <<EOF
|
||||
{
|
||||
"fqdn": "${domain}",
|
||||
"adminLocation": "${adminLocation}",
|
||||
"adminFqdn": "${adminLocation}.${domain}",
|
||||
"zoneName": "${zoneName}",
|
||||
"provider": "${provider}",
|
||||
"apiServerOrigin": "${apiServerOrigin}",
|
||||
"webServerOrigin": "${webServerOrigin}",
|
||||
"restore": {
|
||||
"url": "${restoreUrl}",
|
||||
"key": "${encryptionKey}"
|
||||
},
|
||||
"version": "${version}"
|
||||
}
|
||||
EOF
|
||||
)
|
||||
fi
|
||||
else
|
||||
data="${dataJson}"
|
||||
fi
|
||||
|
||||
echo "=> Downloading version ${version} ..."
|
||||
box_src_tmp_dir=$(mktemp -dt box-src-XXXXXX)
|
||||
@@ -212,15 +258,31 @@ echo -n "=> Waiting for cloudron to be ready (this takes some time) ..."
|
||||
while true; do
|
||||
echo -n "."
|
||||
if status=$($curl -q -f "http://localhost:3000/api/v1/cloudron/status" 2>/dev/null); then
|
||||
break # we are up and running
|
||||
[[ -z "$domain" ]] && break # with no domain, we are up and running
|
||||
[[ "$status" == *"\"tls\": true"* ]] && break # with a domain, wait for the cert
|
||||
fi
|
||||
sleep 10
|
||||
done
|
||||
|
||||
echo -e "\n\n${GREEN}Visit https://<IP> and accept the self-signed certificate to finish setup.${DONE}"
|
||||
autoprovision_data=$(cat <<EOF
|
||||
{
|
||||
"tlsConfig": {
|
||||
"provider": "${tlsProvider}"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
echo "${autoprovision_data}" > /home/yellowtent/configs/autoprovision.json
|
||||
|
||||
if [[ -n "${domain}" ]]; then
|
||||
echo -e "\n\nVisit https://my.${domain} to finish setup once the server has rebooted.\n"
|
||||
else
|
||||
echo -e "\n\nVisit https://<IP> to finish setup once the server has rebooted.\n"
|
||||
fi
|
||||
|
||||
if [[ "${rebootServer}" == "true" ]]; then
|
||||
echo -e "\n${RED}Rebooting this server now to let changes take effect.${DONE}\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,56 +27,60 @@ 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}/../dashboard" && git diff --exit-code >/dev/null); then
|
||||
echo "You have local changes in dashboard, stash or commit them to proceed"
|
||||
if [[ "$(node --version)" != "v8.9.3" ]]; then
|
||||
echo "This script requires node 8.9.3"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$(node --version)" != "v8.11.2" ]]; then
|
||||
echo "This script requires node 8.11.2"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
box_version=$(cd "${SOURCE_DIR}" && git rev-parse "HEAD")
|
||||
branch=$(git rev-parse --abbrev-ref HEAD)
|
||||
if [[ "${branch}" == "master" ]]; then
|
||||
dashboard_version=$(cd "${SOURCE_DIR}/../dashboard" && git rev-parse "${branch}")
|
||||
else
|
||||
dashboard_version=$(cd "${SOURCE_DIR}/../dashboard" && 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}-${dashboard_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 dashboard version [${dashboard_version}] into ${bundle_dir}"
|
||||
(cd "${SOURCE_DIR}" && git archive --format=tar ${box_version} | (cd "${bundle_dir}" && tar xf -))
|
||||
(cd "${SOURCE_DIR}/../dashboard" && git archive --format=tar ${dashboard_version} | (mkdir -p "${bundle_dir}/dashboard.build" && cd "${bundle_dir}/dashboard.build" && tar xf -))
|
||||
(cp "${SOURCE_DIR}/../dashboard/LICENSE" "${bundle_dir}")
|
||||
echo "Checking out code [${version}] into ${bundle_dir}"
|
||||
(cd "${SOURCE_DIR}" && git archive --format=tar ${version} | (cd "${bundle_dir}" && tar xf -))
|
||||
|
||||
echo "==> Installing modules for dashboard asset generation"
|
||||
(cd "${bundle_dir}/dashboard.build" && npm install --production)
|
||||
if diff "${TMPDIR}/boxtarball.cache/package-lock.json.all" "${bundle_dir}/package-lock.json" >/dev/null 2>&1; then
|
||||
echo "Reusing dev modules from cache"
|
||||
cp -r "${TMPDIR}/boxtarball.cache/node_modules-all/." "${bundle_dir}/node_modules"
|
||||
else
|
||||
echo "Installing modules with dev dependencies"
|
||||
(cd "${bundle_dir}" && npm install)
|
||||
|
||||
echo "==> Building dashboard assets"
|
||||
(cd "${bundle_dir}/dashboard.build" && ./node_modules/.bin/gulp --revision ${dashboard_version})
|
||||
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"
|
||||
fi
|
||||
|
||||
echo "==> Move built dashboard assets into destination"
|
||||
mkdir -p "${bundle_dir}/dashboard"
|
||||
mv "${bundle_dir}/dashboard.build/dist" "${bundle_dir}/dashboard/"
|
||||
echo "Building webadmin assets"
|
||||
(cd "${bundle_dir}" && ./node_modules/.bin/gulp)
|
||||
|
||||
echo "==> Cleanup dashboard build artifacts"
|
||||
rm -rf "${bundle_dir}/dashboard.build"
|
||||
echo "Remove intermediate files required at build-time only"
|
||||
rm -rf "${bundle_dir}/node_modules/"
|
||||
rm -rf "${bundle_dir}/webadmin/src"
|
||||
rm -rf "${bundle_dir}/gulpfile.js"
|
||||
|
||||
echo "==> Installing toplevel node modules"
|
||||
(cd "${bundle_dir}" && npm install --production --no-optional)
|
||||
if diff "${TMPDIR}/boxtarball.cache/package-lock.json.prod" "${bundle_dir}/package-lock.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
|
||||
echo "Installing modules for production"
|
||||
(cd "${bundle_dir}" && npm install --production --no-optional)
|
||||
|
||||
echo "==> Create final tarball"
|
||||
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"
|
||||
fi
|
||||
|
||||
echo "Create final tarball"
|
||||
(cd "${bundle_dir}" && tar czf "${bundle_file}" .)
|
||||
|
||||
echo "==> Cleaning up ${bundle_dir}"
|
||||
echo "Cleaning up ${bundle_dir}"
|
||||
rm -rf "${bundle_dir}"
|
||||
|
||||
echo "==> Tarball saved at ${bundle_file}"
|
||||
echo "Tarball saved at ${bundle_file}"
|
||||
|
||||
+17
-35
@@ -35,33 +35,27 @@ while true; do
|
||||
done
|
||||
|
||||
echo "==> installer: updating docker"
|
||||
if [[ $(docker version --format {{.Client.Version}}) != "18.03.1-ce" ]]; then
|
||||
$curl -sL https://download.docker.com/linux/ubuntu/dists/xenial/pool/stable/amd64/docker-ce_18.03.1~ce-0~ubuntu_amd64.deb -o /tmp/docker.deb
|
||||
if [[ $(docker version --format {{.Client.Version}}) != "17.09.0-ce" ]]; then
|
||||
$curl -sL https://download.docker.com/linux/ubuntu/dists/xenial/pool/stable/amd64/docker-ce_17.09.0~ce-0~ubuntu_amd64.deb -o /tmp/docker.deb
|
||||
|
||||
# https://download.docker.com/linux/ubuntu/dists/xenial/stable/binary-amd64/Packages
|
||||
if [[ $(sha256sum /tmp/docker.deb | cut -d' ' -f1) != "54f4c9268492a4fd2ec2e6bcc95553855b025f35dcc8b9f60ac34e0aa307279b" ]]; then
|
||||
echo "==> installer: docker binary download is corrupt"
|
||||
if [[ $(sha256sum /tmp/docker.deb | cut -d' ' -f1) != "d33f6eb134f0ab0876148bd96de95ea47d583d7f2cddfdc6757979453f9bd9bf" ]]; then
|
||||
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"
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# the latest docker might need newer packages
|
||||
while ! apt update -y; do
|
||||
echo "==> installer: Failed to update 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
|
||||
|
||||
@@ -69,15 +63,14 @@ if [[ $(docker version --format {{.Client.Version}}) != "18.03.1-ce" ]]; then
|
||||
fi
|
||||
|
||||
echo "==> installer: updating node"
|
||||
if [[ "$(node --version)" != "v8.11.2" ]]; then
|
||||
mkdir -p /usr/local/node-8.11.2
|
||||
$curl -sL https://nodejs.org/dist/v8.11.2/node-v8.11.2-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-8.11.2
|
||||
ln -sf /usr/local/node-8.11.2/bin/node /usr/bin/node
|
||||
ln -sf /usr/local/node-8.11.2/bin/npm /usr/bin/npm
|
||||
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
|
||||
fi
|
||||
|
||||
# this is here (and not in updater.js) because rebuild requires the above node
|
||||
for try in `seq 1 10`; do
|
||||
# for reasons unknown, the dtrace package will fail. but rebuilding second time will work
|
||||
|
||||
@@ -85,34 +78,23 @@ 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
|
||||
|
||||
echo "==> installer: update cloudron-syslog"
|
||||
CLOUDRON_SYSLOG_DIR=/usr/local/cloudron-syslog
|
||||
CLOUDRON_SYSLOG="${CLOUDRON_SYSLOG_DIR}/bin/cloudron-syslog"
|
||||
CLOUDRON_SYSLOG_VERSION="1.0.3"
|
||||
while [[ ! -f "${CLOUDRON_SYSLOG}" || "$(${CLOUDRON_SYSLOG} --version)" != ${CLOUDRON_SYSLOG_VERSION} ]]; do
|
||||
rm -rf "${CLOUDRON_SYSLOG_DIR}"
|
||||
mkdir -p "${CLOUDRON_SYSLOG_DIR}"
|
||||
if npm install --unsafe-perm -g --prefix "${CLOUDRON_SYSLOG_DIR}" cloudron-syslog@${CLOUDRON_SYSLOG_VERSION}; then break; fi
|
||||
echo "===> installer: Failed to install cloudron-syslog, trying again"
|
||||
sleep 5
|
||||
done
|
||||
|
||||
if ! id "${USER}" 2>/dev/null; then
|
||||
useradd "${USER}" -m
|
||||
fi
|
||||
|
||||
if [[ "${is_update}" == "yes" ]]; then
|
||||
echo "==> installer: stop cloudron.target service for update"
|
||||
${BOX_SRC_DIR}/setup/stop.sh
|
||||
echo "Setting up update splash screen"
|
||||
"${box_src_tmp_dir}/setup/splashpage.sh" --data "${arg_data}" || true # show splash from new code
|
||||
${BOX_SRC_DIR}/setup/stop.sh # stop the old code
|
||||
fi
|
||||
|
||||
# setup links to data directory
|
||||
|
||||
+16
-11
@@ -3,18 +3,20 @@
|
||||
source_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
json="${source_dir}/../node_modules/.bin/json"
|
||||
|
||||
# IMPORTANT: Fix cloudron.js:doUpdate if you add/remove any arg. keep these sorted for readability
|
||||
arg_api_server_origin=""
|
||||
arg_fqdn="" # remove after 1.10
|
||||
arg_admin_domain=""
|
||||
arg_fqdn=""
|
||||
arg_admin_location=""
|
||||
arg_admin_fqdn=""
|
||||
arg_zone_name=""
|
||||
arg_is_custom_domain="false" # can be removed after 1.9
|
||||
arg_retire_reason=""
|
||||
arg_retire_info=""
|
||||
arg_token=""
|
||||
arg_version=""
|
||||
arg_web_server_origin=""
|
||||
arg_provider=""
|
||||
arg_is_demo="false"
|
||||
arg_edition=""
|
||||
|
||||
args=$(getopt -o "" -l "data:,retire-reason:,retire-info:" -n "$0" -- "$@")
|
||||
eval set -- "${args}"
|
||||
@@ -33,13 +35,16 @@ while true; do
|
||||
# these params must be valid in all cases
|
||||
arg_fqdn=$(echo "$2" | $json fqdn)
|
||||
arg_admin_fqdn=$(echo "$2" | $json adminFqdn)
|
||||
arg_zone_name=$(echo "$2" | $json zoneName)
|
||||
[[ "${arg_zone_name}" == "" ]] && arg_zone_name="${arg_fqdn}"
|
||||
|
||||
# can be removed after 1.9
|
||||
arg_is_custom_domain=$(echo "$2" | $json isCustomDomain)
|
||||
[[ "${arg_is_custom_domain}" == "" ]] && arg_is_custom_domain="true"
|
||||
|
||||
arg_admin_location=$(echo "$2" | $json adminLocation)
|
||||
[[ "${arg_admin_location}" == "" ]] && arg_admin_location="my"
|
||||
|
||||
arg_admin_domain=$(echo "$2" | $json adminDomain)
|
||||
[[ "${arg_admin_domain}" == "" ]] && arg_admin_domain="${arg_fqdn}"
|
||||
|
||||
# only update/restore have this valid (but not migrate)
|
||||
arg_api_server_origin=$(echo "$2" | $json apiServerOrigin)
|
||||
[[ "${arg_api_server_origin}" == "" ]] && arg_api_server_origin="https://api.cloudron.io"
|
||||
@@ -53,12 +58,11 @@ while true; do
|
||||
arg_is_demo=$(echo "$2" | $json isDemo)
|
||||
[[ "${arg_is_demo}" == "" ]] && arg_is_demo="false"
|
||||
|
||||
arg_token=$(echo "$2" | $json token)
|
||||
|
||||
arg_provider=$(echo "$2" | $json provider)
|
||||
[[ "${arg_provider}" == "" ]] && arg_provider="generic"
|
||||
|
||||
arg_edition=$(echo "$2" | $json edition)
|
||||
[[ "${arg_edition}" == "" ]] && arg_edition=""
|
||||
|
||||
shift 2
|
||||
;;
|
||||
--) break;;
|
||||
@@ -68,9 +72,10 @@ done
|
||||
|
||||
echo "Parsed arguments:"
|
||||
echo "api server: ${arg_api_server_origin}"
|
||||
echo "admin fqdn: ${arg_admin_fqdn}"
|
||||
echo "fqdn: ${arg_fqdn}"
|
||||
echo "custom domain: ${arg_is_custom_domain}"
|
||||
# do not dump these as they might become available via logs API
|
||||
#echo "token: ${arg_token}"
|
||||
echo "version: ${arg_version}"
|
||||
echo "web server: ${arg_web_server_origin}"
|
||||
echo "provider: ${arg_provider}"
|
||||
echo "edition: ${arg_edition}"
|
||||
|
||||
Executable
+48
@@ -0,0 +1,48 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
readonly SETUP_WEBSITE_DIR="/home/yellowtent/setup/website"
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
readonly box_src_dir="$(realpath ${script_dir}/..)"
|
||||
readonly PLATFORM_DATA_DIR="/home/yellowtent/platformdata"
|
||||
|
||||
echo "Setting up nginx update page"
|
||||
|
||||
if [[ ! -f "${PLATFORM_DATA_DIR}/nginx/applications/admin.conf" ]]; then
|
||||
echo "No admin.conf found. This Cloudron has no domain yet. Skip splash setup"
|
||||
exit
|
||||
fi
|
||||
|
||||
source "${script_dir}/argparser.sh" "$@" # this injects the arg_* variables used below
|
||||
|
||||
# keep this is sync with config.js appFqdn()
|
||||
admin_origin="https://${arg_admin_fqdn}"
|
||||
|
||||
# copy the website
|
||||
rm -rf "${SETUP_WEBSITE_DIR}" && mkdir -p "${SETUP_WEBSITE_DIR}"
|
||||
cp -r "${script_dir}/splash/website/"* "${SETUP_WEBSITE_DIR}"
|
||||
|
||||
# create nginx config
|
||||
readonly current_infra=$(node -e "console.log(require('${script_dir}/../src/infra_version.js').version);")
|
||||
existing_infra="none"
|
||||
[[ -f "${PLATFORM_DATA_DIR}/INFRA_VERSION" ]] && existing_infra=$(node -e "console.log(JSON.parse(require('fs').readFileSync('${PLATFORM_DATA_DIR}/INFRA_VERSION', 'utf8')).version);")
|
||||
if [[ "${arg_retire_reason}" != "" || "${existing_infra}" != "${current_infra}" ]]; then
|
||||
echo "Showing progress bar on all subdomains in retired mode or infra update. retire: ${arg_retire_reason} existing: ${existing_infra} current: ${current_infra}"
|
||||
rm -f ${PLATFORM_DATA_DIR}/nginx/applications/*
|
||||
${box_src_dir}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/appconfig.ejs" \
|
||||
-O "{ \"vhost\": \"~^(.+)\$\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"splash\", \"sourceDir\": \"${SETUP_WEBSITE_DIR}\", \"certFilePath\": \"cert/host.cert\", \"keyFilePath\": \"cert/host.key\", \"xFrameOptions\": \"SAMEORIGIN\", \"robotsTxtQuoted\": null, \"hasIPv6\": false }" > "${PLATFORM_DATA_DIR}/nginx/applications/admin.conf"
|
||||
else
|
||||
echo "Show progress bar only on admin domain for normal update"
|
||||
${box_src_dir}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/appconfig.ejs" \
|
||||
-O "{ \"vhost\": \"${arg_admin_fqdn}\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"splash\", \"sourceDir\": \"${SETUP_WEBSITE_DIR}\", \"certFilePath\": \"cert/host.cert\", \"keyFilePath\": \"cert/host.key\", \"xFrameOptions\": \"SAMEORIGIN\", \"robotsTxtQuoted\": null, \"hasIPv6\": false }" > "${PLATFORM_DATA_DIR}/nginx/applications/admin.conf"
|
||||
fi
|
||||
|
||||
if [[ "${arg_retire_reason}" == "migrate" ]]; then
|
||||
echo "{ \"migrate\": { \"percent\": \"10\", \"message\": \"Migrating cloudron. This could take up to 15 minutes.\", \"info\": ${arg_retire_info} }, \"backup\": null, \"apiServerOrigin\": \"${arg_api_server_origin}\" }" > "${SETUP_WEBSITE_DIR}/progress.json"
|
||||
else
|
||||
echo '{ "update": { "percent": "10", "message": "Updating cloudron software" }, "backup": null }' > "${SETUP_WEBSITE_DIR}/progress.json"
|
||||
fi
|
||||
|
||||
nginx -s reload
|
||||
+30
-27
@@ -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
|
||||
@@ -76,8 +85,6 @@ mkdir -p "${PLATFORM_DATA_DIR}/collectd/collectd.conf.d"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/logrotate.d"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/acme"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/backup"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/logs/backup"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/update"
|
||||
|
||||
mkdir -p "${BOX_DATA_DIR}/appicons"
|
||||
mkdir -p "${BOX_DATA_DIR}/certs"
|
||||
@@ -107,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)
|
||||
@@ -122,7 +129,6 @@ echo "==> Adding systemd services"
|
||||
cp -r "${script_dir}/start/systemd/." /etc/systemd/system/
|
||||
systemctl daemon-reload
|
||||
systemctl enable unbound
|
||||
systemctl enable cloudron-syslog
|
||||
systemctl enable cloudron.target
|
||||
systemctl enable cloudron-firewall
|
||||
|
||||
@@ -135,9 +141,6 @@ systemctl enable --now cron
|
||||
# ensure unbound runs
|
||||
systemctl restart unbound
|
||||
|
||||
# ensure cloudron-syslog runs
|
||||
systemctl restart cloudron-syslog
|
||||
|
||||
echo "==> Configuring sudoers"
|
||||
rm -f /etc/sudoers.d/${USER}
|
||||
cp "${script_dir}/start/sudoers" /etc/sudoers.d/${USER}
|
||||
@@ -152,8 +155,6 @@ echo "==> Configuring logrotate"
|
||||
if ! grep -q "^include ${PLATFORM_DATA_DIR}/logrotate.d" /etc/logrotate.conf; then
|
||||
echo -e "\ninclude ${PLATFORM_DATA_DIR}/logrotate.d\n" >> /etc/logrotate.conf
|
||||
fi
|
||||
cp "${script_dir}/start/app-logrotate" "${PLATFORM_DATA_DIR}/logrotate.d/app-logrotate"
|
||||
chown root:root "${PLATFORM_DATA_DIR}/logrotate.d/app-logrotate"
|
||||
|
||||
echo "==> Adding motd message for admins"
|
||||
cp "${script_dir}/start/cloudron-motd" /etc/update-motd.d/92-cloudron
|
||||
@@ -171,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
|
||||
@@ -201,30 +199,38 @@ readonly mysql_root_password="password"
|
||||
mysqladmin -u root -ppassword password password # reset default root password
|
||||
mysql -u root -p${mysql_root_password} -e 'CREATE DATABASE IF NOT EXISTS box'
|
||||
|
||||
echo "==> Migrating data"
|
||||
set_progress "40" "Migrating data"
|
||||
sudo -u "${USER}" -H bash <<EOF
|
||||
set -eu
|
||||
cd "${BOX_SRC_DIR}"
|
||||
BOX_ENV=cloudron DATABASE_URL=mysql://root:${mysql_root_password}@127.0.0.1/box "${BOX_SRC_DIR}/node_modules/.bin/db-migrate" up
|
||||
EOF
|
||||
|
||||
if [[ -z "${arg_admin_fqdn:-}" ]]; then
|
||||
# can be removed after 1.9
|
||||
admin_fqdn=$([[ "${arg_is_custom_domain}" == "true" ]] && echo "${arg_admin_location}.${arg_fqdn}" || echo "${arg_admin_location}-${arg_fqdn}")
|
||||
else
|
||||
admin_fqdn="${arg_admin_fqdn}"
|
||||
fi
|
||||
|
||||
echo "==> Creating cloudron.conf"
|
||||
cat > "${CONFIG_DIR}/cloudron.conf" <<CONF_END
|
||||
{
|
||||
"version": "${arg_version}",
|
||||
"token": "${arg_token}",
|
||||
"apiServerOrigin": "${arg_api_server_origin}",
|
||||
"webServerOrigin": "${arg_web_server_origin}",
|
||||
"adminDomain": "${arg_admin_domain}",
|
||||
"adminFqdn": "${arg_admin_fqdn}",
|
||||
"fqdn": "${arg_fqdn}",
|
||||
"adminFqdn": "${admin_fqdn}",
|
||||
"adminLocation": "${arg_admin_location}",
|
||||
"zoneName": "${arg_zone_name}",
|
||||
"provider": "${arg_provider}",
|
||||
"isDemo": ${arg_is_demo},
|
||||
"edition": "${arg_edition}"
|
||||
"isDemo": ${arg_is_demo}
|
||||
}
|
||||
CONF_END
|
||||
|
||||
echo "==> Creating config.json for dashboard"
|
||||
cat > "${BOX_SRC_DIR}/dashboard/dist/config.json" <<CONF_END
|
||||
echo "==> Creating config.json for webadmin"
|
||||
cat > "${BOX_SRC_DIR}/webadmin/dist/config.json" <<CONF_END
|
||||
{
|
||||
"webServerOrigin": "${arg_web_server_origin}"
|
||||
}
|
||||
@@ -240,22 +246,19 @@ fi
|
||||
|
||||
echo "==> Changing ownership"
|
||||
chown "${USER}:${USER}" -R "${CONFIG_DIR}"
|
||||
chown "${USER}:${USER}" -R "${PLATFORM_DATA_DIR}/nginx" "${PLATFORM_DATA_DIR}/collectd" "${PLATFORM_DATA_DIR}/addons" "${PLATFORM_DATA_DIR}/acme" "${PLATFORM_DATA_DIR}/backup" "${PLATFORM_DATA_DIR}/logs" "${PLATFORM_DATA_DIR}/update"
|
||||
chown "${USER}:${USER}" -R "${PLATFORM_DATA_DIR}/nginx" "${PLATFORM_DATA_DIR}/collectd" "${PLATFORM_DATA_DIR}/logrotate.d" "${PLATFORM_DATA_DIR}/addons" "${PLATFORM_DATA_DIR}/acme" "${PLATFORM_DATA_DIR}/backup"
|
||||
chown "${USER}:${USER}" "${PLATFORM_DATA_DIR}/INFRA_VERSION" 2>/dev/null || true
|
||||
chown "${USER}:${USER}" "${PLATFORM_DATA_DIR}"
|
||||
|
||||
# logrotate files have to be owned by root, this is here to fixup existing installations where we were resetting the owner to yellowtent
|
||||
chown root:root -R "${PLATFORM_DATA_DIR}/logrotate.d"
|
||||
|
||||
# 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"
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
# logrotate config for app logs
|
||||
|
||||
/home/yellowtent/platformdata/logs/*/*.log {
|
||||
# only keep one rotated file, we currently do not send that over the api
|
||||
rotate 1
|
||||
size 10M
|
||||
# we never compress so we can simply tail the files
|
||||
nocompress
|
||||
copytruncate
|
||||
}
|
||||
@@ -66,9 +66,8 @@ server {
|
||||
# https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # don't use SSLv3 ref: POODLE
|
||||
|
||||
# ciphers according to https://mozilla.github.io/server-side-tls/ssl-config-generator/?server=nginx-1.10.3&openssl=1.0.2g&hsts=yes&profile=modern
|
||||
ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
|
||||
# ciphers according to https://weakdh.org/sysadmin.html
|
||||
ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';
|
||||
ssl_dhparam /home/yellowtent/boxdata/dhparams.pem;
|
||||
add_header Strict-Transport-Security "max-age=15768000";
|
||||
|
||||
@@ -90,11 +89,6 @@ server {
|
||||
add_header Referrer-Policy "no-referrer-when-downgrade";
|
||||
proxy_hide_header Referrer-Policy;
|
||||
|
||||
<% if ( endpoint === 'admin' ) { -%>
|
||||
# CSP headers for the admin/dashboard resources
|
||||
add_header Content-Security-Policy "default-src 'none'; connect-src wss: https: 'self' *.cloudron.io; script-src https: 'self' 'unsafe-inline' 'unsafe-eval'; img-src * data:; style-src https: 'unsafe-inline'; object-src 'none'; font-src https: 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self';";
|
||||
<% } -%>
|
||||
|
||||
proxy_http_version 1.1;
|
||||
proxy_intercept_errors on;
|
||||
proxy_read_timeout 3500;
|
||||
@@ -112,7 +106,7 @@ server {
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
|
||||
# only serve up the status page if we get proxy gateway errors
|
||||
root <%= sourceDir %>/dashboard/dist;
|
||||
root <%= sourceDir %>/webadmin/dist;
|
||||
error_page 502 503 504 /appstatus.html;
|
||||
location /appstatus.html {
|
||||
internal;
|
||||
@@ -160,22 +154,51 @@ server {
|
||||
}
|
||||
|
||||
# graphite paths (uncomment block below and visit /graphite/index.html)
|
||||
# remember to comment out the CSP policy as well to access the graphite dashboard
|
||||
# location ~ ^/(graphite|content|metrics|dashboard|render|browser|composer)/ {
|
||||
# proxy_pass http://127.0.0.1:8000;
|
||||
# client_max_body_size 1m;
|
||||
# }
|
||||
|
||||
location / {
|
||||
root <%= sourceDir %>/dashboard/dist;
|
||||
root <%= sourceDir %>/webadmin/dist;
|
||||
index index.html index.htm;
|
||||
}
|
||||
<% } else if ( endpoint === 'app' ) { %>
|
||||
proxy_pass http://127.0.0.1:<%= port %>;
|
||||
<% } else if ( endpoint === 'redirect' ) { %>
|
||||
# redirect everything to the app. this is temporary because there is no way
|
||||
# to clear a permanent redirect on the browser
|
||||
return 302 https://<%= redirectTo %>$request_uri;
|
||||
<% } 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;
|
||||
}
|
||||
<% } %>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
[Unit]
|
||||
Description=Cloudron Syslog
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/local/cloudron-syslog/bin/cloudron-syslog --port 2514 --logdir /home/yellowtent/platformdata/logs
|
||||
WorkingDirectory=/usr/local/cloudron-syslog
|
||||
Environment="NODE_ENV=production"
|
||||
Restart=always
|
||||
User=yellowtent
|
||||
Group=yellowtent
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -1,143 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
SCOPE_APPS_READ: 'apps:read',
|
||||
SCOPE_APPS_MANAGE: 'apps:manage',
|
||||
SCOPE_CLIENTS: 'clients',
|
||||
SCOPE_CLOUDRON: 'cloudron',
|
||||
SCOPE_DOMAINS_READ: 'domains:read',
|
||||
SCOPE_DOMAINS_MANAGE: 'domains:manage',
|
||||
SCOPE_MAIL: 'mail',
|
||||
SCOPE_PROFILE: 'profile',
|
||||
SCOPE_SETTINGS: 'settings',
|
||||
SCOPE_USERS_READ: 'users:read',
|
||||
SCOPE_USERS_MANAGE: 'users:manage',
|
||||
SCOPE_APPSTORE: 'appstore',
|
||||
VALID_SCOPES: [ 'apps', 'appstore', 'clients', 'cloudron', 'domains', 'mail', 'profile', 'settings', 'users' ], // keep this sorted
|
||||
|
||||
SCOPE_ANY: '*',
|
||||
|
||||
validateScopeString: validateScopeString,
|
||||
hasScopes: hasScopes,
|
||||
canonicalScopeString: canonicalScopeString,
|
||||
intersectScopes: intersectScopes,
|
||||
validateToken: validateToken,
|
||||
scopesForUser: scopesForUser
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
config = require('./config.js'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
debug = require('debug')('box:accesscontrol'),
|
||||
tokendb = require('./tokendb.js'),
|
||||
users = require('./users.js'),
|
||||
UsersError = users.UsersError,
|
||||
_ = require('underscore');
|
||||
|
||||
// returns scopes that does not have wildcards and is sorted
|
||||
function canonicalScopeString(scope) {
|
||||
if (scope === exports.SCOPE_ANY) return exports.VALID_SCOPES.join(',');
|
||||
|
||||
return scope.split(',').sort().join(',');
|
||||
}
|
||||
|
||||
function intersectScopes(allowedScopes, wantedScopes) {
|
||||
assert(Array.isArray(allowedScopes), 'Expecting sorted array');
|
||||
assert(Array.isArray(wantedScopes), 'Expecting sorted array');
|
||||
|
||||
if (_.isEqual(allowedScopes, wantedScopes)) return allowedScopes; // quick path
|
||||
|
||||
let wantedScopesMap = new Map();
|
||||
let results = [];
|
||||
|
||||
// make a map of scope -> [ subscopes ]
|
||||
for (let w of wantedScopes) {
|
||||
let parts = w.split(':');
|
||||
let subscopes = wantedScopesMap.get(parts[0]) || new Set();
|
||||
subscopes.add(parts[1] || '*');
|
||||
wantedScopesMap.set(parts[0], subscopes);
|
||||
}
|
||||
|
||||
for (let a of allowedScopes) {
|
||||
let parts = a.split(':');
|
||||
let as = parts[1] || '*';
|
||||
|
||||
let subscopes = wantedScopesMap.get(parts[0]);
|
||||
if (!subscopes) continue;
|
||||
|
||||
if (subscopes.has('*') || subscopes.has(as)) {
|
||||
results.push(a);
|
||||
} else if (as === '*') {
|
||||
results = results.concat(Array.from(subscopes).map(function (ss) { return `${a}:${ss}`; }));
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
function validateScopeString(scope) {
|
||||
assert.strictEqual(typeof scope, 'string');
|
||||
|
||||
if (scope === '') return new Error('Empty scope not allowed');
|
||||
|
||||
// NOTE: this function intentionally does not allow '*'. This is only allowed in the db to allow
|
||||
// us not write a migration script every time we add a new scope
|
||||
var allValid = scope.split(',').every(function (s) { return exports.VALID_SCOPES.indexOf(s.split(':')[0]) !== -1; });
|
||||
if (!allValid) return new Error('Invalid scope. Available scopes are ' + exports.VALID_SCOPES.join(', '));
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// tests if all requiredScopes are attached to the request
|
||||
function hasScopes(authorizedScopes, requiredScopes) {
|
||||
assert(Array.isArray(authorizedScopes), 'Expecting array');
|
||||
assert(Array.isArray(requiredScopes), 'Expecting array');
|
||||
|
||||
if (authorizedScopes.indexOf(exports.SCOPE_ANY) !== -1) return null;
|
||||
|
||||
for (var i = 0; i < requiredScopes.length; ++i) {
|
||||
const scopeParts = requiredScopes[i].split(':');
|
||||
|
||||
// this allows apps:write if the token has a higher apps scope
|
||||
if (authorizedScopes.indexOf(requiredScopes[i]) === -1 && authorizedScopes.indexOf(scopeParts[0]) === -1) {
|
||||
debug('scope: missing scope "%s".', requiredScopes[i]);
|
||||
return new Error('Missing required scope "' + requiredScopes[i] + '"');
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function scopesForUser(user, callback) {
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (user.admin) return callback(null, exports.VALID_SCOPES);
|
||||
|
||||
callback(null, config.isSpacesEnabled() ? [ 'profile', 'apps', 'domains:read', 'users:read' ] : [ 'profile', 'apps:read' ]);
|
||||
}
|
||||
|
||||
function validateToken(accessToken, callback) {
|
||||
assert.strictEqual(typeof accessToken, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
tokendb.get(accessToken, function (error, token) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, null /* user */, 'Invalid Token'); // will end up as a 401
|
||||
if (error) return callback(error); // this triggers 'internal error' in passport
|
||||
|
||||
users.get(token.identifier, function (error, user) {
|
||||
if (error && error.reason === UsersError.NOT_FOUND) return callback(null, null /* user */, 'Invalid Token'); // will end up as a 401
|
||||
if (error) return callback(error);
|
||||
|
||||
scopesForUser(user, function (error, userScopes) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var authorizedScopes = intersectScopes(userScopes, token.scope.split(','));
|
||||
const skipPasswordVerification = token.clientId === 'cid-sdk' || token.clientId === 'cid-cli'; // these clients do not require password checks unlike UI
|
||||
var info = { authorizedScopes: authorizedScopes, skipPasswordVerification: skipPasswordVerification }; // ends up in req.authInfo
|
||||
|
||||
callback(null, user, info);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
+137
-275
@@ -15,22 +15,19 @@ exports = module.exports = {
|
||||
_teardownOauth: teardownOauth
|
||||
};
|
||||
|
||||
var accesscontrol = require('./accesscontrol.js'),
|
||||
appdb = require('./appdb.js'),
|
||||
var appdb = require('./appdb.js'),
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
clients = require('./clients.js'),
|
||||
config = require('./config.js'),
|
||||
ClientsError = clients.ClientsError,
|
||||
crypto = require('crypto'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
debug = require('debug')('box:addons'),
|
||||
docker = require('./docker.js'),
|
||||
dockerConnection = docker.connection,
|
||||
fs = require('fs'),
|
||||
hat = require('./hat.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'),
|
||||
@@ -109,21 +106,16 @@ var KNOWN_ADDONS = {
|
||||
teardown: NOOP,
|
||||
backup: NOOP,
|
||||
restore: NOOP
|
||||
},
|
||||
docker: {
|
||||
setup: NOOP,
|
||||
teardown: NOOP,
|
||||
backup: NOOP,
|
||||
restore: NOOP
|
||||
}
|
||||
};
|
||||
|
||||
var RMAPPDIR_CMD = path.join(__dirname, 'scripts/rmappdir.sh');
|
||||
|
||||
function debugApp(app, args) {
|
||||
assert(typeof app === 'object');
|
||||
assert(!app || typeof app === 'object');
|
||||
|
||||
debug(app.fqdn + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
|
||||
var prefix = app ? app.intrinsicFqdn : '(no app)';
|
||||
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
|
||||
}
|
||||
|
||||
function setupAddons(app, addons, callback) {
|
||||
@@ -205,8 +197,6 @@ function getEnvironment(app, callback) {
|
||||
appdb.getAddonConfigByAppId(app.id, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (app.manifest.addons['docker']) result.push({ name: 'DOCKER_HOST', value: `tcp://172.18.0.1:${config.get('dockerProxyPort')}` });
|
||||
|
||||
return callback(null, result.map(function (e) { return e.name + '=' + e.value; }));
|
||||
});
|
||||
}
|
||||
@@ -215,21 +205,13 @@ function getBindsSync(app, addons) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert(!addons || typeof addons === 'object');
|
||||
|
||||
let binds = [ ];
|
||||
var binds = [ ];
|
||||
|
||||
if (!addons) return binds;
|
||||
|
||||
for (let addon in addons) {
|
||||
for (var addon in addons) {
|
||||
switch (addon) {
|
||||
case 'localstorage':
|
||||
binds.push(path.join(paths.APPS_DATA_DIR, app.id, 'data') + ':/app/data:rw');
|
||||
if (!Array.isArray(addons[addon].bindMounts)) break;
|
||||
|
||||
for (let mount of addons[addon].bindMounts) {
|
||||
let [ host, container ] = mount.split(':');
|
||||
binds.push(path.join(paths.APPS_DATA_DIR, app.id, 'data', path.normalize(host)) + ':' + container);
|
||||
}
|
||||
break;
|
||||
case 'localstorage': binds.push(path.join(paths.APPS_DATA_DIR, app.id, 'data') + ':/app/data:rw'); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
@@ -268,8 +250,8 @@ function setupOauth(app, options, callback) {
|
||||
if (!app.sso) return callback(null);
|
||||
|
||||
var appId = app.id;
|
||||
var redirectURI = 'https://' + app.fqdn;
|
||||
var scope = accesscontrol.SCOPE_PROFILE;
|
||||
var redirectURI = 'https://' + (app.altDomain || app.intrinsicFqdn);
|
||||
var scope = 'profile';
|
||||
|
||||
clients.delByAppIdAndType(appId, clients.TYPE_OAUTH, function (error) { // remove existing creds
|
||||
if (error && error.reason !== ClientsError.NOT_FOUND) return callback(error);
|
||||
@@ -309,27 +291,20 @@ function setupEmail(app, options, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
mail.getDomains(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) {
|
||||
@@ -381,28 +356,23 @@ function setupSendMail(app, options, callback) {
|
||||
|
||||
debugApp(app, 'Setting up SendMail');
|
||||
|
||||
appdb.getAddonConfigByName(app.id, 'sendmail', 'MAIL_SMTP_PASSWORD', function (error, existingPassword) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
|
||||
mailboxdb.getByOwnerId(app.id, function (error, results) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var password = error ? hat(4 * 48) : existingPassword; // see box#565 for password length
|
||||
var mailbox = results.filter(function (r) { return !r.aliasTarget; })[0];
|
||||
var password = generatePassword(128, false /* memorable */, /[\w\d_]/);
|
||||
|
||||
mailboxdb.getByOwnerId(app.id, function (error, results) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var mailbox = results.filter(function (r) { return !r.aliasTarget; })[0];
|
||||
|
||||
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_SMTP_PASSWORD', value: password },
|
||||
{ name: 'MAIL_FROM', value: mailbox.name + '@' + app.domain },
|
||||
{ name: 'MAIL_DOMAIN', value: app.domain }
|
||||
];
|
||||
debugApp(app, 'Setting sendmail addon config to %j', env);
|
||||
appdb.setAddonConfig(app.id, 'sendmail', env, callback);
|
||||
});
|
||||
var env = [
|
||||
{ name: 'MAIL_SMTP_SERVER', value: 'mail' },
|
||||
{ name: 'MAIL_SMTP_PORT', value: '2525' },
|
||||
{ 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 + '@' + config.fqdn() },
|
||||
{ name: 'MAIL_DOMAIN', value: config.fqdn() }
|
||||
];
|
||||
debugApp(app, 'Setting sendmail addon config to %j', env);
|
||||
appdb.setAddonConfig(app.id, 'sendmail', env, callback);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -423,28 +393,23 @@ function setupRecvMail(app, options, callback) {
|
||||
|
||||
debugApp(app, 'Setting up recvmail');
|
||||
|
||||
appdb.getAddonConfigByName(app.id, 'recvmail', 'MAIL_IMAP_PASSWORD', function (error, existingPassword) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
|
||||
mailboxdb.getByOwnerId(app.id, function (error, results) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var password = error ? hat(4 * 48) : existingPassword; // see box#565 for password length
|
||||
var mailbox = results.filter(function (r) { return !r.aliasTarget; })[0];
|
||||
var password = generatePassword(128, false /* memorable */, /[\w\d_]/);
|
||||
|
||||
mailboxdb.getByOwnerId(app.id, function (error, results) {
|
||||
if (error) return callback(error);
|
||||
var env = [
|
||||
{ name: 'MAIL_IMAP_SERVER', value: 'mail' },
|
||||
{ name: 'MAIL_IMAP_PORT', value: '9993' },
|
||||
{ name: 'MAIL_IMAP_USERNAME', value: mailbox.name },
|
||||
{ name: 'MAIL_IMAP_PASSWORD', value: password },
|
||||
{ name: 'MAIL_TO', value: mailbox.name + '@' + config.fqdn() },
|
||||
{ name: 'MAIL_DOMAIN', value: config.fqdn() }
|
||||
];
|
||||
|
||||
var mailbox = results.filter(function (r) { return !r.aliasTarget; })[0];
|
||||
|
||||
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_PASSWORD', value: password },
|
||||
{ name: 'MAIL_TO', value: mailbox.name + '@' + app.domain },
|
||||
{ name: 'MAIL_DOMAIN', value: app.domain }
|
||||
];
|
||||
|
||||
debugApp(app, 'Setting sendmail addon config to %j', env);
|
||||
appdb.setAddonConfig(app.id, 'recvmail', env, callback);
|
||||
});
|
||||
debugApp(app, 'Setting sendmail addon config to %j', env);
|
||||
appdb.setAddonConfig(app.id, 'recvmail', env, callback);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -458,14 +423,6 @@ function teardownRecvMail(app, options, callback) {
|
||||
appdb.unsetAddonConfig(app.id, 'recvmail', callback);
|
||||
}
|
||||
|
||||
function mysqlDatabaseName(appId) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
|
||||
var md5sum = crypto.createHash('md5'); // get rid of "-"
|
||||
md5sum.update(appId);
|
||||
return md5sum.digest('hex').substring(0, 16); // max length of mysql usernames is 16
|
||||
}
|
||||
|
||||
function setupMySql(app, options, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
@@ -473,36 +430,16 @@ function setupMySql(app, options, callback) {
|
||||
|
||||
debugApp(app, 'Setting up mysql');
|
||||
|
||||
appdb.getAddonConfigByName(app.id, 'mysql', 'MYSQL_PASSWORD', function (error, existingPassword) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
|
||||
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'add-prefix' : 'add', app.id ];
|
||||
|
||||
const dbname = mysqlDatabaseName(app.id);
|
||||
const password = error ? hat(4 * 48) : existingPassword; // see box#362 for password length
|
||||
docker.execContainer('mysql', cmd, { bufferStdout: true }, function (error, stdout) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'add-prefix' : 'add', dbname, password ];
|
||||
var result = stdout.toString('utf8').split('\n').slice(0, -1); // remove trailing newline
|
||||
var env = result.map(function (r) { var idx = r.indexOf('='); return { name: r.substr(0, idx), value: r.substr(idx + 1) }; });
|
||||
|
||||
docker.execContainer('mysql', cmd, { bufferStdout: true }, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var env = [
|
||||
{ name: 'MYSQL_USERNAME', value: dbname },
|
||||
{ name: 'MYSQL_PASSWORD', value: password },
|
||||
{ name: 'MYSQL_HOST', value: 'mysql' },
|
||||
{ name: 'MYSQL_PORT', value: '3306' }
|
||||
];
|
||||
|
||||
if (options.multipleDatabases) {
|
||||
env = env.concat({ name: 'MYSQL_DATABASE_PREFIX', value: `${dbname}_` });
|
||||
} else {
|
||||
env = env.concat(
|
||||
{ name: 'MYSQL_URL', value: `mysql://${dbname}:${password}@mysql/${dbname}` },
|
||||
{ name: 'MYSQL_DATABASE', value: dbname }
|
||||
);
|
||||
}
|
||||
|
||||
debugApp(app, 'Setting mysql addon config to %j', env);
|
||||
appdb.setAddonConfig(app.id, 'mysql', env, callback);
|
||||
});
|
||||
debugApp(app, 'Setting mysql addon config to %j', env);
|
||||
appdb.setAddonConfig(app.id, 'mysql', env, callback);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -511,8 +448,7 @@ function teardownMySql(app, options, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const dbname = mysqlDatabaseName(app.id);
|
||||
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'remove-prefix' : 'remove', dbname ];
|
||||
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'remove-prefix' : 'remove', app.id ];
|
||||
|
||||
debugApp(app, 'Tearing down mysql');
|
||||
|
||||
@@ -524,10 +460,6 @@ function teardownMySql(app, options, callback) {
|
||||
}
|
||||
|
||||
function backupMySql(app, options, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debugApp(app, 'Backing up mysql');
|
||||
|
||||
callback = once(callback); // ChildProcess exit may or may not be called after error
|
||||
@@ -535,17 +467,12 @@ function backupMySql(app, options, callback) {
|
||||
var output = fs.createWriteStream(path.join(paths.APPS_DATA_DIR, app.id, 'mysqldump'));
|
||||
output.on('error', callback);
|
||||
|
||||
const dbname = mysqlDatabaseName(app.id);
|
||||
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'backup-prefix' : 'backup', dbname ];
|
||||
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'backup-prefix' : 'backup', app.id ];
|
||||
|
||||
docker.execContainer('mysql', cmd, { stdout: output }, 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) {
|
||||
@@ -556,8 +483,7 @@ function restoreMySql(app, options, callback) {
|
||||
var input = fs.createReadStream(path.join(paths.APPS_DATA_DIR, app.id, 'mysqldump'));
|
||||
input.on('error', callback);
|
||||
|
||||
const dbname = mysqlDatabaseName(app.id);
|
||||
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'restore-prefix' : 'restore', dbname ];
|
||||
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'restore-prefix' : 'restore', app.id ];
|
||||
docker.execContainer('mysql', cmd, { stdin: input }, callback);
|
||||
});
|
||||
}
|
||||
@@ -569,29 +495,16 @@ function setupPostgreSql(app, options, callback) {
|
||||
|
||||
debugApp(app, 'Setting up postgresql');
|
||||
|
||||
appdb.getAddonConfigByName(app.id, 'postgresql', 'POSTGRESQL_PASSWORD', function (error, existingPassword) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
|
||||
var cmd = [ '/addons/postgresql/service.sh', 'add', app.id ];
|
||||
|
||||
const password = error ? hat(4 * 128) : existingPassword;
|
||||
const appId = app.id.replace(/-/g, '');
|
||||
docker.execContainer('postgresql', cmd, { bufferStdout: true }, function (error, stdout) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var cmd = [ '/addons/postgresql/service.sh', 'add', appId, password ];
|
||||
var result = stdout.toString('utf8').split('\n').slice(0, -1); // remove trailing newline
|
||||
var env = result.map(function (r) { var idx = r.indexOf('='); return { name: r.substr(0, idx), value: r.substr(idx + 1) }; });
|
||||
|
||||
docker.execContainer('postgresql', cmd, { bufferStdout: true }, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var env = [
|
||||
{ name: 'POSTGRESQL_URL', value: `postgres://user${appId}:${password}@postgresql/db${appId}` },
|
||||
{ name: 'POSTGRESQL_USERNAME', value: `user${appId}` },
|
||||
{ name: 'POSTGRESQL_PASSWORD', value: password },
|
||||
{ name: 'POSTGRESQL_HOST', value: 'postgresql' },
|
||||
{ name: 'POSTGRESQL_PORT', value: '5432' },
|
||||
{ name: 'POSTGRESQL_DATABASE', value: `db${appId}` }
|
||||
];
|
||||
|
||||
debugApp(app, 'Setting postgresql addon config to %j', env);
|
||||
appdb.setAddonConfig(app.id, 'postgresql', env, callback);
|
||||
});
|
||||
debugApp(app, 'Setting postgresql addon config to %j', env);
|
||||
appdb.setAddonConfig(app.id, 'postgresql', env, callback);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -600,9 +513,7 @@ function teardownPostgreSql(app, options, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const appId = app.id.replace(/-/g, '');
|
||||
|
||||
var cmd = [ '/addons/postgresql/service.sh', 'remove', appId ];
|
||||
var cmd = [ '/addons/postgresql/service.sh', 'remove', app.id ];
|
||||
|
||||
debugApp(app, 'Tearing down postgresql');
|
||||
|
||||
@@ -614,10 +525,6 @@ function teardownPostgreSql(app, options, callback) {
|
||||
}
|
||||
|
||||
function backupPostgreSql(app, options, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debugApp(app, 'Backing up postgresql');
|
||||
|
||||
callback = once(callback); // ChildProcess exit may or may not be called after error
|
||||
@@ -625,17 +532,12 @@ function backupPostgreSql(app, options, callback) {
|
||||
var output = fs.createWriteStream(path.join(paths.APPS_DATA_DIR, app.id, 'postgresqldump'));
|
||||
output.on('error', callback);
|
||||
|
||||
const appId = app.id.replace(/-/g, '');
|
||||
var cmd = [ '/addons/postgresql/service.sh', 'backup', appId ];
|
||||
var cmd = [ '/addons/postgresql/service.sh', 'backup', app.id ];
|
||||
|
||||
docker.execContainer('postgresql', cmd, { stdout: output }, 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) {
|
||||
@@ -646,8 +548,7 @@ function restorePostgreSql(app, options, callback) {
|
||||
var input = fs.createReadStream(path.join(paths.APPS_DATA_DIR, app.id, 'postgresqldump'));
|
||||
input.on('error', callback);
|
||||
|
||||
const appId = app.id.replace(/-/g, '');
|
||||
var cmd = [ '/addons/postgresql/service.sh', 'restore', appId ];
|
||||
var cmd = [ '/addons/postgresql/service.sh', 'restore', app.id ];
|
||||
|
||||
docker.execContainer('postgresql', cmd, { stdin: input }, callback);
|
||||
});
|
||||
@@ -660,30 +561,16 @@ function setupMongoDb(app, options, callback) {
|
||||
|
||||
debugApp(app, 'Setting up mongodb');
|
||||
|
||||
appdb.getAddonConfigByName(app.id, 'mongodb', 'MONGODB_PASSWORD', function (error, existingPassword) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
|
||||
var cmd = [ '/addons/mongodb/service.sh', 'add', app.id ];
|
||||
|
||||
const password = error ? hat(4 * 128) : existingPassword;
|
||||
docker.execContainer('mongodb', cmd, { bufferStdout: true }, function (error, stdout) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const dbname = app.id;
|
||||
var result = stdout.toString('utf8').split('\n').slice(0, -1); // remove trailing newline
|
||||
var env = result.map(function (r) { var idx = r.indexOf('='); return { name: r.substr(0, idx), value: r.substr(idx + 1) }; });
|
||||
|
||||
var cmd = [ '/addons/mongodb/service.sh', 'add', dbname, password ];
|
||||
|
||||
docker.execContainer('mongodb', cmd, { bufferStdout: true }, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var env = [
|
||||
{ name: 'MONGODB_URL', value : `mongodb://${dbname}:${password}@mongodb/${dbname}` },
|
||||
{ name: 'MONGODB_USERNAME', value : dbname },
|
||||
{ name: 'MONGODB_PASSWORD', value: password },
|
||||
{ name: 'MONGODB_HOST', value : 'mongodb' },
|
||||
{ name: 'MONGODB_PORT', value : '27017' },
|
||||
{ name: 'MONGODB_DATABASE', value : dbname }
|
||||
];
|
||||
|
||||
debugApp(app, 'Setting mongodb addon config to %j', env);
|
||||
appdb.setAddonConfig(app.id, 'mongodb', env, callback);
|
||||
});
|
||||
debugApp(app, 'Setting mongodb addon config to %j', env);
|
||||
appdb.setAddonConfig(app.id, 'mongodb', env, callback);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -692,8 +579,7 @@ function teardownMongoDb(app, options, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const dbname = app.id;
|
||||
var cmd = [ '/addons/mongodb/service.sh', 'remove', dbname ];
|
||||
var cmd = [ '/addons/mongodb/service.sh', 'remove', app.id ];
|
||||
|
||||
debugApp(app, 'Tearing down mongodb');
|
||||
|
||||
@@ -705,10 +591,6 @@ function teardownMongoDb(app, options, callback) {
|
||||
}
|
||||
|
||||
function backupMongoDb(app, options, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debugApp(app, 'Backing up mongodb');
|
||||
|
||||
callback = once(callback); // ChildProcess exit may or may not be called after error
|
||||
@@ -716,17 +598,12 @@ function backupMongoDb(app, options, callback) {
|
||||
var output = fs.createWriteStream(path.join(paths.APPS_DATA_DIR, app.id, 'mongodbdump'));
|
||||
output.on('error', callback);
|
||||
|
||||
const dbname = app.id;
|
||||
var cmd = [ '/addons/mongodb/service.sh', 'backup', dbname ];
|
||||
var cmd = [ '/addons/mongodb/service.sh', 'backup', app.id ];
|
||||
|
||||
docker.execContainer('mongodb', cmd, { stdout: output }, 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) {
|
||||
@@ -737,9 +614,7 @@ function restoreMongoDb(app, options, callback) {
|
||||
var input = fs.createReadStream(path.join(paths.APPS_DATA_DIR, app.id, 'mongodbdump'));
|
||||
input.on('error', callback);
|
||||
|
||||
const dbname = app.id;
|
||||
var cmd = [ '/addons/mongodb/service.sh', 'restore', dbname ];
|
||||
|
||||
var cmd = [ '/addons/mongodb/service.sh', 'restore', app.id ];
|
||||
docker.execContainer('mongodb', cmd, { stdin: input }, callback);
|
||||
});
|
||||
}
|
||||
@@ -750,67 +625,58 @@ function setupRedis(app, options, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
appdb.getAddonConfigByName(app.id, 'redis', 'REDIS_PASSWORD', function (error, existingPassword) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
|
||||
var redisPassword = generatePassword(128, false /* memorable */, /[\w\d_]/); // ensure no / in password for being sed friendly (and be uri friendly)
|
||||
var redisVarsFile = path.join(paths.ADDON_CONFIG_DIR, 'redis-' + app.id + '_vars.sh');
|
||||
var redisDataDir = path.join(paths.APPS_DATA_DIR, app.id + '/redis');
|
||||
|
||||
const redisPassword = error ? hat(4 * 48) : existingPassword; // see box#362 for password length
|
||||
if (!safe.fs.writeFileSync(redisVarsFile, 'REDIS_PASSWORD=' + redisPassword)) {
|
||||
return callback(new Error('Error writing redis config'));
|
||||
}
|
||||
|
||||
var redisVarsFile = path.join(paths.ADDON_CONFIG_DIR, 'redis-' + app.id + '_vars.sh');
|
||||
var redisDataDir = path.join(paths.APPS_DATA_DIR, app.id + '/redis');
|
||||
if (!safe.fs.mkdirSync(redisDataDir) && safe.error.code !== 'EEXIST') return callback(new Error('Error creating redis data dir:' + safe.error));
|
||||
|
||||
if (!safe.fs.writeFileSync(redisVarsFile, 'REDIS_PASSWORD=' + redisPassword)) {
|
||||
return callback(new Error('Error writing redis config'));
|
||||
}
|
||||
// Compute redis memory limit based on app's memory limit (this is arbitrary)
|
||||
var memoryLimit = app.memoryLimit || app.manifest.memoryLimit || 0;
|
||||
|
||||
if (!safe.fs.mkdirSync(redisDataDir) && safe.error.code !== 'EEXIST') return callback(new Error('Error creating redis data dir:' + safe.error));
|
||||
if (memoryLimit === -1) { // unrestricted (debug mode)
|
||||
memoryLimit = 0;
|
||||
} else if (memoryLimit === 0 || memoryLimit <= (2 * 1024 * 1024 * 1024)) { // less than 2G (ram+swap)
|
||||
memoryLimit = 150 * 1024 * 1024; // 150m
|
||||
} else {
|
||||
memoryLimit = 600 * 1024 * 1024; // 600m
|
||||
}
|
||||
|
||||
// Compute redis memory limit based on app's memory limit (this is arbitrary)
|
||||
var memoryLimit = app.memoryLimit || app.manifest.memoryLimit || 0;
|
||||
const tag = infra.images.redis.tag, redisName = 'redis-' + app.id;
|
||||
const label = app.intrinsicFqdn;
|
||||
// note that we do not add appId label because this interferes with the stop/start app logic
|
||||
const cmd = `docker run --restart=always -d --name=${redisName} \
|
||||
--label=location=${label} \
|
||||
--net cloudron \
|
||||
--net-alias ${redisName} \
|
||||
-m ${memoryLimit/2} \
|
||||
--memory-swap ${memoryLimit} \
|
||||
--dns 172.18.0.1 \
|
||||
--dns-search=. \
|
||||
-v ${redisVarsFile}:/etc/redis/redis_vars.sh:ro \
|
||||
-v ${redisDataDir}:/var/lib/redis:rw \
|
||||
--read-only -v /tmp -v /run ${tag}`;
|
||||
|
||||
if (memoryLimit === -1) { // unrestricted (debug mode)
|
||||
memoryLimit = 0;
|
||||
} else if (memoryLimit === 0 || memoryLimit <= (2 * 1024 * 1024 * 1024)) { // less than 2G (ram+swap)
|
||||
memoryLimit = 150 * 1024 * 1024; // 150m
|
||||
} else {
|
||||
memoryLimit = 600 * 1024 * 1024; // 600m
|
||||
}
|
||||
var env = [
|
||||
{ name: 'REDIS_URL', value: 'redis://redisuser:' + redisPassword + '@redis-' + app.id },
|
||||
{ name: 'REDIS_PASSWORD', value: redisPassword },
|
||||
{ name: 'REDIS_HOST', value: redisName },
|
||||
{ name: 'REDIS_PORT', value: '6379' }
|
||||
];
|
||||
|
||||
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} \
|
||||
--net cloudron \
|
||||
--net-alias ${redisName} \
|
||||
--log-driver syslog \
|
||||
--log-opt syslog-address=udp://127.0.0.1:2514 \
|
||||
--log-opt syslog-format=rfc5424 \
|
||||
--log-opt tag="${redisName}" \
|
||||
-m ${memoryLimit/2} \
|
||||
--memory-swap ${memoryLimit} \
|
||||
--dns 172.18.0.1 \
|
||||
--dns-search=. \
|
||||
-v ${redisVarsFile}:/etc/redis/redis_vars.sh:ro \
|
||||
-v ${redisDataDir}:/var/lib/redis:rw \
|
||||
--read-only -v /tmp -v /run ${tag}`;
|
||||
|
||||
var env = [
|
||||
{ name: 'REDIS_URL', value: 'redis://redisuser:' + redisPassword + '@redis-' + app.id },
|
||||
{ name: 'REDIS_PASSWORD', value: redisPassword },
|
||||
{ name: 'REDIS_HOST', value: redisName },
|
||||
{ name: 'REDIS_PORT', value: '6379' }
|
||||
];
|
||||
|
||||
async.series([
|
||||
// stop so that redis can flush itself with SIGTERM
|
||||
shell.execSync.bind(null, 'stopRedis', `docker stop --time=10 ${redisName} 2>/dev/null || true`),
|
||||
shell.execSync.bind(null, 'stopRedis', `docker rm --volumes ${redisName} 2>/dev/null || true`),
|
||||
shell.execSync.bind(null, 'startRedis', cmd),
|
||||
appdb.setAddonConfig.bind(null, app.id, 'redis', env)
|
||||
], function (error) {
|
||||
if (error) debug('Error setting up redis: ', error);
|
||||
callback(error);
|
||||
});
|
||||
async.series([
|
||||
// stop so that redis can flush itself with SIGTERM
|
||||
shell.execSync.bind(null, 'stopRedis', `docker stop --time=10 ${redisName} 2>/dev/null || true`),
|
||||
shell.execSync.bind(null, 'stopRedis', `docker rm --volumes ${redisName} 2>/dev/null || true`),
|
||||
shell.execSync.bind(null, 'startRedis', cmd),
|
||||
appdb.setAddonConfig.bind(null, app.id, 'redis', env)
|
||||
], function (error) {
|
||||
if (error) debug('Error setting up redis: ', error);
|
||||
callback(error);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -819,31 +685,27 @@ function teardownRedis(app, options, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var container = dockerConnection.getContainer('redis-' + app.id);
|
||||
var container = dockerConnection.getContainer('redis-' + app.id);
|
||||
|
||||
var removeOptions = {
|
||||
force: true, // kill container if it's running
|
||||
v: true // removes volumes associated with the container
|
||||
};
|
||||
var removeOptions = {
|
||||
force: true, // kill container if it's running
|
||||
v: true // removes volumes associated with the container
|
||||
};
|
||||
|
||||
container.remove(removeOptions, function (error) {
|
||||
if (error && error.statusCode !== 404) return callback(new Error('Error removing container:' + error));
|
||||
container.remove(removeOptions, function (error) {
|
||||
if (error && error.statusCode !== 404) return callback(new Error('Error removing container:' + error));
|
||||
|
||||
safe.fs.unlinkSync(paths.ADDON_CONFIG_DIR, 'redis-' + app.id + '_vars.sh');
|
||||
safe.fs.unlinkSync(paths.ADDON_CONFIG_DIR, 'redis-' + app.id + '_vars.sh');
|
||||
|
||||
shell.sudo('teardownRedis', [ RMAPPDIR_CMD, app.id + '/redis', true /* delete directory */ ], function (error /* ,stdout , stderr*/) {
|
||||
shell.sudo('teardownRedis', [ RMAPPDIR_CMD, app.id + '/redis', true /* delete directory */ ], function (error, stdout, stderr) {
|
||||
if (error) return callback(new Error('Error removing redis data:' + error));
|
||||
|
||||
appdb.unsetAddonConfig(app.id, 'redis', callback);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function backupRedis(app, options, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debugApp(app, 'Backing up redis');
|
||||
|
||||
var cmd = [ '/addons/redis/service.sh', 'backup' ]; // the redis dir is volume mounted
|
||||
|
||||
+40
-161
@@ -24,9 +24,6 @@ exports = module.exports = {
|
||||
setRunCommand: setRunCommand,
|
||||
getAppStoreIds: getAppStoreIds,
|
||||
|
||||
setOwner: setOwner,
|
||||
transferOwnership: transferOwnership,
|
||||
|
||||
// installation codes (keep in sync in UI)
|
||||
ISTATE_PENDING_INSTALL: 'pending_install', // installs and fresh reinstalls
|
||||
ISTATE_PENDING_CLONE: 'pending_clone', // clone
|
||||
@@ -50,10 +47,6 @@ exports = module.exports = {
|
||||
HEALTH_ERROR: 'error',
|
||||
HEALTH_DEAD: 'dead',
|
||||
|
||||
// subdomain table types
|
||||
SUBDOMAIN_TYPE_PRIMARY: 'primary',
|
||||
SUBDOMAIN_TYPE_REDIRECT: 'redirect',
|
||||
|
||||
_clear: clear
|
||||
};
|
||||
|
||||
@@ -66,14 +59,12 @@ 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', 'subdomains.subdomain AS location', 'subdomains.domain',
|
||||
'apps.accessRestrictionJson', 'apps.restoreConfigJson', 'apps.oldConfigJson', 'apps.updateConfigJson', 'apps.memoryLimit',
|
||||
'apps.xFrameOptions', 'apps.sso', 'apps.debugModeJson', 'apps.robotsTxt', 'apps.enableBackup',
|
||||
'apps.creationTime', 'apps.updateTime', 'apps.ownerId', 'apps.ts' ].join(',');
|
||||
'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.altDomain', 'apps.xFrameOptions', 'apps.sso', 'apps.debugModeJson', 'apps.robotsTxt', 'apps.enableBackup',
|
||||
'apps.creationTime', 'apps.updateTime' ].join(',');
|
||||
|
||||
var PORT_BINDINGS_FIELDS = [ 'hostPort', 'type', 'environmentVariable', 'appId' ].join(',');
|
||||
|
||||
const SUBDOMAIN_FIELDS = [ 'appId', 'domain', 'subdomain', 'type' ].join(',');
|
||||
var PORT_BINDINGS_FIELDS = [ 'hostPort', 'environmentVariable', 'appId' ].join(',');
|
||||
|
||||
function postProcess(result) {
|
||||
assert.strictEqual(typeof result, 'object');
|
||||
@@ -98,16 +89,14 @@ function postProcess(result) {
|
||||
assert(result.environmentVariables === null || typeof result.environmentVariables === 'string');
|
||||
|
||||
result.portBindings = { };
|
||||
let hostPorts = result.hostPorts === null ? [ ] : result.hostPorts.split(',');
|
||||
let environmentVariables = result.environmentVariables === null ? [ ] : result.environmentVariables.split(',');
|
||||
let portTypes = result.portTypes === null ? [ ] : result.portTypes.split(',');
|
||||
var hostPorts = result.hostPorts === null ? [ ] : result.hostPorts.split(',');
|
||||
var environmentVariables = result.environmentVariables === null ? [ ] : result.environmentVariables.split(',');
|
||||
|
||||
delete result.hostPorts;
|
||||
delete result.environmentVariables;
|
||||
delete result.portTypes;
|
||||
|
||||
for (var i = 0; i < environmentVariables.length; i++) {
|
||||
result.portBindings[environmentVariables[i]] = { hostPort: parseInt(hostPorts[i], 10), type: portTypes[i] };
|
||||
result.portBindings[environmentVariables[i]] = parseInt(hostPorts[i], 10);
|
||||
}
|
||||
|
||||
assert(result.accessRestrictionJson === null || typeof result.accessRestrictionJson === 'string');
|
||||
@@ -124,12 +113,6 @@ function postProcess(result) {
|
||||
assert(result.debugModeJson === null || typeof result.debugModeJson === 'string');
|
||||
result.debugMode = safe.JSON.parse(result.debugModeJson);
|
||||
delete result.debugModeJson;
|
||||
|
||||
result.alternateDomains = result.alternateDomains || [];
|
||||
result.alternateDomains.forEach(function (d) {
|
||||
delete d.appId;
|
||||
delete d.type;
|
||||
});
|
||||
}
|
||||
|
||||
function get(id, callback) {
|
||||
@@ -137,23 +120,14 @@ function get(id, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
|
||||
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes'
|
||||
+ ' FROM apps'
|
||||
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
|
||||
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
|
||||
+ ' WHERE apps.id = ? GROUP BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY, id ], function (error, result) {
|
||||
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables'
|
||||
+ ' FROM apps LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId WHERE apps.id = ? GROUP BY apps.id', [ id ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
database.query('SELECT ' + SUBDOMAIN_FIELDS + ' FROM subdomains WHERE appId = ? AND type = ?', [ id, exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
postProcess(result[0]);
|
||||
|
||||
result[0].alternateDomains = alternateDomains;
|
||||
|
||||
postProcess(result[0]);
|
||||
|
||||
callback(null, result[0]);
|
||||
})
|
||||
callback(null, result[0]);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -162,22 +136,14 @@ function getByHttpPort(httpPort, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
|
||||
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes'
|
||||
+ ' FROM apps'
|
||||
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
|
||||
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
|
||||
+ ' WHERE httpPort = ? GROUP BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY, httpPort ], function (error, result) {
|
||||
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables'
|
||||
+ ' FROM apps LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId WHERE httpPort = ? GROUP BY apps.id', [ httpPort ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
database.query('SELECT ' + SUBDOMAIN_FIELDS + ' FROM subdomains WHERE appId = ? AND type = ?', [ result[0].id, exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
postProcess(result[0]);
|
||||
|
||||
result[0].alternateDomains = alternateDomains;
|
||||
postProcess(result[0]);
|
||||
|
||||
callback(null, result[0]);
|
||||
});
|
||||
callback(null, result[0]);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -186,22 +152,14 @@ function getByContainerId(containerId, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
|
||||
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes'
|
||||
+ ' FROM apps'
|
||||
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
|
||||
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
|
||||
+ ' WHERE containerId = ? GROUP BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY, containerId ], function (error, result) {
|
||||
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables'
|
||||
+ ' FROM apps LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId WHERE containerId = ? GROUP BY apps.id', [ containerId ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
database.query('SELECT ' + SUBDOMAIN_FIELDS + ' FROM subdomains WHERE appId = ? AND type = ?', [ result[0].id, exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
postProcess(result[0]);
|
||||
|
||||
result[0].alternateDomains = alternateDomains;
|
||||
postProcess(result[0]);
|
||||
|
||||
callback(null, result[0]);
|
||||
});
|
||||
callback(null, result[0]);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -209,39 +167,24 @@ function getAll(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
|
||||
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes'
|
||||
+ ' FROM apps'
|
||||
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
|
||||
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
|
||||
+ ' GROUP BY apps.id ORDER BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY ], function (error, results) {
|
||||
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables'
|
||||
+ ' FROM apps LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
|
||||
+ ' GROUP BY apps.id ORDER BY apps.id', function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
database.query('SELECT ' + SUBDOMAIN_FIELDS + ' FROM subdomains WHERE type = ?', [ exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
results.forEach(postProcess);
|
||||
|
||||
alternateDomains.forEach(function (d) {
|
||||
var domain = results.find(function (a) { return d.appId === a.id; });
|
||||
if (!domain) return;
|
||||
|
||||
domain.alternateDomains = domain.alternateDomains || [];
|
||||
domain.alternateDomains.push(d);
|
||||
});
|
||||
|
||||
results.forEach(postProcess);
|
||||
|
||||
callback(null, results);
|
||||
});
|
||||
callback(null, results);
|
||||
});
|
||||
}
|
||||
|
||||
function add(id, appStoreId, manifest, location, domain, ownerId, portBindings, data, callback) {
|
||||
function add(id, appStoreId, manifest, location, domain, 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 ownerId, 'string');
|
||||
assert.strictEqual(typeof portBindings, 'object');
|
||||
assert(data && typeof data === 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -253,53 +196,36 @@ function add(id, appStoreId, manifest, location, domain, ownerId, portBindings,
|
||||
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, accessRestrictionJson, memoryLimit, xFrameOptions, restoreConfigJson, sso, debugModeJson, robotsTxt, ownerId) ' +
|
||||
' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
args: [ id, appStoreId, manifestJson, installationState, accessRestrictionJson, memoryLimit, xFrameOptions, restoreConfigJson, sso, debugModeJson, robotsTxt, ownerId ]
|
||||
});
|
||||
|
||||
queries.push({
|
||||
query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)',
|
||||
args: [ id, domain, location, exports.SUBDOMAIN_TYPE_PRIMARY ]
|
||||
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, location, domain, accessRestrictionJson, memoryLimit, altDomain, xFrameOptions, restoreConfigJson, sso, debugModeJson) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
args: [ id, appStoreId, manifestJson, installationState, location, domain, accessRestrictionJson, memoryLimit, altDomain, xFrameOptions, restoreConfigJson, sso, debugModeJson ]
|
||||
});
|
||||
|
||||
Object.keys(portBindings).forEach(function (env) {
|
||||
queries.push({
|
||||
query: 'INSERT INTO appPortBindings (environmentVariable, hostPort, type, appId) VALUES (?, ?, ?, ?)',
|
||||
args: [ env, portBindings[env].hostPort, portBindings[env].type, id ]
|
||||
query: 'INSERT INTO appPortBindings (environmentVariable, hostPort, appId) VALUES (?, ?, ?)',
|
||||
args: [ env, portBindings[env], id ]
|
||||
});
|
||||
});
|
||||
|
||||
// only allocate a mailbox if mailboxName is set
|
||||
if (data.mailboxName) {
|
||||
queries.push({
|
||||
query: 'INSERT INTO mailboxes (name, type, domain, ownerId, ownerType) VALUES (?, ?, ?, ?, ?)',
|
||||
args: [ data.mailboxName, mailboxdb.TYPE_MAILBOX, domain, id, mailboxdb.OWNER_TYPE_APP ]
|
||||
});
|
||||
}
|
||||
|
||||
if (data.alternateDomains) {
|
||||
data.alternateDomains.forEach(function (d) {
|
||||
queries.push({
|
||||
query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)',
|
||||
args: [ id, d.domain, d.subdomain, exports.SUBDOMAIN_TYPE_REDIRECT ]
|
||||
});
|
||||
query: 'INSERT INTO mailboxes (name, domain, ownerId, ownerType) VALUES (?, ?, ?, ?)',
|
||||
args: [ data.mailboxName, domain, 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);
|
||||
@@ -326,19 +252,18 @@ function getPortBindings(id, callback) {
|
||||
|
||||
var portBindings = { };
|
||||
for (var i = 0; i < results.length; i++) {
|
||||
portBindings[results[i].environmentVariable] = { hostPort: results[i].hostPort, type: results[i].type };
|
||||
portBindings[results[i].environmentVariable] = results[i].hostPort;
|
||||
}
|
||||
|
||||
callback(null, portBindings);
|
||||
});
|
||||
}
|
||||
|
||||
function delPortBinding(hostPort, type, callback) {
|
||||
function delPortBinding(hostPort, callback) {
|
||||
assert.strictEqual(typeof hostPort, 'number');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('DELETE FROM appPortBindings WHERE hostPort=? AND type=?', [ hostPort, type ], function (error, result) {
|
||||
database.query('DELETE FROM appPortBindings WHERE hostPort=?', [ hostPort ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
@@ -351,7 +276,6 @@ function del(id, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var queries = [
|
||||
{ query: 'DELETE FROM subdomains WHERE appId = ?', args: [ id ] },
|
||||
{ query: 'DELETE FROM mailboxes WHERE ownerId=?', args: [ id ] },
|
||||
{ query: 'DELETE FROM appPortBindings WHERE appId = ?', args: [ id ] },
|
||||
{ query: 'DELETE FROM apps WHERE id = ?', args: [ id ] }
|
||||
@@ -359,7 +283,7 @@ function del(id, callback) {
|
||||
|
||||
database.transaction(queries, function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (results[3].affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
if (results[2].affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
@@ -369,7 +293,6 @@ function clear(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
async.series([
|
||||
database.query.bind(null, 'DELETE FROM subdomains'),
|
||||
database.query.bind(null, 'DELETE FROM appPortBindings'),
|
||||
database.query.bind(null, 'DELETE FROM appAddonConfigs'),
|
||||
database.query.bind(null, 'DELETE FROM apps')
|
||||
@@ -390,7 +313,6 @@ function updateWithConstraints(id, app, constraints, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
assert(!('portBindings' in app) || typeof app.portBindings === 'object');
|
||||
assert(!('accessRestriction' in app) || typeof app.accessRestriction === 'object' || app.accessRestriction === '');
|
||||
assert(!('alternateDomains' in app) || Array.isArray(app.alternateDomains));
|
||||
|
||||
var queries = [ ];
|
||||
|
||||
@@ -399,23 +321,8 @@ function updateWithConstraints(id, app, constraints, callback) {
|
||||
// replace entries by app id
|
||||
queries.push({ query: 'DELETE FROM appPortBindings WHERE appId = ?', args: [ id ] });
|
||||
Object.keys(portBindings).forEach(function (env) {
|
||||
var values = [ portBindings[env].hostPort, portBindings[env].type, env, id ];
|
||||
queries.push({ query: 'INSERT INTO appPortBindings (hostPort, type, environmentVariable, appId) VALUES(?, ?, ?, ?)', args: values });
|
||||
});
|
||||
}
|
||||
|
||||
if ('location' in app) {
|
||||
queries.push({ query: 'UPDATE subdomains SET subdomain = ? WHERE appId = ? AND type = ?', args: [ app.location, id, exports.SUBDOMAIN_TYPE_PRIMARY ]});
|
||||
}
|
||||
|
||||
if ('domain' in app) {
|
||||
queries.push({ query: 'UPDATE subdomains SET domain = ? WHERE appId = ? AND type = ?', args: [ app.domain, id, exports.SUBDOMAIN_TYPE_PRIMARY ]});
|
||||
}
|
||||
|
||||
if ('alternateDomains' in app) {
|
||||
queries.push({ query: 'DELETE FROM subdomains WHERE appId = ? AND type = ?', args: [ id, exports.SUBDOMAIN_TYPE_REDIRECT ]});
|
||||
app.alternateDomains.forEach(function (d) {
|
||||
queries.push({ query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)', args: [ id, d.domain, d.subdomain, exports.SUBDOMAIN_TYPE_REDIRECT ]});
|
||||
var values = [ portBindings[env], env, id ];
|
||||
queries.push({ query: 'INSERT INTO appPortBindings (hostPort, environmentVariable, appId) VALUES(?, ?, ?)', args: values });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -424,7 +331,7 @@ function updateWithConstraints(id, app, constraints, callback) {
|
||||
if (p === 'manifest' || p === 'oldConfig' || p === 'updateConfig' || p === 'restoreConfig' || p === 'accessRestriction' || p === 'debugMode') {
|
||||
fields.push(`${p}Json = ?`);
|
||||
values.push(JSON.stringify(app[p]));
|
||||
} else if (p !== 'portBindings' && p !== 'location' && p !== 'domain' && p !== 'alternateDomains') {
|
||||
} else if (p !== 'portBindings') {
|
||||
fields.push(p + ' = ?');
|
||||
values.push(app[p]);
|
||||
}
|
||||
@@ -596,31 +503,3 @@ function getAddonConfigByName(appId, addonId, name, callback) {
|
||||
callback(null, results[0].value);
|
||||
});
|
||||
}
|
||||
|
||||
function setOwner(appId, ownerId, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof ownerId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('UPDATE apps SET ownerId=? WHERE appId=?', [ ownerId, appId ], function (error, results) {
|
||||
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND, 'No such user'));
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND, 'No such app'));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function transferOwnership(oldOwnerId, newOwnerId, callback) {
|
||||
assert.strictEqual(typeof oldOwnerId, 'string');
|
||||
assert.strictEqual(typeof newOwnerId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('UPDATE apps SET ownerId=? WHERE ownerId=?', [ newOwnerId, oldOwnerId ], function (error, results) {
|
||||
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND, 'No such user'));
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ var appdb = require('./appdb.js'),
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
config = require('./config.js'),
|
||||
debug = require('debug')('box:apphealthmonitor'),
|
||||
docker = require('./docker.js').connection,
|
||||
mailer = require('./mailer.js'),
|
||||
@@ -23,9 +24,13 @@ var gRunTimeout = null;
|
||||
var gDockerEventStream = null;
|
||||
|
||||
function debugApp(app) {
|
||||
assert(typeof app === 'object');
|
||||
assert(!app || typeof app === 'object');
|
||||
|
||||
debug(app.fqdn + ' ' + app.manifest.id + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)) + ' - ' + app.id);
|
||||
var prefix = app ? app.intrinsicFqdn : '(no app)';
|
||||
var manifestAppId = app ? app.manifest.id : '';
|
||||
var id = app ? app.id : '';
|
||||
|
||||
debug(prefix + ' ' + manifestAppId + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)) + ' - ' + id);
|
||||
}
|
||||
|
||||
function setHealth(app, health, callback) {
|
||||
@@ -66,9 +71,6 @@ function setHealth(app, health, callback) {
|
||||
|
||||
// callback is called with error for fatal errors and not if health check failed
|
||||
function checkAppHealth(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (app.installationState !== appdb.ISTATE_INSTALLED || app.runState !== appdb.RSTATE_RUNNING) {
|
||||
debugApp(app, 'skipped. istate:%s rstate:%s', app.installationState, app.runState);
|
||||
return callback(null);
|
||||
|
||||
+232
-350
@@ -4,8 +4,6 @@ exports = module.exports = {
|
||||
AppsError: AppsError,
|
||||
|
||||
hasAccessTo: hasAccessTo,
|
||||
removeInternalFields: removeInternalFields,
|
||||
removeRestrictedFields: removeRestrictedFields,
|
||||
|
||||
get: get,
|
||||
getByIpAddress: getByIpAddress,
|
||||
@@ -42,25 +40,21 @@ exports = module.exports = {
|
||||
downloadFile: downloadFile,
|
||||
uploadFile: uploadFile,
|
||||
|
||||
setOwner: setOwner,
|
||||
transferOwnership: transferOwnership,
|
||||
|
||||
PORT_TYPE_TCP: 'tcp',
|
||||
PORT_TYPE_UDP: 'udp',
|
||||
|
||||
// exported for testing
|
||||
_validateHostname: validateHostname,
|
||||
_validatePortBindings: validatePortBindings,
|
||||
_validateAccessRestriction: validateAccessRestriction,
|
||||
_translatePortBindings: translatePortBindings
|
||||
_validateAccessRestriction: validateAccessRestriction
|
||||
};
|
||||
|
||||
var appdb = require('./appdb.js'),
|
||||
var addons = require('./addons.js'),
|
||||
appdb = require('./appdb.js'),
|
||||
appstore = require('./appstore.js'),
|
||||
AppstoreError = require('./appstore.js').AppstoreError,
|
||||
assert = require('assert'),
|
||||
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'),
|
||||
@@ -68,29 +62,28 @@ var appdb = require('./appdb.js'),
|
||||
docker = require('./docker.js'),
|
||||
domaindb = require('./domaindb.js'),
|
||||
domains = require('./domains.js'),
|
||||
DomainsError = require('./domains.js').DomainsError,
|
||||
DomainError = require('./domains.js').DomainError,
|
||||
eventlog = require('./eventlog.js'),
|
||||
fs = require('fs'),
|
||||
mail = require('./mail.js'),
|
||||
groups = require('./groups.js'),
|
||||
mailboxdb = require('./mailboxdb.js'),
|
||||
manifestFormat = require('cloudron-manifestformat'),
|
||||
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'),
|
||||
util = require('util'),
|
||||
uuid = require('uuid'),
|
||||
validator = require('validator'),
|
||||
_ = require('underscore');
|
||||
validator = require('validator');
|
||||
|
||||
// http://dustinsenos.com/articles/customErrorsInNode
|
||||
// http://code.google.com/p/v8/wiki/JavaScriptStackTraceApi
|
||||
@@ -125,12 +118,46 @@ AppsError.BILLING_REQUIRED = 'Billing Required';
|
||||
AppsError.ACCESS_DENIED = 'Access denied';
|
||||
AppsError.BAD_CERTIFICATE = 'Invalid certificate';
|
||||
|
||||
// validate the port bindings
|
||||
function validatePortBindings(portBindings, manifest) {
|
||||
assert.strictEqual(typeof portBindings, 'object');
|
||||
assert.strictEqual(typeof manifest, 'object');
|
||||
// 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');
|
||||
|
||||
// keep the public ports in sync with firewall rules in setup/start/cloudron-firewall.sh
|
||||
const RESERVED_LOCATIONS = [
|
||||
constants.API_LOCATION,
|
||||
constants.SMTP_LOCATION,
|
||||
constants.IMAP_LOCATION,
|
||||
constants.POSTMAN_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');
|
||||
|
||||
// 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');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// validate the port bindings
|
||||
function validatePortBindings(portBindings, tcpPorts) {
|
||||
assert.strictEqual(typeof portBindings, 'object');
|
||||
|
||||
// keep the public ports in sync with firewall rules in scripts/initializeBaseUbuntuImage.sh
|
||||
// these ports are reserved even if we listen only on 127.0.0.1 because we setup HostIp to be 127.0.0.1
|
||||
// for custom tcp ports
|
||||
var RESERVED_PORTS = [
|
||||
@@ -146,71 +173,38 @@ function validatePortBindings(portBindings, manifest) {
|
||||
993, /* imaps */
|
||||
2003, /* graphite (lo) */
|
||||
2004, /* graphite (lo) */
|
||||
2020, /* mail server */
|
||||
2514, /* cloudron-syslog (lo) */
|
||||
2020, /* install server */
|
||||
config.get('port'), /* app server (lo) */
|
||||
config.get('sysadminPort'), /* sysadmin app server (lo) */
|
||||
config.get('smtpPort'), /* internal smtp port (lo) */
|
||||
config.get('ldapPort'), /* ldap server (lo) */
|
||||
3306, /* mysql (lo) */
|
||||
4190, /* managesieve */
|
||||
8000, /* graphite (lo) */
|
||||
8000 /* graphite (lo) */
|
||||
];
|
||||
|
||||
if (!portBindings) return null;
|
||||
|
||||
for (let portName in portBindings) {
|
||||
if (!/^[a-zA-Z0-9_]+$/.test(portName)) return new AppsError(AppsError.BAD_FIELD, `${portName} is not a valid environment variable`);
|
||||
var env;
|
||||
for (env in portBindings) {
|
||||
if (!/^[a-zA-Z0-9_]+$/.test(env)) return new AppsError(AppsError.BAD_FIELD, env + ' is not valid environment variable');
|
||||
|
||||
const hostPort = portBindings[portName];
|
||||
if (!Number.isInteger(hostPort)) return new AppsError(AppsError.BAD_FIELD, `${hostPort} is not an integer`);
|
||||
if (RESERVED_PORTS.indexOf(hostPort) !== -1) return new AppsError(AppsError.PORT_RESERVED, String(hostPort));
|
||||
if (hostPort <= 1023 || hostPort > 65535) return new AppsError(AppsError.BAD_FIELD, `${hostPort} is not in permitted range`);
|
||||
if (!Number.isInteger(portBindings[env])) return new AppsError(AppsError.BAD_FIELD, portBindings[env] + ' is not an integer');
|
||||
if (RESERVED_PORTS.indexOf(portBindings[env]) !== -1) return new AppsError(AppsError.PORT_RESERVED, String(portBindings[env]));
|
||||
if (portBindings[env] <= 1023 || portBindings[env] > 65535) return new AppsError(AppsError.BAD_FIELD, portBindings[env] + ' is not in permitted range');
|
||||
|
||||
}
|
||||
|
||||
// it is OK if there is no 1-1 mapping between values in manifest.tcpPorts and portBindings. missing values implies
|
||||
// that the user wants the service disabled
|
||||
const tcpPorts = manifest.tcpPorts || { };
|
||||
const udpPorts = manifest.udpPorts || { };
|
||||
for (let portName in portBindings) {
|
||||
if (!(portName in tcpPorts) && !(portName in udpPorts)) return new AppsError(AppsError.BAD_FIELD, `Invalid portBindings ${portName}`);
|
||||
tcpPorts = tcpPorts || { };
|
||||
for (env in portBindings) {
|
||||
if (!(env in tcpPorts)) return new AppsError(AppsError.BAD_FIELD, 'Invalid portBindings ' + env);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function translatePortBindings(portBindings, manifest) {
|
||||
assert.strictEqual(typeof portBindings, 'object');
|
||||
assert.strictEqual(typeof manifest, 'object');
|
||||
|
||||
if (!portBindings) return null;
|
||||
|
||||
let result = {};
|
||||
const tcpPorts = manifest.tcpPorts || { };
|
||||
|
||||
for (let portName in portBindings) {
|
||||
const portType = portName in tcpPorts ? exports.PORT_TYPE_TCP : exports.PORT_TYPE_UDP;
|
||||
result[portName] = { hostPort: portBindings[portName], type: portType };
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function postProcess(app) {
|
||||
let result = {};
|
||||
for (let portName in app.portBindings) {
|
||||
result[portName] = app.portBindings[portName].hostPort;
|
||||
}
|
||||
app.portBindings = result;
|
||||
}
|
||||
|
||||
function addSpacesSuffix(location, user) {
|
||||
if (user.admin || !config.isSpacesEnabled()) return location;
|
||||
|
||||
const spacesSuffix = user.username.replace(/\./g, '-');
|
||||
return location === '' ? spacesSuffix : `${location}-${spacesSuffix}`;
|
||||
}
|
||||
|
||||
function validateAccessRestriction(accessRestriction) {
|
||||
assert.strictEqual(typeof accessRestriction, 'object');
|
||||
|
||||
@@ -306,44 +300,27 @@ function getDuplicateErrorDetails(location, portBindings, error) {
|
||||
if (match[1] === location) return new AppsError(AppsError.ALREADY_EXISTS);
|
||||
|
||||
// check if any of the port bindings conflict
|
||||
for (let portName in portBindings) {
|
||||
if (portBindings[portName] === parseInt(match[1])) return new AppsError(AppsError.PORT_CONFLICT, match[1]);
|
||||
for (var env in portBindings) {
|
||||
if (portBindings[env] === parseInt(match[1])) return new AppsError(AppsError.PORT_CONFLICT, match[1]);
|
||||
}
|
||||
|
||||
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,
|
||||
intrinsicFqdn: app.intrinsicFqdn,
|
||||
accessRestriction: app.accessRestriction,
|
||||
portBindings: app.portBindings,
|
||||
memoryLimit: app.memoryLimit,
|
||||
xFrameOptions: app.xFrameOptions || 'SAMEORIGIN',
|
||||
robotsTxt: app.robotsTxt,
|
||||
sso: app.sso,
|
||||
alternateDomains: app.alternateDomains || []
|
||||
altDomain: app.altDomain
|
||||
};
|
||||
}
|
||||
|
||||
function removeInternalFields(app) {
|
||||
return _.pick(app,
|
||||
'id', 'appStoreId', 'installationState', 'installationProgress', 'runState', 'health',
|
||||
'location', 'domain', 'fqdn', 'mailboxName',
|
||||
'accessRestriction', 'manifest', 'portBindings', 'iconUrl', 'memoryLimit', 'xFrameOptions',
|
||||
'sso', 'debugMode', 'robotsTxt', 'enableBackup', 'creationTime', 'updateTime', 'ts',
|
||||
'alternateDomains', 'ownerId');
|
||||
}
|
||||
|
||||
function removeRestrictedFields(app) {
|
||||
return _.pick(app,
|
||||
'id', 'appStoreId', 'installationState', 'installationProgress', 'runState', 'health', 'ownerId',
|
||||
'location', 'domain', 'fqdn', 'manifest', 'portBindings', 'iconUrl', 'creationTime', 'ts');
|
||||
}
|
||||
|
||||
function getIconUrlSync(app) {
|
||||
var iconPath = paths.APP_ICONS_DIR + '/' + app.id + '.png';
|
||||
return fs.existsSync(iconPath) ? '/api/v1/apps/' + app.id + '/icon' : null;
|
||||
@@ -359,13 +336,20 @@ function hasAccessTo(app, user, callback) {
|
||||
// check user access
|
||||
if (app.accessRestriction.users.some(function (e) { return e === user.id; })) return callback(null, true);
|
||||
|
||||
if (user.admin) return callback(null, true); // admins can always access any app
|
||||
// check group access
|
||||
groups.getGroups(user.id, function (error, groupIds) {
|
||||
if (error) return callback(null, false);
|
||||
|
||||
if (!app.accessRestriction.groups) return callback(null, false);
|
||||
const isAdmin = groupIds.indexOf(constants.ADMIN_GROUP_ID) !== -1;
|
||||
|
||||
if (app.accessRestriction.groups.some(function (gid) { return user.groupIds.indexOf(gid) !== -1; })) return callback(null, true);
|
||||
if (isAdmin) return callback(null, true); // admins can always access any app
|
||||
|
||||
callback(null, false);
|
||||
if (!app.accessRestriction.groups) return callback(null, false);
|
||||
|
||||
if (app.accessRestriction.groups.some(function (gid) { return groupIds.indexOf(gid) !== -1; })) return callback(null, true);
|
||||
|
||||
callback(null, false);
|
||||
});
|
||||
}
|
||||
|
||||
function get(appId, callback) {
|
||||
@@ -376,21 +360,15 @@ 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));
|
||||
|
||||
postProcess(app);
|
||||
|
||||
domaindb.get(app.domain, function (error, domainObject) {
|
||||
domaindb.get(app.domain, function (error, result) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
app.intrinsicFqdn = domains.fqdn(app.location, app.domain, result.provider);
|
||||
app.iconUrl = getIconUrlSync(app);
|
||||
app.fqdn = domains.fqdn(app.location, domainObject);
|
||||
app.fqdn = app.altDomain || app.intrinsicFqdn;
|
||||
app.cnameTarget = app.altDomain ? app.intrinsicFqdn : null;
|
||||
|
||||
mailboxdb.getByOwnerId(app.id, function (error, mailboxes) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
if (!error) app.mailboxName = mailboxes[0].name;
|
||||
|
||||
callback(null, app);
|
||||
});
|
||||
callback(null, app);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -406,21 +384,15 @@ 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));
|
||||
|
||||
postProcess(app);
|
||||
|
||||
domaindb.get(app.domain, function (error, domainObject) {
|
||||
domaindb.get(app.domain, function (error, result) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
app.intrinsicFqdn = domains.fqdn(app.location, app.domain, result.provider);
|
||||
app.iconUrl = getIconUrlSync(app);
|
||||
app.fqdn = domains.fqdn(app.location, domainObject);
|
||||
app.fqdn = app.altDomain || app.intrinsicFqdn;
|
||||
app.cnameTarget = app.altDomain ? app.intrinsicFqdn : null;
|
||||
|
||||
mailboxdb.getByOwnerId(app.id, function (error, mailboxes) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
if (!error) app.mailboxName = mailboxes[0].name;
|
||||
|
||||
callback(null, app);
|
||||
});
|
||||
callback(null, app);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -432,22 +404,16 @@ function getAll(callback) {
|
||||
appdb.getAll(function (error, apps) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
apps.forEach(postProcess);
|
||||
|
||||
async.eachSeries(apps, function (app, iteratorDone) {
|
||||
domaindb.get(app.domain, function (error, domainObject) {
|
||||
domaindb.get(app.domain, function (error, result) {
|
||||
if (error) return iteratorDone(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
app.intrinsicFqdn = domains.fqdn(app.location, app.domain, result.provider);
|
||||
app.iconUrl = getIconUrlSync(app);
|
||||
app.fqdn = domains.fqdn(app.location, domainObject);
|
||||
app.fqdn = app.altDomain || app.intrinsicFqdn;
|
||||
app.cnameTarget = app.altDomain ? app.intrinsicFqdn : null;
|
||||
|
||||
mailboxdb.getByOwnerId(app.id, function (error, mailboxes) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
if (!error) app.mailboxName = mailboxes[0].name;
|
||||
|
||||
iteratorDone(null, app);
|
||||
});
|
||||
iteratorDone();
|
||||
});
|
||||
}, function (error) {
|
||||
if (error) return callback(error);
|
||||
@@ -484,19 +450,14 @@ function downloadManifest(appStoreId, manifest, callback) {
|
||||
superagent.get(url).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppsError(AppsError.EXTERNAL_ERROR, 'Network error downloading manifest:' + error.message));
|
||||
|
||||
if (result.statusCode !== 200) return callback(new AppsError(AppsError.NOT_FOUND, util.format('Failed to get app info from store.', result.statusCode, result.text)));
|
||||
if (result.statusCode !== 200) return callback(new AppsError(AppsError.EXTERNAL_ERROR, util.format('Failed to get app info from store.', result.statusCode, result.text)));
|
||||
|
||||
callback(null, parts[0], result.body.manifest);
|
||||
});
|
||||
}
|
||||
|
||||
function mailboxNameForLocation(location, manifest) {
|
||||
return (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app';
|
||||
}
|
||||
|
||||
function install(data, user, auditSource, callback) {
|
||||
function install(data, auditSource, callback) {
|
||||
assert(data && typeof data === 'object');
|
||||
assert(user && typeof user === 'object');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -508,15 +469,14 @@ function install(data, user, auditSource, callback) {
|
||||
cert = data.cert || null,
|
||||
key = data.key || null,
|
||||
memoryLimit = data.memoryLimit || 0,
|
||||
altDomain = data.altDomain || null,
|
||||
xFrameOptions = data.xFrameOptions || 'SAMEORIGIN',
|
||||
sso = 'sso' in data ? data.sso : null,
|
||||
debugMode = data.debugMode || null,
|
||||
robotsTxt = data.robotsTxt || null,
|
||||
enableBackup = 'enableBackup' in data ? data.enableBackup : true,
|
||||
backupId = data.backupId || null,
|
||||
backupFormat = data.backupFormat || 'tgz',
|
||||
ownerId = data.ownerId,
|
||||
alternateDomains = data.alternateDomains || [];
|
||||
backupFormat = data.backupFormat || 'tgz';
|
||||
|
||||
assert(data.appStoreId || data.manifest); // atleast one of them is required
|
||||
|
||||
@@ -529,7 +489,7 @@ function install(data, user, auditSource, callback) {
|
||||
error = checkManifestConstraints(manifest);
|
||||
if (error) return callback(error);
|
||||
|
||||
error = validatePortBindings(portBindings, manifest);
|
||||
error = validatePortBindings(portBindings, manifest.tcpPorts);
|
||||
if (error) return callback(error);
|
||||
|
||||
error = validateAccessRestriction(accessRestriction);
|
||||
@@ -554,6 +514,8 @@ function install(data, user, 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) {
|
||||
@@ -565,89 +527,69 @@ function install(data, user, auditSource, callback) {
|
||||
}
|
||||
|
||||
domains.get(domain, function (error, domainObject) {
|
||||
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such domain'));
|
||||
if (error && error.reason === DomainError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such domain'));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Could not get domain info:' + error.message));
|
||||
|
||||
location = addSpacesSuffix(location, user);
|
||||
alternateDomains.forEach(function (ad) { ad.subdomain = addSpacesSuffix(ad.subdomain, user); }); // TODO: validate these
|
||||
var intrinsicFqdn = domains.fqdn(location, domain, domainObject.provider);
|
||||
|
||||
error = domains.validateHostname(location, domainObject);
|
||||
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Bad location: ' + error.message));
|
||||
error = validateHostname(location, domain, intrinsicFqdn);
|
||||
if (error) return callback(error);
|
||||
|
||||
if (cert && key) {
|
||||
let fqdn = domains.fqdn(location, domain, domainObject);
|
||||
error = reverseProxy.validateCertificate(fqdn, cert, key);
|
||||
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
|
||||
}
|
||||
error = certificates.validateCertificate(cert, key, intrinsicFqdn);
|
||||
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
|
||||
|
||||
debug('Will install app with id : ' + appId);
|
||||
|
||||
var data = {
|
||||
accessRestriction: accessRestriction,
|
||||
memoryLimit: memoryLimit,
|
||||
xFrameOptions: xFrameOptions,
|
||||
sso: sso,
|
||||
debugMode: debugMode,
|
||||
mailboxName: mailboxNameForLocation(location, manifest),
|
||||
restoreConfig: backupId ? { backupId: backupId, backupFormat: backupFormat } : null,
|
||||
enableBackup: enableBackup,
|
||||
robotsTxt: robotsTxt,
|
||||
alternateDomains: alternateDomains
|
||||
};
|
||||
|
||||
appdb.add(appId, appStoreId, manifest, location, domain, ownerId, translatePortBindings(portBindings, manifest), 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));
|
||||
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));
|
||||
|
||||
appstore.purchase(appId, appStoreId, function (appstoreError) {
|
||||
// if purchase failed, rollback the appdb record
|
||||
if (appstoreError) {
|
||||
appdb.del(appId, function (error) {
|
||||
if (error) console.error('Failed to rollback app installation.', error);
|
||||
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,
|
||||
intrinsicFqdn: intrinsicFqdn
|
||||
};
|
||||
|
||||
if (appstoreError.reason === AppstoreError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, appstoreError.message));
|
||||
if (appstoreError && appstoreError.reason === AppstoreError.BILLING_REQUIRED) return callback(new AppsError(AppsError.BILLING_REQUIRED, appstoreError.message));
|
||||
if (appstoreError && appstoreError.reason === AppstoreError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, appstoreError.message));
|
||||
|
||||
callback(new AppsError(AppsError.INTERNAL_ERROR, appstoreError));
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
appdb.add(appId, appStoreId, manifest, location, domain, portBindings, data, function (error) {
|
||||
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location, portBindings, error));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
// save cert to boxdata/certs
|
||||
if (cert && key) {
|
||||
let fqdn = domains.fqdn(location, domainObject);
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, fqdn + '.user.cert'), cert)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving cert: ' + safe.error.message));
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, fqdn + '.user.key'), key)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving key: ' + safe.error.message));
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, intrinsicFqdn + '.user.cert'), cert)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving cert: ' + safe.error.message));
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, intrinsicFqdn + '.user.key'), key)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving key: ' + safe.error.message));
|
||||
}
|
||||
|
||||
taskmanager.restartAppTask(appId);
|
||||
|
||||
// fetch fresh app object for eventlog
|
||||
get(appId, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
eventlog.add(eventlog.ACTION_APP_INSTALL, auditSource, { appId: appId, location: location, domain: domain, manifest: manifest, backupId: backupId });
|
||||
|
||||
eventlog.add(eventlog.ACTION_APP_INSTALL, auditSource, { appId: appId, app: result });
|
||||
|
||||
callback(null, { id : appId });
|
||||
});
|
||||
callback(null, { id : appId });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function configure(appId, data, user, auditSource, callback) {
|
||||
function configure(appId, data, auditSource, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert(data && typeof data === 'object');
|
||||
assert(user && typeof user === 'object');
|
||||
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();
|
||||
@@ -662,11 +604,15 @@ function configure(appId, data, user, 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) {
|
||||
error = validatePortBindings(data.portBindings, app.manifest);
|
||||
portBindings = values.portBindings = data.portBindings;
|
||||
error = validatePortBindings(values.portBindings, app.manifest.tcpPorts);
|
||||
if (error) return callback(error);
|
||||
values.portBindings = translatePortBindings(data.portBindings, app.manifest);
|
||||
portBindings = data.portBindings;
|
||||
} else {
|
||||
portBindings = app.portBindings;
|
||||
}
|
||||
@@ -695,39 +641,26 @@ function configure(appId, data, user, auditSource, callback) {
|
||||
if (error) return callback(error);
|
||||
}
|
||||
|
||||
if ('mailboxName' in data) {
|
||||
error = mail.validateName(data.mailboxName);
|
||||
if (error) return callback(error);
|
||||
}
|
||||
|
||||
if ('alternateDomains' in data) {
|
||||
// TODO validate all subdomains [{ domain: '', subdomain: ''}]
|
||||
values.alternateDomains = data.alternateDomains;
|
||||
values.alternateDomains.forEach(function (ad) { ad.subdomain = addSpacesSuffix(ad.subdomain, user); }); // TODO: validate these
|
||||
}
|
||||
|
||||
domains.get(domain, function (error, domainObject) {
|
||||
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such domain'));
|
||||
if (error && error.reason === DomainError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such domain'));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Could not get domain info:' + error.message));
|
||||
|
||||
location = addSpacesSuffix(location, user);
|
||||
var intrinsicFqdn = domains.fqdn(location, domain, domainObject.provider);
|
||||
|
||||
error = domains.validateHostname(location, domainObject);
|
||||
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Bad location: ' + error.message));
|
||||
error = validateHostname(location, domain, intrinsicFqdn);
|
||||
if (error) return callback(error);
|
||||
|
||||
// save cert to boxdata/certs. TODO: move this to apptask when we have a real task queue
|
||||
if ('cert' in data && 'key' in data) {
|
||||
let fqdn = domains.fqdn(location, domainObject);
|
||||
|
||||
if (data.cert && data.key) {
|
||||
error = reverseProxy.validateCertificate(fqdn, data.cert, data.key);
|
||||
error = certificates.validateCertificate(data.cert, data.key, intrinsicFqdn);
|
||||
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
|
||||
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${fqdn}.user.cert`), data.cert)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving cert: ' + safe.error.message));
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${fqdn}.user.key`), data.key)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving key: ' + safe.error.message));
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, intrinsicFqdn + '.user.cert'), data.cert)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving cert: ' + safe.error.message));
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, intrinsicFqdn + '.user.key'), data.key)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving key: ' + safe.error.message));
|
||||
} else { // remove existing cert/key
|
||||
if (!safe.fs.unlinkSync(path.join(paths.APP_CERTS_DIR, `${fqdn}.user.cert`))) debug('Error removing cert: ' + safe.error.message);
|
||||
if (!safe.fs.unlinkSync(path.join(paths.APP_CERTS_DIR, `${fqdn}.user.key`))) debug('Error removing key: ' + safe.error.message);
|
||||
if (!safe.fs.unlinkSync(path.join(paths.APP_CERTS_DIR, intrinsicFqdn + '.user.cert'))) debug('Error removing cert: ' + safe.error.message);
|
||||
if (!safe.fs.unlinkSync(path.join(paths.APP_CERTS_DIR, intrinsicFqdn + '.user.key'))) debug('Error removing key: ' + safe.error.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -737,12 +670,9 @@ function configure(appId, data, user, auditSource, callback) {
|
||||
|
||||
debug('Will configure app with id:%s values:%j', appId, values);
|
||||
|
||||
// make the mailbox name follow the apps new location, if the user did not set it explicitly
|
||||
var oldName = app.mailboxName;
|
||||
var newName = data.mailboxName || (app.mailboxName.endsWith('.app') ? mailboxNameForLocation(location, app.manifest) : app.mailboxName);
|
||||
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 (newName.endsWith('.app')) error = null; // ignore internal mailbox conflict errors since we want to show location conflict errors in the UI
|
||||
|
||||
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));
|
||||
@@ -754,14 +684,9 @@ function configure(appId, data, user, auditSource, callback) {
|
||||
|
||||
taskmanager.restartAppTask(appId);
|
||||
|
||||
// fetch fresh app object for eventlog
|
||||
get(appId, function (error, result) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: appId });
|
||||
|
||||
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: appId, app: result });
|
||||
|
||||
callback(null);
|
||||
});
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -801,8 +726,9 @@ function update(appId, data, auditSource, callback) {
|
||||
}
|
||||
}
|
||||
|
||||
get(appId, function (error, app) {
|
||||
if (error) return callback(error);
|
||||
appdb.get(appId, function (error, app) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
// prevent user from installing a app with different manifest id over an existing app
|
||||
// this allows cloudron install -f --app <appid> for an app installed from the appStore
|
||||
@@ -827,7 +753,7 @@ function update(appId, data, auditSource, callback) {
|
||||
|
||||
taskmanager.restartAppTask(appId);
|
||||
|
||||
eventlog.add(eventlog.ACTION_APP_UPDATE, auditSource, { appId: appId, toManifest: manifest, fromManifest: app.manifest, force: data.force, app: app });
|
||||
eventlog.add(eventlog.ACTION_APP_UPDATE, auditSource, { appId: appId, toManifest: manifest, fromManifest: app.manifest, force: data.force });
|
||||
|
||||
// clear update indicator, if update fails, it will come back through the update checker
|
||||
updateChecker.resetAppUpdateInfo(appId);
|
||||
@@ -838,6 +764,12 @@ function update(appId, data, auditSource, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function appLogFilter(app) {
|
||||
var names = [ app.id ].concat(addons.getContainerNamesSync(app, app.manifest.addons));
|
||||
|
||||
return names.map(function (name) { return 'CONTAINER_NAME=' + name; });
|
||||
}
|
||||
|
||||
function getLogs(appId, options, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert(options && typeof options === 'object');
|
||||
@@ -845,38 +777,34 @@ 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,
|
||||
format = options.format || 'json',
|
||||
follow = !!options.follow;
|
||||
follow = !!options.follow,
|
||||
format = options.format || 'json';
|
||||
|
||||
assert.strictEqual(typeof lines, 'number');
|
||||
assert.strictEqual(typeof format, 'string');
|
||||
var args = [ '--no-pager', '--lines=' + lines ];
|
||||
if (follow) args.push('--follow');
|
||||
if (format == 'short') args.push('--output=short', '-a'); else args.push('--output=json');
|
||||
args = args.concat(appLogFilter(app));
|
||||
|
||||
var args = [ '--lines=' + lines ];
|
||||
if (follow) args.push('--follow', '--retry', '--quiet'); // same as -F. to make it work if file doesn't exist, --quiet to not output file headers, which are no logs
|
||||
args.push(path.join(paths.LOG_DIR, appId, 'apptask.log'));
|
||||
args.push(path.join(paths.LOG_DIR, appId, 'app.log'));
|
||||
|
||||
var cp = spawn('/usr/bin/tail', args);
|
||||
var cp = spawn('/bin/journalctl', args);
|
||||
|
||||
var transformStream = split(function mapper(line) {
|
||||
if (format !== 'json') return line + '\n';
|
||||
|
||||
var data = line.split(' '); // logs are <ISOtimestamp> <msg>
|
||||
var timestamp = (new Date(data[0])).getTime();
|
||||
if (isNaN(timestamp)) timestamp = 0;
|
||||
var message = line.slice(data[0].length+1);
|
||||
|
||||
// ignore faulty empty logs
|
||||
if (!timestamp && !message) return;
|
||||
var obj = safe.JSON.parse(line);
|
||||
if (!obj) return undefined;
|
||||
|
||||
var source = obj.CONTAINER_NAME.slice(app.id.length + 1);
|
||||
return JSON.stringify({
|
||||
realtimeTimestamp: timestamp * 1000,
|
||||
message: message,
|
||||
source: appId
|
||||
realtimeTimestamp: obj.__REALTIME_TIMESTAMP,
|
||||
monotonicTimestamp: obj.__MONOTONIC_TIMESTAMP,
|
||||
message: obj.MESSAGE,
|
||||
source: source || 'main'
|
||||
}) + '\n';
|
||||
});
|
||||
|
||||
@@ -896,8 +824,9 @@ function restore(appId, data, auditSource, callback) {
|
||||
|
||||
debug('Will restore app with id:%s', appId);
|
||||
|
||||
get(appId, function (error, app) {
|
||||
if (error) return callback(error);
|
||||
appdb.get(appId, function (error, app) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
// for empty or null backupId, use existing manifest to mimic a reinstall
|
||||
var func = data.backupId ? backups.get.bind(null, data.backupId) : function (next) { return next(null, { manifest: app.manifest }); };
|
||||
@@ -926,7 +855,7 @@ function restore(appId, data, auditSource, callback) {
|
||||
|
||||
taskmanager.restartAppTask(appId);
|
||||
|
||||
eventlog.add(eventlog.ACTION_APP_RESTORE, auditSource, { appId: appId, app: app });
|
||||
eventlog.add(eventlog.ACTION_APP_RESTORE, auditSource, { appId: appId });
|
||||
|
||||
callback(null);
|
||||
});
|
||||
@@ -934,10 +863,9 @@ function restore(appId, data, auditSource, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function clone(appId, data, user, auditSource, callback) {
|
||||
function clone(appId, data, auditSource, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof data, 'object');
|
||||
assert(user && typeof user === 'object');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -946,21 +874,20 @@ function clone(appId, data, user, auditSource, callback) {
|
||||
var location = data.location.toLowerCase(),
|
||||
domain = data.domain.toLowerCase(),
|
||||
portBindings = data.portBindings || null,
|
||||
backupId = data.backupId,
|
||||
ownerId = data.ownerId;
|
||||
backupId = data.backupId;
|
||||
|
||||
assert.strictEqual(typeof backupId, 'string');
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof portBindings, 'object');
|
||||
assert(ownerId === null || typeof ownerId === 'string');
|
||||
|
||||
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'));
|
||||
@@ -969,61 +896,45 @@ function clone(appId, data, user, auditSource, callback) {
|
||||
error = checkManifestConstraints(backupInfo.manifest);
|
||||
if (error) return callback(error);
|
||||
|
||||
error = validatePortBindings(portBindings, backupInfo.manifest);
|
||||
error = validatePortBindings(portBindings, backupInfo.manifest.tcpPorts);
|
||||
if (error) return callback(error);
|
||||
|
||||
domains.get(domain, function (error, domainObject) {
|
||||
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new AppsError(AppsError.EXTERNAL_ERROR, 'No such domain'));
|
||||
if (error && error.reason === DomainError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such domain'));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Could not get domain info:' + error.message));
|
||||
|
||||
location = addSpacesSuffix(location, user);
|
||||
error = domains.validateHostname(location, domainObject);
|
||||
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Bad location: ' + error.message));
|
||||
var intrinsicFqdn = domains.fqdn(location, domain, domainObject.provider);
|
||||
|
||||
error = validateHostname(location, domain, intrinsicFqdn);
|
||||
if (error) return callback(error);
|
||||
|
||||
var newAppId = uuid.v4(), manifest = backupInfo.manifest;
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
appdb.add(newAppId, app.appStoreId, manifest, location, domain, ownerId, translatePortBindings(portBindings, manifest), data, function (error) {
|
||||
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location, portBindings, error));
|
||||
appstore.purchase(newAppId, app.appStoreId, function (error) {
|
||||
if (error && error.reason === AppstoreError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
|
||||
if (error && error.reason === AppstoreError.BILLING_REQUIRED) return callback(new AppsError(AppsError.BILLING_REQUIRED, error.message));
|
||||
if (error && error.reason === AppstoreError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
appstore.purchase(newAppId, app.appStoreId, function (appstoreError) {
|
||||
// if purchase failed, rollback the appdb record
|
||||
if (appstoreError) {
|
||||
appdb.del(newAppId, function (error) {
|
||||
if (error) console.error('Failed to rollback app installation.', 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'
|
||||
};
|
||||
|
||||
if (appstoreError.reason === AppstoreError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, appstoreError.message));
|
||||
if (appstoreError && appstoreError.reason === AppstoreError.BILLING_REQUIRED) return callback(new AppsError(AppsError.BILLING_REQUIRED, appstoreError.message));
|
||||
if (appstoreError && appstoreError.reason === AppstoreError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, appstoreError.message));
|
||||
|
||||
callback(new AppsError(AppsError.INTERNAL_ERROR, appstoreError));
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
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));
|
||||
|
||||
taskmanager.restartAppTask(newAppId);
|
||||
|
||||
// fetch fresh app object for eventlog
|
||||
get(appId, function (error, result) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
eventlog.add(eventlog.ACTION_APP_CLONE, auditSource, { appId: newAppId, oldAppId: appId, backupId: backupId, location: location, manifest: manifest });
|
||||
|
||||
eventlog.add(eventlog.ACTION_APP_CLONE, auditSource, { appId: newAppId, oldAppId: appId, backupId: backupId, oldApp: app, newApp: result });
|
||||
|
||||
callback(null, { id : newAppId });
|
||||
});
|
||||
callback(null, { id : newAppId });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1038,10 +949,10 @@ function uninstall(appId, auditSource, callback) {
|
||||
|
||||
debug('Will uninstall app with id:%s', appId);
|
||||
|
||||
get(appId, function (error, app) {
|
||||
get(appId, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
appstore.unpurchase(appId, app.appStoreId, function (error) {
|
||||
appstore.unpurchase(appId, result.appStoreId, function (error) {
|
||||
if (error && error.reason === AppstoreError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
|
||||
if (error && error.reason === AppstoreError.BILLING_REQUIRED) return callback(new AppsError(AppsError.BILLING_REQUIRED, error.message));
|
||||
if (error && error.reason === AppstoreError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
|
||||
@@ -1052,7 +963,7 @@ function uninstall(appId, auditSource, callback) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
eventlog.add(eventlog.ACTION_APP_UNINSTALL, auditSource, { appId: appId, app: app });
|
||||
eventlog.add(eventlog.ACTION_APP_UNINSTALL, auditSource, { appId: appId });
|
||||
|
||||
taskmanager.startAppTask(appId, callback);
|
||||
});
|
||||
@@ -1103,7 +1014,7 @@ function checkManifestConstraints(manifest) {
|
||||
}
|
||||
|
||||
if (semver.valid(manifest.minBoxVersion) && semver.gt(manifest.minBoxVersion, config.version())) {
|
||||
return new AppsError(AppsError.BAD_FIELD, 'App version requires a new platform version');
|
||||
return new AppsError(AppsError.BAD_FIELD, 'minBoxVersion exceeds Box version');
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -1117,8 +1028,9 @@ function exec(appId, options, callback) {
|
||||
var cmd = options.cmd || [ '/bin/bash' ];
|
||||
assert(util.isArray(cmd) && cmd.length > 0);
|
||||
|
||||
get(appId, function (error, app) {
|
||||
if (error) return callback(error);
|
||||
appdb.get(appId, function (error, app) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
if (app.installationState !== appdb.ISTATE_INSTALLED || app.runState !== appdb.RSTATE_RUNNING) {
|
||||
return callback(new AppsError(AppsError.BAD_STATE, 'App not installed or running'));
|
||||
@@ -1158,11 +1070,7 @@ function exec(appId, options, callback) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
if (options.rows && options.columns) {
|
||||
// there is a race where resizing too early results in a 404 "no such exec"
|
||||
// https://git.cloudron.io/cloudron/box/issues/549
|
||||
setTimeout(function () {
|
||||
exec.resize({ h: options.rows, w: options.columns }, function (error) { if (error) debug('Error resizing console', error); });
|
||||
}, 2000);
|
||||
exec.resize({ h: options.rows, w: options.columns }, function (error) { if (error) debug('Error resizing console', error); });
|
||||
}
|
||||
|
||||
return callback(null, stream);
|
||||
@@ -1179,12 +1087,11 @@ function autoupdateApps(updateInfo, auditSource, callback) { // updateInfo is {
|
||||
function canAutoupdateApp(app, newManifest) {
|
||||
if ((semver.major(app.manifest.version) !== 0) && (semver.major(app.manifest.version) !== semver.major(newManifest.version))) return new Error('Major version change'); // major changes are blocking
|
||||
|
||||
const newTcpPorts = newManifest.tcpPorts || { };
|
||||
const newUdpPorts = newManifest.udpPorts || { };
|
||||
const portBindings = app.portBindings; // this is never null
|
||||
var newTcpPorts = newManifest.tcpPorts || { };
|
||||
var portBindings = app.portBindings; // this is never null
|
||||
|
||||
for (let portName in portBindings) {
|
||||
if (!(portName in newTcpPorts) && !(portName in newUdpPorts)) return new Error(`${portName} was in use but new update removes it`);
|
||||
for (var env in portBindings) {
|
||||
if (!(env in newTcpPorts)) return new Error(env + ' was in use but new update removes it');
|
||||
}
|
||||
|
||||
// it's fine if one or more (unused) keys got removed
|
||||
@@ -1262,17 +1169,17 @@ function listBackups(page, perPage, appId, callback) {
|
||||
function restoreInstalledApps(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getAll(function (error, apps) {
|
||||
appdb.getAll(function (error, apps) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
async.map(apps, function (app, iteratorDone) {
|
||||
debug('marking %s for restore', app.intrinsicFqdn);
|
||||
|
||||
backups.getByAppIdPaged(1, 1, app.id, function (error, results) {
|
||||
var restoreConfig = !error && results.length ? { backupId: results[0].id, backupFormat: results[0].format } : null;
|
||||
|
||||
debug(`marking ${app.fqdn} for restore using restore config ${JSON.stringify(restoreConfig)}`);
|
||||
|
||||
appdb.setInstallationCommand(app.id, appdb.ISTATE_PENDING_RESTORE, { restoreConfig: restoreConfig, oldConfig: getAppConfig(app) }, function (error) {
|
||||
if (error) debug(`Error marking ${app.fqdn} for restore: ${JSON.stringify(error)}`);
|
||||
appdb.setInstallationCommand(app.id, appdb.ISTATE_PENDING_RESTORE, { restoreConfig: restoreConfig, oldConfig: null }, function (error) {
|
||||
if (error) debug('did not mark %s for restore', app.intrinsicFqdn, error);
|
||||
|
||||
iteratorDone(); // always succeed
|
||||
});
|
||||
@@ -1284,14 +1191,14 @@ function restoreInstalledApps(callback) {
|
||||
function configureInstalledApps(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getAll(function (error, apps) {
|
||||
appdb.getAll(function (error, apps) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
async.map(apps, function (app, iteratorDone) {
|
||||
debug(`marking ${app.fqdn} for reconfigure`);
|
||||
debug('marking %s for reconfigure', app.intrinsicFqdn);
|
||||
|
||||
appdb.setInstallationCommand(app.id, appdb.ISTATE_PENDING_CONFIGURE, { oldConfig: null }, function (error) {
|
||||
if (error) debug(`Error marking ${app.fqdn} for reconfigure: ${JSON.stringify(error)}`);
|
||||
if (error) debug('did not mark %s for reconfigure', app.intrinsicFqdn, error);
|
||||
|
||||
iteratorDone(); // always succeed
|
||||
});
|
||||
@@ -1380,28 +1287,3 @@ function uploadFile(appId, sourceFilePath, destFilePath, callback) {
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function setOwner(appId, ownerId, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
appdb.setOwner(appId, ownerId, function (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));
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
function transferOwnership(oldOwnerId, newOwnerId, callback) {
|
||||
assert.strictEqual(typeof oldOwnerId, 'string');
|
||||
assert.strictEqual(typeof newOwnerId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
appdb.transferOwnership(oldOwnerId, newOwnerId, function (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));
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
+77
-131
@@ -5,7 +5,6 @@ exports = module.exports = {
|
||||
unpurchase: unpurchase,
|
||||
|
||||
getSubscription: getSubscription,
|
||||
isFreePlan: isFreePlan,
|
||||
|
||||
sendAliveStatus: sendAliveStatus,
|
||||
|
||||
@@ -19,18 +18,11 @@ exports = module.exports = {
|
||||
AppstoreError: AppstoreError
|
||||
};
|
||||
|
||||
var appdb = require('./appdb.js'),
|
||||
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');
|
||||
@@ -64,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) {
|
||||
@@ -91,11 +94,6 @@ function getSubscription(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function isFreePlan(subscription) {
|
||||
return !subscription || subscription.plan.id === 'free';
|
||||
}
|
||||
|
||||
// See app.js install it will create a db record first but remove it again if appstore purchase fails
|
||||
function purchase(appId, appstoreId, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof appstoreId, 'string');
|
||||
@@ -137,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));
|
||||
@@ -150,87 +148,62 @@ function unpurchase(appId, appstoreId, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function sendAliveStatus(callback) {
|
||||
function sendAliveStatus(data, callback) {
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
|
||||
var allSettings, allDomains, mailDomains, loginEvents;
|
||||
settings.getAll(function (error, result) {
|
||||
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
|
||||
|
||||
async.series([
|
||||
function (callback) {
|
||||
settings.getAll(function (error, result) {
|
||||
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
|
||||
allSettings = result;
|
||||
callback();
|
||||
});
|
||||
},
|
||||
function (callback) {
|
||||
domains.getAll(function (error, result) {
|
||||
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
|
||||
allDomains = result;
|
||||
callback();
|
||||
});
|
||||
},
|
||||
function (callback) {
|
||||
mail.getDomains(function (error, result) {
|
||||
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
|
||||
mailDomains = result;
|
||||
callback();
|
||||
});
|
||||
},
|
||||
function (callback) {
|
||||
eventlog.getAllPaged([ eventlog.ACTION_USER_LOGIN ], null, 1, 1, function (error, result) {
|
||||
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
|
||||
loginEvents = result;
|
||||
callback();
|
||||
});
|
||||
}
|
||||
], function (error) {
|
||||
if (error) return callback(error);
|
||||
eventlog.getAllPaged(eventlog.ACTION_USER_LOGIN, null, 1, 1, function (error, loginEvents) {
|
||||
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
|
||||
|
||||
var backendSettings = {
|
||||
backupConfig: {
|
||||
provider: allSettings[settings.BACKUP_CONFIG_KEY].provider,
|
||||
hardlinks: !allSettings[settings.BACKUP_CONFIG_KEY].noHardlinks
|
||||
},
|
||||
domainConfig: {
|
||||
count: allDomains.length,
|
||||
domains: Array.from(new Set(allDomains.map(function (d) { return { domain: d.domain, provider: d.provider }; })))
|
||||
},
|
||||
mailConfig: {
|
||||
outboundCount: mailDomains.length,
|
||||
inboundCount: mailDomains.filter(function (d) { return d.enabled; }).length,
|
||||
catchAllCount: mailDomains.filter(function (d) { return d.catchAll.length !== 0; }).length,
|
||||
relayProviders: Array.from(new Set(mailDomains.map(function (d) { return d.relay.provider; })))
|
||||
},
|
||||
appAutoupdatePattern: allSettings[settings.APP_AUTOUPDATE_PATTERN_KEY],
|
||||
boxAutoupdatePattern: allSettings[settings.BOX_AUTOUPDATE_PATTERN_KEY],
|
||||
timeZone: allSettings[settings.TIME_ZONE_KEY],
|
||||
};
|
||||
var backendSettings = {
|
||||
tlsConfig: {
|
||||
provider: result[settings.TLS_CONFIG_KEY].provider
|
||||
},
|
||||
backupConfig: {
|
||||
provider: result[settings.BACKUP_CONFIG_KEY].provider,
|
||||
hardlinks: !result[settings.BACKUP_CONFIG_KEY].noHardlinks
|
||||
},
|
||||
mailConfig: {
|
||||
enabled: result[settings.MAIL_CONFIG_KEY].enabled
|
||||
},
|
||||
mailRelay: {
|
||||
provider: result[settings.MAIL_RELAY_KEY].provider
|
||||
},
|
||||
mailCatchAll: {
|
||||
count: result[settings.CATCH_ALL_ADDRESS_KEY].length
|
||||
},
|
||||
autoupdatePattern: result[settings.AUTOUPDATE_PATTERN_KEY],
|
||||
timeZone: result[settings.TIME_ZONE_KEY],
|
||||
};
|
||||
|
||||
var data = {
|
||||
version: config.version(),
|
||||
adminFqdn: config.adminFqdn(),
|
||||
provider: config.provider(),
|
||||
backendSettings: backendSettings,
|
||||
machine: {
|
||||
cpus: os.cpus(),
|
||||
totalmem: os.totalmem()
|
||||
},
|
||||
events: {
|
||||
lastLogin: loginEvents[0] ? (new Date(loginEvents[0].creationTime).getTime()) : 0
|
||||
}
|
||||
};
|
||||
var data = {
|
||||
domain: config.fqdn(),
|
||||
version: config.version(),
|
||||
adminFqdn: config.adminFqdn(),
|
||||
provider: config.provider(),
|
||||
backendSettings: backendSettings,
|
||||
machine: {
|
||||
cpus: os.cpus(),
|
||||
totalmem: os.totalmem()
|
||||
},
|
||||
events: {
|
||||
lastLogin: loginEvents[0] ? (new Date(loginEvents[0].creationTime).getTime()) : 0
|
||||
}
|
||||
};
|
||||
|
||||
getAppstoreConfig(function (error, appstoreConfig) {
|
||||
if (error) return callback(error);
|
||||
getAppstoreConfig(function (error, appstoreConfig) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/alive';
|
||||
superagent.post(url).send(data).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
|
||||
if (result.statusCode === 404) return callback(new AppstoreError(AppstoreError.NOT_FOUND));
|
||||
if (result.statusCode !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Sending alive status failed. %s %j', result.status, result.body)));
|
||||
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/alive';
|
||||
superagent.post(url).send(data).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
|
||||
if (result.statusCode === 404) return callback(new AppstoreError(AppstoreError.NOT_FOUND));
|
||||
if (result.statusCode !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Sending alive status failed. %s %j', result.status, result.body)));
|
||||
|
||||
callback(null);
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -245,18 +218,12 @@ function getBoxUpdate(callback) {
|
||||
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/boxupdate';
|
||||
|
||||
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.message));
|
||||
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)));
|
||||
if (result.statusCode !== 200) 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('Invalid update version: %s %s', result.statusCode, result.text)));
|
||||
}
|
||||
|
||||
// updateInfo: { version, changelog, upgrade, sourceTarballUrl, sourceTarballSigUrl, boxVersionsUrl, boxVersionsSigUrl }
|
||||
callback(null, updateInfo);
|
||||
// { version, changelog, upgrade, sourceTarballUrl}
|
||||
callback(null, result.body);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -273,21 +240,10 @@ function getAppUpdate(app, callback) {
|
||||
superagent.get(url).query({ accessToken: appstoreConfig.token, boxVersion: config.version(), appId: app.appStoreId, appVersion: app.manifest.version }).timeout(10 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
|
||||
if (result.statusCode === 204) return callback(null); // no update
|
||||
if (result.statusCode !== 200 || !result.body) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
|
||||
|
||||
const updateInfo = result.body;
|
||||
|
||||
// for the appstore, x.y.z is the same as x.y.z-0 but in semver, x.y.z > x.y.z-0
|
||||
const curAppVersion = semver.prerelease(app.manifest.version) ? app.manifest.version : `${app.manifest.version}-0`;
|
||||
|
||||
// do some sanity checks
|
||||
if (!safe.query(updateInfo, 'manifest.version') || semver.gt(curAppVersion, safe.query(updateInfo, 'manifest.version'))) {
|
||||
debug('Skipping malformed update of app %s version: %s. got %j', app.id, curAppVersion, updateInfo);
|
||||
return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Malformed update: %s %s', result.statusCode, result.text)));
|
||||
}
|
||||
if (result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
|
||||
|
||||
// { id, creationDate, manifest }
|
||||
callback(null, updateInfo);
|
||||
callback(null, result.body);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -319,26 +275,16 @@ function sendFeedback(info, callback) {
|
||||
assert.strictEqual(typeof info.description, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
function collectAppInfoIfNeeded(callback) {
|
||||
if (!info.appId) return callback();
|
||||
apps.get(info.appId, callback);
|
||||
}
|
||||
|
||||
getAppstoreConfig(function (error, appstoreConfig) {
|
||||
if (error) return callback(error);
|
||||
|
||||
collectAppInfoIfNeeded(function (error, result) {
|
||||
if (error) console.error('Unable to get app info', error);
|
||||
if (result) info.app = result;
|
||||
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/feedback';
|
||||
|
||||
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/feedback';
|
||||
superagent.post(url).query({ accessToken: appstoreConfig.token }).send(info).timeout(10 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
|
||||
if (result.statusCode !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
|
||||
|
||||
superagent.post(url).query({ accessToken: appstoreConfig.token }).send(info).timeout(10 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
|
||||
if (result.statusCode !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+88
-158
@@ -8,44 +8,51 @@ 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 });
|
||||
|
||||
// remove timestamp from debug() based output
|
||||
require('debug').formatArgs = function formatArgs(args) {
|
||||
args[0] = this.namespace + ' ' + args[0];
|
||||
};
|
||||
|
||||
var addons = require('./addons.js'),
|
||||
appdb = require('./appdb.js'),
|
||||
apps = require('./apps.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'),
|
||||
DomainsError = domains.DomainsError,
|
||||
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'),
|
||||
rimraf = require('rimraf'),
|
||||
safe = require('safetydance'),
|
||||
shell = require('./shell.js'),
|
||||
superagent = require('superagent'),
|
||||
sysinfo = require('./sysinfo.js'),
|
||||
tld = require('tldjs'),
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
|
||||
@@ -65,7 +72,8 @@ function initialize(callback) {
|
||||
function debugApp(app) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
|
||||
debug(app.fqdn + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
|
||||
var prefix = app ? (app.intrinsicFqdn || '(bare)') : '(no app)';
|
||||
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
|
||||
}
|
||||
|
||||
// updates the app object and the database
|
||||
@@ -105,19 +113,23 @@ function reserveHttpPort(app, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function configureReverseProxy(app, callback) {
|
||||
function configureNginx(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
reverseProxy.configureApp(app, { userId: null, username: 'apptask' }, callback);
|
||||
certificates.ensureCertificate(app, function (error, certFilePath, keyFilePath) {
|
||||
if (error) return callback(error);
|
||||
|
||||
nginx.configureApp(app, certFilePath, keyFilePath, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function unconfigureReverseProxy(app, callback) {
|
||||
function unconfigureNginx(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// TODO: maybe revoke the cert
|
||||
reverseProxy.unconfigureApp(app, callback);
|
||||
nginx.unconfigureApp(app, callback);
|
||||
}
|
||||
|
||||
function createContainer(app, callback) {
|
||||
@@ -134,20 +146,6 @@ function createContainer(app, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
// Only delete the main container of the app, not destroy any docker addon created ones
|
||||
function deleteMainContainer(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debugApp(app, 'deleting main app container');
|
||||
|
||||
docker.deleteContainer(app.containerId, function (error) {
|
||||
if (error) return callback(new Error('Error deleting container: ' + error));
|
||||
|
||||
updateApp(app, { containerId: null }, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function deleteContainers(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -273,24 +271,26 @@ function registerSubdomain(app, overwrite, callback) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.retry({ times: 200, interval: 5000 }, function (retryCallback) {
|
||||
debugApp(app, 'Registering subdomain location [%s] overwrite: %s', app.fqdn, overwrite);
|
||||
debugApp(app, 'Registering subdomain location [%s] overwrite: %s', app.intrinsicFqdn, overwrite);
|
||||
|
||||
// get the current record before updating it
|
||||
domains.getDnsRecords(app.location, app.domain, 'A', function (error, values) {
|
||||
domains.getDNSRecords(app.location, app.domain, 'A', function (error, values) {
|
||||
if (error) return retryCallback(error);
|
||||
|
||||
// refuse to update any existing DNS record for custom domains that we did not create
|
||||
if (values.length !== 0 && !overwrite) return retryCallback(null, new Error('DNS Record already exists'));
|
||||
|
||||
domains.upsertDnsRecords(app.location, app.domain, 'A', [ ip ], function (error) {
|
||||
if (error && (error.reason === DomainsError.STILL_BUSY || error.reason === DomainsError.EXTERNAL_ERROR)) return retryCallback(error); // try again
|
||||
domains.upsertDNSRecords(app.location, app.domain, 'A', [ ip ], function (error, changeId) {
|
||||
if (error && (error.reason === DomainError.STILL_BUSY || error.reason === DomainError.EXTERNAL_ERROR)) return retryCallback(error); // try again
|
||||
|
||||
retryCallback(null, error);
|
||||
retryCallback(null, error || changeId);
|
||||
});
|
||||
});
|
||||
}, function (error, result) {
|
||||
if (error || result instanceof Error) return callback(error || result);
|
||||
callback(null);
|
||||
|
||||
// dnsRecordId tracks whether we created this DNS record so that we can unregister later
|
||||
updateApp(app, { dnsRecordId: result }, callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -301,90 +301,31 @@ function unregisterSubdomain(app, location, domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!app.dnsRecordId) {
|
||||
debugApp(app, 'Skip unregister of record not created by cloudron');
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
sysinfo.getPublicIp(function (error, ip) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.retry({ times: 30, interval: 5000 }, function (retryCallback) {
|
||||
debugApp(app, 'Unregistering subdomain: %s', app.fqdn);
|
||||
debugApp(app, 'Unregistering subdomain: %s', app.intrinsicFqdn);
|
||||
|
||||
domains.removeDnsRecords(location, domain, 'A', [ ip ], function (error) {
|
||||
if (error && error.reason === DomainsError.NOT_FOUND) return retryCallback(null, null); // domain can be not found if oldConfig.domain or restoreConfig.domain was removed
|
||||
if (error && (error.reason === DomainsError.STILL_BUSY || error.reason === DomainsError.EXTERNAL_ERROR)) return retryCallback(error); // try again
|
||||
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
|
||||
|
||||
retryCallback(null, error);
|
||||
});
|
||||
}, function (error, result) {
|
||||
if (error || result instanceof Error) return callback(error || result);
|
||||
callback(null);
|
||||
|
||||
updateApp(app, { dnsRecordId: null }, callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function registerAlternateDomains(app, overwrite, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof overwrite, 'boolean');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
sysinfo.getPublicIp(function (error, ip) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.eachSeries(app.alternateDomains, function (domain, callback) {
|
||||
async.retry({ times: 200, interval: 5000 }, function (retryCallback) {
|
||||
debugApp(app, 'Registering alternate subdomain [%s] overwrite: %s', (domain.subdomain ? (domain.subdomain + '.') : '') + domain.domain, overwrite);
|
||||
|
||||
// get the current record before updating it
|
||||
domains.getDnsRecords(domain.subdomain, domain.domain, 'A', function (error, values) {
|
||||
if (error) return retryCallback(error);
|
||||
|
||||
// refuse to update any existing DNS record for custom domains that we did not create
|
||||
if (values.length !== 0 && !overwrite) return retryCallback(null, new Error('DNS Record already exists'));
|
||||
|
||||
domains.upsertDnsRecords(domain.subdomain, domain.domain, 'A', [ ip ], function (error) {
|
||||
if (error && (error.reason === DomainsError.STILL_BUSY || error.reason === DomainsError.EXTERNAL_ERROR)) return retryCallback(error); // try again
|
||||
|
||||
retryCallback(null, error);
|
||||
});
|
||||
});
|
||||
}, function (error, result) {
|
||||
if (error || result instanceof Error) return callback(error || result);
|
||||
callback();
|
||||
});
|
||||
}, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function unregisterAlternateDomains(app, all, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof all, 'boolean');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var obsoleteDomains
|
||||
if (all) obsoleteDomains = app.alternateDomains;
|
||||
else obsoleteDomains = app.oldConfig.alternateDomains.filter(function (o) { return !app.alternateDomains.some(function (n) { return n.subdomain === o.subdomain && n.domain === o.domain; }); });
|
||||
|
||||
if (obsoleteDomains.length === 0) return callback();
|
||||
|
||||
sysinfo.getPublicIp(function (error, ip) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.eachSeries(obsoleteDomains, function (domain, callback) {
|
||||
async.retry({ times: 30, interval: 5000 }, function (retryCallback) {
|
||||
debugApp(app, 'Unregistering subdomain: %s%s', domain.subdomain ? (domain.subdomain + '.') : '', domain.domain);
|
||||
|
||||
domains.removeDnsRecords(domain.subdomain, domain.domain, 'A', [ ip ], function (error) {
|
||||
if (error && error.reason === DomainsError.NOT_FOUND) return retryCallback(null, null);
|
||||
if (error && (error.reason === DomainsError.STILL_BUSY || error.reason === DomainsError.EXTERNAL_ERROR)) return retryCallback(error); // try again
|
||||
|
||||
retryCallback(null, error);
|
||||
});
|
||||
}, function (error, result) {
|
||||
if (error || result instanceof Error) return callback(error || result);
|
||||
callback();
|
||||
});
|
||||
}, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function removeIcon(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -395,16 +336,6 @@ function removeIcon(app, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function cleanupLogs(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
rimraf(path.join(paths.LOG_DIR, app.id), function (error) {
|
||||
if (error) debugApp(app, 'cannot cleanup logs: %s', error);
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function waitForDnsPropagation(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -417,16 +348,25 @@ function waitForDnsPropagation(app, callback) {
|
||||
sysinfo.getPublicIp(function (error, ip) {
|
||||
if (error) return callback(error);
|
||||
|
||||
domains.waitForDnsRecord(app.fqdn, app.domain, ip, { interval: 5000, times: 240 }, function (error) {
|
||||
domains.waitForDNSRecord(app.intrinsicFqdn, app.domain, ip, 'A', { interval: 5000, times: 120 }, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function waitForAltDomainDnsPropagation(app, callback) {
|
||||
if (!app.altDomain) return callback(null);
|
||||
|
||||
// try for 10 minutes before giving up. this allows the user to "reconfigure" the app in the case where
|
||||
// an app has an external domain and cloudron is migrated to custom domain.
|
||||
var isNakedDomain = tld.getDomain(app.altDomain) === app.altDomain;
|
||||
if (isNakedDomain) { // check naked domains with A record since CNAME records don't work there
|
||||
sysinfo.getPublicIp(function (error, ip) {
|
||||
if (error) return callback(error);
|
||||
|
||||
// now wait for alternateDomains, if any
|
||||
async.eachSeries(app.alternateDomains, function (domain, callback) {
|
||||
var fqdn = (domain.subdomain ? (domain.subdomain + '.') : '') + domain.domain;
|
||||
domains.waitForDnsRecord(fqdn, domain.domain, ip, { interval: 5000, times: 240 }, callback);
|
||||
}, callback);
|
||||
domains.waitForDNSRecord(app.altDomain, tld.getDomain(app.altDomain), ip, 'A', { interval: 10000, times: 60 }, callback);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
domains.waitForDNSRecord(app.altDomain, tld.getDomain(app.altDomain), app.intrinsicFqdn + '.', 'CNAME', { interval: 10000, times: 60 }, callback);
|
||||
}
|
||||
}
|
||||
|
||||
// Ordering is based on the following rationale:
|
||||
@@ -453,19 +393,13 @@ 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),
|
||||
deleteMainContainer.bind(null, app),
|
||||
function teardownAddons(next) {
|
||||
// when restoring, app does not require these addons anymore. remove carefully to preserve the db passwords
|
||||
var addonsToRemove = !isRestoring
|
||||
? app.manifest.addons
|
||||
: _.omit(app.oldConfig.manifest.addons, Object.keys(app.manifest.addons));
|
||||
|
||||
addons.teardownAddons(app, addonsToRemove, next);
|
||||
},
|
||||
deleteContainers.bind(null, app),
|
||||
// oldConfig can be null during upgrades
|
||||
addons.teardownAddons.bind(null, app, app.oldConfig ? app.oldConfig.manifest.addons : app.manifest.addons),
|
||||
deleteVolume.bind(null, app, { removeDirectory: false }), // do not remove any symlinked volume
|
||||
|
||||
// for restore case
|
||||
@@ -517,8 +451,11 @@ function install(app, callback) {
|
||||
updateApp.bind(null, app, { installationProgress: '85, Waiting for DNS propagation' }),
|
||||
exports._waitForDnsPropagation.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '95, Configuring reverse proxy' }),
|
||||
configureReverseProxy.bind(null, app),
|
||||
updateApp.bind(null, app, { installationProgress: '90, Waiting for External Domain setup' }),
|
||||
exports._waitForAltDomainDnsPropagation.bind(null, app), // required when restoring and !restoreConfig
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '95, Configure nginx' }),
|
||||
configureNginx.bind(null, app),
|
||||
|
||||
// done!
|
||||
function (callback) {
|
||||
@@ -562,34 +499,30 @@ function configure(app, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// oldConfig can be null during an infra update
|
||||
var locationChanged = app.oldConfig && (app.oldConfig.fqdn !== app.fqdn);
|
||||
var locationChanged = app.oldConfig && (app.oldConfig.intrinsicFqdn !== app.intrinsicFqdn);
|
||||
|
||||
async.series([
|
||||
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
|
||||
unconfigureReverseProxy.bind(null, app),
|
||||
unconfigureNginx.bind(null, app),
|
||||
removeCollectdProfile.bind(null, app),
|
||||
removeLogrotateConfig.bind(null, app),
|
||||
stopApp.bind(null, app),
|
||||
deleteMainContainer.bind(null, app),
|
||||
unregisterAlternateDomains.bind(null, app, false /* all */),
|
||||
deleteContainers.bind(null, app),
|
||||
function (next) {
|
||||
if (!locationChanged) return next();
|
||||
|
||||
unregisterSubdomain(app, app.oldConfig.location, app.oldConfig.domain, next);
|
||||
// the config.fqdn() fallback can be removed after 1.9
|
||||
unregisterSubdomain(app, app.oldConfig.location, app.oldConfig.domain || config.fqdn(), next);
|
||||
},
|
||||
|
||||
|
||||
reserveHttpPort.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '20, Downloading icon' }),
|
||||
downloadIcon.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '30, Registering subdomain' }),
|
||||
updateApp.bind(null, app, { installationProgress: '35, Registering subdomain' }),
|
||||
registerSubdomain.bind(null, app, !locationChanged /* overwrite */), // if location changed, do not overwrite to detect conflicts
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '35, Registering alternate domains'}),
|
||||
registerAlternateDomains.bind(null, app, true /* overwrite */), // figure out when to overwrite
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '40, Downloading image' }),
|
||||
docker.downloadImage.bind(null, app.manifest),
|
||||
|
||||
@@ -614,8 +547,11 @@ function configure(app, callback) {
|
||||
updateApp.bind(null, app, { installationProgress: '80, Waiting for DNS propagation' }),
|
||||
exports._waitForDnsPropagation.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '90, Configuring reverse proxy' }),
|
||||
configureReverseProxy.bind(null, app),
|
||||
updateApp.bind(null, app, { installationProgress: '85, Waiting for External Domain setup' }),
|
||||
exports._waitForAltDomainDnsPropagation.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '90, Configuring Nginx' }),
|
||||
configureNginx.bind(null, app),
|
||||
|
||||
// done!
|
||||
function (callback) {
|
||||
@@ -671,7 +607,7 @@ function update(app, callback) {
|
||||
removeCollectdProfile.bind(null, app),
|
||||
removeLogrotateConfig.bind(null, app),
|
||||
stopApp.bind(null, app),
|
||||
deleteMainContainer.bind(null, app),
|
||||
deleteContainers.bind(null, app),
|
||||
function deleteImageIfChanged(done) {
|
||||
if (app.manifest.dockerImage === app.updateConfig.manifest.dockerImage) return done();
|
||||
|
||||
@@ -683,14 +619,14 @@ function update(app, callback) {
|
||||
|
||||
// free unused ports
|
||||
function (next) {
|
||||
const currentPorts = app.portBindings || {};
|
||||
const newTcpPorts = app.updateConfig.manifest.tcpPorts || {};
|
||||
const newUdpPorts = app.updateConfig.manifest.udpPorts || {};
|
||||
// make sure we always have objects
|
||||
var currentPorts = app.portBindings || {};
|
||||
var newPorts = app.updateConfig.manifest.tcpPorts || {};
|
||||
|
||||
async.each(Object.keys(currentPorts), function (portName, callback) {
|
||||
if (newTcpPorts[portName] || newUdpPorts[portName]) return callback(); // port still in use
|
||||
if (newPorts[portName]) return callback(); // port still in use
|
||||
|
||||
appdb.delPortBinding(currentPorts[portName], apps.PORT_TYPE_TCP, function (error) {
|
||||
appdb.delPortBinding(currentPorts[portName], function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) console.error('Portbinding does not exist in database.');
|
||||
else if (error) return next(error);
|
||||
|
||||
@@ -768,18 +704,14 @@ function uninstall(app, callback) {
|
||||
updateApp.bind(null, app, { installationProgress: '50, Deleting image' }),
|
||||
docker.deleteImage.bind(null, app.manifest),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '60, Unregistering domains' }),
|
||||
unregisterAlternateDomains.bind(null, app, true /* all */),
|
||||
updateApp.bind(null, app, { installationProgress: '60, Unregistering subdomain' }),
|
||||
unregisterSubdomain.bind(null, app, app.location, app.domain),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '70, Cleanup icon' }),
|
||||
updateApp.bind(null, app, { installationProgress: '80, Cleanup icon' }),
|
||||
removeIcon.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '80, Unconfiguring reverse proxy' }),
|
||||
unconfigureReverseProxy.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '90, Cleanup logs' }),
|
||||
cleanupLogs.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)
|
||||
@@ -869,8 +801,6 @@ function startTask(appId, callback) {
|
||||
if (require.main === module) {
|
||||
assert.strictEqual(process.argv.length, 3, 'Pass the appid as argument');
|
||||
|
||||
// add a separator for the log file
|
||||
debug('------------------------------------------------------------');
|
||||
debug('Apptask for %s', process.argv[2]);
|
||||
|
||||
process.on('SIGTERM', function () {
|
||||
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
initialize: initialize,
|
||||
uninitialize: uninitialize,
|
||||
|
||||
accessTokenAuth: accessTokenAuth
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
BasicStrategy = require('passport-http').BasicStrategy,
|
||||
BearerStrategy = require('passport-http-bearer').Strategy,
|
||||
clients = require('./clients'),
|
||||
ClientPasswordStrategy = require('passport-oauth2-client-password').Strategy,
|
||||
ClientsError = clients.ClientsError,
|
||||
DatabaseError = require('./databaseerror'),
|
||||
debug = require('debug')('box:auth'),
|
||||
LocalStrategy = require('passport-local').Strategy,
|
||||
crypto = require('crypto'),
|
||||
passport = require('passport'),
|
||||
tokendb = require('./tokendb'),
|
||||
user = require('./user'),
|
||||
UserError = user.UserError,
|
||||
_ = require('underscore');
|
||||
|
||||
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.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) {
|
||||
if (error && error.reason === UserError.NOT_FOUND) return callback(null, false);
|
||||
if (error && error.reason === UserError.WRONG_PASSWORD) return callback(null, false);
|
||||
if (error) return callback(error);
|
||||
if (!result) return callback(null, false);
|
||||
callback(null, _.pick(result, 'id', 'username', 'email', 'admin'));
|
||||
});
|
||||
} else {
|
||||
user.verifyWithEmail(username, password, function (error, result) {
|
||||
if (error && error.reason === UserError.NOT_FOUND) return callback(null, false);
|
||||
if (error && error.reason === UserError.WRONG_PASSWORD) return callback(null, false);
|
||||
if (error) return callback(error);
|
||||
if (!result) return callback(null, false);
|
||||
callback(null, _.pick(result, 'id', 'username', 'email', 'admin'));
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
passport.use(new BasicStrategy(function (username, password, callback) {
|
||||
if (username.indexOf('cid-') === 0) {
|
||||
debug('BasicStrategy: detected client id %s instead of username:password', username);
|
||||
// username is actually client id here
|
||||
// password is client secret
|
||||
clients.get(username, function (error, client) {
|
||||
if (error && error.reason === ClientsError.NOT_FOUND) return callback(null, false);
|
||||
if (error) return callback(error);
|
||||
if (client.clientSecret != password) return callback(null, false);
|
||||
return callback(null, client);
|
||||
});
|
||||
} else {
|
||||
user.verifyWithUsername(username, password, function (error, result) {
|
||||
if (error && error.reason === UserError.NOT_FOUND) return callback(null, false);
|
||||
if (error && error.reason === UserError.WRONG_PASSWORD) return callback(null, false);
|
||||
if (error) return callback(error);
|
||||
if (!result) return callback(null, false);
|
||||
callback(null, result);
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
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);
|
||||
if (error) { return callback(error); }
|
||||
if (client.clientSecret != clientSecret) { return callback(null, false); }
|
||||
return callback(null, client);
|
||||
});
|
||||
}));
|
||||
|
||||
passport.use(new BearerStrategy(accessTokenAuth));
|
||||
|
||||
callback(null);
|
||||
}
|
||||
|
||||
function uninitialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
callback(null);
|
||||
}
|
||||
|
||||
function accessTokenAuth(accessToken, callback) {
|
||||
assert.strictEqual(typeof accessToken, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
+34
-203
@@ -58,7 +58,6 @@ var addons = require('./addons.js'),
|
||||
safe = require('safetydance'),
|
||||
shell = require('./shell.js'),
|
||||
settings = require('./settings.js'),
|
||||
superagent = require('superagent'),
|
||||
syncer = require('./syncer.js'),
|
||||
tar = require('tar-fs'),
|
||||
util = require('util'),
|
||||
@@ -69,9 +68,10 @@ var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
var BACKUPTASK_CMD = path.join(__dirname, 'backuptask.js');
|
||||
|
||||
function debugApp(app) {
|
||||
assert(typeof app === 'object');
|
||||
assert(!app || typeof app === 'object');
|
||||
|
||||
debug(app.fqdn + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
|
||||
var prefix = app ? app.intrinsicFqdn : '(no app)';
|
||||
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
|
||||
}
|
||||
|
||||
function BackupsError(reason, errorOrMessage) {
|
||||
@@ -124,9 +124,6 @@ function testConfig(backupConfig, callback) {
|
||||
|
||||
if (backupConfig.format !== 'tgz' && backupConfig.format !== 'rsync') return callback(new BackupsError(BackupsError.BAD_FIELD, 'unknown format'));
|
||||
|
||||
// remember to adjust the cron ensureBackup task interval accordingly
|
||||
if (backupConfig.intervalSecs < 6 * 60 * 60) return callback(new BackupsError(BackupsError.BAD_FIELD, 'Interval must be atleast 6 hours'));
|
||||
|
||||
api(backupConfig.provider).testConfig(backupConfig, callback);
|
||||
}
|
||||
|
||||
@@ -161,7 +158,7 @@ function get(backupId, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
backupdb.get(backupId, function (error, result) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new BackupsError(BackupsError.NOT_FOUND));
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new BackupsError(BackupsError.NOT_FOUND, error));
|
||||
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, result);
|
||||
@@ -186,92 +183,6 @@ function log(detail) {
|
||||
progress.setDetail(progress.BACKUP, detail);
|
||||
}
|
||||
|
||||
function encryptFilePath(filePath, key) {
|
||||
assert.strictEqual(typeof filePath, 'string');
|
||||
assert.strictEqual(typeof key, 'string');
|
||||
|
||||
var encryptedParts = filePath.split('/').map(function (part) {
|
||||
const cipher = crypto.createCipher('aes-256-cbc', key);
|
||||
let crypt = cipher.update(part);
|
||||
crypt = Buffer.concat([ crypt, cipher.final() ]);
|
||||
|
||||
return crypt.toString('base64') // ensures path is valid
|
||||
.replace(/\//g, '-') // replace '/' of base64 since it conflicts with path separator
|
||||
.replace(/=/g,''); // strip trailing = padding. this is only needed if we concat base64 strings, which we don't
|
||||
});
|
||||
|
||||
return encryptedParts.join('/');
|
||||
}
|
||||
|
||||
function decryptFilePath(filePath, key) {
|
||||
assert.strictEqual(typeof filePath, 'string');
|
||||
assert.strictEqual(typeof key, 'string');
|
||||
|
||||
let decryptedParts = [];
|
||||
for (let part of filePath.split('/')) {
|
||||
part = part + Array(part.length % 4).join('='); // add back = padding
|
||||
part = part.replace(/-/g, '/'); // replace with '/'
|
||||
|
||||
try {
|
||||
let decrypt = crypto.createDecipher('aes-256-cbc', key);
|
||||
let text = decrypt.update(Buffer.from(part, 'base64'));
|
||||
text = Buffer.concat([ text, decrypt.final() ]);
|
||||
decryptedParts.push(text.toString('utf8'));
|
||||
} catch (error) {
|
||||
debug(`Error decrypting file ${filePath} part ${part}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return decryptedParts.join('/');
|
||||
}
|
||||
|
||||
function createReadStream(sourceFile, key) {
|
||||
assert.strictEqual(typeof sourceFile, 'string');
|
||||
assert(key === null || typeof key === 'string');
|
||||
|
||||
var stream = fs.createReadStream(sourceFile);
|
||||
var ps = progressStream({ time: 10000 }); // display a progress every 10 seconds
|
||||
|
||||
stream.on('error', function (error) {
|
||||
debug('createReadStream: tar stream error.', error);
|
||||
ps.emit('error', new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
|
||||
});
|
||||
|
||||
ps.on('progress', function(progress) {
|
||||
debug('createReadStream: %s@%s (%s)', Math.round(progress.transferred/1024/1024) + 'M', Math.round(progress.speed/1024/1024) + 'Mbps', sourceFile);
|
||||
});
|
||||
|
||||
if (key !== null) {
|
||||
var encrypt = crypto.createCipher('aes-256-cbc', key);
|
||||
encrypt.on('error', function (error) {
|
||||
debug('createReadStream: encrypt stream error.', error);
|
||||
ps.emit('error', new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
|
||||
});
|
||||
return stream.pipe(encrypt).pipe(ps);
|
||||
} else {
|
||||
return stream.pipe(ps);
|
||||
}
|
||||
}
|
||||
|
||||
function createWriteStream(destFile, key) {
|
||||
assert.strictEqual(typeof destFile, 'string');
|
||||
assert(key === null || typeof key === 'string');
|
||||
|
||||
var stream = fs.createWriteStream(destFile);
|
||||
|
||||
if (key !== null) {
|
||||
var decrypt = crypto.createDecipher('aes-256-cbc', key);
|
||||
decrypt.on('error', function (error) {
|
||||
debug('createWriteStream: decrypt stream error.', error);
|
||||
});
|
||||
decrypt.pipe(stream);
|
||||
return decrypt;
|
||||
} else {
|
||||
return stream;
|
||||
}
|
||||
}
|
||||
|
||||
function createTarPackStream(sourceDir, key) {
|
||||
assert.strictEqual(typeof sourceDir, 'string');
|
||||
assert(key === null || typeof key === 'string');
|
||||
@@ -321,47 +232,35 @@ function sync(backupConfig, backupId, dataDir, callback) {
|
||||
assert.strictEqual(typeof dataDir, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
function setBackupProgress(message) {
|
||||
debug('%s', message);
|
||||
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, message);
|
||||
}
|
||||
|
||||
syncer.sync(dataDir, function processTask(task, iteratorCallback) {
|
||||
debug('sync: processing task: %j', task);
|
||||
// the empty task.path is special to signify the directory
|
||||
const destPath = task.path && backupConfig.key ? encryptFilePath(task.path, backupConfig.key) : task.path;
|
||||
const backupFilePath = path.join(getBackupFilePath(backupConfig, backupId, backupConfig.format), destPath);
|
||||
var backupFilePath = path.join(getBackupFilePath(backupConfig, backupId, backupConfig.format), task.path);
|
||||
|
||||
if (task.operation === 'removedir') {
|
||||
setBackupProgress(`Removing directory ${backupFilePath}`);
|
||||
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 ${backupFilePath}`);
|
||||
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} position ${task.position} try ${retryCount}`);
|
||||
var stream = createReadStream(path.join(dataDir, task.path), backupConfig.key || null);
|
||||
stream.on('error', function (error) {
|
||||
setBackupProgress(`read stream error for ${task.path}: ${error.message}`);
|
||||
retryCallback();
|
||||
}); // ignore error if file disappears
|
||||
api(backupConfig.provider).upload(backupConfig, backupFilePath, stream, function (error) {
|
||||
setBackupProgress(error ? `Error uploading ${task.path} try ${retryCount}: ${error.message}` : `Uploaded ${task.path}`);
|
||||
retryCallback(error);
|
||||
});
|
||||
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, `Adding ${task.path}`);
|
||||
var stream = fs.createReadStream(path.join(dataDir, task.path));
|
||||
stream.on('error', function () { return retryCallback(); }); // ignore error if file disappears
|
||||
api(backupConfig.provider).upload(backupConfig, backupFilePath, stream, retryCallback);
|
||||
}
|
||||
}, iteratorCallback);
|
||||
}, backupConfig.syncConcurrency || 10 /* concurrency */, function (error) {
|
||||
}, 10 /* concurrency */, function (error) {
|
||||
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
|
||||
|
||||
callback();
|
||||
@@ -395,6 +294,8 @@ function upload(backupId, format, dataDir, callback) {
|
||||
assert.strictEqual(typeof dataDir, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
callback = once(callback);
|
||||
|
||||
debug('upload: id %s format %s dataDir %s', backupId, format, dataDir);
|
||||
|
||||
settings.getBackupConfig(function (error, backupConfig) {
|
||||
@@ -402,8 +303,6 @@ function upload(backupId, format, dataDir, callback) {
|
||||
|
||||
if (format === 'tgz') {
|
||||
async.retry({ times: 5, interval: 20000 }, function (retryCallback) {
|
||||
retryCallback = once(retryCallback); // protect again upload() erroring much later after tar stream error
|
||||
|
||||
var tarStream = createTarPackStream(dataDir, backupConfig.key || null);
|
||||
tarStream.on('error', retryCallback); // already returns BackupsError
|
||||
|
||||
@@ -458,7 +357,7 @@ function tarExtract(inStream, destination, key, callback) {
|
||||
var decrypt = crypto.createDecipher('aes-256-cbc', key);
|
||||
decrypt.on('error', function (error) {
|
||||
debug('tarExtract: decrypt stream error.', error);
|
||||
callback(new BackupsError(BackupsError.EXTERNAL_ERROR, `Failed to decrypt: ${error.message}`));
|
||||
callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
|
||||
});
|
||||
inStream.pipe(ps).pipe(decrypt).pipe(gunzip).pipe(extract);
|
||||
} else {
|
||||
@@ -492,45 +391,6 @@ function restoreFsMetadata(appDataDir, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function downloadDir(backupConfig, backupFilePath, destDir, callback) {
|
||||
assert.strictEqual(typeof backupConfig, 'object');
|
||||
assert.strictEqual(typeof backupFilePath, 'string');
|
||||
assert.strictEqual(typeof destDir, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug(`downloadDir: ${backupFilePath} to ${destDir}`);
|
||||
|
||||
function downloadFile(entry, callback) {
|
||||
let relativePath = path.relative(backupFilePath, entry.fullPath);
|
||||
if (backupConfig.key) {
|
||||
relativePath = decryptFilePath(relativePath, backupConfig.key);
|
||||
if (!relativePath) return callback(new BackupsError(BackupsError.BAD_STATE, 'Unable to decrypt file'));
|
||||
}
|
||||
const destFilePath = path.join(destDir, relativePath);
|
||||
|
||||
mkdirp(path.dirname(destFilePath), function (error) {
|
||||
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
|
||||
|
||||
api(backupConfig.provider).download(backupConfig, entry.fullPath, function (error, sourceStream) {
|
||||
if (error) return callback(error);
|
||||
|
||||
sourceStream.on('error', callback);
|
||||
|
||||
let destStream = createWriteStream(destFilePath, backupConfig.key || null);
|
||||
destStream.on('error', callback);
|
||||
|
||||
debug(`downloadDir: Copying ${entry.fullPath} to ${destFilePath}`);
|
||||
|
||||
sourceStream.pipe(destStream, { end: true }).on('finish', callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
api(backupConfig.provider).listDir(backupConfig, backupFilePath, 1000, function (entries, done) {
|
||||
async.each(entries, downloadFile, done);
|
||||
}, callback);
|
||||
}
|
||||
|
||||
function download(backupConfig, backupId, format, dataDir, callback) {
|
||||
assert.strictEqual(typeof backupConfig, 'object');
|
||||
assert.strictEqual(typeof backupId, 'string');
|
||||
@@ -549,7 +409,9 @@ function download(backupConfig, backupId, format, dataDir, callback) {
|
||||
tarExtract(sourceStream, dataDir, backupConfig.key || null, callback);
|
||||
});
|
||||
} else {
|
||||
downloadDir(backupConfig, getBackupFilePath(backupConfig, backupId, format), dataDir, function (error) {
|
||||
var events = api(backupConfig.provider).downloadDir(backupConfig, getBackupFilePath(backupConfig, backupId, format), dataDir);
|
||||
events.on('progress', log);
|
||||
events.on('done', function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
restoreFsMetadata(dataDir, callback);
|
||||
@@ -691,34 +553,6 @@ function uploadBoxSnapshot(backupConfig, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function backupDone(apiConfig, backupId, appBackupIds, callback) {
|
||||
assert.strictEqual(typeof apiConfig, 'object');
|
||||
assert.strictEqual(typeof backupId, 'string');
|
||||
assert(Array.isArray(appBackupIds));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (apiConfig.provider !== 'caas') return callback();
|
||||
|
||||
debug('[%s] backupDone: %s apps %j', backupId, backupId, appBackupIds);
|
||||
|
||||
var url = config.apiServerOrigin() + '/api/v1/boxes/' + apiConfig.fqdn + '/backupDone';
|
||||
var data = {
|
||||
boxVersion: config.version(),
|
||||
backupId: backupId,
|
||||
appId: null, // now unused
|
||||
appVersion: null, // now unused
|
||||
appBackupIds: appBackupIds
|
||||
};
|
||||
|
||||
superagent.post(url).send(data).query({ token: apiConfig.token }).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error));
|
||||
if (result.statusCode !== 200) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, result.text));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function rotateBoxBackup(backupConfig, timestamp, appBackupIds, callback) {
|
||||
assert.strictEqual(typeof backupConfig, 'object');
|
||||
assert.strictEqual(typeof timestamp, 'string');
|
||||
@@ -748,7 +582,8 @@ function rotateBoxBackup(backupConfig, timestamp, appBackupIds, callback) {
|
||||
|
||||
log(`Rotated box backup successfully as id ${backupId}`);
|
||||
|
||||
backupDone(backupConfig, backupId, appBackupIds, function (error) {
|
||||
// FIXME this is only needed for caas, hopefully we can remove that in the future
|
||||
api(backupConfig.provider).backupDone(backupConfig, backupId, appBackupIds, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, backupId);
|
||||
@@ -885,7 +720,7 @@ function backupApp(app, callback) {
|
||||
const timestamp = (new Date()).toISOString().replace(/[T.]/g, '-').replace(/[:Z]/g,'');
|
||||
safe.fs.unlinkSync(paths.BACKUP_LOG_FILE); // start fresh log file
|
||||
|
||||
progress.set(progress.BACKUP, 10, 'Backing up ' + app.fqdn);
|
||||
progress.set(progress.BACKUP, 10, 'Backing up ' + (app.altDomain || app.intrinsicFqdn));
|
||||
|
||||
backupAppWithTimestamp(app, timestamp, function (error) {
|
||||
progress.set(progress.BACKUP, 100, error ? error.message : '');
|
||||
@@ -912,12 +747,12 @@ function backupBoxAndApps(auditSource, callback) {
|
||||
var step = 100/(allApps.length+2);
|
||||
|
||||
async.mapSeries(allApps, function iterator(app, iteratorCallback) {
|
||||
progress.set(progress.BACKUP, step * processed, 'Backing up ' + app.fqdn);
|
||||
progress.set(progress.BACKUP, step * processed, 'Backing up ' + (app.altDomain || app.intrinsicFqdn));
|
||||
|
||||
++processed;
|
||||
|
||||
if (!app.enableBackup) {
|
||||
progress.set(progress.BACKUP, step * processed, 'Skipped backup ' + app.fqdn);
|
||||
progress.set(progress.BACKUP, step * processed, 'Skipped backup ' + (app.altDomain || app.intrinsicFqdn));
|
||||
return iteratorCallback(null, null); // nothing to backup
|
||||
}
|
||||
|
||||
@@ -927,7 +762,7 @@ function backupBoxAndApps(auditSource, callback) {
|
||||
return iteratorCallback(error);
|
||||
}
|
||||
|
||||
progress.set(progress.BACKUP, step * processed, 'Backed up ' + app.fqdn);
|
||||
progress.set(progress.BACKUP, step * processed, 'Backed up ' + (app.altDomain || app.intrinsicFqdn));
|
||||
|
||||
iteratorCallback(null, backupId || null); // clear backupId if is in BAD_STATE and never backed up
|
||||
});
|
||||
@@ -984,19 +819,15 @@ function ensureBackup(auditSource, callback) {
|
||||
getByStatePaged(backupdb.BACKUP_STATE_NORMAL, 1, 1, function (error, backups) {
|
||||
if (error) {
|
||||
debug('Unable to list backups', error);
|
||||
return callback(error);
|
||||
return callback(error); // no point trying to backup if appstore is down
|
||||
}
|
||||
|
||||
settings.getBackupConfig(function (error, backupConfig) {
|
||||
if (error) return callback(error);
|
||||
if (backups.length !== 0 && (new Date() - new Date(backups[0].creationTime) < 23 * 60 * 60 * 1000)) { // ~1 day ago
|
||||
debug('Previous backup was %j, no need to backup now', backups[0]);
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
if (backups.length !== 0 && (new Date() - new Date(backups[0].creationTime) < (backupConfig.intervalSecs - 3600) * 1000)) { // adjust 1 hour
|
||||
debug('Previous backup was %j, no need to backup now', backups[0]);
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
backup(auditSource, callback);
|
||||
});
|
||||
backup(auditSource, callback);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -44,9 +44,9 @@ initialize(function (error) {
|
||||
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, '');
|
||||
|
||||
backups.upload(backupId, format, dataDir, function resultHandler(error) {
|
||||
if (error) debug('upload completed with error', error);
|
||||
if (error) debug('completed with error', error);
|
||||
|
||||
debug('upload completed');
|
||||
debug('completed');
|
||||
|
||||
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, error ? error.message : '');
|
||||
|
||||
|
||||
+48
-120
@@ -1,16 +1,11 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
verifySetupToken: verifySetupToken,
|
||||
setupDone: setupDone,
|
||||
|
||||
changePlan: changePlan,
|
||||
upgrade: upgrade,
|
||||
sendHeartbeat: sendHeartbeat,
|
||||
getBoxAndUserDetails: getBoxAndUserDetails,
|
||||
setPtrRecord: setPtrRecord,
|
||||
|
||||
CaasError: CaasError
|
||||
setPtrRecord: setPtrRecord
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
@@ -20,13 +15,13 @@ var assert = require('assert'),
|
||||
locker = require('./locker.js'),
|
||||
path = require('path'),
|
||||
progress = require('./progress.js'),
|
||||
settings = require('./settings.js'),
|
||||
shell = require('./shell.js'),
|
||||
superagent = require('superagent'),
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
|
||||
const RETIRE_CMD = path.join(__dirname, 'scripts/retire.sh');
|
||||
var gBoxAndUserDetails = null; // cached cloudron details like region,size...
|
||||
|
||||
function CaasError(reason, errorOrMessage) {
|
||||
assert.strictEqual(typeof reason, 'string');
|
||||
@@ -48,10 +43,9 @@ function CaasError(reason, errorOrMessage) {
|
||||
}
|
||||
util.inherits(CaasError, Error);
|
||||
CaasError.BAD_FIELD = 'Field error';
|
||||
CaasError.BAD_STATE = 'Bad state';
|
||||
CaasError.INVALID_TOKEN = 'Invalid Token';
|
||||
CaasError.INTERNAL_ERROR = 'Internal Error';
|
||||
CaasError.EXTERNAL_ERROR = 'External Error';
|
||||
CaasError.BAD_STATE = 'Bad state';
|
||||
|
||||
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
|
||||
@@ -62,64 +56,14 @@ function retire(reason, info, callback) {
|
||||
|
||||
var data = {
|
||||
apiServerOrigin: config.apiServerOrigin(),
|
||||
adminFqdn: config.adminFqdn()
|
||||
adminFqdn: config.adminFqdn(),
|
||||
fqdn: config.fqdn()
|
||||
};
|
||||
shell.sudo('retire', [ RETIRE_CMD, reason, JSON.stringify(info), JSON.stringify(data) ], callback);
|
||||
}
|
||||
|
||||
function getCaasConfig(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settings.getCaasConfig(function (error, result) {
|
||||
if (error) return callback(new CaasError(CaasError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, result);
|
||||
});
|
||||
}
|
||||
|
||||
function verifySetupToken(setupToken, callback) {
|
||||
assert.strictEqual(typeof setupToken, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settings.getCaasConfig(function (error, caasConfig) {
|
||||
if (error) return callback(new CaasError(CaasError.INTERNAL_ERROR, error));
|
||||
|
||||
superagent.get(config.apiServerOrigin() + '/api/v1/boxes/' + caasConfig.boxId + '/setup/verify').query({ setupToken: setupToken })
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new CaasError(CaasError.EXTERNAL_ERROR, error));
|
||||
if (result.statusCode === 403) return callback(new CaasError(CaasError.INVALID_TOKEN));
|
||||
if (result.statusCode === 409) return callback(new CaasError(CaasError.BAD_STATE, 'Already setup'));
|
||||
if (result.statusCode !== 200) return callback(new CaasError(CaasError.EXTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setupDone(setupToken, callback) {
|
||||
assert.strictEqual(typeof setupToken, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settings.getCaasConfig(function (error, caasConfig) {
|
||||
if (error) return callback(new CaasError(CaasError.INTERNAL_ERROR, error));
|
||||
|
||||
// Now let the api server know we got activated
|
||||
superagent.post(config.apiServerOrigin() + '/api/v1/boxes/' + caasConfig.boxId + '/setup/done').query({ setupToken: setupToken })
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new CaasError(CaasError.EXTERNAL_ERROR, error));
|
||||
if (result.statusCode === 403) return callback(new CaasError(CaasError.INVALID_TOKEN));
|
||||
if (result.statusCode === 409) return callback(new CaasError(CaasError.BAD_STATE, 'Already setup'));
|
||||
if (result.statusCode !== 201) return callback(new CaasError(CaasError.EXTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
function doMigrate(options, caasConfig, callback) {
|
||||
function doMigrate(options, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof caasConfig, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var error = locker.lock(locker.OP_MIGRATE);
|
||||
@@ -140,8 +84,8 @@ function doMigrate(options, caasConfig, callback) {
|
||||
debug('migrate: domain: %s size %s region %s', options.domain, options.size, options.region);
|
||||
|
||||
superagent
|
||||
.post(config.apiServerOrigin() + '/api/v1/boxes/' + caasConfig.boxId + '/migrate')
|
||||
.query({ token: caasConfig.token })
|
||||
.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/migrate')
|
||||
.query({ token: config.token() })
|
||||
.send(options)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
@@ -165,11 +109,7 @@ function changePlan(options, callback) {
|
||||
|
||||
if (config.isDemo()) return callback(new CaasError(CaasError.BAD_FIELD, 'Not allowed in demo mode'));
|
||||
|
||||
getCaasConfig(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
doMigrate(options, result, callback);
|
||||
});
|
||||
doMigrate(options, callback);
|
||||
}
|
||||
|
||||
// this function expects a lock
|
||||
@@ -186,81 +126,69 @@ function upgrade(boxUpdateInfo, callback) {
|
||||
backups.backupBoxAndApps({ userId: null, username: 'upgrader' }, function (error) {
|
||||
if (error) return upgradeError(error);
|
||||
|
||||
getCaasConfig(function (error, result) {
|
||||
if (error) return upgradeError(error);
|
||||
superagent.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/upgrade')
|
||||
.query({ token: config.token() })
|
||||
.send({ version: boxUpdateInfo.version })
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return upgradeError(new Error('Network error making upgrade request: ' + error));
|
||||
if (result.statusCode !== 202) return upgradeError(new Error(util.format('Server not ready to upgrade. statusCode: %s body: %j', result.status, result.body)));
|
||||
|
||||
superagent.post(config.apiServerOrigin() + '/api/v1/boxes/' + result.boxId + '/upgrade')
|
||||
.query({ token: result.token })
|
||||
.send({ version: boxUpdateInfo.version })
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return upgradeError(new Error('Network error making upgrade request: ' + error));
|
||||
if (result.statusCode !== 202) return upgradeError(new Error(util.format('Server not ready to upgrade. statusCode: %s body: %j', result.status, result.body)));
|
||||
progress.set(progress.UPDATE, 10, 'Updating base system');
|
||||
|
||||
progress.set(progress.UPDATE, 10, 'Updating base system');
|
||||
// no need to unlock since this is the last thing we ever do on this box
|
||||
callback();
|
||||
|
||||
// no need to unlock since this is the last thing we ever do on this box
|
||||
callback();
|
||||
|
||||
retire('upgrade');
|
||||
});
|
||||
});
|
||||
retire('upgrade');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function sendHeartbeat() {
|
||||
assert(config.provider() === 'caas', 'Heartbeat is only sent for managed cloudrons');
|
||||
|
||||
getCaasConfig(function (error, result) {
|
||||
if (error) return debug('Caas config missing', error);
|
||||
|
||||
var url = config.apiServerOrigin() + '/api/v1/boxes/' + result.boxId + '/heartbeat';
|
||||
superagent.post(url).query({ token: result.token, version: config.version() }).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) debug('Network error sending heartbeat.', error);
|
||||
else if (result.statusCode !== 200) debug('Server responded to heartbeat with %s %s', result.statusCode, result.text);
|
||||
else debug('Heartbeat sent to %s', url);
|
||||
});
|
||||
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/heartbeat';
|
||||
superagent.post(url).query({ token: config.token(), version: config.version() }).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) debug('Network error sending heartbeat.', error);
|
||||
else if (result.statusCode !== 200) debug('Server responded to heartbeat with %s %s', result.statusCode, result.text);
|
||||
else debug('Heartbeat sent to %s', url);
|
||||
});
|
||||
}
|
||||
|
||||
function getBoxAndUserDetails(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (gBoxAndUserDetails) return callback(null, gBoxAndUserDetails);
|
||||
|
||||
if (config.provider() !== 'caas') return callback(null, {});
|
||||
|
||||
getCaasConfig(function (error, caasConfig) {
|
||||
if (error) return callback(error);
|
||||
superagent
|
||||
.get(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn())
|
||||
.query({ token: config.token() })
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new CaasError(CaasError.EXTERNAL_ERROR, 'Cannot reach appstore'));
|
||||
if (result.statusCode !== 200) return callback(new CaasError(CaasError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
|
||||
|
||||
superagent
|
||||
.get(config.apiServerOrigin() + '/api/v1/boxes/' + caasConfig.boxId)
|
||||
.query({ token: caasConfig.token })
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new CaasError(CaasError.EXTERNAL_ERROR, 'Cannot reach appstore'));
|
||||
if (result.statusCode !== 200) return callback(new CaasError(CaasError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
|
||||
gBoxAndUserDetails = result.body;
|
||||
|
||||
return callback(null, result.body);
|
||||
});
|
||||
});
|
||||
return callback(null, gBoxAndUserDetails);
|
||||
});
|
||||
}
|
||||
|
||||
function setPtrRecord(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getCaasConfig(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
superagent
|
||||
.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/ptr')
|
||||
.query({ token: config.token() })
|
||||
.send({ domain: domain })
|
||||
.timeout(5 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new CaasError(CaasError.EXTERNAL_ERROR, 'Cannot reach appstore'));
|
||||
if (result.statusCode !== 202) return callback(new CaasError(CaasError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
|
||||
|
||||
superagent
|
||||
.post(config.apiServerOrigin() + '/api/v1/boxes/' + result.boxId + '/ptr')
|
||||
.query({ token: result.token })
|
||||
.send({ domain: domain })
|
||||
.timeout(5 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new CaasError(CaasError.EXTERNAL_ERROR, 'Cannot reach appstore'));
|
||||
if (result.statusCode !== 202) return callback(new CaasError(CaasError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
});
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
+4
-4
@@ -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, '', '');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,447 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
CertificatesError: CertificatesError,
|
||||
|
||||
ensureFallbackCertificate: ensureFallbackCertificate,
|
||||
setFallbackCertificate: setFallbackCertificate,
|
||||
getFallbackCertificate: getFallbackCertificate,
|
||||
|
||||
validateCertificate: validateCertificate,
|
||||
ensureCertificate: ensureCertificate,
|
||||
|
||||
setAdminCertificate: setAdminCertificate,
|
||||
getAdminCertificate: getAdminCertificate,
|
||||
|
||||
renewAll: renewAll,
|
||||
|
||||
initialize: initialize,
|
||||
uninitialize: uninitialize,
|
||||
|
||||
events: null,
|
||||
|
||||
EVENT_CERT_CHANGED: 'cert_changed',
|
||||
|
||||
// exported for testing
|
||||
_getApi: getApi
|
||||
};
|
||||
|
||||
var acme = require('./cert/acme.js'),
|
||||
apps = require('./apps.js'),
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
caas = require('./cert/caas.js'),
|
||||
config = require('./config.js'),
|
||||
constants = require('./constants.js'),
|
||||
debug = require('debug')('box:certificates'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
fallback = require('./cert/fallback.js'),
|
||||
fs = require('fs'),
|
||||
mailer = require('./mailer.js'),
|
||||
nginx = require('./nginx.js'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance'),
|
||||
settings = require('./settings.js'),
|
||||
user = require('./user.js'),
|
||||
util = require('util');
|
||||
|
||||
function CertificatesError(reason, errorOrMessage) {
|
||||
assert.strictEqual(typeof reason, 'string');
|
||||
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
|
||||
|
||||
Error.call(this);
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
|
||||
this.name = this.constructor.name;
|
||||
this.reason = reason;
|
||||
if (typeof errorOrMessage === 'undefined') {
|
||||
this.message = reason;
|
||||
} else if (typeof errorOrMessage === 'string') {
|
||||
this.message = errorOrMessage;
|
||||
} else {
|
||||
this.message = 'Internal error';
|
||||
this.nestedError = errorOrMessage;
|
||||
}
|
||||
}
|
||||
util.inherits(CertificatesError, Error);
|
||||
CertificatesError.INTERNAL_ERROR = 'Internal Error';
|
||||
CertificatesError.INVALID_CERT = 'Invalid certificate';
|
||||
CertificatesError.NOT_FOUND = 'Not Found';
|
||||
|
||||
function initialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
exports.events = new (require('events').EventEmitter)();
|
||||
callback();
|
||||
}
|
||||
|
||||
function uninitialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
exports.events = null;
|
||||
callback();
|
||||
}
|
||||
|
||||
function getApi(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settings.getTlsConfig(function (error, tlsConfig) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (tlsConfig.provider === 'fallback') return callback(null, fallback, {});
|
||||
|
||||
// use acme if we have altDomain or the tlsConfig is not caas
|
||||
var api = (app.altDomain || tlsConfig.provider !== 'caas') ? acme : caas;
|
||||
|
||||
var options = { };
|
||||
if (tlsConfig.provider === 'caas') {
|
||||
options.prod = true; // with altDomain, we will choose acme setting based on this
|
||||
} else { // acme
|
||||
options.prod = tlsConfig.provider.match(/.*-prod/) !== null;
|
||||
}
|
||||
|
||||
// registering user with an email requires A or MX record (https://github.com/letsencrypt/boulder/issues/1197)
|
||||
// we cannot use admin@fqdn because the user might not have set it up.
|
||||
// we simply update the account with the latest email we have each time when getting letsencrypt certs
|
||||
// https://github.com/ietf-wg-acme/acme/issues/30
|
||||
user.getOwner(function (error, owner) {
|
||||
options.email = error ? 'support@cloudron.io' : (owner.alternateEmail || owner.email); // can error if not activated yet
|
||||
|
||||
callback(null, api, options);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function ensureFallbackCertificate(callback) {
|
||||
// ensure a fallback certificate that much of our code requires
|
||||
var certFilePath = path.join(paths.APP_CERTS_DIR, 'host.cert');
|
||||
var keyFilePath = path.join(paths.APP_CERTS_DIR, 'host.key');
|
||||
|
||||
var fallbackCertPath = path.join(paths.NGINX_CERT_DIR, 'host.cert');
|
||||
var fallbackKeyPath = path.join(paths.NGINX_CERT_DIR, 'host.key');
|
||||
|
||||
if (fs.existsSync(fallbackCertPath) && fs.existsSync(fallbackKeyPath)) {
|
||||
debug('ensureFallbackCertificate: pre-existing fallback certs');
|
||||
return callback();
|
||||
}
|
||||
|
||||
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) { // existing custom fallback certs (when restarting, restoring, updating)
|
||||
debug('ensureFallbackCertificate: using fallback certs provided by user');
|
||||
if (!safe.child_process.execSync('cp ' + certFilePath + ' ' + fallbackCertPath)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
|
||||
if (!safe.child_process.execSync('cp ' + keyFilePath + ' ' + fallbackKeyPath)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
|
||||
|
||||
return callback();
|
||||
}
|
||||
|
||||
// generate a self-signed cert. it's in backup dir so that we don't create a new cert across restarts
|
||||
// FIXME: this cert does not cover the naked domain. needs SAN
|
||||
if (config.fqdn()) {
|
||||
debug('ensureFallbackCertificate: generating self-signed certificate');
|
||||
var certCommand = util.format('openssl req -x509 -newkey rsa:2048 -keyout %s -out %s -days 3650 -subj /CN=*.%s -nodes', keyFilePath, certFilePath, config.fqdn());
|
||||
safe.child_process.execSync(certCommand);
|
||||
|
||||
if (!safe.child_process.execSync('cp ' + certFilePath + ' ' + fallbackCertPath)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
|
||||
if (!safe.child_process.execSync('cp ' + keyFilePath + ' ' + fallbackKeyPath)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
|
||||
|
||||
return callback();
|
||||
} else {
|
||||
debug('ensureFallbackCertificate: cannot generate fallback certificate without domain');
|
||||
return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, 'No domain set'));
|
||||
}
|
||||
}
|
||||
|
||||
function isExpiringSync(certFilePath, hours) {
|
||||
assert.strictEqual(typeof certFilePath, 'string');
|
||||
assert.strictEqual(typeof hours, 'number');
|
||||
|
||||
if (!fs.existsSync(certFilePath)) return 2; // not found
|
||||
|
||||
var result = safe.child_process.spawnSync('/usr/bin/openssl', [ 'x509', '-checkend', String(60 * 60 * hours), '-in', certFilePath ]);
|
||||
|
||||
debug('isExpiringSync: %s %s %s', certFilePath, result.stdout.toString('utf8').trim(), result.status);
|
||||
|
||||
return result.status === 1; // 1 - expired 0 - not expired
|
||||
}
|
||||
|
||||
function renewAll(auditSource, callback) {
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('renewAll: Checking certificates for renewal');
|
||||
|
||||
apps.getAll(function (error, allApps) {
|
||||
if (error) return callback(error);
|
||||
|
||||
allApps.push({ intrinsicFqdn: config.adminFqdn() }); // inject fake webadmin app
|
||||
|
||||
var expiringApps = [ ];
|
||||
for (var i = 0; i < allApps.length; i++) {
|
||||
var appDomain = allApps[i].altDomain || allApps[i].intrinsicFqdn;
|
||||
|
||||
var certFilePath = path.join(paths.APP_CERTS_DIR, appDomain + '.user.cert');
|
||||
var keyFilePath = path.join(paths.APP_CERTS_DIR, appDomain + '.user.key');
|
||||
|
||||
if (safe.fs.existsSync(certFilePath) && safe.fs.existsSync(keyFilePath)) {
|
||||
debug('renewAll: existing user key file for %s. skipping', appDomain);
|
||||
continue;
|
||||
}
|
||||
|
||||
// check if we have an auto cert to be renewed
|
||||
certFilePath = path.join(paths.APP_CERTS_DIR, appDomain + '.cert');
|
||||
keyFilePath = path.join(paths.APP_CERTS_DIR, appDomain + '.key');
|
||||
|
||||
if (!safe.fs.existsSync(keyFilePath)) {
|
||||
debug('renewAll: no existing key file for %s. skipping', appDomain);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isExpiringSync(certFilePath, 24 * 30)) { // expired or not found
|
||||
expiringApps.push(allApps[i]);
|
||||
}
|
||||
}
|
||||
|
||||
debug('renewAll: %j needs to be renewed', expiringApps.map(function (app) { return app.altDomain || app.intrinsicFqdn; }));
|
||||
|
||||
async.eachSeries(expiringApps, function iterator(app, iteratorCallback) {
|
||||
var domain = app.altDomain || app.intrinsicFqdn;
|
||||
|
||||
getApi(app, function (error, api, apiOptions) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('renewAll: renewing cert for %s with options %j', domain, apiOptions);
|
||||
|
||||
api.getCertificate(domain, apiOptions, function (error) {
|
||||
var certFilePath = path.join(paths.APP_CERTS_DIR, domain + '.cert');
|
||||
var keyFilePath = path.join(paths.APP_CERTS_DIR, domain + '.key');
|
||||
|
||||
var errorMessage = error ? error.message : '';
|
||||
eventlog.add(eventlog.ACTION_CERTIFICATE_RENEWAL, auditSource, { domain: domain, errorMessage: errorMessage });
|
||||
|
||||
if (error) {
|
||||
debug('renewAll: could not renew cert for %s because %s', domain, error);
|
||||
|
||||
mailer.certificateRenewalError(domain, errorMessage);
|
||||
|
||||
// check if we should fallback if we expire in the coming day
|
||||
if (!isExpiringSync(certFilePath, 24 * 1)) return iteratorCallback();
|
||||
|
||||
debug('renewAll: using fallback certs for %s since it expires soon', domain, error);
|
||||
|
||||
// if no cert was returned use fallback, the fallback provider will not provide any for example
|
||||
var fallbackCertFilePath = path.join(paths.NGINX_CERT_DIR, domain + '.cert');
|
||||
var fallbackKeyFilePath = path.join(paths.NGINX_CERT_DIR, domain + '.key');
|
||||
|
||||
certFilePath = fs.existsSync(fallbackCertFilePath) ? fallbackCertFilePath : 'cert/host.cert';
|
||||
keyFilePath = fs.existsSync(fallbackKeyFilePath) ? fallbackKeyFilePath : 'cert/host.key';
|
||||
} else {
|
||||
debug('renewAll: certificate for %s renewed', domain);
|
||||
}
|
||||
|
||||
// reconfigure and reload nginx. this is required for the case where we got a renewed cert after fallback
|
||||
var configureFunc = app.intrinsicFqdn === config.adminFqdn() ?
|
||||
nginx.configureAdmin.bind(null, certFilePath, keyFilePath, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn())
|
||||
: nginx.configureApp.bind(null, app, certFilePath, keyFilePath);
|
||||
|
||||
configureFunc(function (ignoredError) {
|
||||
if (ignoredError) debug('fallbackExpiredCertificates: error reconfiguring app', ignoredError);
|
||||
|
||||
exports.events.emit(exports.EVENT_CERT_CHANGED, domain);
|
||||
|
||||
iteratorCallback(); // move to next app
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// note: https://tools.ietf.org/html/rfc4346#section-7.4.2 (certificate_list) requires that the
|
||||
// servers certificate appears first (and not the intermediate cert)
|
||||
function validateCertificate(cert, key, fqdn) {
|
||||
assert(cert === null || typeof cert === 'string');
|
||||
assert(key === null || typeof key === 'string');
|
||||
assert.strictEqual(typeof fqdn, 'string');
|
||||
|
||||
function matchesDomain(domain) {
|
||||
if (typeof domain !== 'string') return false;
|
||||
if (domain === fqdn) return true;
|
||||
if (domain.indexOf('*') === 0 && domain.slice(2) === fqdn.slice(fqdn.indexOf('.') + 1)) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (cert === null && key === null) return null;
|
||||
if (!cert && key) return new Error('missing cert');
|
||||
if (cert && !key) return new Error('missing key');
|
||||
|
||||
var result = safe.child_process.execSync('openssl x509 -noout -checkhost "' + fqdn + '"', { encoding: 'utf8', input: cert });
|
||||
if (!result) return new Error('Invalid certificate. Unable to get certificate subject.');
|
||||
|
||||
// if no match, check alt names
|
||||
if (result.indexOf('does match certificate') === -1) {
|
||||
// https://github.com/drwetter/testssl.sh/pull/383
|
||||
var cmd = 'openssl x509 -noout -text | grep -A3 "Subject Alternative Name" | \
|
||||
grep "DNS:" | \
|
||||
sed -e "s/DNS://g" -e "s/ //g" -e "s/,/ /g" -e "s/othername:<unsupported>//g"';
|
||||
result = safe.child_process.execSync(cmd, { encoding: 'utf8', input: cert });
|
||||
var altNames = result ? [ ] : result.trim().split(' '); // might fail if cert has no SAN
|
||||
debug('validateCertificate: detected altNames as %j', altNames);
|
||||
|
||||
// check altNames
|
||||
if (!altNames.some(matchesDomain)) return new Error(util.format('Certificate is not valid for this domain. Expecting %s in %j', fqdn, altNames));
|
||||
}
|
||||
|
||||
// http://httpd.apache.org/docs/2.0/ssl/ssl_faq.html#verify
|
||||
var certModulus = safe.child_process.execSync('openssl x509 -noout -modulus', { encoding: 'utf8', input: cert });
|
||||
var keyModulus = safe.child_process.execSync('openssl rsa -noout -modulus', { encoding: 'utf8', input: key });
|
||||
if (certModulus !== keyModulus) return new Error('Key does not match the certificate.');
|
||||
|
||||
// check expiration
|
||||
result = safe.child_process.execSync('openssl x509 -checkend 0', { encoding: 'utf8', input: cert });
|
||||
if (!result) return new Error('Certificate is expired.');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function setFallbackCertificate(cert, key, fqdn, callback) {
|
||||
assert.strictEqual(typeof cert, 'string');
|
||||
assert.strictEqual(typeof key, 'string');
|
||||
assert.strictEqual(typeof fqdn, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var error = validateCertificate(cert, key, '*.' + fqdn);
|
||||
if (error) return callback(new CertificatesError(CertificatesError.INVALID_CERT, error.message));
|
||||
|
||||
// backup the cert
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, fqdn + '.cert'), cert)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, fqdn + '.key'), key)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
|
||||
|
||||
// copy over fallback cert
|
||||
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, fqdn + '.cert'), cert)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
|
||||
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, fqdn + '.key'), key)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
|
||||
|
||||
exports.events.emit(exports.EVENT_CERT_CHANGED, '*.' + fqdn);
|
||||
|
||||
nginx.reload(function (error) {
|
||||
if (error) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, error));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function getFallbackCertificate(fqdn, callback) {
|
||||
assert.strictEqual(typeof fqdn, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var cert = safe.fs.readFileSync(path.join(paths.NGINX_CERT_DIR, fqdn + '.cert'), 'utf-8');
|
||||
var key = safe.fs.readFileSync(path.join(paths.NGINX_CERT_DIR, fqdn + '.key'), 'utf-8');
|
||||
|
||||
if (!cert || !key) return callback(new CertificatesError(CertificatesError.NOT_FOUND));
|
||||
|
||||
callback(null, { cert: cert, key: key });
|
||||
}
|
||||
|
||||
function setAdminCertificate(cert, key, callback) {
|
||||
assert.strictEqual(typeof cert, 'string');
|
||||
assert.strictEqual(typeof key, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var vhost = config.adminFqdn();
|
||||
var certFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.user.cert');
|
||||
var keyFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.user.key');
|
||||
|
||||
var error = validateCertificate(cert, key, vhost);
|
||||
if (error) return callback(new CertificatesError(CertificatesError.INVALID_CERT, error.message));
|
||||
|
||||
// backup the cert
|
||||
if (!safe.fs.writeFileSync(certFilePath, cert)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
|
||||
if (!safe.fs.writeFileSync(keyFilePath, key)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
|
||||
|
||||
exports.events.emit(exports.EVENT_CERT_CHANGED, vhost);
|
||||
|
||||
nginx.configureAdmin(certFilePath, keyFilePath, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn(), callback);
|
||||
}
|
||||
|
||||
function getAdminCertificatePath(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var vhost = config.adminFqdn();
|
||||
var certFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.user.cert');
|
||||
var keyFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.user.key');
|
||||
|
||||
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, certFilePath, keyFilePath);
|
||||
|
||||
certFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.cert');
|
||||
keyFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.key');
|
||||
|
||||
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, certFilePath, keyFilePath);
|
||||
|
||||
// any user fallback cert is always copied over to nginx cert dir
|
||||
callback(null, path.join(paths.NGINX_CERT_DIR, 'host.cert'), path.join(paths.NGINX_CERT_DIR, 'host.key'));
|
||||
}
|
||||
|
||||
function getAdminCertificate(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getAdminCertificatePath(function (error, certFilePath, keyFilePath) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var cert = safe.fs.readFileSync(certFilePath);
|
||||
if (!cert) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error));
|
||||
|
||||
var key = safe.fs.readFileSync(keyFilePath);
|
||||
if (!cert) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error));
|
||||
|
||||
return callback(null, cert, key);
|
||||
});
|
||||
}
|
||||
|
||||
function ensureCertificate(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var domain = app.altDomain || app.intrinsicFqdn;
|
||||
|
||||
var certFilePath = path.join(paths.APP_CERTS_DIR, domain + '.user.cert');
|
||||
var keyFilePath = path.join(paths.APP_CERTS_DIR, domain + '.user.key');
|
||||
|
||||
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) {
|
||||
debug('ensureCertificate: %s. user certificate already exists at %s', domain, keyFilePath);
|
||||
return callback(null, certFilePath, keyFilePath);
|
||||
}
|
||||
|
||||
certFilePath = path.join(paths.APP_CERTS_DIR, domain + '.cert');
|
||||
keyFilePath = path.join(paths.APP_CERTS_DIR, domain + '.key');
|
||||
|
||||
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) {
|
||||
debug('ensureCertificate: %s. certificate already exists at %s', domain, keyFilePath);
|
||||
|
||||
if (!isExpiringSync(certFilePath, 24 * 1)) return callback(null, certFilePath, keyFilePath);
|
||||
debug('ensureCertificate: %s cert require renewal', domain);
|
||||
} else {
|
||||
debug('ensureCertificate: %s cert does not exist', domain);
|
||||
}
|
||||
|
||||
getApi(app, function (error, api, apiOptions) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('ensureCertificate: getting certificate for %s with options %j', domain, apiOptions);
|
||||
|
||||
api.getCertificate(domain, apiOptions, function (error, certFilePath, keyFilePath) {
|
||||
if (error) debug('ensureCertificate: could not get certificate. using fallback certs', error);
|
||||
|
||||
// if no cert was returned use fallback, the fallback provider will not provide any for example
|
||||
if (!certFilePath || !keyFilePath) {
|
||||
var fallbackCertFilePath = path.join(paths.NGINX_CERT_DIR, app.domain + '.cert');
|
||||
var fallbackKeyFilePath = path.join(paths.NGINX_CERT_DIR, app.domain + '.key');
|
||||
|
||||
certFilePath = fs.existsSync(fallbackCertFilePath) ? fallbackCertFilePath : 'cert/host.cert';
|
||||
keyFilePath = fs.existsSync(fallbackKeyFilePath) ? fallbackKeyFilePath : 'cert/host.key';
|
||||
}
|
||||
|
||||
callback(null, certFilePath, keyFilePath);
|
||||
});
|
||||
});
|
||||
}
|
||||
+3
-3
@@ -182,8 +182,8 @@ function clear(callback) {
|
||||
|
||||
function addDefaultClients(callback) {
|
||||
async.series([
|
||||
add.bind(null, 'cid-webadmin', 'Settings', 'built-in', 'secret-webadmin', 'https://admin-localhost', '*'),
|
||||
add.bind(null, 'cid-sdk', 'SDK', 'built-in', 'secret-sdk', 'https://admin-localhost', '*'),
|
||||
add.bind(null, 'cid-cli', 'Cloudron Tool', 'built-in', 'secret-cli', 'https://admin-localhost', '*')
|
||||
add.bind(null, 'cid-webadmin', 'Settings', 'built-in', 'secret-webadmin', 'https://admin-localhost', 'cloudron,profile,users,apps,settings'),
|
||||
add.bind(null, 'cid-sdk', 'SDK', 'built-in', 'secret-sdk', 'https://admin-localhost', '*,roleSdk'),
|
||||
add.bind(null, 'cid-cli', 'Cloudron Tool', 'built-in', 'secret-cli', 'https://admin-localhost', '*,roleSdk')
|
||||
], callback);
|
||||
}
|
||||
|
||||
+62
-70
@@ -8,16 +8,26 @@ exports = module.exports = {
|
||||
del: del,
|
||||
getAll: getAll,
|
||||
getByAppIdAndType: getByAppIdAndType,
|
||||
getTokensByUserId: getTokensByUserId,
|
||||
delTokensByUserId: delTokensByUserId,
|
||||
getClientTokensByUserId: getClientTokensByUserId,
|
||||
delClientTokensByUserId: delClientTokensByUserId,
|
||||
delByAppIdAndType: delByAppIdAndType,
|
||||
addTokenByUserId: addTokenByUserId,
|
||||
addClientTokenByUserId: addClientTokenByUserId,
|
||||
delToken: delToken,
|
||||
|
||||
issueDeveloperToken: issueDeveloperToken,
|
||||
|
||||
addDefaultClients: addDefaultClients,
|
||||
|
||||
// keep this in sync with start.sh ADMIN_SCOPES that generates the cid-webadmin
|
||||
SCOPE_APPS: 'apps',
|
||||
SCOPE_DEVELOPER: 'developer', // obsolete
|
||||
SCOPE_PROFILE: 'profile',
|
||||
SCOPE_CLOUDRON: 'cloudron',
|
||||
SCOPE_SETTINGS: 'settings',
|
||||
SCOPE_USERS: 'users',
|
||||
|
||||
// roles are handled just like the above scopes, they are parallel to scopes
|
||||
// scopes enclose API groups, roles specify the usage role
|
||||
SCOPE_ROLE_SDK: 'roleSdk',
|
||||
|
||||
// client type enums
|
||||
TYPE_EXTERNAL: 'external',
|
||||
TYPE_BUILT_IN: 'built-in',
|
||||
@@ -29,15 +39,11 @@ var apps = require('./apps.js'),
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
clientdb = require('./clientdb.js'),
|
||||
constants = require('./constants.js'),
|
||||
config = require('./config.js'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
debug = require('debug')('box:clients'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
hat = require('./hat.js'),
|
||||
accesscontrol = require('./accesscontrol.js'),
|
||||
hat = require('hat'),
|
||||
tokendb = require('./tokendb.js'),
|
||||
users = require('./users.js'),
|
||||
UsersError = users.UsersError,
|
||||
util = require('util'),
|
||||
uuid = require('uuid');
|
||||
|
||||
@@ -68,7 +74,7 @@ ClientsError.NOT_FOUND = 'Not found';
|
||||
ClientsError.INTERNAL_ERROR = 'Internal Error';
|
||||
ClientsError.NOT_ALLOWED = 'Not allowed to remove this client';
|
||||
|
||||
function validateClientName(name) {
|
||||
function validateName(name) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
|
||||
if (name.length < 1) return new ClientsError(ClientsError.BAD_FIELD, 'Name must be atleast 1 character');
|
||||
@@ -79,10 +85,24 @@ function validateClientName(name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateTokenName(name) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
function validateScope(scope) {
|
||||
assert.strictEqual(typeof scope, 'string');
|
||||
|
||||
if (name.length > 64) return new ClientsError(ClientsError.BAD_FIELD, 'Name too long');
|
||||
var VALID_SCOPES = [
|
||||
exports.SCOPE_APPS,
|
||||
exports.SCOPE_DEVELOPER,
|
||||
exports.SCOPE_PROFILE,
|
||||
exports.SCOPE_CLOUDRON,
|
||||
exports.SCOPE_SETTINGS,
|
||||
exports.SCOPE_USERS,
|
||||
'*', // includes all scopes, but not roles
|
||||
exports.SCOPE_ROLE_SDK
|
||||
];
|
||||
|
||||
if (scope === '') return new ClientsError(ClientsError.INVALID_SCOPE, 'Empty scope not allowed');
|
||||
|
||||
var allValid = scope.split(',').every(function (s) { return VALID_SCOPES.indexOf(s) !== -1; });
|
||||
if (!allValid) return new ClientsError(ClientsError.INVALID_SCOPE, 'Invalid scope. Available scopes are ' + VALID_SCOPES.join(', '));
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -94,10 +114,14 @@ function add(appId, type, redirectURI, scope, callback) {
|
||||
assert.strictEqual(typeof scope, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var error = accesscontrol.validateScopeString(scope);
|
||||
if (error) return callback(new ClientsError(ClientsError.INVALID_SCOPE, error.message));
|
||||
// allow whitespace
|
||||
scope = scope.split(',').map(function (s) { return s.trim(); }).join(',');
|
||||
|
||||
error = validateClientName(appId);
|
||||
var error = validateScope(scope);
|
||||
if (error) return callback(error);
|
||||
|
||||
// appId is also client name
|
||||
error = validateName(appId);
|
||||
if (error) return callback(error);
|
||||
|
||||
var id = 'cid-' + uuid.v4();
|
||||
@@ -168,7 +192,7 @@ function getAll(callback) {
|
||||
if (record.type === exports.TYPE_PROXY) record.name = result.manifest.title + ' Website Proxy';
|
||||
if (record.type === exports.TYPE_OAUTH) record.name = result.manifest.title + ' OAuth';
|
||||
|
||||
record.domain = result.fqdn;
|
||||
record.domain = result.altDomain || result.intrinsicFqdn;
|
||||
|
||||
tmp.push(record);
|
||||
|
||||
@@ -193,7 +217,7 @@ function getByAppIdAndType(appId, type, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getTokensByUserId(clientId, userId, callback) {
|
||||
function getClientTokensByUserId(clientId, userId, callback) {
|
||||
assert.strictEqual(typeof clientId, 'string');
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -212,7 +236,7 @@ function getTokensByUserId(clientId, userId, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function delTokensByUserId(clientId, userId, callback) {
|
||||
function delClientTokensByUserId(clientId, userId, callback) {
|
||||
assert.strictEqual(typeof clientId, 'string');
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -252,65 +276,31 @@ function delByAppIdAndType(appId, type, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function addTokenByUserId(clientId, userId, expiresAt, options, callback) {
|
||||
function addClientTokenByUserId(clientId, userId, expiresAt, callback) {
|
||||
assert.strictEqual(typeof clientId, 'string');
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert.strictEqual(typeof expiresAt, 'number');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const name = options.name || '';
|
||||
let error = validateTokenName(name);
|
||||
if (error) return callback(error);
|
||||
|
||||
get(clientId, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
users.get(userId, function (error, user) {
|
||||
if (error && error.reason === UsersError.NOT_FOUND) return callback(new ClientsError(ClientsError.NOT_FOUND, 'No such user'));
|
||||
var token = tokendb.generateToken();
|
||||
|
||||
tokendb.add(token, userId, result.id, expiresAt, result.scope, function (error) {
|
||||
if (error) return callback(new ClientsError(ClientsError.INTERNAL_ERROR, error));
|
||||
|
||||
accesscontrol.scopesForUser(user, function (error, userScopes) {
|
||||
if (error) return callback(new ClientsError(ClientsError.INTERNAL_ERROR, error));
|
||||
|
||||
var scope = accesscontrol.canonicalScopeString(result.scope);
|
||||
var authorizedScopes = accesscontrol.intersectScopes(userScopes, scope.split(','));
|
||||
|
||||
var token = tokendb.generateToken();
|
||||
|
||||
tokendb.add(token, userId, result.id, expiresAt, authorizedScopes.join(','), name, function (error) {
|
||||
if (error) return callback(new ClientsError(ClientsError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, {
|
||||
accessToken: token,
|
||||
tokenScopes: authorizedScopes,
|
||||
identifier: userId,
|
||||
clientId: result.id,
|
||||
expires: expiresAt
|
||||
});
|
||||
});
|
||||
callback(null, {
|
||||
accessToken: token,
|
||||
identifier: userId,
|
||||
clientId: result.id,
|
||||
scope: result.id,
|
||||
expires: expiresAt
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// this issues a cid-cli token that does not require a password in various routes
|
||||
function issueDeveloperToken(userObject, auditSource, callback) {
|
||||
assert.strictEqual(typeof userObject, 'object');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const expiresAt = Date.now() + constants.DEFAULT_TOKEN_EXPIRATION;
|
||||
|
||||
addTokenByUserId('cid-cli', userObject.id, expiresAt, {}, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource, { userId: userObject.id, user: users.removePrivateFields(userObject) });
|
||||
|
||||
callback(null, result);
|
||||
});
|
||||
}
|
||||
|
||||
function delToken(clientId, tokenId, callback) {
|
||||
assert.strictEqual(typeof clientId, 'string');
|
||||
assert.strictEqual(typeof tokenId, 'string');
|
||||
@@ -328,17 +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';
|
||||
|
||||
// id, appId, type, clientSecret, redirectURI, scope
|
||||
async.series([
|
||||
clientdb.upsert.bind(null, 'cid-webadmin', 'Settings', 'built-in', 'secret-webadmin', origin, '*'),
|
||||
clientdb.upsert.bind(null, 'cid-sdk', 'SDK', 'built-in', 'secret-sdk', origin, '*'),
|
||||
clientdb.upsert.bind(null, 'cid-cli', 'Cloudron Tool', 'built-in', 'secret-cli', origin, '*')
|
||||
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);
|
||||
}
|
||||
|
||||
+681
-73
@@ -5,42 +5,77 @@ exports = module.exports = {
|
||||
|
||||
initialize: initialize,
|
||||
uninitialize: uninitialize,
|
||||
activate: activate,
|
||||
getConfig: getConfig,
|
||||
getStatus: getStatus,
|
||||
getDisks: getDisks,
|
||||
dnsSetup: dnsSetup,
|
||||
getLogs: getLogs,
|
||||
|
||||
updateToLatest: updateToLatest,
|
||||
restore: restore,
|
||||
reboot: reboot,
|
||||
|
||||
onActivated: onActivated,
|
||||
checkDiskSpace: checkDiskSpace,
|
||||
|
||||
checkDiskSpace: checkDiskSpace
|
||||
readDkimPublicKeySync: readDkimPublicKeySync,
|
||||
refreshDNS: refreshDNS,
|
||||
configureWebadmin: configureWebadmin
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
var appdb = require('./appdb.js'),
|
||||
apps = require('./apps.js'),
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
backups = require('./backups.js'),
|
||||
BackupsError = require('./backups.js').BackupsError,
|
||||
caas = require('./caas.js'),
|
||||
certificates = require('./certificates.js'),
|
||||
child_process = require('child_process'),
|
||||
clients = require('./clients.js'),
|
||||
config = require('./config.js'),
|
||||
constants = require('./constants.js'),
|
||||
cron = require('./cron.js'),
|
||||
debug = require('debug')('box:cloudron'),
|
||||
df = require('@sindresorhus/df'),
|
||||
domains = require('./domains.js'),
|
||||
DomainError = domains.DomainError,
|
||||
eventlog = require('./eventlog.js'),
|
||||
fs = require('fs'),
|
||||
locker = require('./locker.js'),
|
||||
mailer = require('./mailer.js'),
|
||||
nginx = require('./nginx.js'),
|
||||
os = require('os'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
platform = require('./platform.js'),
|
||||
progress = require('./progress.js'),
|
||||
reverseProxy = require('./reverseproxy.js'),
|
||||
safe = require('safetydance'),
|
||||
semver = require('semver'),
|
||||
settings = require('./settings.js'),
|
||||
settingsdb = require('./settingsdb.js'),
|
||||
SettingsError = settings.SettingsError,
|
||||
shell = require('./shell.js'),
|
||||
spawn = require('child_process').spawn,
|
||||
split = require('split'),
|
||||
users = require('./users.js'),
|
||||
util = require('util');
|
||||
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');
|
||||
var REBOOT_CMD = path.join(__dirname, 'scripts/reboot.sh'),
|
||||
UPDATE_CMD = path.join(__dirname, 'scripts/update.sh'),
|
||||
RESTART_CMD = path.join(__dirname, 'scripts/restart.sh');
|
||||
|
||||
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
|
||||
var gWebadminStatus = { dns: false, tls: false, configuring: false, restoring: false };
|
||||
|
||||
function CloudronError(reason, errorOrMessage) {
|
||||
assert.strictEqual(typeof reason, 'string');
|
||||
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
|
||||
@@ -63,6 +98,8 @@ util.inherits(CloudronError, Error);
|
||||
CloudronError.BAD_FIELD = 'Field error';
|
||||
CloudronError.INTERNAL_ERROR = 'Internal Error';
|
||||
CloudronError.EXTERNAL_ERROR = 'External Error';
|
||||
CloudronError.ALREADY_PROVISIONED = 'Already Provisioned';
|
||||
CloudronError.ALREADY_SETUP = 'Already Setup';
|
||||
CloudronError.BAD_STATE = 'Bad state';
|
||||
CloudronError.ALREADY_UPTODATE = 'No Update Available';
|
||||
CloudronError.NOT_FOUND = 'Not found';
|
||||
@@ -71,12 +108,21 @@ CloudronError.SELF_UPGRADE_NOT_SUPPORTED = 'Self upgrade not supported';
|
||||
function initialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
gWebadminStatus = { dns: false, tls: false, configuring: false, restoring: false };
|
||||
|
||||
async.series([
|
||||
certificates.initialize,
|
||||
settings.initialize,
|
||||
reverseProxy.configureDefaultServer,
|
||||
cron.initialize, // required for caas heartbeat before activation
|
||||
configureDefaultServer,
|
||||
onDomainConfigured,
|
||||
onActivated
|
||||
], callback);
|
||||
], function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
configureWebadmin(NOOP_CALLBACK); // for restore() and caas initial setup. do not block
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
function uninitialize(callback) {
|
||||
@@ -85,17 +131,31 @@ function uninitialize(callback) {
|
||||
async.series([
|
||||
cron.uninitialize,
|
||||
platform.stop,
|
||||
certificates.uninitialize,
|
||||
settings.uninitialize
|
||||
], callback);
|
||||
}
|
||||
|
||||
function onDomainConfigured(callback) {
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
|
||||
if (!config.fqdn()) return callback();
|
||||
|
||||
async.series([
|
||||
clients.addDefaultClients,
|
||||
certificates.ensureFallbackCertificate,
|
||||
ensureDkimKey,
|
||||
cron.initialize // required for caas heartbeat before activation
|
||||
], callback);
|
||||
}
|
||||
|
||||
function onActivated(callback) {
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
|
||||
// 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)
|
||||
users.count(function (error, count) {
|
||||
user.count(function (error, count) {
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
if (!count) return callback(); // not activated
|
||||
|
||||
@@ -103,6 +163,238 @@ function onActivated(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function autoprovision(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const confJson = safe.fs.readFileSync(paths.AUTO_PROVISION_FILE, 'utf8');
|
||||
if (!confJson) return callback();
|
||||
|
||||
const conf = safe.JSON.parse(confJson);
|
||||
if (!conf) return callback();
|
||||
|
||||
async.eachSeries(Object.keys(conf), function (key, iteratorDone) {
|
||||
var name;
|
||||
switch (key) {
|
||||
case 'dnsConfig': name = 'dns_config'; break;
|
||||
case 'tlsConfig': name = 'tls_config'; break;
|
||||
case 'backupConfig': name = 'backup_config'; break;
|
||||
case 'tlsCert':
|
||||
debug(`autoprovision: ${key}`);
|
||||
return fs.writeFile(path.join(paths.NGINX_CERT_DIR, 'host.cert'), conf[key], iteratorDone);
|
||||
case 'tlsKey':
|
||||
debug(`autoprovision: ${key}`);
|
||||
return fs.writeFile(path.join(paths.NGINX_CERT_DIR, 'host.key'), conf[key], iteratorDone);
|
||||
default:
|
||||
debug(`autoprovision: ${key} ignored`);
|
||||
return iteratorDone();
|
||||
}
|
||||
|
||||
debug(`autoprovision: ${name}`);
|
||||
settingsdb.set(name, JSON.stringify(conf[key]), iteratorDone);
|
||||
}, callback);
|
||||
}
|
||||
|
||||
function dnsSetup(adminFqdn, domain, zoneName, provider, dnsConfig, callback) {
|
||||
assert.strictEqual(typeof adminFqdn, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof provider, 'string');
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (config.fqdn()) return callback(new CloudronError(CloudronError.ALREADY_SETUP));
|
||||
|
||||
if (!zoneName) zoneName = tld.getDomain(domain) || domain;
|
||||
|
||||
debug('dnsSetup: Setting up Cloudron with domain %s and zone %s', domain, zoneName);
|
||||
|
||||
function done(error) {
|
||||
if (error && error.reason === DomainError.BAD_FIELD) return callback(new CloudronError(CloudronError.BAD_FIELD, error.message));
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
|
||||
autoprovision(function (error) {
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
|
||||
config.setFqdn(domain); // set fqdn only after dns config is valid, otherwise cannot re-setup if we failed
|
||||
config.setAdminFqdn(adminFqdn);
|
||||
config.setAdminLocation('my');
|
||||
config.setZoneName(zoneName);
|
||||
|
||||
callback();
|
||||
|
||||
async.series([ // do not block
|
||||
onDomainConfigured,
|
||||
configureWebadmin
|
||||
], NOOP_CALLBACK);
|
||||
});
|
||||
}
|
||||
|
||||
domains.get(domain, function (error, result) {
|
||||
if (error && error.reason !== DomainError.NOT_FOUND) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
|
||||
|
||||
if (!result) domains.add(domain, zoneName, provider, dnsConfig, null /* cert */, done);
|
||||
else domains.update(domain, provider, dnsConfig, null /* cert */, done);
|
||||
});
|
||||
}
|
||||
|
||||
function configureDefaultServer(callback) {
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
|
||||
debug('configureDefaultServer: domain %s', config.fqdn());
|
||||
|
||||
if (process.env.BOX_ENV === 'test') return callback();
|
||||
|
||||
var certFilePath = path.join(paths.NGINX_CERT_DIR, 'default.cert');
|
||||
var keyFilePath = path.join(paths.NGINX_CERT_DIR, 'default.key');
|
||||
|
||||
if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) {
|
||||
debug('configureDefaultServer: create new cert');
|
||||
|
||||
var cn = 'cloudron-' + (new Date()).toISOString(); // randomize date a bit to keep firefox happy
|
||||
var certCommand = util.format('openssl req -x509 -newkey rsa:2048 -keyout %s -out %s -days 3650 -subj /CN=%s -nodes', keyFilePath, certFilePath, cn);
|
||||
safe.child_process.execSync(certCommand);
|
||||
}
|
||||
|
||||
nginx.configureAdmin(certFilePath, keyFilePath, 'default.conf', '', function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('configureDefaultServer: done');
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function configureWebadmin(callback) {
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
|
||||
debug('configureWebadmin: fqdn:%s status:%j', config.fqdn(), gWebadminStatus);
|
||||
|
||||
if (process.env.BOX_ENV === 'test' || !config.fqdn() || gWebadminStatus.configuring) return callback();
|
||||
|
||||
gWebadminStatus.configuring = true; // re-entracy guard
|
||||
|
||||
function done(error) {
|
||||
gWebadminStatus.configuring = false;
|
||||
debug('configureWebadmin: done error: %j', error || {});
|
||||
callback(error);
|
||||
}
|
||||
|
||||
function configureNginx(error) {
|
||||
debug('configureNginx: dns update: %j', error || {});
|
||||
|
||||
certificates.ensureCertificate({ domain: config.fqdn(), location: config.adminLocation(), intrinsicFqdn: config.adminFqdn() }, function (error, certFilePath, keyFilePath) {
|
||||
if (error) return done(error);
|
||||
|
||||
gWebadminStatus.tls = true;
|
||||
|
||||
nginx.configureAdmin(certFilePath, keyFilePath, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn(), done);
|
||||
});
|
||||
}
|
||||
|
||||
// update the DNS. configure nginx regardless of whether it succeeded so that
|
||||
// box is accessible even if dns creds are invalid
|
||||
sysinfo.getPublicIp(function (error, ip) {
|
||||
if (error) return configureNginx(error);
|
||||
|
||||
addDnsRecords(ip, function (error) {
|
||||
if (error) return configureNginx(error);
|
||||
|
||||
domains.waitForDNSRecord(config.adminFqdn(), config.fqdn(), ip, 'A', { interval: 30000, times: 50000 }, function (error) {
|
||||
if (error) return configureNginx(error);
|
||||
|
||||
gWebadminStatus.dns = true;
|
||||
|
||||
configureNginx();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setTimeZone(ip, callback) {
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('setTimeZone ip:%s', ip);
|
||||
|
||||
superagent.get('https://geolocation.cloudron.io/json').query({ ip: ip }).timeout(10 * 1000).end(function (error, result) {
|
||||
if ((error && !error.response) || result.statusCode !== 200) {
|
||||
debug('Failed to get geo location: %s', error.message);
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
var timezone = safe.query(result.body, 'location.time_zone');
|
||||
|
||||
if (!timezone || typeof timezone !== 'string') {
|
||||
debug('No timezone in geoip response : %j', result.body);
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
debug('Setting timezone to ', timezone);
|
||||
|
||||
settings.setTimeZone(timezone, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function activate(username, password, email, displayName, ip, auditSource, callback) {
|
||||
assert.strictEqual(typeof username, 'string');
|
||||
assert.strictEqual(typeof password, 'string');
|
||||
assert.strictEqual(typeof email, 'string');
|
||||
assert.strictEqual(typeof displayName, 'string');
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('activating user:%s email:%s', username, email);
|
||||
|
||||
setTimeZone(ip, function () { }); // TODO: get this from user. note that timezone is detected based on the browser location and not the cloudron region
|
||||
|
||||
user.createOwner(username, password, email, displayName, auditSource, function (error, userObject) {
|
||||
if (error && error.reason === UserError.ALREADY_EXISTS) return callback(new CloudronError(CloudronError.ALREADY_PROVISIONED));
|
||||
if (error && error.reason === UserError.BAD_FIELD) return callback(new CloudronError(CloudronError.BAD_FIELD, error.message));
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
|
||||
clients.get('cid-webadmin', function (error, result) {
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
|
||||
// Also generate a token so the admin creation can also act as a login
|
||||
var token = tokendb.generateToken();
|
||||
var expires = Date.now() + constants.DEFAULT_TOKEN_EXPIRATION;
|
||||
|
||||
tokendb.add(token, userObject.id, result.id, expires, '*', function (error) {
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
|
||||
eventlog.add(eventlog.ACTION_ACTIVATE, auditSource, { });
|
||||
|
||||
onActivated();
|
||||
|
||||
callback(null, { token: token, expires: expires });
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getStatus(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
user.count(function (error, count) {
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
|
||||
settings.getCloudronName(function (error, cloudronName) {
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, {
|
||||
activated: count !== 0,
|
||||
version: config.version(),
|
||||
apiServerOrigin: config.apiServerOrigin(), // used by CaaS tool
|
||||
provider: config.provider(),
|
||||
cloudronName: cloudronName,
|
||||
adminFqdn: config.fqdn() ? config.adminFqdn() : null,
|
||||
webadminStatus: gWebadminStatus
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getDisks(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -132,23 +424,208 @@ function getDisks(callback) {
|
||||
function getConfig(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settings.getAll(function (error, allSettings) {
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, 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: ''
|
||||
}
|
||||
};
|
||||
|
||||
// be picky about what we send out here since this is sent for 'normal' users as well
|
||||
callback(null, {
|
||||
apiServerOrigin: config.apiServerOrigin(),
|
||||
webServerOrigin: config.webServerOrigin(),
|
||||
adminDomain: config.adminDomain(),
|
||||
adminFqdn: config.adminFqdn(),
|
||||
mailFqdn: config.mailFqdn(),
|
||||
version: config.version(),
|
||||
progress: progress.getAll(),
|
||||
isDemo: config.isDemo(),
|
||||
edition: config.edition(),
|
||||
memory: os.totalmem(),
|
||||
provider: config.provider(),
|
||||
cloudronName: allSettings[settings.CLOUDRON_NAME_KEY]
|
||||
caas.getBoxAndUserDetails(function (error, result) {
|
||||
if (error) debug('Failed to fetch cloudron details.', error.reason, error.message);
|
||||
|
||||
result = _.extend(BOX_AND_USER_TEMPLATE, result || {});
|
||||
|
||||
settings.getCloudronName(function (error, cloudronName) {
|
||||
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(),
|
||||
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
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function ensureDkimKey(callback) {
|
||||
assert(config.fqdn(), 'fqdn is not set');
|
||||
|
||||
var dkimPath = path.join(paths.MAIL_DATA_DIR, 'dkim/' + config.fqdn());
|
||||
var dkimPrivateKeyFile = path.join(dkimPath, 'private');
|
||||
var dkimPublicKeyFile = path.join(dkimPath, 'public');
|
||||
|
||||
if (!fs.existsSync(dkimPrivateKeyFile) || !fs.existsSync(dkimPublicKeyFile)) {
|
||||
debug('Generating new DKIM keys');
|
||||
|
||||
if (!safe.fs.mkdirSync(dkimPath) && safe.error.code !== 'EEXIST') {
|
||||
debug('Error creating dkim.', safe.error);
|
||||
return null;
|
||||
}
|
||||
|
||||
child_process.execSync('openssl genrsa -out ' + dkimPrivateKeyFile + ' 1024');
|
||||
child_process.execSync('openssl rsa -in ' + dkimPrivateKeyFile + ' -out ' + dkimPublicKeyFile + ' -pubout -outform PEM');
|
||||
} else {
|
||||
debug('DKIM keys already present');
|
||||
}
|
||||
|
||||
callback();
|
||||
}
|
||||
|
||||
function readDkimPublicKeySync() {
|
||||
if (!config.fqdn()) {
|
||||
debug('Cannot read dkim public key without a domain.', safe.error);
|
||||
return null;
|
||||
}
|
||||
|
||||
var dkimPath = path.join(paths.MAIL_DATA_DIR, 'dkim/' + config.fqdn());
|
||||
var dkimPublicKeyFile = path.join(dkimPath, 'public');
|
||||
|
||||
var publicKey = safe.fs.readFileSync(dkimPublicKeyFile, 'utf8');
|
||||
|
||||
if (publicKey === null) {
|
||||
debug('Error reading dkim public key.', safe.error);
|
||||
return null;
|
||||
}
|
||||
|
||||
// remove header, footer and new lines
|
||||
publicKey = publicKey.split('\n').slice(1, -2).join('');
|
||||
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
// NOTE: if you change the SPF record here, be sure the wait check in mailer.js
|
||||
// https://agari.zendesk.com/hc/en-us/articles/202952749-How-long-can-my-SPF-record-be-
|
||||
function txtRecordsWithSpf(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
domains.getDNSRecords('', config.fqdn(), 'TXT', function (error, txtRecords) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('txtRecordsWithSpf: current txt records - %j', txtRecords);
|
||||
|
||||
var i, matches, validSpf;
|
||||
|
||||
for (i = 0; i < txtRecords.length; i++) {
|
||||
matches = txtRecords[i].match(/^("?v=spf1) /); // DO backend may return without quotes
|
||||
if (matches === null) continue;
|
||||
|
||||
// this won't work if the entry is arbitrarily "split" across quoted strings
|
||||
validSpf = txtRecords[i].indexOf('a:' + config.adminFqdn()) !== -1;
|
||||
break; // there can only be one SPF record
|
||||
}
|
||||
|
||||
if (validSpf) return callback(null, null);
|
||||
|
||||
if (!matches) { // no spf record was found, create one
|
||||
txtRecords.push('"v=spf1 a:' + config.adminFqdn() + ' ~all"');
|
||||
debug('txtRecordsWithSpf: adding txt record');
|
||||
} else { // just add ourself
|
||||
txtRecords[i] = matches[1] + ' a:' + config.adminFqdn() + txtRecords[i].slice(matches[1].length);
|
||||
debug('txtRecordsWithSpf: inserting txt record');
|
||||
}
|
||||
|
||||
return callback(null, txtRecords);
|
||||
});
|
||||
}
|
||||
|
||||
function addDnsRecords(ip, callback) {
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
|
||||
if (process.env.BOX_ENV === 'test') return callback();
|
||||
|
||||
var dkimKey = readDkimPublicKeySync();
|
||||
if (!dkimKey) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, new Error('Failed to read dkim public key')));
|
||||
|
||||
var webadminRecord = { subdomain: config.adminLocation(), domain: config.fqdn(), type: 'A', values: [ ip ] };
|
||||
// t=s limits the domainkey to this domain and not it's subdomains
|
||||
var dkimRecord = { subdomain: config.dkimSelector() + '._domainkey', domain: config.fqdn(), type: 'TXT', values: [ '"v=DKIM1; t=s; p=' + dkimKey + '"' ] };
|
||||
|
||||
var records = [ ];
|
||||
records.push(webadminRecord);
|
||||
records.push(dkimRecord);
|
||||
|
||||
debug('addDnsRecords: %j', records);
|
||||
|
||||
async.retry({ times: 10, interval: 20000 }, function (retryCallback) {
|
||||
txtRecordsWithSpf(function (error, txtRecords) {
|
||||
if (error) return retryCallback(error);
|
||||
|
||||
if (txtRecords) records.push({ subdomain: '', domain: config.fqdn(), type: 'TXT', values: txtRecords });
|
||||
|
||||
debug('addDnsRecords: will update %j', records);
|
||||
|
||||
async.mapSeries(records, function (record, iteratorCallback) {
|
||||
domains.upsertDNSRecords(record.subdomain, record.domain, record.type, record.values, iteratorCallback);
|
||||
}, function (error, changeIds) {
|
||||
if (error) debug('addDnsRecords: failed to update : %s. will retry', error);
|
||||
else debug('addDnsRecords: records %j added with changeIds %j', records, changeIds);
|
||||
|
||||
retryCallback(error);
|
||||
});
|
||||
});
|
||||
}, function (error) {
|
||||
if (error) debug('addDnsRecords: done updating records with error:', error);
|
||||
else debug('addDnsRecords: done');
|
||||
|
||||
callback(error);
|
||||
});
|
||||
}
|
||||
|
||||
function restore(backupConfig, backupId, version, callback) {
|
||||
assert.strictEqual(typeof backupConfig, 'object');
|
||||
assert.strictEqual(typeof backupId, 'string');
|
||||
assert.strictEqual(typeof version, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!semver.valid(version)) return callback(new CloudronError(CloudronError.BAD_STATE, 'version is not a valid semver'));
|
||||
if (semver.major(config.version()) !== semver.major(version) || semver.minor(config.version()) !== semver.minor(version)) return callback(new CloudronError(CloudronError.BAD_STATE, `Run cloudron-setup with --version ${version} to restore from this backup`));
|
||||
|
||||
user.count(function (error, count) {
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
if (count) return callback(new CloudronError(CloudronError.ALREADY_PROVISIONED, 'Already activated'));
|
||||
|
||||
backups.testConfig(backupConfig, function (error) {
|
||||
if (error && error.reason === BackupsError.BAD_FIELD) return callback(new CloudronError(CloudronError.BAD_FIELD, error.message));
|
||||
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new CloudronError(CloudronError.EXTERNAL_ERROR, error.message));
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
|
||||
debug(`restore: restoring from ${backupId} from provider ${backupConfig.provider}`);
|
||||
|
||||
gWebadminStatus.restoring = true;
|
||||
|
||||
callback(null); // do no block
|
||||
|
||||
async.series([
|
||||
backups.restore.bind(null, backupConfig, backupId),
|
||||
autoprovision,
|
||||
shell.sudo.bind(null, 'restart', [ RESTART_CMD ])
|
||||
], function (error) {
|
||||
debug('restore:', error);
|
||||
gWebadminStatus.restoring = false;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -157,6 +634,125 @@ function reboot(callback) {
|
||||
shell.sudo('reboot', [ REBOOT_CMD ], callback);
|
||||
}
|
||||
|
||||
function update(boxUpdateInfo, auditSource, callback) {
|
||||
assert.strictEqual(typeof boxUpdateInfo, 'object');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!boxUpdateInfo) return callback(null);
|
||||
|
||||
var error = locker.lock(locker.OP_BOX_UPDATE);
|
||||
if (error) return callback(new CloudronError(CloudronError.BAD_STATE, error.message));
|
||||
|
||||
eventlog.add(eventlog.ACTION_UPDATE, auditSource, { boxUpdateInfo: boxUpdateInfo });
|
||||
|
||||
// ensure tools can 'wait' on progress
|
||||
progress.set(progress.UPDATE, 0, 'Starting');
|
||||
|
||||
// initiate the update/upgrade but do not wait for it
|
||||
if (boxUpdateInfo.upgrade) {
|
||||
debug('Starting upgrade');
|
||||
caas.upgrade(boxUpdateInfo, function (error) {
|
||||
if (error) {
|
||||
debug('Upgrade failed with error:', error);
|
||||
locker.unlock(locker.OP_BOX_UPDATE);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
debug('Starting update');
|
||||
doUpdate(boxUpdateInfo, function (error) {
|
||||
if (error) {
|
||||
debug('Update failed with error:', error);
|
||||
locker.unlock(locker.OP_BOX_UPDATE);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
callback(null);
|
||||
}
|
||||
|
||||
function updateToLatest(auditSource, callback) {
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var boxUpdateInfo = updateChecker.getUpdateInfo().box;
|
||||
if (!boxUpdateInfo) return callback(new CloudronError(CloudronError.ALREADY_UPTODATE, 'No update available'));
|
||||
if (!boxUpdateInfo.sourceTarballUrl) return callback(new CloudronError(CloudronError.BAD_STATE, 'No automatic update available'));
|
||||
|
||||
// check if this is just a version number change
|
||||
if (config.version().match(/[-+]/) !== null && config.version().replace(/[-+].*/, '') === boxUpdateInfo.version) {
|
||||
doShortCircuitUpdate(boxUpdateInfo, function (error) {
|
||||
if (error) debug('Short-circuit update failed', error);
|
||||
});
|
||||
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
if (boxUpdateInfo.upgrade && config.provider() !== 'caas') return callback(new CloudronError(CloudronError.SELF_UPGRADE_NOT_SUPPORTED));
|
||||
|
||||
update(boxUpdateInfo, auditSource, callback);
|
||||
}
|
||||
|
||||
function doShortCircuitUpdate(boxUpdateInfo, callback) {
|
||||
assert(boxUpdateInfo !== null && typeof boxUpdateInfo === 'object');
|
||||
|
||||
debug('Starting short-circuit from prerelease version %s to release version %s', config.version(), boxUpdateInfo.version);
|
||||
config.setVersion(boxUpdateInfo.version);
|
||||
progress.clear(progress.UPDATE);
|
||||
updateChecker.resetUpdateInfo();
|
||||
callback();
|
||||
}
|
||||
|
||||
function doUpdate(boxUpdateInfo, callback) {
|
||||
assert(boxUpdateInfo && typeof boxUpdateInfo === 'object');
|
||||
|
||||
function updateError(e) {
|
||||
progress.set(progress.UPDATE, -1, e.message);
|
||||
callback(e);
|
||||
}
|
||||
|
||||
progress.set(progress.UPDATE, 5, 'Backing up for update');
|
||||
|
||||
backups.backupBoxAndApps({ userId: null, username: 'updater' }, function (error) {
|
||||
if (error) return updateError(error);
|
||||
|
||||
// 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(),
|
||||
fqdn: config.fqdn(),
|
||||
adminFqdn: config.adminFqdn(),
|
||||
adminLocation: config.adminLocation(),
|
||||
isDemo: config.isDemo(),
|
||||
zoneName: config.zoneName(),
|
||||
|
||||
appstore: {
|
||||
token: config.token(),
|
||||
apiServerOrigin: config.apiServerOrigin()
|
||||
},
|
||||
caas: {
|
||||
token: config.token(),
|
||||
apiServerOrigin: config.apiServerOrigin(),
|
||||
webServerOrigin: config.webServerOrigin()
|
||||
},
|
||||
|
||||
version: boxUpdateInfo.version
|
||||
};
|
||||
|
||||
debug('updating box %s %j', boxUpdateInfo.sourceTarballUrl, _.omit(data, 'tlsCert', 'tlsKey', 'token', 'appstore', 'caas'));
|
||||
|
||||
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);
|
||||
|
||||
// Do not add any code here. The installer script will stop the box code any instant
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function checkDiskSpace(callback) {
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
|
||||
@@ -199,66 +795,78 @@ function checkDiskSpace(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getLogs(unit, options, callback) {
|
||||
assert.strictEqual(typeof unit, 'string');
|
||||
// called for dynamic dns setups where we have to update the IP
|
||||
function refreshDNS(callback) {
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
|
||||
sysinfo.getPublicIp(function (error, ip) {
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
|
||||
debug('refreshDNS: current ip %s', ip);
|
||||
|
||||
addDnsRecords(ip, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('refreshDNS: done for system records');
|
||||
|
||||
apps.getAll(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.each(result, function (app, callback) {
|
||||
// do not change state of installing apps since apptask will error if dns record already exists
|
||||
if (app.installationState !== appdb.ISTATE_INSTALLED) return callback();
|
||||
|
||||
domains.upsertDNSRecords(app.location, app.domain, 'A', [ ip ], callback);
|
||||
}, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('refreshDNS: done for apps');
|
||||
|
||||
callback();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getLogs(options, callback) {
|
||||
assert(options && typeof options === 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var lines = options.lines || 100,
|
||||
var units = options.units || [],
|
||||
lines = options.lines || 100,
|
||||
format = options.format || 'json',
|
||||
follow = !!options.follow;
|
||||
|
||||
assert(Array.isArray(units));
|
||||
assert.strictEqual(typeof lines, 'number');
|
||||
assert.strictEqual(typeof format, 'string');
|
||||
|
||||
assert.strictEqual(typeof lines, 'number');
|
||||
assert.strictEqual(typeof format, 'string');
|
||||
debug('Getting logs for %j', units);
|
||||
|
||||
debug('Getting logs for %s as %s', unit, format);
|
||||
var args = [ '--no-pager', '--lines=' + lines ];
|
||||
units.forEach(function (u) {
|
||||
if (u === 'box') args.push('--unit=box');
|
||||
else if (u === 'mail') args.push('CONTAINER_NAME=mail');
|
||||
});
|
||||
if (format === 'short') args.push('--output=short', '-a'); else args.push('--output=json');
|
||||
if (follow) args.push('--follow');
|
||||
|
||||
var cp, transformStream;
|
||||
if (unit === 'box') {
|
||||
let args = [ '--no-pager', `--lines=${lines}` ];
|
||||
if (format === 'short') args.push('--output=short', '-a'); else args.push('--output=json');
|
||||
if (follow) args.push('--follow');
|
||||
args.push('--unit=box');
|
||||
args.push('--unit=cloudron-updater');
|
||||
cp = spawn('/bin/journalctl', args);
|
||||
var cp = spawn('/bin/journalctl', args);
|
||||
|
||||
transformStream = split(function mapper(line) {
|
||||
if (format !== 'json') return line + '\n';
|
||||
var transformStream = split(function mapper(line) {
|
||||
if (format !== 'json') return line + '\n';
|
||||
|
||||
var obj = safe.JSON.parse(line);
|
||||
if (!obj) return undefined;
|
||||
var obj = safe.JSON.parse(line);
|
||||
if (!obj) return undefined;
|
||||
|
||||
return JSON.stringify({
|
||||
realtimeTimestamp: obj.__REALTIME_TIMESTAMP,
|
||||
monotonicTimestamp: obj.__MONOTONIC_TIMESTAMP,
|
||||
message: obj.MESSAGE,
|
||||
source: obj.SYSLOG_IDENTIFIER || ''
|
||||
}) + '\n';
|
||||
});
|
||||
} else { // mail, mongodb, mysql, postgresql, backup
|
||||
let args = [ '--lines=' + lines ];
|
||||
if (follow) args.push('--follow');
|
||||
args.push(path.join(paths.LOG_DIR, unit, 'app.log'));
|
||||
|
||||
cp = spawn('/usr/bin/tail', args);
|
||||
|
||||
transformStream = split(function mapper(line) {
|
||||
if (format !== 'json') return line + '\n';
|
||||
|
||||
var data = line.split(' '); // logs are <ISOtimestamp> <msg>
|
||||
var timestamp = (new Date(data[0])).getTime();
|
||||
if (isNaN(timestamp)) timestamp = 0;
|
||||
|
||||
return JSON.stringify({
|
||||
realtimeTimestamp: timestamp * 1000,
|
||||
message: line.slice(data[0].length+1),
|
||||
source: unit
|
||||
}) + '\n';
|
||||
});
|
||||
}
|
||||
return JSON.stringify({
|
||||
realtimeTimestamp: obj.__REALTIME_TIMESTAMP,
|
||||
monotonicTimestamp: obj.__MONOTONIC_TIMESTAMP,
|
||||
message: obj.MESSAGE,
|
||||
source: obj.SYSLOG_IDENTIFIER || ''
|
||||
}) + '\n';
|
||||
});
|
||||
|
||||
transformStream.close = cp.kill.bind(cp, 'SIGKILL'); // closing stream kills the child process
|
||||
|
||||
|
||||
+35
-43
@@ -16,15 +16,15 @@ exports = module.exports = {
|
||||
provider: provider,
|
||||
apiServerOrigin: apiServerOrigin,
|
||||
webServerOrigin: webServerOrigin,
|
||||
adminDomain: adminDomain,
|
||||
setFqdn: setAdminDomain,
|
||||
setAdminDomain: setAdminDomain,
|
||||
fqdn: fqdn,
|
||||
zoneName: zoneName,
|
||||
setFqdn: setFqdn,
|
||||
setAdminFqdn: setAdminFqdn,
|
||||
setAdminLocation: setAdminLocation,
|
||||
token: token,
|
||||
version: version,
|
||||
setVersion: setVersion,
|
||||
database: database,
|
||||
edition: edition,
|
||||
|
||||
// these values are derived
|
||||
adminOrigin: adminOrigin,
|
||||
@@ -34,17 +34,12 @@ exports = module.exports = {
|
||||
adminFqdn: adminFqdn,
|
||||
mailLocation: mailLocation,
|
||||
mailFqdn: mailFqdn,
|
||||
setZoneName: setZoneName,
|
||||
hasIPv6: hasIPv6,
|
||||
dkimSelector: dkimSelector,
|
||||
|
||||
isDemo: isDemo,
|
||||
|
||||
// feature flags based on editions (these have a separate license from standard edition)
|
||||
isSpacesEnabled: isSpacesEnabled,
|
||||
allowHyphenatedSubdomains: allowHyphenatedSubdomains,
|
||||
allowOperatorActions: allowOperatorActions,
|
||||
isAdminDomainLocked: isAdminDomainLocked,
|
||||
|
||||
// for testing resets to defaults
|
||||
_reset: _reset
|
||||
};
|
||||
@@ -53,6 +48,7 @@ var assert = require('assert'),
|
||||
fs = require('fs'),
|
||||
path = require('path'),
|
||||
safe = require('safetydance'),
|
||||
tld = require('tldjs'),
|
||||
_ = require('underscore');
|
||||
|
||||
|
||||
@@ -77,14 +73,15 @@ function saveSync() {
|
||||
// only save values we want to have in the cloudron.conf, see start.sh
|
||||
var conf = {
|
||||
version: data.version,
|
||||
token: data.token,
|
||||
apiServerOrigin: data.apiServerOrigin,
|
||||
webServerOrigin: data.webServerOrigin,
|
||||
adminDomain: data.adminDomain,
|
||||
fqdn: data.fqdn,
|
||||
adminFqdn: data.adminFqdn,
|
||||
zoneName: data.zoneName,
|
||||
adminLocation: data.adminLocation,
|
||||
provider: data.provider,
|
||||
isDemo: data.isDemo,
|
||||
edition: data.edition
|
||||
isDemo: data.isDemo
|
||||
};
|
||||
|
||||
fs.writeFileSync(cloudronConfigFileName, JSON.stringify(conf, null, 4)); // functions are ignored by JSON.stringify
|
||||
@@ -100,19 +97,19 @@ function _reset(callback) {
|
||||
|
||||
function initConfig() {
|
||||
// setup defaults
|
||||
data.fqdn = '';
|
||||
data.adminFqdn = '';
|
||||
data.adminDomain = '';
|
||||
data.zoneName = '';
|
||||
data.adminLocation = 'my';
|
||||
data.port = 3000;
|
||||
data.token = null;
|
||||
data.version = null;
|
||||
data.apiServerOrigin = null;
|
||||
data.webServerOrigin = null;
|
||||
data.provider = 'generic';
|
||||
data.provider = 'caas';
|
||||
data.smtpPort = 2525; // this value comes from mail container
|
||||
data.sysadminPort = 3001;
|
||||
data.ldapPort = 3002;
|
||||
data.dockerProxyPort = 3003;
|
||||
data.edition = '';
|
||||
|
||||
// keep in sync with start.sh
|
||||
data.database = {
|
||||
@@ -127,6 +124,7 @@ function initConfig() {
|
||||
if (exports.TEST) {
|
||||
data.version = '1.1.1-test';
|
||||
data.port = 5454;
|
||||
data.token = 'APPSTORE_TOKEN';
|
||||
data.apiServerOrigin = 'http://localhost:6060'; // hock doesn't support https
|
||||
data.database.password = '';
|
||||
data.database.name = 'boxtest';
|
||||
@@ -150,7 +148,6 @@ function set(key, value) {
|
||||
} else {
|
||||
data = safe.set(data, key, value);
|
||||
}
|
||||
|
||||
saveSync();
|
||||
}
|
||||
|
||||
@@ -168,12 +165,24 @@ function webServerOrigin() {
|
||||
return get('webServerOrigin');
|
||||
}
|
||||
|
||||
function setAdminDomain(domain) {
|
||||
set('adminDomain', domain);
|
||||
function setFqdn(fqdn) {
|
||||
set('fqdn', fqdn);
|
||||
}
|
||||
|
||||
function adminDomain() {
|
||||
return get('adminDomain');
|
||||
function fqdn() {
|
||||
return get('fqdn');
|
||||
}
|
||||
|
||||
function setZoneName(zone) {
|
||||
set('zoneName', zone);
|
||||
}
|
||||
|
||||
function zoneName() {
|
||||
var zone = get('zoneName');
|
||||
if (zone) return zone;
|
||||
|
||||
// TODO: move this to migration code path instead
|
||||
return tld.getDomain(fqdn()) || '';
|
||||
}
|
||||
|
||||
function mailLocation() {
|
||||
@@ -213,6 +222,10 @@ function sysadminOrigin() {
|
||||
return 'http://127.0.0.1:' + get('sysadminPort');
|
||||
}
|
||||
|
||||
function token() {
|
||||
return get('token');
|
||||
}
|
||||
|
||||
function version() {
|
||||
return get('version');
|
||||
}
|
||||
@@ -229,22 +242,6 @@ function isDemo() {
|
||||
return get('isDemo') === true;
|
||||
}
|
||||
|
||||
function isSpacesEnabled() {
|
||||
return get('edition') === 'education';
|
||||
}
|
||||
|
||||
function allowHyphenatedSubdomains() {
|
||||
return get('edition') === 'hostingprovider';
|
||||
}
|
||||
|
||||
function allowOperatorActions() {
|
||||
return get('edition') !== 'hostingprovider';
|
||||
}
|
||||
|
||||
function isAdminDomainLocked() {
|
||||
return get('edition') === 'hostingprovider';
|
||||
}
|
||||
|
||||
function provider() {
|
||||
return get('provider');
|
||||
}
|
||||
@@ -254,13 +251,8 @@ function hasIPv6() {
|
||||
return fs.existsSync(IPV6_PROC_FILE);
|
||||
}
|
||||
|
||||
// it has to change with the adminLocation so that multiple cloudrons
|
||||
// can send out emails at the same time.
|
||||
function dkimSelector() {
|
||||
var loc = adminLocation();
|
||||
return loc === 'my' ? 'cloudron' : `cloudron-${loc.replace(/\./g, '')}`;
|
||||
}
|
||||
|
||||
function edition() {
|
||||
return get('edition');
|
||||
}
|
||||
|
||||
+7
-3
@@ -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
|
||||
@@ -19,6 +18,11 @@ exports = module.exports = {
|
||||
|
||||
ADMIN_NAME: 'Settings',
|
||||
|
||||
ADMIN_CLIENT_ID: 'webadmin', // oauth client id
|
||||
|
||||
ADMIN_GROUP_NAME: 'admin',
|
||||
ADMIN_GROUP_ID: 'admin',
|
||||
|
||||
NGINX_ADMIN_CONFIG_FILE_NAME: 'admin.conf',
|
||||
|
||||
GHOST_USER_FILE: '/tmp/cloudron_ghost.json',
|
||||
|
||||
+25
-49
@@ -10,25 +10,23 @@ var apps = require('./apps.js'),
|
||||
assert = require('assert'),
|
||||
backups = require('./backups.js'),
|
||||
caas = require('./caas.js'),
|
||||
certificates = require('./certificates.js'),
|
||||
cloudron = require('./cloudron.js'),
|
||||
config = require('./config.js'),
|
||||
constants = require('./constants.js'),
|
||||
CronJob = require('cron').CronJob,
|
||||
debug = require('debug')('box:cron'),
|
||||
digest = require('./digest.js'),
|
||||
dyndns = require('./dyndns.js'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
janitor = require('./janitor.js'),
|
||||
reverseProxy = require('./reverseproxy.js'),
|
||||
scheduler = require('./scheduler.js'),
|
||||
settings = require('./settings.js'),
|
||||
updater = require('./updater.js'),
|
||||
semver = require('semver'),
|
||||
updateChecker = require('./updatechecker.js');
|
||||
|
||||
var gJobs = {
|
||||
alive: null, // send periodic stats
|
||||
appAutoUpdater: null,
|
||||
boxAutoUpdater: null,
|
||||
autoUpdater: null,
|
||||
appUpdateChecker: null,
|
||||
backup: null,
|
||||
boxUpdateChecker: null,
|
||||
@@ -79,17 +77,15 @@ function initialize(callback) {
|
||||
});
|
||||
|
||||
settings.events.on(settings.TIME_ZONE_KEY, recreateJobs);
|
||||
settings.events.on(settings.APP_AUTOUPDATE_PATTERN_KEY, appAutoupdatePatternChanged);
|
||||
settings.events.on(settings.BOX_AUTOUPDATE_PATTERN_KEY, boxAutoupdatePatternChanged);
|
||||
settings.events.on(settings.DYNAMIC_DNS_KEY, dynamicDnsChanged);
|
||||
settings.events.on(settings.AUTOUPDATE_PATTERN_KEY, autoupdatePatternChanged);
|
||||
settings.events.on(settings.DYNAMIC_DNS_KEY, dynamicDNSChanged);
|
||||
|
||||
settings.getAll(function (error, allSettings) {
|
||||
if (error) return callback(error);
|
||||
|
||||
recreateJobs(allSettings[settings.TIME_ZONE_KEY]);
|
||||
appAutoupdatePatternChanged(allSettings[settings.APP_AUTOUPDATE_PATTERN_KEY]);
|
||||
boxAutoupdatePatternChanged(allSettings[settings.BOX_AUTOUPDATE_PATTERN_KEY]);
|
||||
dynamicDnsChanged(allSettings[settings.DYNAMIC_DNS_KEY]);
|
||||
autoupdatePatternChanged(allSettings[settings.AUTOUPDATE_PATTERN_KEY]);
|
||||
dynamicDNSChanged(allSettings[settings.DYNAMIC_DNS_KEY]);
|
||||
|
||||
callback();
|
||||
});
|
||||
@@ -102,7 +98,7 @@ function recreateJobs(tz) {
|
||||
|
||||
if (gJobs.backup) gJobs.backup.stop();
|
||||
gJobs.backup = new CronJob({
|
||||
cronTime: '00 00 */6 * * *', // check every 6 hours
|
||||
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
|
||||
@@ -178,7 +174,7 @@ function recreateJobs(tz) {
|
||||
if (gJobs.certificateRenew) gJobs.certificateRenew.stop();
|
||||
gJobs.certificateRenew = new CronJob({
|
||||
cronTime: '00 00 */12 * * *', // every 12 hours
|
||||
onTick: reverseProxy.renewAll.bind(null, AUDIT_SOURCE, NOOP_CALLBACK),
|
||||
onTick: certificates.renewAll.bind(null, AUDIT_SOURCE, NOOP_CALLBACK),
|
||||
start: true,
|
||||
timeZone: tz
|
||||
});
|
||||
@@ -192,51 +188,32 @@ function recreateJobs(tz) {
|
||||
});
|
||||
}
|
||||
|
||||
function boxAutoupdatePatternChanged(pattern) {
|
||||
function autoupdatePatternChanged(pattern) {
|
||||
assert.strictEqual(typeof pattern, 'string');
|
||||
assert(gJobs.boxUpdateCheckerJob);
|
||||
|
||||
debug('Box auto update pattern changed to %s', pattern);
|
||||
debug('Auto update pattern changed to %s', pattern);
|
||||
|
||||
if (gJobs.boxAutoUpdater) gJobs.boxAutoUpdater.stop();
|
||||
if (gJobs.autoUpdater) gJobs.autoUpdater.stop();
|
||||
|
||||
if (pattern === constants.AUTOUPDATE_PATTERN_NEVER) return;
|
||||
|
||||
gJobs.boxAutoUpdater = new CronJob({
|
||||
gJobs.autoUpdater = new CronJob({
|
||||
cronTime: pattern,
|
||||
onTick: function() {
|
||||
var updateInfo = updateChecker.getUpdateInfo();
|
||||
if (updateInfo.box) {
|
||||
debug('Starting autoupdate to %j', updateInfo.box);
|
||||
updater.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,
|
||||
@@ -244,7 +221,7 @@ function appAutoupdatePatternChanged(pattern) {
|
||||
});
|
||||
}
|
||||
|
||||
function dynamicDnsChanged(enabled) {
|
||||
function dynamicDNSChanged(enabled) {
|
||||
assert.strictEqual(typeof enabled, 'boolean');
|
||||
assert(gJobs.boxUpdateCheckerJob);
|
||||
|
||||
@@ -253,7 +230,7 @@ function dynamicDnsChanged(enabled) {
|
||||
if (enabled) {
|
||||
gJobs.dynamicDNS = new CronJob({
|
||||
cronTime: '00 */10 * * * *',
|
||||
onTick: dyndns.sync,
|
||||
onTick: cloudron.refreshDNS,
|
||||
start: true,
|
||||
timeZone: gJobs.boxUpdateCheckerJob.cronTime.zone // hack
|
||||
});
|
||||
@@ -267,9 +244,8 @@ function uninitialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settings.events.removeListener(settings.TIME_ZONE_KEY, recreateJobs);
|
||||
settings.events.removeListener(settings.APP_AUTOUPDATE_PATTERN_KEY, appAutoupdatePatternChanged);
|
||||
settings.events.removeListener(settings.BOX_AUTOUPDATE_PATTERN_KEY, boxAutoupdatePatternChanged);
|
||||
settings.events.removeListener(settings.DYNAMIC_DNS_KEY, dynamicDnsChanged);
|
||||
settings.events.removeListener(settings.AUTOUPDATE_PATTERN_KEY, autoupdatePatternChanged);
|
||||
settings.events.removeListener(settings.DYNAMIC_DNS_KEY, dynamicDNSChanged);
|
||||
|
||||
for (var job in gJobs) {
|
||||
if (!gJobs[job]) continue;
|
||||
|
||||
+17
-3
@@ -6,6 +6,10 @@ exports = module.exports = {
|
||||
query: query,
|
||||
transaction: transaction,
|
||||
|
||||
beginTransaction: beginTransaction,
|
||||
rollback: rollback,
|
||||
commit: commit,
|
||||
|
||||
importFromFile: importFromFile,
|
||||
exportToFile: exportToFile,
|
||||
|
||||
@@ -23,13 +27,21 @@ var assert = require('assert'),
|
||||
var gConnectionPool = null,
|
||||
gDefaultConnection = null;
|
||||
|
||||
function initialize(callback) {
|
||||
function initialize(options, callback) {
|
||||
if (typeof options === 'function') {
|
||||
callback = options;
|
||||
options = {
|
||||
connectionLimit: 5
|
||||
};
|
||||
}
|
||||
|
||||
assert.strictEqual(typeof options.connectionLimit, 'number');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (gConnectionPool !== null) return callback(null);
|
||||
|
||||
gConnectionPool = mysql.createPool({
|
||||
connectionLimit: 5, // this has to be > 1 since we store one connection as 'default'. the rest for transactions
|
||||
connectionLimit: options.connectionLimit,
|
||||
host: config.database().hostname,
|
||||
user: config.database().username,
|
||||
password: config.database().password,
|
||||
@@ -91,7 +103,9 @@ function clear(callback) {
|
||||
|
||||
async.series([
|
||||
child_process.exec.bind(null, cmd),
|
||||
require('./clientdb.js')._addDefaultClients
|
||||
require('./clientdb.js')._addDefaultClients,
|
||||
require('./domaindb.js')._addDefaultDomain,
|
||||
require('./groupdb.js')._addDefaultGroups
|
||||
], callback);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
/* jslint node: true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
DeveloperError: DeveloperError,
|
||||
|
||||
issueDeveloperToken: issueDeveloperToken
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
clients = require('./clients.js'),
|
||||
constants = require('./constants.js'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
tokendb = require('./tokendb.js'),
|
||||
util = require('util');
|
||||
|
||||
function DeveloperError(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(DeveloperError, Error);
|
||||
DeveloperError.INTERNAL_ERROR = 'Internal Error';
|
||||
DeveloperError.EXTERNAL_ERROR = 'External Error';
|
||||
|
||||
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, user.id, 'cid-cli', expiresAt, scopes, function (error) {
|
||||
if (error) return callback(new DeveloperError(DeveloperError.INTERNAL_ERROR, error));
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
+31
-23
@@ -28,36 +28,44 @@ function maybeSend(callback) {
|
||||
var pendingAppUpdates = updateInfo.apps || {};
|
||||
pendingAppUpdates = Object.keys(pendingAppUpdates).map(function (key) { return pendingAppUpdates[key]; });
|
||||
|
||||
eventlog.getByCreationTime(new Date(new Date() - 7*86400000), function (error, events) {
|
||||
if (error) return callback(error);
|
||||
appstore.getSubscription(function (error, result) {
|
||||
if (error) debug('Error getting subscription:', error);
|
||||
|
||||
var appUpdates = events.filter(function (e) { return e.action === eventlog.ACTION_APP_UPDATE; }).map(function (e) { return e.data; });
|
||||
var boxUpdates = events.filter(function (e) { return e.action === eventlog.ACTION_UPDATE; }).map(function (e) { return e.data; });
|
||||
var certRenewals = events.filter(function (e) { return e.action === eventlog.ACTION_CERTIFICATE_RENEWAL; }).map(function (e) { return e.data; });
|
||||
var usersAdded = events.filter(function (e) { return e.action === eventlog.ACTION_USER_ADD; }).map(function (e) { return e.data; });
|
||||
var usersRemoved = events.filter(function (e) { return e.action === eventlog.ACTION_USER_REMOVE; }).map(function (e) { return e.data; });
|
||||
var finishedBackups = events.filter(function (e) { return e.action === eventlog.ACTION_BACKUP_FINISH && !e.errorMessage; }).map(function (e) { return e.data; });
|
||||
var hasSubscription = result && result.plan.id !== 'free' && result.plan.id !== 'undecided';
|
||||
|
||||
if (error) return callback(error);
|
||||
eventlog.getByCreationTime(new Date(new Date() - 7*86400000), function (error, events) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var info = {
|
||||
pendingAppUpdates: pendingAppUpdates,
|
||||
pendingBoxUpdate: updateInfo.box || null,
|
||||
var appUpdates = events.filter(function (e) { return e.action === eventlog.ACTION_APP_UPDATE; }).map(function (e) { return e.data; });
|
||||
var boxUpdates = events.filter(function (e) { return e.action === eventlog.ACTION_UPDATE; }).map(function (e) { return e.data; });
|
||||
var certRenewals = events.filter(function (e) { return e.action === eventlog.ACTION_CERTIFICATE_RENEWAL; }).map(function (e) { return e.data; });
|
||||
var usersAdded = events.filter(function (e) { return e.action === eventlog.ACTION_USER_ADD; }).map(function (e) { return e.data; });
|
||||
var usersRemoved = events.filter(function (e) { return e.action === eventlog.ACTION_USER_REMOVE; }).map(function (e) { return e.data; });
|
||||
var finishedBackups = events.filter(function (e) { return e.action === eventlog.ACTION_BACKUP_FINISH && !e.errorMessage; }).map(function (e) { return e.data; });
|
||||
|
||||
finishedAppUpdates: appUpdates,
|
||||
finishedBoxUpdates: boxUpdates,
|
||||
if (error) return callback(error);
|
||||
|
||||
certRenewals: certRenewals,
|
||||
finishedBackups: finishedBackups, // only the successful backups
|
||||
usersAdded: usersAdded,
|
||||
usersRemoved: usersRemoved // unused because we don't have username to work with
|
||||
};
|
||||
var info = {
|
||||
hasSubscription: hasSubscription,
|
||||
|
||||
// always send digest for backup failure notification
|
||||
debug('maybeSend: sending digest email', info);
|
||||
mailer.sendDigest(info);
|
||||
pendingAppUpdates: pendingAppUpdates,
|
||||
pendingBoxUpdate: updateInfo.box || null,
|
||||
|
||||
callback();
|
||||
finishedAppUpdates: appUpdates,
|
||||
finishedBoxUpdates: boxUpdates,
|
||||
|
||||
certRenewals: certRenewals,
|
||||
finishedBackups: finishedBackups, // only the successful backups
|
||||
usersAdded: usersAdded,
|
||||
usersRemoved: usersRemoved // unused because we don't have username to work with
|
||||
};
|
||||
|
||||
// always send digest for backup failure notification
|
||||
debug('maybeSend: sending digest email', info);
|
||||
mailer.sendDigest(info);
|
||||
|
||||
callback();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+15
-32
@@ -11,7 +11,7 @@ exports = module.exports = {
|
||||
var assert = require('assert'),
|
||||
config = require('../config.js'),
|
||||
debug = require('debug')('box:dns/caas'),
|
||||
DomainsError = require('../domains.js').DomainsError,
|
||||
DomainError = require('../domains.js').DomainError,
|
||||
superagent = require('superagent'),
|
||||
util = require('util');
|
||||
|
||||
@@ -45,12 +45,12 @@ function add(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
.send(data)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
|
||||
if (result.statusCode === 400) return callback(new DomainsError(DomainsError.BAD_FIELD, result.body.message));
|
||||
if (result.statusCode === 420) return callback(new DomainsError(DomainsError.STILL_BUSY));
|
||||
if (result.statusCode !== 201) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
|
||||
if (error && !error.response) return callback(new DomainError(DomainError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
|
||||
if (result.statusCode === 400) return callback(new DomainError(DomainError.BAD_FIELD, result.body.message));
|
||||
if (result.statusCode === 420) return callback(new DomainError(DomainError.STILL_BUSY));
|
||||
if (result.statusCode !== 201) return callback(new DomainError(DomainError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
|
||||
|
||||
return callback(null);
|
||||
return callback(null, result.body.changeId);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -70,8 +70,8 @@ function get(dnsConfig, zoneName, subdomain, type, callback) {
|
||||
.query({ token: dnsConfig.token, type: type })
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
|
||||
if (result.statusCode !== 200) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
|
||||
if (error && !error.response) return callback(new DomainError(DomainError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
|
||||
if (result.statusCode !== 200) return callback(new DomainError(DomainError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
|
||||
|
||||
return callback(null, result.body.values);
|
||||
});
|
||||
@@ -109,11 +109,11 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
.send(data)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
|
||||
if (result.statusCode === 400) return callback(new DomainsError(DomainsError.BAD_FIELD, result.body.message));
|
||||
if (result.statusCode === 420) return callback(new DomainsError(DomainsError.STILL_BUSY));
|
||||
if (result.statusCode === 404) return callback(new DomainsError(DomainsError.NOT_FOUND));
|
||||
if (result.statusCode !== 204) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
|
||||
if (error && !error.response) return callback(new DomainError(DomainError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
|
||||
if (result.statusCode === 400) return callback(new DomainError(DomainError.BAD_FIELD, result.body.message));
|
||||
if (result.statusCode === 420) return callback(new DomainError(DomainError.STILL_BUSY));
|
||||
if (result.statusCode === 404) return callback(new DomainError(DomainError.NOT_FOUND));
|
||||
if (result.statusCode !== 204) return callback(new DomainError(DomainError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
@@ -126,27 +126,10 @@ function verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback) {
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!dnsConfig.token || typeof dnsConfig.token !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'token must be a non-empty string'));
|
||||
|
||||
var credentials = {
|
||||
token: dnsConfig.token,
|
||||
fqdn: domain,
|
||||
hyphenatedSubdomains: true // this will ensure we always use them, regardless of passed-in configs
|
||||
fqdn: domain
|
||||
};
|
||||
|
||||
const testSubdomain = 'cloudrontestdns';
|
||||
|
||||
upsert(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error, changeId) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('verifyDnsConfig: Test A record added with change id %s', changeId);
|
||||
|
||||
del(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('verifyDnsConfig: Test A record removed again');
|
||||
|
||||
callback(null, credentials);
|
||||
});
|
||||
});
|
||||
return callback(null, credentials);
|
||||
}
|
||||
|
||||
+69
-67
@@ -11,8 +11,8 @@ exports = module.exports = {
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
debug = require('debug')('box:dns/cloudflare'),
|
||||
dns = require('../native-dns.js'),
|
||||
DomainsError = require('../domains.js').DomainsError,
|
||||
dns = require('dns'),
|
||||
DomainError = require('../domains.js').DomainError,
|
||||
superagent = require('superagent'),
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
@@ -24,20 +24,20 @@ function translateRequestError(result, callback) {
|
||||
assert.strictEqual(typeof result, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (result.statusCode === 404) return callback(new DomainsError(DomainsError.NOT_FOUND, util.format('%s %j', result.statusCode, 'API does not exist')));
|
||||
if (result.statusCode === 422) return callback(new DomainsError(DomainsError.BAD_FIELD, result.body.message));
|
||||
if (result.statusCode === 404) return callback(new DomainError(DomainError.NOT_FOUND, util.format('%s %j', result.statusCode, 'API does not exist')));
|
||||
if (result.statusCode === 422) return callback(new DomainError(DomainError.BAD_FIELD, result.body.message));
|
||||
if ((result.statusCode === 400 || result.statusCode === 401 || result.statusCode === 403) && result.body.errors.length > 0) {
|
||||
let error = result.body.errors[0];
|
||||
let message = `message: ${error.message} statusCode: ${result.statusCode} code:${error.code}`;
|
||||
let message = error.message;
|
||||
if (error.code === 6003) {
|
||||
if (error.error_chain[0] && error.error_chain[0].code === 6103) message = 'Invalid API Key';
|
||||
else message = 'Invalid credentials';
|
||||
}
|
||||
|
||||
return callback(new DomainsError(DomainsError.ACCESS_DENIED, message));
|
||||
return callback(new DomainError(DomainError.ACCESS_DENIED, message));
|
||||
}
|
||||
|
||||
callback(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
|
||||
callback(new DomainError(DomainError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
|
||||
}
|
||||
|
||||
function getZoneByName(dnsConfig, zoneName, callback) {
|
||||
@@ -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 DomainsError(DomainsError.NOT_FOUND, util.format('%s %j', result.statusCode, result.body)));
|
||||
.set('X-Auth-Key', dnsConfig.token)
|
||||
.set('X-Auth-Email', dnsConfig.email)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(error);
|
||||
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
|
||||
if (!result.body.result.length) return callback(new DomainError(DomainError.NOT_FOUND, util.format('%s %j', result.statusCode, result.body)));
|
||||
|
||||
callback(null, result.body.result[0]);
|
||||
});
|
||||
callback(null, result.body.result[0]);
|
||||
});
|
||||
}
|
||||
|
||||
function getDnsRecordsByZoneId(dnsConfig, zoneId, zoneName, subdomain, type, callback) {
|
||||
function getDNSRecordsByZoneId(dnsConfig, zoneId, zoneName, subdomain, type, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneId, 'string');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
@@ -69,18 +69,18 @@ function getDnsRecordsByZoneId(dnsConfig, zoneId, zoneName, subdomain, type, cal
|
||||
var fqdn = subdomain === '' ? zoneName : subdomain + '.' + zoneName;
|
||||
|
||||
superagent.get(CLOUDFLARE_ENDPOINT + '/zones/' + zoneId + '/dns_records')
|
||||
.set('X-Auth-Key',dnsConfig.token)
|
||||
.set('X-Auth-Email',dnsConfig.email)
|
||||
.query({ type: type, name: fqdn })
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(error);
|
||||
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
|
||||
.set('X-Auth-Key',dnsConfig.token)
|
||||
.set('X-Auth-Email',dnsConfig.email)
|
||||
.query({ type: type, name: fqdn })
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(error);
|
||||
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
|
||||
|
||||
var tmp = result.body.result;
|
||||
var tmp = result.body.result;
|
||||
|
||||
return callback(null, tmp);
|
||||
});
|
||||
return callback(null, tmp);
|
||||
});
|
||||
}
|
||||
|
||||
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
@@ -100,7 +100,7 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
|
||||
var zoneId = result.id;
|
||||
|
||||
getDnsRecordsByZoneId(dnsConfig, zoneId, zoneName, subdomain, type, function (error, result) {
|
||||
getDNSRecordsByZoneId(dnsConfig, zoneId, zoneName, subdomain, type, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var dnsRecords = result;
|
||||
@@ -126,33 +126,37 @@ 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);
|
||||
});
|
||||
}
|
||||
}, callback);
|
||||
}, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, 'unused');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -167,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; });
|
||||
@@ -189,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);
|
||||
|
||||
@@ -202,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);
|
||||
.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);
|
||||
|
||||
debug('del: done');
|
||||
debug('del: done');
|
||||
|
||||
callback(null);
|
||||
});
|
||||
callback(null);
|
||||
});
|
||||
}, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
@@ -229,28 +233,26 @@ 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 DomainsError(DomainsError.BAD_FIELD, 'token must be a non-empty string'));
|
||||
if (!dnsConfig.email || typeof dnsConfig.email !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'email must be a non-empty string'));
|
||||
if ('hyphenatedSubdomains' in dnsConfig && typeof dnsConfig.hyphenatedSubdomains !== 'boolean') return callback(new DomainsError(DomainsError.BAD_FIELD, 'hyphenatedSubdomains must be a boolean'));
|
||||
if (!dnsConfig.token || typeof dnsConfig.token !== 'string') return callback(new DomainError(DomainError.BAD_FIELD, 'token must be a non-empty string'));
|
||||
if (!dnsConfig.email || typeof dnsConfig.email !== 'string') return callback(new DomainError(DomainError.BAD_FIELD, 'email must be a non-empty string'));
|
||||
|
||||
var credentials = {
|
||||
token: dnsConfig.token,
|
||||
email: dnsConfig.email,
|
||||
hyphenatedSubdomains: !!dnsConfig.hyphenatedSubdomains
|
||||
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 DomainsError(DomainsError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
|
||||
if (error || !nameservers) return callback(new DomainsError(DomainsError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
|
||||
dns.resolveNs(zoneName, function (error, nameservers) {
|
||||
if (error && error.code === 'ENOTFOUND') return callback(new DomainError(DomainError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
|
||||
if (error || !nameservers) return callback(new DomainError(DomainError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
|
||||
|
||||
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 DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to Cloudflare'));
|
||||
return callback(new DomainError(DomainError.BAD_FIELD, 'Domain nameservers are not set to Cloudflare'));
|
||||
}
|
||||
|
||||
const testSubdomain = 'cloudrontestdns';
|
||||
|
||||
+27
-27
@@ -11,8 +11,8 @@ exports = module.exports = {
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
debug = require('debug')('box:dns/digitalocean'),
|
||||
dns = require('../native-dns.js'),
|
||||
DomainsError = require('../domains.js').DomainsError,
|
||||
dns = require('dns'),
|
||||
DomainError = require('../domains.js').DomainError,
|
||||
safe = require('safetydance'),
|
||||
superagent = require('superagent'),
|
||||
util = require('util');
|
||||
@@ -39,10 +39,10 @@ function getInternal(dnsConfig, zoneName, subdomain, type, callback) {
|
||||
.set('Authorization', 'Bearer ' + dnsConfig.token)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
|
||||
if (result.statusCode === 404) return callback(new DomainsError(DomainsError.NOT_FOUND, formatError(result)));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new DomainsError(DomainsError.ACCESS_DENIED, formatError(result)));
|
||||
if (result.statusCode !== 200) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
|
||||
if (error && !error.response) return callback(new DomainError(DomainError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
|
||||
if (result.statusCode === 404) return callback(new DomainError(DomainError.NOT_FOUND, formatError(result)));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new DomainError(DomainError.ACCESS_DENIED, formatError(result)));
|
||||
if (result.statusCode !== 200) return callback(new DomainError(DomainError.EXTERNAL_ERROR, formatError(result)));
|
||||
|
||||
matchingRecords = matchingRecords.concat(result.body.domain_records.filter(function (record) {
|
||||
return (record.type === type && record.name === subdomain);
|
||||
@@ -101,10 +101,10 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
.send(data)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return iteratorCallback(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return iteratorCallback(new DomainsError(DomainsError.ACCESS_DENIED, formatError(result)));
|
||||
if (result.statusCode === 422) return iteratorCallback(new DomainsError(DomainsError.BAD_FIELD, result.body.message));
|
||||
if (result.statusCode !== 201) return iteratorCallback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
|
||||
if (error && !error.response) return iteratorCallback(new DomainError(DomainError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return iteratorCallback(new DomainError(DomainError.ACCESS_DENIED, formatError(result)));
|
||||
if (result.statusCode === 422) return iteratorCallback(new DomainError(DomainError.BAD_FIELD, result.body.message));
|
||||
if (result.statusCode !== 201) return iteratorCallback(new DomainError(DomainError.EXTERNAL_ERROR, formatError(result)));
|
||||
|
||||
recordIds.push(safe.query(result.body, 'domain_record.id'));
|
||||
|
||||
@@ -119,17 +119,21 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
// increment, as we have consumed the record
|
||||
++i;
|
||||
|
||||
if (error && !error.response) return iteratorCallback(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return iteratorCallback(new DomainsError(DomainsError.ACCESS_DENIED, formatError(result)));
|
||||
if (result.statusCode === 422) return iteratorCallback(new DomainsError(DomainsError.BAD_FIELD, result.body.message));
|
||||
if (result.statusCode !== 200) return iteratorCallback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
|
||||
if (error && !error.response) return iteratorCallback(new DomainError(DomainError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return iteratorCallback(new DomainError(DomainError.ACCESS_DENIED, formatError(result)));
|
||||
if (result.statusCode === 422) return iteratorCallback(new DomainError(DomainError.BAD_FIELD, result.body.message));
|
||||
if (result.statusCode !== 200) return iteratorCallback(new DomainError(DomainError.EXTERNAL_ERROR, formatError(result)));
|
||||
|
||||
recordIds.push(safe.query(result.body, 'domain_record.id'));
|
||||
|
||||
return iteratorCallback(null);
|
||||
});
|
||||
}
|
||||
}, callback);
|
||||
}, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, '' + recordIds[0]); // DO ids are integers
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -181,10 +185,10 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
.set('Authorization', 'Bearer ' + dnsConfig.token)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
|
||||
if (error && !error.response) return callback(new DomainError(DomainError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
|
||||
if (result.statusCode === 404) return callback(null);
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new DomainsError(DomainsError.ACCESS_DENIED, formatError(result)));
|
||||
if (result.statusCode !== 204) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new DomainError(DomainError.ACCESS_DENIED, formatError(result)));
|
||||
if (result.statusCode !== 204) return callback(new DomainError(DomainError.EXTERNAL_ERROR, formatError(result)));
|
||||
|
||||
debug('del: done');
|
||||
|
||||
@@ -200,23 +204,19 @@ 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 DomainsError(DomainsError.BAD_FIELD, 'token must be a non-empty string'));
|
||||
if ('hyphenatedSubdomains' in dnsConfig && typeof dnsConfig.hyphenatedSubdomains !== 'boolean') return callback(new DomainsError(DomainsError.BAD_FIELD, 'hyphenatedSubdomains must be a boolean'));
|
||||
|
||||
var credentials = {
|
||||
token: dnsConfig.token,
|
||||
hyphenatedSubdomains: !!dnsConfig.hyphenatedSubdomains
|
||||
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 DomainsError(DomainsError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
|
||||
if (error || !nameservers) return callback(new DomainsError(DomainsError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
|
||||
dns.resolveNs(zoneName, function (error, nameservers) {
|
||||
if (error && error.code === 'ENOTFOUND') return callback(new DomainError(DomainError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
|
||||
if (error || !nameservers) return callback(new DomainError(DomainError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
|
||||
|
||||
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 DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to Digital Ocean'));
|
||||
return callback(new DomainError(DomainError.BAD_FIELD, 'Domain nameservers are not set to Digital Ocean'));
|
||||
}
|
||||
|
||||
const testSubdomain = 'cloudrontestdns';
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
upsert: upsert,
|
||||
get: get,
|
||||
del: del,
|
||||
waitForDns: require('./waitfordns.js'),
|
||||
verifyDnsConfig: verifyDnsConfig
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
debug = require('debug')('box:dns/gandi'),
|
||||
dns = require('../native-dns.js'),
|
||||
DomainsError = require('../domains.js').DomainsError,
|
||||
superagent = require('superagent'),
|
||||
util = require('util');
|
||||
|
||||
var GANDI_API = 'https://dns.api.gandi.net/api/v5';
|
||||
|
||||
function formatError(response) {
|
||||
return util.format(`Gandi DNS error [${response.statusCode}] ${response.body.message}`);
|
||||
}
|
||||
|
||||
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert(util.isArray(values));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
subdomain = subdomain || '@';
|
||||
|
||||
debug(`upsert: ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
|
||||
|
||||
var data = {
|
||||
'rrset_ttl': 300, // this is the minimum allowed
|
||||
'rrset_values': values // for mx records, value is already of the '<priority> <server>' format
|
||||
};
|
||||
|
||||
superagent.put(`${GANDI_API}/domains/${zoneName}/records/${subdomain}/${type}`)
|
||||
.set('X-Api-Key', dnsConfig.token)
|
||||
.timeout(30 * 1000)
|
||||
.send(data)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new DomainsError(DomainsError.ACCESS_DENIED, formatError(result)));
|
||||
if (result.statusCode === 400) return callback(new DomainsError(DomainsError.BAD_FIELD, formatError(result)));
|
||||
if (result.statusCode !== 201) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function get(dnsConfig, zoneName, subdomain, type, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
subdomain = subdomain || '@';
|
||||
|
||||
debug(`get: ${subdomain} in zone ${zoneName} of type ${type}`);
|
||||
|
||||
superagent.get(`${GANDI_API}/domains/${zoneName}/records/${subdomain}/${type}`)
|
||||
.set('X-Api-Key', dnsConfig.token)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new DomainsError(DomainsError.ACCESS_DENIED, formatError(result)));
|
||||
if (result.statusCode === 404) return callback(null, [ ]);
|
||||
if (result.statusCode !== 200) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
|
||||
|
||||
debug('get: %j', result.body);
|
||||
|
||||
return callback(null, result.body.rrset_values);
|
||||
});
|
||||
}
|
||||
|
||||
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert(util.isArray(values));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
subdomain = subdomain || '@';
|
||||
|
||||
debug(`del: ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
|
||||
|
||||
superagent.del(`${GANDI_API}/domains/${zoneName}/records/${subdomain}/${type}`)
|
||||
.set('X-Api-Key', dnsConfig.token)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new DomainsError(DomainsError.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 DomainsError(DomainsError.ACCESS_DENIED, formatError(result)));
|
||||
if (result.statusCode !== 204) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
|
||||
|
||||
debug('del: done');
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof fqdn, 'string');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!dnsConfig.token || typeof dnsConfig.token !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'token must be a non-empty string'));
|
||||
if ('hyphenatedSubdomains' in dnsConfig && typeof dnsConfig.hyphenatedSubdomains !== 'boolean') return callback(new DomainsError(DomainsError.BAD_FIELD, 'hyphenatedSubdomains must be a boolean'));
|
||||
|
||||
var credentials = {
|
||||
token: dnsConfig.token,
|
||||
hyphenatedSubdomains: !!dnsConfig.hyphenatedSubdomains
|
||||
};
|
||||
|
||||
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 DomainsError(DomainsError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
|
||||
if (error || !nameservers) return callback(new DomainsError(DomainsError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
|
||||
|
||||
if (!nameservers.every(function (n) { return n.toLowerCase().indexOf('.gandi.net') !== -1; })) {
|
||||
debug('verifyDnsConfig: %j does not contain Gandi NS', nameservers);
|
||||
return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to Gandi'));
|
||||
}
|
||||
|
||||
const testSubdomain = 'cloudrontestdns';
|
||||
|
||||
upsert(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error, changeId) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('verifyDnsConfig: Test 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
+38
-38
@@ -9,9 +9,10 @@ exports = module.exports = {
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
config = require('../config.js'),
|
||||
debug = require('debug')('box:dns/gcdns'),
|
||||
dns = require('../native-dns.js'),
|
||||
DomainsError = require('../domains.js').DomainsError,
|
||||
dns = require('dns'),
|
||||
DomainError = require('../domains.js').DomainError,
|
||||
GCDNS = require('@google-cloud/dns'),
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
@@ -19,14 +20,19 @@ var assert = require('assert'),
|
||||
function getDnsCredentials(dnsConfig) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
|
||||
return {
|
||||
var config = {
|
||||
projectId: dnsConfig.projectId,
|
||||
credentials: {
|
||||
keyFilename: dnsConfig.keyFilename,
|
||||
email: dnsConfig.email
|
||||
};
|
||||
|
||||
if (dnsConfig.credentials) {
|
||||
config.credentials = {
|
||||
client_email: dnsConfig.credentials.client_email,
|
||||
private_key: dnsConfig.credentials.private_key
|
||||
},
|
||||
hyphenatedSubdomains: !!dnsConfig.hyphenatedSubdomains
|
||||
};
|
||||
};
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
function getZoneByName(dnsConfig, zoneName, callback) {
|
||||
@@ -37,20 +43,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 DomainsError(DomainsError.ACCESS_DENIED, 'The key was probably revoked'));
|
||||
if (error && error.reason === 'No such domain') return callback(new DomainsError(DomainsError.NOT_FOUND, error.message));
|
||||
if (error && error.code === 403) return callback(new DomainsError(DomainsError.ACCESS_DENIED, error.message));
|
||||
if (error && error.code === 404) return callback(new DomainsError(DomainsError.NOT_FOUND, error.message));
|
||||
if (error && error.message === 'invalid_grant') return callback(new DomainError(DomainError.ACCESS_DENIED, 'The key was probably revoked'));
|
||||
if (error && error.reason === 'No such domain') return callback(new DomainError(DomainError.NOT_FOUND, error.message));
|
||||
if (error && error.code === 403) return callback(new DomainError(DomainError.ACCESS_DENIED, error.message));
|
||||
if (error && error.code === 404) return callback(new DomainError(DomainError.NOT_FOUND, error.message));
|
||||
if (error) {
|
||||
debug('gcdns.getZones', error);
|
||||
return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error));
|
||||
return callback(new DomainError(DomainError.EXTERNAL_ERROR, error));
|
||||
}
|
||||
|
||||
var zone = zones.filter(function (zone) {
|
||||
return zone.metadata.dnsName.slice(0, -1) === zoneName; // the zone name contains a '.' at the end
|
||||
})[0];
|
||||
|
||||
if (!zone) return callback(new DomainsError(DomainsError.NOT_FOUND, 'no such zone'));
|
||||
if (!zone) return callback(new DomainError(DomainError.NOT_FOUND, 'no such zone'));
|
||||
|
||||
callback(null, zone); //zone.metadata ~= {name="", dnsName="", nameServers:[]}
|
||||
});
|
||||
@@ -72,10 +78,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 DomainsError(DomainsError.ACCESS_DENIED, error.message));
|
||||
if (error && error.code === 403) return callback(new DomainError(DomainError.ACCESS_DENIED, error.message));
|
||||
if (error) {
|
||||
debug('upsert->zone.getRecords', error);
|
||||
return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error.message));
|
||||
return callback(new DomainError(DomainError.EXTERNAL_ERROR, error.message));
|
||||
}
|
||||
|
||||
var newRecord = zone.record(type, {
|
||||
@@ -85,14 +91,14 @@ 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 DomainsError(DomainsError.ACCESS_DENIED, error.message));
|
||||
if (error && error.code === 412) return callback(new DomainsError(DomainsError.STILL_BUSY, error.message));
|
||||
if (error && error.code === 403) return callback(new DomainError(DomainError.ACCESS_DENIED, error.message));
|
||||
if (error && error.code === 412) return callback(new DomainError(DomainError.STILL_BUSY, error.message));
|
||||
if (error) {
|
||||
debug('upsert->zone.createChange', error);
|
||||
return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error.message));
|
||||
return callback(new DomainError(DomainError.EXTERNAL_ERROR, error.message));
|
||||
}
|
||||
|
||||
callback(null);
|
||||
callback(null, change.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -114,8 +120,8 @@ function get(dnsConfig, zoneName, subdomain, type, callback) {
|
||||
};
|
||||
|
||||
zone.getRecords(params, function (error, records) {
|
||||
if (error && error.code === 403) return callback(new DomainsError(DomainsError.ACCESS_DENIED, error.message));
|
||||
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error));
|
||||
if (error && error.code === 403) return callback(new DomainError(DomainError.ACCESS_DENIED, error.message));
|
||||
if (error) return callback(new DomainError(DomainError.EXTERNAL_ERROR, error));
|
||||
if (records.length === 0) return callback(null, [ ]);
|
||||
|
||||
return callback(null, records[0].data);
|
||||
@@ -137,18 +143,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 DomainsError(DomainsError.ACCESS_DENIED, error.message));
|
||||
if (error && error.code === 403) return callback(new DomainError(DomainError.ACCESS_DENIED, error.message));
|
||||
if (error) {
|
||||
debug('del->zone.getRecords', error);
|
||||
return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error.message));
|
||||
return callback(new DomainError(DomainError.EXTERNAL_ERROR, error.message));
|
||||
}
|
||||
|
||||
zone.deleteRecords(oldRecords, function (error, change) {
|
||||
if (error && error.code === 403) return callback(new DomainsError(DomainsError.ACCESS_DENIED, error.message));
|
||||
if (error && error.code === 412) return callback(new DomainsError(DomainsError.STILL_BUSY, error.message));
|
||||
if (error && error.code === 403) return callback(new DomainError(DomainError.ACCESS_DENIED, error.message));
|
||||
if (error && error.code === 412) return callback(new DomainError(DomainError.STILL_BUSY, error.message));
|
||||
if (error) {
|
||||
debug('del->zone.createChange', error);
|
||||
return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error.message));
|
||||
return callback(new DomainError(DomainError.EXTERNAL_ERROR, error.message));
|
||||
}
|
||||
|
||||
callback(null, change.id);
|
||||
@@ -164,26 +170,20 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (typeof dnsConfig.projectId !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'projectId must be a string'));
|
||||
if (!dnsConfig.credentials || typeof dnsConfig.credentials !== 'object') return callback(new DomainsError(DomainsError.BAD_FIELD, 'credentials must be an object'));
|
||||
if (typeof dnsConfig.credentials.client_email !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'credentials.client_email must be a string'));
|
||||
if (typeof dnsConfig.credentials.private_key !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'credentials.private_key must be a string'));
|
||||
if ('hyphenatedSubdomains' in dnsConfig && typeof dnsConfig.hyphenatedSubdomains !== 'boolean') return callback(new DomainsError(DomainsError.BAD_FIELD, 'hyphenatedSubdomains must be a boolean'));
|
||||
|
||||
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 DomainsError(DomainsError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
|
||||
if (error || !nameservers) return callback(new DomainsError(DomainsError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
|
||||
dns.resolveNs(zoneName, function (error, resolvedNS) {
|
||||
if (error && error.code === 'ENOTFOUND') return callback(new DomainError(DomainError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
|
||||
if (error || !resolvedNS) return callback(new DomainError(DomainError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
|
||||
|
||||
getZoneByName(credentials, zoneName, function (error, zone) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var definedNS = zone.metadata.nameServers.sort().map(function(r) { return r.replace(/\.$/, ''); });
|
||||
if (!_.isEqual(definedNS, nameservers.sort())) {
|
||||
debug('verifyDnsConfig: %j and %j do not match', nameservers, definedNS);
|
||||
return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to Google Cloud DNS'));
|
||||
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'));
|
||||
}
|
||||
|
||||
const testSubdomain = 'cloudrontestdns';
|
||||
|
||||
@@ -1,186 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
upsert: upsert,
|
||||
get: get,
|
||||
del: del,
|
||||
waitForDns: require('./waitfordns.js'),
|
||||
verifyDnsConfig: verifyDnsConfig
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
debug = require('debug')('box:dns/godaddy'),
|
||||
dns = require('../native-dns.js'),
|
||||
DomainsError = require('../domains.js').DomainsError,
|
||||
superagent = require('superagent'),
|
||||
util = require('util');
|
||||
|
||||
// const GODADDY_API_OTE = 'https://api.ote-godaddy.com/v1/domains';
|
||||
const GODADDY_API = 'https://api.godaddy.com/v1/domains';
|
||||
|
||||
// this is a workaround for godaddy not having a delete API
|
||||
// https://stackoverflow.com/questions/39347464/delete-record-libcloud-godaddy-api
|
||||
const GODADDY_INVALID_IP = '0.0.0.0';
|
||||
|
||||
function formatError(response) {
|
||||
return util.format(`GoDaddy DNS error [${response.statusCode}] ${response.body.message}`);
|
||||
}
|
||||
|
||||
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert(util.isArray(values));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
subdomain = subdomain || '@';
|
||||
|
||||
debug(`upsert: ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
|
||||
|
||||
var records = [ ];
|
||||
values.forEach(function (value) {
|
||||
var record = { ttl: 600 }; // 600 is the min ttl
|
||||
|
||||
if (type === 'MX') {
|
||||
record.priority = parseInt(value.split(' ')[0], 10);
|
||||
record.data = value.split(' ')[1];
|
||||
} else {
|
||||
record.data = value;
|
||||
}
|
||||
|
||||
records.push(record);
|
||||
});
|
||||
|
||||
superagent.put(`${GODADDY_API}/${zoneName}/records/${type}/${subdomain}`)
|
||||
.set('Authorization', `sso-key ${dnsConfig.apiKey}:${dnsConfig.apiSecret}`)
|
||||
.timeout(30 * 1000)
|
||||
.send(records)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new DomainsError(DomainsError.ACCESS_DENIED, formatError(result)));
|
||||
if (result.statusCode === 400) return callback(new DomainsError(DomainsError.BAD_FIELD, formatError(result))); // no such zone
|
||||
if (result.statusCode === 422) return callback(new DomainsError(DomainsError.BAD_FIELD, formatError(result))); // conflict
|
||||
if (result.statusCode !== 200) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function get(dnsConfig, zoneName, subdomain, type, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
subdomain = subdomain || '@';
|
||||
|
||||
debug(`get: ${subdomain} in zone ${zoneName} of type ${type}`);
|
||||
|
||||
superagent.get(`${GODADDY_API}/${zoneName}/records/${type}/${subdomain}`)
|
||||
.set('Authorization', `sso-key ${dnsConfig.apiKey}:${dnsConfig.apiSecret}`)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new DomainsError(DomainsError.ACCESS_DENIED, formatError(result)));
|
||||
if (result.statusCode === 404) return callback(null, [ ]);
|
||||
if (result.statusCode !== 200) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
|
||||
|
||||
debug('get: %j', result.body);
|
||||
|
||||
var values = result.body.map(function (record) { return record.data; });
|
||||
|
||||
if (values.length === 1 && values[0] === GODADDY_INVALID_IP) return callback(null, [ ]); // pretend this record doesn't exist
|
||||
|
||||
return callback(null, values);
|
||||
});
|
||||
}
|
||||
|
||||
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert(util.isArray(values));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
subdomain = subdomain || '@';
|
||||
|
||||
debug(`get: ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
|
||||
|
||||
if (type !== 'A') return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, new Error('Not supported by GoDaddy API'))); // can never happen
|
||||
|
||||
// check if the record exists at all so that we don't insert the "Dead" record for no reason
|
||||
get(dnsConfig, zoneName, subdomain, type, function (error, values) {
|
||||
if (error) return callback(error);
|
||||
if (values.length === 0) return callback();
|
||||
|
||||
// godaddy does not have a delete API. so fill it up with an invalid IP that we can ignore in future get()
|
||||
var records = [{
|
||||
ttl: 600,
|
||||
data: GODADDY_INVALID_IP
|
||||
}];
|
||||
|
||||
superagent.put(`${GODADDY_API}/${zoneName}/records/${type}/${subdomain}`)
|
||||
.set('Authorization', `sso-key ${dnsConfig.apiKey}:${dnsConfig.apiSecret}`)
|
||||
.send(records)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new DomainsError(DomainsError.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 DomainsError(DomainsError.ACCESS_DENIED, formatError(result)));
|
||||
if (result.statusCode !== 200) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
|
||||
|
||||
debug('del: done');
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof fqdn, 'string');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!dnsConfig.apiKey || typeof dnsConfig.apiKey !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'apiKey must be a non-empty string'));
|
||||
if (!dnsConfig.apiSecret || typeof dnsConfig.apiSecret !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'apiSecret must be a non-empty string'));
|
||||
if ('hyphenatedSubdomains' in dnsConfig && typeof dnsConfig.hyphenatedSubdomains !== 'boolean') return callback(new DomainsError(DomainsError.BAD_FIELD, 'hyphenatedSubdomains must be a boolean'));
|
||||
|
||||
var credentials = {
|
||||
apiKey: dnsConfig.apiKey,
|
||||
apiSecret: dnsConfig.apiSecret,
|
||||
hyphenatedSubdomains: !!dnsConfig.hyphenatedSubdomains
|
||||
};
|
||||
|
||||
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 DomainsError(DomainsError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
|
||||
if (error || !nameservers) return callback(new DomainsError(DomainsError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
|
||||
|
||||
if (!nameservers.every(function (n) { return n.toLowerCase().indexOf('.domaincontrol.com') !== -1; })) {
|
||||
debug('verifyDnsConfig: %j does not contain GoDaddy NS', nameservers);
|
||||
return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to GoDaddy'));
|
||||
}
|
||||
|
||||
const testSubdomain = 'cloudrontestdns';
|
||||
|
||||
upsert(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error, changeId) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('verifyDnsConfig: Test 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -15,7 +15,7 @@ exports = module.exports = {
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
DomainsError = require('../domains.js').DomainsError,
|
||||
DomainError = require('../domains.js').DomainError,
|
||||
util = require('util');
|
||||
|
||||
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
@@ -26,7 +26,7 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
assert(util.isArray(values));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// Result: none
|
||||
// Result: backend specific change id, to be passed into getChangeStatus()
|
||||
|
||||
callback(new Error('not implemented'));
|
||||
}
|
||||
|
||||
+6
-15
@@ -10,8 +10,8 @@ exports = module.exports = {
|
||||
|
||||
var assert = require('assert'),
|
||||
debug = require('debug')('box:dns/manual'),
|
||||
dns = require('../native-dns.js'),
|
||||
DomainsError = require('../domains.js').DomainsError,
|
||||
dns = require('dns'),
|
||||
DomainError = require('../domains.js').DomainError,
|
||||
util = require('util');
|
||||
|
||||
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
@@ -24,7 +24,7 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
|
||||
debug('upsert: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
|
||||
|
||||
return callback(null);
|
||||
return callback(null, 'noop-record-id');
|
||||
}
|
||||
|
||||
function get(dnsConfig, zoneName, subdomain, type, callback) {
|
||||
@@ -55,19 +55,10 @@ function verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback) {
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if ('wildcard' in dnsConfig && typeof dnsConfig.wildcard !== 'boolean') return callback(new DomainsError(DomainsError.BAD_FIELD, 'wildcard must be a boolean'));
|
||||
if ('hyphenatedSubdomains' in dnsConfig && typeof dnsConfig.hyphenatedSubdomains !== 'boolean') return callback(new DomainsError(DomainsError.BAD_FIELD, 'hyphenatedSubdomains must be a boolean'));
|
||||
|
||||
var config = {
|
||||
wildcard: !!dnsConfig.wildcard,
|
||||
hyphenatedSubdomains: !!dnsConfig.hyphenatedSubdomains
|
||||
};
|
||||
|
||||
// Very basic check if the nameservers can be fetched
|
||||
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
|
||||
if (error && error.code === 'ENOTFOUND') return callback(new DomainsError(DomainsError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
|
||||
if (error || !nameservers) return callback(new DomainsError(DomainsError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
|
||||
dns.resolveNs(zoneName, function (error, nameservers) {
|
||||
if (error || !nameservers) return callback(new DomainError(DomainError.BAD_FIELD, 'Unable to get nameservers'));
|
||||
|
||||
callback(null, config);
|
||||
callback(null, { wildcard: !!dnsConfig.wildcard });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,248 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
upsert: upsert,
|
||||
get: get,
|
||||
del: del,
|
||||
waitForDns: require('./waitfordns.js'),
|
||||
verifyDnsConfig: verifyDnsConfig
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
debug = require('debug')('box:dns/namecom'),
|
||||
dns = require('../native-dns.js'),
|
||||
safe = require('safetydance'),
|
||||
DomainsError = require('../domains.js').DomainsError,
|
||||
superagent = require('superagent');
|
||||
|
||||
const NAMECOM_API = 'https://api.name.com/v4';
|
||||
|
||||
function formatError(response) {
|
||||
return `Name.com DNS error [${response.statusCode}] ${response.text}`;
|
||||
}
|
||||
|
||||
function addRecord(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert(Array.isArray(values));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug(`add: ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
|
||||
|
||||
var data = {
|
||||
host: subdomain,
|
||||
type: type,
|
||||
ttl: 300 // 300 is the lowest
|
||||
};
|
||||
|
||||
if (type === 'MX') {
|
||||
data.priority = parseInt(values[0].split(' ')[0], 10);
|
||||
data.answer = values[0].split(' ')[1];
|
||||
} else {
|
||||
data.answer = values[0];
|
||||
}
|
||||
|
||||
superagent.post(`${NAMECOM_API}/domains/${zoneName}/records`)
|
||||
.auth(dnsConfig.username, dnsConfig.token)
|
||||
.timeout(30 * 1000)
|
||||
.send(data)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, `Network error ${error.message}`));
|
||||
if (result.statusCode === 403) return callback(new DomainsError(DomainsError.ACCESS_DENIED, formatError(result)));
|
||||
if (result.statusCode !== 200) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
|
||||
|
||||
return callback(null, 'unused-id');
|
||||
});
|
||||
}
|
||||
|
||||
function updateRecord(dnsConfig, zoneName, recordId, subdomain, type, values, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof recordId, 'number');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert(Array.isArray(values));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug(`update:${recordId} on ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
|
||||
|
||||
var data = {
|
||||
host: subdomain,
|
||||
type: type,
|
||||
ttl: 300 // 300 is the lowest
|
||||
};
|
||||
|
||||
if (type === 'MX') {
|
||||
data.priority = parseInt(values[0].split(' ')[0], 10);
|
||||
data.answer = values[0].split(' ')[1];
|
||||
} else {
|
||||
data.answer = values[0];
|
||||
}
|
||||
|
||||
superagent.put(`${NAMECOM_API}/domains/${zoneName}/records/${recordId}`)
|
||||
.auth(dnsConfig.username, dnsConfig.token)
|
||||
.timeout(30 * 1000)
|
||||
.send(data)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, `Network error ${error.message}`));
|
||||
if (result.statusCode === 403) return callback(new DomainsError(DomainsError.ACCESS_DENIED, formatError(result)));
|
||||
if (result.statusCode !== 200) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function getInternal(dnsConfig, zoneName, subdomain, type, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
subdomain = subdomain || '@';
|
||||
|
||||
debug(`getInternal: ${subdomain} in zone ${zoneName} of type ${type}`);
|
||||
|
||||
superagent.get(`${NAMECOM_API}/domains/${zoneName}/records`)
|
||||
.auth(dnsConfig.username, dnsConfig.token)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, `Network error ${error.message}`));
|
||||
if (result.statusCode === 403) return callback(new DomainsError(DomainsError.ACCESS_DENIED, formatError(result)));
|
||||
if (result.statusCode !== 200) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
|
||||
|
||||
// name.com does not return the correct content-type
|
||||
result.body = safe.JSON.parse(result.text);
|
||||
if (!result.body.records) result.body.records = [];
|
||||
|
||||
result.body.records.forEach(function (r) {
|
||||
// name.com api simply strips empty properties
|
||||
r.host = r.host || '@';
|
||||
});
|
||||
|
||||
var results = result.body.records.filter(function (r) {
|
||||
return (r.host === subdomain && r.type === type);
|
||||
});
|
||||
|
||||
debug('getInternal: %j', results);
|
||||
|
||||
return callback(null, results);
|
||||
});
|
||||
}
|
||||
|
||||
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert(Array.isArray(values));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
subdomain = subdomain || '@';
|
||||
|
||||
debug(`upsert: ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
|
||||
|
||||
getInternal(dnsConfig, zoneName, subdomain, type, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (result.length === 0) return addRecord(dnsConfig, zoneName, subdomain, type, values, callback);
|
||||
|
||||
return updateRecord(dnsConfig, zoneName, result[0].id, subdomain, type, values, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function get(dnsConfig, zoneName, subdomain, type, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getInternal(dnsConfig, zoneName, subdomain, type, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var tmp = result.map(function (record) { return record.answer; });
|
||||
|
||||
debug('get: %j', tmp);
|
||||
|
||||
return callback(null, tmp);
|
||||
});
|
||||
}
|
||||
|
||||
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert(Array.isArray(values));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
subdomain = subdomain || '@';
|
||||
|
||||
debug(`del: ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
|
||||
|
||||
getInternal(dnsConfig, zoneName, subdomain, type, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (result.length === 0) return callback();
|
||||
|
||||
superagent.del(`${NAMECOM_API}/domains/${zoneName}/records/${result[0].id}`)
|
||||
.auth(dnsConfig.username, dnsConfig.token)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, `Network error ${error.message}`));
|
||||
if (result.statusCode === 403) return callback(new DomainsError(DomainsError.ACCESS_DENIED, formatError(result)));
|
||||
if (result.statusCode !== 200) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof fqdn, 'string');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (typeof dnsConfig.username !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'username must be a string'));
|
||||
if (typeof dnsConfig.token !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'token must be a string'));
|
||||
if ('hyphenatedSubdomains' in dnsConfig && typeof dnsConfig.hyphenatedSubdomains !== 'boolean') return callback(new DomainsError(DomainsError.BAD_FIELD, 'hyphenatedSubdomains must be a boolean'));
|
||||
|
||||
var credentials = {
|
||||
username: dnsConfig.username,
|
||||
token: dnsConfig.token,
|
||||
hyphenatedSubdomains: !!dnsConfig.hyphenatedSubdomains
|
||||
};
|
||||
|
||||
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 DomainsError(DomainsError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
|
||||
if (error || !nameservers) return callback(new DomainsError(DomainsError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
|
||||
|
||||
if (!nameservers.every(function (n) { return n.toLowerCase().indexOf('.name.com') !== -1; })) {
|
||||
debug('verifyDnsConfig: %j does not contain Name.com NS', nameservers);
|
||||
return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to Name.com'));
|
||||
}
|
||||
|
||||
const testSubdomain = 'cloudrontestdns';
|
||||
|
||||
upsert(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error, changeId) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('verifyDnsConfig: Test 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
+4
-3
@@ -22,7 +22,7 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
|
||||
debug('upsert: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
|
||||
|
||||
return callback(null);
|
||||
return callback(null, 'noop-record-id');
|
||||
}
|
||||
|
||||
function get(dnsConfig, zoneName, subdomain, type, callback) {
|
||||
@@ -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');
|
||||
|
||||
|
||||
+36
-50
@@ -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'),
|
||||
DomainsError = require('../domains.js').DomainsError,
|
||||
dns = require('dns'),
|
||||
DomainError = require('../domains.js').DomainError,
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
|
||||
@@ -39,25 +40,16 @@ function getZoneByName(dnsConfig, zoneName, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
|
||||
|
||||
// backward compat for 2.2, where we only required access to "listHostedZones"
|
||||
let listHostedZones;
|
||||
if (dnsConfig.listHostedZonesByName) {
|
||||
listHostedZones = route53.listHostedZonesByName.bind(route53, { MaxItems: '1', DNSName: zoneName + '.' });
|
||||
} else {
|
||||
listHostedZones = route53.listHostedZones.bind(route53, {}); // currently, this route does not support > 100 zones
|
||||
}
|
||||
|
||||
listHostedZones(function (error, result) {
|
||||
if (error && error.code === 'AccessDenied') return callback(new DomainsError(DomainsError.ACCESS_DENIED, error.message));
|
||||
if (error && error.code === 'InvalidClientTokenId') return callback(new DomainsError(DomainsError.ACCESS_DENIED, error.message));
|
||||
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error.message));
|
||||
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));
|
||||
|
||||
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 DomainsError(DomainsError.NOT_FOUND, 'no such zone'));
|
||||
if (!zone) return callback(new DomainError(DomainError.NOT_FOUND, 'no such zone'));
|
||||
|
||||
callback(null, zone);
|
||||
});
|
||||
@@ -73,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 DomainsError(DomainsError.ACCESS_DENIED, error.message));
|
||||
if (error && error.code === 'InvalidClientTokenId') return callback(new DomainsError(DomainsError.ACCESS_DENIED, error.message));
|
||||
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error.message));
|
||||
if (error && error.code === 'AccessDenied') return callback(new DomainError(DomainError.ACCESS_DENIED, error.message));
|
||||
if (error && error.code === 'InvalidClientTokenId') return callback(new DomainError(DomainError.ACCESS_DENIED, error.message));
|
||||
if (error) return callback(new DomainError(DomainError.EXTERNAL_ERROR, error.message));
|
||||
|
||||
callback(null, result);
|
||||
});
|
||||
@@ -96,7 +88,7 @@ function add(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var fqdn = subdomain === '' ? zoneName : subdomain + '.' + zoneName;
|
||||
var records = values.map(function (v) { return { Value: v }; }); // for mx records, value is already of the '<priority> <server>' format
|
||||
var records = values.map(function (v) { return { Value: v }; });
|
||||
|
||||
var params = {
|
||||
ChangeBatch: {
|
||||
@@ -114,14 +106,14 @@ function add(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 DomainsError(DomainsError.ACCESS_DENIED, error.message));
|
||||
if (error && error.code === 'InvalidClientTokenId') return callback(new DomainsError(DomainsError.ACCESS_DENIED, error.message));
|
||||
if (error && error.code === 'PriorRequestNotComplete') return callback(new DomainsError(DomainsError.STILL_BUSY, error.message));
|
||||
if (error && error.code === 'InvalidChangeBatch') return callback(new DomainsError(DomainsError.BAD_FIELD, error.message));
|
||||
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error.message));
|
||||
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));
|
||||
|
||||
callback(null);
|
||||
callback(null, result.ChangeInfo.Id);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -156,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 DomainsError(DomainsError.ACCESS_DENIED, error.message));
|
||||
if (error && error.code === 'InvalidClientTokenId') return callback(new DomainsError(DomainsError.ACCESS_DENIED, error.message));
|
||||
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error.message));
|
||||
if (error && error.code === 'AccessDenied') return callback(new DomainError(DomainError.ACCESS_DENIED, error.message));
|
||||
if (error && error.code === 'InvalidClientTokenId') return callback(new DomainError(DomainError.ACCESS_DENIED, error.message));
|
||||
if (error) return callback(new DomainError(DomainError.EXTERNAL_ERROR, error.message));
|
||||
if (result.ResourceRecordSets.length === 0) return callback(null, [ ]);
|
||||
if (result.ResourceRecordSets[0].Name !== params.StartRecordName || result.ResourceRecordSets[0].Type !== params.StartRecordType) return callback(null, [ ]);
|
||||
|
||||
@@ -201,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 DomainsError(DomainsError.ACCESS_DENIED, error.message));
|
||||
if (error && error.code === 'InvalidClientTokenId') return callback(new DomainsError(DomainsError.ACCESS_DENIED, error.message));
|
||||
route53.changeResourceRecordSets(params, function(error, result) {
|
||||
if (error && error.code === 'AccessDenied') return callback(new DomainError(DomainError.ACCESS_DENIED, error.message));
|
||||
if (error && error.code === 'InvalidClientTokenId') return callback(new DomainError(DomainError.ACCESS_DENIED, error.message));
|
||||
if (error && error.message && error.message.indexOf('it was not found') !== -1) {
|
||||
debug('del: resource record set not found.', error);
|
||||
return callback(new DomainsError(DomainsError.NOT_FOUND, error.message));
|
||||
return callback(new DomainError(DomainError.NOT_FOUND, error.message));
|
||||
} else if (error && error.code === 'NoSuchHostedZone') {
|
||||
debug('del: hosted zone not found.', error);
|
||||
return callback(new DomainsError(DomainsError.NOT_FOUND, error.message));
|
||||
return callback(new DomainError(DomainError.NOT_FOUND, error.message));
|
||||
} else if (error && error.code === 'PriorRequestNotComplete') {
|
||||
debug('del: resource is still busy', error);
|
||||
return callback(new DomainsError(DomainsError.STILL_BUSY, error.message));
|
||||
return callback(new DomainError(DomainError.STILL_BUSY, error.message));
|
||||
} else if (error && error.code === 'InvalidChangeBatch') {
|
||||
debug('del: invalid change batch. No such record to be deleted.');
|
||||
return callback(new DomainsError(DomainsError.NOT_FOUND, error.message));
|
||||
return callback(new DomainError(DomainError.NOT_FOUND, error.message));
|
||||
} else if (error) {
|
||||
debug('del: error', error);
|
||||
return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error.message));
|
||||
return callback(new DomainError(DomainError.EXTERNAL_ERROR, error.message));
|
||||
}
|
||||
|
||||
callback(null);
|
||||
@@ -233,31 +225,25 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!dnsConfig.accessKeyId || typeof dnsConfig.accessKeyId !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'accessKeyId must be a non-empty string'));
|
||||
if (!dnsConfig.secretAccessKey || typeof dnsConfig.secretAccessKey !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'secretAccessKey must be a non-empty string'));
|
||||
if ('hyphenatedSubdomains' in dnsConfig && typeof dnsConfig.hyphenatedSubdomains !== 'boolean') return callback(new DomainsError(DomainsError.BAD_FIELD, 'hyphenatedSubdomains must be a boolean'));
|
||||
|
||||
var credentials = {
|
||||
accessKeyId: dnsConfig.accessKeyId,
|
||||
secretAccessKey: dnsConfig.secretAccessKey,
|
||||
region: dnsConfig.region || 'us-east-1',
|
||||
endpoint: dnsConfig.endpoint || null,
|
||||
listHostedZonesByName: true, // new/updated creds require this perm
|
||||
hyphenatedSubdomains: !!dnsConfig.hyphenatedSubdomains
|
||||
endpoint: dnsConfig.endpoint || null
|
||||
};
|
||||
|
||||
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 DomainsError(DomainsError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
|
||||
if (error || !nameservers) return callback(new DomainsError(DomainsError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
|
||||
dns.resolveNs(zoneName, function (error, nameservers) {
|
||||
if (error && error.code === 'ENOTFOUND') return callback(new DomainError(DomainError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
|
||||
if (error || !nameservers) return callback(new DomainError(DomainError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
|
||||
|
||||
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 DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to Route53'));
|
||||
return callback(new DomainError(DomainError.BAD_FIELD, 'Domain nameservers are not set to Route53'));
|
||||
}
|
||||
|
||||
const testSubdomain = 'cloudrontestdns';
|
||||
@@ -267,7 +253,7 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
|
||||
|
||||
debug('verifyDnsConfig: Test A record added with change id %s', changeId);
|
||||
|
||||
del(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error) {
|
||||
del(dnsConfig, zoneName, testSubdomain, 'A', [ ip ], function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('verifyDnsConfig: Test A record removed again');
|
||||
|
||||
+46
-47
@@ -5,59 +5,53 @@ exports = module.exports = waitForDns;
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
debug = require('debug')('box:dns/waitfordns'),
|
||||
dns = require('../native-dns.js'),
|
||||
DomainsError = require('../domains.js').DomainsError;
|
||||
dig = require('../dig.js'),
|
||||
dns = require('dns'),
|
||||
DomainError = require('../domains.js').DomainError,
|
||||
util = require('util');
|
||||
|
||||
function resolveIp(hostname, options, callback) {
|
||||
assert.strictEqual(typeof hostname, 'string');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// try A record at authoritative server
|
||||
debug(`resolveIp: Checking if ${hostname} has A record at ${options.server}`);
|
||||
dns.resolve(hostname, 'A', options, function (error, results) {
|
||||
if (!error && results.length !== 0) return callback(null, results);
|
||||
|
||||
// try CNAME record at authoritative server
|
||||
debug(`resolveIp: Checking if ${hostname} has CNAME record at ${options.server}`);
|
||||
dns.resolve(hostname, 'CNAME', options, function (error, results) {
|
||||
if (error || results.length === 0) return callback(error, results);
|
||||
|
||||
// recurse lookup the CNAME record
|
||||
debug(`resolveIp: Resolving ${hostname}'s CNAME record ${results[0]}`);
|
||||
dns.resolve(results[0], 'A', { server: '127.0.0.1', timeout: options.timeout }, callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function isChangeSynced(domain, value, nameserver, callback) {
|
||||
function isChangeSynced(domain, value, type, nameserver, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof value, 'string');
|
||||
assert(util.isRegExp(value));
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert.strictEqual(typeof nameserver, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// ns records cannot have cname
|
||||
dns.resolve(nameserver, 'A', { timeout: 5000 }, function (error, nsIps) {
|
||||
dns.resolve4(nameserver, function (error, nsIps) {
|
||||
if (error || !nsIps || nsIps.length === 0) {
|
||||
debug(`isChangeSynced: cannot resolve NS ${nameserver}`); // it's fine if one or more ns are dead
|
||||
return callback(null, true);
|
||||
debug('nameserver %s does not resolve. assuming it stays bad.', nameserver); // it's fine if one or more ns are dead
|
||||
return callback(true);
|
||||
}
|
||||
|
||||
async.every(nsIps, function (nsIp, iteratorCallback) {
|
||||
resolveIp(domain, { server: nsIp, timeout: 5000 }, function (error, answer) {
|
||||
if (error && error.code === 'TIMEOUT') {
|
||||
debug(`isChangeSynced: NS ${nameserver} (${nsIp}) timed out when resolving ${domain}`);
|
||||
dig.resolve(domain, type, { server: nsIp, timeout: 5000 }, function (error, answer) {
|
||||
if (error && error.code === 'ETIMEDOUT') {
|
||||
debug('nameserver %s (%s) timed out when trying to resolve %s', nameserver, nsIp, domain);
|
||||
return iteratorCallback(null, true); // should be ok if dns server is down
|
||||
}
|
||||
|
||||
if (error) {
|
||||
debug(`isChangeSynced: NS ${nameserver} (${nsIp}) errored when resolve ${domain}: ${error}`);
|
||||
debug('nameserver %s (%s) returned error trying to resolve %s: %s', nameserver, nsIp, domain, error);
|
||||
return iteratorCallback(null, false);
|
||||
}
|
||||
|
||||
debug(`isChangeSynced: ${domain} was resolved to ${answer} at NS ${nameserver} (${nsIp}). Expecting ${value}`);
|
||||
if (!answer || answer.length === 0) {
|
||||
debug('bad answer from nameserver %s (%s) resolving %s (%s)', nameserver, nsIp, domain, type);
|
||||
return iteratorCallback(null, false);
|
||||
}
|
||||
|
||||
iteratorCallback(null, answer.length === 1 && answer[0] === value);
|
||||
debug('isChangeSynced: ns: %s (%s), name:%s Actual:%j Expecting:%s', nameserver, nsIp, domain, answer, value);
|
||||
|
||||
var match = answer.some(function (a) {
|
||||
return ((type === 'A' && value.test(a)) ||
|
||||
(type === 'CNAME' && value.test(a)) ||
|
||||
(type === 'TXT' && value.test(a)));
|
||||
});
|
||||
|
||||
if (match) return iteratorCallback(null, true); // done!
|
||||
|
||||
iteratorCallback(null, false);
|
||||
});
|
||||
}, callback);
|
||||
|
||||
@@ -65,33 +59,38 @@ function isChangeSynced(domain, value, nameserver, callback) {
|
||||
}
|
||||
|
||||
// check if IP change has propagated to every nameserver
|
||||
function waitForDns(domain, zoneName, value, options, callback) {
|
||||
function waitForDns(domain, zoneName, value, type, options, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof value, 'string');
|
||||
assert(typeof value === 'string' || util.isRegExp(value));
|
||||
assert(type === 'A' || type === 'CNAME' || type === 'TXT');
|
||||
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('waitForDns: domain %s to be %s in zone %s.', domain, value, zoneName);
|
||||
if (typeof value === 'string') {
|
||||
// http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript
|
||||
value = new RegExp('^' + value.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') + '$');
|
||||
}
|
||||
|
||||
var attempt = 0;
|
||||
debug('waitForIp: domain %s to be %s in zone %s.', domain, value, zoneName);
|
||||
|
||||
var attempt = 1;
|
||||
async.retry(options, function (retryCallback) {
|
||||
++attempt;
|
||||
debug(`waitForDns (try ${attempt}): ${domain} to be ${value} in zone ${zoneName}`);
|
||||
debug('waitForDNS: %s (zone: %s) attempt %s.', domain, zoneName, attempt++);
|
||||
|
||||
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
|
||||
if (error || !nameservers) return retryCallback(error || new DomainsError(DomainsError.EXTERNAL_ERROR, 'Unable to get nameservers'));
|
||||
dns.resolveNs(zoneName, function (error, nameservers) {
|
||||
if (error || !nameservers) return retryCallback(error || new DomainError(DomainError.EXTERNAL_ERROR, 'Unable to get nameservers'));
|
||||
|
||||
async.every(nameservers, isChangeSynced.bind(null, domain, value), function (error, synced) {
|
||||
debug('waitForDns: %s %s ns: %j', domain, synced ? 'done' : 'not done', nameservers);
|
||||
async.every(nameservers, isChangeSynced.bind(null, domain, value, type), function (error, synced) {
|
||||
debug('waitForIp: %s %s ns: %j', domain, synced ? 'done' : 'not done', nameservers);
|
||||
|
||||
retryCallback(synced ? null : new DomainsError(DomainsError.EXTERNAL_ERROR, 'ETRYAGAIN'));
|
||||
retryCallback(synced ? null : new DomainError(DomainError.EXTERNAL_ERROR, 'ETRYAGAIN'));
|
||||
});
|
||||
});
|
||||
}, function retryDone(error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug(`waitForDns: ${domain} has propagated`);
|
||||
debug('waitForDNS: %s done.', domain);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
|
||||
+43
-41
@@ -15,7 +15,6 @@ exports = module.exports = {
|
||||
createSubcontainer: createSubcontainer,
|
||||
getContainerIdByIp: getContainerIdByIp,
|
||||
inspect: inspect,
|
||||
inspectByName: inspect,
|
||||
execContainer: execContainer
|
||||
};
|
||||
|
||||
@@ -45,38 +44,56 @@ var addons = require('./addons.js'),
|
||||
debug = require('debug')('box:docker.js'),
|
||||
once = require('once'),
|
||||
safe = require('safetydance'),
|
||||
shell = require('./shell.js'),
|
||||
spawn = child_process.spawn,
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
|
||||
function debugApp(app, args) {
|
||||
assert(typeof app === 'object');
|
||||
assert(!app || typeof app === 'object');
|
||||
|
||||
debug(app.fqdn + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
|
||||
var prefix = app ? app.intrinsicFqdn : '(no app)';
|
||||
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
|
||||
}
|
||||
|
||||
function pullImage(manifest, callback) {
|
||||
var docker = exports.connection;
|
||||
|
||||
// Use docker CLI here to support downloading of private repos. for dockerode, we have to use
|
||||
// https://github.com/apocas/dockerode#pull-from-private-repos
|
||||
shell.exec('pullImage', '/usr/bin/docker', [ 'pull', manifest.dockerImage ], { }, function (error) {
|
||||
if (error) {
|
||||
debug(`pullImage: Error pulling image ${manifest.dockerImage} of ${manifest.id}: ${error.message}`);
|
||||
return callback(new Error('Failed to pull image'));
|
||||
}
|
||||
docker.pull(manifest.dockerImage, function (err, stream) {
|
||||
if (err) return callback(new Error('Error connecting to docker. statusCode: ' + err.statusCode));
|
||||
|
||||
var image = docker.getImage(manifest.dockerImage);
|
||||
// https://github.com/dotcloud/docker/issues/1074 says each status message
|
||||
// is emitted as a chunk
|
||||
stream.on('data', function (chunk) {
|
||||
var data = safe.JSON.parse(chunk) || { };
|
||||
debug('pullImage %s: %j', manifest.id, data);
|
||||
|
||||
image.inspect(function (err, data) {
|
||||
if (err) return callback(new Error('Error inspecting image:' + err.message));
|
||||
if (!data || !data.Config) return callback(new Error('Missing Config in image:' + JSON.stringify(data, null, 4)));
|
||||
if (!data.Config.Entrypoint && !data.Config.Cmd) return callback(new Error('Only images with entry point are allowed'));
|
||||
// The information here is useless because this is per layer as opposed to per image
|
||||
if (data.status) {
|
||||
} else if (data.error) {
|
||||
debug('pullImage error %s: %s', manifest.id, data.errorDetail.message);
|
||||
}
|
||||
});
|
||||
|
||||
if (data.Config.ExposedPorts) debug('This image of %s exposes ports: %j', manifest.id, data.Config.ExposedPorts);
|
||||
stream.on('end', function () {
|
||||
debug('downloaded image %s of %s successfully', manifest.dockerImage, manifest.id);
|
||||
|
||||
callback(null);
|
||||
var image = docker.getImage(manifest.dockerImage);
|
||||
|
||||
image.inspect(function (err, data) {
|
||||
if (err) return callback(new Error('Error inspecting image:' + err.message));
|
||||
if (!data || !data.Config) return callback(new Error('Missing Config in image:' + JSON.stringify(data, null, 4)));
|
||||
if (!data.Config.Entrypoint && !data.Config.Cmd) return callback(new Error('Only images with entry point are allowed'));
|
||||
|
||||
if (data.Config.ExposedPorts) debug('This image of %s exposes ports: %j', manifest.id, data.Config.ExposedPorts);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
|
||||
stream.on('error', function (error) {
|
||||
debug('error pulling image %s of %s: %j', manifest.dockerImage, manifest.id, error);
|
||||
|
||||
callback(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -112,7 +129,7 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
||||
|
||||
var manifest = app.manifest;
|
||||
var exposedPorts = {}, dockerPortBindings = { };
|
||||
var domain = app.fqdn;
|
||||
var domain = app.altDomain || app.intrinsicFqdn;
|
||||
var stdEnv = [
|
||||
'CLOUDRON=1',
|
||||
'WEBADMIN_ORIGIN=' + config.adminOrigin(),
|
||||
@@ -127,17 +144,14 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
||||
dockerPortBindings[manifest.httpPort + '/tcp'] = [ { HostIp: '127.0.0.1', HostPort: app.httpPort + '' } ];
|
||||
|
||||
var portEnv = [];
|
||||
for (let portName in app.portBindings) {
|
||||
const hostPort = app.portBindings[portName];
|
||||
const portType = portName in manifest.tcpPorts ? 'tcp' : 'udp';
|
||||
const ports = portType == 'tcp' ? manifest.tcpPorts : manifest.udpPorts;
|
||||
for (var e in app.portBindings) {
|
||||
var hostPort = app.portBindings[e];
|
||||
var containerPort = manifest.tcpPorts[e].containerPort || hostPort;
|
||||
|
||||
var containerPort = ports[portName].containerPort || hostPort;
|
||||
exposedPorts[containerPort + '/tcp'] = {};
|
||||
portEnv.push(e + '=' + hostPort);
|
||||
|
||||
exposedPorts[`${containerPort}/${portType}`] = {};
|
||||
portEnv.push(`${portName}=${hostPort}`);
|
||||
|
||||
dockerPortBindings[`${containerPort}/${portType}`] = [ { HostIp: '0.0.0.0', HostPort: hostPort + '' } ];
|
||||
dockerPortBindings[containerPort + '/tcp'] = [ { HostIp: '0.0.0.0', HostPort: hostPort + '' } ];
|
||||
}
|
||||
|
||||
// first check db record, then manifest
|
||||
@@ -149,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);
|
||||
|
||||
@@ -176,20 +186,12 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
||||
'/run': {}
|
||||
},
|
||||
Labels: {
|
||||
'fqdn': app.fqdn,
|
||||
'fqdn': app.intrinsicFqdn,
|
||||
'appId': app.id,
|
||||
'isSubcontainer': String(!isAppContainer)
|
||||
},
|
||||
HostConfig: {
|
||||
Binds: addons.getBindsSync(app, app.manifest.addons),
|
||||
LogConfig: {
|
||||
Type: 'syslog',
|
||||
Config: {
|
||||
'tag': app.id,
|
||||
'syslog-address': 'udp://127.0.0.1:2514', // see apps.js:validatePortBindings()
|
||||
'syslog-format': 'rfc5424'
|
||||
}
|
||||
},
|
||||
Memory: memoryLimit / 2,
|
||||
MemorySwap: memoryLimit, // Memory + Swap
|
||||
PortBindings: isAppContainer ? dockerPortBindings : { },
|
||||
|
||||
@@ -1,178 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
start: start,
|
||||
stop: stop
|
||||
};
|
||||
|
||||
var apps = require('./apps.js'),
|
||||
AppsError = apps.AppsError,
|
||||
assert = require('assert'),
|
||||
config = require('./config.js'),
|
||||
express = require('express'),
|
||||
debug = require('debug')('box:dockerproxy'),
|
||||
http = require('http'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
middleware = require('./middleware'),
|
||||
net = require('net'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance'),
|
||||
_ = require('underscore');
|
||||
|
||||
var gHttpServer = null;
|
||||
|
||||
function authorizeApp(req, res, next) {
|
||||
// TODO add here some authorization
|
||||
// - block apps not using the docker addon
|
||||
// - block calls regarding platform containers
|
||||
// - only allow managing and inspection of containers belonging to the app
|
||||
|
||||
// make the tests pass for now
|
||||
if (config.TEST) {
|
||||
req.app = { id: 'testappid' };
|
||||
return next();
|
||||
}
|
||||
|
||||
apps.getByIpAddress(req.connection.remoteAddress, function (error, app) {
|
||||
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(401, 'Unauthorized'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
if (!('docker' in app.manifest.addons)) return next(new HttpError(401, 'Unauthorized'));
|
||||
|
||||
req.app = app;
|
||||
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
function attachDockerRequest(req, res, next) {
|
||||
var options = {
|
||||
socketPath: '/var/run/docker.sock',
|
||||
method: req.method,
|
||||
path: req.url,
|
||||
headers: req.headers
|
||||
};
|
||||
|
||||
req.dockerRequest = http.request(options, function (dockerResponse) {
|
||||
res.writeHead(dockerResponse.statusCode, dockerResponse.headers);
|
||||
|
||||
// Force node to send out the headers, this is required for the /container/wait api to make the docker cli proceed
|
||||
res.write(' ');
|
||||
|
||||
dockerResponse.on('error', function (error) { console.error('dockerResponse error:', error); });
|
||||
dockerResponse.pipe(res, { end: true });
|
||||
});
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
function containersCreate(req, res, next) {
|
||||
safe.set(req.body, 'HostConfig.NetworkMode', 'cloudron'); // overwrite the network the container lives in
|
||||
safe.set(req.body, 'NetworkingConfig', {}); // drop any custom network configs
|
||||
safe.set(req.body, 'Labels', _.extend({ }, safe.query(req.body, 'Labels'), { appId: req.app.id })); // overwrite the app id to track containers of an app
|
||||
safe.set(req.body, 'HostConfig.LogConfig', { Type: 'syslog', Config: { 'tag': req.app.id, 'syslog-address': 'udp://127.0.0.1:2514', 'syslog-format': 'rfc5424' }});
|
||||
|
||||
const appDataDir = path.join(paths.APPS_DATA_DIR, req.app.id, 'data'),
|
||||
dockerDataDir = path.join(paths.APPS_DATA_DIR, req.app.id, 'docker');
|
||||
|
||||
debug('Original volume binds:', req.body.HostConfig.Binds);
|
||||
|
||||
let binds = [];
|
||||
for (let bind of (req.body.HostConfig.Binds || [])) {
|
||||
if (bind.startsWith(appDataDir)) binds.push(bind); // eclipse will inspect docker to find out the host folders and pass that to child containers
|
||||
else if (bind.startsWith('/app/data')) binds.push(bind.replace(new RegExp('^/app/data'), appDataDir));
|
||||
else binds.push(`${dockerDataDir}/${bind}`);
|
||||
}
|
||||
|
||||
// cleanup the paths from potential double slashes
|
||||
binds = binds.map(function (bind) { return bind.replace(/\/+/g, '/'); });
|
||||
|
||||
debug('Rewritten volume binds:', binds);
|
||||
safe.set(req.body, 'HostConfig.Binds', binds);
|
||||
|
||||
let plainBody = JSON.stringify(req.body);
|
||||
|
||||
req.dockerRequest.setHeader('Content-Length', Buffer.byteLength(plainBody));
|
||||
req.dockerRequest.end(plainBody);
|
||||
}
|
||||
|
||||
function process(req, res, next) {
|
||||
// we have to rebuild the body since we consumed in in the parser
|
||||
if (Object.keys(req.body).length !== 0) {
|
||||
let plainBody = JSON.stringify(req.body);
|
||||
req.dockerRequest.setHeader('Content-Length', Buffer.byteLength(plainBody));
|
||||
req.dockerRequest.end(plainBody);
|
||||
} else if (!req.readable) {
|
||||
req.dockerRequest.end();
|
||||
} else {
|
||||
req.pipe(req.dockerRequest, { end: true });
|
||||
}
|
||||
}
|
||||
|
||||
function start(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
assert(gHttpServer === null, 'Already started');
|
||||
|
||||
let json = middleware.json({ strict: true });
|
||||
let router = new express.Router();
|
||||
router.post('/:version/containers/create', containersCreate);
|
||||
|
||||
let proxyServer = express();
|
||||
|
||||
if (config.TEST) {
|
||||
proxyServer.use(function (req, res, next) {
|
||||
console.log('Proxying: ' + req.method, req.url);
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
proxyServer.use(authorizeApp)
|
||||
.use(attachDockerRequest)
|
||||
.use(json)
|
||||
.use(router)
|
||||
.use(process)
|
||||
.use(middleware.lastMile());
|
||||
|
||||
gHttpServer = http.createServer(proxyServer);
|
||||
gHttpServer.listen(config.get('dockerProxyPort'), '0.0.0.0', callback);
|
||||
|
||||
debug(`startDockerProxy: started proxy on port ${config.get('dockerProxyPort')}`);
|
||||
|
||||
gHttpServer.on('upgrade', function (req, client, head) {
|
||||
// Create a new tcp connection to the TCP server
|
||||
var remote = net.connect('/var/run/docker.sock', function () {
|
||||
var upgradeMessage = req.method + ' ' + req.url + ' HTTP/1.1\r\n' +
|
||||
`Host: ${req.headers.host}\r\n` +
|
||||
'Connection: Upgrade\r\n' +
|
||||
'Upgrade: tcp\r\n';
|
||||
|
||||
if (req.headers['content-type'] === 'application/json') {
|
||||
// TODO we have to parse the immediate upgrade request body, but I don't know how
|
||||
let plainBody = '{"Detach":false,"Tty":false}\r\n';
|
||||
upgradeMessage += `Content-Type: application/json\r\n`;
|
||||
upgradeMessage += `Content-Length: ${Buffer.byteLength(plainBody)}\r\n`;
|
||||
upgradeMessage += '\r\n';
|
||||
upgradeMessage += plainBody;
|
||||
}
|
||||
|
||||
upgradeMessage += '\r\n';
|
||||
|
||||
// resend the upgrade event to the docker daemon, so it responds with the proper message through the pipes
|
||||
remote.write(upgradeMessage);
|
||||
|
||||
// two-way pipes between client and docker daemon
|
||||
client.pipe(remote).pipe(client);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function stop(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (gHttpServer) gHttpServer.close();
|
||||
|
||||
gHttpServer = null;
|
||||
|
||||
callback();
|
||||
}
|
||||
+40
-34
@@ -7,23 +7,22 @@ exports = module.exports = {
|
||||
get: get,
|
||||
getAll: getAll,
|
||||
update: update,
|
||||
upsert: upsert,
|
||||
del: del,
|
||||
|
||||
_clear: clear
|
||||
_clear: clear,
|
||||
_addDefaultDomain: addDefaultDomain
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
database = require('./database.js'),
|
||||
DatabaseError = require('./databaseerror'),
|
||||
config = require('./config.js'),
|
||||
safe = require('safetydance');
|
||||
|
||||
var DOMAINS_FIELDS = [ 'domain', 'zoneName', 'provider', 'configJson', 'tlsConfigJson' ].join(',');
|
||||
|
||||
function postProcess(data) {
|
||||
data.config = safe.JSON.parse(data.configJson);
|
||||
data.tlsConfig = safe.JSON.parse(data.tlsConfigJson);
|
||||
delete data.configJson;
|
||||
delete data.tlsConfigJson;
|
||||
|
||||
return data;
|
||||
}
|
||||
@@ -32,7 +31,7 @@ function get(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query(`SELECT ${DOMAINS_FIELDS} FROM domains WHERE domain=?`, [ domain ], function (error, result) {
|
||||
database.query('SELECT * FROM domains WHERE domain=?', [ domain ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
@@ -43,7 +42,7 @@ function get(domain, callback) {
|
||||
}
|
||||
|
||||
function getAll(callback) {
|
||||
database.query(`SELECT ${DOMAINS_FIELDS} FROM domains ORDER BY domain`, function (error, results) {
|
||||
database.query('SELECT * FROM domains ORDER BY domain', function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
results.forEach(postProcess);
|
||||
@@ -52,16 +51,14 @@ function getAll(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function add(name, domain, callback) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof domain, 'object');
|
||||
assert.strictEqual(typeof domain.zoneName, 'string');
|
||||
assert.strictEqual(typeof domain.provider, 'string');
|
||||
assert.strictEqual(typeof domain.config, 'object');
|
||||
assert.strictEqual(typeof domain.tlsConfig, 'object');
|
||||
function add(domain, zoneName, provider, config, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof provider, 'string');
|
||||
assert.strictEqual(typeof config, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('INSERT INTO domains (domain, zoneName, provider, configJson, tlsConfigJson) VALUES (?, ?, ?, ?, ?)', [ name, domain.zoneName, domain.provider, JSON.stringify(domain.config), JSON.stringify(domain.tlsConfig) ], function (error) {
|
||||
database.query('INSERT INTO domains (domain, zoneName, provider, configJson) VALUES (?, ?, ?, ?)', [ domain, zoneName, provider, JSON.stringify(config) ], function (error) {
|
||||
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error));
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
@@ -69,27 +66,27 @@ function add(name, domain, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function update(name, domain, callback) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof domain, 'object');
|
||||
function upsert(domain, zoneName, provider, config, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof provider, 'string');
|
||||
assert.strictEqual(typeof config, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var args = [ ], fields = [ ];
|
||||
for (var k in domain) {
|
||||
if (k === 'config') {
|
||||
fields.push('configJson = ?');
|
||||
args.push(JSON.stringify(domain[k]));
|
||||
} else if (k === 'tlsConfig') {
|
||||
fields.push('tlsConfigJson = ?');
|
||||
args.push(JSON.stringify(domain[k]));
|
||||
} else {
|
||||
fields.push(k + ' = ?');
|
||||
args.push(domain[k]);
|
||||
}
|
||||
}
|
||||
args.push(name);
|
||||
database.query('REPLACE INTO domains (domain, zoneName, provider, configJson) VALUES (?, ?, ?, ?)', [ domain, zoneName, provider, JSON.stringify(config) ], function (error) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
database.query('UPDATE domains SET ' + fields.join(', ') + ' WHERE domain=?', args, function (error) {
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function update(domain, provider, config, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof provider, 'string');
|
||||
assert.strictEqual(typeof config, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('UPDATE domains SET provider=?, configJson=? WHERE domain=?', [ provider, JSON.stringify(config), domain ], function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
@@ -104,7 +101,7 @@ function del(domain, callback) {
|
||||
database.query('DELETE FROM domains WHERE domain=?', [ domain ], function (error, result) {
|
||||
if (error && error.code === 'ER_ROW_IS_REFERENCED_2') return callback(new DatabaseError(DatabaseError.IN_USE));
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.affectedRows === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
@@ -117,3 +114,12 @@ function clear(callback) {
|
||||
callback(error);
|
||||
});
|
||||
}
|
||||
|
||||
function addDefaultDomain(callback) {
|
||||
assert(config.fqdn(), 'no fqdn set in config, cannot continue');
|
||||
|
||||
add(config.fqdn(), config.zoneName(), 'manual', { }, function (error) {
|
||||
if (error && error.reason !== DatabaseError.ALREADY_EXISTS) return callback(error);
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
+96
-215
@@ -6,46 +6,37 @@ module.exports = exports = {
|
||||
getAll: getAll,
|
||||
update: update,
|
||||
del: del,
|
||||
isLocked: isLocked,
|
||||
|
||||
fqdn: fqdn,
|
||||
setAdmin: setAdmin,
|
||||
|
||||
getDnsRecords: getDnsRecords,
|
||||
upsertDnsRecords: upsertDnsRecords,
|
||||
removeDnsRecords: removeDnsRecords,
|
||||
getDNSRecords: getDNSRecords,
|
||||
upsertDNSRecords: upsertDNSRecords,
|
||||
removeDNSRecords: removeDNSRecords,
|
||||
|
||||
waitForDnsRecord: waitForDnsRecord,
|
||||
waitForDNSRecord: waitForDNSRecord,
|
||||
|
||||
removePrivateFields: removePrivateFields,
|
||||
removeRestrictedFields: removeRestrictedFields,
|
||||
|
||||
validateHostname: validateHostname,
|
||||
|
||||
DomainsError: DomainsError
|
||||
DomainError: DomainError
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
caas = require('./caas.js'),
|
||||
config = require('./config.js'),
|
||||
constants = require('./constants.js'),
|
||||
certificates = require('./certificates.js'),
|
||||
CertificatesError = certificates.CertificatesError,
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
debug = require('debug')('box:domains'),
|
||||
domaindb = require('./domaindb.js'),
|
||||
path = require('path'),
|
||||
reverseProxy = require('./reverseproxy.js'),
|
||||
ReverseProxyError = reverseProxy.ReverseProxyError,
|
||||
safe = require('safetydance'),
|
||||
shell = require('./shell.js'),
|
||||
sysinfo = require('./sysinfo.js'),
|
||||
tld = require('tldjs'),
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
util = require('util');
|
||||
|
||||
var RESTART_CMD = path.join(__dirname, 'scripts/restart.sh');
|
||||
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
|
||||
function DomainsError(reason, errorOrMessage) {
|
||||
function DomainError(reason, errorOrMessage) {
|
||||
assert.strictEqual(typeof reason, 'string');
|
||||
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
|
||||
|
||||
@@ -63,17 +54,17 @@ function DomainsError(reason, errorOrMessage) {
|
||||
this.nestedError = errorOrMessage;
|
||||
}
|
||||
}
|
||||
util.inherits(DomainsError, Error);
|
||||
util.inherits(DomainError, Error);
|
||||
|
||||
DomainsError.NOT_FOUND = 'No such domain';
|
||||
DomainsError.ALREADY_EXISTS = 'Domain already exists';
|
||||
DomainsError.EXTERNAL_ERROR = 'External error';
|
||||
DomainsError.BAD_FIELD = 'Bad Field';
|
||||
DomainsError.STILL_BUSY = 'Still busy';
|
||||
DomainsError.IN_USE = 'In Use';
|
||||
DomainsError.INTERNAL_ERROR = 'Internal error';
|
||||
DomainsError.ACCESS_DENIED = 'Access denied';
|
||||
DomainsError.INVALID_PROVIDER = 'provider must be route53, gcdns, digitalocean, gandi, cloudflare, namecom, noop, manual or caas';
|
||||
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) {
|
||||
@@ -85,15 +76,13 @@ function api(provider) {
|
||||
case 'route53': return require('./dns/route53.js');
|
||||
case 'gcdns': return require('./dns/gcdns.js');
|
||||
case 'digitalocean': return require('./dns/digitalocean.js');
|
||||
case 'gandi': return require('./dns/gandi.js');
|
||||
case 'godaddy': return require('./dns/godaddy.js');
|
||||
case 'namecom': return require('./dns/namecom.js');
|
||||
case 'noop': return require('./dns/noop.js');
|
||||
case 'manual': return require('./dns/manual.js');
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO make it return a DomainError instead of DomainError
|
||||
function verifyDnsConfig(config, domain, zoneName, provider, ip, callback) {
|
||||
assert(config && typeof config === 'object'); // the dns config to test with
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
@@ -103,102 +92,48 @@ function verifyDnsConfig(config, domain, zoneName, provider, ip, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var backend = api(provider);
|
||||
if (!backend) return callback(new DomainsError(DomainsError.INVALID_PROVIDER));
|
||||
if (!backend) return callback(new DomainError(DomainError.INVALID_PROVIDER));
|
||||
|
||||
api(provider).verifyDnsConfig(config, domain, zoneName, ip, callback);
|
||||
}
|
||||
|
||||
function fqdn(location, domainObject) {
|
||||
return location + (location ? (domainObject.config.hyphenatedSubdomains ? '-' : '.') : '') + domainObject.domain;
|
||||
}
|
||||
|
||||
// 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, domainObject) {
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof domainObject, 'object');
|
||||
|
||||
const hostname = fqdn(location, domainObject);
|
||||
|
||||
const RESERVED_LOCATIONS = [
|
||||
constants.API_LOCATION,
|
||||
constants.SMTP_LOCATION,
|
||||
constants.IMAP_LOCATION
|
||||
];
|
||||
if (RESERVED_LOCATIONS.indexOf(location) !== -1) return new DomainsError(DomainsError.BAD_FIELD, location + ' is reserved');
|
||||
|
||||
if (hostname === config.adminFqdn()) return new DomainsError(DomainsError.BAD_FIELD, location + ' is reserved');
|
||||
|
||||
// workaround https://github.com/oncletom/tld.js/issues/73
|
||||
var tmp = hostname.replace('_', '-');
|
||||
if (!tld.isValid(tmp)) return new DomainsError(DomainsError.BAD_FIELD, 'Hostname is not a valid domain name');
|
||||
|
||||
if (hostname.length > 253) return new DomainsError(DomainsError.BAD_FIELD, 'Hostname length exceeds 253 characters');
|
||||
|
||||
if (location) {
|
||||
// label validation
|
||||
if (location.split('.').some(function (p) { return p.length > 63 || p.length < 1; })) return new DomainsError(DomainsError.BAD_FIELD, 'Invalid subdomain length');
|
||||
if (location.match(/^[A-Za-z0-9-.]+$/) === null) return new DomainsError(DomainsError.BAD_FIELD, 'Subdomain can only contain alphanumeric, hyphen and dot');
|
||||
if (/^[-.]/.test(location)) return new DomainsError(DomainsError.BAD_FIELD, 'Subdomain cannot start or end with hyphen or dot');
|
||||
}
|
||||
|
||||
if (domainObject.config.hyphenatedSubdomains) {
|
||||
if (location.indexOf('.') !== -1) return new DomainsError(DomainsError.BAD_FIELD, 'Subdomain cannot contain a dot');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function add(domain, zoneName, provider, dnsConfig, fallbackCertificate, tlsConfig, callback) {
|
||||
function add(domain, zoneName, provider, config, fallbackCertificate, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof provider, 'string');
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
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 DomainsError(DomainsError.BAD_FIELD, 'Invalid domain'));
|
||||
if (domain.endsWith('.')) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Invalid domain'));
|
||||
|
||||
if (zoneName) {
|
||||
if (!tld.isValid(zoneName)) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Invalid zoneName'));
|
||||
if (zoneName.endsWith('.')) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Invalid zoneName'));
|
||||
} else {
|
||||
zoneName = tld.getDomain(domain) || domain;
|
||||
}
|
||||
if (!tld.isValid(domain)) return callback(new DomainError(DomainError.BAD_FIELD, 'Invalid domain'));
|
||||
if (!tld.isValid(zoneName)) return callback(new DomainError(DomainError.BAD_FIELD, 'Invalid zoneName'));
|
||||
|
||||
if (fallbackCertificate) {
|
||||
let error = reverseProxy.validateCertificate(`test.${domain}`, fallbackCertificate.cert, fallbackCertificate.key);
|
||||
if (error) return callback(new DomainsError(DomainsError.BAD_FIELD, error.message));
|
||||
let error = certificates.validateCertificate(fallbackCertificate.cert, fallbackCertificate.key, domain);
|
||||
if (error) return callback(new DomainError(DomainError.BAD_FIELD, error.message));
|
||||
}
|
||||
|
||||
if (tlsConfig.provider !== 'fallback' && tlsConfig.provider !== 'caas' && tlsConfig.provider.indexOf('letsencrypt-') !== 0) {
|
||||
return callback(new DomainsError(DomainsError.BAD_FIELD, 'tlsConfig.provider must be caas, fallback or le-*'));
|
||||
}
|
||||
|
||||
if (dnsConfig.hyphenatedSubdomains && !config.allowHyphenatedSubdomains()) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Not allowed in this edition'));
|
||||
|
||||
sysinfo.getPublicIp(function (error, ip) {
|
||||
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, 'Error getting IP:' + error.message));
|
||||
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, 'Error getting IP:' + error.message));
|
||||
|
||||
verifyDnsConfig(dnsConfig, domain, zoneName, provider, ip, function (error, result) {
|
||||
if (error && error.reason === DomainsError.ACCESS_DENIED) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Error adding A record. Access denied'));
|
||||
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Zone not found'));
|
||||
if (error && error.reason === DomainsError.EXTERNAL_ERROR) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Error adding A record: ' + error.message));
|
||||
if (error && error.reason === DomainsError.BAD_FIELD) return callback(new DomainsError(DomainsError.BAD_FIELD, error.message));
|
||||
if (error && error.reason === DomainsError.INVALID_PROVIDER) return callback(new DomainsError(DomainsError.BAD_FIELD, error.message));
|
||||
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
|
||||
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 DomainsError(DomainsError.ALREADY_EXISTS));
|
||||
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
|
||||
domaindb.add(domain, zoneName, provider, result, function (error) {
|
||||
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new DomainError(DomainError.ALREADY_EXISTS));
|
||||
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
|
||||
|
||||
reverseProxy.setFallbackCertificate(domain, fallbackCertificate, function (error) {
|
||||
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
|
||||
if (!fallbackCertificate) return callback();
|
||||
|
||||
// cert validation already happened above no need to check all errors again
|
||||
certificates.setFallbackCertificate(fallbackCertificate.cert, fallbackCertificate.key, domain, function (error) {
|
||||
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
|
||||
callback();
|
||||
});
|
||||
});
|
||||
@@ -206,30 +141,19 @@ function add(domain, zoneName, provider, dnsConfig, fallbackCertificate, tlsConf
|
||||
});
|
||||
}
|
||||
|
||||
function isLocked(domain) {
|
||||
return domain === config.adminDomain() && config.isAdminDomainLocked();
|
||||
}
|
||||
|
||||
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 DomainsError(DomainsError.NOT_FOUND));
|
||||
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, 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));
|
||||
|
||||
result.locked = isLocked(domain);
|
||||
certificates.getFallbackCertificate(domain, function (error, fallbackCertificate) {
|
||||
if (error && error.reason !== CertificatesError.NOT_FOUND) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
|
||||
|
||||
reverseProxy.getFallbackCertificate(domain, function (error, bundle) {
|
||||
if (error && error.reason !== ReverseProxyError.NOT_FOUND) return callback(new DomainsError(DomainsError.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 DomainsError(DomainsError.INTERNAL_ERROR, 'unable to read certificates from disk'));
|
||||
|
||||
result.fallbackCertificate = { cert: cert, key: key };
|
||||
if (fallbackCertificate) result.fallbackCertificate = fallbackCertificate;
|
||||
|
||||
return callback(null, result);
|
||||
});
|
||||
@@ -240,64 +164,48 @@ function getAll(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
domaindb.getAll(function (error, result) {
|
||||
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
|
||||
|
||||
result.forEach(function (r) { r.locked = isLocked(r.domain); });
|
||||
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
|
||||
|
||||
return callback(null, result);
|
||||
});
|
||||
}
|
||||
|
||||
function update(domain, zoneName, provider, dnsConfig, fallbackCertificate, tlsConfig, callback) {
|
||||
function update(domain, provider, config, fallbackCertificate, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof provider, 'string');
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
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 DomainsError(DomainsError.NOT_FOUND));
|
||||
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
|
||||
|
||||
if (zoneName) {
|
||||
if (!tld.isValid(zoneName)) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Invalid zoneName'));
|
||||
} else {
|
||||
zoneName = result.zoneName;
|
||||
}
|
||||
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 DomainsError(DomainsError.BAD_FIELD, error.message));
|
||||
let error = certificates.validateCertificate(fallbackCertificate.cert, fallbackCertificate.key, domain);
|
||||
if (error) return callback(new DomainError(DomainError.BAD_FIELD, error.message));
|
||||
}
|
||||
|
||||
if (tlsConfig.provider !== 'fallback' && tlsConfig.provider !== 'caas' && tlsConfig.provider.indexOf('letsencrypt-') !== 0) {
|
||||
return callback(new DomainsError(DomainsError.BAD_FIELD, 'tlsConfig.provider must be caas, fallback or letsencrypt-*'));
|
||||
}
|
||||
|
||||
if (dnsConfig.hyphenatedSubdomains && !config.allowHyphenatedSubdomains()) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Not allowed in this edition'));
|
||||
|
||||
sysinfo.getPublicIp(function (error, ip) {
|
||||
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, 'Error getting IP:' + error.message));
|
||||
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, 'Error getting IP:' + error.message));
|
||||
|
||||
verifyDnsConfig(dnsConfig, domain, zoneName, provider, ip, function (error, result) {
|
||||
if (error && error.reason === DomainsError.ACCESS_DENIED) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Error adding A record. Access denied'));
|
||||
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Zone not found'));
|
||||
if (error && error.reason === DomainsError.EXTERNAL_ERROR) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Error adding A record:' + error.message));
|
||||
if (error && error.reason === DomainsError.BAD_FIELD) return callback(new DomainsError(DomainsError.BAD_FIELD, error.message));
|
||||
if (error && error.reason === DomainsError.INVALID_PROVIDER) return callback(new DomainsError(DomainsError.BAD_FIELD, error.message));
|
||||
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
|
||||
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, { zoneName: zoneName, provider: provider, config: result, tlsConfig: tlsConfig }, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DomainsError(DomainsError.NOT_FOUND));
|
||||
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
|
||||
domaindb.update(domain, provider, result, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DomainError(DomainError.NOT_FOUND));
|
||||
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
|
||||
|
||||
if (!fallbackCertificate) return callback();
|
||||
|
||||
reverseProxy.setFallbackCertificate(domain, fallbackCertificate, function (error) {
|
||||
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
|
||||
|
||||
// cert validation already happened above no need to check all errors again
|
||||
certificates.setFallbackCertificate(fallbackCertificate.cert, fallbackCertificate.key, domain, function (error) {
|
||||
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
|
||||
callback();
|
||||
});
|
||||
});
|
||||
@@ -310,45 +218,25 @@ function del(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (domain === config.adminDomain()) return callback(new DomainsError(DomainsError.IN_USE));
|
||||
|
||||
domaindb.del(domain, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DomainsError(DomainsError.NOT_FOUND));
|
||||
if (error && error.reason === DatabaseError.IN_USE) return callback(new DomainsError(DomainsError.IN_USE));
|
||||
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, 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);
|
||||
});
|
||||
}
|
||||
|
||||
// returns the 'name' that needs to be inserted into zone
|
||||
function getName(domain, subdomain, type) {
|
||||
// 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);
|
||||
|
||||
if (subdomain === '') {
|
||||
return part;
|
||||
} else if (type === 'TXT') {
|
||||
return `${subdomain}.${part}`;
|
||||
} else {
|
||||
return subdomain + (domain.config.hyphenatedSubdomains ? '-' : '.') + part;
|
||||
}
|
||||
}
|
||||
|
||||
function getDnsRecords(subdomain, domain, type, callback) {
|
||||
function getDNSRecords(subdomain, domain, type, callback) {
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
get(domain, function (error, result) {
|
||||
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
|
||||
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
|
||||
|
||||
api(result.provider).get(result.config, result.zoneName, getName(result, subdomain, type), type, function (error, values) {
|
||||
api(result.provider).get(result.config, result.zoneName, subdomain, type, function (error, values) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, values);
|
||||
@@ -356,7 +244,7 @@ function getDnsRecords(subdomain, domain, type, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function upsertDnsRecords(subdomain, domain, type, values, callback) {
|
||||
function upsertDNSRecords(subdomain, domain, type, values, callback) {
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
@@ -366,17 +254,17 @@ function upsertDnsRecords(subdomain, domain, type, values, callback) {
|
||||
debug('upsertDNSRecord: %s on %s type %s values', subdomain, domain, type, values);
|
||||
|
||||
get(domain, function (error, result) {
|
||||
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
|
||||
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
|
||||
|
||||
api(result.provider).upsert(result.config, result.zoneName, getName(result, subdomain, type), type, values, function (error) {
|
||||
api(result.provider).upsert(result.config, result.zoneName, subdomain, type, values, function (error, changeId) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null);
|
||||
callback(null, changeId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function removeDnsRecords(subdomain, domain, type, values, callback) {
|
||||
function removeDNSRecords(subdomain, domain, type, values, callback) {
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
@@ -388,26 +276,30 @@ function removeDnsRecords(subdomain, domain, type, values, callback) {
|
||||
get(domain, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
api(result.provider).del(result.config, result.zoneName, getName(result, subdomain, type), type, values, function (error) {
|
||||
if (error && error.reason !== DomainsError.NOT_FOUND) return callback(error);
|
||||
api(result.provider).del(result.config, result.zoneName, subdomain, type, values, function (error) {
|
||||
if (error && error.reason !== DomainError.NOT_FOUND) return callback(error);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// only wait for A record
|
||||
function waitForDnsRecord(fqdn, domain, value, options, callback) {
|
||||
function waitForDNSRecord(fqdn, domain, value, type, options, callback) {
|
||||
assert.strictEqual(typeof fqdn, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof value, 'string');
|
||||
assert(typeof value === 'string' || util.isRegExp(value));
|
||||
assert(type === 'A' || type === 'CNAME' || type === 'TXT');
|
||||
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
get(domain, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
// domain can be not found when waiting for altDomain. When we migrate altDomain, this can never happen
|
||||
if (error && error.reason !== DomainError.NOT_FOUND) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
|
||||
|
||||
api(result.provider).waitForDns(fqdn, result ? result.zoneName : domain, value, options, callback);
|
||||
// hack for lack of provider with altDomain. When we migrate altDomain, this will be automatically "manual"
|
||||
const provider = result ? result.provider : 'manual';
|
||||
|
||||
api(provider).waitForDns(fqdn, result ? result.zoneName : domain, value, type, options, callback);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -423,11 +315,12 @@ function setAdmin(domain, callback) {
|
||||
var setPtrRecord = config.provider() === 'caas' ? caas.setPtrRecord : function (d, next) { next(); };
|
||||
|
||||
setPtrRecord(domain, function (error) {
|
||||
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, 'Error setting PTR record:' + error.message));
|
||||
if (error) return callback(new DomainError(DomainError.EXTERNAL_ERROR, 'Error setting PTR record:' + error.message));
|
||||
|
||||
config.setAdminDomain(result.domain);
|
||||
config.setFqdn(result.domain);
|
||||
config.setAdminLocation('my');
|
||||
config.setAdminFqdn('my' + (result.config.hyphenatedSubdomains ? '-' : '.') + result.domain);
|
||||
config.setAdminFqdn('my' + (result.provider === 'caas' ? '-' : '.') + result.domain);
|
||||
config.setZoneName(result.zoneName);
|
||||
|
||||
callback();
|
||||
|
||||
@@ -436,19 +329,7 @@ function setAdmin(domain, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
// removes all fields that are strictly private and should never be returned by API calls
|
||||
function removePrivateFields(domain) {
|
||||
var result = _.pick(domain, 'domain', 'zoneName', 'provider', 'config', 'tlsConfig', 'fallbackCertificate', 'locked');
|
||||
if (result.fallbackCertificate) delete result.fallbackCertificate.key; // do not return the 'key'. in caas, this is private
|
||||
return result;
|
||||
function fqdn(location, domain, provider) {
|
||||
return location + (location ? (provider !== 'caas' ? '.' : '-') : '') + domain;
|
||||
}
|
||||
|
||||
// removes all fields that are not accessible by a normal user
|
||||
function removeRestrictedFields(domain) {
|
||||
var result = _.pick(domain, 'domain', 'zoneName', 'provider', 'locked');
|
||||
|
||||
// always ensure config object
|
||||
result.config = { hyphenatedSubdomains: !!domain.config.hyphenatedSubdomains };
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
sync: sync
|
||||
};
|
||||
|
||||
var appdb = require('./appdb.js'),
|
||||
apps = require('./apps.js'),
|
||||
async = require('async'),
|
||||
config = require('./config.js'),
|
||||
debug = require('debug')('box:dyndns'),
|
||||
domains = require('./domains.js'),
|
||||
sysinfo = require('./sysinfo.js');
|
||||
|
||||
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
|
||||
// called for dynamic dns setups where we have to update the IP
|
||||
function sync(callback) {
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
|
||||
sysinfo.getPublicIp(function (error, ip) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('refreshDNS: current ip %s', ip);
|
||||
|
||||
domains.upsertDnsRecords(config.adminLocation(), config.adminDomain(), 'A', [ ip ], function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('refreshDNS: done for admin location');
|
||||
|
||||
apps.getAll(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.each(result, function (app, callback) {
|
||||
// do not change state of installing apps since apptask will error if dns record already exists
|
||||
if (app.installationState !== appdb.ISTATE_INSTALLED) return callback();
|
||||
|
||||
domains.upsertDnsRecords(app.location, app.domain, 'A', [ ip ], callback);
|
||||
}, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('refreshDNS: done for apps');
|
||||
|
||||
callback();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
+398
@@ -0,0 +1,398 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
verifyRelay: verifyRelay,
|
||||
getStatus: getStatus,
|
||||
checkRblStatus: checkRblStatus,
|
||||
|
||||
EmailError: EmailError
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
cloudron = require('./cloudron.js'),
|
||||
config = require('./config.js'),
|
||||
debug = require('debug')('box:email'),
|
||||
dig = require('./dig.js'),
|
||||
net = require('net'),
|
||||
nodemailer = require('nodemailer'),
|
||||
safe = require('safetydance'),
|
||||
settings = require('./settings.js'),
|
||||
smtpTransport = require('nodemailer-smtp-transport'),
|
||||
sysinfo = require('./sysinfo.js'),
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
|
||||
const digOptions = { server: '127.0.0.1', port: 53, timeout: 5000 };
|
||||
|
||||
function EmailError(reason, errorOrMessage) {
|
||||
assert.strictEqual(typeof reason, 'string');
|
||||
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
|
||||
|
||||
Error.call(this);
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
|
||||
this.name = this.constructor.name;
|
||||
this.reason = reason;
|
||||
if (typeof errorOrMessage === 'undefined') {
|
||||
this.message = reason;
|
||||
} else if (typeof errorOrMessage === 'string') {
|
||||
this.message = errorOrMessage;
|
||||
} else {
|
||||
this.message = 'Internal error';
|
||||
this.nestedError = errorOrMessage;
|
||||
}
|
||||
}
|
||||
util.inherits(EmailError, Error);
|
||||
EmailError.INTERNAL_ERROR = 'Internal Error';
|
||||
EmailError.BAD_FIELD = 'Bad Field';
|
||||
|
||||
function checkOutboundPort25(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var smtpServer = _.sample([
|
||||
'smtp.gmail.com',
|
||||
'smtp.live.com',
|
||||
'smtp.mail.yahoo.com',
|
||||
'smtp.o2.ie',
|
||||
'smtp.comcast.net',
|
||||
'outgoing.verizon.net'
|
||||
]);
|
||||
|
||||
var relay = {
|
||||
value: 'OK',
|
||||
status: false
|
||||
};
|
||||
|
||||
var client = new net.Socket();
|
||||
client.setTimeout(5000);
|
||||
client.connect(25, smtpServer);
|
||||
client.on('connect', function () {
|
||||
relay.status = true;
|
||||
relay.value = 'OK';
|
||||
client.destroy(); // do not use end() because it still triggers timeout
|
||||
callback(null, relay);
|
||||
});
|
||||
client.on('timeout', function () {
|
||||
relay.status = false;
|
||||
relay.value = 'Connect to ' + smtpServer + ' timed out';
|
||||
client.destroy();
|
||||
callback(new Error('Timeout'), relay);
|
||||
});
|
||||
client.on('error', function (error) {
|
||||
relay.status = false;
|
||||
relay.value = 'Connect to ' + smtpServer + ' failed: ' + error.message;
|
||||
client.destroy();
|
||||
callback(error, relay);
|
||||
});
|
||||
}
|
||||
|
||||
function checkSmtpRelay(relay, callback) {
|
||||
var result = {
|
||||
value: 'OK',
|
||||
status: false
|
||||
};
|
||||
|
||||
var transporter = nodemailer.createTransport(smtpTransport({
|
||||
host: relay.host,
|
||||
port: relay.port,
|
||||
auth: {
|
||||
user: relay.username,
|
||||
pass: relay.password
|
||||
}
|
||||
}));
|
||||
|
||||
transporter.verify(function(error) {
|
||||
result.status = !error;
|
||||
if (error) {
|
||||
result.value = error.message;
|
||||
return callback(error, result);
|
||||
}
|
||||
|
||||
callback(null, result);
|
||||
});
|
||||
}
|
||||
|
||||
function verifyRelay(relay, callback) {
|
||||
assert.strictEqual(typeof relay, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var verifier = relay.provider === 'cloudron-smtp' ? checkOutboundPort25 : checkSmtpRelay.bind(null, relay);
|
||||
|
||||
verifier(function (error) {
|
||||
if (error) return callback(new EmailError(EmailError.BAD_FIELD, error.message));
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
function checkDkim(callback) {
|
||||
var dkim = {
|
||||
domain: config.dkimSelector() + '._domainkey.' + config.fqdn(),
|
||||
type: 'TXT',
|
||||
expected: null,
|
||||
value: null,
|
||||
status: false
|
||||
};
|
||||
|
||||
var dkimKey = cloudron.readDkimPublicKeySync();
|
||||
if (!dkimKey) return callback(new Error('Failed to read dkim public key'), dkim);
|
||||
|
||||
dkim.expected = '"v=DKIM1; t=s; p=' + dkimKey + '"';
|
||||
|
||||
dig.resolve(dkim.domain, dkim.type, digOptions, function (error, txtRecords) {
|
||||
if (error && error.code === 'ENOTFOUND') return callback(null, dkim); // not setup
|
||||
if (error) return callback(error, dkim);
|
||||
|
||||
if (Array.isArray(txtRecords) && txtRecords.length !== 0) {
|
||||
dkim.value = txtRecords[0];
|
||||
dkim.status = (dkim.value === dkim.expected);
|
||||
}
|
||||
|
||||
callback(null, dkim);
|
||||
});
|
||||
}
|
||||
|
||||
function checkSpf(callback) {
|
||||
var spf = {
|
||||
domain: config.fqdn(),
|
||||
type: 'TXT',
|
||||
value: null,
|
||||
expected: '"v=spf1 a:' + config.adminFqdn() + ' ~all"',
|
||||
status: false
|
||||
};
|
||||
|
||||
// https://agari.zendesk.com/hc/en-us/articles/202952749-How-long-can-my-SPF-record-be-
|
||||
dig.resolve(spf.domain, spf.type, digOptions, function (error, txtRecords) {
|
||||
if (error && error.code === 'ENOTFOUND') return callback(null, spf); // not setup
|
||||
if (error) return callback(error, spf);
|
||||
|
||||
if (!Array.isArray(txtRecords)) return callback(null, spf);
|
||||
|
||||
var i;
|
||||
for (i = 0; i < txtRecords.length; i++) {
|
||||
if (txtRecords[i].indexOf('"v=spf1 ') !== 0) continue; // not SPF
|
||||
spf.value = txtRecords[i];
|
||||
spf.status = spf.value.indexOf(' a:' + config.adminFqdn()) !== -1;
|
||||
break;
|
||||
}
|
||||
|
||||
if (spf.status) {
|
||||
spf.expected = spf.value;
|
||||
} else if (i !== txtRecords.length) {
|
||||
spf.expected = '"v=spf1 a:' + config.adminFqdn() + ' ' + spf.value.slice('"v=spf1 '.length);
|
||||
}
|
||||
|
||||
callback(null, spf);
|
||||
});
|
||||
}
|
||||
|
||||
function checkMx(callback) {
|
||||
var mx = {
|
||||
domain: config.fqdn(),
|
||||
type: 'MX',
|
||||
value: null,
|
||||
expected: '10 ' + config.mailFqdn() + '.',
|
||||
status: false
|
||||
};
|
||||
|
||||
dig.resolve(mx.domain, mx.type, digOptions, function (error, mxRecords) {
|
||||
if (error && error.code === 'ENOTFOUND') return callback(null, mx); // not setup
|
||||
if (error) return callback(error, mx);
|
||||
|
||||
if (Array.isArray(mxRecords) && mxRecords.length !== 0) {
|
||||
mx.status = mxRecords.length == 1 && mxRecords[0].exchange === (config.mailFqdn() + '.');
|
||||
mx.value = mxRecords.map(function (r) { return r.priority + ' ' + r.exchange; }).join(' ');
|
||||
}
|
||||
|
||||
callback(null, mx);
|
||||
});
|
||||
}
|
||||
|
||||
function checkDmarc(callback) {
|
||||
var dmarc = {
|
||||
domain: '_dmarc.' + config.fqdn(),
|
||||
type: 'TXT',
|
||||
value: null,
|
||||
expected: '"v=DMARC1; p=reject; pct=100"',
|
||||
status: false
|
||||
};
|
||||
|
||||
dig.resolve(dmarc.domain, dmarc.type, digOptions, function (error, txtRecords) {
|
||||
if (error && error.code === 'ENOTFOUND') return callback(null, dmarc); // not setup
|
||||
if (error) return callback(error, dmarc);
|
||||
|
||||
if (Array.isArray(txtRecords) && txtRecords.length !== 0) {
|
||||
dmarc.value = txtRecords[0];
|
||||
dmarc.status = (dmarc.value === dmarc.expected);
|
||||
}
|
||||
|
||||
callback(null, dmarc);
|
||||
});
|
||||
}
|
||||
|
||||
function checkPtr(callback) {
|
||||
var ptr = {
|
||||
domain: null,
|
||||
type: 'PTR',
|
||||
value: null,
|
||||
expected: config.mailFqdn() + '.',
|
||||
status: false
|
||||
};
|
||||
|
||||
sysinfo.getPublicIp(function (error, ip) {
|
||||
if (error) return callback(error, ptr);
|
||||
|
||||
ptr.domain = ip.split('.').reverse().join('.') + '.in-addr.arpa';
|
||||
|
||||
dig.resolve(ip, 'PTR', digOptions, function (error, ptrRecords) {
|
||||
if (error && error.code === 'ENOTFOUND') return callback(null, ptr); // not setup
|
||||
if (error) return callback(error, ptr);
|
||||
|
||||
if (Array.isArray(ptrRecords) && ptrRecords.length !== 0) {
|
||||
ptr.value = ptrRecords.join(' ');
|
||||
ptr.status = ptrRecords.some(function (v) { return v === ptr.expected; });
|
||||
}
|
||||
|
||||
return callback(null, ptr);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// https://raw.githubusercontent.com/jawsome/node-dnsbl/master/list.json
|
||||
const RBL_LIST = [
|
||||
{
|
||||
'name': 'Barracuda',
|
||||
'dns': 'b.barracudacentral.org',
|
||||
'site': 'http://www.barracudacentral.org/rbl/removal-request'
|
||||
},
|
||||
{
|
||||
'name': 'SpamCop',
|
||||
'dns': 'bl.spamcop.net',
|
||||
'site': 'http://spamcop.net'
|
||||
},
|
||||
{
|
||||
'name': 'Sorbs Aggregate Zone',
|
||||
'dns': 'dnsbl.sorbs.net',
|
||||
'site': 'http://dnsbl.sorbs.net/'
|
||||
},
|
||||
{
|
||||
'name': 'Sorbs spam.dnsbl Zone',
|
||||
'dns': 'spam.dnsbl.sorbs.net',
|
||||
'site': 'http://sorbs.net'
|
||||
},
|
||||
{
|
||||
'name': 'Composite Blocking List',
|
||||
'dns': 'cbl.abuseat.org',
|
||||
'site': 'http://www.abuseat.org'
|
||||
},
|
||||
{
|
||||
'name': 'SpamHaus Zen',
|
||||
'dns': 'zen.spamhaus.org',
|
||||
'site': 'http://spamhaus.org'
|
||||
},
|
||||
{
|
||||
'name': 'Multi SURBL',
|
||||
'dns': 'multi.surbl.org',
|
||||
'site': 'http://www.surbl.org'
|
||||
},
|
||||
{
|
||||
'name': 'Spam Cannibal',
|
||||
'dns': 'bl.spamcannibal.org',
|
||||
'site': 'http://www.spamcannibal.org/cannibal.cgi'
|
||||
},
|
||||
{
|
||||
'name': 'dnsbl.abuse.ch',
|
||||
'dns': 'spam.abuse.ch',
|
||||
'site': 'http://dnsbl.abuse.ch/'
|
||||
},
|
||||
{
|
||||
'name': 'The Unsubscribe Blacklist(UBL)',
|
||||
'dns': 'ubl.unsubscore.com ',
|
||||
'site': 'http://www.lashback.com/blacklist/'
|
||||
},
|
||||
{
|
||||
'name': 'UCEPROTECT Network',
|
||||
'dns': 'dnsbl-1.uceprotect.net',
|
||||
'site': 'http://www.uceprotect.net/en'
|
||||
}
|
||||
];
|
||||
|
||||
function checkRblStatus(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
sysinfo.getPublicIp(function (error, ip) {
|
||||
if (error) return callback(error, ip);
|
||||
|
||||
var flippedIp = ip.split('.').reverse().join('.');
|
||||
|
||||
// https://tools.ietf.org/html/rfc5782
|
||||
async.map(RBL_LIST, function (rblServer, iteratorDone) {
|
||||
dig.resolve(flippedIp + '.' + rblServer.dns, 'A', digOptions, function (error, records) {
|
||||
if (error || !records) return iteratorDone(null, null); // not listed
|
||||
|
||||
debug('checkRblStatus: %s (ip: %s) is in the blacklist of %j', config.fqdn(), flippedIp, rblServer);
|
||||
|
||||
var result = _.extend({ }, rblServer);
|
||||
|
||||
dig.resolve(flippedIp + '.' + rblServer.dns, 'TXT', digOptions, function (error, txtRecords) {
|
||||
result.txtRecords = error || !txtRecords ? 'No txt record' : txtRecords;
|
||||
|
||||
debug('checkRblStatus: %s (error: %s) (txtRecords: %j)', config.fqdn(), error, txtRecords);
|
||||
|
||||
return iteratorDone(null, result);
|
||||
});
|
||||
});
|
||||
}, function (ignoredError, blacklistedServers) {
|
||||
blacklistedServers = blacklistedServers.filter(function(b) { return b !== null; });
|
||||
|
||||
debug('checkRblStatus: %s (ip: %s) servers: %j', config.fqdn(), ip, blacklistedServers);
|
||||
|
||||
return callback(null, { status: blacklistedServers.length === 0, ip: ip, servers: blacklistedServers });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getStatus(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var results = {};
|
||||
|
||||
function recordResult(what, func) {
|
||||
return function (callback) {
|
||||
func(function (error, result) {
|
||||
if (error) debug('Ignored error - ' + what + ':', error);
|
||||
|
||||
safe.set(results, what, result);
|
||||
|
||||
callback();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
settings.getMailRelay(function (error, relay) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var checks = [
|
||||
recordResult('dns.mx', checkMx),
|
||||
recordResult('dns.dmarc', checkDmarc)
|
||||
];
|
||||
|
||||
if (relay.provider === 'cloudron-smtp') {
|
||||
// these tests currently only make sense when using Cloudron's SMTP server at this point
|
||||
checks.push(
|
||||
recordResult('dns.spf', checkSpf),
|
||||
recordResult('dns.dkim', checkDkim),
|
||||
recordResult('dns.ptr', checkPtr),
|
||||
recordResult('relay', checkOutboundPort25),
|
||||
recordResult('rbl', checkRblStatus)
|
||||
);
|
||||
} else {
|
||||
checks.push(recordResult('relay', checkSmtpRelay.bind(null, relay)));
|
||||
}
|
||||
|
||||
async.parallel(checks, function () {
|
||||
callback(null, results);
|
||||
});
|
||||
});
|
||||
}
|
||||
+13
-6
@@ -22,13 +22,13 @@ 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',
|
||||
ACTION_USER_LOGIN: 'user.login',
|
||||
ACTION_USER_REMOVE: 'user.remove',
|
||||
ACTION_USER_UPDATE: 'user.update',
|
||||
ACTION_USER_TRANSFER: 'user.transfer',
|
||||
ACTION_USER_UPDATE: 'user.update'
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
@@ -91,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);
|
||||
@@ -122,7 +122,14 @@ function cleanup(callback) {
|
||||
var d = new Date();
|
||||
d.setDate(d.getDate() - 10); // 10 days ago
|
||||
|
||||
eventlogdb.delByCreationTime(d, function (error) {
|
||||
// only cleanup high frequency events
|
||||
var actions = [
|
||||
exports.ACTION_USER_LOGIN,
|
||||
exports.ACTION_BACKUP_START,
|
||||
exports.ACTION_BACKUP_FINISH
|
||||
];
|
||||
|
||||
eventlogdb.delByCreationTime(d, actions, function (error) {
|
||||
if (error) return callback(new EventLogError(EventLogError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
|
||||
+13
-12
@@ -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 ?,?';
|
||||
|
||||
@@ -121,13 +120,15 @@ function clear(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function delByCreationTime(creationTime, callback) {
|
||||
function delByCreationTime(creationTime, actions, callback) {
|
||||
assert(util.isDate(creationTime));
|
||||
assert(Array.isArray(actions));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var query = 'DELETE FROM eventlog WHERE creationTime < ?';
|
||||
var query = 'DELETE FROM eventlog WHERE creationTime < ? ';
|
||||
if (actions.length) query += ' AND ( ' + actions.map(function () { return 'action != ?'; }).join(' AND ') + ' ) ';
|
||||
|
||||
database.query(query, [ creationTime ], function (error) {
|
||||
database.query(query, [ creationTime ].concat(actions), function (error) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(error);
|
||||
|
||||
+21
-48
@@ -6,7 +6,6 @@ exports = module.exports = {
|
||||
getAll: getAll,
|
||||
getAllWithMembers: getAllWithMembers,
|
||||
add: add,
|
||||
update: update,
|
||||
del: del,
|
||||
count: count,
|
||||
|
||||
@@ -16,17 +15,19 @@ exports = module.exports = {
|
||||
setMembers: setMembers,
|
||||
isMember: isMember,
|
||||
|
||||
getMembership: getMembership,
|
||||
setMembership: setMembership,
|
||||
|
||||
getGroups: getGroups,
|
||||
setGroups: setGroups,
|
||||
|
||||
_clear: clear
|
||||
_clear: clear,
|
||||
_addDefaultGroups: addDefaultGroups
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
constants = require('./constants.js'),
|
||||
config = require('./config.js'),
|
||||
database = require('./database.js'),
|
||||
DatabaseError = require('./databaseerror');
|
||||
DatabaseError = require('./databaseerror'),
|
||||
mailboxdb = require('./mailboxdb.js');
|
||||
|
||||
var GROUPS_FIELDS = [ 'id', 'name' ].join(',');
|
||||
|
||||
@@ -63,10 +64,10 @@ function getWithMembers(groupId, callback) {
|
||||
function getAll(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + GROUPS_FIELDS + ' FROM groups', function (error, results) {
|
||||
database.query('SELECT ' + GROUPS_FIELDS + ' FROM groups', function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, results);
|
||||
callback(null, result);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -88,39 +89,18 @@ function add(id, name, callback) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('INSERT INTO groups (id, name) VALUES (?, ?)', [ id, name ], function (error, result) {
|
||||
var queries = [];
|
||||
queries.push({ query: 'INSERT INTO mailboxes (name, domain, ownerId, ownerType) VALUES (?, ?, ?, ?)', args: [ name, config.fqdn(), id, mailboxdb.TYPE_GROUP ] });
|
||||
queries.push({ query: 'INSERT INTO groups (id, name) VALUES (?, ?)', args: [ id, name ] });
|
||||
|
||||
database.transaction(queries, function (error, result) {
|
||||
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error));
|
||||
if (error || result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (error || result[1].affectedRows !== 1) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function update(id, data, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert(data && typeof data === 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var args = [ ];
|
||||
var fields = [ ];
|
||||
for (var k in data) {
|
||||
if (k === 'name') {
|
||||
assert.strictEqual(typeof data.name, 'string');
|
||||
fields.push(k + ' = ?');
|
||||
args.push(data.name);
|
||||
}
|
||||
}
|
||||
args.push(id);
|
||||
|
||||
database.query('UPDATE groups SET ' + fields.join(', ') + ' WHERE id = ?', args, function (error, result) {
|
||||
if (error && error.code === 'ER_DUP_ENTRY' && error.sqlMessage.indexOf('groups_name') !== -1) return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, 'name already exists'));
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function del(id, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -129,6 +109,7 @@ function del(id, callback) {
|
||||
var queries = [];
|
||||
queries.push({ query: 'DELETE FROM groupMembers WHERE groupId = ?', args: [ id ] });
|
||||
queries.push({ query: 'DELETE FROM groups WHERE id = ?', args: [ id ] });
|
||||
queries.push({ query: 'DELETE FROM mailboxes WHERE ownerId=?', args: [ id ] });
|
||||
|
||||
database.transaction(queries, function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
@@ -152,7 +133,7 @@ function clear(callback) {
|
||||
database.query('DELETE FROM groupMembers', function (error) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
database.query('DELETE FROM groups', function (error) {
|
||||
database.query('DELETE FROM groups WHERE id != ?', [ 'admin' ], function (error) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(error);
|
||||
@@ -191,7 +172,7 @@ function setMembers(groupId, userIds, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getMembership(userId, callback) {
|
||||
function getGroups(userId, callback) {
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -203,7 +184,7 @@ function getMembership(userId, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function setMembership(userId, groupIds, callback) {
|
||||
function setGroups(userId, groupIds, callback) {
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert(Array.isArray(groupIds));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -261,14 +242,6 @@ function isMember(groupId, userId, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getGroups(userId, callback) {
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + GROUPS_FIELDS + ' ' +
|
||||
' FROM groups INNER JOIN groupMembers ON groups.id = groupMembers.groupId AND groupMembers.userId = ?', [ userId ], function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, results);
|
||||
});
|
||||
function addDefaultGroups(callback) {
|
||||
add(constants.ADMIN_GROUP_ID, 'admin', callback);
|
||||
}
|
||||
|
||||
+50
-79
@@ -1,12 +1,11 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
GroupsError: GroupsError,
|
||||
GroupError: GroupError,
|
||||
|
||||
create: create,
|
||||
remove: remove,
|
||||
get: get,
|
||||
update: update,
|
||||
getWithMembers: getWithMembers,
|
||||
getAll: getAll,
|
||||
getAllWithMembers: getAllWithMembers,
|
||||
@@ -18,9 +17,7 @@ exports = module.exports = {
|
||||
isMember: isMember,
|
||||
|
||||
getGroups: getGroups,
|
||||
|
||||
setMembership: setMembership,
|
||||
getMembership: getMembership
|
||||
setGroups: setGroups
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
@@ -28,12 +25,11 @@ var assert = require('assert'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
groupdb = require('./groupdb.js'),
|
||||
util = require('util'),
|
||||
uuid = require('uuid'),
|
||||
_ = require('underscore');
|
||||
uuid = require('uuid');
|
||||
|
||||
// http://dustinsenos.com/articles/customErrorsInNode
|
||||
// http://code.google.com/p/v8/wiki/JavaScriptStackTraceApi
|
||||
function GroupsError(reason, errorOrMessage) {
|
||||
function GroupError(reason, errorOrMessage) {
|
||||
assert.strictEqual(typeof reason, 'string');
|
||||
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
|
||||
|
||||
@@ -51,25 +47,28 @@ function GroupsError(reason, errorOrMessage) {
|
||||
this.nestedError = errorOrMessage;
|
||||
}
|
||||
}
|
||||
util.inherits(GroupsError, Error);
|
||||
GroupsError.INTERNAL_ERROR = 'Internal Error';
|
||||
GroupsError.ALREADY_EXISTS = 'Already Exists';
|
||||
GroupsError.NOT_FOUND = 'Not Found';
|
||||
GroupsError.BAD_FIELD = 'Field error';
|
||||
GroupsError.NOT_EMPTY = 'Not Empty';
|
||||
GroupsError.NOT_ALLOWED = 'Not Allowed';
|
||||
util.inherits(GroupError, Error);
|
||||
GroupError.INTERNAL_ERROR = 'Internal Error';
|
||||
GroupError.ALREADY_EXISTS = 'Already Exists';
|
||||
GroupError.NOT_FOUND = 'Not Found';
|
||||
GroupError.BAD_FIELD = 'Field error';
|
||||
GroupError.NOT_EMPTY = 'Not Empty';
|
||||
GroupError.NOT_ALLOWED = 'Not Allowed';
|
||||
|
||||
// keep this in sync with validateUsername
|
||||
function validateGroupname(name) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
|
||||
if (name.length < 1) return new GroupsError(GroupsError.BAD_FIELD, 'name must be atleast 1 char');
|
||||
if (name.length >= 200) return new GroupsError(GroupsError.BAD_FIELD, 'name too long');
|
||||
if (name.length < 1) return new GroupError(GroupError.BAD_FIELD, 'name must be atleast 1 char');
|
||||
if (name.length >= 200) return new GroupError(GroupError.BAD_FIELD, 'name too long');
|
||||
|
||||
if (constants.RESERVED_NAMES.indexOf(name) !== -1) return new GroupsError(GroupsError.BAD_FIELD, 'name is reserved');
|
||||
if (constants.RESERVED_NAMES.indexOf(name) !== -1) return new GroupError(GroupError.BAD_FIELD, 'name is reserved');
|
||||
|
||||
// need to consider valid LDAP characters here (e.g '+' is reserved)
|
||||
if (/[^a-zA-Z0-9.-]/.test(name)) return new GroupsError(GroupsError.BAD_FIELD, 'name can only contain alphanumerals, hyphen and dot');
|
||||
// +/- can be tricky in emails. also need to consider valid LDAP characters here (e.g '+' is reserved)
|
||||
if (/[^a-zA-Z0-9.]/.test(name)) return new GroupError(GroupError.BAD_FIELD, 'name can only contain alphanumerals and dot');
|
||||
|
||||
// app emails are sent using the .app suffix
|
||||
if (name.indexOf('.app') !== -1) return new GroupError(GroupError.BAD_FIELD, 'name pattern is reserved for apps');
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -86,8 +85,8 @@ function create(name, callback) {
|
||||
|
||||
var id = 'gid-' + uuid.v4();
|
||||
groupdb.add(id, name, function (error) {
|
||||
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new GroupsError(GroupsError.ALREADY_EXISTS));
|
||||
if (error) return callback(new GroupsError(GroupsError.INTERNAL_ERROR, error));
|
||||
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new GroupError(GroupError.ALREADY_EXISTS));
|
||||
if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, { id: id, name: name });
|
||||
});
|
||||
@@ -97,9 +96,12 @@ function remove(id, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// never allow admin group to be deleted
|
||||
if (id === constants.ADMIN_GROUP_ID) return callback(new GroupError(GroupError.NOT_ALLOWED));
|
||||
|
||||
groupdb.del(id, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupsError(GroupsError.NOT_FOUND));
|
||||
if (error) return callback(new GroupsError(GroupsError.INTERNAL_ERROR, 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);
|
||||
});
|
||||
@@ -110,8 +112,8 @@ function get(id, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
groupdb.get(id, function (error, result) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupsError(GroupsError.NOT_FOUND));
|
||||
if (error) return callback(new GroupsError(GroupsError.INTERNAL_ERROR, 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));
|
||||
|
||||
return callback(null, result);
|
||||
});
|
||||
@@ -122,8 +124,8 @@ function getWithMembers(id, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
groupdb.getWithMembers(id, function (error, result) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupsError(GroupsError.NOT_FOUND));
|
||||
if (error) return callback(new GroupsError(GroupsError.INTERNAL_ERROR, 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));
|
||||
|
||||
return callback(null, result);
|
||||
});
|
||||
@@ -133,7 +135,7 @@ function getAll(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
groupdb.getAll(function (error, result) {
|
||||
if (error) return callback(new GroupsError(GroupsError.INTERNAL_ERROR, error));
|
||||
if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, error));
|
||||
|
||||
return callback(null, result);
|
||||
});
|
||||
@@ -143,7 +145,7 @@ function getAllWithMembers(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
groupdb.getAllWithMembers(function (error, result) {
|
||||
if (error) return callback(new GroupsError(GroupsError.INTERNAL_ERROR, error));
|
||||
if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, error));
|
||||
|
||||
return callback(null, result);
|
||||
});
|
||||
@@ -154,33 +156,33 @@ function getMembers(groupId, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
groupdb.getMembers(groupId, function (error, result) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupsError(GroupsError.NOT_FOUND));
|
||||
if (error) return callback(new GroupsError(GroupsError.INTERNAL_ERROR, 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));
|
||||
|
||||
return callback(null, result);
|
||||
});
|
||||
}
|
||||
|
||||
function getMembership(userId, callback) {
|
||||
function getGroups(userId, callback) {
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
groupdb.getMembership(userId, function (error, result) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupsError(GroupsError.NOT_FOUND));
|
||||
if (error) return callback(new GroupsError(GroupsError.INTERNAL_ERROR, error));
|
||||
groupdb.getGroups(userId, function (error, result) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupError(GroupError.NOT_FOUND));
|
||||
if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, error));
|
||||
|
||||
return callback(null, result);
|
||||
});
|
||||
}
|
||||
|
||||
function setMembership(userId, groupIds, callback) {
|
||||
function setGroups(userId, groupIds, callback) {
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert(Array.isArray(groupIds));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
groupdb.setMembership(userId, groupIds, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupsError(GroupsError.NOT_FOUND));
|
||||
if (error) return callback(new GroupsError(GroupsError.INTERNAL_ERROR, error));
|
||||
groupdb.setGroups(userId, groupIds, 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));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
@@ -192,8 +194,8 @@ function addMember(groupId, userId, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
groupdb.addMember(groupId, userId, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupsError(GroupsError.NOT_FOUND));
|
||||
if (error) return callback(new GroupsError(GroupsError.INTERNAL_ERROR, 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));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
@@ -205,8 +207,8 @@ function setMembers(groupId, userIds, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
groupdb.setMembers(groupId, userIds, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupsError(GroupsError.NOT_FOUND, 'Invalid group or user id'));
|
||||
if (error) return callback(new GroupsError(GroupsError.INTERNAL_ERROR, error));
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupError(GroupError.NOT_FOUND, 'Invalid group or user id'));
|
||||
if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, error));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
@@ -218,8 +220,8 @@ function removeMember(groupId, userId, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
groupdb.removeMember(groupId, userId, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupsError(GroupsError.NOT_FOUND));
|
||||
if (error) return callback(new GroupsError(GroupsError.INTERNAL_ERROR, 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));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
@@ -231,40 +233,9 @@ function isMember(groupId, userId, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
groupdb.isMember(groupId, userId, function (error, result) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupsError(GroupsError.NOT_FOUND));
|
||||
if (error) return callback(new GroupsError(GroupsError.INTERNAL_ERROR, 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));
|
||||
|
||||
return callback(null, result);
|
||||
});
|
||||
}
|
||||
|
||||
function update(groupId, data, callback) {
|
||||
assert.strictEqual(typeof groupId, 'string');
|
||||
assert(data && typeof data === 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
let error;
|
||||
if ('name' in data) {
|
||||
assert.strictEqual(typeof data.name, 'string');
|
||||
error = validateGroupname(data.name);
|
||||
if (error) return callback(error);
|
||||
}
|
||||
|
||||
groupdb.update(groupId, _.pick(data, 'name'), function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupsError(GroupsError.NOT_FOUND));
|
||||
if (error) return callback(new GroupsError(GroupsError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function getGroups(userId, callback) {
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
groupdb.getGroups(userId, function (error, results) {
|
||||
if (error) return callback(new GroupsError(GroupsError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, results);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = hat;
|
||||
|
||||
var crypto = require('crypto');
|
||||
|
||||
function hat (bits) {
|
||||
return crypto.randomBytes(bits / 8).toString('hex');
|
||||
}
|
||||
@@ -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.11.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.1.0' },
|
||||
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:1.1.0' },
|
||||
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:1.1.0' },
|
||||
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:1.0.0' },
|
||||
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:1.4.0' },
|
||||
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:1.0.0' }
|
||||
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:0.18.0' },
|
||||
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:0.17.1' },
|
||||
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:0.13.0' },
|
||||
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:0.11.0' },
|
||||
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:0.40.0' },
|
||||
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:0.12.0' }
|
||||
}
|
||||
};
|
||||
|
||||
+115
-171
@@ -13,11 +13,9 @@ var assert = require('assert'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
debug = require('debug')('box:ldap'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
users = require('./users.js'),
|
||||
UsersError = users.UsersError,
|
||||
user = require('./user.js'),
|
||||
UserError = user.UserError,
|
||||
ldap = require('ldapjs'),
|
||||
mail = require('./mail.js'),
|
||||
MailError = mail.MailError,
|
||||
mailboxdb = require('./mailboxdb.js'),
|
||||
safe = require('safetydance');
|
||||
|
||||
@@ -28,32 +26,37 @@ var NOOP = function () {};
|
||||
var GROUP_USERS_DN = 'cn=users,ou=groups,dc=cloudron';
|
||||
var GROUP_ADMINS_DN = 'cn=admins,ou=groups,dc=cloudron';
|
||||
|
||||
// Will attach req.app if successful
|
||||
function authenticateApp(req, res, next) {
|
||||
function getAppByRequest(req, callback) {
|
||||
assert.strictEqual(typeof req, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var sourceIp = req.connection.ldap.id.split(':')[0];
|
||||
if (sourceIp.split('.').length !== 4) return next(new ldap.InsufficientAccessRightsError('Missing source identifier'));
|
||||
if (sourceIp.split('.').length !== 4) return callback(new ldap.InsufficientAccessRightsError('Missing source identifier'));
|
||||
|
||||
apps.getByIpAddress(sourceIp, function (error, app) {
|
||||
if (error) return next(new ldap.OperationsError(error.message));
|
||||
if (!app) return next(new ldap.OperationsError('Could not detect app source'));
|
||||
if (error) return callback(new ldap.OperationsError(error.message));
|
||||
|
||||
req.app = app;
|
||||
if (!app) return callback(new ldap.OperationsError('Could not detect app source'));
|
||||
|
||||
next();
|
||||
callback(null, app);
|
||||
});
|
||||
}
|
||||
|
||||
function getUsersWithAccessToApp(req, callback) {
|
||||
assert.strictEqual(typeof req.app, 'object');
|
||||
assert.strictEqual(typeof req, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
users.list(function (error, result) {
|
||||
if (error) return callback(new ldap.OperationsError(error.toString()));
|
||||
getAppByRequest(req, function (error, app) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.filter(result, apps.hasAccessTo.bind(null, req.app), function (error, allowedUsers) {
|
||||
user.list(function (error, result){
|
||||
if (error) return callback(new ldap.OperationsError(error.toString()));
|
||||
|
||||
callback(null, allowedUsers);
|
||||
async.filter(result, apps.hasAccessTo.bind(null, app), function (error, result) {
|
||||
if (error) return callback(new ldap.OperationsError(error.toString()));
|
||||
|
||||
callback(null, result);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -134,7 +137,7 @@ function userSearch(req, res, next) {
|
||||
var dn = ldap.parseDN('cn=' + entry.id + ',ou=users,dc=cloudron');
|
||||
|
||||
var groups = [ GROUP_USERS_DN ];
|
||||
if (entry.admin || req.app.ownerId === entry.id) groups.push(GROUP_ADMINS_DN);
|
||||
if (entry.admin) groups.push(GROUP_ADMINS_DN);
|
||||
|
||||
var displayName = entry.displayName || entry.username || ''; // displayName can be empty and username can be null
|
||||
var nameParts = displayName.split(' ');
|
||||
@@ -149,12 +152,12 @@ 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,
|
||||
samaccountname: entry.username, // to support ActiveDirectory clients
|
||||
isadmin: (entry.admin || req.app.ownerId === entry.id) ? 1 : 0,
|
||||
isadmin: entry.admin ? 1 : 0,
|
||||
memberof: groups
|
||||
}
|
||||
};
|
||||
@@ -194,7 +197,7 @@ function groupSearch(req, res, next) {
|
||||
|
||||
groups.forEach(function (group) {
|
||||
var dn = ldap.parseDN('cn=' + group.name + ',ou=groups,dc=cloudron');
|
||||
var members = group.admin ? result.filter(function (entry) { return entry.admin || req.app.ownerId === entry.id; }) : result;
|
||||
var members = group.admin ? result.filter(function (entry) { return entry.admin; }) : result;
|
||||
|
||||
var obj = {
|
||||
dn: dn.toString(),
|
||||
@@ -243,7 +246,7 @@ function groupAdminsCompare(req, res, next) {
|
||||
// we only support memberuid here, if we add new group attributes later add them here
|
||||
if (req.attribute === 'memberuid') {
|
||||
var found = result.find(function (u) { return u.id === req.value; });
|
||||
if (found && (found.admin || req.app.ownerId == found.id)) return res.end(true);
|
||||
if (found && found.admin) return res.end(true);
|
||||
}
|
||||
|
||||
res.end(false);
|
||||
@@ -253,83 +256,41 @@ function groupAdminsCompare(req, res, next) {
|
||||
function mailboxSearch(req, res, next) {
|
||||
debug('mailbox search: dn %s, scope %s, filter %s (from %s)', req.dn.toString(), req.scope, req.filter.toString(), req.connection.ldap.id);
|
||||
|
||||
// if cn is set we only search for one mailbox specifically
|
||||
if (req.dn.rdns[0].attrs.cn) {
|
||||
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()));
|
||||
if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
|
||||
mailboxdb.getMailbox(parts[0], parts[1], function (error, mailbox) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
if (error) return next(new ldap.OperationsError(error.toString()));
|
||||
|
||||
var obj = {
|
||||
dn: req.dn.toString(),
|
||||
attributes: {
|
||||
objectclass: ['mailbox'],
|
||||
objectcategory: 'mailbox',
|
||||
cn: `${mailbox.name}@${mailbox.domain}`,
|
||||
uid: `${mailbox.name}@${mailbox.domain}`,
|
||||
mail: `${mailbox.name}@${mailbox.domain}`,
|
||||
ownerType: mailbox.ownerType,
|
||||
displayname: 'Max Mustermann',
|
||||
givenName: 'Max',
|
||||
username: 'mmustermann',
|
||||
samaccountname: 'mmustermann'
|
||||
}
|
||||
};
|
||||
|
||||
// ensure all filter values are also lowercase
|
||||
var lowerCaseFilter = safe(function () { return ldap.parseFilter(req.filter.toString().toLowerCase()); }, null);
|
||||
if (!lowerCaseFilter) return next(new ldap.OperationsError(safe.error.toString()));
|
||||
|
||||
if (lowerCaseFilter.matches(obj.attributes)) {
|
||||
finalSend([ obj ], req, res, next);
|
||||
} else {
|
||||
res.end();
|
||||
}
|
||||
});
|
||||
} else if (req.dn.rdns[0].attrs.domain) {
|
||||
var domain = req.dn.rdns[0].attrs.domain.value.toLowerCase();
|
||||
|
||||
mailboxdb.listMailboxes(domain, function (error, result) {
|
||||
if (error) return next(new ldap.OperationsError(error.toString()));
|
||||
|
||||
var results = [];
|
||||
|
||||
// only send user mailboxes
|
||||
result = result.filter(function (m) { return m.ownerType === mailboxdb.OWNER_TYPE_USER; });
|
||||
|
||||
// send mailbox objects
|
||||
result.forEach(function (mailbox) {
|
||||
var dn = ldap.parseDN(`cn=${mailbox.name}@${domain},domain=${domain},ou=mailboxes,dc=cloudron`);
|
||||
|
||||
var obj = {
|
||||
dn: dn.toString(),
|
||||
attributes: {
|
||||
objectclass: ['mailbox'],
|
||||
objectcategory: 'mailbox',
|
||||
cn: `${mailbox.name}@${domain}`,
|
||||
uid: `${mailbox.name}@${domain}`,
|
||||
mail: `${mailbox.name}@${domain}`,
|
||||
ownerType: mailbox.ownerType
|
||||
}
|
||||
};
|
||||
|
||||
// ensure all filter values are also lowercase
|
||||
var lowerCaseFilter = safe(function () { return ldap.parseFilter(req.filter.toString().toLowerCase()); }, null);
|
||||
if (!lowerCaseFilter) return next(new ldap.OperationsError(safe.error.toString()));
|
||||
|
||||
if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && lowerCaseFilter.matches(obj.attributes)) {
|
||||
results.push(obj);
|
||||
}
|
||||
});
|
||||
|
||||
finalSend(results, req, res, next);
|
||||
});
|
||||
} else {
|
||||
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(name, config.fqdn(), function (error, mailbox) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
if (error) return next(new ldap.OperationsError(error.toString()));
|
||||
|
||||
var obj = {
|
||||
dn: req.dn.toString(),
|
||||
attributes: {
|
||||
objectclass: ['mailbox'],
|
||||
objectcategory: 'mailbox',
|
||||
cn: mailbox.name,
|
||||
uid: mailbox.name,
|
||||
mail: mailbox.name + '@' + config.fqdn(),
|
||||
ownerType: mailbox.ownerType
|
||||
}
|
||||
};
|
||||
|
||||
// ensure all filter values are also lowercase
|
||||
var lowerCaseFilter = safe(function () { return ldap.parseFilter(req.filter.toString().toLowerCase()); }, null);
|
||||
if (!lowerCaseFilter) return next(new ldap.OperationsError(safe.error.toString()));
|
||||
|
||||
if (lowerCaseFilter.matches(obj.attributes)) {
|
||||
finalSend([ obj ], req, res, next);
|
||||
} else {
|
||||
res.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function mailAliasSearch(req, res, next) {
|
||||
@@ -337,24 +298,19 @@ function mailAliasSearch(req, res, next) {
|
||||
|
||||
if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
|
||||
var email = req.dn.rdns[0].attrs.cn.value.toLowerCase();
|
||||
var parts = email.split('@');
|
||||
if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
|
||||
mailboxdb.getAlias(parts[0], parts[1], function (error, alias) {
|
||||
mailboxdb.getAlias(req.dn.rdns[0].attrs.cn.value.toLowerCase(), config.fqdn(), function (error, alias) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
if (error) return next(new ldap.OperationsError(error.toString()));
|
||||
|
||||
// https://wiki.debian.org/LDAP/MigrationTools/Examples
|
||||
// https://docs.oracle.com/cd/E19455-01/806-5580/6jej518pp/index.html
|
||||
// member is fully qualified - https://docs.oracle.com/cd/E19957-01/816-6082-10/chap4.doc.html#43314
|
||||
var obj = {
|
||||
dn: req.dn.toString(),
|
||||
attributes: {
|
||||
objectclass: ['nisMailAlias'],
|
||||
objectcategory: 'nisMailAlias',
|
||||
cn: `${alias.name}@${alias.domain}`,
|
||||
rfc822MailMember: `${alias.aliasTarget}@${alias.domain}`
|
||||
cn: alias.name,
|
||||
rfc822MailMember: alias.aliasTarget
|
||||
}
|
||||
};
|
||||
|
||||
@@ -375,24 +331,19 @@ function mailingListSearch(req, res, next) {
|
||||
|
||||
if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
|
||||
var email = req.dn.rdns[0].attrs.cn.value.toLowerCase();
|
||||
var parts = email.split('@');
|
||||
if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
|
||||
mailboxdb.getGroup(parts[0], parts[1], function (error, group) {
|
||||
mailboxdb.getGroup(req.dn.rdns[0].attrs.cn.value.toLowerCase(), config.fqdn(), function (error, group) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
if (error) return next(new ldap.OperationsError(error.toString()));
|
||||
|
||||
// http://ldapwiki.willeke.com/wiki/Original%20Mailgroup%20Schema%20From%20Netscape
|
||||
// members are fully qualified (https://docs.oracle.com/cd/E19444-01/816-6018-10/groups.htm#13356)
|
||||
var obj = {
|
||||
dn: req.dn.toString(),
|
||||
attributes: {
|
||||
objectclass: ['mailGroup'],
|
||||
objectcategory: 'mailGroup',
|
||||
cn: `${group.name}@${group.domain}`, // fully qualified
|
||||
mail: `${group.name}@${group.domain}`,
|
||||
mgrpRFC822MailMember: group.members.map(function (m) { return `${m}@${group.domain}`; })
|
||||
cn: group.name,
|
||||
mail: group.name + '@' + config.fqdn(),
|
||||
mgrpRFC822MailMember: group.members
|
||||
}
|
||||
};
|
||||
|
||||
@@ -408,7 +359,6 @@ function mailingListSearch(req, res, next) {
|
||||
});
|
||||
}
|
||||
|
||||
// Will attach req.user if successful
|
||||
function authenticateUser(req, res, next) {
|
||||
debug('user bind: %s (from %s)', req.dn.toString(), req.connection.ldap.id);
|
||||
|
||||
@@ -419,18 +369,18 @@ function authenticateUser(req, res, next) {
|
||||
|
||||
var api;
|
||||
if (attributeName === 'mail') {
|
||||
api = users.verifyWithEmail;
|
||||
api = user.verifyWithEmail;
|
||||
} else if (commonName.indexOf('@') !== -1) { // if mail is specified, enforce mail check
|
||||
api = users.verifyWithEmail;
|
||||
api = user.verifyWithEmail;
|
||||
} else if (commonName.indexOf('uid-') === 0) {
|
||||
api = users.verify;
|
||||
api = user.verify;
|
||||
} else {
|
||||
api = users.verifyWithUsername;
|
||||
api = user.verifyWithUsername;
|
||||
}
|
||||
|
||||
api(commonName, req.credentials || '', function (error, user) {
|
||||
if (error && error.reason === UsersError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
if (error && error.reason === UsersError.WRONG_PASSWORD) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
|
||||
if (error && 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));
|
||||
|
||||
req.user = user;
|
||||
@@ -440,67 +390,62 @@ function authenticateUser(req, res, next) {
|
||||
}
|
||||
|
||||
function authorizeUserForApp(req, res, next) {
|
||||
assert.strictEqual(typeof req.user, 'object');
|
||||
assert.strictEqual(typeof req.app, 'object');
|
||||
assert(req.user);
|
||||
|
||||
apps.hasAccessTo(req.app, req.user, function (error, result) {
|
||||
if (error) return next(new ldap.OperationsError(error.toString()));
|
||||
getAppByRequest(req, function (error, app) {
|
||||
if (error) return next(error);
|
||||
|
||||
// we return no such object, to avoid leakage of a users existence
|
||||
if (!result) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
apps.hasAccessTo(app, req.user, function (error, result) {
|
||||
if (error) return next(new ldap.OperationsError(error.toString()));
|
||||
|
||||
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', appId: req.app.id, app: req.app }, { userId: req.user.id, user: users.removePrivateFields(req.user) });
|
||||
// we return no such object, to avoid leakage of a users existence
|
||||
if (!result) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
|
||||
res.end();
|
||||
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', appId: app.id }, { userId: req.user.id });
|
||||
|
||||
res.end();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function authenticateMailbox(req, res, next) {
|
||||
debug('mailbox auth: %s (from %s)', req.dn.toString(), req.connection.ldap.id);
|
||||
|
||||
if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
|
||||
var email = req.dn.rdns[0].attrs.cn.value.toLowerCase();
|
||||
var parts = email.split('@');
|
||||
if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
var name = req.dn.rdns[0].attrs.cn.value.toLowerCase();
|
||||
|
||||
mailboxdb.getMailbox(parts[0], parts[1], function (error, mailbox) {
|
||||
// allow login via email
|
||||
var parts = name.split('@');
|
||||
if (parts[1] === config.fqdn()) {
|
||||
name = parts[0];
|
||||
}
|
||||
|
||||
mailboxdb.getMailbox(name, config.fqdn(), function (error, mailbox) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
if (error) return next(new ldap.OperationsError(error.message));
|
||||
|
||||
mail.getDomain(parts[1], function (error, domain) {
|
||||
if (error && error.reason === MailError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
if (error) return next(new ldap.OperationsError(error.message));
|
||||
if (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.OWNER_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.OWNER_TYPE_USER) {
|
||||
if (!domain.enabled) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
|
||||
users.verify(mailbox.ownerId, req.credentials || '', function (error, result) {
|
||||
if (error && error.reason === UsersError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
if (error && error.reason === UsersError.WRONG_PASSWORD) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
|
||||
if (error) return next(new ldap.OperationsError(error.message));
|
||||
|
||||
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: email }, { userId: result.id, user: users.removePrivateFields(result) });
|
||||
res.end();
|
||||
});
|
||||
} 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'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -518,21 +463,20 @@ function start(callback) {
|
||||
|
||||
gServer = ldap.createServer({ log: logger });
|
||||
|
||||
gServer.search('ou=users,dc=cloudron', authenticateApp, userSearch);
|
||||
gServer.search('ou=groups,dc=cloudron', authenticateApp, groupSearch);
|
||||
gServer.bind('ou=users,dc=cloudron', authenticateApp, authenticateUser, authorizeUserForApp);
|
||||
gServer.search('ou=users,dc=cloudron', userSearch);
|
||||
gServer.search('ou=groups,dc=cloudron', groupSearch);
|
||||
gServer.bind('ou=users,dc=cloudron', authenticateUser, authorizeUserForApp);
|
||||
|
||||
// http://www.ietf.org/proceedings/43/I-D/draft-srivastava-ldap-mail-00.txt
|
||||
gServer.search('ou=mailboxes,dc=cloudron', mailboxSearch);
|
||||
gServer.search('ou=mailaliases,dc=cloudron', mailAliasSearch);
|
||||
gServer.search('ou=mailinglists,dc=cloudron', mailingListSearch);
|
||||
|
||||
gServer.bind('ou=mailboxes,dc=cloudron', authenticateMailbox);
|
||||
gServer.bind('ou=recvmail,dc=cloudron', authenticateMailbox);
|
||||
gServer.bind('ou=sendmail,dc=cloudron', authenticateMailbox);
|
||||
|
||||
gServer.compare('cn=users,ou=groups,dc=cloudron', authenticateApp, groupUsersCompare);
|
||||
gServer.compare('cn=admins,ou=groups,dc=cloudron', authenticateApp, groupAdminsCompare);
|
||||
gServer.compare('cn=users,ou=groups,dc=cloudron', groupUsersCompare);
|
||||
gServer.compare('cn=admins,ou=groups,dc=cloudron', groupAdminsCompare);
|
||||
|
||||
// this is the bind for addons (after bind, they might search and authenticate)
|
||||
gServer.bind('ou=addons,dc=cloudron', function(req, res /*, next */) {
|
||||
|
||||
-1099
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user