Compare commits

..

7 Commits

Author SHA1 Message Date
Girish Ramakrishnan 7a63512d21 cloudflare: del returns 200 now 2018-01-31 12:04:12 +01:00
Girish Ramakrishnan 7ed18b3016 update cloudron-manifestformat (for prerelease in semver) 2018-01-30 16:37:41 -08:00
Girish Ramakrishnan 0d3d7d2b88 1.9.4 changes 2018-01-30 15:30:56 -08:00
Girish Ramakrishnan 457f76367c Fix typo that is causing LE auto-renewal to fail 2018-01-30 15:28:52 -08:00
Johannes Zellner 74b3d06cd7 Add 1.9.3 changes
From 8bb15aedbd (1.9.3 branch)
2018-01-30 15:27:23 -08:00
Johannes Zellner d22f385402 The DNS provider property moved to the root dns config object
From 0bd6d3b9f8 (1.9.3 branch)
2018-01-30 15:27:06 -08:00
Girish Ramakrishnan 08f920afee 1.9.2 changes 2018-01-17 10:49:33 -08:00
248 changed files with 53596 additions and 7193 deletions
+1
View File
@@ -1,6 +1,7 @@
node_modules/
coverage/
webadmin/dist/
setup/splash/website/
installer/src/certs/server.key
# vim swap files
-26
View File
@@ -1184,29 +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
+1 -1
View File
@@ -29,7 +29,7 @@ function create_droplet() {
local ssh_key_id="$1"
local box_name="$2"
local image_region="sfo2"
local image_region="sfo1"
local ubuntu_image_slug="ubuntu-16-04-x64"
local box_size="1gb"
+1 -8
View File
@@ -2,8 +2,6 @@
'use strict';
var database = require('./src/database.js');
var sendFailureLogs = require('./src/logcollector').sendFailureLogs;
function main() {
@@ -12,12 +10,7 @@ function main() {
var processName = process.argv[2];
console.log('Started crash notifier for', processName);
// mailer needs the db
database.initialize(function (error) {
if (error) return console.error('Cannot connect to database. Unable to send crash log.', error);
sendFailureLogs(processName, { unit: processName });
});
sendFailureLogs(processName, { unit: processName });
}
main();
+257
View File
@@ -0,0 +1,257 @@
/* jslint node:true */
'use strict';
var argv = require('yargs').argv,
autoprefixer = require('gulp-autoprefixer'),
concat = require('gulp-concat'),
cssnano = require('gulp-cssnano'),
ejs = require('gulp-ejs'),
gulp = require('gulp'),
rimraf = require('rimraf'),
sass = require('gulp-sass'),
serve = require('gulp-serve'),
sourcemaps = require('gulp-sourcemaps'),
uglify = require('gulp-uglify'),
url = require('url');
gulp.task('3rdparty', function () {
gulp.src([
'webadmin/src/3rdparty/**/*.js',
'webadmin/src/3rdparty/**/*.map',
'webadmin/src/3rdparty/**/*.css',
'webadmin/src/3rdparty/**/*.otf',
'webadmin/src/3rdparty/**/*.eot',
'webadmin/src/3rdparty/**/*.svg',
'webadmin/src/3rdparty/**/*.gif',
'webadmin/src/3rdparty/**/*.ttf',
'webadmin/src/3rdparty/**/*.woff',
'webadmin/src/3rdparty/**/*.woff2'
])
.pipe(gulp.dest('webadmin/dist/3rdparty/'))
.pipe(gulp.dest('setup/splash/website/3rdparty'));
gulp.src('node_modules/bootstrap-sass/assets/javascripts/bootstrap.min.js')
.pipe(gulp.dest('webadmin/dist/3rdparty/js'))
.pipe(gulp.dest('setup/splash/website/3rdparty/js'));
});
// --------------
// JavaScript
// --------------
if (argv.help || argv.h) {
console.log('Supported arguments for "gulp develop":');
console.log(' --client-id <clientId>');
console.log(' --client-secret <clientSecret>');
console.log(' --api-origin <cloudron api uri>');
process.exit(1);
}
gulp.task('js', ['js-index', 'js-logs', 'js-terminal', 'js-setup', 'js-setupdns', 'js-restore', 'js-update'], function () {});
var oauth = {
clientId: argv.clientId || 'cid-webadmin',
clientSecret: argv.clientSecret || 'unused',
apiOrigin: argv.apiOrigin || '',
apiOriginHostname: argv.apiOrigin ? url.parse(argv.apiOrigin).hostname : ''
};
console.log();
console.log('Using OAuth credentials:');
console.log(' ClientId: %s', oauth.clientId);
console.log(' ClientSecret: %s', oauth.clientSecret);
console.log(' Cloudron API: %s', oauth.apiOrigin || 'default');
console.log(' Cloudron Host: %s', oauth.apiOriginHostname);
console.log();
gulp.task('js-index', function () {
// needs special treatment for error handling
var uglifyer = uglify();
uglifyer.on('error', function (error) {
console.error(error);
});
gulp.src([
'webadmin/src/js/index.js',
'webadmin/src/js/client.js',
'webadmin/src/js/appstore.js',
'webadmin/src/js/main.js',
'webadmin/src/views/*.js'
])
.pipe(ejs({ oauth: oauth }, {}, { ext: '.js' }))
.pipe(sourcemaps.init())
.pipe(concat('index.js', { newLine: ';' }))
.pipe(uglifyer)
.pipe(sourcemaps.write())
.pipe(gulp.dest('webadmin/dist/js'));
});
gulp.task('js-logs', function () {
// needs special treatment for error handling
var uglifyer = uglify();
uglifyer.on('error', function (error) {
console.error(error);
});
gulp.src(['webadmin/src/js/logs.js', 'webadmin/src/js/client.js'])
.pipe(ejs({ oauth: oauth }, {}, { ext: '.js' }))
.pipe(sourcemaps.init())
.pipe(concat('logs.js', { newLine: ';' }))
.pipe(uglifyer)
.pipe(sourcemaps.write())
.pipe(gulp.dest('webadmin/dist/js'));
});
gulp.task('js-terminal', function () {
// needs special treatment for error handling
var uglifyer = uglify();
uglifyer.on('error', function (error) {
console.error(error);
});
gulp.src(['webadmin/src/js/terminal.js', 'webadmin/src/js/client.js'])
.pipe(ejs({ oauth: oauth }, {}, { ext: '.js' }))
.pipe(sourcemaps.init())
.pipe(concat('terminal.js', { newLine: ';' }))
.pipe(uglifyer)
.pipe(sourcemaps.write())
.pipe(gulp.dest('webadmin/dist/js'));
});
gulp.task('js-setup', function () {
// needs special treatment for error handling
var uglifyer = uglify();
uglifyer.on('error', function (error) {
console.error(error);
});
gulp.src(['webadmin/src/js/setup.js', 'webadmin/src/js/client.js'])
.pipe(ejs({ oauth: oauth }, {}, { ext: '.js' }))
.pipe(sourcemaps.init())
.pipe(concat('setup.js', { newLine: ';' }))
.pipe(uglifyer)
.pipe(sourcemaps.write())
.pipe(gulp.dest('webadmin/dist/js'));
});
gulp.task('js-setupdns', function () {
// needs special treatment for error handling
var uglifyer = uglify();
uglifyer.on('error', function (error) {
console.error(error);
});
gulp.src(['webadmin/src/js/setupdns.js', 'webadmin/src/js/client.js'])
.pipe(ejs({ oauth: oauth }, {}, { ext: '.js' }))
.pipe(sourcemaps.init())
.pipe(concat('setupdns.js', { newLine: ';' }))
.pipe(uglifyer)
.pipe(sourcemaps.write())
.pipe(gulp.dest('webadmin/dist/js'));
});
gulp.task('js-restore', function () {
// needs special treatment for error handling
var uglifyer = uglify();
uglifyer.on('error', function (error) {
console.error(error);
});
gulp.src(['webadmin/src/js/restore.js', 'webadmin/src/js/client.js'])
.pipe(ejs({ oauth: oauth }, {}, { ext: '.js' }))
.pipe(sourcemaps.init())
.pipe(concat('restore.js', { newLine: ';' }))
.pipe(uglifyer)
.pipe(sourcemaps.write())
.pipe(gulp.dest('webadmin/dist/js'));
});
gulp.task('js-update', function () {
// needs special treatment for error handling
var uglifyer = uglify();
uglifyer.on('error', function (error) {
console.error(error);
});
gulp.src(['webadmin/src/js/update.js'])
.pipe(sourcemaps.init())
.pipe(uglifyer)
.pipe(sourcemaps.write())
.pipe(gulp.dest('webadmin/dist/js'))
.pipe(gulp.dest('setup/splash/website/js'));
});
// --------------
// HTML
// --------------
gulp.task('html', ['html-views', 'html-update', 'html-templates'], function () {
return gulp.src('webadmin/src/*.html').pipe(ejs({ apiOriginHostname: oauth.apiOriginHostname }, {}, { ext: '.html' })).pipe(gulp.dest('webadmin/dist'));
});
gulp.task('html-update', function () {
return gulp.src(['webadmin/src/update.html']).pipe(gulp.dest('setup/splash/website'));
});
gulp.task('html-views', function () {
return gulp.src('webadmin/src/views/**/*.html').pipe(gulp.dest('webadmin/dist/views'));
});
gulp.task('html-templates', function () {
return gulp.src('webadmin/src/templates/**/*.html').pipe(gulp.dest('webadmin/dist/templates'));
});
// --------------
// CSS
// --------------
gulp.task('css', function () {
return gulp.src('webadmin/src/*.scss')
.pipe(sourcemaps.init())
.pipe(sass({ includePaths: ['node_modules/bootstrap-sass/assets/stylesheets/'] }).on('error', sass.logError))
.pipe(autoprefixer())
.pipe(cssnano())
.pipe(sourcemaps.write())
.pipe(gulp.dest('webadmin/dist'))
.pipe(gulp.dest('setup/splash/website'));
});
gulp.task('images', function () {
return gulp.src('webadmin/src/img/**')
.pipe(gulp.dest('webadmin/dist/img'));
});
// --------------
// Utilities
// --------------
gulp.task('watch', ['default'], function () {
gulp.watch(['webadmin/src/*.scss'], ['css']);
gulp.watch(['webadmin/src/img/*'], ['images']);
gulp.watch(['webadmin/src/**/*.html'], ['html']);
gulp.watch(['webadmin/src/views/*.html'], ['html-views']);
gulp.watch(['webadmin/src/templates/*.html'], ['html-templates']);
gulp.watch(['webadmin/src/js/update.js'], ['js-update']);
gulp.watch(['webadmin/src/js/setup.js', 'webadmin/src/js/client.js'], ['js-setup']);
gulp.watch(['webadmin/src/js/setupdns.js', 'webadmin/src/js/client.js'], ['js-setupdns']);
gulp.watch(['webadmin/src/js/restore.js', 'webadmin/src/js/client.js'], ['js-restore']);
gulp.watch(['webadmin/src/js/logs.js', 'webadmin/src/js/client.js'], ['js-logs']);
gulp.watch(['webadmin/src/js/terminal.js', 'webadmin/src/js/client.js'], ['js-terminal']);
gulp.watch(['webadmin/src/js/index.js', 'webadmin/src/js/client.js', 'webadmin/src/js/appstore.js', 'webadmin/src/js/main.js', 'webadmin/src/views/*.js'], ['js-index']);
gulp.watch(['webadmin/src/3rdparty/**/*'], ['3rdparty']);
});
gulp.task('clean', function () {
rimraf.sync('webadmin/dist');
rimraf.sync('setup/splash/website');
});
gulp.task('default', ['clean', 'html', 'js', '3rdparty', 'images', 'css'], function () {});
gulp.task('develop', ['watch'], serve({ root: 'webadmin/dist', port: 4000 }));
@@ -1,24 +0,0 @@
'use strict';
exports.up = function(db, callback) {
var cmd = 'CREATE TABLE IF NOT EXISTS mail(' +
'domain VARCHAR(128) NOT NULL UNIQUE,' +
'enabled BOOLEAN DEFAULT 0,' +
'mailFromValidation BOOLEAN DEFAULT 1,' +
'catchAllJson TEXT,' +
'relayJson TEXT,' +
'FOREIGN KEY(domain) REFERENCES domains(domain),' +
'PRIMARY KEY(domain)) CHARACTER SET utf8 COLLATE utf8_bin';
db.runSql(cmd, function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('DROP TABLE mail', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,34 +0,0 @@
'use strict';
exports.up = function(db, callback) {
db.all('SELECT * FROM domains', function (error, domains) {
if (error) return callback(error);
if (domains.length === 0) return callback();
db.all('SELECT * FROM settings', function (error, allSettings) {
if (error) return callback(error);
// defaults
var mailFromValidation = true;
var catchAll = [ ];
var relay = { provider: 'cloudron-smtp' };
var mailEnabled = false;
allSettings.forEach(function (setting) {
switch (setting.name) {
case 'mail_from_validation': mailFromValidation = !!setting.value; break;
case 'catch_all_address': catchAll = JSON.parse(setting.value); break;
case 'mail_relay': relay = JSON.parse(setting.value); break;
case 'mail_config': mailEnabled = JSON.parse(setting.value).enabled; break;
}
});
db.runSql('INSERT INTO mail (domain, enabled, mailFromValidation, catchAllJson, relayJson) VALUES (?, ?, ?, ?, ?)',
[ domains[0].domain, mailEnabled, mailFromValidation, JSON.stringify(catchAll), JSON.stringify(relay) ], callback);
});
});
};
exports.down = function(db, callback) {
callback();
};
@@ -1,44 +0,0 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
db.all('SELECT * FROM users', [ ], function (error, users) {
if (error) return callback(error);
db.all('SELECT * FROM mail WHERE enabled=1', [ ], function (error, mailDomains) {
if (error) return callback(error);
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
db.runSql.bind(db, 'ALTER TABLE users DROP INDEX users_email'),
db.runSql.bind(db, 'ALTER TABLE users ADD COLUMN fallbackEmail VARCHAR(512) DEFAULT ""'),
function setDefaults(done) {
async.eachSeries(users, function (user, iteratorCallback) {
var defaultEmail = '';
var fallbackEmail = '';
if (mailDomains.length === 0) {
defaultEmail = user.email;
fallbackEmail = user.email;
} else {
defaultEmail = user.username ? (user.username + '@' + mailDomains[0].domain) : user.email;
fallbackEmail = user.email;
}
db.runSql('UPDATE users SET email = ?, fallbackEmail = ? WHERE id = ?', [ defaultEmail, fallbackEmail, user.id ], iteratorCallback);
}, done);
},
db.runSql.bind(db, 'ALTER TABLE users ADD UNIQUE users_email (email)'),
db.runSql.bind(db, 'COMMIT')
], callback);
});
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE users DROP COLUMN fallbackEmail', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,26 +0,0 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
db.all('SELECT * FROM settings WHERE name = ?', [ 'tls_config' ], function (error, result) {
if (error) return callback(error);
var tlsConfig = (result[0] && result[0].value) ? JSON.parse(result[0].value) : { provider: 'letsencrypt-prod'};
tlsConfig.provider = tlsConfig.provider.replace(/$le\-/, 'letsencrypt-'); // old cloudrons had le-prod/le-staging
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
db.runSql.bind(db, 'ALTER TABLE domains ADD COLUMN tlsConfigJson TEXT'),
db.runSql.bind(db, 'UPDATE domains SET tlsConfigJson = ?', [ JSON.stringify(tlsConfig) ]),
db.runSql.bind(db, 'COMMIT')
], callback);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE domains DROP COLUMN tlsConfigJson', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,47 +0,0 @@
'use strict';
var async = require('async'),
fs = require('fs'),
superagent = require('superagent');
exports.up = function(db, callback) {
if (!fs.existsSync('/home/yellowtent/configs/cloudron.conf')) {
console.log('Unable to locate cloudron.conf');
return callback();
}
var config = JSON.parse(fs.readFileSync('/home/yellowtent/configs/cloudron.conf', 'utf8'));
if (config.provider !== 'caas' || !config.fqdn) {
console.log('Not caas (%s) or no fqdn', config.provider, config.fqdn);
return callback();
}
db.runSql('SELECT COUNT(*) AS total FROM users', function (error, result) {
if (error) return callback(error);
if (result[0].total === 0) {
console.log('This cloudron is not activated. It will automatically get appstore and caas configs from autoprovision logic');
return callback();
}
console.log('Downloading appstore and caas config');
superagent.get(config.apiServerOrigin + `/api/v1/boxes/${config.fqdn}/config`)
.query({ token: config.token })
.timeout(30 * 1000).end(function (error, result) {
if (error) return callback(error);
console.log('Adding %j config', result.body);
async.series([
db.runSql.bind(db, 'INSERT settings (name, value) VALUES(?, ?)', [ 'appstore_config', JSON.stringify(result.body.appstoreConfig) ]),
db.runSql.bind(db, 'INSERT settings (name, value) VALUES(?, ?)', [ 'caas_config', JSON.stringify(result.body.caasConfig) ])
], callback);
});
});
};
exports.down = function(db, callback) {
callback();
};
+1 -19
View File
@@ -23,8 +23,6 @@ CREATE TABLE IF NOT EXISTS users(
modifiedAt VARCHAR(512) NOT NULL,
admin INTEGER NOT NULL,
displayName VARCHAR(512) DEFAULT '',
fallbackEmail VARCHAR(512) DEFAULT ""
PRIMARY KEY(id));
CREATE TABLE IF NOT EXISTS groups(
@@ -147,31 +145,15 @@ CREATE TABLE IF NOT EXISTS mailboxes(
domain VARCHAR(128),
FOREIGN KEY(domain) REFERENCES domains(domain),
UNIQUE (name, 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 */
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;
+130 -50
View File
@@ -44,37 +44,55 @@ fi
initBaseImage="true"
# provisioning data
domain=""
adminLocation="my"
zoneName=""
provider=""
tlsProvider="le-prod"
requestedVersion=""
apiServerOrigin="https://api.cloudron.io"
webServerOrigin="https://cloudron.io"
dataJson=""
prerelease="false"
sourceTarballUrl=""
rebootServer="true"
baseDataDir=""
args=$(getopt -o "" -l "help,skip-baseimage-init,data-dir:,provider:,version:,env:,prerelease,skip-reboot" -n "$0" -- "$@")
# these are here for pre-1.9 compat
encryptionKey=""
restoreUrl=""
args=$(getopt -o "" -l "domain:,help,skip-baseimage-init,data:,data-dir:,provider:,encryption-key:,restore-url:,tls-provider:,version:,dns-provider:,env:,admin-location:,prerelease,skip-reboot,source-url:" -n "$0" -- "$@")
eval set -- "${args}"
while true; do
case "$1" in
--domain) domain="$2"; shift 2;;
--admin-location) adminLocation="$2"; shift 2;;
--help) echo "See https://cloudron.io/documentation/installation/ on how to install Cloudron"; exit 0;;
--provider) provider="$2"; shift 2;;
--encryption-key) encryptionKey="$2"; shift 2;;
--restore-url) restoreUrl="$2"; shift 2;;
--tls-provider) tlsProvider="$2"; shift 2;;
--version) requestedVersion="$2"; shift 2;;
--env)
if [[ "$2" == "dev" ]]; then
apiServerOrigin="https://api.dev.cloudron.io"
webServerOrigin="https://dev.cloudron.io"
tlsProvider="le-staging"
prerelease="true"
elif [[ "$2" == "staging" ]]; then
apiServerOrigin="https://api.staging.cloudron.io"
webServerOrigin="https://staging.cloudron.io"
tlsProvider="le-staging"
prerelease="true"
fi
shift 2;;
--skip-baseimage-init) initBaseImage="false"; shift;;
--skip-reboot) rebootServer="false"; shift;;
--data) dataJson="$2"; shift 2;;
--prerelease) prerelease="true"; shift;;
--source-url) sourceTarballUrl="$2"; version="0.0.1+custom"; shift 2;;
--data-dir) baseDataDir=$(realpath "$2"); shift 2;;
--) break;;
*) echo "Unknown option $1"; exit 1;;
@@ -82,34 +100,39 @@ while true; do
done
# validate arguments in the absence of data
if [[ -z "${provider}" ]]; then
echo "--provider is required (azure, cloudscale, digitalocean, ec2, exoscale, hetzner, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic)"
exit 1
elif [[ \
"${provider}" != "ami" && \
"${provider}" != "azure" && \
"${provider}" != "caas" && \
"${provider}" != "cloudscale" && \
"${provider}" != "digitalocean" && \
"${provider}" != "ec2" && \
"${provider}" != "exoscale" && \
"${provider}" != "gce" && \
"${provider}" != "hetzner" && \
"${provider}" != "lightsail" && \
"${provider}" != "linode" && \
"${provider}" != "ovh" && \
"${provider}" != "rosehosting" && \
"${provider}" != "scaleway" && \
"${provider}" != "vultr" && \
"${provider}" != "generic" \
]]; then
echo "--provider must be one of: azure, cloudscale.ch, digitalocean, ec2, exoscale, gce, hetzner, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic"
exit 1
fi
if [[ -z "${dataJson}" ]]; then
if [[ -z "${provider}" ]]; then
echo "--provider is required (azure, cloudscale.ch, digitalocean, ec2, exoscale, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic)"
exit 1
elif [[ \
"${provider}" != "ami" && \
"${provider}" != "azure" && \
"${provider}" != "cloudscale.ch" && \
"${provider}" != "digitalocean" && \
"${provider}" != "ec2" && \
"${provider}" != "exoscale" && \
"${provider}" != "gce" && \
"${provider}" != "lightsail" && \
"${provider}" != "linode" && \
"${provider}" != "ovh" && \
"${provider}" != "rosehosting" && \
"${provider}" != "scaleway" && \
"${provider}" != "vultr" && \
"${provider}" != "generic" \
]]; then
echo "--provider must be one of: azure, cloudscale.ch, digitalocean, ec2, exoscale, gce, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic"
exit 1
fi
if [[ -n "${baseDataDir}" && ! -d "${baseDataDir}" ]]; then
echo "${baseDataDir} does not exist"
exit 1
if [[ "${tlsProvider}" != "fallback" && "${tlsProvider}" != "le-prod" && "${tlsProvider}" != "le-staging" ]]; then
echo "--tls-provider must be one of: le-prod, le-staging, fallback"
exit 1
fi
if [[ -n "${baseDataDir}" && ! -d "${baseDataDir}" ]]; then
echo "${baseDataDir} does not exist"
exit 1
fi
fi
echo ""
@@ -137,33 +160,74 @@ if [[ "${initBaseImage}" == "true" ]]; then
fi
echo "=> Checking version"
if ! releaseJson=$($curl -s "${apiServerOrigin}/api/v1/releases?prerelease=${prerelease}&boxVersion=${requestedVersion}"); then
echo "Failed to get release information"
exit 1
fi
if [[ "${sourceTarballUrl}" == "" ]]; then
if ! releaseJson=$($curl -s "${apiServerOrigin}/api/v1/releases?prerelease=${prerelease}&boxVersion=${requestedVersion}"); then
echo "Failed to get release information"
exit 1
fi
if [[ "$requestedVersion" == "" ]]; then
version=$(echo "${releaseJson}" | python3 -c 'import json,sys;obj=json.load(sys.stdin);print(obj["version"])')
else
version="${requestedVersion}"
fi
if [[ "$requestedVersion" == "" ]]; then
version=$(echo "${releaseJson}" | python3 -c 'import json,sys;obj=json.load(sys.stdin);print(obj["version"])')
else
version="${requestedVersion}"
fi
if ! sourceTarballUrl=$(echo "${releaseJson}" | python3 -c 'import json,sys;obj=json.load(sys.stdin);print(obj["info"]["sourceTarballUrl"])'); then
echo "No source code for version '${requestedVersion:-latest}'"
exit 1
if ! sourceTarballUrl=$(echo "${releaseJson}" | python3 -c 'import json,sys;obj=json.load(sys.stdin);print(obj["info"]["sourceTarballUrl"])'); then
echo "No source code for version '${requestedVersion:-latest}'"
exit 1
fi
fi
# Build data
# tlsConfig, dnsConfig, backupConfig are here for backward compat with < 1.9
# from 1.9, we use autoprovision.json
data=$(cat <<EOF
{
"provider": "${provider}",
"apiServerOrigin": "${apiServerOrigin}",
"webServerOrigin": "${webServerOrigin}",
"version": "${version}"
}
if [[ -z "${dataJson}" ]]; then
if [[ -z "${restoreUrl}" ]]; then
data=$(cat <<EOF
{
"fqdn": "${domain}",
"adminLocation": "${adminLocation}",
"adminFqdn": "${adminLocation}.${domain}",
"zoneName": "${zoneName}",
"provider": "${provider}",
"apiServerOrigin": "${apiServerOrigin}",
"webServerOrigin": "${webServerOrigin}",
"version": "${version}",
"tlsConfig": {
"provider": "${tlsProvider}"
},
"backupConfig" : {
"provider": "filesystem",
"backupFolder": "/var/backups",
"key": "${encryptionKey}",
"format": "tgz",
"retentionSecs": 172800
}
}
EOF
)
)
else
data=$(cat <<EOF
{
"fqdn": "${domain}",
"adminLocation": "${adminLocation}",
"adminFqdn": "${adminLocation}.${domain}",
"zoneName": "${zoneName}",
"provider": "${provider}",
"apiServerOrigin": "${apiServerOrigin}",
"webServerOrigin": "${webServerOrigin}",
"restore": {
"url": "${restoreUrl}",
"key": "${encryptionKey}"
},
"version": "${version}"
}
EOF
)
fi
else
data="${dataJson}"
fi
echo "=> Downloading version ${version} ..."
box_src_tmp_dir=$(mktemp -dt box-src-XXXXXX)
@@ -194,12 +258,28 @@ 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\nVisit https://<IP> to finish setup once the server has rebooted.\n"
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\nRebooting this server now to let bootloader changes take effect.\n"
+8 -15
View File
@@ -7,15 +7,17 @@ set -eu
[[ $(uname -s) == "Darwin" ]] && GNU_GETOPT="/usr/local/opt/gnu-getopt/bin/getopt" || GNU_GETOPT="getopt"
readonly GNU_GETOPT
args=$(${GNU_GETOPT} -o "" -l "output:" -n "$0" -- "$@")
args=$(${GNU_GETOPT} -o "" -l "revision:,output:" -n "$0" -- "$@")
eval set -- "${args}"
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
commitish="HEAD"
bundle_file=""
while true; do
case "$1" in
--revision) commitish="$2"; shift 2;;
--output) bundle_file="$2"; shift 2;;
--) break;;
*) echo "Unknown option $1"; exit 1;;
@@ -25,12 +27,7 @@ done
readonly TMPDIR=${TMPDIR:-/tmp} # why is this not set on mint?
if ! $(cd "${SOURCE_DIR}" && git diff --exit-code >/dev/null); then
echo "You have local changes in box, stash or commit them to proceed"
exit 1
fi
if ! $(cd "${SOURCE_DIR}/../webadmin" && git diff --exit-code >/dev/null); then
echo "You have local changes in webadmin, stash or commit them to proceed"
echo "You have local changes, stash or commit them to proceed"
exit 1
fi
@@ -39,17 +36,13 @@ if [[ "$(node --version)" != "v8.9.3" ]]; then
exit 1
fi
box_version=$(cd "${SOURCE_DIR}" && git rev-parse "HEAD")
branch=$(git rev-parse --abbrev-ref HEAD)
webadmin_version=$(cd "${SOURCE_DIR}/../webadmin" && git fetch && git rev-parse "origin/${branch}")
version=$(cd "${SOURCE_DIR}" && git rev-parse "${commitish}")
bundle_dir=$(mktemp -d -t box 2>/dev/null || mktemp -d box-XXXXXXXXXX --tmpdir=$TMPDIR)
[[ -z "$bundle_file" ]] && bundle_file="${TMPDIR}/box-${box_version:0:10}-${webadmin_version:0:10}.tar.gz"
[[ -z "$bundle_file" ]] && bundle_file="${TMPDIR}/box-${version}.tar.gz"
chmod "o+rx,g+rx" "${bundle_dir}" # otherwise extracted tarball director won't be readable by others/group
echo "Checking out code box version [${box_version}] and webadmin version [${webadmin_version}] into ${bundle_dir}"
(cd "${SOURCE_DIR}" && git archive --format=tar ${box_version} | (cd "${bundle_dir}" && tar xf -))
(cd "${SOURCE_DIR}/../webadmin" && git archive --format=tar ${webadmin_version} | (cd "${bundle_dir}" && tar xf -))
(cp "${SOURCE_DIR}/../webadmin/LICENSE" "${bundle_dir}")
echo "Checking out code [${version}] into ${bundle_dir}"
(cd "${SOURCE_DIR}" && git archive --format=tar ${version} | (cd "${bundle_dir}" && tar xf -))
if diff "${TMPDIR}/boxtarball.cache/package-lock.json.all" "${bundle_dir}/package-lock.json" >/dev/null 2>&1; then
echo "Reusing dev modules from cache"
+9 -8
View File
@@ -40,22 +40,22 @@ if [[ $(docker version --format {{.Client.Version}}) != "17.09.0-ce" ]]; then
# https://download.docker.com/linux/ubuntu/dists/xenial/stable/binary-amd64/Packages
if [[ $(sha256sum /tmp/docker.deb | cut -d' ' -f1) != "d33f6eb134f0ab0876148bd96de95ea47d583d7f2cddfdc6757979453f9bd9bf" ]]; then
echo "==> installer: docker binary download is corrupt"
echo "docker binary download is corrupt"
exit 5
fi
echo "==> installer: Waiting for all dpkg tasks to finish..."
echo "Waiting for all dpkg tasks to finish..."
while fuser /var/lib/dpkg/lock; do
sleep 1
done
while ! dpkg --force-confold --configure -a; do
echo "==> installer: Failed to fix packages. Retry"
echo "Failed to fix packages. Retry"
sleep 1
done
while ! apt install -y /tmp/docker.deb; do
echo "==> installer: Failed to install docker. Retry"
echo "Failed to install docker. Retry"
sleep 1
done
@@ -78,12 +78,12 @@ for try in `seq 1 10`; do
# however by default npm drops privileges for npm rebuild
# https://docs.npmjs.com/misc/config#unsafe-perm
if cd "${box_src_tmp_dir}" && npm rebuild --unsafe-perm; then break; fi
echo "==> installer: Failed to rebuild, trying again"
echo "Failed to rebuild, trying again"
sleep 5
done
if [[ ${try} -eq 10 ]]; then
echo "==> installer: npm rebuild failed, giving up"
echo "npm rebuild failed"
exit 4
fi
@@ -92,8 +92,9 @@ if ! id "${USER}" 2>/dev/null; then
fi
if [[ "${is_update}" == "yes" ]]; then
echo "==> installer: stop cloudron.target service for update"
${BOX_SRC_DIR}/setup/stop.sh
echo "Setting up update splash screen"
"${box_src_tmp_dir}/setup/splashpage.sh" --data "${arg_data}" || true # show splash from new code
${BOX_SRC_DIR}/setup/stop.sh # stop the old code
fi
# setup links to data directory
+16 -6
View File
@@ -3,13 +3,16 @@
source_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
json="${source_dir}/../node_modules/.bin/json"
# IMPORTANT: Fix cloudron.js:doUpdate if you add/remove any arg. keep these sorted for readability
arg_api_server_origin=""
arg_fqdn="" # remove after 1.10
arg_admin_domain=""
arg_fqdn=""
arg_admin_location=""
arg_admin_fqdn=""
arg_zone_name=""
arg_is_custom_domain="false" # can be removed after 1.9
arg_retire_reason=""
arg_retire_info=""
arg_token=""
arg_version=""
arg_web_server_origin=""
arg_provider=""
@@ -32,13 +35,16 @@ while true; do
# these params must be valid in all cases
arg_fqdn=$(echo "$2" | $json fqdn)
arg_admin_fqdn=$(echo "$2" | $json adminFqdn)
arg_zone_name=$(echo "$2" | $json zoneName)
[[ "${arg_zone_name}" == "" ]] && arg_zone_name="${arg_fqdn}"
# can be removed after 1.9
arg_is_custom_domain=$(echo "$2" | $json isCustomDomain)
[[ "${arg_is_custom_domain}" == "" ]] && arg_is_custom_domain="true"
arg_admin_location=$(echo "$2" | $json adminLocation)
[[ "${arg_admin_location}" == "" ]] && arg_admin_location="my"
arg_admin_domain=$(echo "$2" | $json adminDomain)
[[ "${arg_admin_domain}" == "" ]] && arg_admin_domain="${arg_fqdn}"
# only update/restore have this valid (but not migrate)
arg_api_server_origin=$(echo "$2" | $json apiServerOrigin)
[[ "${arg_api_server_origin}" == "" ]] && arg_api_server_origin="https://api.cloudron.io"
@@ -52,6 +58,8 @@ while true; do
arg_is_demo=$(echo "$2" | $json isDemo)
[[ "${arg_is_demo}" == "" ]] && arg_is_demo="false"
arg_token=$(echo "$2" | $json token)
arg_provider=$(echo "$2" | $json provider)
[[ "${arg_provider}" == "" ]] && arg_provider="generic"
@@ -64,8 +72,10 @@ done
echo "Parsed arguments:"
echo "api server: ${arg_api_server_origin}"
echo "admin fqdn: ${arg_admin_fqdn}"
echo "fqdn: ${arg_fqdn}"
echo "custom domain: ${arg_is_custom_domain}"
# do not dump these as they might become available via logs API
#echo "token: ${arg_token}"
echo "version: ${arg_version}"
echo "web server: ${arg_web_server_origin}"
echo "provider: ${arg_provider}"
+48
View File
@@ -0,0 +1,48 @@
#!/bin/bash
set -eu -o pipefail
readonly SETUP_WEBSITE_DIR="/home/yellowtent/setup/website"
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly box_src_dir="$(realpath ${script_dir}/..)"
readonly PLATFORM_DATA_DIR="/home/yellowtent/platformdata"
echo "Setting up nginx update page"
if [[ ! -f "${PLATFORM_DATA_DIR}/nginx/applications/admin.conf" ]]; then
echo "No admin.conf found. This Cloudron has no domain yet. Skip splash setup"
exit
fi
source "${script_dir}/argparser.sh" "$@" # this injects the arg_* variables used below
# keep this is sync with config.js appFqdn()
admin_origin="https://${arg_admin_fqdn}"
# copy the website
rm -rf "${SETUP_WEBSITE_DIR}" && mkdir -p "${SETUP_WEBSITE_DIR}"
cp -r "${script_dir}/splash/website/"* "${SETUP_WEBSITE_DIR}"
# create nginx config
readonly current_infra=$(node -e "console.log(require('${script_dir}/../src/infra_version.js').version);")
existing_infra="none"
[[ -f "${PLATFORM_DATA_DIR}/INFRA_VERSION" ]] && existing_infra=$(node -e "console.log(JSON.parse(require('fs').readFileSync('${PLATFORM_DATA_DIR}/INFRA_VERSION', 'utf8')).version);")
if [[ "${arg_retire_reason}" != "" || "${existing_infra}" != "${current_infra}" ]]; then
echo "Showing progress bar on all subdomains in retired mode or infra update. retire: ${arg_retire_reason} existing: ${existing_infra} current: ${current_infra}"
rm -f ${PLATFORM_DATA_DIR}/nginx/applications/*
${box_src_dir}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/appconfig.ejs" \
-O "{ \"vhost\": \"~^(.+)\$\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"splash\", \"sourceDir\": \"${SETUP_WEBSITE_DIR}\", \"certFilePath\": \"cert/host.cert\", \"keyFilePath\": \"cert/host.key\", \"xFrameOptions\": \"SAMEORIGIN\", \"robotsTxtQuoted\": null, \"hasIPv6\": false }" > "${PLATFORM_DATA_DIR}/nginx/applications/admin.conf"
else
echo "Show progress bar only on admin domain for normal update"
${box_src_dir}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/appconfig.ejs" \
-O "{ \"vhost\": \"${arg_admin_fqdn}\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"splash\", \"sourceDir\": \"${SETUP_WEBSITE_DIR}\", \"certFilePath\": \"cert/host.cert\", \"keyFilePath\": \"cert/host.key\", \"xFrameOptions\": \"SAMEORIGIN\", \"robotsTxtQuoted\": null, \"hasIPv6\": false }" > "${PLATFORM_DATA_DIR}/nginx/applications/admin.conf"
fi
if [[ "${arg_retire_reason}" == "migrate" ]]; then
echo "{ \"migrate\": { \"percent\": \"10\", \"message\": \"Migrating cloudron. This could take up to 15 minutes.\", \"info\": ${arg_retire_info} }, \"backup\": null, \"apiServerOrigin\": \"${arg_api_server_origin}\" }" > "${SETUP_WEBSITE_DIR}/progress.json"
else
echo '{ "update": { "percent": "10", "message": "Updating cloudron software" }, "backup": null }' > "${SETUP_WEBSITE_DIR}/progress.json"
fi
nginx -s reload
+26 -11
View File
@@ -11,6 +11,7 @@ readonly PLATFORM_DATA_DIR="${HOME_DIR}/platformdata" # platform data
readonly APPS_DATA_DIR="${HOME_DIR}/appsdata" # app data
readonly BOX_DATA_DIR="${HOME_DIR}/boxdata" # box data
readonly CONFIG_DIR="${HOME_DIR}/configs"
readonly SETUP_PROGRESS_JSON="${HOME_DIR}/setup/website/progress.json"
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400"
@@ -18,11 +19,19 @@ readonly script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${script_dir}/argparser.sh" "$@" # this injects the arg_* variables used below
echo "==> Configuring host"
set_progress() {
local percent="$1"
local message="$2"
echo "==> ${percent} - ${message}"
(echo "{ \"update\": { \"percent\": \"${percent}\", \"message\": \"${message}\" }, \"backup\": {} }" > "${SETUP_PROGRESS_JSON}") 2> /dev/null || true # as this will fail in non-update mode
}
set_progress "20" "Configuring host"
sed -e 's/^#NTP=/NTP=0.ubuntu.pool.ntp.org 1.ubuntu.pool.ntp.org 2.ubuntu.pool.ntp.org 3.ubuntu.pool.ntp.org/' -i /etc/systemd/timesyncd.conf
timedatectl set-ntp 1
timedatectl set-timezone UTC
hostnamectl set-hostname "${arg_admin_fqdn}"
hostnamectl set-hostname "${arg_fqdn}"
echo "==> Configuring docker"
cp "${script_dir}/start/docker-cloudron-app.apparmor" /etc/apparmor.d/docker-cloudron-app
@@ -105,7 +114,7 @@ systemctl restart systemd-journald
setfacl -n -m u:${USER}:r /var/log/journal/*/system.journal
echo "==> Creating config directory"
mkdir -p "${CONFIG_DIR}"
rm -rf "${CONFIG_DIR}" && mkdir "${CONFIG_DIR}"
echo "==> Setting up unbound"
# DO uses Google nameservers by default. This causes RBL queries to fail (host 2.0.0.127.zen.spamhaus.org)
@@ -163,9 +172,6 @@ if ! grep -q "^Restart=" /etc/systemd/system/multi-user.target.wants/nginx.servi
echo -e "\n[Service]\nRestart=always\n" >> /etc/systemd/system/multi-user.target.wants/nginx.service
systemctl daemon-reload
fi
# remove this migration after 1.10
[[ -f /etc/nginx/cert/host.cert ]] && cp /etc/nginx/cert/host.cert "/etc/nginx/cert/${arg_admin_domain}.host.cert"
[[ -f /etc/nginx/cert/host.key ]] && cp /etc/nginx/cert/host.key "/etc/nginx/cert/${arg_admin_domain}.host.key"
systemctl start nginx
# bookkeep the version as part of data
@@ -193,22 +199,31 @@ readonly mysql_root_password="password"
mysqladmin -u root -ppassword password password # reset default root password
mysql -u root -p${mysql_root_password} -e 'CREATE DATABASE IF NOT EXISTS box'
echo "==> Migrating data"
set_progress "40" "Migrating data"
sudo -u "${USER}" -H bash <<EOF
set -eu
cd "${BOX_SRC_DIR}"
BOX_ENV=cloudron DATABASE_URL=mysql://root:${mysql_root_password}@127.0.0.1/box "${BOX_SRC_DIR}/node_modules/.bin/db-migrate" up
EOF
if [[ -z "${arg_admin_fqdn:-}" ]]; then
# can be removed after 1.9
admin_fqdn=$([[ "${arg_is_custom_domain}" == "true" ]] && echo "${arg_admin_location}.${arg_fqdn}" || echo "${arg_admin_location}-${arg_fqdn}")
else
admin_fqdn="${arg_admin_fqdn}"
fi
echo "==> Creating cloudron.conf"
cat > "${CONFIG_DIR}/cloudron.conf" <<CONF_END
{
"version": "${arg_version}",
"token": "${arg_token}",
"apiServerOrigin": "${arg_api_server_origin}",
"webServerOrigin": "${arg_web_server_origin}",
"adminDomain": "${arg_admin_domain}",
"adminFqdn": "${arg_admin_fqdn}",
"fqdn": "${arg_fqdn}",
"adminFqdn": "${admin_fqdn}",
"adminLocation": "${arg_admin_location}",
"zoneName": "${arg_zone_name}",
"provider": "${arg_provider}",
"isDemo": ${arg_is_demo}
}
@@ -241,9 +256,9 @@ find "${BOX_DATA_DIR}" -mindepth 1 -maxdepth 1 -not -path "${BOX_DATA_DIR}/mail"
chown "${USER}:${USER}" "${BOX_DATA_DIR}/mail"
chown "${USER}:${USER}" -R "${BOX_DATA_DIR}/mail/dkim" # this is owned by box currently since it generates the keys
echo "==> Starting Cloudron"
set_progress "60" "Starting Cloudron"
systemctl start cloudron.target
sleep 2 # give systemd sometime to start the processes
echo "==> Almost done"
set_progress "90" "Almost done"
+34
View File
@@ -165,6 +165,40 @@ server {
}
<% } else if ( endpoint === 'app' ) { %>
proxy_pass http://127.0.0.1:<%= port %>;
<% } else if ( endpoint === 'splash' ) { %>
root <%= sourceDir %>;
error_page 503 /update.html;
location /update.html {
add_header Cache-Control no-cache;
}
location /theme.css {
add_header Cache-Control no-cache;
}
location /3rdparty/ {
add_header Cache-Control no-cache;
}
location /js/ {
add_header Cache-Control no-cache;
}
location /progress.json {
add_header Cache-Control no-cache;
}
location /api/v1/cloudron/progress {
add_header Cache-Control no-cache;
default_type application/json;
alias <%= sourceDir %>/progress.json;
}
location / {
return 503;
}
<% } %>
}
}
+17 -17
View File
@@ -299,7 +299,7 @@ function setupEmail(app, options, callback) {
{ 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_DOMAIN', value: config.fqdn() }
];
debugApp(app, 'Setting up Email');
@@ -365,11 +365,11 @@ function setupSendMail(app, options, callback) {
var env = [
{ name: 'MAIL_SMTP_SERVER', value: 'mail' },
{ name: 'MAIL_SMTP_PORT', value: '2525' },
{ name: 'MAIL_SMTPS_PORT', value: '2465' },
{ name: 'MAIL_SMTP_USERNAME', value: mailbox.name + '@' + app.domain },
{ name: 'MAIL_SMTPS_PORT', value: '4650' },
{ name: 'MAIL_SMTP_USERNAME', value: mailbox.name },
{ name: 'MAIL_SMTP_PASSWORD', value: password },
{ name: 'MAIL_FROM', value: mailbox.name + '@' + app.domain },
{ name: 'MAIL_DOMAIN', value: app.domain }
{ name: 'MAIL_FROM', value: mailbox.name + '@' + config.fqdn() },
{ name: 'MAIL_DOMAIN', value: config.fqdn() }
];
debugApp(app, 'Setting sendmail addon config to %j', env);
appdb.setAddonConfig(app.id, 'sendmail', env, callback);
@@ -402,10 +402,10 @@ function setupRecvMail(app, options, callback) {
var env = [
{ name: 'MAIL_IMAP_SERVER', value: 'mail' },
{ name: 'MAIL_IMAP_PORT', value: '9993' },
{ name: 'MAIL_IMAP_USERNAME', value: mailbox.name + '@' + app.domain },
{ name: 'MAIL_IMAP_USERNAME', value: mailbox.name },
{ name: 'MAIL_IMAP_PASSWORD', value: password },
{ name: 'MAIL_TO', value: mailbox.name + '@' + app.domain },
{ name: 'MAIL_DOMAIN', value: app.domain }
{ name: 'MAIL_TO', value: mailbox.name + '@' + config.fqdn() },
{ name: 'MAIL_DOMAIN', value: config.fqdn() }
];
debugApp(app, 'Setting sendmail addon config to %j', env);
@@ -685,24 +685,24 @@ 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) {
if (error) return callback(new Error('Error removing redis data:' + error));
appdb.unsetAddonConfig(app.id, 'redis', callback);
});
});
});
}
function backupRedis(app, options, callback) {
+1 -2
View File
@@ -60,7 +60,7 @@ var assert = require('assert'),
var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationState', 'apps.installationProgress', 'apps.runState',
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.httpPort', 'apps.location', 'apps.domain', 'apps.dnsRecordId',
'apps.accessRestrictionJson', 'apps.restoreConfigJson', 'apps.oldConfigJson', 'apps.updateConfigJson', 'apps.memoryLimit',
'apps.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(',');
@@ -226,7 +226,6 @@ function add(id, appStoreId, manifest, location, domain, portBindings, data, cal
database.transaction(queries, function (error) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error.message));
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND, 'no such domain'));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null);
+13 -16
View File
@@ -54,6 +54,7 @@ var addons = require('./addons.js'),
async = require('async'),
backups = require('./backups.js'),
BackupsError = backups.BackupsError,
certificates = require('./certificates.js'),
config = require('./config.js'),
constants = require('./constants.js'),
DatabaseError = require('./databaseerror.js'),
@@ -70,7 +71,6 @@ var addons = require('./addons.js'),
os = require('os'),
path = require('path'),
paths = require('./paths.js'),
reverseProxy = require('./reverseproxy.js'),
safe = require('safetydance'),
semver = require('semver'),
spawn = require('child_process').spawn,
@@ -130,7 +130,8 @@ function validateHostname(location, domain, hostname) {
const RESERVED_LOCATIONS = [
constants.API_LOCATION,
constants.SMTP_LOCATION,
constants.IMAP_LOCATION
constants.IMAP_LOCATION,
constants.POSTMAN_LOCATION
];
if (RESERVED_LOCATIONS.indexOf(location) !== -1) return new AppsError(AppsError.BAD_FIELD, location + ' is reserved');
@@ -449,7 +450,7 @@ function downloadManifest(appStoreId, manifest, callback) {
superagent.get(url).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppsError(AppsError.EXTERNAL_ERROR, 'Network error downloading manifest:' + error.message));
if (result.statusCode !== 200) return callback(new AppsError(AppsError.NOT_FOUND, util.format('Failed to get app info from store.', result.statusCode, result.text)));
if (result.statusCode !== 200) return callback(new AppsError(AppsError.EXTERNAL_ERROR, util.format('Failed to get app info from store.', result.statusCode, result.text)));
callback(null, parts[0], result.body.manifest);
});
@@ -534,10 +535,8 @@ function install(data, auditSource, callback) {
error = validateHostname(location, domain, intrinsicFqdn);
if (error) return callback(error);
if (cert && key) {
error = reverseProxy.validateCertificate(intrinsicFqdn, 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);
@@ -654,16 +653,14 @@ function configure(appId, data, auditSource, callback) {
// save cert to boxdata/certs. TODO: move this to apptask when we have a real task queue
if ('cert' in data && 'key' in data) {
if (data.cert && data.key) {
var vhost = values.altDomain || intrinsicFqdn;
error = reverseProxy.validateCertificate(vhost, 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, `${vhost}.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, `${vhost}.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, `${vhost}.user.cert`))) debug('Error removing cert: ' + safe.error.message);
if (!safe.fs.unlinkSync(path.join(paths.APP_CERTS_DIR, `${vhost}..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);
}
}
@@ -890,7 +887,7 @@ function clone(appId, data, auditSource, callback) {
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'));
@@ -903,7 +900,7 @@ function clone(appId, data, auditSource, callback) {
if (error) return callback(error);
domains.get(domain, function (error, domainObject) {
if (error && error.reason === DomainError.NOT_FOUND) return callback(new AppsError(AppsError.EXTERNAL_ERROR, 'No such domain'));
if (error && error.reason === DomainError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such domain'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Could not get domain info:' + error.message));
var intrinsicFqdn = domains.fqdn(location, domain, domainObject.provider);
+63 -69
View File
@@ -22,10 +22,7 @@ var assert = require('assert'),
config = require('./config.js'),
debug = require('debug')('box:appstore'),
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');
@@ -59,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) {
@@ -146,55 +154,55 @@ function sendAliveStatus(data, callback) {
settings.getAll(function (error, result) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
mail.getAll(function (error, mailDomains) {
eventlog.getAllPaged(eventlog.ACTION_USER_LOGIN, null, 1, 1, function (error, loginEvents) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, 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 = {
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 backendSettings = {
backupConfig: {
provider: result[settings.BACKUP_CONFIG_KEY].provider,
hardlinks: !result[settings.BACKUP_CONFIG_KEY].noHardlinks
},
domainConfig: {
count: mailDomains.length
},
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; })))
},
autoupdatePattern: result[settings.AUTOUPDATE_PATTERN_KEY],
timeZone: result[settings.TIME_ZONE_KEY],
};
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
}
};
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
}
};
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);
});
});
});
@@ -212,16 +220,10 @@ function getBoxUpdate(callback) {
superagent.get(url).query({ accessToken: appstoreConfig.token, boxVersion: config.version() }).timeout(10 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
if (result.statusCode === 204) return callback(null); // no update
if (result.statusCode !== 200 || !result.body) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
var updateInfo = result.body;
if (!semver.valid(updateInfo.version) || semver.gt(config.version(), updateInfo.version)) {
return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
}
if (result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
// { version, changelog, upgrade, sourceTarballUrl}
callback(null, updateInfo);
callback(null, result.body);
});
});
}
@@ -238,18 +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;
// do some sanity checks
if (!safe.query(updateInfo, 'manifest.version') || semver.gt(app.manifest.version, safe.query(updateInfo, 'manifest.version'))) {
debug('Skipping malformed update of app %s version: %s. got %j', app.id, app.manifest.version, 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);
});
});
}
+22 -16
View File
@@ -8,8 +8,8 @@ exports = module.exports = {
// exported for testing
_reserveHttpPort: reserveHttpPort,
_configureReverseProxy: configureReverseProxy,
_unconfigureReverseProxy: unconfigureReverseProxy,
_configureNginx: configureNginx,
_unconfigureNginx: unconfigureNginx,
_createVolume: createVolume,
_deleteVolume: deleteVolume,
_verifyManifest: verifyManifest,
@@ -32,6 +32,7 @@ var addons = require('./addons.js'),
assert = require('assert'),
async = require('async'),
backups = require('./backups.js'),
certificates = require('./certificates.js'),
config = require('./config.js'),
database = require('./database.js'),
DatabaseError = require('./databaseerror.js'),
@@ -43,10 +44,10 @@ var addons = require('./addons.js'),
fs = require('fs'),
manifestFormat = require('cloudron-manifestformat'),
net = require('net'),
nginx = require('./nginx.js'),
os = require('os'),
path = require('path'),
paths = require('./paths.js'),
reverseProxy = require('./reverseproxy.js'),
safe = require('safetydance'),
shell = require('./shell.js'),
superagent = require('superagent'),
@@ -112,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) {
@@ -388,7 +393,7 @@ function install(app, callback) {
// teardown for re-installs
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
unconfigureReverseProxy.bind(null, app),
unconfigureNginx.bind(null, app),
removeCollectdProfile.bind(null, app),
removeLogrotateConfig.bind(null, app),
stopApp.bind(null, app),
@@ -449,8 +454,8 @@ function install(app, callback) {
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, Configuring reverse proxy' }),
configureReverseProxy.bind(null, app),
updateApp.bind(null, app, { installationProgress: '95, Configure nginx' }),
configureNginx.bind(null, app),
// done!
function (callback) {
@@ -498,7 +503,7 @@ function configure(app, callback) {
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),
@@ -506,7 +511,8 @@ function configure(app, callback) {
function (next) {
if (!locationChanged) return next();
unregisterSubdomain(app, app.oldConfig.location, app.oldConfig.domain, next);
// the config.fqdn() fallback can be removed after 1.9
unregisterSubdomain(app, app.oldConfig.location, app.oldConfig.domain || config.fqdn(), next);
},
reserveHttpPort.bind(null, app),
@@ -544,8 +550,8 @@ function configure(app, callback) {
updateApp.bind(null, app, { installationProgress: '85, Waiting for External Domain setup' }),
exports._waitForAltDomainDnsPropagation.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Configuring reverse proxy' }),
configureReverseProxy.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Configuring Nginx' }),
configureNginx.bind(null, app),
// done!
function (callback) {
@@ -704,8 +710,8 @@ function uninstall(app, callback) {
updateApp.bind(null, app, { installationProgress: '80, Cleanup icon' }),
removeIcon.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Unconfiguring reverse proxy' }),
unconfigureReverseProxy.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Unconfiguring Nginx' }),
unconfigureNginx.bind(null, app),
updateApp.bind(null, app, { installationProgress: '95, Remove app from database' }),
appdb.del.bind(null, app.id)
+14 -14
View File
@@ -25,22 +25,22 @@ var assert = require('assert'),
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
passport.serializeUser(function (user, callback) {
callback(null, user.id);
});
passport.deserializeUser(function(userId, callback) {
user.get(userId, function (error, result) {
if (error) return callback(error);
var md5 = crypto.createHash('md5').update(result.email).digest('hex');
var md5 = crypto.createHash('md5').update(result.alternateEmail || result.email).digest('hex');
result.gravatar = 'https://www.gravatar.com/avatar/' + md5 + '.jpg?s=24&d=mm';
callback(null, result);
});
});
passport.use(new LocalStrategy(function (username, password, callback) {
if (username.indexOf('@') === -1) {
user.verifyWithUsername(username, password, function (error, result) {
@@ -60,7 +60,7 @@ function initialize(callback) {
});
}
}));
passport.use(new BasicStrategy(function (username, password, callback) {
if (username.indexOf('cid-') === 0) {
debug('BasicStrategy: detected client id %s instead of username:password', username);
@@ -82,7 +82,7 @@ function initialize(callback) {
});
}
}));
passport.use(new ClientPasswordStrategy(function (clientId, clientSecret, callback) {
clients.get(clientId, function(error, client) {
if (error && error.reason === ClientsError.NOT_FOUND) return callback(null, false);
@@ -91,15 +91,15 @@ function initialize(callback) {
return callback(null, client);
});
}));
passport.use(new BearerStrategy(accessTokenAuth));
callback(null);
}
function uninitialize(callback) {
assert.strictEqual(typeof callback, 'function');
callback(null);
}
@@ -110,15 +110,15 @@ function accessTokenAuth(accessToken, callback) {
tokendb.get(accessToken, function (error, token) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, false);
if (error) return callback(error);
// scopes here can define what capabilities that token carries
// passport put the 'info' object into req.authInfo, where we can further validate the scopes
var info = { scope: token.scope };
user.get(token.identifier, function (error, user) {
if (error && error.reason === UserError.NOT_FOUND) return callback(null, false);
if (error) return callback(error);
callback(null, user, info);
});
});
+48 -120
View File
@@ -1,16 +1,11 @@
'use strict';
exports = module.exports = {
verifySetupToken: verifySetupToken,
setupDone: setupDone,
changePlan: changePlan,
upgrade: upgrade,
sendHeartbeat: sendHeartbeat,
getBoxAndUserDetails: getBoxAndUserDetails,
setPtrRecord: setPtrRecord,
CaasError: CaasError
setPtrRecord: setPtrRecord
};
var assert = require('assert'),
@@ -20,13 +15,13 @@ var assert = require('assert'),
locker = require('./locker.js'),
path = require('path'),
progress = require('./progress.js'),
settings = require('./settings.js'),
shell = require('./shell.js'),
superagent = require('superagent'),
util = require('util'),
_ = require('underscore');
const RETIRE_CMD = path.join(__dirname, 'scripts/retire.sh');
var gBoxAndUserDetails = null; // cached cloudron details like region,size...
function CaasError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
@@ -48,10 +43,9 @@ function CaasError(reason, errorOrMessage) {
}
util.inherits(CaasError, Error);
CaasError.BAD_FIELD = 'Field error';
CaasError.BAD_STATE = 'Bad state';
CaasError.INVALID_TOKEN = 'Invalid Token';
CaasError.INTERNAL_ERROR = 'Internal Error';
CaasError.EXTERNAL_ERROR = 'External Error';
CaasError.BAD_STATE = 'Bad state';
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
@@ -62,64 +56,14 @@ function retire(reason, info, callback) {
var data = {
apiServerOrigin: config.apiServerOrigin(),
adminFqdn: config.adminFqdn()
adminFqdn: config.adminFqdn(),
fqdn: config.fqdn()
};
shell.sudo('retire', [ RETIRE_CMD, reason, JSON.stringify(info), JSON.stringify(data) ], callback);
}
function getCaasConfig(callback) {
assert.strictEqual(typeof callback, 'function');
settings.getCaasConfig(function (error, result) {
if (error) return callback(new CaasError(CaasError.INTERNAL_ERROR, error));
callback(null, result);
});
}
function verifySetupToken(setupToken, callback) {
assert.strictEqual(typeof setupToken, 'string');
assert.strictEqual(typeof callback, 'function');
settings.getCaasConfig(function (error, caasConfig) {
if (error) return callback(new CaasError(CaasError.INTERNAL_ERROR, error));
superagent.get(config.apiServerOrigin() + '/api/v1/boxes/' + caasConfig.boxId + '/setup/verify').query({ setupToken: setupToken })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(new CaasError(CaasError.EXTERNAL_ERROR, error));
if (result.statusCode === 403) return callback(new CaasError(CaasError.INVALID_TOKEN));
if (result.statusCode === 409) return callback(new CaasError(CaasError.BAD_STATE, 'Already setup'));
if (result.statusCode !== 200) return callback(new CaasError(CaasError.EXTERNAL_ERROR, error));
callback(null);
});
});
}
function setupDone(setupToken, callback) {
assert.strictEqual(typeof setupToken, 'string');
assert.strictEqual(typeof callback, 'function');
settings.getCaasConfig(function (error, caasConfig) {
if (error) return callback(new CaasError(CaasError.INTERNAL_ERROR, error));
// Now let the api server know we got activated
superagent.post(config.apiServerOrigin() + '/api/v1/boxes/' + caasConfig.boxId + '/setup/done').query({ setupToken: setupToken })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(new CaasError(CaasError.EXTERNAL_ERROR, error));
if (result.statusCode === 403) return callback(new CaasError(CaasError.INVALID_TOKEN));
if (result.statusCode === 409) return callback(new CaasError(CaasError.BAD_STATE, 'Already setup'));
if (result.statusCode !== 201) return callback(new CaasError(CaasError.EXTERNAL_ERROR, error));
callback(null);
});
});
}
function doMigrate(options, caasConfig, callback) {
function doMigrate(options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof caasConfig, 'object');
assert.strictEqual(typeof callback, 'function');
var error = locker.lock(locker.OP_MIGRATE);
@@ -140,8 +84,8 @@ function doMigrate(options, caasConfig, callback) {
debug('migrate: domain: %s size %s region %s', options.domain, options.size, options.region);
superagent
.post(config.apiServerOrigin() + '/api/v1/boxes/' + caasConfig.boxId + '/migrate')
.query({ token: caasConfig.token })
.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/migrate')
.query({ token: config.token() })
.send(options)
.timeout(30 * 1000)
.end(function (error, result) {
@@ -165,11 +109,7 @@ function changePlan(options, callback) {
if (config.isDemo()) return callback(new CaasError(CaasError.BAD_FIELD, 'Not allowed in demo mode'));
getCaasConfig(function (error, result) {
if (error) return callback(error);
doMigrate(options, result, callback);
});
doMigrate(options, callback);
}
// this function expects a lock
@@ -186,81 +126,69 @@ function upgrade(boxUpdateInfo, callback) {
backups.backupBoxAndApps({ userId: null, username: 'upgrader' }, function (error) {
if (error) return upgradeError(error);
getCaasConfig(function (error, result) {
if (error) return upgradeError(error);
superagent.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/upgrade')
.query({ token: config.token() })
.send({ version: boxUpdateInfo.version })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return upgradeError(new Error('Network error making upgrade request: ' + error));
if (result.statusCode !== 202) return upgradeError(new Error(util.format('Server not ready to upgrade. statusCode: %s body: %j', result.status, result.body)));
superagent.post(config.apiServerOrigin() + '/api/v1/boxes/' + result.boxId + '/upgrade')
.query({ token: result.token })
.send({ version: boxUpdateInfo.version })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return upgradeError(new Error('Network error making upgrade request: ' + error));
if (result.statusCode !== 202) return upgradeError(new Error(util.format('Server not ready to upgrade. statusCode: %s body: %j', result.status, result.body)));
progress.set(progress.UPDATE, 10, 'Updating base system');
progress.set(progress.UPDATE, 10, 'Updating base system');
// no need to unlock since this is the last thing we ever do on this box
callback();
// no need to unlock since this is the last thing we ever do on this box
callback();
retire('upgrade');
});
});
retire('upgrade');
});
});
}
function sendHeartbeat() {
assert(config.provider() === 'caas', 'Heartbeat is only sent for managed cloudrons');
getCaasConfig(function (error, result) {
if (error) return debug('Caas config missing', error);
var url = config.apiServerOrigin() + '/api/v1/boxes/' + result.boxId + '/heartbeat';
superagent.post(url).query({ token: result.token, version: config.version() }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) debug('Network error sending heartbeat.', error);
else if (result.statusCode !== 200) debug('Server responded to heartbeat with %s %s', result.statusCode, result.text);
else debug('Heartbeat sent to %s', url);
});
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/heartbeat';
superagent.post(url).query({ token: config.token(), version: config.version() }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) debug('Network error sending heartbeat.', error);
else if (result.statusCode !== 200) debug('Server responded to heartbeat with %s %s', result.statusCode, result.text);
else debug('Heartbeat sent to %s', url);
});
}
function getBoxAndUserDetails(callback) {
assert.strictEqual(typeof callback, 'function');
if (gBoxAndUserDetails) return callback(null, gBoxAndUserDetails);
if (config.provider() !== 'caas') return callback(null, {});
getCaasConfig(function (error, caasConfig) {
if (error) return callback(error);
superagent
.get(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn())
.query({ token: config.token() })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(new CaasError(CaasError.EXTERNAL_ERROR, 'Cannot reach appstore'));
if (result.statusCode !== 200) return callback(new CaasError(CaasError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
superagent
.get(config.apiServerOrigin() + '/api/v1/boxes/' + caasConfig.boxId)
.query({ token: caasConfig.token })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(new CaasError(CaasError.EXTERNAL_ERROR, 'Cannot reach appstore'));
if (result.statusCode !== 200) return callback(new CaasError(CaasError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
gBoxAndUserDetails = result.body;
return callback(null, result.body);
});
});
return callback(null, gBoxAndUserDetails);
});
}
function setPtrRecord(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
getCaasConfig(function (error, result) {
if (error) return callback(error);
superagent
.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/ptr')
.query({ token: config.token() })
.send({ domain: domain })
.timeout(5 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(new CaasError(CaasError.EXTERNAL_ERROR, 'Cannot reach appstore'));
if (result.statusCode !== 202) return callback(new CaasError(CaasError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
superagent
.post(config.apiServerOrigin() + '/api/v1/boxes/' + result.boxId + '/ptr')
.query({ token: result.token })
.send({ domain: domain })
.timeout(5 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(new CaasError(CaasError.EXTERNAL_ERROR, 'Cannot reach appstore'));
if (result.statusCode !== 202) return callback(new CaasError(CaasError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
return callback(null);
});
});
return callback(null);
});
}
+4 -4
View File
@@ -10,12 +10,12 @@ exports = module.exports = {
var assert = require('assert'),
debug = require('debug')('box:cert/caas.js');
function getCertificate(vhost, options, callback) {
assert.strictEqual(typeof vhost, 'string');
function getCertificate(domain, options, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debug('getCertificate: using fallback certificate', vhost);
debug('getCertificate: using fallback certificate', domain);
return callback(null, '', '');
return callback(null, 'cert/host.cert', 'cert/host.key');
}
+3 -3
View File
@@ -10,12 +10,12 @@ exports = module.exports = {
var assert = require('assert'),
debug = require('debug')('box:cert/fallback.js');
function getCertificate(vhost, options, callback) {
assert.strictEqual(typeof vhost, 'string');
function getCertificate(domain, options, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debug('getCertificate: using fallback certificate', vhost);
debug('getCertificate: using fallback certificate', domain);
return callback(null, '', '');
}
+447
View File
@@ -0,0 +1,447 @@
'use strict';
exports = module.exports = {
CertificatesError: CertificatesError,
ensureFallbackCertificate: ensureFallbackCertificate,
setFallbackCertificate: setFallbackCertificate,
getFallbackCertificate: getFallbackCertificate,
validateCertificate: validateCertificate,
ensureCertificate: ensureCertificate,
setAdminCertificate: setAdminCertificate,
getAdminCertificate: getAdminCertificate,
renewAll: renewAll,
initialize: initialize,
uninitialize: uninitialize,
events: null,
EVENT_CERT_CHANGED: 'cert_changed',
// exported for testing
_getApi: getApi
};
var acme = require('./cert/acme.js'),
apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
caas = require('./cert/caas.js'),
config = require('./config.js'),
constants = require('./constants.js'),
debug = require('debug')('box:certificates'),
eventlog = require('./eventlog.js'),
fallback = require('./cert/fallback.js'),
fs = require('fs'),
mailer = require('./mailer.js'),
nginx = require('./nginx.js'),
path = require('path'),
paths = require('./paths.js'),
safe = require('safetydance'),
settings = require('./settings.js'),
user = require('./user.js'),
util = require('util');
function CertificatesError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
Error.call(this);
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.reason = reason;
if (typeof errorOrMessage === 'undefined') {
this.message = reason;
} else if (typeof errorOrMessage === 'string') {
this.message = errorOrMessage;
} else {
this.message = 'Internal error';
this.nestedError = errorOrMessage;
}
}
util.inherits(CertificatesError, Error);
CertificatesError.INTERNAL_ERROR = 'Internal Error';
CertificatesError.INVALID_CERT = 'Invalid certificate';
CertificatesError.NOT_FOUND = 'Not Found';
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
exports.events = new (require('events').EventEmitter)();
callback();
}
function uninitialize(callback) {
assert.strictEqual(typeof callback, 'function');
exports.events = null;
callback();
}
function getApi(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
settings.getTlsConfig(function (error, tlsConfig) {
if (error) return callback(error);
if (tlsConfig.provider === 'fallback') return callback(null, fallback, {});
// use acme if we have altDomain or the tlsConfig is not caas
var api = (app.altDomain || tlsConfig.provider !== 'caas') ? acme : caas;
var options = { };
if (tlsConfig.provider === 'caas') {
options.prod = true; // with altDomain, we will choose acme setting based on this
} else { // acme
options.prod = tlsConfig.provider.match(/.*-prod/) !== null;
}
// registering user with an email requires A or MX record (https://github.com/letsencrypt/boulder/issues/1197)
// we cannot use admin@fqdn because the user might not have set it up.
// we simply update the account with the latest email we have each time when getting letsencrypt certs
// https://github.com/ietf-wg-acme/acme/issues/30
user.getOwner(function (error, owner) {
options.email = error ? 'support@cloudron.io' : (owner.alternateEmail || owner.email); // can error if not activated yet
callback(null, api, options);
});
});
}
function ensureFallbackCertificate(callback) {
// ensure a fallback certificate that much of our code requires
var certFilePath = path.join(paths.APP_CERTS_DIR, 'host.cert');
var keyFilePath = path.join(paths.APP_CERTS_DIR, 'host.key');
var fallbackCertPath = path.join(paths.NGINX_CERT_DIR, 'host.cert');
var fallbackKeyPath = path.join(paths.NGINX_CERT_DIR, 'host.key');
if (fs.existsSync(fallbackCertPath) && fs.existsSync(fallbackKeyPath)) {
debug('ensureFallbackCertificate: pre-existing fallback certs');
return callback();
}
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) { // existing custom fallback certs (when restarting, restoring, updating)
debug('ensureFallbackCertificate: using fallback certs provided by user');
if (!safe.child_process.execSync('cp ' + certFilePath + ' ' + fallbackCertPath)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
if (!safe.child_process.execSync('cp ' + keyFilePath + ' ' + fallbackKeyPath)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
return callback();
}
// generate a self-signed cert. it's in backup dir so that we don't create a new cert across restarts
// FIXME: this cert does not cover the naked domain. needs SAN
if (config.fqdn()) {
debug('ensureFallbackCertificate: generating self-signed certificate');
var certCommand = util.format('openssl req -x509 -newkey rsa:2048 -keyout %s -out %s -days 3650 -subj /CN=*.%s -nodes', keyFilePath, certFilePath, config.fqdn());
safe.child_process.execSync(certCommand);
if (!safe.child_process.execSync('cp ' + certFilePath + ' ' + fallbackCertPath)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
if (!safe.child_process.execSync('cp ' + keyFilePath + ' ' + fallbackKeyPath)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
return callback();
} else {
debug('ensureFallbackCertificate: cannot generate fallback certificate without domain');
return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, 'No domain set'));
}
}
function isExpiringSync(certFilePath, hours) {
assert.strictEqual(typeof certFilePath, 'string');
assert.strictEqual(typeof hours, 'number');
if (!fs.existsSync(certFilePath)) return 2; // not found
var result = safe.child_process.spawnSync('/usr/bin/openssl', [ 'x509', '-checkend', String(60 * 60 * hours), '-in', certFilePath ]);
debug('isExpiringSync: %s %s %s', certFilePath, result.stdout.toString('utf8').trim(), result.status);
return result.status === 1; // 1 - expired 0 - not expired
}
function renewAll(auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
debug('renewAll: Checking certificates for renewal');
apps.getAll(function (error, allApps) {
if (error) return callback(error);
allApps.push({ intrinsicFqdn: config.adminFqdn() }); // inject fake webadmin app
var expiringApps = [ ];
for (var i = 0; i < allApps.length; i++) {
var appDomain = allApps[i].altDomain || allApps[i].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);
});
});
}
+5 -5
View File
@@ -39,6 +39,7 @@ var apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
clientdb = require('./clientdb.js'),
config = require('./config.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:clients'),
hat = require('hat'),
@@ -317,8 +318,7 @@ function delToken(clientId, tokenId, callback) {
});
}
function addDefaultClients(origin, callback) {
assert.strictEqual(typeof origin, 'string');
function addDefaultClients(callback) {
assert.strictEqual(typeof callback, 'function');
debug('Adding default clients');
@@ -329,8 +329,8 @@ function addDefaultClients(origin, callback) {
// id, appId, type, clientSecret, redirectURI, scope
async.series([
clientdb.upsert.bind(null, 'cid-webadmin', 'Settings', 'built-in', 'secret-webadmin', origin, ADMIN_SCOPES),
clientdb.upsert.bind(null, 'cid-sdk', 'SDK', 'built-in', 'secret-sdk', origin, '*,roleSdk'),
clientdb.upsert.bind(null, 'cid-cli', 'Cloudron Tool', 'built-in', 'secret-cli', origin, '*, roleSdk')
clientdb.upsert.bind(null, 'cid-webadmin', 'Settings', 'built-in', 'secret-webadmin', config.adminOrigin(), ADMIN_SCOPES),
clientdb.upsert.bind(null, 'cid-sdk', 'SDK', 'built-in', 'secret-sdk', config.adminOrigin(), '*,roleSdk'),
clientdb.upsert.bind(null, 'cid-cli', 'Cloudron Tool', 'built-in', 'secret-cli', config.adminOrigin(), '*, roleSdk')
], callback);
}
+493 -10
View File
@@ -5,50 +5,77 @@ exports = module.exports = {
initialize: initialize,
uninitialize: uninitialize,
activate: activate,
getConfig: getConfig,
getStatus: getStatus,
getDisks: getDisks,
dnsSetup: dnsSetup,
getLogs: getLogs,
updateToLatest: updateToLatest,
restore: restore,
reboot: reboot,
onActivated: onActivated,
checkDiskSpace: checkDiskSpace,
checkDiskSpace: checkDiskSpace
readDkimPublicKeySync: readDkimPublicKeySync,
refreshDNS: refreshDNS,
configureWebadmin: configureWebadmin
};
var assert = require('assert'),
var appdb = require('./appdb.js'),
apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
backups = require('./backups.js'),
BackupsError = require('./backups.js').BackupsError,
caas = require('./caas.js'),
certificates = require('./certificates.js'),
child_process = require('child_process'),
clients = require('./clients.js'),
config = require('./config.js'),
constants = require('./constants.js'),
cron = require('./cron.js'),
debug = require('debug')('box:cloudron'),
df = require('@sindresorhus/df'),
domains = require('./domains.js'),
DomainError = domains.DomainError,
eventlog = require('./eventlog.js'),
fs = require('fs'),
locker = require('./locker.js'),
mailer = require('./mailer.js'),
nginx = require('./nginx.js'),
os = require('os'),
path = require('path'),
paths = require('./paths.js'),
platform = require('./platform.js'),
progress = require('./progress.js'),
reverseProxy = require('./reverseproxy.js'),
safe = require('safetydance'),
semver = require('semver'),
settings = require('./settings.js'),
settingsdb = require('./settingsdb.js'),
SettingsError = settings.SettingsError,
shell = require('./shell.js'),
spawn = require('child_process').spawn,
split = require('split'),
superagent = require('superagent'),
sysinfo = require('./sysinfo.js'),
tld = require('tldjs'),
tokendb = require('./tokendb.js'),
updateChecker = require('./updatechecker.js'),
user = require('./user.js'),
UserError = user.UserError,
util = require('util'),
_ = require('underscore');
var REBOOT_CMD = path.join(__dirname, 'scripts/reboot.sh'),
UPDATE_CMD = path.join(__dirname, 'scripts/update.sh');
UPDATE_CMD = path.join(__dirname, 'scripts/update.sh'),
RESTART_CMD = path.join(__dirname, 'scripts/restart.sh');
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
var gWebadminStatus = { dns: false, tls: false, configuring: false, restoring: false };
function CloudronError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
@@ -71,6 +98,8 @@ util.inherits(CloudronError, Error);
CloudronError.BAD_FIELD = 'Field error';
CloudronError.INTERNAL_ERROR = 'Internal Error';
CloudronError.EXTERNAL_ERROR = 'External Error';
CloudronError.ALREADY_PROVISIONED = 'Already Provisioned';
CloudronError.ALREADY_SETUP = 'Already Setup';
CloudronError.BAD_STATE = 'Bad state';
CloudronError.ALREADY_UPTODATE = 'No Update Available';
CloudronError.NOT_FOUND = 'Not found';
@@ -79,12 +108,21 @@ CloudronError.SELF_UPGRADE_NOT_SUPPORTED = 'Self upgrade not supported';
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
gWebadminStatus = { dns: false, tls: false, configuring: false, restoring: false };
async.series([
certificates.initialize,
settings.initialize,
reverseProxy.configureDefaultServer,
cron.initialize, // required for caas heartbeat before activation
configureDefaultServer,
onDomainConfigured,
onActivated
], callback);
], function (error) {
if (error) return callback(error);
configureWebadmin(NOOP_CALLBACK); // for restore() and caas initial setup. do not block
callback();
});
}
function uninitialize(callback) {
@@ -93,10 +131,24 @@ function uninitialize(callback) {
async.series([
cron.uninitialize,
platform.stop,
certificates.uninitialize,
settings.uninitialize
], callback);
}
function onDomainConfigured(callback) {
callback = callback || NOOP_CALLBACK;
if (!config.fqdn()) return callback();
async.series([
clients.addDefaultClients,
certificates.ensureFallbackCertificate,
ensureDkimKey,
cron.initialize // required for caas heartbeat before activation
], callback);
}
function onActivated(callback) {
callback = callback || NOOP_CALLBACK;
@@ -111,6 +163,238 @@ function onActivated(callback) {
});
}
function autoprovision(callback) {
assert.strictEqual(typeof callback, 'function');
const confJson = safe.fs.readFileSync(paths.AUTO_PROVISION_FILE, 'utf8');
if (!confJson) return callback();
const conf = safe.JSON.parse(confJson);
if (!conf) return callback();
async.eachSeries(Object.keys(conf), function (key, iteratorDone) {
var name;
switch (key) {
case 'dnsConfig': name = 'dns_config'; break;
case 'tlsConfig': name = 'tls_config'; break;
case 'backupConfig': name = 'backup_config'; break;
case 'tlsCert':
debug(`autoprovision: ${key}`);
return fs.writeFile(path.join(paths.NGINX_CERT_DIR, 'host.cert'), conf[key], iteratorDone);
case 'tlsKey':
debug(`autoprovision: ${key}`);
return fs.writeFile(path.join(paths.NGINX_CERT_DIR, 'host.key'), conf[key], iteratorDone);
default:
debug(`autoprovision: ${key} ignored`);
return iteratorDone();
}
debug(`autoprovision: ${name}`);
settingsdb.set(name, JSON.stringify(conf[key]), iteratorDone);
}, callback);
}
function dnsSetup(adminFqdn, domain, zoneName, provider, dnsConfig, callback) {
assert.strictEqual(typeof adminFqdn, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof provider, 'string');
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof callback, 'function');
if (config.fqdn()) return callback(new CloudronError(CloudronError.ALREADY_SETUP));
if (!zoneName) zoneName = tld.getDomain(domain) || domain;
debug('dnsSetup: Setting up Cloudron with domain %s and zone %s', domain, zoneName);
function done(error) {
if (error && error.reason === DomainError.BAD_FIELD) return callback(new CloudronError(CloudronError.BAD_FIELD, error.message));
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
autoprovision(function (error) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
config.setFqdn(domain); // set fqdn only after dns config is valid, otherwise cannot re-setup if we failed
config.setAdminFqdn(adminFqdn);
config.setAdminLocation('my');
config.setZoneName(zoneName);
callback();
async.series([ // do not block
onDomainConfigured,
configureWebadmin
], NOOP_CALLBACK);
});
}
domains.get(domain, function (error, result) {
if (error && error.reason !== DomainError.NOT_FOUND) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
if (!result) domains.add(domain, zoneName, provider, dnsConfig, null /* cert */, done);
else domains.update(domain, provider, dnsConfig, null /* cert */, done);
});
}
function configureDefaultServer(callback) {
callback = callback || NOOP_CALLBACK;
debug('configureDefaultServer: domain %s', config.fqdn());
if (process.env.BOX_ENV === 'test') return callback();
var certFilePath = path.join(paths.NGINX_CERT_DIR, 'default.cert');
var keyFilePath = path.join(paths.NGINX_CERT_DIR, 'default.key');
if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) {
debug('configureDefaultServer: create new cert');
var cn = 'cloudron-' + (new Date()).toISOString(); // randomize date a bit to keep firefox happy
var certCommand = util.format('openssl req -x509 -newkey rsa:2048 -keyout %s -out %s -days 3650 -subj /CN=%s -nodes', keyFilePath, certFilePath, cn);
safe.child_process.execSync(certCommand);
}
nginx.configureAdmin(certFilePath, keyFilePath, 'default.conf', '', function (error) {
if (error) return callback(error);
debug('configureDefaultServer: done');
callback(null);
});
}
function configureWebadmin(callback) {
callback = callback || NOOP_CALLBACK;
debug('configureWebadmin: fqdn:%s status:%j', config.fqdn(), gWebadminStatus);
if (process.env.BOX_ENV === 'test' || !config.fqdn() || gWebadminStatus.configuring) return callback();
gWebadminStatus.configuring = true; // re-entracy guard
function done(error) {
gWebadminStatus.configuring = false;
debug('configureWebadmin: done error: %j', error || {});
callback(error);
}
function configureNginx(error) {
debug('configureNginx: dns update: %j', error || {});
certificates.ensureCertificate({ domain: config.fqdn(), location: config.adminLocation(), intrinsicFqdn: config.adminFqdn() }, function (error, certFilePath, keyFilePath) {
if (error) return done(error);
gWebadminStatus.tls = true;
nginx.configureAdmin(certFilePath, keyFilePath, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn(), done);
});
}
// update the DNS. configure nginx regardless of whether it succeeded so that
// box is accessible even if dns creds are invalid
sysinfo.getPublicIp(function (error, ip) {
if (error) return configureNginx(error);
addDnsRecords(ip, function (error) {
if (error) return configureNginx(error);
domains.waitForDNSRecord(config.adminFqdn(), config.fqdn(), ip, 'A', { interval: 30000, times: 50000 }, function (error) {
if (error) return configureNginx(error);
gWebadminStatus.dns = true;
configureNginx();
});
});
});
}
function setTimeZone(ip, callback) {
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
debug('setTimeZone ip:%s', ip);
superagent.get('https://geolocation.cloudron.io/json').query({ ip: ip }).timeout(10 * 1000).end(function (error, result) {
if ((error && !error.response) || result.statusCode !== 200) {
debug('Failed to get geo location: %s', error.message);
return callback(null);
}
var timezone = safe.query(result.body, 'location.time_zone');
if (!timezone || typeof timezone !== 'string') {
debug('No timezone in geoip response : %j', result.body);
return callback(null);
}
debug('Setting timezone to ', timezone);
settings.setTimeZone(timezone, callback);
});
}
function activate(username, password, email, displayName, ip, auditSource, callback) {
assert.strictEqual(typeof username, 'string');
assert.strictEqual(typeof password, 'string');
assert.strictEqual(typeof email, 'string');
assert.strictEqual(typeof displayName, 'string');
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
debug('activating user:%s email:%s', username, email);
setTimeZone(ip, function () { }); // TODO: get this from user. note that timezone is detected based on the browser location and not the cloudron region
user.createOwner(username, password, email, displayName, auditSource, function (error, userObject) {
if (error && error.reason === UserError.ALREADY_EXISTS) return callback(new CloudronError(CloudronError.ALREADY_PROVISIONED));
if (error && error.reason === UserError.BAD_FIELD) return callback(new CloudronError(CloudronError.BAD_FIELD, error.message));
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
clients.get('cid-webadmin', function (error, result) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
// Also generate a token so the admin creation can also act as a login
var token = tokendb.generateToken();
var expires = Date.now() + constants.DEFAULT_TOKEN_EXPIRATION;
tokendb.add(token, userObject.id, result.id, expires, '*', function (error) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_ACTIVATE, auditSource, { });
onActivated();
callback(null, { token: token, expires: expires });
});
});
});
}
function getStatus(callback) {
assert.strictEqual(typeof callback, 'function');
user.count(function (error, count) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
settings.getCloudronName(function (error, cloudronName) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
callback(null, {
activated: count !== 0,
version: config.version(),
apiServerOrigin: config.apiServerOrigin(), // used by CaaS tool
provider: config.provider(),
cloudronName: cloudronName,
adminFqdn: config.fqdn() ? config.adminFqdn() : null,
webadminStatus: gWebadminStatus
});
});
});
}
function getDisks(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -164,7 +448,7 @@ function getConfig(callback) {
callback(null, {
apiServerOrigin: config.apiServerOrigin(),
webServerOrigin: config.webServerOrigin(),
adminDomain: config.adminDomain(),
fqdn: config.fqdn(),
adminLocation: config.adminLocation(),
adminFqdn: config.adminFqdn(),
mailFqdn: config.mailFqdn(),
@@ -185,6 +469,167 @@ function getConfig(callback) {
});
}
function ensureDkimKey(callback) {
assert(config.fqdn(), 'fqdn is not set');
var dkimPath = path.join(paths.MAIL_DATA_DIR, 'dkim/' + config.fqdn());
var dkimPrivateKeyFile = path.join(dkimPath, 'private');
var dkimPublicKeyFile = path.join(dkimPath, 'public');
if (!fs.existsSync(dkimPrivateKeyFile) || !fs.existsSync(dkimPublicKeyFile)) {
debug('Generating new DKIM keys');
if (!safe.fs.mkdirSync(dkimPath) && safe.error.code !== 'EEXIST') {
debug('Error creating dkim.', safe.error);
return null;
}
child_process.execSync('openssl genrsa -out ' + dkimPrivateKeyFile + ' 1024');
child_process.execSync('openssl rsa -in ' + dkimPrivateKeyFile + ' -out ' + dkimPublicKeyFile + ' -pubout -outform PEM');
} else {
debug('DKIM keys already present');
}
callback();
}
function readDkimPublicKeySync() {
if (!config.fqdn()) {
debug('Cannot read dkim public key without a domain.', safe.error);
return null;
}
var dkimPath = path.join(paths.MAIL_DATA_DIR, 'dkim/' + config.fqdn());
var dkimPublicKeyFile = path.join(dkimPath, 'public');
var publicKey = safe.fs.readFileSync(dkimPublicKeyFile, 'utf8');
if (publicKey === null) {
debug('Error reading dkim public key.', safe.error);
return null;
}
// remove header, footer and new lines
publicKey = publicKey.split('\n').slice(1, -2).join('');
return publicKey;
}
// NOTE: if you change the SPF record here, be sure the wait check in mailer.js
// https://agari.zendesk.com/hc/en-us/articles/202952749-How-long-can-my-SPF-record-be-
function txtRecordsWithSpf(callback) {
assert.strictEqual(typeof callback, 'function');
domains.getDNSRecords('', config.fqdn(), 'TXT', function (error, txtRecords) {
if (error) return callback(error);
debug('txtRecordsWithSpf: current txt records - %j', txtRecords);
var i, matches, validSpf;
for (i = 0; i < txtRecords.length; i++) {
matches = txtRecords[i].match(/^("?v=spf1) /); // DO backend may return without quotes
if (matches === null) continue;
// this won't work if the entry is arbitrarily "split" across quoted strings
validSpf = txtRecords[i].indexOf('a:' + config.adminFqdn()) !== -1;
break; // there can only be one SPF record
}
if (validSpf) return callback(null, null);
if (!matches) { // no spf record was found, create one
txtRecords.push('"v=spf1 a:' + config.adminFqdn() + ' ~all"');
debug('txtRecordsWithSpf: adding txt record');
} else { // just add ourself
txtRecords[i] = matches[1] + ' a:' + config.adminFqdn() + txtRecords[i].slice(matches[1].length);
debug('txtRecordsWithSpf: inserting txt record');
}
return callback(null, txtRecords);
});
}
function addDnsRecords(ip, callback) {
assert.strictEqual(typeof ip, 'string');
callback = callback || NOOP_CALLBACK;
if (process.env.BOX_ENV === 'test') return callback();
var dkimKey = readDkimPublicKeySync();
if (!dkimKey) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, new Error('Failed to read dkim public key')));
var webadminRecord = { subdomain: config.adminLocation(), domain: config.fqdn(), type: 'A', values: [ ip ] };
// t=s limits the domainkey to this domain and not it's subdomains
var dkimRecord = { subdomain: config.dkimSelector() + '._domainkey', domain: config.fqdn(), type: 'TXT', values: [ '"v=DKIM1; t=s; p=' + dkimKey + '"' ] };
var records = [ ];
records.push(webadminRecord);
records.push(dkimRecord);
debug('addDnsRecords: %j', records);
async.retry({ times: 10, interval: 20000 }, function (retryCallback) {
txtRecordsWithSpf(function (error, txtRecords) {
if (error) return retryCallback(error);
if (txtRecords) records.push({ subdomain: '', domain: config.fqdn(), type: 'TXT', values: txtRecords });
debug('addDnsRecords: will update %j', records);
async.mapSeries(records, function (record, iteratorCallback) {
domains.upsertDNSRecords(record.subdomain, record.domain, record.type, record.values, iteratorCallback);
}, function (error, changeIds) {
if (error) debug('addDnsRecords: failed to update : %s. will retry', error);
else debug('addDnsRecords: records %j added with changeIds %j', records, changeIds);
retryCallback(error);
});
});
}, function (error) {
if (error) debug('addDnsRecords: done updating records with error:', error);
else debug('addDnsRecords: done');
callback(error);
});
}
function restore(backupConfig, backupId, version, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof version, 'string');
assert.strictEqual(typeof callback, 'function');
if (!semver.valid(version)) return callback(new CloudronError(CloudronError.BAD_STATE, 'version is not a valid semver'));
if (semver.major(config.version()) !== semver.major(version) || semver.minor(config.version()) !== semver.minor(version)) return callback(new CloudronError(CloudronError.BAD_STATE, `Run cloudron-setup with --version ${version} to restore from this backup`));
user.count(function (error, count) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
if (count) return callback(new CloudronError(CloudronError.ALREADY_PROVISIONED, 'Already activated'));
backups.testConfig(backupConfig, function (error) {
if (error && error.reason === BackupsError.BAD_FIELD) return callback(new CloudronError(CloudronError.BAD_FIELD, error.message));
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new CloudronError(CloudronError.EXTERNAL_ERROR, error.message));
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
debug(`restore: restoring from ${backupId} from provider ${backupConfig.provider}`);
gWebadminStatus.restoring = true;
callback(null); // do no block
async.series([
backups.restore.bind(null, backupConfig, backupId),
autoprovision,
shell.sudo.bind(null, 'restart', [ RESTART_CMD ])
], function (error) {
debug('restore:', error);
gWebadminStatus.restoring = false;
});
});
});
}
function reboot(callback) {
shell.sudo('reboot', [ REBOOT_CMD ], callback);
}
@@ -274,17 +719,21 @@ function doUpdate(boxUpdateInfo, callback) {
// NOTE: this data is opaque and will be passed through the installer.sh
var data= {
provider: config.provider(),
token: config.token(),
apiServerOrigin: config.apiServerOrigin(),
webServerOrigin: config.webServerOrigin(),
adminDomain: config.adminDomain(),
fqdn: config.fqdn(),
adminFqdn: config.adminFqdn(),
adminLocation: config.adminLocation(),
isDemo: config.isDemo(),
zoneName: config.zoneName(),
appstore: {
token: config.token(),
apiServerOrigin: config.apiServerOrigin()
},
caas: {
token: config.token(),
apiServerOrigin: config.apiServerOrigin(),
webServerOrigin: config.webServerOrigin()
},
@@ -346,6 +795,40 @@ function checkDiskSpace(callback) {
});
}
// called for dynamic dns setups where we have to update the IP
function refreshDNS(callback) {
callback = callback || NOOP_CALLBACK;
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
debug('refreshDNS: current ip %s', ip);
addDnsRecords(ip, function (error) {
if (error) return callback(error);
debug('refreshDNS: done for system records');
apps.getAll(function (error, result) {
if (error) return callback(error);
async.each(result, function (app, callback) {
// do not change state of installing apps since apptask will error if dns record already exists
if (app.installationState !== appdb.ISTATE_INSTALLED) return callback();
domains.upsertDNSRecords(app.location, app.domain, 'A', [ ip ], callback);
}, function (error) {
if (error) return callback(error);
debug('refreshDNS: done for apps');
callback();
});
});
});
});
}
function getLogs(options, callback) {
assert(options && typeof options === 'object');
assert.strictEqual(typeof callback, 'function');
+34 -13
View File
@@ -16,11 +16,12 @@ exports = module.exports = {
provider: provider,
apiServerOrigin: apiServerOrigin,
webServerOrigin: webServerOrigin,
adminDomain: adminDomain,
setFqdn: setAdminDomain,
setAdminDomain: setAdminDomain,
fqdn: fqdn,
zoneName: zoneName,
setFqdn: setFqdn,
setAdminFqdn: setAdminFqdn,
setAdminLocation: setAdminLocation,
token: token,
version: version,
setVersion: setVersion,
database: database,
@@ -33,6 +34,7 @@ exports = module.exports = {
adminFqdn: adminFqdn,
mailLocation: mailLocation,
mailFqdn: mailFqdn,
setZoneName: setZoneName,
hasIPv6: hasIPv6,
dkimSelector: dkimSelector,
@@ -46,6 +48,7 @@ var assert = require('assert'),
fs = require('fs'),
path = require('path'),
safe = require('safetydance'),
tld = require('tldjs'),
_ = require('underscore');
@@ -70,10 +73,12 @@ function saveSync() {
// only save values we want to have in the cloudron.conf, see start.sh
var conf = {
version: data.version,
token: data.token,
apiServerOrigin: data.apiServerOrigin,
webServerOrigin: data.webServerOrigin,
adminDomain: data.adminDomain,
fqdn: data.fqdn,
adminFqdn: data.adminFqdn,
zoneName: data.zoneName,
adminLocation: data.adminLocation,
provider: data.provider,
isDemo: data.isDemo
@@ -92,14 +97,16 @@ function _reset(callback) {
function initConfig() {
// setup defaults
data.fqdn = '';
data.adminFqdn = '';
data.adminDomain = '';
data.zoneName = '';
data.adminLocation = 'my';
data.port = 3000;
data.token = null;
data.version = null;
data.apiServerOrigin = null;
data.webServerOrigin = null;
data.provider = 'generic';
data.provider = 'caas';
data.smtpPort = 2525; // this value comes from mail container
data.sysadminPort = 3001;
data.ldapPort = 3002;
@@ -117,6 +124,7 @@ function initConfig() {
if (exports.TEST) {
data.version = '1.1.1-test';
data.port = 5454;
data.token = 'APPSTORE_TOKEN';
data.apiServerOrigin = 'http://localhost:6060'; // hock doesn't support https
data.database.password = '';
data.database.name = 'boxtest';
@@ -140,7 +148,6 @@ function set(key, value) {
} else {
data = safe.set(data, key, value);
}
saveSync();
}
@@ -158,12 +165,24 @@ function webServerOrigin() {
return get('webServerOrigin');
}
function setAdminDomain(domain) {
set('adminDomain', domain);
function setFqdn(fqdn) {
set('fqdn', fqdn);
}
function adminDomain() {
return get('adminDomain');
function fqdn() {
return get('fqdn');
}
function setZoneName(zone) {
set('zoneName', zone);
}
function zoneName() {
var zone = get('zoneName');
if (zone) return zone;
// TODO: move this to migration code path instead
return tld.getDomain(fqdn()) || '';
}
function mailLocation() {
@@ -203,6 +222,10 @@ function sysadminOrigin() {
return 'http://127.0.0.1:' + get('sysadminPort');
}
function token() {
return get('token');
}
function version() {
return get('version');
}
@@ -228,8 +251,6 @@ function hasIPv6() {
return fs.existsSync(IPV6_PROC_FILE);
}
// it has to change with the adminLocation so that multiple cloudrons
// can send out emails at the same time.
function dkimSelector() {
var loc = adminLocation();
return loc === 'my' ? 'cloudron' : `cloudron-${loc.replace(/\./g, '')}`;
+2 -3
View File
@@ -4,14 +4,13 @@ exports = module.exports = {
API_LOCATION: 'api', // this is unused but reserved for future use (#403)
SMTP_LOCATION: 'smtp',
IMAP_LOCATION: 'imap',
POSTMAN_LOCATION: 'postman', // used in dovecot bounces
// These are combined into one array because users and groups become mailboxes
RESERVED_NAMES: [
// Reserved usernames
// https://github.com/gogits/gogs/blob/52c8f691630548fe091d30bcfe8164545a05d3d5/models/repo.go#L393
// apps like wordpress, gogs don't like these
// postmaster is used in dovecot and haraka
'admin', 'no-reply', 'postmaster', 'mailer-daemon',
'admin', 'no-reply', 'postmaster', 'mailer-daemon', // apps like wordpress, gogs don't like these
// Reserved groups
'admins', 'users' // ldap code uses 'users' pseudo group
+13 -10
View File
@@ -10,16 +10,15 @@ var apps = require('./apps.js'),
assert = require('assert'),
backups = require('./backups.js'),
caas = require('./caas.js'),
certificates = require('./certificates.js'),
cloudron = require('./cloudron.js'),
config = require('./config.js'),
constants = require('./constants.js'),
CronJob = require('cron').CronJob,
debug = require('debug')('box:cron'),
digest = require('./digest.js'),
dyndns = require('./dyndns.js'),
eventlog = require('./eventlog.js'),
janitor = require('./janitor.js'),
reverseProxy = require('./reverseproxy.js'),
scheduler = require('./scheduler.js'),
settings = require('./settings.js'),
semver = require('semver'),
@@ -79,14 +78,14 @@ function initialize(callback) {
settings.events.on(settings.TIME_ZONE_KEY, recreateJobs);
settings.events.on(settings.AUTOUPDATE_PATTERN_KEY, autoupdatePatternChanged);
settings.events.on(settings.DYNAMIC_DNS_KEY, dynamicDnsChanged);
settings.events.on(settings.DYNAMIC_DNS_KEY, dynamicDNSChanged);
settings.getAll(function (error, allSettings) {
if (error) return callback(error);
recreateJobs(allSettings[settings.TIME_ZONE_KEY]);
autoupdatePatternChanged(allSettings[settings.AUTOUPDATE_PATTERN_KEY]);
dynamicDnsChanged(allSettings[settings.DYNAMIC_DNS_KEY]);
dynamicDNSChanged(allSettings[settings.DYNAMIC_DNS_KEY]);
callback();
});
@@ -175,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
});
@@ -204,8 +203,12 @@ function autoupdatePatternChanged(pattern) {
onTick: function() {
var updateInfo = updateChecker.getUpdateInfo();
if (updateInfo.box) {
debug('Starting autoupdate to %j', updateInfo.box);
cloudron.updateToLatest(AUDIT_SOURCE, NOOP_CALLBACK);
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);
@@ -218,7 +221,7 @@ function autoupdatePatternChanged(pattern) {
});
}
function dynamicDnsChanged(enabled) {
function dynamicDNSChanged(enabled) {
assert.strictEqual(typeof enabled, 'boolean');
assert(gJobs.boxUpdateCheckerJob);
@@ -227,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
});
@@ -242,7 +245,7 @@ function uninitialize(callback) {
settings.events.removeListener(settings.TIME_ZONE_KEY, recreateJobs);
settings.events.removeListener(settings.AUTOUPDATE_PATTERN_KEY, autoupdatePatternChanged);
settings.events.removeListener(settings.DYNAMIC_DNS_KEY, dynamicDnsChanged);
settings.events.removeListener(settings.DYNAMIC_DNS_KEY, dynamicDNSChanged);
for (var job in gJobs) {
if (!gJobs[job]) continue;
+1
View File
@@ -104,6 +104,7 @@ function clear(callback) {
async.series([
child_process.exec.bind(null, cmd),
require('./clientdb.js')._addDefaultClients,
require('./domaindb.js')._addDefaultDomain,
require('./groupdb.js')._addDefaultGroups
], callback);
}
+46 -46
View File
@@ -46,16 +46,16 @@ function getZoneByName(dnsConfig, zoneName, callback) {
assert.strictEqual(typeof callback, 'function');
superagent.get(CLOUDFLARE_ENDPOINT + '/zones?name=' + zoneName + '&status=active')
.set('X-Auth-Key', dnsConfig.token)
.set('X-Auth-Email', dnsConfig.email)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(error);
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
if (!result.body.result.length) return callback(new DomainError(DomainError.NOT_FOUND, util.format('%s %j', result.statusCode, result.body)));
.set('X-Auth-Key', dnsConfig.token)
.set('X-Auth-Email', dnsConfig.email)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(error);
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
if (!result.body.result.length) return callback(new DomainError(DomainError.NOT_FOUND, util.format('%s %j', result.statusCode, result.body)));
callback(null, result.body.result[0]);
});
callback(null, result.body.result[0]);
});
}
function getDNSRecordsByZoneId(dnsConfig, zoneId, zoneName, subdomain, type, callback) {
@@ -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) {
@@ -126,31 +126,31 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
if (i >= dnsRecords.length) {
superagent.post(CLOUDFLARE_ENDPOINT + '/zones/'+ zoneId + '/dns_records')
.set('X-Auth-Key',dnsConfig.token)
.set('X-Auth-Email',dnsConfig.email)
.send(data)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(error);
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
.set('X-Auth-Key',dnsConfig.token)
.set('X-Auth-Email',dnsConfig.email)
.send(data)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(error);
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
callback(null);
});
callback(null);
});
} else {
superagent.put(CLOUDFLARE_ENDPOINT + '/zones/'+ zoneId + '/dns_records/' + dnsRecords[i].id)
.set('X-Auth-Key',dnsConfig.token)
.set('X-Auth-Email',dnsConfig.email)
.send(data)
.timeout(30 * 1000)
.end(function (error, result) {
.set('X-Auth-Key',dnsConfig.token)
.set('X-Auth-Email',dnsConfig.email)
.send(data)
.timeout(30 * 1000)
.end(function (error, result) {
// increment, as we have consumed the record
++i;
++i;
if (error && !error.response) return callback(error);
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
if (error && !error.response) return callback(error);
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
callback(null);
});
callback(null);
});
}
}, function (error) {
if (error) return callback(error);
@@ -206,17 +206,17 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
async.eachSeries(tmp, function (record, callback) {
superagent.del(CLOUDFLARE_ENDPOINT + '/zones/'+ zoneId + '/dns_records/' + record.id)
.set('X-Auth-Key',dnsConfig.token)
.set('X-Auth-Email',dnsConfig.email)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(error);
.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);
+1
View File
@@ -9,6 +9,7 @@ exports = module.exports = {
};
var assert = require('assert'),
config = require('../config.js'),
debug = require('debug')('box:dns/gcdns'),
dns = require('dns'),
DomainError = require('../domains.js').DomainError,
+40 -34
View File
@@ -7,23 +7,22 @@ exports = module.exports = {
get: get,
getAll: getAll,
update: update,
upsert: upsert,
del: del,
_clear: clear
_clear: clear,
_addDefaultDomain: addDefaultDomain
};
var assert = require('assert'),
database = require('./database.js'),
DatabaseError = require('./databaseerror'),
config = require('./config.js'),
safe = require('safetydance');
var DOMAINS_FIELDS = [ 'domain', 'zoneName', 'provider', 'configJson', 'tlsConfigJson' ].join(',');
function postProcess(data) {
data.config = safe.JSON.parse(data.configJson);
data.tlsConfig = safe.JSON.parse(data.tlsConfigJson);
delete data.configJson;
delete data.tlsConfigJson;
return data;
}
@@ -32,7 +31,7 @@ function get(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
database.query(`SELECT ${DOMAINS_FIELDS} FROM domains WHERE domain=?`, [ domain ], function (error, result) {
database.query('SELECT * FROM domains WHERE domain=?', [ domain ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
@@ -43,7 +42,7 @@ function get(domain, callback) {
}
function getAll(callback) {
database.query(`SELECT ${DOMAINS_FIELDS} FROM domains ORDER BY domain`, function (error, results) {
database.query('SELECT * FROM domains ORDER BY domain', function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
results.forEach(postProcess);
@@ -52,16 +51,14 @@ function getAll(callback) {
});
}
function add(name, domain, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'object');
assert.strictEqual(typeof domain.zoneName, 'string');
assert.strictEqual(typeof domain.provider, 'string');
assert.strictEqual(typeof domain.config, 'object');
assert.strictEqual(typeof domain.tlsConfig, 'object');
function add(domain, zoneName, provider, config, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof provider, 'string');
assert.strictEqual(typeof config, 'object');
assert.strictEqual(typeof callback, 'function');
database.query('INSERT INTO domains (domain, zoneName, provider, configJson, tlsConfigJson) VALUES (?, ?, ?, ?, ?)', [ name, domain.zoneName, domain.provider, JSON.stringify(domain.config), JSON.stringify(domain.tlsConfig) ], function (error) {
database.query('INSERT INTO domains (domain, zoneName, provider, configJson) VALUES (?, ?, ?, ?)', [ domain, zoneName, provider, JSON.stringify(config) ], function (error) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
@@ -69,27 +66,27 @@ function add(name, domain, callback) {
});
}
function update(name, domain, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'object');
function upsert(domain, zoneName, provider, config, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof provider, 'string');
assert.strictEqual(typeof config, 'object');
assert.strictEqual(typeof callback, 'function');
var args = [ ], fields = [ ];
for (var k in domain) {
if (k === 'config') {
fields.push('configJson = ?');
args.push(JSON.stringify(domain[k]));
} else if (k === 'tlsConfig') {
fields.push('tlsConfigJson = ?');
args.push(JSON.stringify(domain[k]));
} else {
fields.push(k + ' = ?');
args.push(domain[k]);
}
}
args.push(name);
database.query('REPLACE INTO domains (domain, zoneName, provider, configJson) VALUES (?, ?, ?, ?)', [ domain, zoneName, provider, JSON.stringify(config) ], function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
database.query('UPDATE domains SET ' + fields.join(', ') + ' WHERE domain=?', args, function (error) {
callback(null);
});
}
function update(domain, provider, config, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof provider, 'string');
assert.strictEqual(typeof config, 'object');
assert.strictEqual(typeof callback, 'function');
database.query('UPDATE domains SET provider=?, configJson=? WHERE domain=?', [ provider, JSON.stringify(config), domain ], function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
@@ -104,7 +101,7 @@ function del(domain, callback) {
database.query('DELETE FROM domains WHERE domain=?', [ domain ], function (error, result) {
if (error && error.code === 'ER_ROW_IS_REFERENCED_2') return callback(new DatabaseError(DatabaseError.IN_USE));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
callback(null);
});
@@ -117,3 +114,12 @@ function clear(callback) {
callback(error);
});
}
function addDefaultDomain(callback) {
assert(config.fqdn(), 'no fqdn set in config, cannot continue');
add(config.fqdn(), config.zoneName(), 'manual', { }, function (error) {
if (error && error.reason !== DatabaseError.ALREADY_EXISTS) return callback(error);
callback();
});
}
+24 -52
View File
@@ -22,13 +22,12 @@ module.exports = exports = {
var assert = require('assert'),
caas = require('./caas.js'),
config = require('./config.js'),
certificates = require('./certificates.js'),
CertificatesError = certificates.CertificatesError,
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:domains'),
domaindb = require('./domaindb.js'),
path = require('path'),
reverseProxy = require('./reverseproxy.js'),
ReverseProxyError = reverseProxy.ReverseProxyError,
safe = require('safetydance'),
shell = require('./shell.js'),
sysinfo = require('./sysinfo.js'),
tld = require('tldjs'),
@@ -83,6 +82,7 @@ function api(provider) {
}
}
// TODO make it return a DomainError instead of DomainError
function verifyDnsConfig(config, domain, zoneName, provider, ip, callback) {
assert(config && typeof config === 'object'); // the dns config to test with
assert.strictEqual(typeof domain, 'string');
@@ -98,32 +98,22 @@ function verifyDnsConfig(config, domain, zoneName, provider, ip, callback) {
}
function add(domain, zoneName, provider, config, fallbackCertificate, tlsConfig, callback) {
function add(domain, zoneName, provider, config, fallbackCertificate, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof provider, 'string');
assert.strictEqual(typeof config, 'object');
assert.strictEqual(typeof fallbackCertificate, 'object');
assert.strictEqual(typeof tlsConfig, 'object');
assert.strictEqual(typeof callback, 'function');
if (!tld.isValid(domain)) return callback(new DomainError(DomainError.BAD_FIELD, 'Invalid domain'));
if (zoneName) {
if (!tld.isValid(zoneName)) return callback(new DomainError(DomainError.BAD_FIELD, 'Invalid zoneName'));
} else {
zoneName = tld.getDomain(domain) || domain;
}
if (!tld.isValid(zoneName)) return callback(new DomainError(DomainError.BAD_FIELD, 'Invalid zoneName'));
if (fallbackCertificate) {
let error = reverseProxy.validateCertificate(fallbackCertificate.cert, fallbackCertificate.key, domain);
let error = certificates.validateCertificate(fallbackCertificate.cert, fallbackCertificate.key, domain);
if (error) return callback(new DomainError(DomainError.BAD_FIELD, error.message));
}
if (tlsConfig.provider !== 'fallback' && tlsConfig.provider !== 'caas' && tlsConfig.provider.indexOf('letsencrypt-') !== 0) {
return callback(new DomainError(DomainError.BAD_FIELD, 'tlsConfig.provider must be caas, fallback or le-*'));
}
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, 'Error getting IP:' + error.message));
@@ -135,13 +125,15 @@ function add(domain, zoneName, provider, config, fallbackCertificate, tlsConfig,
if (error && error.reason === DomainError.INVALID_PROVIDER) return callback(new DomainError(DomainError.BAD_FIELD, error.message));
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
domaindb.add(domain, { zoneName: zoneName, provider: provider, config: result, tlsConfig: tlsConfig }, function (error) {
domaindb.add(domain, zoneName, provider, result, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new DomainError(DomainError.ALREADY_EXISTS));
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
reverseProxy.setFallbackCertificate(domain, fallbackCertificate, function (error) {
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
if (!fallbackCertificate) return callback();
// cert validation already happened above no need to check all errors again
certificates.setFallbackCertificate(fallbackCertificate.cert, fallbackCertificate.key, domain, function (error) {
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
callback();
});
});
@@ -158,15 +150,10 @@ function get(domain, callback) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DomainError(DomainError.NOT_FOUND));
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
reverseProxy.getFallbackCertificate(domain, function (error, bundle) {
if (error && error.reason !== ReverseProxyError.NOT_FOUND) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
certificates.getFallbackCertificate(domain, function (error, fallbackCertificate) {
if (error && error.reason !== CertificatesError.NOT_FOUND) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
var cert = safe.fs.readFileSync(bundle.certFilePath, 'utf-8');
var key = safe.fs.readFileSync(bundle.keyFilePath, 'utf-8');
if (!cert || !key) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
result.fallbackCertificate = { cert: cert, key: key };
if (fallbackCertificate) result.fallbackCertificate = fallbackCertificate;
return callback(null, result);
});
@@ -183,12 +170,11 @@ function getAll(callback) {
});
}
function update(domain, provider, config, fallbackCertificate, tlsConfig, callback) {
function update(domain, provider, config, fallbackCertificate, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof provider, 'string');
assert.strictEqual(typeof config, 'object');
assert.strictEqual(typeof fallbackCertificate, 'object');
assert.strictEqual(typeof tlsConfig, 'object');
assert.strictEqual(typeof callback, 'function');
domaindb.get(domain, function (error, result) {
@@ -196,14 +182,10 @@ function update(domain, provider, config, fallbackCertificate, tlsConfig, callba
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
if (fallbackCertificate) {
let error = reverseProxy.validateCertificate(fallbackCertificate.cert, fallbackCertificate.key, domain);
let error = certificates.validateCertificate(fallbackCertificate.cert, fallbackCertificate.key, domain);
if (error) return callback(new DomainError(DomainError.BAD_FIELD, error.message));
}
if (tlsConfig.provider !== 'fallback' && tlsConfig.provider !== 'caas' && tlsConfig.provider.indexOf('letsencrypt-') !== 0) {
return callback(new DomainError(DomainError.BAD_FIELD, 'tlsConfig.provider must be caas, fallback or letsencrypt-*'));
}
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, 'Error getting IP:' + error.message));
@@ -215,15 +197,15 @@ function update(domain, provider, config, fallbackCertificate, tlsConfig, callba
if (error && error.reason === DomainError.INVALID_PROVIDER) return callback(new DomainError(DomainError.BAD_FIELD, error.message));
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
domaindb.update(domain, { provider: provider, config: result, tlsConfig: tlsConfig }, function (error) {
domaindb.update(domain, provider, result, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DomainError(DomainError.NOT_FOUND));
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
if (!fallbackCertificate) return callback();
reverseProxy.setFallbackCertificate(domain, fallbackCertificate, function (error) {
// cert validation already happened above no need to check all errors again
certificates.setFallbackCertificate(fallbackCertificate.cert, fallbackCertificate.key, domain, function (error) {
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
callback();
});
});
@@ -245,17 +227,6 @@ function del(domain, callback) {
});
}
function getName(domain, subdomain) {
// support special caas domains
if (domain.provider === 'caas') return subdomain;
if (domain.domain === domain.zoneName) return subdomain;
var part = domain.domain.slice(0, -domain.zoneName.length - 1);
return subdomain === '' ? part : subdomain + '.' + part;
}
function getDNSRecords(subdomain, domain, type, callback) {
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof domain, 'string');
@@ -265,7 +236,7 @@ function getDNSRecords(subdomain, domain, type, callback) {
get(domain, function (error, result) {
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
api(result.provider).get(result.config, result.zoneName, getName(result, subdomain), type, function (error, values) {
api(result.provider).get(result.config, result.zoneName, subdomain, type, function (error, values) {
if (error) return callback(error);
callback(null, values);
@@ -285,7 +256,7 @@ function upsertDNSRecords(subdomain, domain, type, values, callback) {
get(domain, function (error, result) {
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
api(result.provider).upsert(result.config, result.zoneName, getName(result, subdomain), type, values, function (error, changeId) {
api(result.provider).upsert(result.config, result.zoneName, subdomain, type, values, function (error, changeId) {
if (error) return callback(error);
callback(null, changeId);
@@ -305,7 +276,7 @@ function removeDNSRecords(subdomain, domain, type, values, callback) {
get(domain, function (error, result) {
if (error) return callback(error);
api(result.provider).del(result.config, result.zoneName, getName(result, subdomain), type, values, function (error) {
api(result.provider).del(result.config, result.zoneName, subdomain, type, values, function (error) {
if (error && error.reason !== DomainError.NOT_FOUND) return callback(error);
callback(null);
@@ -346,9 +317,10 @@ function setAdmin(domain, callback) {
setPtrRecord(domain, function (error) {
if (error) return callback(new DomainError(DomainError.EXTERNAL_ERROR, 'Error setting PTR record:' + error.message));
config.setAdminDomain(result.domain);
config.setFqdn(result.domain);
config.setAdminLocation('my');
config.setAdminFqdn('my' + (result.provider === 'caas' ? '-' : '.') + result.domain);
config.setZoneName(result.zoneName);
callback();
-49
View File
@@ -1,49 +0,0 @@
'use strict';
exports = module.exports = {
sync: sync
};
var appdb = require('./appdb.js'),
apps = require('./apps.js'),
async = require('async'),
config = require('./config.js'),
debug = require('debug')('box:dyndns'),
domains = require('./domains.js'),
sysinfo = require('./sysinfo.js');
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
// called for dynamic dns setups where we have to update the IP
function sync(callback) {
callback = callback || NOOP_CALLBACK;
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error);
debug('refreshDNS: current ip %s', ip);
domains.upsertDNSRecords(config.adminLocation(), config.adminDomain(), 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('refreshDNS: done for admin location');
apps.getAll(function (error, result) {
if (error) return callback(error);
async.each(result, function (app, callback) {
// do not change state of installing apps since apptask will error if dns record already exists
if (app.installationState !== appdb.ISTATE_INSTALLED) return callback();
domains.upsertDNSRecords(app.location, app.domain, 'A', [ ip ], callback);
}, function (error) {
if (error) return callback(error);
debug('refreshDNS: done for apps');
callback();
});
});
});
});
}
+398
View File
@@ -0,0 +1,398 @@
'use strict';
exports = module.exports = {
verifyRelay: verifyRelay,
getStatus: getStatus,
checkRblStatus: checkRblStatus,
EmailError: EmailError
};
var assert = require('assert'),
async = require('async'),
cloudron = require('./cloudron.js'),
config = require('./config.js'),
debug = require('debug')('box:email'),
dig = require('./dig.js'),
net = require('net'),
nodemailer = require('nodemailer'),
safe = require('safetydance'),
settings = require('./settings.js'),
smtpTransport = require('nodemailer-smtp-transport'),
sysinfo = require('./sysinfo.js'),
util = require('util'),
_ = require('underscore');
const digOptions = { server: '127.0.0.1', port: 53, timeout: 5000 };
function EmailError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
Error.call(this);
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.reason = reason;
if (typeof errorOrMessage === 'undefined') {
this.message = reason;
} else if (typeof errorOrMessage === 'string') {
this.message = errorOrMessage;
} else {
this.message = 'Internal error';
this.nestedError = errorOrMessage;
}
}
util.inherits(EmailError, Error);
EmailError.INTERNAL_ERROR = 'Internal Error';
EmailError.BAD_FIELD = 'Bad Field';
function checkOutboundPort25(callback) {
assert.strictEqual(typeof callback, 'function');
var smtpServer = _.sample([
'smtp.gmail.com',
'smtp.live.com',
'smtp.mail.yahoo.com',
'smtp.o2.ie',
'smtp.comcast.net',
'outgoing.verizon.net'
]);
var relay = {
value: 'OK',
status: false
};
var client = new net.Socket();
client.setTimeout(5000);
client.connect(25, smtpServer);
client.on('connect', function () {
relay.status = true;
relay.value = 'OK';
client.destroy(); // do not use end() because it still triggers timeout
callback(null, relay);
});
client.on('timeout', function () {
relay.status = false;
relay.value = 'Connect to ' + smtpServer + ' timed out';
client.destroy();
callback(new Error('Timeout'), relay);
});
client.on('error', function (error) {
relay.status = false;
relay.value = 'Connect to ' + smtpServer + ' failed: ' + error.message;
client.destroy();
callback(error, relay);
});
}
function checkSmtpRelay(relay, callback) {
var result = {
value: 'OK',
status: false
};
var transporter = nodemailer.createTransport(smtpTransport({
host: relay.host,
port: relay.port,
auth: {
user: relay.username,
pass: relay.password
}
}));
transporter.verify(function(error) {
result.status = !error;
if (error) {
result.value = error.message;
return callback(error, result);
}
callback(null, result);
});
}
function verifyRelay(relay, callback) {
assert.strictEqual(typeof relay, 'object');
assert.strictEqual(typeof callback, 'function');
var verifier = relay.provider === 'cloudron-smtp' ? checkOutboundPort25 : checkSmtpRelay.bind(null, relay);
verifier(function (error) {
if (error) return callback(new EmailError(EmailError.BAD_FIELD, error.message));
callback();
});
}
function checkDkim(callback) {
var dkim = {
domain: config.dkimSelector() + '._domainkey.' + config.fqdn(),
type: 'TXT',
expected: null,
value: null,
status: false
};
var dkimKey = cloudron.readDkimPublicKeySync();
if (!dkimKey) return callback(new Error('Failed to read dkim public key'), dkim);
dkim.expected = '"v=DKIM1; t=s; p=' + dkimKey + '"';
dig.resolve(dkim.domain, dkim.type, digOptions, function (error, txtRecords) {
if (error && error.code === 'ENOTFOUND') return callback(null, dkim); // not setup
if (error) return callback(error, dkim);
if (Array.isArray(txtRecords) && txtRecords.length !== 0) {
dkim.value = txtRecords[0];
dkim.status = (dkim.value === dkim.expected);
}
callback(null, dkim);
});
}
function checkSpf(callback) {
var spf = {
domain: config.fqdn(),
type: 'TXT',
value: null,
expected: '"v=spf1 a:' + config.adminFqdn() + ' ~all"',
status: false
};
// https://agari.zendesk.com/hc/en-us/articles/202952749-How-long-can-my-SPF-record-be-
dig.resolve(spf.domain, spf.type, digOptions, function (error, txtRecords) {
if (error && error.code === 'ENOTFOUND') return callback(null, spf); // not setup
if (error) return callback(error, spf);
if (!Array.isArray(txtRecords)) return callback(null, spf);
var i;
for (i = 0; i < txtRecords.length; i++) {
if (txtRecords[i].indexOf('"v=spf1 ') !== 0) continue; // not SPF
spf.value = txtRecords[i];
spf.status = spf.value.indexOf(' a:' + config.adminFqdn()) !== -1;
break;
}
if (spf.status) {
spf.expected = spf.value;
} else if (i !== txtRecords.length) {
spf.expected = '"v=spf1 a:' + config.adminFqdn() + ' ' + spf.value.slice('"v=spf1 '.length);
}
callback(null, spf);
});
}
function checkMx(callback) {
var mx = {
domain: config.fqdn(),
type: 'MX',
value: null,
expected: '10 ' + config.mailFqdn() + '.',
status: false
};
dig.resolve(mx.domain, mx.type, digOptions, function (error, mxRecords) {
if (error && error.code === 'ENOTFOUND') return callback(null, mx); // not setup
if (error) return callback(error, mx);
if (Array.isArray(mxRecords) && mxRecords.length !== 0) {
mx.status = mxRecords.length == 1 && mxRecords[0].exchange === (config.mailFqdn() + '.');
mx.value = mxRecords.map(function (r) { return r.priority + ' ' + r.exchange; }).join(' ');
}
callback(null, mx);
});
}
function checkDmarc(callback) {
var dmarc = {
domain: '_dmarc.' + config.fqdn(),
type: 'TXT',
value: null,
expected: '"v=DMARC1; p=reject; pct=100"',
status: false
};
dig.resolve(dmarc.domain, dmarc.type, digOptions, function (error, txtRecords) {
if (error && error.code === 'ENOTFOUND') return callback(null, dmarc); // not setup
if (error) return callback(error, dmarc);
if (Array.isArray(txtRecords) && txtRecords.length !== 0) {
dmarc.value = txtRecords[0];
dmarc.status = (dmarc.value === dmarc.expected);
}
callback(null, dmarc);
});
}
function checkPtr(callback) {
var ptr = {
domain: null,
type: 'PTR',
value: null,
expected: config.mailFqdn() + '.',
status: false
};
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error, ptr);
ptr.domain = ip.split('.').reverse().join('.') + '.in-addr.arpa';
dig.resolve(ip, 'PTR', digOptions, function (error, ptrRecords) {
if (error && error.code === 'ENOTFOUND') return callback(null, ptr); // not setup
if (error) return callback(error, ptr);
if (Array.isArray(ptrRecords) && ptrRecords.length !== 0) {
ptr.value = ptrRecords.join(' ');
ptr.status = ptrRecords.some(function (v) { return v === ptr.expected; });
}
return callback(null, ptr);
});
});
}
// https://raw.githubusercontent.com/jawsome/node-dnsbl/master/list.json
const RBL_LIST = [
{
'name': 'Barracuda',
'dns': 'b.barracudacentral.org',
'site': 'http://www.barracudacentral.org/rbl/removal-request'
},
{
'name': 'SpamCop',
'dns': 'bl.spamcop.net',
'site': 'http://spamcop.net'
},
{
'name': 'Sorbs Aggregate Zone',
'dns': 'dnsbl.sorbs.net',
'site': 'http://dnsbl.sorbs.net/'
},
{
'name': 'Sorbs spam.dnsbl Zone',
'dns': 'spam.dnsbl.sorbs.net',
'site': 'http://sorbs.net'
},
{
'name': 'Composite Blocking List',
'dns': 'cbl.abuseat.org',
'site': 'http://www.abuseat.org'
},
{
'name': 'SpamHaus Zen',
'dns': 'zen.spamhaus.org',
'site': 'http://spamhaus.org'
},
{
'name': 'Multi SURBL',
'dns': 'multi.surbl.org',
'site': 'http://www.surbl.org'
},
{
'name': 'Spam Cannibal',
'dns': 'bl.spamcannibal.org',
'site': 'http://www.spamcannibal.org/cannibal.cgi'
},
{
'name': 'dnsbl.abuse.ch',
'dns': 'spam.abuse.ch',
'site': 'http://dnsbl.abuse.ch/'
},
{
'name': 'The Unsubscribe Blacklist(UBL)',
'dns': 'ubl.unsubscore.com ',
'site': 'http://www.lashback.com/blacklist/'
},
{
'name': 'UCEPROTECT Network',
'dns': 'dnsbl-1.uceprotect.net',
'site': 'http://www.uceprotect.net/en'
}
];
function checkRblStatus(callback) {
assert.strictEqual(typeof callback, 'function');
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error, ip);
var flippedIp = ip.split('.').reverse().join('.');
// https://tools.ietf.org/html/rfc5782
async.map(RBL_LIST, function (rblServer, iteratorDone) {
dig.resolve(flippedIp + '.' + rblServer.dns, 'A', digOptions, function (error, records) {
if (error || !records) return iteratorDone(null, null); // not listed
debug('checkRblStatus: %s (ip: %s) is in the blacklist of %j', config.fqdn(), flippedIp, rblServer);
var result = _.extend({ }, rblServer);
dig.resolve(flippedIp + '.' + rblServer.dns, 'TXT', digOptions, function (error, txtRecords) {
result.txtRecords = error || !txtRecords ? 'No txt record' : txtRecords;
debug('checkRblStatus: %s (error: %s) (txtRecords: %j)', config.fqdn(), error, txtRecords);
return iteratorDone(null, result);
});
});
}, function (ignoredError, blacklistedServers) {
blacklistedServers = blacklistedServers.filter(function(b) { return b !== null; });
debug('checkRblStatus: %s (ip: %s) servers: %j', config.fqdn(), ip, blacklistedServers);
return callback(null, { status: blacklistedServers.length === 0, ip: ip, servers: blacklistedServers });
});
});
}
function getStatus(callback) {
assert.strictEqual(typeof callback, 'function');
var results = {};
function recordResult(what, func) {
return function (callback) {
func(function (error, result) {
if (error) debug('Ignored error - ' + what + ':', error);
safe.set(results, what, result);
callback();
});
};
}
settings.getMailRelay(function (error, relay) {
if (error) return callback(error);
var checks = [
recordResult('dns.mx', checkMx),
recordResult('dns.dmarc', checkDmarc)
];
if (relay.provider === 'cloudron-smtp') {
// these tests currently only make sense when using Cloudron's SMTP server at this point
checks.push(
recordResult('dns.spf', checkSpf),
recordResult('dns.dkim', checkDkim),
recordResult('dns.ptr', checkPtr),
recordResult('relay', checkOutboundPort25),
recordResult('rbl', checkRblStatus)
);
} else {
checks.push(recordResult('relay', checkSmtpRelay.bind(null, relay)));
}
async.parallel(checks, function () {
callback(null, results);
});
});
}
+10 -3
View File
@@ -24,8 +24,10 @@ exports = module.exports = {
var assert = require('assert'),
constants = require('./constants.js'),
config = require('./config.js'),
database = require('./database.js'),
DatabaseError = require('./databaseerror');
DatabaseError = require('./databaseerror'),
mailboxdb = require('./mailboxdb.js');
var GROUPS_FIELDS = [ 'id', 'name' ].join(',');
@@ -87,9 +89,13 @@ function add(id, name, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('INSERT INTO groups (id, name) VALUES (?, ?)', [ id, name ], function (error, result) {
var queries = [];
queries.push({ query: 'INSERT INTO mailboxes (name, domain, ownerId, ownerType) VALUES (?, ?, ?, ?)', args: [ name, config.fqdn(), id, mailboxdb.TYPE_GROUP ] });
queries.push({ query: 'INSERT INTO groups (id, name) VALUES (?, ?)', args: [ id, name ] });
database.transaction(queries, function (error, result) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error));
if (error || result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error || result[1].affectedRows !== 1) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null);
});
@@ -103,6 +109,7 @@ function del(id, callback) {
var queries = [];
queries.push({ query: 'DELETE FROM groupMembers WHERE groupId = ?', args: [ id ] });
queries.push({ query: 'DELETE FROM groups WHERE id = ?', args: [ id ] });
queries.push({ query: 'DELETE FROM mailboxes WHERE ownerId=?', args: [ id ] });
database.transaction(queries, function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
+3 -8
View File
@@ -24,7 +24,6 @@ var assert = require('assert'),
constants = require('./constants.js'),
DatabaseError = require('./databaseerror.js'),
groupdb = require('./groupdb.js'),
mailboxdb = require('./mailboxdb.js'),
util = require('util'),
uuid = require('uuid');
@@ -100,15 +99,11 @@ function remove(id, callback) {
// never allow admin group to be deleted
if (id === constants.ADMIN_GROUP_ID) return callback(new GroupError(GroupError.NOT_ALLOWED));
mailboxdb.delByOwnerId(id, function (error) {
groupdb.del(id, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupError(GroupError.NOT_FOUND));
if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, error));
groupdb.del(id, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupError(GroupError.NOT_FOUND));
if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, error));
callback(null);
});
callback(null);
});
}
+7 -7
View File
@@ -7,18 +7,18 @@
exports = module.exports = {
// a major version makes all apps restore from backup. #451 must be fixed before we do this.
// a minor version makes all apps re-configure themselves
'version': '48.9.0',
'version': '48.8.0',
'baseImages': [ 'cloudron/base:0.10.0' ],
// Note that if any of the databases include an upgrade, bump the infra version above
// This is because we upgrade using dumps instead of mysql_upgrade, pg_upgrade etc
'images': {
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:1.0.0' },
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:1.0.0' },
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:1.0.1' },
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:1.0.0' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:1.1.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' }
}
};
+47 -62
View File
@@ -16,8 +16,6 @@ var assert = require('assert'),
user = require('./user.js'),
UserError = user.UserError,
ldap = require('ldapjs'),
mail = require('./mail.js'),
MailError = mail.MailError,
mailboxdb = require('./mailboxdb.js'),
safe = require('safetydance');
@@ -154,7 +152,7 @@ function userSearch(req, res, next) {
cn: entry.id,
uid: entry.id,
mail: entry.email,
mailAlternateAddress: entry.fallbackEmail,
mailAlternateAddress: entry.alternateEmail,
displayname: displayName,
givenName: firstName,
username: entry.username,
@@ -260,11 +258,14 @@ function mailboxSearch(req, res, next) {
if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString()));
var email = req.dn.rdns[0].attrs.cn.value.toLowerCase();
var parts = email.split('@');
if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString()));
var name = req.dn.rdns[0].attrs.cn.value.toLowerCase();
// allow login via email
var parts = name.split('@');
if (parts[1] === config.fqdn()) {
name = parts[0];
}
mailboxdb.getMailbox(parts[0], parts[1], function (error, mailbox) {
mailboxdb.getMailbox(name, config.fqdn(), function (error, mailbox) {
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.toString()));
@@ -273,9 +274,9 @@ function mailboxSearch(req, res, next) {
attributes: {
objectclass: ['mailbox'],
objectcategory: 'mailbox',
cn: `${mailbox.name}@${mailbox.domain}`,
uid: `${mailbox.name}@${mailbox.domain}`,
mail: `${mailbox.name}@${mailbox.domain}`,
cn: mailbox.name,
uid: mailbox.name,
mail: mailbox.name + '@' + config.fqdn(),
ownerType: mailbox.ownerType
}
};
@@ -297,24 +298,19 @@ function mailAliasSearch(req, res, next) {
if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString()));
var email = req.dn.rdns[0].attrs.cn.value.toLowerCase();
var parts = email.split('@');
if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString()));
mailboxdb.getAlias(parts[0], parts[1], function (error, alias) {
mailboxdb.getAlias(req.dn.rdns[0].attrs.cn.value.toLowerCase(), config.fqdn(), function (error, alias) {
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.toString()));
// https://wiki.debian.org/LDAP/MigrationTools/Examples
// https://docs.oracle.com/cd/E19455-01/806-5580/6jej518pp/index.html
// member is fully qualified - https://docs.oracle.com/cd/E19957-01/816-6082-10/chap4.doc.html#43314
var obj = {
dn: req.dn.toString(),
attributes: {
objectclass: ['nisMailAlias'],
objectcategory: 'nisMailAlias',
cn: `${alias.name}@${alias.domain}`,
rfc822MailMember: `${alias.aliasTarget}@${alias.domain}`
cn: alias.name,
rfc822MailMember: alias.aliasTarget
}
};
@@ -335,24 +331,19 @@ function mailingListSearch(req, res, next) {
if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString()));
var email = req.dn.rdns[0].attrs.cn.value.toLowerCase();
var parts = email.split('@');
if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString()));
mailboxdb.getGroup(parts[0], parts[1], function (error, group) {
mailboxdb.getGroup(req.dn.rdns[0].attrs.cn.value.toLowerCase(), config.fqdn(), function (error, group) {
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.toString()));
// http://ldapwiki.willeke.com/wiki/Original%20Mailgroup%20Schema%20From%20Netscape
// members are fully qualified (https://docs.oracle.com/cd/E19444-01/816-6018-10/groups.htm#13356)
var obj = {
dn: req.dn.toString(),
attributes: {
objectclass: ['mailGroup'],
objectcategory: 'mailGroup',
cn: `${group.name}@${group.domain}`, // fully qualified
mail: `${group.name}@${group.domain}`,
mgrpRFC822MailMember: group.members.map(function (m) { return `${m}@${group.domain}`; })
cn: group.name,
mail: group.name + '@' + config.fqdn(),
mgrpRFC822MailMember: group.members
}
};
@@ -420,47 +411,41 @@ function authorizeUserForApp(req, res, next) {
function authenticateMailbox(req, res, next) {
if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString()));
var email = req.dn.rdns[0].attrs.cn.value.toLowerCase();
var parts = email.split('@');
if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString()));
var name = req.dn.rdns[0].attrs.cn.value.toLowerCase();
mailboxdb.getMailbox(parts[0], parts[1], function (error, mailbox) {
// allow login via email
var parts = name.split('@');
if (parts[1] === config.fqdn()) {
name = parts[0];
}
mailboxdb.getMailbox(name, config.fqdn(), function (error, mailbox) {
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.message));
mail.get(parts[1], function (error, domain) {
if (error && error.reason === MailError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.message));
if (mailbox.ownerType === mailboxdb.TYPE_APP) {
var addonId = req.dn.rdns[1].attrs.ou.value.toLowerCase(); // 'sendmail' or 'recvmail'
var name;
if (addonId === 'sendmail') name = 'MAIL_SMTP_PASSWORD';
else if (addonId === 'recvmail') name = 'MAIL_IMAP_PASSWORD';
else return next(new ldap.OperationsError('Invalid DN'));
if (mailbox.ownerType === mailboxdb.TYPE_APP) {
var addonId = req.dn.rdns[1].attrs.ou.value.toLowerCase(); // 'sendmail' or 'recvmail'
var name;
if (addonId === 'sendmail') name = 'MAIL_SMTP_PASSWORD';
else if (addonId === 'recvmail') name = 'MAIL_IMAP_PASSWORD';
else return next(new ldap.OperationsError('Invalid DN'));
appdb.getAddonConfigByName(mailbox.ownerId, addonId, name, function (error, value) {
if (error) return next(new ldap.OperationsError(error.message));
if (req.credentials !== value) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
appdb.getAddonConfigByName(mailbox.ownerId, addonId, name, function (error, value) {
if (error) return next(new ldap.OperationsError(error.message));
if (req.credentials !== value) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
eventlog.add(eventlog.ACTION_APP_LOGIN, { authType: 'ldap', mailboxId: name }, { appId: mailbox.ownerId, addonId: addonId });
return res.end();
});
} else if (mailbox.ownerType === mailboxdb.TYPE_USER) {
if (!domain.enabled) return next(new ldap.NoSuchObjectError(req.dn.toString()));
user.verifyWithUsername(parts[0], req.credentials || '', function (error, user) {
if (error && error.reason === UserError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error && error.reason === UserError.WRONG_PASSWORD) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.message));
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: email }, { userId: user.username });
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'));
}
});
}
-999
View File
@@ -1,999 +0,0 @@
'use strict';
exports = module.exports = {
getStatus: getStatus,
get: get,
getAll: getAll,
add: add,
del: del,
setMailFromValidation: setMailFromValidation,
setCatchAllAddress: setCatchAllAddress,
setMailRelay: setMailRelay,
setMailEnabled: setMailEnabled,
startMail: restartMail,
sendTestMail: sendTestMail,
getMailboxes: getMailboxes,
getUserMailbox: getUserMailbox,
enableUserMailbox: enableUserMailbox,
disableUserMailbox: disableUserMailbox,
getAliases: getAliases,
setAliases: setAliases,
getLists: getLists,
getList: getList,
addList: addList,
removeList: removeList,
_readDkimPublicKeySync: readDkimPublicKeySync,
MailError: MailError
};
var assert = require('assert'),
async = require('async'),
config = require('./config.js'),
constants = require('./constants.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:mail'),
dig = require('./dig.js'),
domains = require('./domains.js'),
groups = require('./groups.js'),
GroupError = groups.GroupError,
infra = require('./infra_version.js'),
mailboxdb = require('./mailboxdb.js'),
maildb = require('./maildb.js'),
mailer = require('./mailer.js'),
net = require('net'),
nodemailer = require('nodemailer'),
os = require('os'),
path = require('path'),
paths = require('./paths.js'),
reverseProxy = require('./reverseproxy.js'),
safe = require('safetydance'),
shell = require('./shell.js'),
smtpTransport = require('nodemailer-smtp-transport'),
sysinfo = require('./sysinfo.js'),
user = require('./user.js'),
UserError = user.UserError,
util = require('util'),
_ = require('underscore');
const digOptions = { server: '127.0.0.1', port: 53, timeout: 5000 };
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
function MailError(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(MailError, Error);
MailError.INTERNAL_ERROR = 'Internal Error';
MailError.BAD_FIELD = 'Bad Field';
MailError.ALREADY_EXISTS = 'Already Exists';
MailError.NOT_FOUND = 'Not Found';
function validateAlias(alias) {
assert.strictEqual(typeof alias, 'string');
if (alias.length < 1) return new MailError(MailError.BAD_FIELD, 'alias must be atleast 1 char');
if (alias.length >= 200) return new MailError(MailError.BAD_FIELD, 'alias too long');
if (constants.RESERVED_NAMES.indexOf(alias) !== -1) return new MailError(MailError.BAD_FIELD, 'alias is reserved');
// +/- can be tricky in emails. also need to consider valid LDAP characters here (e.g '+' is reserved)
if (/[^a-zA-Z0-9.]/.test(alias)) return new MailError(MailError.BAD_FIELD, 'alias can only contain alphanumerals and dot');
// app emails are sent using the .app suffix
if (alias.indexOf('.app') !== -1) return new MailError(MailError.BAD_FIELD, 'alias pattern is reserved for apps');
return null;
}
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 MailError(MailError.BAD_FIELD, error.message));
callback();
});
}
function checkDkim(domain, callback) {
var dkim = {
domain: config.dkimSelector() + '._domainkey.' + domain,
type: 'TXT',
expected: null,
value: null,
status: false
};
var dkimKey = readDkimPublicKeySync(domain);
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(domain, callback) {
var spf = {
domain: domain,
type: 'TXT',
value: null,
expected: '"v=spf1 a:' + config.mailFqdn() + ' ~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(domain, callback) {
var mx = {
domain: domain,
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(domain, callback) {
var dmarc = {
domain: '_dmarc.' + domain,
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'
}
];
// this function currently only looks for black lists based on IP. TODO: also look up by domain
function checkRblStatus(domain, 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', domain, 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)', domain, error, txtRecords);
return iteratorDone(null, result);
});
});
}, function (ignoredError, blacklistedServers) {
blacklistedServers = blacklistedServers.filter(function(b) { return b !== null; });
debug('checkRblStatus: %s (ip: %s) servers: %j', domain, ip, blacklistedServers);
return callback(null, { status: blacklistedServers.length === 0, ip: ip, servers: blacklistedServers });
});
});
}
function getStatus(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
// ensure we always have a valid toplevel properties for the api
var results = {
dns: {},
rbl: {},
relay: {}
};
function recordResult(what, func) {
return function (callback) {
func(function (error, result) {
if (error) debug('Ignored error - ' + what + ':', error);
safe.set(results, what, result);
callback();
});
};
}
get(domain, function (error, result) {
if (error) return callback(error);
var checks = [
recordResult('dns.mx', checkMx.bind(null, domain)),
recordResult('dns.dmarc', checkDmarc.bind(null, domain))
];
if (result.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.bind(null, domain)),
recordResult('dns.dkim', checkDkim.bind(null, domain)),
recordResult('dns.ptr', checkPtr),
recordResult('relay', checkOutboundPort25),
recordResult('rbl', checkRblStatus.bind(null, domain))
);
} else {
checks.push(recordResult('relay', checkSmtpRelay.bind(null, result.relay)));
}
async.parallel(checks, function () {
callback(null, results);
});
});
}
function createMailConfig(callback) {
assert.strictEqual(typeof callback, 'function');
const mailFqdn = config.mailFqdn();
debug('createMailConfig: generating mail config');
maildb.getAll(function (error, mailOutDomains) {
if (error) return callback(error);
var mailDomain = mailOutDomains[0]; // mail container can only handle one domain at this point
const alertsFrom = `no-reply@${mailDomain.domain}`;
user.getOwner(function (error, owner) {
const alertsTo = config.provider() === 'caas' ? [ 'support@cloudron.io' ] : [ ];
alertsTo.concat(error ? [] : owner.email).join(','); // owner may not exist yet
const mailOutDomain = mailDomain.domain;
const mailInDomain = mailDomain.enabled ? mailDomain.domain : '';
const catchAll = mailDomain.catchAll.map(function (c) { return `${c}@${mailDomain.domain}`; }).join(',');
const mailFromValidation = mailDomain.mailFromValidation;
if (!safe.fs.writeFileSync(paths.ADDON_CONFIG_DIR + '/mail/mail.ini',
`mail_in_domains=${mailInDomain}\nmail_out_domains=${mailOutDomain}\nmail_default_domain=${mailDomain.domain}\nmail_server_name=${mailFqdn}\nalerts_from=${alertsFrom}\nalerts_to=${alertsTo}\ncatch_all=${catchAll}\nmail_from_validation=${mailFromValidation}\n`, 'utf8')) {
return callback(new Error('Could not create mail var file:' + safe.error.message));
}
var relay = mailDomain.relay;
const enabled = relay.provider !== 'cloudron-smtp' ? true : false,
host = relay.host || '',
port = relay.port || 25,
username = relay.username || '',
password = relay.password || '';
if (!safe.fs.writeFileSync(paths.ADDON_CONFIG_DIR + '/mail/smtp_forward.ini',
`enable_outbound=${enabled}\nhost=${host}\nport=${port}\nenable_tls=true\nauth_type=plain\nauth_user=${username}\nauth_pass=${password}`, 'utf8')) {
return callback(new Error('Could not create mail var file:' + safe.error.message));
}
callback(null, mailInDomain.length !== 0);
});
});
}
function restartMail(callback) {
// mail (note: 2525 is hardcoded in mail container and app use this port)
// MAIL_SERVER_NAME is the hostname of the mailserver i.e server uses these certs
// MAIL_DOMAIN is the domain for which this server is relaying mails
// mail container uses /app/data for backed up data and /run for restart-able data
if (process.env.BOX_ENV === 'test' && !process.env.TEST_CREATE_INFRA) return callback();
const tag = infra.images.mail.tag;
const memoryLimit = Math.max((1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 128, 256);
// admin and mail share the same certificate
reverseProxy.getCertificate({ intrinsicFqdn: config.adminFqdn(), domain: config.adminDomain() }, function (error, bundle) {
if (error) return callback(error);
// the setup script copies dhparams.pem to /addons/mail
const mailCertFilePath = path.join(paths.ADDON_CONFIG_DIR, 'mail/tls_cert.pem');
const mailKeyFilePath = path.join(paths.ADDON_CONFIG_DIR, 'mail/tls_key.pem');
if (!safe.child_process.execSync(`cp ${bundle.certFilePath} ${mailCertFilePath}`)) return callback(new Error('Could not create cert file:' + safe.error.message));
if (!safe.child_process.execSync(`cp ${bundle.keyFilePath} ${mailKeyFilePath}`)) return callback(new Error('Could not create key file:' + safe.error.message));
shell.execSync('startMail', 'docker rm -f mail || true');
createMailConfig(function (error, allowInbound) {
if (error) return callback(error);
var ports = allowInbound ? '-p 587:2525 -p 993:9993 -p 4190:4190 -p 25:2525' : '';
const cmd = `docker run --restart=always -d --name="mail" \
--net cloudron \
--net-alias mail \
-m ${memoryLimit}m \
--memory-swap ${memoryLimit * 2}m \
--dns 172.18.0.1 \
--dns-search=. \
-v "${paths.MAIL_DATA_DIR}:/app/data" \
-v "${paths.PLATFORM_DATA_DIR}/addons/mail:/etc/mail" \
${ports} \
--read-only -v /run -v /tmp ${tag}`;
shell.execSync('startMail', cmd);
callback();
});
});
}
function get(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
maildb.get(domain, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
return callback(null, result);
});
}
function getAll(callback) {
assert.strictEqual(typeof callback, 'function');
maildb.getAll(function (error, results) {
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
return callback(null, results);
});
}
function ensureDkimKey(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
var dkimPath = path.join(paths.MAIL_DATA_DIR, `dkim/${domain}`);
var dkimPrivateKeyFile = path.join(dkimPath, 'private');
var dkimPublicKeyFile = path.join(dkimPath, 'public');
var dkimSelectorFile = path.join(dkimPath, 'selector');
debug('Generating new DKIM keys');
if (!safe.fs.mkdirSync(dkimPath) && safe.error.code !== 'EEXIST') {
debug('Error creating dkim.', safe.error);
return new MailError(MailError.INTERNAL_ERROR, safe.error);
}
if (!safe.child_process.execSync('openssl genrsa -out ' + dkimPrivateKeyFile + ' 1024')) return new MailError(MailError.INTERNAL_ERROR, safe.error);
if (!safe.child_process.execSync('openssl rsa -in ' + dkimPrivateKeyFile + ' -out ' + dkimPublicKeyFile + ' -pubout -outform PEM')) return new MailError(MailError.INTERNAL_ERROR, safe.error);
if (!safe.fs.writeFileSync(dkimSelectorFile, config.dkimSelector(), 'utf8')) return new MailError(MailError.INTERNAL_ERROR, safe.error);
callback();
}
// 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.adminDomain(), '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 readDkimPublicKeySync(domain) {
assert.strictEqual(typeof domain, 'string');
var dkimPath = path.join(paths.MAIL_DATA_DIR, `dkim/${domain}`);
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;
}
function addDnsRecords(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
if (process.env.BOX_ENV === 'test') return callback();
var dkimKey = readDkimPublicKeySync(domain);
if (!dkimKey) return callback(new MailError(MailError.INTERNAL_ERROR, new Error('Failed to read dkim public key')));
// t=s limits the domainkey to this domain and not it's subdomains
var dkimRecord = { subdomain: config.dkimSelector() + '._domainkey', domain: domain, type: 'TXT', values: [ '"v=DKIM1; t=s; p=' + dkimKey + '"' ] };
var records = [ ];
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: domain, 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 add(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
ensureDkimKey(domain, function (error) {
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
maildb.add(domain, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new MailError(MailError.ALREADY_EXISTS, 'Domain already exists'));
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, 'No such domain'));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
addDnsRecords(domain, NOOP_CALLBACK); // add the required dns records asynchronously
callback();
});
});
}
function del(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
maildb.del(domain, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, error.message));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
callback();
});
}
function setMailFromValidation(domain, enabled, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof enabled, 'boolean');
assert.strictEqual(typeof callback, 'function');
maildb.update(domain, { mailFromValidation: enabled }, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
restartMail(NOOP_CALLBACK); // have to restart mail container since haraka cannot watch symlinked config files (mail.ini)
callback(null);
});
}
function setCatchAllAddress(domain, address, callback) {
assert.strictEqual(typeof domain, 'string');
assert(Array.isArray(address));
assert.strictEqual(typeof callback, 'function');
maildb.update(domain, { catchAll: address }, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
restartMail(NOOP_CALLBACK); // have to restart mail container since haraka cannot watch symlinked config files (mail.ini)
callback(null);
});
}
function setMailRelay(domain, relay, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof relay, 'object');
assert.strictEqual(typeof callback, 'function');
verifyRelay(relay, function (error) {
if (error) return callback(error);
maildb.update(domain, { relay: relay }, function (error) {
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
restartMail(NOOP_CALLBACK);
callback(null);
});
});
}
function setMailEnabled(domain, enabled, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof enabled, 'boolean');
assert.strictEqual(typeof callback, 'function');
maildb.update(domain, { enabled: enabled }, function (error) {
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
restartMail(NOOP_CALLBACK);
if (!enabled || process.env.BOX_ENV === 'test') return callback(null);
// Add MX and DMARC record. Note that DMARC policy depends on DKIM signing and thus works
// only if we use our internal mail server.
var records = [
{ subdomain: '_dmarc', type: 'TXT', values: [ '"v=DMARC1; p=reject; pct=100"' ] },
{ subdomain: '', type: 'MX', values: [ '10 ' + config.mailFqdn() + '.' ] }
];
async.mapSeries(records, function (record, iteratorCallback) {
domains.upsertDNSRecords(record.subdomain, domain, record.type, record.values, iteratorCallback);
}, NOOP_CALLBACK);
callback(null);
});
}
function sendTestMail(domain, to, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof to, 'string');
assert.strictEqual(typeof callback, 'function');
get(domain, function (error, result) {
if (error) return callback(error);
mailer.sendTestMail(result.domain, to);
callback();
});
}
function getMailboxes(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
mailboxdb.listMailboxes(domain, function (error, result) {
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
callback(null, result);
});
}
function getUserMailbox(domain, userId, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof callback, 'function');
user.get(userId, function (error, result) {
if (error && error.reason === UserError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, 'no such user'));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
mailboxdb.getMailbox(result.username, domain, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, 'no such mailbox'));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
callback(null, result);
});
});
}
function enableUserMailbox(domain, userId, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof callback, 'function');
user.get(userId, function (error, result) {
if (error && error.reason === UserError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, 'no such user'));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR));
mailboxdb.add(result.username, domain, userId, mailboxdb.TYPE_USER, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new MailError(MailError.ALREADY_EXISTS, 'mailbox already exists'));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
callback(null);
});
});
}
function disableUserMailbox(domain, userId, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof callback, 'function');
user.get(userId, function (error, result) {
if (error && error.reason === UserError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, 'no such user'));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
mailboxdb.del(result.username, domain, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, 'no such mailbox'));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
callback(null);
});
});
}
function getAliases(domain, userId, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof callback, 'function');
user.get(userId, function (error, result) {
if (error && error.reason === UserError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, 'no such user'));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
if (!result.username) return callback(null, []);
mailboxdb.getAliasesForName(result.username, domain, function (error, aliases) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, 'no such mailbox'));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
callback(null, aliases);
});
});
}
function setAliases(domain, userId, aliases, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof userId, 'string');
assert(Array.isArray(aliases));
assert.strictEqual(typeof callback, 'function');
for (var i = 0; i < aliases.length; i++) {
aliases[i] = aliases[i].toLowerCase();
var error = validateAlias(aliases[i]);
if (error) return callback(error);
}
user.get(userId, function (error, result) {
if (error && error.reason === UserError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, 'no such user'));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
mailboxdb.setAliasesForName(result.username, domain, aliases, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new MailError(MailError.ALREADY_EXISTS, error.message));
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, 'no such mailbox'));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
callback(null);
});
});
}
function getLists(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
mailboxdb.listGroups(domain, function (error, result) {
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
callback(null, result);
});
}
function getList(domain, groupId, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof groupId, 'string');
assert.strictEqual(typeof callback, 'function');
groups.get(groupId, function (error, result) {
if (error && error.reason === GroupError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, 'no such group'));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
mailboxdb.getGroup(result.name, domain, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, 'no such list'));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
callback(null, result);
});
});
}
function addList(domain, groupId, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof groupId, 'string');
assert.strictEqual(typeof callback, 'function');
groups.get(groupId, function (error, result) {
if (error && error.reason === GroupError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, 'no such group'));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
mailboxdb.add(result.name, domain, groupId, mailboxdb.TYPE_GROUP, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new MailError(MailError.ALREADY_EXISTS, 'list already exits'));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
callback();
});
});
}
function removeList(domain, groupId, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof groupId, 'string');
assert.strictEqual(typeof callback, 'function');
groups.get(groupId, function (error, result) {
if (error && error.reason === GroupError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, 'no such group'));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
mailboxdb.del(result.name, domain, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, 'no such list'));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
callback();
});
});
}
+2 -2
View File
@@ -1,8 +1,8 @@
<%if (format === 'text') { %>
Dear <%= cloudronName %> Admin,
Dear Cloudron Admin,
creating a backup has failed.
creating a backup of <%= fqdn %> has failed.
-------------------------------------
+2 -2
View File
@@ -2,7 +2,7 @@
Dear <%= cloudronName %> Admin,
Version <%= newBoxVersion %> is now available!
Version <%= newBoxVersion %> for Cloudron <%= fqdn %> is now available!
Changelog:
<% for (var i = 0; i < changelog.length; i++) { %>
@@ -27,7 +27,7 @@ Sent at: <%= new Date().toUTCString() %>
<div style="width: 650px; text-align: left;">
<p>
Version <b><%= newBoxVersion %></b> is now available!
Version <b><%= newBoxVersion %></b> for Cloudron <%= fqdn %> is now available!
</p>
<h5>Changelog:</h5>
+1 -1
View File
@@ -2,7 +2,7 @@
Dear <%= cloudronName %> Admin,
This is a summary of the activities on your Cloudron.
This is a summary of the activities on your Cloudron <%= fqdn %>.
<% if (info.usersAdded.length) { -%>
The following users were added:
+2 -2
View File
@@ -1,8 +1,8 @@
<%if (format === 'text') { %>
Dear <%= cloudronName %> Admin,
Dear Cloudron Admin,
<%= program %> exited unexpectedly using too much memory!
<%= program %> on <%= fqdn %> exited unexpectedly using too much memory!
The app has been restarted now. Should this message appear repeatedly or
undefined behavior is observed, give the app more memory.
+2 -2
View File
@@ -1,8 +1,8 @@
<%if (format === 'text') { %>
Dear <%= cloudronName %> Admin,
Dear Cloudron Admin,
your server is running out of disk space.
<%= fqdn %> is running out of disk space.
Disk space logs are attached.
+2 -2
View File
@@ -1,6 +1,6 @@
<%if (format === 'text') { %>
Hi <%= user.displayName || user.username || user.email %>,
Hi <%= user.displayName || user.username || user.alternateEmail || user.email %>,
Someone, hopefully you, has requested your account's password
be reset. If you did not request this reset, please ignore this message.
@@ -18,7 +18,7 @@ Powered by https://cloudron.io
<img src="<%= cloudronAvatarUrl %>" width="128px" height="128px"/>
<h3>Hi <%= user.displayName || user.username || user.email %>,</h3>
<h3>Hi <%= user.displayName || user.username || user.alternateEmail || user.email %>,</h3>
<p>
Someone, hopefully you, has requested your account's password be reset.<br/>
+1 -1
View File
@@ -1,6 +1,6 @@
<%if (format === 'text') { %>
Test email from <%= cloudronName %>,
Test email from <%= fqdn %>,
If you can read this, your Cloudron email settings are good.
+1
View File
@@ -1,6 +1,7 @@
{
"format": "html",
"webadminUrl": "https://my.cloudron.io",
"fqdn": "my.cloudron.io",
"cloudronName": "Smartserver",
"cloudronAvatarUrl": "https://cloudron.io/img/logo.png",
"info": {
+2 -2
View File
@@ -1,8 +1,8 @@
<%if (format === 'text') { %>
Dear <%= cloudronName %> Admin,
Dear Cloudron Admin,
Unfortunately <%= program %> exited unexpectedly!
Unfortunately <%= program %> on <%= fqdn %> exited unexpectedly!
Please see some excerpt of the logs below:
+2 -2
View File
@@ -2,7 +2,7 @@
Dear <%= cloudronName %> Admin,
A new user with email <%= user.email %> was added to <%= cloudronName %>.
A new user with email <%= user.alternateEmail || user.email %> was added to <%= cloudronName %>.
<% if (inviteLink) { %>
As requested, this user has not been sent an invitation email.
@@ -24,7 +24,7 @@ Powered by https://cloudron.io
<h3>Dear <%= cloudronName %> Admin,</h3>
<p>
A new user with email <%= user.email %> was added to <%= cloudronName %>.
A new user with email <%= user.alternateEmail || user.email %> was added to <%= cloudronName %>.
</p>
<% if (inviteLink) { %>
+1 -1
View File
@@ -2,7 +2,7 @@
Dear Cloudron Admin,
User <%= user.username || user.email %> <%= event %>.
User <%= user.username || user.alternateEmail || user.email %> <%= event %>.
Powered by https://cloudron.io
+2 -2
View File
@@ -1,6 +1,6 @@
<%if (format === 'text') { %>
Dear <%= user.displayName || user.username || user.email %>,
Dear <%= user.displayName || user.username || user.alternateEmail || user.email %>,
Welcome to <%= cloudronName %>!
@@ -20,7 +20,7 @@ Powered by https://cloudron.io
<img src="<%= cloudronAvatarUrl %>" width="128px" height="128px"/>
<h3>Hi <%= user.displayName || user.username || user.email %>,</h3>
<h3>Hi <%= user.displayName || user.username || user.alternateEmail || user.email %>,</h3>
<h2>Welcome to <%= cloudronName %>!</h2>
+1 -14
View File
@@ -6,7 +6,6 @@ exports = module.exports = {
listAliases: listAliases,
listMailboxes: listMailboxes,
listGroups: listGroups,
getMailbox: getMailbox,
getGroup: getGroup,
@@ -127,18 +126,6 @@ function listMailboxes(domain, callback) {
});
}
function listGroups(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE domain = ? AND ownerType = ? AND aliasTarget IS NULL', [ domain, exports.TYPE_GROUP ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
callback(null, results);
});
}
function getGroup(name, domain, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
@@ -191,7 +178,7 @@ function setAliasesForName(name, domain, aliases, callback) {
queries.push({ query: 'DELETE FROM mailboxes WHERE aliasTarget = ? AND domain = ?', args: [ name, domain ] });
aliases.forEach(function (alias) {
queries.push({ query: 'INSERT INTO mailboxes (name, domain, aliasTarget, ownerId, ownerType) VALUES (?, ?, ?, ?, ?)',
args: [ alias, domain, name, results[0].ownerId, results[0].ownerType ] });
args: [ alias, domain, name, results[0].ownerId, results[0].ownerType ] });
});
database.transaction(queries, function (error) {
-122
View File
@@ -1,122 +0,0 @@
'use strict';
exports = module.exports = {
add: add,
del: del,
get: get,
getAll: getAll,
update: update,
_clear: clear,
TYPE_USER: 'user',
TYPE_APP: 'app',
TYPE_GROUP: 'group'
};
var assert = require('assert'),
database = require('./database.js'),
DatabaseError = require('./databaseerror.js'),
safe = require('safetydance');
var MAILDB_FIELDS = [ 'domain', 'enabled', 'mailFromValidation', 'catchAllJson', 'relayJson' ].join(',');
function postProcess(data) {
data.enabled = !!data.enabled; // int to boolean
data.mailFromValidation = !!data.mailFromValidation; // int to boolean
data.catchAll = safe.JSON.parse(data.catchAllJson) || [ ];
delete data.catchAllJson;
data.relay = safe.JSON.parse(data.relayJson) || { provider: 'cloudron-smtp' };
delete data.relayJson;
return data;
}
function add(domain, callback) {
assert.strictEqual(typeof domain, 'string');
database.query('INSERT INTO mail (domain) VALUES (?)', [ domain ], function (error) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, 'mail domain already exists'));
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND), 'no such domain');
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null);
});
}
function clear(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('TRUNCATE TABLE mail', [], function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null);
});
}
function del(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
// deletes aliases as well
database.query('DELETE FROM mail WHERE domain=?', [ domain ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
callback(null);
});
}
function get(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + MAILDB_FIELDS + ' FROM mail WHERE domain = ?', [ domain ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
callback(null, postProcess(results[0]));
});
}
function getAll(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + MAILDB_FIELDS + ' FROM mail ORDER BY domain', function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
results.forEach(function (result) { postProcess(result); });
callback(null, results);
});
}
function update(domain, data, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof callback, 'function');
var args = [ ];
var fields = [ ];
for (var k in data) {
if (k === 'catchAll') {
fields.push('catchAllJson = ?');
args.push(JSON.stringify(data[k]));
} else if (k === 'relay') {
fields.push('relayJson = ?');
args.push(JSON.stringify(data[k]));
} else {
fields.push(k + ' = ?');
args.push(data[k]);
}
}
args.push(domain);
database.query('UPDATE mail SET ' + fields.join(', ') + ' WHERE domain=?', args, function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
callback(null);
});
}
+196 -192
View File
@@ -32,7 +32,6 @@ var assert = require('assert'),
debug = require('debug')('box:mailer'),
docker = require('./docker.js').connection,
ejs = require('ejs'),
mail = require('./mail.js'),
nodemailer = require('nodemailer'),
path = require('path'),
safe = require('safetydance'),
@@ -60,38 +59,12 @@ function splatchError(error) {
return util.inspect(result, { depth: null, showHidden: true });
}
// This will collect the most common details required for notification emails
function getMailConfig(callback) {
assert.strictEqual(typeof callback, 'function');
getAdminEmails(function (error, adminEmails) {
if (error) return callback(error);
settings.getCloudronName(function (error, cloudronName) {
// this is not fatal
if (error) {
debug(error);
cloudronName = 'Cloudron';
}
mail.getAll(function (error, domains) {
if (error) return callback(error);
if (domains.length === 0) return callback('No domains configured');
const defaultDomain = domains[0];
callback(null, {
adminEmails: adminEmails,
cloudronName: cloudronName,
notificationDomain: defaultDomain.domain,
notificationFrom: `"${cloudronName}" <no-reply@${defaultDomain.domain}>`
});
});
});
});
function mailConfig() {
return {
from: '"Cloudron" <no-reply@' + config.fqdn() + '>'
};
}
function processQueue() {
sendMails(gMailQueue);
gMailQueue = [ ];
@@ -164,7 +137,7 @@ function getAdminEmails(callback) {
if (admins.length === 0) return callback(new Error('No admins on this cloudron')); // box not activated yet
var adminEmails = [ ];
adminEmails.push(admins[0].fallbackEmail);
if (admins[0].alternateEmail) adminEmails.push(admins[0].alternateEmail);
admins.forEach(function (admin) { adminEmails.push(admin.email); });
callback(null, adminEmails);
@@ -175,16 +148,16 @@ function mailUserEventToAdmins(user, event) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof event, 'string');
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
getAdminEmails(function (error, adminEmails) {
if (error) return debug('Error getting admins', error);
var adminEmails = _.difference(mailConfig.adminEmails, [ user.email ]);
adminEmails = _.difference(adminEmails, [ user.email ]);
var mailOptions = {
from: mailConfig.notificationFrom,
from: mailConfig().from,
to: adminEmails.join(', '),
subject: util.format('[%s] %s %s', mailConfig.cloudronName, user.username || user.fallbackEmail || user.email, event),
text: render('user_event.ejs', { user: user, event: event, format: 'text' }),
subject: util.format('[%s] %s %s', config.fqdn(), user.username || user.alternateEmail || user.email, event),
text: render('user_event.ejs', { fqdn: config.fqdn(), user: user, event: event, format: 'text' }),
};
enqueue(mailOptions);
@@ -197,15 +170,19 @@ function sendInvite(user, invitor) {
debug('Sending invite mail');
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
settings.getCloudronName(function (error, cloudronName) {
if (error) {
debug(error);
cloudronName = 'Cloudron';
}
var templateData = {
user: user,
webadminUrl: config.adminOrigin(),
setupLink: config.adminOrigin() + '/api/v1/session/account/setup.html?reset_token=' + user.resetToken,
fqdn: config.fqdn(),
invitor: invitor,
cloudronName: mailConfig.cloudronName,
cloudronName: cloudronName,
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
};
@@ -216,9 +193,9 @@ function sendInvite(user, invitor) {
templateDataHTML.format = 'html';
var mailOptions = {
from: mailConfig.notificationFrom,
to: user.fallbackEmail,
subject: util.format('Welcome to %s', mailConfig.cloudronName),
from: mailConfig().from,
to: user.alternateEmail || user.email,
subject: util.format('Welcome to %s', cloudronName),
text: render('welcome_user.ejs', templateDataText),
html: render('welcome_user.ejs', templateDataHTML)
};
@@ -233,33 +210,41 @@ function userAdded(user, inviteSent) {
debug('Sending mail for userAdded %s including invite link', inviteSent ? 'not' : '');
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
getAdminEmails(function (error, adminEmails) {
if (error) return debug('Error getting admins', error);
var adminEmails = _.difference(mailConfig.adminEmails, [ user.email ]);
adminEmails = _.difference(adminEmails, [ user.email ]);
var templateData = {
user: user,
inviteLink: inviteSent ? null : config.adminOrigin() + '/api/v1/session/account/setup.html?reset_token=' + user.resetToken,
cloudronName: mailConfig.cloudronName,
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
};
settings.getCloudronName(function (error, cloudronName) {
if (error) {
debug(error);
cloudronName = 'Cloudron';
}
var templateDataText = JSON.parse(JSON.stringify(templateData));
templateDataText.format = 'text';
var templateData = {
fqdn: config.fqdn(),
user: user,
inviteLink: inviteSent ? null : config.adminOrigin() + '/api/v1/session/account/setup.html?reset_token=' + user.resetToken,
cloudronName: cloudronName,
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
};
var templateDataHTML = JSON.parse(JSON.stringify(templateData));
templateDataHTML.format = 'html';
var templateDataText = JSON.parse(JSON.stringify(templateData));
templateDataText.format = 'text';
var mailOptions = {
from: mailConfig.notificationFrom,
to: adminEmails.join(', '),
subject: util.format('[%s] User %s added', mailConfig.cloudronName, user.fallbackEmail),
text: render('user_added.ejs', templateDataText),
html: render('user_added.ejs', templateDataHTML)
};
var templateDataHTML = JSON.parse(JSON.stringify(templateData));
templateDataHTML.format = 'html';
enqueue(mailOptions);
var mailOptions = {
from: mailConfig().from,
to: adminEmails.join(', '),
subject: util.format('[%s] User %s added', config.fqdn(), user.alternateEmail || user.email),
text: render('user_added.ejs', templateDataText),
html: render('user_added.ejs', templateDataHTML)
};
enqueue(mailOptions);
});
});
}
@@ -285,13 +270,17 @@ function passwordReset(user) {
debug('Sending mail for password reset for user %s.', user.email, user.id);
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
settings.getCloudronName(function (error, cloudronName) {
if (error) {
debug(error);
cloudronName = 'Cloudron';
}
var templateData = {
fqdn: config.fqdn(),
user: user,
resetLink: config.adminOrigin() + '/api/v1/session/password/reset.html?reset_token=' + user.resetToken,
cloudronName: mailConfig.cloudronName,
cloudronName: cloudronName,
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
};
@@ -302,9 +291,9 @@ function passwordReset(user) {
templateDataHTML.format = 'html';
var mailOptions = {
from: mailConfig.notificationFrom,
to: user.fallbackEmail,
subject: util.format('[%s] Password Reset', mailConfig.cloudronName),
from: mailConfig().from,
to: user.alternateEmail || user.email,
subject: util.format('[%s] Password Reset', config.fqdn()),
text: render('password_reset.ejs', templateDataText),
html: render('password_reset.ejs', templateDataHTML)
};
@@ -318,14 +307,14 @@ function appDied(app) {
debug('Sending mail for app %s @ %s died', app.id, app.fqdn);
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
getAdminEmails(function (error, adminEmails) {
if (error) return debug('Error getting admins', error);
var mailOptions = {
from: mailConfig.notificationFrom,
to: config.provider() === 'caas' ? 'support@cloudron.io' : mailConfig.adminEmails.join(', '),
subject: util.format('[%s] App %s is down', mailConfig.cloudronName, app.fqdn),
text: render('app_down.ejs', { title: app.manifest.title, appFqdn: app.fqdn, format: 'text' })
from: mailConfig().from,
to: config.provider() === 'caas' ? 'support@cloudron.io' : adminEmails.join(', '),
subject: util.format('[%s] App %s is down', config.fqdn(), app.fqdn),
text: render('app_down.ejs', { fqdn: config.fqdn(), title: app.manifest.title, appFqdn: app.fqdn, format: 'text' })
};
enqueue(mailOptions);
@@ -337,36 +326,44 @@ function boxUpdateAvailable(hasSubscription, newBoxVersion, changelog) {
assert.strictEqual(typeof newBoxVersion, 'string');
assert(util.isArray(changelog));
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
getAdminEmails(function (error, adminEmails) {
if (error) return debug('Error getting admins', error);
var converter = new showdown.Converter();
settings.getCloudronName(function (error, cloudronName) {
if (error) {
debug(error);
cloudronName = 'Cloudron';
}
var templateData = {
webadminUrl: config.adminOrigin(),
newBoxVersion: newBoxVersion,
hasSubscription: hasSubscription,
changelog: changelog,
changelogHTML: changelog.map(function (e) { return converter.makeHtml(e); }),
cloudronName: mailConfig.cloudronName,
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
};
var converter = new showdown.Converter();
var templateDataText = JSON.parse(JSON.stringify(templateData));
templateDataText.format = 'text';
var templateData = {
fqdn: config.fqdn(),
webadminUrl: config.adminOrigin(),
newBoxVersion: newBoxVersion,
hasSubscription: hasSubscription,
changelog: changelog,
changelogHTML: changelog.map(function (e) { return converter.makeHtml(e); }),
cloudronName: cloudronName,
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
};
var templateDataHTML = JSON.parse(JSON.stringify(templateData));
templateDataHTML.format = 'html';
var templateDataText = JSON.parse(JSON.stringify(templateData));
templateDataText.format = 'text';
var mailOptions = {
from: mailConfig.notificationFrom,
to: mailConfig.adminEmails.join(', '),
subject: util.format('%s has a new update available', mailConfig.cloudronName),
text: render('box_update_available.ejs', templateDataText),
html: render('box_update_available.ejs', templateDataHTML)
};
var templateDataHTML = JSON.parse(JSON.stringify(templateData));
templateDataHTML.format = 'html';
enqueue(mailOptions);
var mailOptions = {
from: mailConfig().from,
to: adminEmails.join(', '),
subject: util.format('%s has a new update available', config.fqdn()),
text: render('box_update_available.ejs', templateDataText),
html: render('box_update_available.ejs', templateDataHTML)
};
enqueue(mailOptions);
});
});
}
@@ -375,81 +372,97 @@ function appUpdateAvailable(app, hasSubscription, info) {
assert.strictEqual(typeof hasSubscription, 'boolean');
assert.strictEqual(typeof info, 'object');
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
getAdminEmails(function (error, adminEmails) {
if (error) return debug('Error getting admins', error);
var converter = new showdown.Converter();
settings.getCloudronName(function (error, cloudronName) {
if (error) {
debug(error);
cloudronName = 'Cloudron';
}
var templateData = {
webadminUrl: config.adminOrigin(),
hasSubscription: hasSubscription,
app: app,
updateInfo: info,
changelogHTML: converter.makeHtml(info.manifest.changelog),
cloudronName: mailConfig.cloudronName,
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
};
var converter = new showdown.Converter();
var templateDataText = JSON.parse(JSON.stringify(templateData));
templateDataText.format = 'text';
var templateData = {
fqdn: config.fqdn(),
webadminUrl: config.adminOrigin(),
hasSubscription: hasSubscription,
app: app,
updateInfo: info,
changelogHTML: converter.makeHtml(info.manifest.changelog),
cloudronName: cloudronName,
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
};
var templateDataHTML = JSON.parse(JSON.stringify(templateData));
templateDataHTML.format = 'html';
var templateDataText = JSON.parse(JSON.stringify(templateData));
templateDataText.format = 'text';
var mailOptions = {
from: mailConfig.notificationFrom,
to: mailConfig.adminEmails.join(', '),
subject: util.format('App %s has a new update available', app.fqdn),
text: render('app_update_available.ejs', templateDataText),
html: render('app_update_available.ejs', templateDataHTML)
};
var templateDataHTML = JSON.parse(JSON.stringify(templateData));
templateDataHTML.format = 'html';
enqueue(mailOptions);
var mailOptions = {
from: mailConfig().from,
to: adminEmails.join(', '),
subject: util.format('App %s has a new update available', app.fqdn),
text: render('app_update_available.ejs', templateDataText),
html: render('app_update_available.ejs', templateDataHTML)
};
enqueue(mailOptions);
});
});
}
function sendDigest(info) {
assert.strictEqual(typeof info, 'object');
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
getAdminEmails(function (error, adminEmails) {
if (error) return debug('Error getting admins', error);
var templateData = {
webadminUrl: config.adminOrigin(),
cloudronName: mailConfig.cloudronName,
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar',
info: info
};
settings.getCloudronName(function (error, cloudronName) {
if (error) {
debug(error);
cloudronName = 'Cloudron';
}
var templateDataText = JSON.parse(JSON.stringify(templateData));
templateDataText.format = 'text';
var templateData = {
fqdn: config.fqdn(),
webadminUrl: config.adminOrigin(),
cloudronName: cloudronName,
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar',
info: info
};
var templateDataHTML = JSON.parse(JSON.stringify(templateData));
templateDataHTML.format = 'html';
var templateDataText = JSON.parse(JSON.stringify(templateData));
templateDataText.format = 'text';
var mailOptions = {
from: mailConfig.notificationFrom,
to: mailConfig.adminEmails.join(', '),
subject: util.format('[%s] Cloudron - Weekly activity digest', mailConfig.cloudronName),
text: render('digest.ejs', templateDataText),
html: render('digest.ejs', templateDataHTML)
};
var templateDataHTML = JSON.parse(JSON.stringify(templateData));
templateDataHTML.format = 'html';
enqueue(mailOptions);
var mailOptions = {
from: mailConfig().from,
to: adminEmails.join(', '),
subject: util.format('[%s] Cloudron - Weekly activity digest', config.fqdn()),
text: render('digest.ejs', templateDataText),
html: render('digest.ejs', templateDataHTML)
};
enqueue(mailOptions);
});
});
}
function outOfDiskSpace(message) {
assert.strictEqual(typeof message, 'string');
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
getAdminEmails(function (error, adminEmails) {
if (error) return debug('Error getting admins', error);
var mailOptions = {
from: mailConfig.notificationFrom,
to: config.provider() === 'caas' ? 'support@cloudron.io' : mailConfig.adminEmails.join(', '),
subject: util.format('[%s] Out of disk space alert', mailConfig.cloudronName),
text: render('out_of_disk_space.ejs', { cloudronName: mailConfig.cloudronName, message: message, format: 'text' })
from: mailConfig().from,
to: config.provider() === 'caas' ? 'support@cloudron.io' : adminEmails.join(', '),
subject: util.format('[%s] Out of disk space alert', config.fqdn()),
text: render('out_of_disk_space.ejs', { fqdn: config.fqdn(), message: message, format: 'text' })
};
sendMails([ mailOptions ]);
@@ -459,14 +472,14 @@ function outOfDiskSpace(message) {
function backupFailed(error) {
var message = splatchError(error);
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
getAdminEmails(function (error, adminEmails) {
if (error) return debug('Error getting admins', error);
var mailOptions = {
from: mailConfig.notificationFrom,
to: config.provider() === 'caas' ? 'support@cloudron.io' : mailConfig.adminEmails.join(', '),
subject: util.format('[%s] Failed to backup', mailConfig.cloudronName),
text: render('backup_failed.ejs', { cloudronName: mailConfig.cloudronName, message: message, format: 'text' })
from: mailConfig().from,
to: config.provider() === 'caas' ? 'support@cloudron.io' : adminEmails.join(', '),
subject: util.format('[%s] Failed to backup', config.fqdn()),
text: render('backup_failed.ejs', { fqdn: config.fqdn(), message: message, format: 'text' })
};
enqueue(mailOptions);
@@ -477,12 +490,12 @@ function certificateRenewalError(domain, message) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof message, 'string');
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
getAdminEmails(function (error, adminEmails) {
if (error) return debug('Error getting admins', error);
var mailOptions = {
from: mailConfig.notificationFrom,
to: config.provider() === 'caas' ? 'support@cloudron.io' : mailConfig.adminEmails.join(', '),
from: mailConfig().from,
to: config.provider() === 'caas' ? 'support@cloudron.io' : adminEmails.join(', '),
subject: util.format('[%s] Certificate renewal error', domain),
text: render('certificate_renewal_error.ejs', { domain: domain, message: message, format: 'text' })
};
@@ -495,14 +508,14 @@ function oomEvent(program, context) {
assert.strictEqual(typeof program, 'string');
assert.strictEqual(typeof context, 'string');
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
getAdminEmails(function (error, adminEmails) {
if (error) return debug('Error getting admins', error);
var mailOptions = {
from: mailConfig.notificationFrom,
to: config.provider() === 'caas' ? 'support@cloudron.io' : mailConfig.adminEmails.join(', '),
subject: util.format('[%s] %s exited unexpectedly', mailConfig.cloudronName, program),
text: render('oom_event.ejs', { cloudronName: mailConfig.cloudronName, program: program, context: context, format: 'text' })
from: mailConfig().from,
to: config.provider() === 'caas' ? 'support@cloudron.io' : adminEmails.join(', '),
subject: util.format('[%s] %s exited unexpectedly', config.fqdn(), program),
text: render('oom_event.ejs', { fqdn: config.fqdn(), program: program, context: context, format: 'text' })
};
sendMails([ mailOptions ]);
@@ -510,7 +523,7 @@ function oomEvent(program, context) {
}
// this function bypasses the queue intentionally. it is also expected to work without the mailer module initialized
// NOTE: crashnotifier should ideally be able to send mail when there is no db, however we need the 'from' address domain from the db
// NOTE: crashnotifier should be able to send mail when there is no db
function unexpectedExit(program, context, callback) {
assert.strictEqual(typeof program, 'string');
assert.strictEqual(typeof context, 'string');
@@ -518,36 +531,27 @@ function unexpectedExit(program, context, callback) {
if (config.provider() !== 'caas') return callback(); // no way to get admins without db access
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
var mailOptions = {
from: mailConfig().from,
to: 'support@cloudron.io',
subject: util.format('[%s] %s exited unexpectedly', config.fqdn(), program),
text: render('unexpected_exit.ejs', { fqdn: config.fqdn(), program: program, context: context, format: 'text' })
};
var mailOptions = {
from: mailConfig.notificationFrom,
to: 'support@cloudron.io',
subject: util.format('[%s] %s exited unexpectedly', mailConfig.cloudronName, program),
text: render('unexpected_exit.ejs', { cloudronName: mailConfig.cloudronName, program: program, context: context, format: 'text' })
};
sendMails([ mailOptions ], callback);
});
sendMails([ mailOptions ], callback);
}
function sendTestMail(domain, email) {
assert.strictEqual(typeof domain, 'string');
function sendTestMail(email) {
assert.strictEqual(typeof email, 'string');
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
var mailOptions = {
from: mailConfig().from,
to: email,
subject: util.format('Test Email from %s', config.fqdn()),
text: render('test.ejs', { fqdn: config.fqdn(), format: 'text'})
};
var mailOptions = {
from: `"${mailConfig.cloudronName}" <no-reply@${domain}>`,
to: email,
subject: util.format('Test Email from %s', mailConfig.cloudronName),
text: render('test.ejs', { cloudronName: mailConfig.cloudronName, format: 'text'})
};
enqueue(mailOptions);
});
enqueue(mailOptions);
}
function _getMailQueue() {
+108
View File
@@ -0,0 +1,108 @@
'use strict';
var assert = require('assert'),
config = require('./config.js'),
debug = require('debug')('box:nginx'),
ejs = require('ejs'),
fs = require('fs'),
path = require('path'),
paths = require('./paths.js'),
safe = require('safetydance'),
shell = require('./shell.js');
exports = module.exports = {
configureAdmin: configureAdmin,
configureApp: configureApp,
unconfigureApp: unconfigureApp,
reload: reload,
removeAppConfigs: removeAppConfigs
};
var NGINX_APPCONFIG_EJS = fs.readFileSync(__dirname + '/../setup/start/nginx/appconfig.ejs', { encoding: 'utf8' }),
RELOAD_NGINX_CMD = path.join(__dirname, 'scripts/reloadnginx.sh');
function configureAdmin(certFilePath, keyFilePath, configFileName, vhost, callback) {
assert.strictEqual(typeof certFilePath, 'string');
assert.strictEqual(typeof keyFilePath, 'string');
assert.strictEqual(typeof configFileName, 'string');
assert.strictEqual(typeof vhost, 'string');
assert.strictEqual(typeof callback, 'function');
var data = {
sourceDir: path.resolve(__dirname, '..'),
adminOrigin: config.adminOrigin(),
vhost: vhost, // if vhost is empty it will become the default_server
hasIPv6: config.hasIPv6(),
endpoint: 'admin',
certFilePath: certFilePath,
keyFilePath: keyFilePath,
xFrameOptions: 'SAMEORIGIN',
robotsTxtQuoted: JSON.stringify('User-agent: *\nDisallow: /\n')
};
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, configFileName);
if (!safe.fs.writeFileSync(nginxConfigFilename, nginxConf)) return callback(safe.error);
reload(callback);
}
function configureApp(app, certFilePath, keyFilePath, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof certFilePath, 'string');
assert.strictEqual(typeof keyFilePath, 'string');
assert.strictEqual(typeof callback, 'function');
var sourceDir = path.resolve(__dirname, '..');
var endpoint = 'app';
var vhost = app.altDomain || app.intrinsicFqdn;
var data = {
sourceDir: sourceDir,
adminOrigin: config.adminOrigin(),
vhost: vhost,
hasIPv6: config.hasIPv6(),
port: app.httpPort,
endpoint: endpoint,
certFilePath: certFilePath,
keyFilePath: keyFilePath,
robotsTxtQuoted: app.robotsTxt ? JSON.stringify(app.robotsTxt) : null,
xFrameOptions: app.xFrameOptions || 'SAMEORIGIN' // once all apps have been updated/
};
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, app.id + '.conf');
debug('writing config for "%s" to %s with options %j', vhost, nginxConfigFilename, data);
if (!safe.fs.writeFileSync(nginxConfigFilename, nginxConf)) {
debug('Error creating nginx config for "%s" : %s', vhost, safe.error.message);
return callback(safe.error);
}
reload(callback);
}
function unconfigureApp(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
var vhost = app.altDomain || app.intrinsicFqdn;
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, app.id + '.conf');
if (!safe.fs.unlinkSync(nginxConfigFilename)) {
if (safe.error.code !== 'ENOENT') debug('Error removing nginx configuration of "%s": %s', vhost, safe.error.message);
return callback(null);
}
reload(callback);
}
function reload(callback) {
shell.sudo('reload', [ RELOAD_NGINX_CMD ], callback);
}
function removeAppConfigs() {
for (var appConfigFile of fs.readdirSync(paths.NGINX_APPCONFIG_DIR)) {
fs.unlinkSync(path.join(paths.NGINX_APPCONFIG_DIR, appConfigFile));
}
}
+120 -14
View File
@@ -4,26 +4,29 @@ exports = module.exports = {
start: start,
stop: stop,
handleCertChanged: handleCertChanged
createMailConfig: createMailConfig
};
var apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
config = require('./config.js'),
certificates = require('./certificates.js'),
debug = require('debug')('box:platform'),
domains = require('./domains.js'),
fs = require('fs'),
hat = require('hat'),
infra = require('./infra_version.js'),
locker = require('./locker.js'),
mail = require('./mail.js'),
nginx = require('./nginx.js'),
os = require('os'),
paths = require('./paths.js'),
reverseProxy = require('./reverseproxy.js'),
safe = require('safetydance'),
semver = require('semver'),
settings = require('./settings.js'),
shell = require('./shell.js'),
taskmanager = require('./taskmanager.js'),
user = require('./user.js'),
util = require('util'),
_ = require('underscore');
@@ -38,6 +41,14 @@ function start(callback) {
debug('initializing addon infrastructure');
// restart mail container if any of these keys change
settings.events.on(settings.MAIL_CONFIG_KEY, function () { startMail(NOOP_CALLBACK); });
settings.events.on(settings.MAIL_RELAY_KEY, function () { startMail(NOOP_CALLBACK); });
certificates.events.on(certificates.EVENT_CERT_CHANGED, function (domain) {
if (domain === '*.' + config.fqdn() || domain === config.adminFqdn()) startMail(NOOP_CALLBACK);
});
var existingInfra = { version: 'none' };
if (fs.existsSync(paths.INFRA_VERSION_FILE)) {
existingInfra = safe.JSON.parse(fs.readFileSync(paths.INFRA_VERSION_FILE, 'utf8'));
@@ -229,13 +240,116 @@ function startMongodb(callback) {
setTimeout(callback, 5000);
}
function createMailConfig(callback) {
assert.strictEqual(typeof callback, 'function');
const fqdn = config.fqdn();
const mailFqdn = config.mailFqdn();
const alertsFrom = 'no-reply@' + config.fqdn();
debug('createMailConfig: generating mail config');
user.getOwner(function (error, owner) {
var alertsTo = config.provider() === 'caas' ? [ 'support@cloudron.io' ] : [ ];
alertsTo.concat(error ? [] : owner.email).join(','); // owner may not exist yet
settings.getAll(function (error, result) {
if (error) return callback(error);
var catchAll = result[settings.CATCH_ALL_ADDRESS_KEY].join(',');
var mailFromValidation = result[settings.MAIL_FROM_VALIDATION_KEY];
if (!safe.fs.writeFileSync(paths.ADDON_CONFIG_DIR + '/mail/mail.ini',
`mail_domain=${fqdn}\nmail_server_name=${mailFqdn}\nalerts_from=${alertsFrom}\nalerts_to=${alertsTo}\ncatch_all=${catchAll}\nmail_from_validation=${mailFromValidation}\ndkim_selector=${config.dkimSelector()}\n`, 'utf8')) {
return callback(new Error('Could not create mail var file:' + safe.error.message));
}
var relay = result[settings.MAIL_RELAY_KEY];
const enabled = relay.provider !== 'cloudron-smtp' ? true : false,
host = relay.host || '',
port = relay.port || 25,
username = relay.username || '',
password = relay.password || '';
if (!safe.fs.writeFileSync(paths.ADDON_CONFIG_DIR + '/mail/smtp_forward.ini',
`enable_outbound=${enabled}\nhost=${host}\nport=${port}\nenable_tls=true\nauth_type=plain\nauth_user=${username}\nauth_pass=${password}`, 'utf8')) {
return callback(new Error('Could not create mail var file:' + safe.error.message));
}
callback();
});
});
}
function startMail(callback) {
// mail (note: 2525 is hardcoded in mail container and app use this port)
// MAIL_SERVER_NAME is the hostname of the mailserver i.e server uses these certs
// MAIL_DOMAIN is the domain for which this server is relaying mails
// mail container uses /app/data for backed up data and /run for restart-able data
const tag = infra.images.mail.tag;
const memoryLimit = Math.max((1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 128, 256);
// admin and mail share the same certificate
certificates.getAdminCertificate(function (error, cert, key) {
if (error) return callback(error);
// the setup script copies dhparams.pem to /addons/mail
if (!safe.fs.writeFileSync(paths.ADDON_CONFIG_DIR + '/mail/tls_cert.pem', cert)) return callback(new Error('Could not create cert file:' + safe.error.message));
if (!safe.fs.writeFileSync(paths.ADDON_CONFIG_DIR + '/mail/tls_key.pem', key)) return callback(new Error('Could not create key file:' + safe.error.message));
settings.getMailConfig(function (error, mailConfig) {
if (error) return callback(error);
shell.execSync('startMail', 'docker rm -f mail || true');
createMailConfig(function (error) {
if (error) return callback(error);
var ports = mailConfig.enabled ? '-p 587:2525 -p 993:9993 -p 4190:4190 -p 25:2525' : '';
const cmd = `docker run --restart=always -d --name="mail" \
--net cloudron \
--net-alias mail \
-m ${memoryLimit}m \
--memory-swap ${memoryLimit * 2}m \
--dns 172.18.0.1 \
--dns-search=. \
--env ENABLE_MDA=${mailConfig.enabled} \
-v "${paths.MAIL_DATA_DIR}:/app/data" \
-v "${paths.PLATFORM_DATA_DIR}/addons/mail:/etc/mail" \
${ports} \
--read-only -v /run -v /tmp ${tag}`;
shell.execSync('startMail', cmd);
if (!mailConfig.enabled || process.env.BOX_ENV === 'test') return callback();
// Add MX and DMARC record. Note that DMARC policy depends on DKIM signing and thus works
// only if we use our internal mail server.
var records = [
{ subdomain: '_dmarc', type: 'TXT', values: [ '"v=DMARC1; p=reject; pct=100"' ] },
{ subdomain: '', type: 'MX', values: [ '10 ' + config.mailFqdn() + '.' ] }
];
async.mapSeries(records, function (record, iteratorCallback) {
domains.upsertDNSRecords(record.subdomain, config.fqdn(), record.type, record.values, iteratorCallback);
}, NOOP_CALLBACK); // do not crash if DNS creds do not work in startup sequence
callback();
});
});
});
}
function startAddons(existingInfra, callback) {
var startFuncs = [ ];
// always start addons on any infra change, regardless of minor or major update
if (existingInfra.version !== infra.version) {
debug('startAddons: no existing infra or infra upgrade. starting all addons');
startFuncs.push(startGraphite, startMysql, startPostgresql, startMongodb, mail.startMail);
startFuncs.push(startGraphite, startMysql, startPostgresql, startMongodb, startMail);
} else {
assert.strictEqual(typeof existingInfra.images, 'object');
@@ -243,7 +357,7 @@ function startAddons(existingInfra, callback) {
if (infra.images.mysql.tag !== existingInfra.images.mysql.tag) startFuncs.push(startMysql);
if (infra.images.postgresql.tag !== existingInfra.images.postgresql.tag) startFuncs.push(startPostgresql);
if (infra.images.mongodb.tag !== existingInfra.images.mongodb.tag) startFuncs.push(startMongodb);
if (infra.images.mail.tag !== existingInfra.images.mail.tag) startFuncs.push(mail.startMail);
if (infra.images.mail.tag !== existingInfra.images.mail.tag) startFuncs.push(startMail);
debug('startAddons: existing infra. incremental addon create %j', startFuncs.map(function (f) { return f.name; }));
}
@@ -265,15 +379,7 @@ function startApps(existingInfra, callback) {
apps.restoreInstalledApps(callback);
} else {
debug('startApps: reconfiguring installed apps');
reverseProxy.removeAppConfigs(); // should we change the cert location, nginx will not start
nginx.removeAppConfigs(); // should we change the cert location, nginx will not start
apps.configureInstalledApps(callback);
}
}
function handleCertChanged(cn) {
assert.strictEqual(typeof cn, 'string');
if (cn === '*.' + config.adminDomain() || cn === config.adminFqdn()) {
mail.startMail(NOOP_CALLBACK);
}
}
-445
View File
@@ -1,445 +0,0 @@
'use strict';
exports = module.exports = {
ReverseProxyError: ReverseProxyError,
setFallbackCertificate: setFallbackCertificate,
getFallbackCertificate: getFallbackCertificate,
validateCertificate: validateCertificate,
getCertificate: getCertificate,
renewAll: renewAll,
configureDefaultServer: configureDefaultServer,
configureAdmin: configureAdmin,
configureApp: configureApp,
unconfigureApp: unconfigureApp,
reload: reload,
removeAppConfigs: removeAppConfigs,
// exported for testing
_getApi: getApi
};
var acme = require('./cert/acme.js'),
apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
caas = require('./cert/caas.js'),
config = require('./config.js'),
constants = require('./constants.js'),
debug = require('debug')('box:certificates'),
domains = require('./domains.js'),
ejs = require('ejs'),
eventlog = require('./eventlog.js'),
fallback = require('./cert/fallback.js'),
fs = require('fs'),
mailer = require('./mailer.js'),
path = require('path'),
paths = require('./paths.js'),
platform = require('./platform.js'),
safe = require('safetydance'),
shell = require('./shell.js'),
tld = require('tldjs'),
user = require('./user.js'),
util = require('util');
var NGINX_APPCONFIG_EJS = fs.readFileSync(__dirname + '/../setup/start/nginx/appconfig.ejs', { encoding: 'utf8' }),
RELOAD_NGINX_CMD = path.join(__dirname, 'scripts/reloadnginx.sh'),
NOOP_CALLBACK = function (error) { if (error) debug(error); };
function ReverseProxyError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
Error.call(this);
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.reason = reason;
if (typeof errorOrMessage === 'undefined') {
this.message = reason;
} else if (typeof errorOrMessage === 'string') {
this.message = errorOrMessage;
} else {
this.message = 'Internal error';
this.nestedError = errorOrMessage;
}
}
util.inherits(ReverseProxyError, Error);
ReverseProxyError.INTERNAL_ERROR = 'Internal Error';
ReverseProxyError.INVALID_CERT = 'Invalid certificate';
ReverseProxyError.NOT_FOUND = 'Not Found';
function getApi(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
domains.get(app.domain, function (error, domain) {
if (error) return callback(error);
if (domain.tlsConfig.provider === 'fallback') return callback(null, fallback, {});
// use acme if we have altDomain or the tlsConfig is not caas
var api = (app.altDomain || domain.tlsConfig.provider !== 'caas') ? acme : caas;
var options = { };
if (domain.tlsConfig.provider === 'caas') {
options.prod = true; // with altDomain, we will choose acme setting based on this
} else { // acme
options.prod = domain.tlsConfig.provider.match(/.*-prod/) !== null; // matches 'le-prod' or 'letsencrypt-prod'
}
// registering user with an email requires A or MX record (https://github.com/letsencrypt/boulder/issues/1197)
// we cannot use admin@fqdn because the user might not have set it up.
// we simply update the account with the latest email we have each time when getting letsencrypt certs
// https://github.com/ietf-wg-acme/acme/issues/30
user.getOwner(function (error, owner) {
options.email = error ? 'support@cloudron.io' : (owner.fallbackEmail || owner.email); // can error if not activated yet
callback(null, api, options);
});
});
}
function isExpiringSync(certFilePath, hours) {
assert.strictEqual(typeof certFilePath, 'string');
assert.strictEqual(typeof hours, 'number');
if (!fs.existsSync(certFilePath)) return 2; // not found
var result = safe.child_process.spawnSync('/usr/bin/openssl', [ 'x509', '-checkend', String(60 * 60 * hours), '-in', certFilePath ]);
debug('isExpiringSync: %s %s %s', certFilePath, result.stdout.toString('utf8').trim(), result.status);
return result.status === 1; // 1 - expired 0 - not expired
}
// note: https://tools.ietf.org/html/rfc4346#section-7.4.2 (certificate_list) requires that the
// servers certificate appears first (and not the intermediate cert)
function validateCertificate(domain, cert, key) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof cert, 'string');
assert.strictEqual(typeof key, 'string');
function matchesDomain(candidate) {
if (typeof candidate !== 'string') return false;
if (candidate === domain) return true;
if (candidate.indexOf('*') === 0 && candidate.slice(2) === domain.slice(domain.indexOf('.') + 1)) return true;
return false;
}
// check for empty cert and key strings
if (!cert && key) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'missing cert');
if (cert && !key) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'missing key');
var result = safe.child_process.execSync('openssl x509 -noout -checkhost "' + domain + '"', { encoding: 'utf8', input: cert });
if (!result) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'Unable to get certificate subject.');
// if 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 ReverseProxyError(ReverseProxyError.INVALID_CERT, util.format('Certificate is not valid for this domain. Expecting %s in %j', domain, 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 ReverseProxyError(ReverseProxyError.INVALID_CERT, 'Key does not match the certificate.');
// check expiration
result = safe.child_process.execSync('openssl x509 -checkend 0', { encoding: 'utf8', input: cert });
if (!result) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'Certificate has expired.');
return null;
}
function reload(callback) {
if (process.env.BOX_ENV === 'test') return callback();
shell.sudo('reload', [ RELOAD_NGINX_CMD ], callback);
}
function setFallbackCertificate(domain, fallback, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof fallback, 'object');
assert.strictEqual(typeof callback, 'function');
const certFilePath = path.join(paths.APP_CERTS_DIR, `${domain}.host.cert`);
const keyFilePath = path.join(paths.APP_CERTS_DIR, `${domain}.host.key`);
if (fallback) {
// backup the cert
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${domain}.host.cert`), fallback.cert)) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${domain}.host.key`), fallback.key)) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, safe.error.message));
} else if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) { // generate it
var certCommand = util.format('openssl req -x509 -newkey rsa:2048 -keyout %s -out %s -days 3650 -subj /CN=*.%s -nodes', keyFilePath, certFilePath, domain);
if (!safe.child_process.execSync(certCommand)) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, safe.error.message));
}
platform.handleCertChanged('*.' + domain);
reload(function (error) {
if (error) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, error));
return callback(null);
});
}
function getFallbackCertificate(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
// check for any pre-provisioned (caas) certs. they get first priority
var certFilePath = path.join(paths.NGINX_CERT_DIR, `${domain}.host.cert`);
var keyFilePath = path.join(paths.NGINX_CERT_DIR, `${domain}.host.key`);
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, { certFilePath, keyFilePath });
// check for auto-generated or user set fallback certs
certFilePath = path.join(paths.APP_CERTS_DIR, `${domain}.host.cert`);
keyFilePath = path.join(paths.APP_CERTS_DIR, `${domain}.host.key`);
callback(null, { certFilePath, keyFilePath });
}
function getCertificate(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
var vhost = app.altDomain || app.intrinsicFqdn;
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 });
return getFallbackCertificate(app.domain, callback);
}
function ensureCertificate(app, auditSource, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
var vhost = app.altDomain || app.intrinsicFqdn;
var certFilePath = path.join(paths.APP_CERTS_DIR, `${vhost}.user.cert`);
var keyFilePath = path.join(paths.APP_CERTS_DIR, `${vhost}.user.key`);
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) {
debug('ensureCertificate: %s. user certificate already exists at %s', vhost, keyFilePath);
return callback(null, { certFilePath, keyFilePath, reason: 'user' });
}
certFilePath = path.join(paths.APP_CERTS_DIR, `${vhost}.cert`);
keyFilePath = path.join(paths.APP_CERTS_DIR, `${vhost}.key`);
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) {
debug('ensureCertificate: %s. certificate already exists at %s', vhost, keyFilePath);
if (!isExpiringSync(certFilePath, 24 * 30)) return callback(null, { certFilePath, keyFilePath, reason: 'existing-le' });
debug('ensureCertificate: %s cert require renewal', vhost);
} else {
debug('ensureCertificate: %s cert does not exist', vhost);
}
getApi(app, function (error, api, apiOptions) {
if (error) return callback(error);
debug('ensureCertificate: getting certificate for %s with options %j', vhost, apiOptions);
api.getCertificate(vhost, apiOptions, function (error, certFilePath, keyFilePath) {
var errorMessage = error ? error.message : '';
if (error) {
debug('ensureCertificate: could not get certificate. using fallback certs', error);
mailer.certificateRenewalError(vhost, errorMessage);
}
eventlog.add(eventlog.ACTION_CERTIFICATE_RENEWAL, auditSource, { domain: vhost, errorMessage: errorMessage });
// if no cert was returned use fallback. the fallback/caas provider will not provide any for example
if (!certFilePath || !keyFilePath) return getFallbackCertificate(app.altDomain ? tld.getDomain(app.altDomain) : app.domain, callback);
callback(null, { certFilePath, keyFilePath, reason: 'new-le' });
});
});
}
function configureAdminInternal(bundle, configFileName, vhost, callback) {
assert.strictEqual(typeof bundle, 'object');
assert.strictEqual(typeof configFileName, 'string');
assert.strictEqual(typeof vhost, 'string');
assert.strictEqual(typeof callback, 'function');
var data = {
sourceDir: path.resolve(__dirname, '..'),
adminOrigin: config.adminOrigin(),
vhost: vhost, // if vhost is empty it will become the default_server
hasIPv6: config.hasIPv6(),
endpoint: 'admin',
certFilePath: bundle.certFilePath,
keyFilePath: bundle.keyFilePath,
xFrameOptions: 'SAMEORIGIN',
robotsTxtQuoted: JSON.stringify('User-agent: *\nDisallow: /\n')
};
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, configFileName);
if (!safe.fs.writeFileSync(nginxConfigFilename, nginxConf)) return callback(safe.error);
reload(callback);
}
function configureAdmin(auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
var adminApp = { domain: config.adminDomain(), intrinsicFqdn: config.adminFqdn() };
ensureCertificate(adminApp, auditSource, function (error, bundle) {
if (error) return callback(error);
configureAdminInternal(bundle, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn(), callback);
});
}
function configureAppInternal(app, bundle, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof bundle, 'object');
assert.strictEqual(typeof callback, 'function');
var sourceDir = path.resolve(__dirname, '..');
var endpoint = 'app';
var vhost = app.altDomain || app.intrinsicFqdn;
var data = {
sourceDir: sourceDir,
adminOrigin: config.adminOrigin(),
vhost: vhost,
hasIPv6: config.hasIPv6(),
port: app.httpPort,
endpoint: endpoint,
certFilePath: bundle.certFilePath,
keyFilePath: bundle.keyFilePath,
robotsTxtQuoted: app.robotsTxt ? JSON.stringify(app.robotsTxt) : null,
xFrameOptions: app.xFrameOptions || 'SAMEORIGIN' // once all apps have been updated/
};
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, app.id + '.conf');
debug('writing config for "%s" to %s with options %j', vhost, nginxConfigFilename, data);
if (!safe.fs.writeFileSync(nginxConfigFilename, nginxConf)) {
debug('Error creating nginx config for "%s" : %s', vhost, safe.error.message);
return callback(safe.error);
}
reload(callback);
}
function configureApp(app, auditSource, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
ensureCertificate(app, auditSource, function (error, bundle) {
if (error) return callback(error);
configureAppInternal(app, bundle, callback);
});
}
function unconfigureApp(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
var vhost = app.altDomain || app.intrinsicFqdn;
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, app.id + '.conf');
if (!safe.fs.unlinkSync(nginxConfigFilename)) {
if (safe.error.code !== 'ENOENT') debug('Error removing nginx configuration of "%s": %s', vhost, safe.error.message);
return callback(null);
}
reload(callback);
}
function renewAll(auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
debug('renewAll: Checking certificates for renewal');
apps.getAll(function (error, allApps) {
if (error) return callback(error);
allApps.push({ domain: config.adminDomain(), intrinsicFqdn: config.adminFqdn() }); // inject fake webadmin app
async.eachSeries(allApps, function (app, iteratorCallback) {
ensureCertificate(app, auditSource, function (error, bundle) {
if (bundle.reason !== 'new-le' && bundle.reason !== 'fallback') return iteratorCallback();
// reconfigure for the case where we got a renewed cert after fallback
var configureFunc = app.intrinsicFqdn === config.adminFqdn() ?
configureAdminInternal.bind(null, bundle, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn())
: configureAppInternal.bind(null, app, bundle);
configureFunc(function (ignoredError) {
if (ignoredError) debug('fallbackExpiredCertificates: error reconfiguring app', ignoredError);
platform.handleCertChanged(app.intrinsicFqdn);
iteratorCallback(); // move to next app
});
});
});
});
}
function removeAppConfigs() {
for (var appConfigFile of fs.readdirSync(paths.NGINX_APPCONFIG_DIR)) {
fs.unlinkSync(path.join(paths.NGINX_APPCONFIG_DIR, appConfigFile));
}
}
function configureDefaultServer(callback) {
callback = callback || NOOP_CALLBACK;
var certFilePath = path.join(paths.NGINX_CERT_DIR, 'default.cert');
var keyFilePath = path.join(paths.NGINX_CERT_DIR, 'default.key');
if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) {
debug('configureDefaultServer: create new cert');
var cn = 'cloudron-' + (new Date()).toISOString(); // randomize date a bit to keep firefox happy
var certCommand = util.format('openssl req -x509 -newkey rsa:2048 -keyout %s -out %s -days 3650 -subj /CN=%s -nodes', keyFilePath, certFilePath, cn);
safe.child_process.execSync(certCommand);
}
configureAdminInternal({ certFilePath, keyFilePath }, 'default.conf', '', function (error) {
if (error) return callback(error);
debug('configureDefaultServer: done');
callback(null);
});
}
-1
View File
@@ -149,7 +149,6 @@ function installApp(req, res, next) {
debug('Installing app :%j', data);
apps.install(data, auditSource(req), function (error, app) {
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === AppsError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
if (error && error.reason === AppsError.PORT_RESERVED) return next(new HttpError(409, 'Port ' + error.message + ' is reserved.'));
if (error && error.reason === AppsError.PORT_CONFLICT) return next(new HttpError(409, 'Port ' + error.message + ' is already in use.'));
+153 -2
View File
@@ -1,6 +1,12 @@
'use strict';
exports = module.exports = {
activate: activate,
dnsSetup: dnsSetup,
setupTokenAuth: setupTokenAuth,
providerTokenAuth: providerTokenAuth,
getStatus: getStatus,
restore: restore,
reboot: reboot,
getProgress: getProgress,
getConfig: getConfig,
@@ -9,7 +15,8 @@ exports = module.exports = {
feedback: feedback,
checkForUpdates: checkForUpdates,
getLogs: getLogs,
getLogStream: getLogStream
getLogStream: getLogStream,
sendTestMail: sendTestMail
};
var appstore = require('../appstore.js'),
@@ -18,9 +25,13 @@ var appstore = require('../appstore.js'),
async = require('async'),
cloudron = require('../cloudron.js'),
CloudronError = cloudron.CloudronError,
config = require('../config.js'),
debug = require('debug')('box:routes/cloudron'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
progress = require('../progress.js'),
mailer = require('../mailer.js'),
superagent = require('superagent'),
updateChecker = require('../updatechecker.js'),
_ = require('underscore');
@@ -29,6 +40,136 @@ function auditSource(req) {
return { ip: ip, username: req.user ? req.user.username : null, userId: req.user ? req.user.id : null };
}
function activate(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.username !== 'string') return next(new HttpError(400, 'username must be string'));
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'password must be string'));
if (typeof req.body.email !== 'string') return next(new HttpError(400, 'email must be string'));
if ('displayName' in req.body && typeof req.body.displayName !== 'string') return next(new HttpError(400, 'displayName must be string'));
var username = req.body.username;
var password = req.body.password;
var email = req.body.email;
var displayName = req.body.displayName || '';
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
debug('activate: username:%s ip:%s', username, ip);
cloudron.activate(username, password, email, displayName, ip, auditSource(req), function (error, info) {
if (error && error.reason === CloudronError.ALREADY_PROVISIONED) return next(new HttpError(409, 'Already setup'));
if (error && error.reason === CloudronError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
// only in caas case do we have to notify the api server about activation
if (config.provider() !== 'caas') return next(new HttpSuccess(201, info));
// Now let the api server know we got activated
superagent.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/setup/done').query({ setupToken: req.query.setupToken })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return next(new HttpError(500, error));
if (result.statusCode === 403) return next(new HttpError(403, 'Invalid token'));
if (result.statusCode === 409) return next(new HttpError(409, 'Already setup'));
if (result.statusCode !== 201) return next(new HttpError(500, result.text || 'Internal error'));
next(new HttpSuccess(201, info));
});
});
}
function restore(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (!req.body.backupConfig || typeof req.body.backupConfig !== 'object') return next(new HttpError(400, 'backupConfig is required'));
var backupConfig = req.body.backupConfig;
if (typeof backupConfig.provider !== 'string') return next(new HttpError(400, 'provider is required'));
if ('key' in backupConfig && typeof backupConfig.key !== 'string') return next(new HttpError(400, 'key must be a string'));
if (typeof backupConfig.format !== 'string') return next(new HttpError(400, 'format must be a string'));
if ('acceptSelfSignedCerts' in backupConfig && typeof backupConfig.acceptSelfSignedCerts !== 'boolean') return next(new HttpError(400, 'format must be a boolean'));
if (typeof req.body.backupId !== 'string') return next(new HttpError(400, 'backupId must be a string or null'));
if (typeof req.body.version !== 'string') return next(new HttpError(400, 'version must be a string'));
cloudron.restore(backupConfig, req.body.backupId, req.body.version, function (error) {
if (error && error.reason === CloudronError.ALREADY_SETUP) return next(new HttpError(409, error.message));
if (error && error.reason === CloudronError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === CloudronError.BAD_STATE) return next(new HttpError(409, error.message));
if (error && error.reason === CloudronError.EXTERNAL_ERROR) return next(new HttpError(402, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200));
});
}
function dnsSetup(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.provider !== 'string' || !req.body.provider) return next(new HttpError(400, 'provider is required'));
if (typeof req.body.domain !== 'string' || !req.body.domain) return next(new HttpError(400, 'domain is required'));
if (typeof req.body.adminFqdn !== 'string' || !req.body.domain) return next(new HttpError(400, 'adminFqdn is required'));
if ('zoneName' in req.body && typeof req.body.zoneName !== 'string') return next(new HttpError(400, 'zoneName must be a string'));
if (!req.body.config || typeof req.body.config !== 'object') return next(new HttpError(400, 'config must be an object'));
cloudron.dnsSetup(req.body.adminFqdn.toLowerCase(), req.body.domain.toLowerCase(), req.body.zoneName || '', req.body.provider, req.body.config, function (error) {
if (error && error.reason === CloudronError.ALREADY_SETUP) return next(new HttpError(409, error.message));
if (error && error.reason === CloudronError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200));
});
}
function setupTokenAuth(req, res, next) {
assert.strictEqual(typeof req.query, 'object');
if (config.provider() === 'caas') {
if (typeof req.query.setupToken !== 'string' || !req.query.setupToken) return next(new HttpError(400, 'setupToken must be a non empty string'));
superagent.get(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/setup/verify').query({ setupToken:req.query.setupToken })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return next(new HttpError(500, error));
if (result.statusCode === 403) return next(new HttpError(403, 'Invalid token'));
if (result.statusCode === 409) return next(new HttpError(409, 'Already setup'));
if (result.statusCode !== 200) return next(new HttpError(500, result.text || 'Internal error'));
next();
});
} else {
next();
}
}
function providerTokenAuth(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (config.provider() === 'ami') {
if (typeof req.body.providerToken !== 'string' || !req.body.providerToken) return next(new HttpError(400, 'providerToken must be a non empty string'));
superagent.get('http://169.254.169.254/latest/meta-data/instance-id').timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return next(new HttpError(500, error));
if (result.statusCode !== 200) return next(new HttpError(500, 'Unable to get meta data'));
if (result.text !== req.body.providerToken) return next(new HttpError(403, 'Invalid providerToken'));
next();
});
} else {
next();
}
}
function getStatus(req, res, next) {
cloudron.getStatus(function (error, status) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, status));
});
}
function getProgress(req, res, next) {
return next(new HttpSuccess(200, progress.getAll()));
}
@@ -90,7 +231,7 @@ function feedback(req, res, next) {
if (typeof req.body.subject !== 'string' || !req.body.subject) return next(new HttpError(400, 'subject must be string'));
if (typeof req.body.description !== 'string' || !req.body.description) return next(new HttpError(400, 'description must be string'));
appstore.sendFeedback(_.extend(req.body, { email: req.user.email, displayName: req.user.displayName }), function (error) {
appstore.sendFeedback(_.extend(req.body, { email: req.user.alternateEmail || req.user.email, displayName: req.user.displayName }), function (error) {
if (error && error.reason === AppstoreError.BILLING_REQUIRED) return next(new HttpError(402, 'Login to App Store to create support tickets. You can also email support@cloudron.io'));
if (error) return next(new HttpError(503, 'Error contacting cloudron.io. Please email support@cloudron.io'));
@@ -164,3 +305,13 @@ function getLogStream(req, res, next) {
logStream.on('error', res.end.bind(res, null));
});
}
function sendTestMail(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (!req.body.email || typeof req.body.email !== 'string') return next(new HttpError(400, 'email must be a non-empty string'));
mailer.sendTestMail(req.body.email);
next(new HttpSuccess(202));
}
+3 -9
View File
@@ -26,10 +26,8 @@ function add(req, res, next) {
if ('fallbackCertificate' in req.body && typeof req.body.fallbackCertificate !== 'object') return next(new HttpError(400, 'fallbackCertificate must be a object with cert and key strings'));
if (req.body.fallbackCertificate && (!req.body.cert || typeof req.body.cert !== 'string')) return next(new HttpError(400, 'fallbackCertificate.cert must be a string'));
if (req.body.fallbackCertificate && (!req.body.key || typeof req.body.key !== 'string')) return next(new HttpError(400, 'fallbackCertificate.key must be a string'));
if ('tlsConfig' in req.body && typeof req.body.tlsConfig !== 'object') return next(new HttpError(400, 'tlsConfig must be a object with a provider string property'));
if (req.body.tlsConfig && (!req.body.tlsConfig.provider || typeof req.body.tlsConfig.provider !== 'string')) return next(new HttpError(400, 'tlsConfig.provider must be a string'));
domains.add(req.body.domain, req.body.zoneName || '', req.body.provider, req.body.config, req.body.fallbackCertificate || null, req.body.tlsConfig || { provider: 'letsencrypt-prod' }, function (error) {
domains.add(req.body.domain, req.body.zoneName || req.body.domain, req.body.provider, req.body.config, req.body.fallbackCertificate || null, function (error) {
if (error && error.reason === DomainError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
if (error && error.reason === DomainError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === DomainError.INVALID_PROVIDER) return next(new HttpError(400, error.message));
@@ -46,8 +44,6 @@ function get(req, res, next) {
if (error && error.reason === DomainError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
delete result.fallbackCertificate.key; // do not return the 'key'. in caas, this is private
next(new HttpSuccess(200, result));
});
}
@@ -69,10 +65,8 @@ function update(req, res, next) {
if ('fallbackCertificate' in req.body && typeof req.body.fallbackCertificate !== 'object') return next(new HttpError(400, 'fallbackCertificate must be a object with cert and key strings'));
if (req.body.fallbackCertificate && (!req.body.fallbackCertificate.cert || typeof req.body.fallbackCertificate.cert !== 'string')) return next(new HttpError(400, 'fallbackCertificate.cert must be a string'));
if (req.body.fallbackCertificate && (!req.body.fallbackCertificate.key || typeof req.body.fallbackCertificate.key !== 'string')) return next(new HttpError(400, 'fallbackCertificate.key must be a string'));
if ('tlsConfig' in req.body && typeof req.body.tlsConfig !== 'object') return next(new HttpError(400, 'tlsConfig must be a object with a provider string property'));
if (req.body.tlsConfig && (!req.body.tlsConfig.provider || typeof req.body.tlsConfig.provider !== 'string')) return next(new HttpError(400, 'tlsConfig.provider must be a string'));
domains.update(req.params.domain, req.body.provider, req.body.config, req.body.fallbackCertificate || null, req.body.tlsConfig || { provider: 'letsencrypt-prod' }, function (error) {
domains.update(req.params.domain, req.body.provider, req.body.config, req.body.fallbackCertificate || null, function (error) {
if (error && error.reason === DomainError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === DomainError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === DomainError.INVALID_PROVIDER) return next(new HttpError(400, error.message));
@@ -90,7 +84,7 @@ function del(req, res, next) {
if (error && error.reason === DomainError.IN_USE) return next(new HttpError(409, 'Domain is still in use'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(204));
next(new HttpSuccess(204, {}));
});
}
-2
View File
@@ -12,9 +12,7 @@ exports = module.exports = {
graphs: require('./graphs.js'),
groups: require('./groups.js'),
oauth2: require('./oauth2.js'),
mail: require('./mail.js'),
profile: require('./profile.js'),
setup: require('./setup.js'),
sysadmin: require('./sysadmin.js'),
settings: require('./settings.js'),
ssh: require('./ssh.js'),
-294
View File
@@ -1,294 +0,0 @@
'use strict';
exports = module.exports = {
get: get,
add: add,
del: del,
getStatus: getStatus,
setMailFromValidation: setMailFromValidation,
setCatchAllAddress: setCatchAllAddress,
setMailRelay: setMailRelay,
setMailEnabled: setMailEnabled,
sendTestMail: sendTestMail,
getMailboxes: getMailboxes,
getUserMailbox: getUserMailbox,
enableUserMailbox: enableUserMailbox,
disableUserMailbox: disableUserMailbox,
getAliases: getAliases,
setAliases: setAliases,
getLists: getLists,
getList: getList,
addList: addList,
removeList: removeList
};
var assert = require('assert'),
mail = require('../mail.js'),
MailError = mail.MailError,
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess;
function get(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
mail.get(req.params.domain, function (error, result) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, result));
});
}
function add(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.domain !== 'string') return next(new HttpError(400, 'domain must be a string'));
mail.add(req.body.domain, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === MailError.ALREADY_EXISTS) return next(new HttpError(409, 'domain already exists'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(201, { domain: req.body.domain }));
});
}
function del(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
mail.del(req.params.domain, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(204));
});
}
function getStatus(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
mail.getStatus(req.params.domain, function (error, records) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, records));
});
}
function setMailFromValidation(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.enabled !== 'boolean') return next(new HttpError(400, 'enabled is required'));
mail.setMailFromValidation(req.params.domain, req.body.enabled, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === MailError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202));
});
}
function setCatchAllAddress(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.body, 'object');
if (!req.body.address || !Array.isArray(req.body.address)) return next(new HttpError(400, 'address array is required'));
for (var i = 0; i < req.body.address.length; i++) {
if (typeof req.body.address[i] !== 'string') return next(new HttpError(400, 'address must be an array of string'));
}
mail.setCatchAllAddress(req.params.domain, req.body.address, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === MailError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202));
});
}
function setMailRelay(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider is required'));
if ('host' in req.body && typeof req.body.host !== 'string') return next(new HttpError(400, 'host must be a string'));
if ('port' in req.body && typeof req.body.port !== 'number') return next(new HttpError(400, 'port must be a string'));
if ('username' in req.body && typeof req.body.username !== 'string') return next(new HttpError(400, 'username must be a string'));
if ('password' in req.body && typeof req.body.password !== 'string') return next(new HttpError(400, 'password must be a string'));
mail.setMailRelay(req.params.domain, req.body, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === MailError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202));
});
}
function setMailEnabled(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.enabled !== 'boolean') return next(new HttpError(400, 'enabled is required'));
mail.setMailEnabled(req.params.domain, !!req.body.enabled, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === MailError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202));
});
}
function sendTestMail(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.body, 'object');
if (!req.body.to || typeof req.body.to !== 'string') return next(new HttpError(400, 'to must be a non-empty string'));
mail.sendTestMail(req.params.domain, req.body.to, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202));
});
}
function getMailboxes(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
mail.getMailboxes(req.params.domain, function (error, result) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { mailboxes: result }));
});
}
function getUserMailbox(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.params.userId, 'string');
mail.getUserMailbox(req.params.domain, req.params.userId, function (error, result) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { mailbox: result }));
});
}
function enableUserMailbox(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.params.userId, 'string');
mail.enableUserMailbox(req.params.domain, req.params.userId, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === MailError.ALREADY_EXISTS) return next(new HttpSuccess(201, {}));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(201, {}));
});
}
function disableUserMailbox(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.params.userId, 'string');
mail.disableUserMailbox(req.params.domain, req.params.userId, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpSuccess(201, {}));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(201, {}));
});
}
function getAliases(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.params.userId, 'string');
mail.getAliases(req.params.domain, req.params.userId, function (error, result) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { aliases: result }));
});
}
function setAliases(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.params.userId, 'string');
assert.strictEqual(typeof req.body, 'object');
if (!Array.isArray(req.body.aliases)) return next(new HttpError(400, 'aliases must be an array'));
for (var i = 0; i < req.body.aliases.length; i++) {
if (typeof req.body.aliases[i] !== 'string') return next(new HttpError(400, 'alias must be a string'));
}
mail.setAliases(req.params.domain, req.params.userId, req.body.aliases, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202));
});
}
function getLists(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
mail.getLists(req.params.domain, function (error, result) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { lists: result }));
});
}
function getList(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.params.groupId, 'string');
mail.getList(req.params.domain, req.params.groupId, function (error, result) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { list: result }));
});
}
function addList(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.groupId !== 'string') return next(new HttpError(400, 'groupId must be a string'));
mail.addList(req.params.domain, req.body.groupId, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === MailError.ALREADY_EXISTS) return next(new HttpError(409, 'list already exists'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(201, {}));
});
}
function removeList(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.params.groupId, 'string');
mail.removeList(req.params.domain, req.params.groupId, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(204));
});
}
+2 -3
View File
@@ -25,7 +25,7 @@ function get(req, res, next) {
id: req.user.id,
username: req.user.username,
email: req.user.email,
fallbackEmail: req.user.fallbackEmail,
alternateEmail: req.user.alternateEmail,
admin: req.user.admin,
displayName: req.user.displayName
}));
@@ -36,10 +36,9 @@ function update(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if ('email' in req.body && typeof req.body.email !== 'string') return next(new HttpError(400, 'email must be string'));
if ('fallbackEmail' in req.body && typeof req.body.fallbackEmail !== 'string') return next(new HttpError(400, 'fallbackEmail must be string'));
if ('displayName' in req.body && typeof req.body.displayName !== 'string') return next(new HttpError(400, 'displayName must be string'));
var data = _.pick(req.body, 'email', 'fallbackEmail', 'displayName');
var data = _.pick(req.body, 'email', 'displayName');
user.update(req.user.id, data, auditSource(req), function (error) {
if (error && error.reason === UserError.BAD_FIELD) return next(new HttpError(400, error.message));
+136 -1
View File
@@ -10,17 +10,37 @@ exports = module.exports = {
getCloudronAvatar: getCloudronAvatar,
setCloudronAvatar: setCloudronAvatar,
getEmailStatus: getEmailStatus,
getBackupConfig: getBackupConfig,
setBackupConfig: setBackupConfig,
getTimeZone: getTimeZone,
setTimeZone: setTimeZone,
getMailConfig: getMailConfig,
setMailConfig: setMailConfig,
getMailRelay: getMailRelay,
setMailRelay: setMailRelay,
getCatchAllAddress: getCatchAllAddress,
setCatchAllAddress: setCatchAllAddress,
getMailFromValidation: getMailFromValidation,
setMailFromValidation: setMailFromValidation,
getAppstoreConfig: getAppstoreConfig,
setAppstoreConfig: setAppstoreConfig
setAppstoreConfig: setAppstoreConfig,
setAdminCertificate: setAdminCertificate
};
var assert = require('assert'),
certificates = require('../certificates.js'),
CertificatesError = require('../certificates.js').CertificatesError,
config = require('../config.js'),
email = require('../email.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
safe = require('safetydance'),
@@ -90,6 +110,98 @@ function setTimeZone(req, res, next) {
});
}
function getMailConfig(req, res, next) {
settings.getMailConfig(function (error, mail) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, mail));
});
}
function setMailConfig(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.enabled !== 'boolean') return next(new HttpError(400, 'enabled is required'));
settings.setMailConfig({ enabled: req.body.enabled }, function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202));
});
}
function getMailFromValidation(req, res, next) {
settings.getMailFromValidation(function (error, enabled) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { enabled: enabled }));
});
}
function setMailFromValidation(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.enabled !== 'boolean') return next(new HttpError(400, 'enabled is required'));
settings.setMailFromValidation(req.body.enabled, function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202));
});
}
function getMailRelay(req, res, next) {
settings.getMailRelay(function (error, mail) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, mail));
});
}
function setMailRelay(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider is required'));
if ('host' in req.body && typeof req.body.host !== 'string') return next(new HttpError(400, 'host must be a string'));
if ('port' in req.body && typeof req.body.port !== 'number') return next(new HttpError(400, 'port must be a string'));
if ('username' in req.body && typeof req.body.username !== 'string') return next(new HttpError(400, 'username must be a string'));
if ('password' in req.body && typeof req.body.password !== 'string') return next(new HttpError(400, 'password must be a string'));
settings.setMailRelay(req.body, function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202));
});
}
function getCatchAllAddress(req, res, next) {
settings.getCatchAllAddress(function (error, address) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { address: address }));
});
}
function setCatchAllAddress(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (!req.body.address || !Array.isArray(req.body.address)) return next(new HttpError(400, 'address array is required'));
for (var i = 0; i < req.body.address.length; i++) {
if (typeof req.body.address[i] !== 'string') return next(new HttpError(400, 'address must be an array of string'));
}
settings.setCatchAllAddress(req.body.address, function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202));
});
}
function setCloudronAvatar(req, res, next) {
assert.strictEqual(typeof req.files, 'object');
@@ -115,6 +227,14 @@ function getCloudronAvatar(req, res, next) {
});
}
function getEmailStatus(req, res, next) {
email.getStatus(function (error, records) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, records));
});
}
function getBackupConfig(req, res, next) {
settings.getBackupConfig(function (error, config) {
if (error) return next(new HttpError(500, error));
@@ -172,3 +292,18 @@ function setAppstoreConfig(req, res, next) {
});
});
}
// only webadmin cert, until it can be treated just like a normal app
function setAdminCertificate(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (!req.body.cert || typeof req.body.cert !== 'string') return next(new HttpError(400, 'cert must be a string'));
if (!req.body.key || typeof req.body.key !== 'string') return next(new HttpError(400, 'key must be a string'));
certificates.setAdminCertificate(req.body.cert, req.body.key, function (error) {
if (error && error.reason === CertificatesError.INVALID_CERT) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, {}));
});
}
-154
View File
@@ -1,154 +0,0 @@
'use strict';
exports = module.exports = {
providerTokenAuth: providerTokenAuth,
setupTokenAuth: setupTokenAuth,
dnsSetup: dnsSetup,
activate: activate,
restore: restore,
getStatus: getStatus,
};
var assert = require('assert'),
caas = require('../caas.js'),
CaasError = require('../caas.js').CaasError,
config = require('../config.js'),
debug = require('debug')('box:routes/setup'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
setup = require('../setup.js'),
SetupError = require('../setup.js').SetupError,
superagent = require('superagent');
function auditSource(req) {
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
return { ip: ip, username: req.user ? req.user.username : null, userId: req.user ? req.user.id : null };
}
function providerTokenAuth(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (config.provider() === 'ami') {
if (typeof req.body.providerToken !== 'string' || !req.body.providerToken) return next(new HttpError(400, 'providerToken must be a non empty string'));
superagent.get('http://169.254.169.254/latest/meta-data/instance-id').timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return next(new HttpError(500, error));
if (result.statusCode !== 200) return next(new HttpError(500, 'Unable to get meta data'));
if (result.text !== req.body.providerToken) return next(new HttpError(403, 'Invalid providerToken'));
next();
});
} else {
next();
}
}
function setupTokenAuth(req, res, next) {
assert.strictEqual(typeof req.query, 'object');
if (config.provider() !== 'caas') return next();
if (typeof req.query.setupToken !== 'string' || !req.query.setupToken) return next(new HttpError(400, 'setupToken must be a non empty string'));
caas.verifySetupToken(req.query.setupToken, function (error) {
if (error && error.reason === CaasError.BAD_STATE) return next(new HttpError(409, 'Already setup'));
if (error && error.reason === CaasError.INVALID_TOKEN) return next(new HttpError(403, 'Invalid token'));
if (error && error.reason === CaasError.EXTERNAL_ERROR) return next(new HttpError(503, error.message));
if (error) return next(new HttpError(500, error));
next();
});
}
function dnsSetup(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.provider !== 'string' || !req.body.provider) return next(new HttpError(400, 'provider is required'));
if (typeof req.body.domain !== 'string' || !req.body.domain) return next(new HttpError(400, 'domain is required'));
if (typeof req.body.adminFqdn !== 'string' || !req.body.adminFqdn) return next(new HttpError(400, 'adminFqdn is required'));
if ('zoneName' in req.body && typeof req.body.zoneName !== 'string') return next(new HttpError(400, 'zoneName must be a string'));
if (!req.body.config || typeof req.body.config !== 'object') return next(new HttpError(400, 'config must be an object'));
if ('tlsConfig' in req.body && typeof req.body.tlsConfig !== 'object') return next(new HttpError(400, 'tlsConfig must be an object'));
if (req.body.tlsConfig && (!req.body.tlsConfig.provider || typeof req.body.tlsConfig.provider !== 'string')) return next(new HttpError(400, 'tlsConfig.provider must be a string'));
setup.dnsSetup(req.body.adminFqdn.toLowerCase(), req.body.domain.toLowerCase(), req.body.zoneName || '', req.body.provider, req.body.config, req.body.tlsConfig || { provider: 'letsencrypt-prod' }, function (error) {
if (error && error.reason === SetupError.ALREADY_SETUP) return next(new HttpError(409, error.message));
if (error && error.reason === SetupError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200));
});
}
function getStatus(req, res, next) {
setup.getStatus(function (error, status) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, status));
});
}
function activate(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.username !== 'string') return next(new HttpError(400, 'username must be string'));
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'password must be string'));
if (typeof req.body.email !== 'string') return next(new HttpError(400, 'email must be string'));
if ('displayName' in req.body && typeof req.body.displayName !== 'string') return next(new HttpError(400, 'displayName must be string'));
var username = req.body.username;
var password = req.body.password;
var email = req.body.email;
var displayName = req.body.displayName || '';
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
debug('activate: username:%s ip:%s', username, ip);
setup.activate(username, password, email, displayName, ip, auditSource(req), function (error, info) {
if (error && error.reason === SetupError.ALREADY_PROVISIONED) return next(new HttpError(409, 'Already setup'));
if (error && error.reason === SetupError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
// only in caas case do we have to notify the api server about activation
if (config.provider() !== 'caas') return next(new HttpSuccess(201, info));
caas.setupDone(req.query.setupToken, function (error) {
if (error && error.reason === CaasError.BAD_STATE) return next(new HttpError(409, 'Already setup'));
if (error && error.reason === CaasError.INVALID_TOKEN) return next(new HttpError(403, 'Invalid token'));
if (error && error.reason === CaasError.EXTERNAL_ERROR) return next(new HttpError(503, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(201, info));
});
});
}
function restore(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (!req.body.backupConfig || typeof req.body.backupConfig !== 'object') return next(new HttpError(400, 'backupConfig is required'));
var backupConfig = req.body.backupConfig;
if (typeof backupConfig.provider !== 'string') return next(new HttpError(400, 'provider is required'));
if ('key' in backupConfig && typeof backupConfig.key !== 'string') return next(new HttpError(400, 'key must be a string'));
if (typeof backupConfig.format !== 'string') return next(new HttpError(400, 'format must be a string'));
if ('acceptSelfSignedCerts' in backupConfig && typeof backupConfig.acceptSelfSignedCerts !== 'boolean') return next(new HttpError(400, 'format must be a boolean'));
if (typeof req.body.backupId !== 'string') return next(new HttpError(400, 'backupId must be a string or null'));
if (typeof req.body.version !== 'string') return next(new HttpError(400, 'version must be a string'));
setup.restore(backupConfig, req.body.backupId, req.body.version, function (error) {
if (error && error.reason === SetupError.ALREADY_SETUP) return next(new HttpError(409, error.message));
if (error && error.reason === SetupError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === SetupError.BAD_STATE) return next(new HttpError(409, error.message));
if (error && error.reason === SetupError.EXTERNAL_ERROR) return next(new HttpError(402, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200));
});
}
+69 -94
View File
@@ -17,7 +17,6 @@ var appdb = require('../../appdb.js'),
constants = require('../../constants.js'),
database = require('../../database.js'),
docker = require('../../docker.js').connection,
domains = require('../../domains.js'),
expect = require('expect.js'),
fs = require('fs'),
hock = require('hock'),
@@ -25,7 +24,6 @@ var appdb = require('../../appdb.js'),
https = require('https'),
js2xml = require('js2xmlparser').parse,
ldap = require('../../ldap.js'),
mail = require('../../mail.js'),
net = require('net'),
nock = require('nock'),
paths = require('../../paths.js'),
@@ -47,21 +45,11 @@ var TEST_IMAGE_TAG = '25.2.0';
var TEST_IMAGE = TEST_IMAGE_REPO + ':' + TEST_IMAGE_TAG;
// var TEST_IMAGE_ID = child_process.execSync('docker inspect --format={{.Id}} ' + TEST_IMAGE).toString('utf8').trim();
const DOMAIN_0 = {
domain: 'example-apps-test.com',
zoneName: 'example-apps-test.com',
config: {},
provider: 'noop',
fallbackCertificate: null,
tlsConfig: { provider: 'fallback' }
};
const CLOUDRON_ID = 'somecloudronid';
var APP_STORE_ID = 'test', APP_ID;
var APP_LOCATION = 'appslocation';
var APP_DOMAIN = 'example-apps-test.com';
var APP_LOCATION_2 = 'appslocationtwo';
var APP_DOMAIN_2 = 'example-apps-test.com';
var APP_LOCATION_NEW = 'appslocationnew';
var APP_MANIFEST = JSON.parse(fs.readFileSync(__dirname + '/../../../../test-app/CloudronManifest.json', 'utf8'));
@@ -72,7 +60,6 @@ APP_MANIFEST_1.dockerImage = TEST_IMAGE;
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='admin@me.com';
var USER_1_ID = null, USERNAME_1 = 'user', EMAIL_1 ='user@me.com';
const USER_1_APPSTORE_TOKEN = 'appstoretoken';
var token = null; // authentication token
var token_1 = null;
@@ -164,45 +151,44 @@ function startBox(done) {
safe.fs.unlinkSync(paths.INFRA_VERSION_FILE);
child_process.execSync('docker ps -qa | xargs --no-run-if-empty docker rm -f');
// awsHostedZones = {
// HostedZones: [{
// Id: '/hostedzone/ZONEID',
// Name: config.zoneName() + '.',
// CallerReference: '305AFD59-9D73-4502-B020-F4E6F889CB30',
// ResourceRecordSetCount: 2,
// ChangeInfo: {
// Id: '/change/CKRTFJA0ANHXB',
// Status: 'INSYNC'
// }
// }],
// IsTruncated: false,
// MaxItems: '100'
// };
config.setFqdn(APP_DOMAIN);
config.setZoneName('foobar.com');
awsHostedZones = {
HostedZones: [{
Id: '/hostedzone/ZONEID',
Name: config.zoneName() + '.',
CallerReference: '305AFD59-9D73-4502-B020-F4E6F889CB30',
ResourceRecordSetCount: 2,
ChangeInfo: {
Id: '/change/CKRTFJA0ANHXB',
Status: 'INSYNC'
}
}],
IsTruncated: false,
MaxItems: '100'
};
async.series([
// first clear, then start server. otherwise, taskmanager spins up tasks for obsolete appIds
database.initialize,
database._clear,
server.start,
server.start.bind(server),
ldap.start,
function (callback) {
superagent.post(SERVER_URL + '/api/v1/cloudron/dns_setup')
.send({ provider: 'noop', domain: DOMAIN_0.domain, adminFqdn: 'my.' + DOMAIN_0.domain, config: DOMAIN_0.config, tlsConfig: DOMAIN_0.tlsConfig })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(200);
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
callback();
});
},
function (callback) {
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(201);
expect(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
// stash for further use
token = result.body.token;
@@ -246,7 +232,11 @@ function startBox(done) {
}
return false;
}, callback);
}
},
settings.setDnsConfig.bind(null, { provider: 'route53', accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey', endpoint: 'http://localhost:5353' }, config.fqdn(), config.zoneName()),
settings.setTlsConfig.bind(null, { provider: 'caas' }),
settings.setBackupConfig.bind(null, { provider: 'caas', token: 'BACKUP_TOKEN', bucket: 'Bucket', prefix: 'Prefix' })
], function (error) {
if (error) return done(error);
@@ -331,24 +321,13 @@ describe('App API', function () {
});
});
it('app install fails - missing domain', function (done) {
it('app install fails - invalid location', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ manifest: APP_MANIFEST, location: 'some', accessRestriction: null })
.send({ manifest: APP_MANIFEST, location: '!awesome', accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('domain is required');
done();
});
});
it('app install fails - non-existing domain', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ manifest: APP_MANIFEST, location: 'some', accessRestriction: null, domain: 'doesnotexist.com' })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
expect(res.body.message).to.eql('No such domain');
expect(res.body.message).to.eql('Hostname can only contain alphanumerics and hyphen');
done();
});
});
@@ -356,7 +335,7 @@ describe('App API', function () {
it('app install fails - invalid location type', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ manifest: APP_MANIFEST, location: 42, accessRestriction: null, domain: DOMAIN_0.domain })
.send({ manifest: APP_MANIFEST, location: 42, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('location is required');
@@ -367,7 +346,7 @@ describe('App API', function () {
it('app install fails - reserved admin location', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ manifest: APP_MANIFEST, location: 'my', accessRestriction: null, domain: DOMAIN_0.domain })
.send({ manifest: APP_MANIFEST, location: 'my', accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('my is reserved');
@@ -378,7 +357,7 @@ describe('App API', function () {
it('app install fails - reserved api location', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ manifest: APP_MANIFEST, location: constants.API_LOCATION, accessRestriction: null, domain: DOMAIN_0.domain })
.send({ manifest: APP_MANIFEST, location: constants.API_LOCATION, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql(constants.API_LOCATION + ' is reserved');
@@ -389,7 +368,7 @@ describe('App API', function () {
it('app install fails - portBindings must be object', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ manifest: APP_MANIFEST, location: APP_LOCATION, portBindings: 23, accessRestriction: null, domain: DOMAIN_0.domain })
.send({ manifest: APP_MANIFEST, location: APP_LOCATION, portBindings: 23, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('portBindings must be an object');
@@ -400,7 +379,7 @@ describe('App API', function () {
it('app install fails - accessRestriction is required', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ manifest: APP_MANIFEST, location: APP_LOCATION, portBindings: {}, domain: DOMAIN_0.domain })
.send({ manifest: APP_MANIFEST, location: APP_LOCATION, portBindings: {} })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('accessRestriction is required');
@@ -411,7 +390,7 @@ describe('App API', function () {
it('app install fails - accessRestriction type is wrong', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ manifest: APP_MANIFEST, location: APP_LOCATION, portBindings: {}, accessRestriction: '', domain: DOMAIN_0.domain })
.send({ manifest: APP_MANIFEST, location: APP_LOCATION, portBindings: {}, accessRestriction: '' })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('accessRestriction is required');
@@ -422,7 +401,7 @@ describe('App API', function () {
it('app install fails for non admin', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token_1 })
.send({ manifest: APP_MANIFEST, location: APP_LOCATION, portBindings: null, accessRestriction: null, domain: DOMAIN_0.domain })
.send({ manifest: APP_MANIFEST, location: APP_LOCATION, portBindings: null, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(403);
done();
@@ -434,9 +413,9 @@ describe('App API', function () {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, location: APP_LOCATION, portBindings: null, domain: DOMAIN_0.domain, accessRestriction: { users: [ 'someuser' ], groups: [] } })
.send({ appStoreId: APP_STORE_ID, location: APP_LOCATION, portBindings: null, accessRestriction: { users: [ 'someuser' ], groups: [] } })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
expect(res.statusCode).to.equal(400);
expect(fake.isDone()).to.be.ok();
done();
});
@@ -444,45 +423,40 @@ describe('App API', function () {
it('app install fails due to purchase failure', function (done) {
var fake1 = nock(config.apiServerOrigin()).get('/api/v1/apps/test').reply(200, { manifest: APP_MANIFEST });
var fake2 = nock(config.apiServerOrigin()).post('/api/v1/exchangeBoxTokenWithUserToken?token=APPSTORE_TOKEN').reply(403);
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, location: APP_LOCATION, domain: DOMAIN_0.domain, portBindings: null, accessRestriction: null })
.send({ appStoreId: APP_STORE_ID, location: APP_LOCATION, portBindings: null, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(402);
expect(res.statusCode).to.equal(503);
expect(fake1.isDone()).to.be.ok();
expect(fake2.isDone()).to.be.ok();
done();
});
});
it('app install succeeds with purchase', function (done) {
var fake1 = nock(config.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/users/' + USER_1_ID + '/cloudrons') >= 0; }, { 'domain': DOMAIN_0.domain }).reply(201, { cloudron: { id: CLOUDRON_ID } });
var fake2 = nock(config.apiServerOrigin()).get('/api/v1/apps/' + APP_STORE_ID).reply(200, { manifest: APP_MANIFEST });
var fake3 = nock(config.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/users/' + USER_1_ID + '/cloudrons/' + CLOUDRON_ID + '/apps/') >= 0; }, { 'appstoreId': APP_STORE_ID }).reply(201, { });
settings.setAppstoreConfig({ userId: USER_1_ID, token: USER_1_APPSTORE_TOKEN }, function (error) {
if (error) return done(error);
var fake1 = nock(config.apiServerOrigin()).get('/api/v1/apps/' + APP_STORE_ID).reply(200, { manifest: APP_MANIFEST });
var fake2 = nock(config.apiServerOrigin()).post('/api/v1/exchangeBoxTokenWithUserToken?token=APPSTORE_TOKEN').reply(201, { userId: 'USER_ID', cloudronId: 'CLOUDRON_ID', token: 'ACCESS_TOKEN' });
var fake3 = nock(config.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/users/USER_ID/cloudrons/CLOUDRON_ID/apps/') >= 0; }, { 'appstoreId': APP_STORE_ID }).reply(201, { });
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, location: APP_LOCATION, portBindings: null, accessRestriction: { users: [ 'someuser' ], groups: [] } })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
expect(res.body.id).to.be.a('string');
APP_ID = res.body.id;
expect(fake1.isDone()).to.be.ok();
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, location: APP_LOCATION, domain: DOMAIN_0.domain, portBindings: null, accessRestriction: { users: [ 'someuser' ], groups: [] } })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
expect(res.body.id).to.be.a('string');
APP_ID = res.body.id;
expect(fake2.isDone()).to.be.ok();
expect(fake3.isDone()).to.be.ok();
done();
});
done();
});
});
it('app install fails because of conflicting location', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ manifest: APP_MANIFEST, location: APP_LOCATION, domain: DOMAIN_0.domain, portBindings: null, accessRestriction: null })
.send({ manifest: APP_MANIFEST, location: APP_LOCATION, portBindings: null, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(409);
done();
@@ -572,33 +546,32 @@ describe('App API', function () {
});
it('can uninstall app', function (done) {
var fake1 = nock(config.apiServerOrigin()).get(function (uri) { return uri.indexOf('/api/v1/users/' + USER_1_ID + '/cloudrons/' + CLOUDRON_ID + '/apps/') >= 0; }).reply(200, { });
var fake2 = nock(config.apiServerOrigin()).delete(function (uri) { return uri.indexOf('/api/v1/users/' + USER_1_ID + '/cloudrons/' + CLOUDRON_ID + '/apps/') >= 0; }).reply(204, { });
var fake1 = nock(config.apiServerOrigin()).post('/api/v1/exchangeBoxTokenWithUserToken?token=APPSTORE_TOKEN').reply(201, { userId: 'USER_ID', cloudronId: 'CLOUDRON_ID', token: 'ACCESS_TOKEN' });
var fake2 = nock(config.apiServerOrigin()).get(function (uri) { return uri.indexOf('/api/v1/users/USER_ID/cloudrons/CLOUDRON_ID/apps/') >= 0; }).reply(200, { });
var fake3 = nock(config.apiServerOrigin()).delete(function (uri) { return uri.indexOf('/api/v1/users/USER_ID/cloudrons/CLOUDRON_ID/apps/') >= 0; }).reply(204, { });
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/uninstall')
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
expect(fake1.isDone()).to.be.ok();
expect(fake2.isDone()).to.be.ok();
done();
});
});
it('app install succeeds again', function (done) {
var fake1 = nock(config.apiServerOrigin()).get('/api/v1/apps/' + APP_STORE_ID).reply(200, { manifest: APP_MANIFEST });
var fake2 = nock(config.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/users/' + USER_1_ID + '/cloudrons/' + CLOUDRON_ID + '/apps/') >= 0; }, { 'appstoreId': APP_STORE_ID }).reply(201, { });
var fake2 = nock(config.apiServerOrigin()).post('/api/v1/exchangeBoxTokenWithUserToken?token=APPSTORE_TOKEN').reply(201, { userId: 'USER_ID', cloudronId: 'CLOUDRON_ID', token: 'ACCESS_TOKEN' });
var fake3 = nock(config.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/users/USER_ID/cloudrons/CLOUDRON_ID/apps/') >= 0; }, { 'appstoreId': APP_STORE_ID }).reply(201, { });
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, location: APP_LOCATION_2, domain: DOMAIN_0.domain, portBindings: null, accessRestriction: null })
.send({ appStoreId: APP_STORE_ID, location: APP_LOCATION_2, portBindings: null, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
expect(res.body.id).to.be.a('string');
APP_ID = res.body.id;
expect(fake1.isDone()).to.be.ok();
expect(fake2.isDone()).to.be.ok();
done();
});
});
@@ -617,7 +590,7 @@ describe('App API', function () {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ manifest: APP_MANIFEST, location: APP_LOCATION+APP_LOCATION, domain: DOMAIN_0.domain, portBindings: null, accessRestriction: null })
.send({ manifest: APP_MANIFEST, location: APP_LOCATION+APP_LOCATION, portBindings: null, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
expect(res.body.id).to.be.a('string');
@@ -637,7 +610,7 @@ describe('App API', function () {
});
});
xdescribe('App installation', function () {
describe('App installation', function () {
this.timeout(100000);
var apiHockInstance = hock.createHock({ throwOnUnmatched: false }), apiHockServer;
@@ -665,7 +638,7 @@ xdescribe('App installation', function () {
apiHockServer = http.createServer(apiHockInstance.handler).listen(port, callback);
},
settings.setDnsConfig.bind(null, { provider: 'route53', accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey', endpoint: 'http://localhost:5353' }, config.adminDomain(), config.zoneName()),
settings.setDnsConfig.bind(null, { provider: 'route53', accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey', endpoint: 'http://localhost:5353' }, config.fqdn(), config.zoneName()),
settings.setTlsConfig.bind(null, { provider: 'caas' }),
@@ -701,6 +674,7 @@ xdescribe('App installation', function () {
it('can install test app', function (done) {
var fake1 = nock(config.apiServerOrigin()).get('/api/v1/apps/' + APP_STORE_ID).reply(200, { manifest: APP_MANIFEST });
var fake2 = nock(config.apiServerOrigin()).post('/api/v1/exchangeBoxTokenWithUserToken?token=APPSTORE_TOKEN').reply(201, { userId: 'USER_ID', cloudronId: 'CLOUDRON_ID', token: 'ACCESS_TOKEN' });
var fake3 = nock(config.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/users/USER_ID/cloudrons/CLOUDRON_ID/apps/') >= 0; }, { 'appstoreId': APP_STORE_ID }).reply(201, { });
var count = 0;
@@ -1129,6 +1103,7 @@ xdescribe('App installation', function () {
});
it('can uninstall app', function (done) {
var fake1 = nock(config.apiServerOrigin()).post('/api/v1/exchangeBoxTokenWithUserToken?token=APPSTORE_TOKEN').reply(201, { userId: 'USER_ID', cloudronId: 'CLOUDRON_ID', token: 'ACCESS_TOKEN' });
var fake2 = nock(config.apiServerOrigin()).get(function (uri) { return uri.indexOf('/api/v1/users/USER_ID/cloudrons/CLOUDRON_ID/apps/') >= 0; }).reply(200, { });
var fake3 = nock(config.apiServerOrigin()).delete(function (uri) { return uri.indexOf('/api/v1/users/USER_ID/cloudrons/CLOUDRON_ID/apps/') >= 0; }).reply(204, { });
+22 -17
View File
@@ -9,44 +9,40 @@ var appdb = require('../../appdb.js'),
async = require('async'),
config = require('../../config.js'),
database = require('../../database.js'),
domains = require('../../domains.js'),
expect = require('expect.js'),
nock = require('nock'),
superagent = require('superagent'),
server = require('../../server.js'),
settings = require('../../settings.js');
const SERVER_URL = 'http://localhost:' + config.get('port');
const USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
const DOMAIN_0 = {
domain: 'example-backups-test.com',
zoneName: 'example-backups-test.com',
config: {},
provider: 'noop',
fallbackCertificate: null,
tlsConfig: { provider: 'fallback' }
};
var SERVER_URL = 'http://localhost:' + config.get('port');
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var token = null;
function setup(done) {
nock.cleanAll();
config._reset();
config.setVersion('1.2.3');
config.setFqdn('example-backups-test.com');
async.series([
server.start,
server.start.bind(server),
database._clear,
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0.zoneName, DOMAIN_0.provider, DOMAIN_0.config, DOMAIN_0.fallbackCertificate, DOMAIN_0.tlsConfig),
function createAdmin(callback) {
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(201);
expect(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
// stash token for further use
token = result.body.token;
@@ -57,7 +53,7 @@ function setup(done) {
function addApp(callback) {
var manifest = { version: '0.0.1', manifestVersion: 1, dockerImage: 'foo', healthCheckPath: '/', httpPort: 3, title: 'ok', addons: { } };
appdb.add('appid', 'appStoreId', manifest, 'location', DOMAIN_0.domain, [ ] /* portBindings */, { }, callback);
appdb.add('appid', 'appStoreId', manifest, 'location', config.fqdn(), [ ] /* portBindings */, { }, callback);
},
function createSettings(callback) {
@@ -75,6 +71,9 @@ function cleanup(done) {
}
describe('Backups API', function () {
var scope1 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=BACKUP_TOKEN')
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey' } }, { 'Content-Type': 'application/json' });
before(setup);
after(cleanup);
@@ -103,7 +102,13 @@ describe('Backups API', function () {
.end(function (error, result) {
expect(result.statusCode).to.equal(202);
done();
function checkAppstoreServerCalled() {
if (scope1.isDone()) return done();
setTimeout(checkAppstoreServerCalled, 100);
}
checkAppstoreServerCalled();
});
});
});
-367
View File
@@ -1,367 +0,0 @@
/* global it:false */
/* global describe:false */
/* global xdescribe:false */
/* global before:false */
/* global after:false */
'use strict';
var appdb = require('../../appdb.js'),
async = require('async'),
config = require('../../config.js'),
database = require('../../database.js'),
domains = require('../../domains.js'),
expect = require('expect.js'),
locker = require('../../locker.js'),
nock = require('nock'),
os = require('os'),
superagent = require('superagent'),
server = require('../../server.js'),
settings = require('../../settings.js'),
settingsdb = require('../../settingsdb.js'),
shell = require('../../shell.js');
const SERVER_URL = 'http://localhost:' + config.get('port');
const USERNAME = 'superadmin';
const PASSWORD = 'Foobar?1337';
const EMAIL ='silly@me.com';
const DOMAIN_0 = {
domain: 'example-backups-test.com',
zoneName: 'example-backups-test.com',
config: {},
provider: 'noop',
fallbackCertificate: null,
tlsConfig: { provider: 'fallback' }
};
var token = null;
var gSudoOriginal = null;
function injectShellMock() {
gSudoOriginal = shell.sudo;
shell.sudo = function (tag, options, callback) { callback(null); };
}
function restoreShellMock() {
shell.sudo = gSudoOriginal;
}
function setup(done) {
nock.cleanAll();
config._reset();
config.set('provider', 'caas');
config.setVersion('1.2.3');
async.series([
server.start.bind(server),
database._clear,
settingsdb.set.bind(null, settings.CAAS_CONFIG_KEY, JSON.stringify({ boxId: 'BOX_ID', token: 'ACCESS_TOKEN2' })),
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0.zoneName, DOMAIN_0.provider, DOMAIN_0.config, DOMAIN_0.fallbackCertificate, DOMAIN_0.tlsConfig),
function createAdmin(callback) {
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/BOX_ID/setup/verify?setupToken=somesetuptoken').reply(200, {});
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/BOX_ID/setup/done?setupToken=somesetuptoken').reply(201, {});
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(201);
expect(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
// stash token for further use
token = result.body.token;
callback();
});
},
function addApp(callback) {
var manifest = { version: '0.0.1', manifestVersion: 1, dockerImage: 'foo', healthCheckPath: '/', httpPort: 3, title: 'ok', addons: { } };
appdb.add('appid', 'appStoreId', manifest, 'location', DOMAIN_0.domain, [ ] /* portBindings */, { }, callback);
},
function createSettings(callback) {
settings.setBackupConfig({ provider: 'filesystem', backupFolder: '/tmp', format: 'tgz' }, callback);
}
], done);
}
function cleanup(done) {
database._clear(function (error) {
expect(!error).to.be.ok();
server.stop(done);
});
}
describe('Caas', function () {
describe('activate', function () {
before(setup);
after(cleanup);
it('fails due to missing setupToken', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.send({ username: '', password: 'somepassword', email: 'admin@foo.bar' })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
// note that while the server itself returns 503, the cloudron gets activated. this is just the way it is
it('fails due to internal server error on appstore side', function (done) {
var scope = nock(config.apiServerOrigin()).get('/api/v1/boxes/BOX_ID/setup/verify?setupToken=somesetuptoken').reply(500, { message: 'this is wrong' });
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: 'someuser', password: 'strong#A3asdf', email: 'admin@foo.bar' })
.end(function (error, result) {
expect(result.statusCode).to.equal(503);
expect(scope.isDone()).to.be.ok();
done();
});
});
xit('succeeds', function (done) {
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/BOX_ID/setup/verify?setupToken=somesetuptoken').reply(200, {});
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/BOX_ID/setup/done?setupToken=somesetuptoken').reply(201, {});
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: 'someuser', password: 'ADSF#asd546', email: 'admin@foo.bar', displayName: 'tester' })
.end(function (error, result) {
console.dir(error);
expect(result.statusCode).to.equal(201);
expect(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
done();
});
});
});
describe('get config', function () {
before(setup);
after(cleanup);
it('succeeds (admin)', function (done) {
var scope = nock(config.apiServerOrigin())
.get('/api/v1/boxes/BOX_ID?token=ACCESS_TOKEN2')
.reply(200, { box: { region: 'sfo', size: '1gb' }, user: { }});
superagent.get(SERVER_URL + '/api/v1/cloudron/config')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.apiServerOrigin).to.eql('http://localhost:6060');
expect(result.body.webServerOrigin).to.eql(null);
expect(result.body.adminFqdn).to.eql(config.adminFqdn());
expect(result.body.progress).to.be.an('object');
expect(result.body.update).to.be.an('object');
expect(result.body.version).to.eql(config.version());
expect(result.body.size).to.eql('1gb');
expect(result.body.region).to.eql('sfo');
expect(result.body.memory).to.eql(os.totalmem());
expect(result.body.cloudronName).to.be.a('string');
expect(result.body.provider).to.be.a('string');
expect(scope.isDone()).to.be.ok();
done();
});
});
});
describe('Backups API', function () {
var scope1 = nock(config.apiServerOrigin()).post('/api/v1/boxes/BOX_ID/awscredentials?token=BACKUP_TOKEN')
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey' } }, { 'Content-Type': 'application/json' });
before(setup);
after(cleanup);
it('calls the appstore after backup is done', function (done) {
superagent.post(SERVER_URL + '/api/v1/backups')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(202);
function checkAppstoreServerCalled() {
if (scope1.isDone()) return done();
setTimeout(checkAppstoreServerCalled, 100);
}
checkAppstoreServerCalled();
});
});
});
xdescribe('migrate', function () {
before(function (done) {
async.series([
setup,
function (callback) {
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/BOX_ID/setup/verify?setupToken=somesetuptoken').reply(200, {});
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/BOX_ID/setup/done?setupToken=somesetuptoken').reply(201, {});
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
// stash token for further use
token = result.body.token;
callback();
});
}
], done);
});
after(function (done) {
locker.unlock(locker._operation); // migrate never unlocks
cleanup(done);
});
it('fails without token', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 'small', region: 'sfo'})
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails without password', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 'small', region: 'sfo'})
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('succeeds without size', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ region: 'sfo', password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(202);
done();
});
});
it('fails with wrong size type', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 4, region: 'sfo', password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('succeeds without region', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 'small', password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(202);
done();
});
});
it('fails with wrong region type', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 'small', region: 4, password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails when in wrong state', function (done) {
var scope2 = nock(config.apiServerOrigin())
.post('/api/v1/boxes/BOX_ID/awscredentials?token=BACKUP_TOKEN')
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } });
var scope3 = nock(config.apiServerOrigin())
.post('/api/v1/boxes/BOX_ID/backupDone?token=APPSTORE_TOKEN', function (body) {
return body.boxVersion && body.restoreKey && !body.appId && !body.appVersion && body.appBackupIds.length === 0;
})
.reply(200, { id: 'someid' });
var scope1 = nock(config.apiServerOrigin())
.post('/api/v1/boxes/BOX_ID/migrate?token=APPSTORE_TOKEN', function (body) {
return body.size && body.region && body.restoreKey;
}).reply(409, {});
injectShellMock();
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 'small', region: 'sfo', password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(202);
function checkAppstoreServerCalled() {
if (scope1.isDone() && scope2.isDone() && scope3.isDone()) {
restoreShellMock();
return done();
}
setTimeout(checkAppstoreServerCalled, 100);
}
checkAppstoreServerCalled();
});
});
it('succeeds', function (done) {
var scope1 = nock(config.apiServerOrigin()).post('/api/v1/boxes/BOX_ID/migrate?token=APPSTORE_TOKEN', function (body) {
return body.size && body.region && body.restoreKey;
}).reply(202, {});
var scope2 = nock(config.apiServerOrigin())
.post('/api/v1/boxes/BOX_ID/backupDone?token=APPSTORE_TOKEN', function (body) {
return body.boxVersion && body.restoreKey && !body.appId && !body.appVersion && body.appBackupIds.length === 0;
})
.reply(200, { id: 'someid' });
var scope3 = nock(config.apiServerOrigin())
.post('/api/v1/boxes/BOX_ID/awscredentials?token=BACKUP_TOKEN')
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } });
injectShellMock();
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 'small', region: 'sfo', password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(202);
function checkAppstoreServerCalled() {
if (scope1.isDone() && scope2.isDone() && scope3.isDone()) {
restoreShellMock();
return done();
}
setTimeout(checkAppstoreServerCalled, 100);
}
checkAppstoreServerCalled();
});
});
});
});
+237 -229
View File
@@ -12,9 +12,11 @@ var async = require('async'),
oauth2 = require('../oauth2.js'),
expect = require('expect.js'),
uuid = require('uuid'),
nock = require('nock'),
hat = require('hat'),
superagent = require('superagent'),
server = require('../../server.js');
server = require('../../server.js'),
settings = require('../../settings.js');
var SERVER_URL = 'http://localhost:' + config.get('port');
@@ -24,24 +26,30 @@ var token = null;
function setup(done) {
config._reset();
config.setFqdn('example-clients-test.com');
config.set('provider', 'caas');
async.series([
server.start,
database._clear,
function (callback) {
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.equal(201);
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.equal(201);
expect(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
// stash token for further use
token = result.body.token;
// stash token for further use
token = result.body.token;
callback();
});
callback();
});
}
], done);
}
@@ -61,118 +69,118 @@ describe('OAuth Clients API', function () {
it('fails without token', function (done) {
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
.send({ appId: 'someApp', redirectURI: 'http://foobar.com', scope: 'profile' })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
.send({ appId: 'someApp', redirectURI: 'http://foobar.com', scope: 'profile' })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails without appId', function (done) {
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
.query({ access_token: token })
.send({ redirectURI: 'http://foobar.com', scope: 'profile' })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
.query({ access_token: token })
.send({ redirectURI: 'http://foobar.com', scope: 'profile' })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with empty appId', function (done) {
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
.query({ access_token: token })
.send({ appId: '', redirectURI: 'http://foobar.com', scope: 'profile' })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
.query({ access_token: token })
.send({ appId: '', redirectURI: 'http://foobar.com', scope: 'profile' })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails without scope', function (done) {
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
.query({ access_token: token })
.send({ appId: 'someApp', redirectURI: 'http://foobar.com' })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
.query({ access_token: token })
.send({ appId: 'someApp', redirectURI: 'http://foobar.com' })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with empty scope', function (done) {
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
.query({ access_token: token })
.send({ appId: 'someApp', redirectURI: 'http://foobar.com', scope: '' })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
.query({ access_token: token })
.send({ appId: 'someApp', redirectURI: 'http://foobar.com', scope: '' })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails without redirectURI', function (done) {
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
.query({ access_token: token })
.send({ appId: 'someApp', scope: 'profile' })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
.query({ access_token: token })
.send({ appId: 'someApp', scope: 'profile' })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with empty redirectURI', function (done) {
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
.query({ access_token: token })
.send({ appId: 'someApp', redirectURI: '', scope: 'profile' })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
.query({ access_token: token })
.send({ appId: 'someApp', redirectURI: '', scope: 'profile' })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with malformed redirectURI', function (done) {
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
.query({ access_token: token })
.send({ appId: 'someApp', redirectURI: 'foobar', scope: 'profile' })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
.query({ access_token: token })
.send({ appId: 'someApp', redirectURI: 'foobar', scope: 'profile' })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with invalid name', function (done) {
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
.query({ access_token: token })
.send({ appId: '$"$%^45asdfasdfadf.adf.', redirectURI: 'http://foobar.com', scope: 'profile' })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
.query({ access_token: token })
.send({ appId: '$"$%^45asdfasdfadf.adf.', redirectURI: 'http://foobar.com', scope: 'profile' })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('succeeds with dash', function (done) {
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
.query({ access_token: token })
.send({ appId: 'fo-1234-bar', redirectURI: 'http://foobar.com', scope: 'profile' })
.end(function (error, result) {
expect(result.statusCode).to.equal(201);
done();
});
.query({ access_token: token })
.send({ appId: 'fo-1234-bar', redirectURI: 'http://foobar.com', scope: 'profile' })
.end(function (error, result) {
expect(result.statusCode).to.equal(201);
done();
});
});
it('succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
.query({ access_token: token })
.send({ appId: 'someApp', redirectURI: 'http://foobar.com', scope: 'profile' })
.end(function (error, result) {
expect(result.statusCode).to.equal(201);
expect(result.body.id).to.be.a('string');
expect(result.body.appId).to.be.a('string');
expect(result.body.redirectURI).to.be.a('string');
expect(result.body.clientSecret).to.be.a('string');
expect(result.body.scope).to.be.a('string');
expect(result.body.type).to.equal(clients.TYPE_EXTERNAL);
.query({ access_token: token })
.send({ appId: 'someApp', redirectURI: 'http://foobar.com', scope: 'profile' })
.end(function (error, result) {
expect(result.statusCode).to.equal(201);
expect(result.body.id).to.be.a('string');
expect(result.body.appId).to.be.a('string');
expect(result.body.redirectURI).to.be.a('string');
expect(result.body.clientSecret).to.be.a('string');
expect(result.body.scope).to.be.a('string');
expect(result.body.type).to.equal(clients.TYPE_EXTERNAL);
done();
});
done();
});
});
});
@@ -190,15 +198,15 @@ describe('OAuth Clients API', function () {
function (callback) {
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
.query({ access_token: token })
.send({ appId: CLIENT_0.appId, redirectURI: CLIENT_0.redirectURI, scope: CLIENT_0.scope })
.end(function (error, result) {
expect(result.statusCode).to.equal(201);
.query({ access_token: token })
.send({ appId: CLIENT_0.appId, redirectURI: CLIENT_0.redirectURI, scope: CLIENT_0.scope })
.end(function (error, result) {
expect(result.statusCode).to.equal(201);
CLIENT_0 = result.body;
CLIENT_0 = result.body;
callback();
});
callback();
});
}
], done);
});
@@ -207,30 +215,30 @@ describe('OAuth Clients API', function () {
it('fails without token', function (done) {
superagent.get(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails with unknown id', function (done) {
superagent.get(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id.toUpperCase())
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(404);
done();
});
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(404);
done();
});
});
it('succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body).to.eql(CLIENT_0);
done();
});
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body).to.eql(CLIENT_0);
done();
});
});
});
@@ -256,15 +264,15 @@ describe('OAuth Clients API', function () {
function (callback) {
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
.query({ access_token: token })
.send({ appId: CLIENT_0.appId, redirectURI: CLIENT_0.redirectURI, scope: CLIENT_0.scope })
.end(function (error, result) {
expect(result.statusCode).to.equal(201);
.query({ access_token: token })
.send({ appId: CLIENT_0.appId, redirectURI: CLIENT_0.redirectURI, scope: CLIENT_0.scope })
.end(function (error, result) {
expect(result.statusCode).to.equal(201);
CLIENT_0 = result.body;
CLIENT_0 = result.body;
callback();
});
callback();
});
}
], done);
});
@@ -273,52 +281,52 @@ describe('OAuth Clients API', function () {
it('fails without token', function (done) {
superagent.del(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails with unknown id', function (done) {
superagent.del(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id.toUpperCase())
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(404);
done();
});
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(404);
done();
});
});
it('succeeds', function (done) {
superagent.del(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(204);
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(204);
superagent.get(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
superagent.get(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(404);
expect(result.statusCode).to.equal(404);
done();
});
done();
});
});
});
it('fails for cid-webadmin', function (done) {
superagent.del(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(405);
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(405);
superagent.get(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin')
superagent.get(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.statusCode).to.equal(200);
done();
});
done();
});
});
});
it('fails for addon auth client', function (done) {
@@ -328,18 +336,18 @@ describe('OAuth Clients API', function () {
CLIENT_1.id = result.id;
superagent.del(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_1.id)
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(405);
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(405);
superagent.get(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_1.id)
superagent.get(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_1.id)
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.statusCode).to.equal(200);
done();
});
done();
});
});
});
});
});
@@ -370,15 +378,15 @@ describe('Clients', function () {
function (callback) {
superagent.get(SERVER_URL + '/api/v1/profile')
.query({ access_token: token })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(200);
.query({ access_token: token })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(200);
USER_0.id = result.body.id;
USER_0.id = result.body.id;
callback();
});
callback();
});
}
], done);
}
@@ -389,40 +397,40 @@ describe('Clients', function () {
it('fails due to missing token', function (done) {
superagent.get(SERVER_URL + '/api/v1/oauth/clients')
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails due to empty token', function (done) {
superagent.get(SERVER_URL + '/api/v1/oauth/clients')
.query({ access_token: '' })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
.query({ access_token: '' })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails due to wrong token', function (done) {
superagent.get(SERVER_URL + '/api/v1/oauth/clients')
.query({ access_token: token.toUpperCase() })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
.query({ access_token: token.toUpperCase() })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/oauth/clients')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.clients.length).to.eql(3);
expect(result.body.clients.length).to.eql(3);
done();
});
done();
});
});
});
@@ -432,50 +440,50 @@ describe('Clients', function () {
it('fails due to missing token', function (done) {
superagent.get(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin/tokens')
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails due to empty token', function (done) {
superagent.get(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin/tokens')
.query({ access_token: '' })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
.query({ access_token: '' })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails due to wrong token', function (done) {
superagent.get(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin/tokens')
.query({ access_token: token.toUpperCase() })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
.query({ access_token: token.toUpperCase() })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails due to unkown client', function (done) {
superagent.get(SERVER_URL + '/api/v1/oauth/clients/CID-WEBADMIN/tokens')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(404);
done();
});
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(404);
done();
});
});
it('succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin/tokens')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.tokens.length).to.eql(1);
expect(result.body.tokens[0].identifier).to.eql(USER_0.id);
expect(result.body.tokens.length).to.eql(1);
expect(result.body.tokens[0].identifier).to.eql(USER_0.id);
done();
});
done();
});
});
});
@@ -485,62 +493,62 @@ describe('Clients', function () {
it('fails due to missing token', function (done) {
superagent.del(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin/tokens')
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails due to empty token', function (done) {
superagent.del(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin/tokens')
.query({ access_token: '' })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
.query({ access_token: '' })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails due to wrong token', function (done) {
superagent.del(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin/tokens')
.query({ access_token: token.toUpperCase() })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
.query({ access_token: token.toUpperCase() })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails due to unkown client', function (done) {
superagent.del(SERVER_URL + '/api/v1/oauth/clients/CID-WEBADMIN/tokens')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(404);
done();
});
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(404);
done();
});
});
it('succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin/tokens')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.tokens.length).to.eql(1);
expect(result.body.tokens[0].identifier).to.eql(USER_0.id);
superagent.del(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin/tokens')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.statusCode).to.equal(204);
expect(result.body.tokens.length).to.eql(1);
expect(result.body.tokens[0].identifier).to.eql(USER_0.id);
superagent.del(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin/tokens')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(204);
// further calls with this token should not work
superagent.get(SERVER_URL + '/api/v1/profile')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
// further calls with this token should not work
superagent.get(SERVER_URL + '/api/v1/profile')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
});
});
});
});
+264 -4
View File
@@ -10,12 +10,13 @@ var async = require('async'),
database = require('../../database.js'),
expect = require('expect.js'),
http = require('http'),
locker = require('../../locker.js'),
nock = require('nock'),
os = require('os'),
superagent = require('superagent'),
server = require('../../server.js'),
settings = require('../../settings.js'),
settingsdb = require('../../settingsdb.js'),
shell = require('../../shell.js'),
tokendb = require('../../tokendb.js');
var SERVER_URL = 'http://localhost:' + config.get('port');
@@ -33,8 +34,7 @@ function setup(done) {
async.series([
server.start.bind(server),
database._clear,
settings.setBackupConfig.bind(null, { provider: 'filesystem', backupFolder: '/tmp', format: 'tgz' }),
settingsdb.set.bind(null, settings.APPSTORE_CONFIG_KEY, JSON.stringify({ userId: 'USER_ID', cloudronId: 'CLOUDRON_ID', token: 'ACCESS_TOKEN' }))
settings.setBackupConfig.bind(null, { provider: 'filesystem', backupFolder: '/tmp', format: 'tgz' })
], done);
}
@@ -48,6 +48,16 @@ function cleanup(done) {
});
}
var gSudoOriginal = null;
function injectShellMock() {
gSudoOriginal = shell.sudo;
shell.sudo = function (tag, options, callback) { callback(null); };
}
function restoreShellMock() {
shell.sudo = gSudoOriginal;
}
describe('Cloudron', function () {
describe('activate', function () {
@@ -64,72 +74,108 @@ describe('Cloudron', function () {
});
});
it('fails due to internal server error on appstore side', function (done) {
var scope = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(500, { message: 'this is wrong' });
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: 'someuser', password: 'strong#A3asdf', email: 'admin@foo.bar' })
.end(function (error, result) {
expect(result.statusCode).to.equal(500);
expect(scope.isDone()).to.be.ok();
done();
});
});
it('fails due to empty username', function (done) {
var scope = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: '', password: 'ADSFsdf$%436', email: 'admin@foo.bar' })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
expect(scope.isDone()).to.be.ok();
done();
});
});
it('fails due to empty password', function (done) {
var scope = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: 'someuser', password: '', email: 'admin@foo.bar' })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
expect(scope.isDone()).to.be.ok();
done();
});
});
it('fails due to empty email', function (done) {
var scope = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: 'someuser', password: 'ADSF#asd546', email: '' })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
expect(scope.isDone()).to.be.ok();
done();
});
});
it('fails due to wrong displayName type', function (done) {
var scope = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: 'someuser', password: 'ADSF?#asd546', email: 'admin@foo.bar', displayName: 1234 })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
expect(scope.isDone()).to.be.ok();
done();
});
});
it('fails due to invalid email', function (done) {
var scope = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: 'someuser', password: 'ADSF#asd546', email: 'invalidemail' })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
expect(scope.isDone()).to.be.ok();
done();
});
});
it('succeeds', function (done) {
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: 'someuser', password: 'ADSF#asd546', email: 'admin@foo.bar', displayName: 'tester' })
.end(function (error, result) {
expect(result.statusCode).to.equal(201);
expect(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
done();
});
});
it('fails the second time', function (done) {
var scope = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: 'someuser', password: 'ADSF#asd546', email: 'admin@foo.bar' })
.end(function (error, result) {
expect(result.statusCode).to.equal(409);
expect(scope.isDone()).to.be.ok();
done();
});
});
@@ -141,11 +187,16 @@ describe('Cloudron', function () {
setup,
function (callback) {
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
// stash token for further use
token = result.body.token;
@@ -182,13 +233,14 @@ describe('Cloudron', function () {
});
});
it('succeeds (admin)', function (done) {
it('succeeds without appstore', function (done) {
superagent.get(SERVER_URL + '/api/v1/cloudron/config')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.apiServerOrigin).to.eql('http://localhost:6060');
expect(result.body.webServerOrigin).to.eql(null);
expect(result.body.fqdn).to.eql(config.fqdn());
expect(result.body.adminFqdn).to.eql(config.adminFqdn());
expect(result.body.progress).to.be.an('object');
expect(result.body.update).to.be.an('object');
@@ -202,6 +254,34 @@ describe('Cloudron', function () {
});
});
it('succeeds (admin)', function (done) {
var scope = nock(config.apiServerOrigin())
.get(`/api/v1/boxes/${config.fqdn()}?token=${config.token()}`)
.reply(200, { box: { region: 'sfo', size: '1gb' }, user: { }});
superagent.get(SERVER_URL + '/api/v1/cloudron/config')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.apiServerOrigin).to.eql('http://localhost:6060');
expect(result.body.webServerOrigin).to.eql(null);
expect(result.body.fqdn).to.eql(config.fqdn());
expect(result.body.adminFqdn).to.eql(config.adminFqdn());
expect(result.body.progress).to.be.an('object');
expect(result.body.update).to.be.an('object');
expect(result.body.version).to.eql(config.version());
expect(result.body.size).to.eql('1gb');
expect(result.body.region).to.eql('sfo');
expect(result.body.memory).to.eql(os.totalmem());
expect(result.body.cloudronName).to.be.a('string');
expect(result.body.provider).to.be.a('string');
expect(scope.isDone()).to.be.ok();
done();
});
});
it('succeeds (non-admin)', function (done) {
superagent.get(SERVER_URL + '/api/v1/cloudron/config')
.query({ access_token: token_1 })
@@ -210,6 +290,7 @@ describe('Cloudron', function () {
expect(result.body.apiServerOrigin).to.eql('http://localhost:6060');
expect(result.body.webServerOrigin).to.eql(null);
expect(result.body.fqdn).to.eql(config.fqdn());
expect(result.body.adminFqdn).to.eql(config.adminFqdn());
expect(result.body.progress).to.be.an('object');
expect(result.body.version).to.eql(config.version());
@@ -224,6 +305,171 @@ describe('Cloudron', function () {
done();
});
});
});
xdescribe('migrate', function () {
before(function (done) {
async.series([
setup,
function (callback) {
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
// stash token for further use
token = result.body.token;
callback();
});
}
], done);
});
after(function (done) {
locker.unlock(locker._operation); // migrate never unlocks
cleanup(done);
});
it('fails without token', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 'small', region: 'sfo'})
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails without password', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 'small', region: 'sfo'})
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('succeeds without size', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ region: 'sfo', password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(202);
done();
});
});
it('fails with wrong size type', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 4, region: 'sfo', password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('succeeds without region', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 'small', password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(202);
done();
});
});
it('fails with wrong region type', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 'small', region: 4, password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails when in wrong state', function (done) {
var scope2 = nock(config.apiServerOrigin())
.post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=BACKUP_TOKEN')
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } });
var scope3 = nock(config.apiServerOrigin())
.post('/api/v1/boxes/' + config.fqdn() + '/backupDone?token=APPSTORE_TOKEN', function (body) {
return body.boxVersion && body.restoreKey && !body.appId && !body.appVersion && body.appBackupIds.length === 0;
})
.reply(200, { id: 'someid' });
var scope1 = nock(config.apiServerOrigin())
.post('/api/v1/boxes/' + config.fqdn() + '/migrate?token=APPSTORE_TOKEN', function (body) {
return body.size && body.region && body.restoreKey;
}).reply(409, {});
injectShellMock();
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 'small', region: 'sfo', password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(202);
function checkAppstoreServerCalled() {
if (scope1.isDone() && scope2.isDone() && scope3.isDone()) {
restoreShellMock();
return done();
}
setTimeout(checkAppstoreServerCalled, 100);
}
checkAppstoreServerCalled();
});
});
it('succeeds', function (done) {
var scope1 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/migrate?token=APPSTORE_TOKEN', function (body) {
return body.size && body.region && body.restoreKey;
}).reply(202, {});
var scope2 = nock(config.apiServerOrigin())
.post('/api/v1/boxes/' + config.fqdn() + '/backupDone?token=APPSTORE_TOKEN', function (body) {
return body.boxVersion && body.restoreKey && !body.appId && !body.appVersion && body.appBackupIds.length === 0;
})
.reply(200, { id: 'someid' });
var scope3 = nock(config.apiServerOrigin())
.post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=BACKUP_TOKEN')
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } });
injectShellMock();
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 'small', region: 'sfo', password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(202);
function checkAppstoreServerCalled() {
if (scope1.isDone() && scope2.isDone() && scope3.isDone()) {
restoreShellMock();
return done();
}
setTimeout(checkAppstoreServerCalled, 100);
}
checkAppstoreServerCalled();
});
});
});
describe('feedback', function () {
@@ -232,11 +478,16 @@ describe('Cloudron', function () {
setup,
function (callback) {
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
// stash token for further use
token = result.body.token;
@@ -329,6 +580,7 @@ describe('Cloudron', function () {
});
it('succeeds with ticket type', function (done) {
var scope1 = nock(config.apiServerOrigin()).post('/api/v1/exchangeBoxTokenWithUserToken?token=APPSTORE_TOKEN').reply(201, { userId: 'USER_ID', cloudronId: 'CLOUDRON_ID', token: 'ACCESS_TOKEN' });
var scope2 = nock(config.apiServerOrigin())
.filteringRequestBody(function (/* unusedBody */) { return ''; }) // strip out body
.post('/api/v1/users/USER_ID/cloudrons/CLOUDRON_ID/feedback?accessToken=ACCESS_TOKEN')
@@ -339,12 +591,14 @@ describe('Cloudron', function () {
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(201);
expect(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
done();
});
});
it('succeeds with app type', function (done) {
var scope1 = nock(config.apiServerOrigin()).post('/api/v1/exchangeBoxTokenWithUserToken?token=APPSTORE_TOKEN').reply(201, { userId: 'USER_ID', cloudronId: 'CLOUDRON_ID', token: 'ACCESS_TOKEN' });
var scope2 = nock(config.apiServerOrigin())
.filteringRequestBody(function (/* unusedBody */) { return ''; }) // strip out body
.post('/api/v1/users/USER_ID/cloudrons/CLOUDRON_ID/feedback?accessToken=ACCESS_TOKEN')
@@ -355,6 +609,7 @@ describe('Cloudron', function () {
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(201);
expect(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
done();
});
@@ -367,11 +622,16 @@ describe('Cloudron', function () {
setup,
function (callback) {
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
// stash token for further use
token = result.body.token;
+106 -89
View File
@@ -10,13 +10,17 @@ var async = require('async'),
config = require('../../config.js'),
database = require('../../database.js'),
expect = require('expect.js'),
nock = require('nock'),
superagent = require('superagent'),
server = require('../../server.js');
server = require('../../server.js'),
settings = require('../../settings.js');
var SERVER_URL = 'http://localhost:' + config.get('port');
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var token = null; // authentication token
var server;
function setup(done) {
config._reset();
config.setFqdn('example-developer-test.com');
@@ -41,14 +45,22 @@ describe('Developer API', function () {
async.series([
setup,
function (callback) {
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
callback();
});
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
// stash token for further use
token = result.body.token;
callback();
});
},
], done);
});
@@ -57,117 +69,117 @@ describe('Developer API', function () {
it('fails without body', function (done) {
superagent.post(SERVER_URL + '/api/v1/developer/login')
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails without username', function (done) {
superagent.post(SERVER_URL + '/api/v1/developer/login')
.send({ password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
.send({ password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails without password', function (done) {
superagent.post(SERVER_URL + '/api/v1/developer/login')
.send({ username: USERNAME })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
.send({ username: USERNAME })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails with empty username', function (done) {
superagent.post(SERVER_URL + '/api/v1/developer/login')
.send({ username: '', password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
.send({ username: '', password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails with empty password', function (done) {
superagent.post(SERVER_URL + '/api/v1/developer/login')
.send({ username: USERNAME, password: '' })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
.send({ username: USERNAME, password: '' })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails with unknown username', function (done) {
superagent.post(SERVER_URL + '/api/v1/developer/login')
.send({ username: USERNAME + USERNAME, password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
.send({ username: USERNAME + USERNAME, password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails with unknown email', function (done) {
superagent.post(SERVER_URL + '/api/v1/developer/login')
.send({ username: USERNAME + EMAIL, password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
.send({ username: USERNAME + EMAIL, password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails with wrong password', function (done) {
superagent.post(SERVER_URL + '/api/v1/developer/login')
.send({ username: USERNAME, password: PASSWORD.toUpperCase() })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
.send({ username: USERNAME, password: PASSWORD.toUpperCase() })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('with username succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/developer/login')
.send({ username: USERNAME, password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(new Date(result.body.expiresAt).toString()).to.not.be('Invalid Date');
expect(result.body.token).to.be.a('string');
done();
});
.send({ username: USERNAME, password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(new Date(result.body.expiresAt).toString()).to.not.be('Invalid Date');
expect(result.body.token).to.be.a('string');
done();
});
});
it('with uppercase username succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/developer/login')
.send({ username: USERNAME.toUpperCase(), password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(new Date(result.body.expiresAt).toString()).to.not.be('Invalid Date');
expect(result.body.token).to.be.a('string');
done();
});
.send({ username: USERNAME.toUpperCase(), password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(new Date(result.body.expiresAt).toString()).to.not.be('Invalid Date');
expect(result.body.token).to.be.a('string');
done();
});
});
it('with email succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/developer/login')
.send({ username: EMAIL, password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(new Date(result.body.expiresAt).toString()).to.not.be('Invalid Date');
expect(result.body.token).to.be.a('string');
done();
});
.send({ username: EMAIL, password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(new Date(result.body.expiresAt).toString()).to.not.be('Invalid Date');
expect(result.body.token).to.be.a('string');
done();
});
});
it('with uppercase email succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/developer/login')
.send({ username: EMAIL.toUpperCase(), password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(new Date(result.body.expiresAt).toString()).to.not.be('Invalid Date');
expect(result.body.token).to.be.a('string');
done();
});
.send({ username: EMAIL.toUpperCase(), password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(new Date(result.body.expiresAt).toString()).to.not.be('Invalid Date');
expect(result.body.token).to.be.a('string');
done();
});
});
});
@@ -178,26 +190,31 @@ describe('Developer API', function () {
async.series([
setup,
function (callback) {
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
token_normal = result.body.token;
token_normal = result.body.token;
superagent.post(SERVER_URL + '/api/v1/developer/login')
.send({ username: USERNAME, password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(new Date(result.body.expiresAt).toString()).to.not.be('Invalid Date');
expect(result.body.token).to.be.a('string');
superagent.post(SERVER_URL + '/api/v1/developer/login')
.send({ username: USERNAME, password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(new Date(result.body.expiresAt).toString()).to.not.be('Invalid Date');
expect(result.body.token).to.be.a('string');
token_sdk = result.body.token;
token_sdk = result.body.token;
callback();
});
callback();
});
});
},
], done);
});
+112 -125
View File
@@ -9,6 +9,7 @@ var async = require('async'),
config = require('../../config.js'),
database = require('../../database.js'),
expect = require('expect.js'),
nock = require('nock'),
superagent = require('superagent'),
server = require('../../server.js');
@@ -16,25 +17,18 @@ var SERVER_URL = 'http://localhost:' + config.get('port');
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var token = null;
var DOMAIN = 'example-domains-test.com';
var DOMAIN_0 = {
domain: 'cloudron.com',
zoneName: 'cloudron.com',
provider: 'noop',
config: { },
tlsConfig: {
provider: 'fallback'
}
config: { }
};
var DOMAIN_1 = {
domain: 'foobar.com',
provider: 'noop',
config: { },
tlsConfig: {
provider: 'fallback'
}
config: { }
};
describe('Domains API', function () {
@@ -42,7 +36,8 @@ describe('Domains API', function () {
before(function (done) {
config._reset();
config.setFqdn(DOMAIN);
config.set('provider', 'digitalocean');
config.setFqdn('example-domains-test.com');
async.series([
server.start.bind(null),
@@ -50,17 +45,17 @@ describe('Domains API', function () {
function (callback) {
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.equal(201);
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.equal(201);
// stash token for further use
token = result.body.token;
// stash token for further use
token = result.body.token;
callback();
});
callback();
});
},
], done);
});
@@ -75,181 +70,173 @@ describe('Domains API', function () {
describe('add', function () {
it('fails with missing domain', function (done) {
superagent.post(SERVER_URL + '/api/v1/domains')
.query({ access_token: token })
.send({})
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
.query({ access_token: token })
.send({})
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
done();
});
});
it('fails with invalid domain', function (done) {
superagent.post(SERVER_URL + '/api/v1/domains')
.query({ access_token: token })
.send({ domain: 'abc' })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
.query({ access_token: token })
.send({ domain: 'abc' })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
done();
});
});
it('fails with unknown provider', function (done) {
var scope = nock(config.apiServerOrigin()).get('/api/v1/helper/public_ip').reply(200, { ip: '127.0.0.1' });
superagent.post(SERVER_URL + '/api/v1/domains')
.query({ access_token: token })
.send({ domain: 'cloudron.com', provider: 'doesnotexist', config: { }})
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
.query({ access_token: token })
.send({ domain: 'cloudron.com', provider: 'doesnotexist', config: { }})
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
expect(scope.isDone()).to.be.ok();
done();
});
});
it('fails with invalid tlsConfig', function (done) {
superagent.post(SERVER_URL + '/api/v1/domains')
.query({ access_token: token })
.send({ domain: 'cloudron.com', provider: 'noop', config: { }, tlsConfig: 'foobar' })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with unknown tls provider', function (done) {
superagent.post(SERVER_URL + '/api/v1/domains')
.query({ access_token: token })
.send({ domain: 'cloudron.com', provider: 'noop', config: { }, tlsConfig: { provider: 'hello' }})
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
done();
});
});
it('succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/domains')
.query({ access_token: token })
.send(DOMAIN_0)
.end(function (error, result) {
expect(result.statusCode).to.equal(201);
var scope = nock(config.apiServerOrigin()).get('/api/v1/helper/public_ip').reply(200, { ip: '127.0.0.1' });
done();
});
superagent.post(SERVER_URL + '/api/v1/domains')
.query({ access_token: token })
.send(DOMAIN_0)
.end(function (error, result) {
expect(result.statusCode).to.equal(201);
expect(scope.isDone()).to.be.ok();
done();
});
});
it('succeeds for second domain without zoneName', function (done) {
superagent.post(SERVER_URL + '/api/v1/domains')
.query({ access_token: token })
.send(DOMAIN_1)
.end(function (error, result) {
expect(result.statusCode).to.equal(201);
var scope = nock(config.apiServerOrigin()).get('/api/v1/helper/public_ip').reply(200, { ip: '127.0.0.1' });
done();
});
superagent.post(SERVER_URL + '/api/v1/domains')
.query({ access_token: token })
.send(DOMAIN_1)
.end(function (error, result) {
expect(result.statusCode).to.equal(201);
expect(scope.isDone()).to.be.ok();
done();
});
});
it('fails for already added domain', function (done) {
superagent.post(SERVER_URL + '/api/v1/domains')
.query({ access_token: token })
.send(DOMAIN_0)
.end(function (error, result) {
expect(result.statusCode).to.equal(409);
var scope = nock(config.apiServerOrigin()).get('/api/v1/helper/public_ip').reply(200, { ip: '127.0.0.1' });
done();
});
superagent.post(SERVER_URL + '/api/v1/domains')
.query({ access_token: token })
.send(DOMAIN_0)
.end(function (error, result) {
expect(result.statusCode).to.equal(409);
expect(scope.isDone()).to.be.ok();
done();
});
});
});
describe('list', function () {
it('succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/domains')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.domains).to.be.an(Array);
expect(result.body.domains.length).to.equal(2);
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.domains).to.be.an(Array);
// includes currently the implicitly added config.fqdn()
expect(result.body.domains.length).to.equal(3);
expect(result.body.domains[0].domain).to.equal(DOMAIN_0.domain);
expect(result.body.domains[1].domain).to.equal(DOMAIN_1.domain);
expect(result.body.domains[0].domain).to.equal(DOMAIN_0.domain);
expect(result.body.domains[1].domain).to.equal(config.fqdn());
expect(result.body.domains[2].domain).to.equal(DOMAIN_1.domain);
done();
});
done();
});
});
});
describe('get', function () {
it('fails for non-existing domain', function (done) {
superagent.get(SERVER_URL + '/api/v1/domains/' + DOMAIN_0.domain + DOMAIN_0.domain)
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(404);
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(404);
done();
});
done();
});
});
it('succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/domains/' + DOMAIN_0.domain)
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.domain).to.equal(DOMAIN_0.domain);
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.domain).to.equal(DOMAIN_0.domain);
done();
});
done();
});
});
});
describe('delete', function () {
it('fails without password', function (done) {
superagent.delete(SERVER_URL + '/api/v1/domains/' + DOMAIN_0.domain + DOMAIN_0.domain)
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
done();
});
});
it('fails with wrong password', function (done) {
superagent.delete(SERVER_URL + '/api/v1/domains/' + DOMAIN_0.domain + DOMAIN_0.domain)
.query({ access_token: token })
.send({ password: PASSWORD + PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(403);
.query({ access_token: token })
.send({ password: PASSWORD + PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(403);
done();
});
done();
});
});
it('fails for non-existing domain', function (done) {
superagent.delete(SERVER_URL + '/api/v1/domains/' + DOMAIN_0.domain + DOMAIN_0.domain)
.query({ access_token: token })
.send({ password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(404);
.query({ access_token: token })
.send({ password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(404);
done();
});
done();
});
});
it('succeeds', function (done) {
superagent.delete(SERVER_URL + '/api/v1/domains/' + DOMAIN_0.domain)
.query({ access_token: token })
.send({ password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(204);
.query({ access_token: token })
.send({ password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(204);
superagent.get(SERVER_URL + '/api/v1/domains/' + DOMAIN_0.domain)
superagent.get(SERVER_URL + '/api/v1/domains/' + DOMAIN_0.domain)
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(404);
expect(result.statusCode).to.equal(404);
done();
});
done();
});
});
});
});
});
+51 -50
View File
@@ -23,6 +23,7 @@ var USER_1_ID = null, token_1;
function setup(done) {
config._reset();
config.set('provider', 'notcaas');
config.setFqdn('example-eventlog-test.com');
async.series([
@@ -32,30 +33,30 @@ function setup(done) {
function createAdmin(callback) {
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(201);
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(201);
// stash token for further use
token = result.body.token;
// stash token for further use
token = result.body.token;
callback();
});
callback();
});
},
function (callback) {
superagent.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
.send({ username: 'nonadmin', email: 'notadmin@server.test', invite: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
.query({ access_token: token })
.send({ username: 'nonadmin', email: 'notadmin@server.test', invite: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
USER_1_ID = res.body.id;
USER_1_ID = res.body.id;
callback(null);
});
callback(null);
});
},
function (callback) {
@@ -83,65 +84,65 @@ describe('Eventlog API', function () {
describe('get', function () {
it('fails due to wrong token', function (done) {
superagent.get(SERVER_URL + '/api/v1/cloudron/eventlog')
.query({ access_token: token.toUpperCase() })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
.query({ access_token: token.toUpperCase() })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails for non-admin', function (done) {
superagent.get(SERVER_URL + '/api/v1/cloudron/eventlog')
.query({ access_token: token_1, page: 1, per_page: 10 })
.end(function (error, result) {
expect(result.statusCode).to.equal(403);
.query({ access_token: token_1, page: 1, per_page: 10 })
.end(function (error, result) {
expect(result.statusCode).to.equal(403);
done();
});
done();
});
});
it('succeeds for admin', function (done) {
superagent.get(SERVER_URL + '/api/v1/cloudron/eventlog')
.query({ access_token: token, page: 1, per_page: 10 })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.eventlogs.length >= 2).to.be.ok(); // activate, user.add
.query({ access_token: token, page: 1, per_page: 10 })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.eventlogs.length >= 2).to.be.ok(); // activate, user.add
done();
});
done();
});
});
it('succeeds with action', function (done) {
superagent.get(SERVER_URL + '/api/v1/cloudron/eventlog')
.query({ access_token: token, page: 1, per_page: 10, action: 'cloudron.activate' })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.eventlogs.length).to.equal(1);
.query({ access_token: token, page: 1, per_page: 10, action: 'cloudron.activate' })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.eventlogs.length).to.equal(1);
done();
});
done();
});
});
it('succeeds with search', function (done) {
superagent.get(SERVER_URL + '/api/v1/cloudron/eventlog')
.query({ access_token: token, page: 1, per_page: 10, search: EMAIL })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.eventlogs.length).to.equal(1);
.query({ access_token: token, page: 1, per_page: 10, search: EMAIL })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.eventlogs.length).to.equal(1);
done();
});
done();
});
});
it('succeeds with search', function (done) {
superagent.get(SERVER_URL + '/api/v1/cloudron/eventlog')
.query({ access_token: token, page: 1, per_page: 10, search: EMAIL, action: 'cloudron.activate' })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.eventlogs.length).to.equal(0);
.query({ access_token: token, page: 1, per_page: 10, search: EMAIL, action: 'cloudron.activate' })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.eventlogs.length).to.equal(0);
done();
});
done();
});
});
});
});
+149 -141
View File
@@ -13,7 +13,8 @@ var async = require('async'),
groups = require('../../groups.js'),
superagent = require('superagent'),
server = require('../../server.js'),
tokendb = require('../../tokendb.js');
tokendb = require('../../tokendb.js'),
nock = require('nock');
var SERVER_URL = 'http://localhost:' + config.get('port');
@@ -25,8 +26,10 @@ var userId, userId_1 = null;
var GROUP_NAME = 'externals';
var groupObject;
var server;
function setup(done) {
config._reset();
config.set('provider', 'caas');
config.setFqdn('example-groups-test.com');
async.series([
@@ -35,44 +38,49 @@ function setup(done) {
database._clear,
function createAdmin(callback) {
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(201);
expect(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
// stash token for further use
token = result.body.token;
superagent.get(SERVER_URL + '/api/v1/profile')
.query({ access_token: token })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(201);
expect(result.statusCode).to.eql(200);
// stash token for further use
token = result.body.token;
userId = result.body.id;
superagent.get(SERVER_URL + '/api/v1/profile')
.query({ access_token: token })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(200);
userId = result.body.id;
callback();
});
callback();
});
});
},
function (callback) {
superagent.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
.send({ username: USERNAME_1, email: EMAIL_1, invite: false })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(201);
.query({ access_token: token })
.send({ username: USERNAME_1, email: EMAIL_1, invite: false })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(201);
token_1 = tokendb.generateToken();
userId_1 = result.body.id;
token_1 = tokendb.generateToken();
userId_1 = result.body.id;
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
tokendb.add(token_1, userId_1, 'test-client-id', Date.now() + 100000, '*', callback);
});
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
tokendb.add(token_1, userId_1, 'test-client-id', Date.now() + 100000, '*', callback);
});
}
], done);
], done);
}
function cleanup(done) {
@@ -90,203 +98,203 @@ describe('Groups API', function () {
describe('list', function () {
it('cannot get groups without token', function (done) {
superagent.get(SERVER_URL + '/api/v1/groups')
.end(function (err, res) {
expect(res.statusCode).to.equal(401);
done();
});
.end(function (err, res) {
expect(res.statusCode).to.equal(401);
done();
});
});
it('cannot get groups as normal user', function (done) {
superagent.get(SERVER_URL + '/api/v1/groups')
.query({ access_token: token_1 })
.end(function (err, res) {
expect(res.statusCode).to.equal(403);
done();
});
.query({ access_token: token_1 })
.end(function (err, res) {
expect(res.statusCode).to.equal(403);
done();
});
});
it('can get groups', function (done) {
superagent.get(SERVER_URL + '/api/v1/groups')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.groups).to.be.an(Array);
expect(res.body.groups.length).to.be(1);
expect(res.body.groups[0].name).to.eql('admin');
expect(res.body.groups[0].userIds).to.be.an(Array);
expect(res.body.groups[0].userIds.length).to.be(1);
expect(res.body.groups[0].userIds[0]).to.be(userId);
done();
});
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.groups).to.be.an(Array);
expect(res.body.groups.length).to.be(1);
expect(res.body.groups[0].name).to.eql('admin');
expect(res.body.groups[0].userIds).to.be.an(Array);
expect(res.body.groups[0].userIds.length).to.be(1);
expect(res.body.groups[0].userIds[0]).to.be(userId);
done();
});
});
});
describe('create', function () {
it('fails due to mising token', function (done) {
superagent.post(SERVER_URL + '/api/v1/groups')
.send({ name: GROUP_NAME})
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
.send({ name: GROUP_NAME})
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/groups')
.query({ access_token: token })
.send({ name: GROUP_NAME})
.end(function (error, result) {
expect(result.statusCode).to.equal(201);
groupObject = result.body;
done();
});
.query({ access_token: token })
.send({ name: GROUP_NAME})
.end(function (error, result) {
expect(result.statusCode).to.equal(201);
groupObject = result.body;
done();
});
});
it('fails for already exists', function (done) {
superagent.post(SERVER_URL + '/api/v1/groups')
.query({ access_token: token })
.send({ name: GROUP_NAME})
.end(function (error, result) {
expect(result.statusCode).to.equal(409);
done();
});
.query({ access_token: token })
.send({ name: GROUP_NAME})
.end(function (error, result) {
expect(result.statusCode).to.equal(409);
done();
});
});
});
describe('get', function () {
it('cannot get non-existing group', function (done) {
superagent.get(SERVER_URL + '/api/v1/groups/nope')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(404);
done();
});
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(404);
done();
});
});
it('cannot get existing group with normal user', function (done) {
superagent.get(SERVER_URL + '/api/v1/groups/admin')
.query({ access_token: token_1 })
.end(function (error, result) {
expect(result.statusCode).to.equal(403);
done();
});
.query({ access_token: token_1 })
.end(function (error, result) {
expect(result.statusCode).to.equal(403);
done();
});
});
it('can get existing group', function (done) {
superagent.get(SERVER_URL + '/api/v1/groups/admin')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.name).to.be('admin');
expect(result.body.userIds.length).to.be(1);
expect(result.body.userIds[0]).to.be(userId);
done();
});
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.name).to.be('admin');
expect(result.body.userIds.length).to.be(1);
expect(result.body.userIds[0]).to.be(userId);
done();
});
});
});
describe('remove', function () {
it('cannot remove without token', function (done) {
superagent.del(SERVER_URL + '/api/v1/groups/externals')
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('can remove empty group', function (done) {
superagent.del(SERVER_URL + '/api/v1/groups/' + groupObject.id)
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(204);
done();
});
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(204);
done();
});
});
it('cannot remove non-empty group', function (done) {
superagent.del(SERVER_URL + '/api/v1/groups/admin')
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(409);
done();
});
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(409);
done();
});
});
});
describe('Set groups', function () {
var group0Object, group1Object;
var group0Object, group1Object;
before(function (done) {
groups.create('group0', function (e, r) {
group0Object = r;
groups.create('group1', function (e, r) {
group1Object = r;
done();
group1Object = r;
done();
});
});
});
it('cannot add user to invalid group', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + userId + '/groups')
.query({ access_token: token })
.send({ groupIds: [ 'admin', 'something' ]})
.end(function (error, result) {
expect(result.statusCode).to.equal(404);
done();
});
.query({ access_token: token })
.send({ groupIds: [ 'admin', 'something' ]})
.end(function (error, result) {
expect(result.statusCode).to.equal(404);
done();
});
});
it('can add user to valid group', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + userId + '/groups')
.query({ access_token: token })
.send({ groupIds: [ 'admin', group0Object.id, group1Object.id ]})
.end(function (error, result) {
expect(result.statusCode).to.equal(204);
done();
});
.query({ access_token: token })
.send({ groupIds: [ 'admin', group0Object.id, group1Object.id ]})
.end(function (error, result) {
expect(result.statusCode).to.equal(204);
done();
});
});
it('cannot remove self from admin', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + userId + '/groups')
.query({ access_token: token })
.send({ groupIds: [ group0Object.id, group1Object.id ]})
.end(function (error, result) {
expect(result.statusCode).to.equal(403); // not allowed
done();
});
.query({ access_token: token })
.send({ groupIds: [ group0Object.id, group1Object.id ]})
.end(function (error, result) {
expect(result.statusCode).to.equal(403); // not allowed
done();
});
});
it('can add another user to admin', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + userId_1 + '/groups')
.query({ access_token: token })
.send({ groupIds: [ 'admin' ]})
.end(function (error, result) {
expect(result.statusCode).to.equal(204);
done();
});
.query({ access_token: token })
.send({ groupIds: [ 'admin' ]})
.end(function (error, result) {
expect(result.statusCode).to.equal(204);
done();
});
});
it('lists members of admin group', function (done) {
superagent.get(SERVER_URL + '/api/v1/groups/admin')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.userIds.length).to.be(2);
expect(result.body.userIds[0]).to.be(userId);
expect(result.body.userIds[1]).to.be(userId_1);
done();
});
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.userIds.length).to.be(2);
expect(result.body.userIds[0]).to.be(userId);
expect(result.body.userIds[1]).to.be(userId_1);
done();
});
});
it('remove activation user from admin', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + userId + '/groups')
.query({ access_token: token_1 })
.send({ groupIds: [ group1Object.id, group0Object.id ]})
.end(function (error, result) {
expect(result.statusCode).to.equal(204); // user_1 is still admin, so we can remove the other person
done();
});
.query({ access_token: token_1 })
.send({ groupIds: [ group1Object.id, group0Object.id ]})
.end(function (error, result) {
expect(result.statusCode).to.equal(204); // user_1 is still admin, so we can remove the other person
done();
});
});
});
});
File diff suppressed because it is too large Load Diff
+4 -16
View File
@@ -17,7 +17,6 @@ var expect = require('expect.js'),
server = require('../../server.js'),
querystring = require('querystring'),
database = require('../../database.js'),
domains = require('../../domains.js'),
clientdb = require('../../clientdb.js'),
clients = require('../../clients.js'),
userdb = require('../../userdb.js'),
@@ -138,15 +137,6 @@ describe('OAuth2', function () {
});
describe('flow', function () {
const DOMAIN_0 = {
domain: 'example.com',
zoneName: 'example.com',
config: {},
provider: 'manual',
fallbackCertificate: null,
tlsConfig: { provider: 'fallback' }
};
var USER_0 = {
id: uuid.v4(),
username: 'someUSERname',
@@ -164,7 +154,7 @@ describe('OAuth2', function () {
appStoreId: '',
manifest: { version: '0.1.0', addons: { } },
location: 'test',
domain: DOMAIN_0.domain,
domain: 'example.com',
portBindings: {},
accessRestriction: null,
memoryLimit: 0,
@@ -176,7 +166,7 @@ describe('OAuth2', function () {
appStoreId: '',
manifest: { version: '0.1.0', addons: { } },
location: 'test1',
domain: DOMAIN_0.domain,
domain: 'example.com',
portBindings: {},
accessRestriction: { users: [ 'foobar' ] },
memoryLimit: 0,
@@ -188,7 +178,7 @@ describe('OAuth2', function () {
appStoreId: '',
manifest: { version: '0.1.0', addons: { } },
location: 'test2',
domain: DOMAIN_0.domain,
domain: 'example.com',
portBindings: {},
accessRestriction: { users: [ USER_0.id ] },
memoryLimit: 0,
@@ -200,7 +190,7 @@ describe('OAuth2', function () {
appStoreId: '',
manifest: { version: '0.1.0', addons: { } },
location: 'test3',
domain: DOMAIN_0.domain,
domain: 'example.com',
portBindings: {},
accessRestriction: { groups: [ 'someothergroup', 'admin', 'anothergroup' ] },
memoryLimit: 0,
@@ -311,7 +301,6 @@ describe('OAuth2', function () {
async.series([
server.start,
database._clear,
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0.zoneName, DOMAIN_0.provider, DOMAIN_0.config, DOMAIN_0.fallbackCertificate, DOMAIN_0.tlsConfig),
clientdb.add.bind(null, CLIENT_0.id, CLIENT_0.appId, CLIENT_0.type, CLIENT_0.clientSecret, CLIENT_0.redirectURI, CLIENT_0.scope),
clientdb.add.bind(null, CLIENT_1.id, CLIENT_1.appId, CLIENT_1.type, CLIENT_1.clientSecret, CLIENT_1.redirectURI, CLIENT_1.scope),
clientdb.add.bind(null, CLIENT_2.id, CLIENT_2.appId, CLIENT_2.type, CLIENT_2.clientSecret, CLIENT_2.redirectURI, CLIENT_2.scope),
@@ -1257,7 +1246,6 @@ describe('Password', function () {
username: 'someusername',
password: 'passWord%1234',
email: 'some@email.com',
fallbackEmail: 'somefallback@email.com',
admin: true,
salt: 'somesalt',
createdAt: (new Date()).toUTCString(),
+93 -93
View File
@@ -12,16 +12,12 @@ var config = require('../../config.js'),
expect = require('expect.js'),
mailer = require('../../mailer.js'),
superagent = require('superagent'),
nock = require('nock'),
server = require('../../server.js');
const SERVER_URL = 'http://localhost:' + config.get('port');
var SERVER_URL = 'http://localhost:' + config.get('port');
const USERNAME_0 = 'superaDmIn';
const PASSWORD = 'Foobar?1337';
const EMAIL_0 = 'silLY@me.com';
const EMAIL_0_NEW = 'stupID@me.com';
const EMAIL_0_NEW_FALLBACK = 'stupIDfallback@me.com';
const DISPLAY_NAME_0_NEW = 'New Name';
var USERNAME_0 = 'superaDmIn', PASSWORD = 'Foobar?1337', EMAIL_0 = 'silLY@me.com', EMAIL_0_NEW = 'stupID@me.com', DISPLAY_NAME_0_NEW = 'New Name';
describe('Profile API', function () {
this.timeout(5000);
@@ -41,18 +37,24 @@ describe('Profile API', function () {
database._clear(function (error) {
expect(error).to.eql(null);
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME_0, password: PASSWORD, email: EMAIL_0 })
.end(function (err, res) {
expect(err).to.eql(null);
expect(res.statusCode).to.equal(201);
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME_0, password: PASSWORD, email: EMAIL_0 })
.end(function (err, res) {
expect(err).to.eql(null);
expect(res.statusCode).to.equal(201);
// stash for later use
token_0 = res.body.token;
// stash for later use
token_0 = res.body.token;
done();
});
expect(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
done();
});
});
});
}
@@ -100,7 +102,6 @@ describe('Profile API', function () {
expect(result.statusCode).to.equal(200);
expect(result.body.username).to.equal(USERNAME_0.toLowerCase());
expect(result.body.email).to.equal(EMAIL_0.toLowerCase());
expect(result.body.fallbackEmail).to.equal(EMAIL_0.toLowerCase());
expect(result.body.admin).to.be.ok();
expect(result.body.displayName).to.be.a('string');
expect(result.body.password).to.not.be.ok();
@@ -154,74 +155,73 @@ describe('Profile API', function () {
it('change email fails due to missing token', function (done) {
superagent.post(SERVER_URL + '/api/v1/profile')
.send({ email: EMAIL_0_NEW })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
.send({ email: EMAIL_0_NEW })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('change email fails due to invalid email', function (done) {
superagent.post(SERVER_URL + '/api/v1/profile')
.query({ access_token: token_0 })
.send({ email: 'foo@bar' })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
.query({ access_token: token_0 })
.send({ email: 'foo@bar' })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('change user succeeds without email nor displayName', function (done) {
superagent.post(SERVER_URL + '/api/v1/profile')
.query({ access_token: token_0 })
.send({})
.end(function (error, result) {
expect(result.statusCode).to.equal(204);
done();
});
.query({ access_token: token_0 })
.send({})
.end(function (error, result) {
expect(result.statusCode).to.equal(204);
done();
});
});
it('change email succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/profile')
.query({ access_token: token_0 })
.send({ email: EMAIL_0_NEW, fallbackEmail: EMAIL_0_NEW_FALLBACK })
.end(function (error, result) {
expect(result.statusCode).to.equal(204);
.query({ access_token: token_0 })
.send({ email: EMAIL_0_NEW })
.end(function (error, result) {
expect(result.statusCode).to.equal(204);
superagent.get(SERVER_URL + '/api/v1/profile')
.query({ access_token: token_0 })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.username).to.equal(USERNAME_0.toLowerCase());
expect(res.body.email).to.equal(EMAIL_0_NEW.toLowerCase());
expect(res.body.fallbackEmail).to.equal(EMAIL_0_NEW_FALLBACK.toLowerCase());
expect(res.body.admin).to.equal(true);
expect(res.body.displayName).to.equal('');
superagent.get(SERVER_URL + '/api/v1/profile')
.query({ access_token: token_0 })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.username).to.equal(USERNAME_0.toLowerCase());
expect(res.body.email).to.equal(EMAIL_0_NEW.toLowerCase());
expect(res.body.admin).to.equal(true);
expect(res.body.displayName).to.equal('');
done();
});
done();
});
});
});
it('change displayName succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/profile')
.query({ access_token: token_0 })
.send({ displayName: DISPLAY_NAME_0_NEW })
.end(function (error, result) {
expect(result.statusCode).to.equal(204);
.query({ access_token: token_0 })
.send({ displayName: DISPLAY_NAME_0_NEW })
.end(function (error, result) {
expect(result.statusCode).to.equal(204);
superagent.get(SERVER_URL + '/api/v1/profile')
.query({ access_token: token_0 })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.username).to.equal(USERNAME_0.toLowerCase());
expect(res.body.email).to.equal(EMAIL_0_NEW.toLowerCase());
expect(res.body.admin).to.be.ok();
expect(res.body.displayName).to.equal(DISPLAY_NAME_0_NEW);
superagent.get(SERVER_URL + '/api/v1/profile')
.query({ access_token: token_0 })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.username).to.equal(USERNAME_0.toLowerCase());
expect(res.body.email).to.equal(EMAIL_0_NEW.toLowerCase());
expect(res.body.admin).to.be.ok();
expect(res.body.displayName).to.equal(DISPLAY_NAME_0_NEW);
done();
});
done();
});
});
});
});
@@ -231,52 +231,52 @@ describe('Profile API', function () {
it('fails due to missing current password', function (done) {
superagent.post(SERVER_URL + '/api/v1/profile/password')
.query({ access_token: token_0 })
.send({ newPassword: 'some wrong password' })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
.query({ access_token: token_0 })
.send({ newPassword: 'some wrong password' })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('fails due to missing new password', function (done) {
superagent.post(SERVER_URL + '/api/v1/profile/password')
.query({ access_token: token_0 })
.send({ password: PASSWORD })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
.query({ access_token: token_0 })
.send({ password: PASSWORD })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('fails due to wrong password', function (done) {
superagent.post(SERVER_URL + '/api/v1/profile/password')
.query({ access_token: token_0 })
.send({ password: 'some wrong password', newPassword: 'MOre#$%34' })
.end(function (err, res) {
expect(res.statusCode).to.equal(403);
done();
});
.query({ access_token: token_0 })
.send({ password: 'some wrong password', newPassword: 'MOre#$%34' })
.end(function (err, res) {
expect(res.statusCode).to.equal(403);
done();
});
});
it('fails due to invalid password', function (done) {
superagent.post(SERVER_URL + '/api/v1/profile/password')
.query({ access_token: token_0 })
.send({ password: PASSWORD, newPassword: 'five' })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
.query({ access_token: token_0 })
.send({ password: PASSWORD, newPassword: 'five' })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/profile/password')
.query({ access_token: token_0 })
.send({ password: PASSWORD, newPassword: 'MOre#$%34' })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
done();
});
.query({ access_token: token_0 })
.send({ password: PASSWORD, newPassword: 'MOre#$%34' })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
done();
});
});
});
});
+9
View File
@@ -10,6 +10,8 @@ var async = require('async'),
config = require('../../config.js'),
database = require('../../database.js'),
expect = require('expect.js'),
net = require('net'),
nock = require('nock'),
superagent = require('superagent'),
server = require('../../server.js');
@@ -18,9 +20,11 @@ var SERVER_URL = 'http://localhost:' + config.get('port');
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var token = null;
var server;
function setup(done) {
config._reset();
config.setFqdn('example-server-test.com');
config.set('provider', 'caas');
config.setVersion('1.2.3');
async.series([
@@ -29,12 +33,17 @@ function setup(done) {
database._clear,
function createAdmin(callback) {
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(201);
expect(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
// stash token for further use
token = result.body.token;
File diff suppressed because it is too large Load Diff
+119 -112
View File
@@ -11,7 +11,8 @@ var ssh = require('../../ssh.js'),
database = require('../../database.js'),
expect = require('expect.js'),
superagent = require('superagent'),
server = require('../../server.js');
server = require('../../server.js'),
nock = require('nock');
var SERVER_URL = 'http://localhost:' + config.get('port');
@@ -25,6 +26,7 @@ var VALID_KEY_1 = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCibC8G04mZy3o3AVMxjUMQo
var token = null;
var server;
function setup(done) {
config._reset();
config.setFqdn('example-ssh-test.com');
@@ -36,18 +38,23 @@ function setup(done) {
database._clear,
function createAdmin(callback) {
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(201);
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(201);
expect(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
// stash token for further use
token = result.body.token;
// stash token for further use
token = result.body.token;
callback();
});
callback();
});
}
], done);
}
@@ -69,166 +76,166 @@ describe('SSH API', function () {
describe('add authorized_keys', function () {
it('fails due to missing key', function (done) {
superagent.put(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('fails due to empty key', function (done) {
superagent.put(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
.query({ access_token: token })
.send({ key: '' })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
.query({ access_token: token })
.send({ key: '' })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('fails due to invalid key', function (done) {
superagent.put(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
.query({ access_token: token })
.send({ key: 'foobar' })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
.query({ access_token: token })
.send({ key: 'foobar' })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('fails due to invalid key type', function (done) {
superagent.put(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
.query({ access_token: token })
.send({ key: INVALID_KEY_TYPE })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
.query({ access_token: token })
.send({ key: INVALID_KEY_TYPE })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('fails due to invalid key value', function (done) {
superagent.put(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
.query({ access_token: token })
.send({ key: INVALID_KEY_VALUE })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
.query({ access_token: token })
.send({ key: INVALID_KEY_VALUE })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('fails due to invalid key identifier', function (done) {
superagent.put(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
.query({ access_token: token })
.send({ key: INVALID_KEY_IDENTIFIER })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
.query({ access_token: token })
.send({ key: INVALID_KEY_IDENTIFIER })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('succeeds', function (done) {
superagent.put(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
.query({ access_token: token })
.send({ key: VALID_KEY })
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
done();
});
.query({ access_token: token })
.send({ key: VALID_KEY })
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
done();
});
});
});
describe('get authorized_keys', function () {
it('fails for non existing key', function (done) {
superagent.get(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys/foobar')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
done();
});
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
done();
});
});
it('succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys/' + VALID_KEY.split(' ')[2])
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body).to.be.an('object');
expect(res.body.identifier).to.be.a('string');
expect(res.body.identifier).to.equal(VALID_KEY.split(' ')[2]);
expect(res.body.key).to.equal(VALID_KEY);
done();
});
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body).to.be.an('object');
expect(res.body.identifier).to.be.a('string');
expect(res.body.identifier).to.equal(VALID_KEY.split(' ')[2]);
expect(res.body.key).to.equal(VALID_KEY);
done();
});
});
});
describe('list authorized_keys', function () {
it('succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body).to.be.an('object');
expect(res.body.keys).to.be.an('array');
expect(res.body.keys.length).to.equal(1);
expect(res.body.keys[0]).to.be.an('object');
expect(res.body.keys[0].identifier).to.be.a('string');
expect(res.body.keys[0].identifier).to.equal(VALID_KEY.split(' ')[2]);
expect(res.body.keys[0].key).to.equal(VALID_KEY);
done();
});
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body).to.be.an('object');
expect(res.body.keys).to.be.an('array');
expect(res.body.keys.length).to.equal(1);
expect(res.body.keys[0]).to.be.an('object');
expect(res.body.keys[0].identifier).to.be.a('string');
expect(res.body.keys[0].identifier).to.equal(VALID_KEY.split(' ')[2]);
expect(res.body.keys[0].key).to.equal(VALID_KEY);
done();
});
});
it('succeeds with two keys', function (done) {
superagent.put(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
.query({ access_token: token })
.send({ key: VALID_KEY_1 })
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
.query({ access_token: token })
.send({ key: VALID_KEY_1 })
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
superagent.get(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body).to.be.an('object');
expect(res.body.keys).to.be.an('array');
expect(res.body.keys.length).to.equal(2);
expect(res.body.keys[0]).to.be.an('object');
expect(res.body.keys[0].identifier).to.be.a('string');
expect(res.body.keys[0].identifier).to.equal(VALID_KEY_1.split(' ')[2]);
expect(res.body.keys[0].key).to.equal(VALID_KEY_1);
expect(res.body.keys[1]).to.be.an('object');
expect(res.body.keys[1].identifier).to.be.a('string');
expect(res.body.keys[1].identifier).to.equal(VALID_KEY.split(' ')[2]);
expect(res.body.keys[1].key).to.equal(VALID_KEY);
done();
});
superagent.get(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body).to.be.an('object');
expect(res.body.keys).to.be.an('array');
expect(res.body.keys.length).to.equal(2);
expect(res.body.keys[0]).to.be.an('object');
expect(res.body.keys[0].identifier).to.be.a('string');
expect(res.body.keys[0].identifier).to.equal(VALID_KEY_1.split(' ')[2]);
expect(res.body.keys[0].key).to.equal(VALID_KEY_1);
expect(res.body.keys[1]).to.be.an('object');
expect(res.body.keys[1].identifier).to.be.a('string');
expect(res.body.keys[1].identifier).to.equal(VALID_KEY.split(' ')[2]);
expect(res.body.keys[1].key).to.equal(VALID_KEY);
done();
});
});
});
});
describe('delete authorized_keys', function () {
it('fails for non existing key', function (done) {
superagent.del(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys/foobar')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
done();
});
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
done();
});
});
it('succeeds', function (done) {
superagent.del(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys/' + VALID_KEY.split(' ')[2])
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
superagent.get(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys/' + VALID_KEY.split(' ')[2])
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
done();
});
superagent.get(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys/' + VALID_KEY.split(' ')[2])
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
done();
});
});
});
});
});
+15 -18
View File
@@ -9,11 +9,11 @@ var appdb = require('../../appdb.js'),
async = require('async'),
config = require('../../config.js'),
database = require('../../database.js'),
domains = require('../../domains.js'),
expect = require('expect.js'),
hock = require('hock'),
http = require('http'),
MockS3 = require('mock-aws-s3'),
nock = require('nock'),
os = require('os'),
path = require('path'),
rimraf = require('rimraf'),
@@ -25,36 +25,33 @@ var appdb = require('../../appdb.js'),
superagent = require('superagent'),
url = require('url');
const SERVER_URL = 'http://localhost:' + config.get('port');
var SERVER_URL = 'http://localhost:' + config.get('port');
const USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
const DOMAIN_0 = {
domain: 'example-sysadmin-test.com',
zoneName: 'example-sysadmin-test.com',
config: {},
provider: 'noop',
fallbackCertificate: null,
tlsConfig: { provider: 'fallback' }
};
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
function setup(done) {
config._reset();
config.setFqdn(DOMAIN_0.domain);
config.setFqdn('example-sysadmin-test.com');
config.set('provider', 'caas');
config.setVersion('1.2.3');
async.series([
server.start,
server.start.bind(server),
database._clear,
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0.zoneName, DOMAIN_0.provider, DOMAIN_0.config, DOMAIN_0.fallbackCertificate, DOMAIN_0.tlsConfig),
function createAdmin(callback) {
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(201);
expect(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
callback();
});
@@ -62,7 +59,7 @@ function setup(done) {
function addApp(callback) {
var manifest = { version: '0.0.1', manifestVersion: 1, dockerImage: 'foo', healthCheckPath: '/', httpPort: 3, title: 'ok', addons: { } };
appdb.add('appid', 'appStoreId', manifest, 'location', DOMAIN_0.domain, [ ] /* portBindings */, { }, callback);
appdb.add('appid', 'appStoreId', manifest, 'location', config.fqdn(), [ ] /* portBindings */, { }, callback);
},
function createSettings(callback) {
@@ -71,7 +68,7 @@ function setup(done) {
s3._mockInject(MockS3);
safe.fs.mkdirSync('/tmp/box-sysadmin-test');
settingsdb.set(settings.BACKUP_CONFIG_KEY, JSON.stringify({ provider: 'caas', token: 'BACKUP_TOKEN', fqdn: DOMAIN_0.domain, key: 'key', prefix: 'boxid', format: 'tgz'}), callback);
settingsdb.set(settings.BACKUP_CONFIG_KEY, JSON.stringify({ provider: 'caas', token: 'BACKUP_TOKEN', fqdn: config.fqdn(), key: 'key', prefix: 'boxid', format: 'tgz'}), callback);
}
], done);
}
@@ -95,7 +92,7 @@ describe('Internal API', function () {
before(function (done) {
apiHockInstance
.post('/api/v1/boxes/' + config.adminDomain() + '/awscredentials?token=BACKUP_TOKEN')
.post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=BACKUP_TOKEN')
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey' } });
var port = parseInt(url.parse(config.apiServerOrigin()).port, 10);
apiHockServer = http.createServer(apiHockInstance.handler).listen(port, done);
File diff suppressed because it is too large Load Diff
+37 -5
View File
@@ -9,7 +9,9 @@ exports = module.exports = {
verifyPassword: verifyPassword,
requireAdmin: requireAdmin,
sendInvite: sendInvite,
setGroups: setGroups
setGroups: setGroups,
setAliases: setAliases,
getAliases: getAliases
};
var assert = require('assert'),
@@ -21,6 +23,7 @@ var assert = require('assert'),
oauth2 = require('./oauth2.js'),
user = require('../user.js'),
UserError = user.UserError,
util = require('util'),
_ = require('underscore');
function auditSource(req) {
@@ -52,7 +55,7 @@ function create(req, res, next) {
username: user.username,
displayName: user.displayName,
email: user.email,
fallbackEmail: user.fallbackEmail,
alternateEmail: user.alternateEmail,
admin: user.admin,
groupIds: [ ],
resetToken: user.resetToken
@@ -68,7 +71,6 @@ function update(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if ('email' in req.body && typeof req.body.email !== 'string') return next(new HttpError(400, 'email must be string'));
if ('fallbackEmail' in req.body && typeof req.body.fallbackEmail !== 'string') return next(new HttpError(400, 'fallbackEmail must be string'));
if ('displayName' in req.body && typeof req.body.displayName !== 'string') return next(new HttpError(400, 'displayName must be string'));
if ('username' in req.body && typeof req.body.username !== 'string') return next(new HttpError(400, 'username must be a string'));
@@ -89,7 +91,7 @@ function list(req, res, next) {
if (error) return next(new HttpError(500, error));
var users = results.map(function (result) {
return _.pick(result, 'id', 'username', 'email', 'fallbackEmail', 'displayName', 'groupIds', 'admin');
return _.pick(result, 'id', 'username', 'email', 'alternateEmail', 'displayName', 'groupIds', 'admin');
});
next(new HttpSuccess(200, { users: users }));
@@ -111,7 +113,7 @@ function get(req, res, next) {
username: result.username,
displayName: result.displayName,
email: result.email,
fallbackEmail: result.fallbackEmail,
alternateEmail: result.alternateEmail,
admin: result.admin,
groupIds: result.groupIds
}));
@@ -195,3 +197,33 @@ function setGroups(req, res, next) {
next(new HttpSuccess(204));
});
}
function setAliases(req, res, next) {
assert.strictEqual(typeof req.params.userId, 'string');
if (!util.isArray(req.body.aliases)) return next(new HttpError(400, 'aliases must be an array'));
for (var i = 0; i < req.body.aliases.length; i++) {
if (typeof req.body.aliases[i] !== 'string') return next(new HttpError(400, 'alias must be a string'));
}
user.setAliases(req.params.userId, req.body.aliases, function (error) {
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(404, 'No such user'));
if (error && error.reason === UserError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === UserError.ALREADY_EXISTS) return next(new HttpError(409, 'One or more aliases already exist'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200));
});
}
function getAliases(req, res, next) {
assert.strictEqual(typeof req.params.userId, 'string');
user.getAliases(req.params.userId, function (error, aliases) {
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(404, 'No such user'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { aliases: aliases }));
});
}
+2
View File
@@ -20,6 +20,8 @@ if [[ "${BOX_ENV}" != "cloudron" ]]; then
exit 0
fi
"${BOX_SRC_DIR}/setup/splashpage.sh" --retire-reason "$1" --retire-info "$2" --data "$3" # show splash
echo "Stopping apps"
systemctl stop docker # stop the apps
+17 -25
View File
@@ -20,7 +20,6 @@ var assert = require('assert'),
passport = require('passport'),
path = require('path'),
routes = require('./routes/index.js'),
setup = require('./setup.js'),
ws = require('ws');
var gHttpServer = null;
@@ -98,12 +97,12 @@ function initializeExpressSync() {
var csrf = routes.oauth2.csrf;
// public routes
router.post('/api/v1/cloudron/dns_setup', routes.setup.providerTokenAuth, routes.setup.dnsSetup); // only available until no-domain
router.post('/api/v1/cloudron/restore', routes.setup.restore); // only available until activated
router.post('/api/v1/cloudron/activate', routes.setup.setupTokenAuth, routes.setup.activate);
router.get ('/api/v1/cloudron/status', routes.setup.getStatus);
router.post('/api/v1/cloudron/dns_setup', routes.cloudron.providerTokenAuth, routes.cloudron.dnsSetup); // only available until no-domain
router.post('/api/v1/cloudron/activate', routes.cloudron.setupTokenAuth, routes.cloudron.activate);
router.post('/api/v1/cloudron/restore', routes.cloudron.restore); // only available until activated
router.get ('/api/v1/cloudron/progress', routes.cloudron.getProgress);
router.get ('/api/v1/cloudron/status', routes.cloudron.getStatus);
router.get ('/api/v1/cloudron/avatar', routes.settings.getCloudronAvatar); // this is a public alias for /api/v1/settings/cloudron_avatar
// developer routes
@@ -123,6 +122,7 @@ function initializeExpressSync() {
router.get ('/api/v1/cloudron/ssh/authorized_keys/:identifier', cloudronScope, routes.user.requireAdmin, routes.ssh.getAuthorizedKey);
router.del ('/api/v1/cloudron/ssh/authorized_keys/:identifier', cloudronScope, routes.user.requireAdmin, routes.ssh.delAuthorizedKey);
router.get ('/api/v1/cloudron/eventlog', cloudronScope, routes.user.requireAdmin, routes.eventlog.get);
router.post('/api/v1/cloudron/send_test_mail', cloudronScope, routes.user.requireAdmin, routes.cloudron.sendTestMail);
// profile api, working off the user behind the provided token
router.get ('/api/v1/profile', profileScope, routes.profile.get);
@@ -136,6 +136,8 @@ function initializeExpressSync() {
router.del ('/api/v1/users/:userId', usersScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.user.remove);
router.post('/api/v1/users/:userId', usersScope, routes.user.requireAdmin, routes.user.update);
router.put ('/api/v1/users/:userId/groups', usersScope, routes.user.requireAdmin, routes.user.setGroups);
router.get ('/api/v1/users/:userId/aliases', usersScope, routes.user.requireAdmin, routes.user.getAliases);
router.put ('/api/v1/users/:userId/aliases', usersScope, routes.user.requireAdmin, routes.user.setAliases);
router.post('/api/v1/users/:userId/invite', usersScope, routes.user.requireAdmin, routes.user.sendInvite);
// Group management
@@ -201,34 +203,25 @@ function initializeExpressSync() {
router.post('/api/v1/settings/cloudron_name', settingsScope, routes.user.requireAdmin, routes.settings.setCloudronName);
router.get ('/api/v1/settings/cloudron_avatar', settingsScope, routes.user.requireAdmin, routes.settings.getCloudronAvatar);
router.post('/api/v1/settings/cloudron_avatar', settingsScope, routes.user.requireAdmin, multipart, routes.settings.setCloudronAvatar);
router.get ('/api/v1/settings/email_status', settingsScope, routes.user.requireAdmin, routes.settings.getEmailStatus);
router.get ('/api/v1/settings/backup_config', settingsScope, routes.user.requireAdmin, routes.settings.getBackupConfig);
router.post('/api/v1/settings/backup_config', settingsScope, routes.user.requireAdmin, routes.settings.setBackupConfig);
router.post('/api/v1/settings/admin_certificate', settingsScope, routes.user.requireAdmin, routes.settings.setAdminCertificate);
router.get ('/api/v1/settings/time_zone', settingsScope, routes.user.requireAdmin, routes.settings.getTimeZone);
router.post('/api/v1/settings/time_zone', settingsScope, routes.user.requireAdmin, routes.settings.setTimeZone);
router.get ('/api/v1/settings/appstore_config', settingsScope, routes.user.requireAdmin, routes.settings.getAppstoreConfig);
router.post('/api/v1/settings/appstore_config', settingsScope, routes.user.requireAdmin, routes.settings.setAppstoreConfig);
// email routes
router.get ('/api/v1/mail/:domain', settingsScope, routes.user.requireAdmin, routes.mail.get);
router.post('/api/v1/mail', settingsScope, routes.user.requireAdmin, routes.mail.add);
router.del ('/api/v1/mail/:domain', settingsScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.mail.del);
router.get ('/api/v1/mail/:domain/status', settingsScope, routes.user.requireAdmin, routes.mail.getStatus);
router.post('/api/v1/mail/:domain/mail_from_validation', settingsScope, routes.user.requireAdmin, routes.mail.setMailFromValidation);
router.post('/api/v1/mail/:domain/catch_all', settingsScope, routes.user.requireAdmin, routes.mail.setCatchAllAddress);
router.post('/api/v1/mail/:domain/relay', settingsScope, routes.user.requireAdmin, routes.mail.setMailRelay);
router.post('/api/v1/mail/:domain/enable', settingsScope, routes.user.requireAdmin, routes.mail.setMailEnabled);
router.post('/api/v1/mail/:domain/send_test_mail', settingsScope, routes.user.requireAdmin, routes.mail.sendTestMail);
router.get ('/api/v1/mail/:domain/mailboxes', settingsScope, routes.user.requireAdmin, routes.mail.getMailboxes);
router.get ('/api/v1/mail/:domain/mailboxes/:userId', settingsScope, routes.user.requireAdmin, routes.mail.getUserMailbox);
router.post('/api/v1/mail/:domain/mailboxes/:userId', settingsScope, routes.user.requireAdmin, routes.mail.enableUserMailbox);
router.del ('/api/v1/mail/:domain/mailboxes/:userId', settingsScope, routes.user.requireAdmin, routes.mail.disableUserMailbox);
router.get ('/api/v1/mail/:domain/aliases/:userId', settingsScope, routes.user.requireAdmin, routes.mail.getAliases);
router.put ('/api/v1/mail/:domain/aliases/:userId', settingsScope, routes.user.requireAdmin, routes.mail.setAliases);
router.get ('/api/v1/mail/:domain/lists', settingsScope, routes.user.requireAdmin, routes.mail.getLists);
router.post('/api/v1/mail/:domain/lists', settingsScope, routes.user.requireAdmin, routes.mail.addList);
router.get ('/api/v1/mail/:domain/lists/:groupId', settingsScope, routes.user.requireAdmin, routes.mail.getList);
router.del ('/api/v1/mail/:domain/lists/:groupId', settingsScope, routes.user.requireAdmin, routes.mail.removeList);
router.get ('/api/v1/settings/mail_config', settingsScope, routes.user.requireAdmin, routes.settings.getMailConfig);
router.post('/api/v1/settings/mail_config', settingsScope, routes.user.requireAdmin, routes.settings.setMailConfig);
router.get ('/api/v1/settings/mail_relay', settingsScope, routes.user.requireAdmin, routes.settings.getMailRelay);
router.post('/api/v1/settings/mail_relay', settingsScope, routes.user.requireAdmin, routes.settings.setMailRelay);
router.get ('/api/v1/settings/catch_all_address', settingsScope, routes.user.requireAdmin, routes.settings.getCatchAllAddress);
router.put ('/api/v1/settings/catch_all_address', settingsScope, routes.user.requireAdmin, routes.settings.setCatchAllAddress);
router.get ('/api/v1/settings/mail_from_validation', settingsScope, routes.user.requireAdmin, routes.settings.getMailFromValidation);
router.post('/api/v1/settings/mail_from_validation', settingsScope, routes.user.requireAdmin, routes.settings.setMailFromValidation);
// feedback
router.post('/api/v1/feedback', usersScope, routes.cloudron.feedback);
@@ -321,7 +314,6 @@ function start(callback) {
auth.initialize,
database.initialize,
cloudron.initialize,
setup.configureWebadmin,
gHttpServer.listen.bind(gHttpServer, config.get('port'), '127.0.0.1'),
gSysadminHttpServer.listen.bind(gSysadminHttpServer, config.get('sysadminPort'), '127.0.0.1'),
eventlog.add.bind(null, eventlog.ACTION_START, { userId: null, username: 'boot' }, { version: config.version() })
+159 -8
View File
@@ -24,11 +24,24 @@ exports = module.exports = {
getBackupConfig: getBackupConfig,
setBackupConfig: setBackupConfig,
getCaasConfig: getCaasConfig,
getTlsConfig: getTlsConfig,
setTlsConfig: setTlsConfig,
getAppstoreConfig: getAppstoreConfig,
setAppstoreConfig: setAppstoreConfig,
getMailConfig: getMailConfig,
setMailConfig: setMailConfig,
getMailFromValidation: getMailFromValidation,
setMailFromValidation: setMailFromValidation,
getMailRelay: getMailRelay,
setMailRelay: setMailRelay,
setCatchAllAddress: setCatchAllAddress,
getCatchAllAddress: getCatchAllAddress,
getEmailDigest: getEmailDigest,
setEmailDigest: setEmailDigest,
@@ -36,13 +49,17 @@ exports = module.exports = {
// booleans. if you add an entry here, be sure to fix getAll
DYNAMIC_DNS_KEY: 'dynamic_dns',
MAIL_FROM_VALIDATION_KEY: 'mail_from_validation',
EMAIL_DIGEST: 'email_digest',
// json. if you add an entry here, be sure to fix getAll
BACKUP_CONFIG_KEY: 'backup_config',
TLS_CONFIG_KEY: 'tls_config',
UPDATE_CONFIG_KEY: 'update_config',
APPSTORE_CONFIG_KEY: 'appstore_config',
CAAS_CONFIG_KEY: 'caas_config',
MAIL_CONFIG_KEY: 'mail_config',
MAIL_RELAY_KEY: 'mail_relay',
CATCH_ALL_ADDRESS_KEY: 'catch_all_address',
// strings
AUTOUPDATE_PATTERN_KEY: 'autoupdate_pattern',
@@ -59,8 +76,12 @@ var assert = require('assert'),
constants = require('./constants.js'),
CronJob = require('cron').CronJob,
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:settings'),
moment = require('moment-timezone'),
paths = require('./paths.js'),
platform = require('./platform.js'),
email = require('./email.js'),
EmailError = email.EmailError,
safe = require('safetydance'),
settingsdb = require('./settingsdb.js'),
superagent = require('superagent'),
@@ -79,14 +100,20 @@ var gDefaults = (function () {
backupFolder: '/var/backups',
retentionSecs: 172800
};
result[exports.TLS_CONFIG_KEY] = { provider: 'letsencrypt-prod' };
result[exports.UPDATE_CONFIG_KEY] = { prerelease: false };
result[exports.APPSTORE_CONFIG_KEY] = {};
result[exports.CAAS_CONFIG_KEY] = {};
result[exports.MAIL_CONFIG_KEY] = { enabled: false };
result[exports.MAIL_RELAY_KEY] = { provider: 'cloudron-smtp' };
result[exports.CATCH_ALL_ADDRESS_KEY] = [ ];
result[exports.MAIL_FROM_VALIDATION_KEY] = true;
result[exports.EMAIL_DIGEST] = true;
return result;
})();
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
function SettingsError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
@@ -257,6 +284,34 @@ function setDynamicDnsConfig(enabled, callback) {
});
}
function getTlsConfig(callback) {
assert.strictEqual(typeof callback, 'function');
settingsdb.get(exports.TLS_CONFIG_KEY, function (error, value) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.TLS_CONFIG_KEY]);
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
callback(null, JSON.parse(value)); // provider
});
}
function setTlsConfig(tlsConfig, callback) {
assert.strictEqual(typeof tlsConfig, 'object');
assert.strictEqual(typeof callback, 'function');
if (tlsConfig.provider !== 'fallback' && tlsConfig.provider !== 'caas' && tlsConfig.provider.indexOf('le-') !== 0) {
return callback(new SettingsError(SettingsError.BAD_FIELD, 'provider must be caas, fallback or le-*'));
}
settingsdb.set(exports.TLS_CONFIG_KEY, JSON.stringify(tlsConfig), function (error) {
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
exports.events.emit(exports.TLS_CONFIG_KEY, tlsConfig);
callback(null);
});
}
function getBackupConfig(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -291,6 +346,56 @@ function setBackupConfig(backupConfig, callback) {
});
}
function getMailConfig(callback) {
assert.strictEqual(typeof callback, 'function');
settingsdb.get(exports.MAIL_CONFIG_KEY, function (error, value) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.MAIL_CONFIG_KEY]);
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
callback(null, JSON.parse(value));
});
}
function setMailConfig(mailConfig, callback) {
assert.strictEqual(typeof mailConfig, 'object');
assert.strictEqual(typeof callback, 'function');
settingsdb.set(exports.MAIL_CONFIG_KEY, JSON.stringify(mailConfig), function (error) {
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
exports.events.emit(exports.MAIL_CONFIG_KEY, mailConfig);
callback(null);
});
}
function getMailFromValidation(callback) {
assert.strictEqual(typeof callback, 'function');
settingsdb.get(exports.MAIL_FROM_VALIDATION_KEY, function (error, enabled) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.MAIL_FROM_VALIDATION_KEY]);
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
callback(null, !!enabled); // settingsdb holds string values only
});
}
function setMailFromValidation(enabled, callback) {
assert.strictEqual(typeof enabled, 'boolean');
assert.strictEqual(typeof callback, 'function');
settingsdb.set(exports.MAIL_FROM_VALIDATION_KEY, enabled ? 'enabled' : '', function (error) {
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
exports.events.emit(exports.MAIL_FROM_VALIDATION_KEY, enabled);
platform.createMailConfig(NOOP_CALLBACK);
callback(null);
});
}
function getEmailDigest(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -315,17 +420,61 @@ function setEmailDigest(enabled, callback) {
});
}
function getCaasConfig(callback) {
function getMailRelay(callback) {
assert.strictEqual(typeof callback, 'function');
settingsdb.get(exports.CAAS_CONFIG_KEY, function (error, value) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.CAAS_CONFIG_KEY]);
settingsdb.get(exports.MAIL_RELAY_KEY, function (error, value) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.MAIL_RELAY_KEY]);
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
callback(null, JSON.parse(value));
});
}
function setMailRelay(relay, callback) {
assert.strictEqual(typeof relay, 'object');
assert.strictEqual(typeof callback, 'function');
email.verifyRelay(relay, function (error) {
if (error && error.reason === EmailError.BAD_FIELD) return callback(new SettingsError(SettingsError.BAD_FIELD, error.message));
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
settingsdb.set(exports.MAIL_RELAY_KEY, JSON.stringify(relay), function (error) {
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
exports.events.emit(exports.MAIL_RELAY_KEY, relay);
callback(null);
});
});
}
function getCatchAllAddress(callback) {
assert.strictEqual(typeof callback, 'function');
settingsdb.get(exports.CATCH_ALL_ADDRESS_KEY, function (error, value) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.CATCH_ALL_ADDRESS_KEY]);
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
callback(null, JSON.parse(value));
});
}
function setCatchAllAddress(address, callback) {
assert(Array.isArray(address));
assert.strictEqual(typeof callback, 'function');
settingsdb.set(exports.CATCH_ALL_ADDRESS_KEY, JSON.stringify(address), function (error) {
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
exports.events.emit(exports.CATCH_ALL_ADDRESS_KEY, address);
platform.createMailConfig(NOOP_CALLBACK);
callback(null);
});
}
function getAppstoreConfig(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -365,7 +514,7 @@ function setAppstoreConfig(appstoreConfig, callback) {
function registerCloudron() {
const url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons';
const data = {
domain: config.adminDomain()
domain: config.fqdn()
};
superagent.post(url).send(data).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
@@ -407,9 +556,11 @@ function getAll(callback) {
// convert booleans
result[exports.DYNAMIC_DNS_KEY] = !!result[exports.DYNAMIC_DNS_KEY];
result[exports.MAIL_FROM_VALIDATION_KEY] = !!result[exports.MAIL_FROM_VALIDATION_KEY];
// convert JSON objects
[exports.BACKUP_CONFIG_KEY, exports.UPDATE_CONFIG_KEY, exports.APPSTORE_CONFIG_KEY ].forEach(function (key) {
[exports.TLS_CONFIG_KEY, exports.BACKUP_CONFIG_KEY, exports.MAIL_CONFIG_KEY,
exports.UPDATE_CONFIG_KEY, exports.APPSTORE_CONFIG_KEY, exports.MAIL_RELAY_KEY, exports.CATCH_ALL_ADDRESS_KEY].forEach(function (key) {
result[key] = typeof result[key] === 'object' ? result[key] : safe.JSON.parse(result[key]);
});
-345
View File
@@ -1,345 +0,0 @@
'use strict';
exports = module.exports = {
dnsSetup: dnsSetup,
restore: restore,
getStatus: getStatus,
activate: activate,
configureWebadmin: configureWebadmin,
SetupError: SetupError
};
var assert = require('assert'),
async = require('async'),
backups = require('./backups.js'),
BackupsError = require('./backups.js').BackupsError,
config = require('./config.js'),
constants = require('./constants.js'),
clients = require('./clients.js'),
cloudron = require('./cloudron.js'),
debug = require('debug')('box:setup'),
domains = require('./domains.js'),
DomainError = domains.DomainError,
eventlog = require('./eventlog.js'),
fs = require('fs'),
mail = require('./mail.js'),
path = require('path'),
paths = require('./paths.js'),
reverseProxy = require('./reverseproxy.js'),
safe = require('safetydance'),
semver = require('semver'),
settingsdb = require('./settingsdb.js'),
settings = require('./settings.js'),
SettingsError = settings.SettingsError,
shell = require('./shell.js'),
superagent = require('superagent'),
sysinfo = require('./sysinfo.js'),
tokendb = require('./tokendb.js'),
user = require('./user.js'),
UserError = user.UserError,
tld = require('tldjs'),
util = require('util');
var 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 SetupError(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(SetupError, Error);
SetupError.BAD_FIELD = 'Field error';
SetupError.BAD_STATE = 'Bad State';
SetupError.ALREADY_SETUP = 'Already Setup';
SetupError.INTERNAL_ERROR = 'Internal Error';
SetupError.EXTERNAL_ERROR = 'External Error';
SetupError.ALREADY_PROVISIONED = 'Already Provisioned';
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 'appstoreConfig': name = settings.APPSTORE_CONFIG_KEY; break;
case 'caasConfig': name = settings.CAAS_CONFIG_KEY; break;
case 'backupConfig': name = settings.BACKUP_CONFIG_KEY; break;
case 'tlsCert':
debug(`autoprovision: ${key}`);
return fs.writeFile(path.join(paths.NGINX_CERT_DIR, config.adminDomain() + '.host.cert'), conf[key], iteratorDone);
case 'tlsKey':
debug(`autoprovision: ${key}`);
return fs.writeFile(path.join(paths.NGINX_CERT_DIR, config.adminDomain() + '.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 configureWebadmin(callback) {
callback = callback || NOOP_CALLBACK;
debug('configureWebadmin: adminDomain:%s status:%j', config.adminDomain(), gWebadminStatus);
if (process.env.BOX_ENV === 'test' || !config.adminDomain() || 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 configureReverseProxy(error) {
debug('configureReverseProxy: error %j', error || null);
reverseProxy.configureAdmin({ userId: null, username: 'setup' }, function (error) {
if (error) return done(error);
gWebadminStatus.tls = true;
done();
});
}
function addWebadminDnsRecord(ip, domain, callback) {
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
if (process.env.BOX_ENV === 'test') return callback();
async.retry({ times: 10, interval: 20000 }, function (retryCallback) {
domains.upsertDNSRecords(config.adminLocation(), domain, 'A', [ ip ], retryCallback);
}, function (error) {
if (error) debug('addWebadminDnsRecord: done updating records with error:', error);
else debug('addWebadminDnsRecord: done');
callback(error);
});
}
// 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 configureReverseProxy(error);
addWebadminDnsRecord(ip, config.adminDomain(), function (error) {
if (error) return configureReverseProxy(error);
domains.waitForDNSRecord(config.adminFqdn(), config.adminDomain(), ip, 'A', { interval: 30000, times: 50000 }, function (error) {
if (error) return configureReverseProxy(error);
gWebadminStatus.dns = true;
configureReverseProxy();
});
});
});
}
function dnsSetup(adminFqdn, domain, zoneName, provider, dnsConfig, tlsConfig, 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 tlsConfig, 'object');
assert.strictEqual(typeof callback, 'function');
if (config.adminDomain()) return callback(new SetupError(SetupError.ALREADY_SETUP));
if (gWebadminStatus.configuring || gWebadminStatus.restoring) return callback(new SetupError(SetupError.BAD_STATE, 'Already restoring or configuring'));
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 SetupError(SetupError.BAD_FIELD, error.message));
if (error) return callback(new SetupError(SetupError.INTERNAL_ERROR, error));
config.setAdminDomain(domain); // set fqdn only after dns config is valid, otherwise cannot re-setup if we failed
config.setAdminFqdn(adminFqdn);
config.setAdminLocation('my');
autoprovision(function (error) {
if (error) return callback(new SetupError(SetupError.INTERNAL_ERROR, error));
clients.addDefaultClients(config.adminOrigin(), callback);
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) {
async.series([
domains.add.bind(null, domain, zoneName, provider, dnsConfig, null /* cert */, tlsConfig),
mail.add.bind(null, domain)
], done);
} else {
domains.update(domain, provider, dnsConfig, null /* cert */, tlsConfig, done);
}
});
}
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 SetupError(SetupError.ALREADY_PROVISIONED));
if (error && error.reason === UserError.BAD_FIELD) return callback(new SetupError(SetupError.BAD_FIELD, error.message));
if (error) return callback(new SetupError(SetupError.INTERNAL_ERROR, error));
clients.get('cid-webadmin', function (error, result) {
if (error) return callback(new SetupError(SetupError.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 SetupError(SetupError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_ACTIVATE, auditSource, { });
callback(null, { token: token, expires: expires });
setTimeout(cloudron.onActivated, 3000); // hack for now to not block the above http response
});
});
});
}
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 SetupError(SetupError.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 SetupError(SetupError.BAD_STATE, `Run cloudron-setup with --version ${version} to restore from this backup`));
if (gWebadminStatus.configuring || gWebadminStatus.restoring) return callback(new SetupError(SetupError.BAD_STATE, 'Already restoring or configuring'));
user.count(function (error, count) {
if (error) return callback(new SetupError(SetupError.INTERNAL_ERROR, error));
if (count) return callback(new SetupError(SetupError.ALREADY_PROVISIONED, 'Already activated'));
backups.testConfig(backupConfig, function (error) {
if (error && error.reason === BackupsError.BAD_FIELD) return callback(new SetupError(SetupError.BAD_FIELD, error.message));
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new SetupError(SetupError.EXTERNAL_ERROR, error.message));
if (error) return callback(new SetupError(SetupError.INTERNAL_ERROR, error));
debug(`restore: restoring from ${backupId} from provider ${backupConfig.provider}`);
gWebadminStatus.restoring = true;
callback(null); // do no block
async.series([
backups.restore.bind(null, backupConfig, backupId),
autoprovision,
shell.sudo.bind(null, 'restart', [ RESTART_CMD ])
], function (error) {
debug('restore:', error);
gWebadminStatus.restoring = false;
});
});
});
}
function getStatus(callback) {
assert.strictEqual(typeof callback, 'function');
user.count(function (error, count) {
if (error) return callback(new SetupError(SetupError.INTERNAL_ERROR, error));
settings.getCloudronName(function (error, cloudronName) {
if (error) return callback(new SetupError(SetupError.INTERNAL_ERROR, error));
callback(null, {
version: config.version(),
apiServerOrigin: config.apiServerOrigin(), // used by CaaS tool
provider: config.provider(),
cloudronName: cloudronName,
adminFqdn: config.adminDomain() ? config.adminFqdn() : null,
activated: count !== 0,
webadminStatus: gWebadminStatus // only valid when !activated
});
});
});
}
+12 -17
View File
@@ -32,6 +32,7 @@ var assert = require('assert'),
PassThrough = require('stream').PassThrough,
path = require('path'),
S3BlockReadStream = require('s3-block-read-stream'),
safe = require('safetydance'),
superagent = require('superagent');
// test only
@@ -142,6 +143,14 @@ function upload(apiConfig, backupFilePath, sourceStream, callback) {
var s3 = new AWS.S3(credentials);
// exoscale does not like multi-part uploads. so avoid them for filesystem streams < 5GB
if (apiConfig.provider === 'exoscale-sos' && typeof sourceStream.path === 'string') {
var stat = safe.fs.statSync(sourceStream.path);
if (!stat) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, `Error detecting size ${sourceStream.path}. Message: ${safe.error.message}`));
if (stat.size <= 5 * 1024 * 1024 * 1024) return s3.putObject(params, done);
}
// s3.upload automatically does a multi-part upload. we set queueSize to 1 to reduce memory usage
// uploader will buffer at most queueSize * partSize bytes into memory at any given time.
return s3.upload(params, { partSize: 10 * 1024 * 1024, queueSize: 1 }, done);
@@ -266,21 +275,6 @@ function downloadDir(apiConfig, backupFilePath, destDir) {
return events;
}
// https://github.com/aws/aws-sdk-js/blob/2b6bcbdec1f274fe931640c1b61ece999aae7a19/lib/util.js#L41
// https://github.com/GeorgePhillips/node-s3-url-encode/blob/master/index.js
// See aws-sdk-js/issues/1302
function encodeCopySource(bucket, path) {
var output = encodeURI(path);
// AWS percent-encodes some extra non-standard characters in a URI
output = output.replace(/[+!"#$@&'()*+,:;=?@]/g, function(ch) {
return '%' + ch.charCodeAt(0).toString(16).toUpperCase();
});
// the slash at the beginning is optional
return `/${bucket}/${output}`;
}
function copy(apiConfig, oldFilePath, newFilePath) {
assert.strictEqual(typeof apiConfig, 'object');
assert.strictEqual(typeof oldFilePath, 'string');
@@ -310,7 +304,8 @@ function copy(apiConfig, oldFilePath, newFilePath) {
if (content.Size < 5 * 1024 * 1024 * 1024 || apiConfig.provider === 'digitalocean-spaces') { // DO has not implemented this yet
events.emit('progress', `Copying ${relativePath}`);
copyParams.CopySource = encodeCopySource(apiConfig.bucket, content.Key);
// for exoscale, '/' should not be encoded
copyParams.CopySource = path.join(apiConfig.bucket, encodeURIComponent(content.Key)); // See aws-sdk-js/issues/1302
s3.copyObject(copyParams, done).on('retry', function (response) {
++retryCount;
events.emit('progress', `Retrying (${response.retryCount+1}) copy of ${relativePath}. Status code: ${response.httpResponse.statusCode}`);
@@ -339,7 +334,7 @@ function copy(apiConfig, oldFilePath, newFilePath) {
var params = {
Bucket: apiConfig.bucket,
Key: path.join(newFilePath, relativePath),
CopySource: encodeCopySource(apiConfig.bucket, content.Key), // See aws-sdk-js/issues/1302
CopySource: path.join(apiConfig.bucket, encodeURIComponent(content.Key)), // See aws-sdk-js/issues/1302
CopySourceRange: 'bytes=' + startBytes + '-' + endBytes,
PartNumber: partNumber,
UploadId: uploadId
-2
View File
@@ -13,8 +13,6 @@ var assert = require('assert'),
function getPublicIp(callback) {
assert.strictEqual(typeof callback, 'function');
if (process.env.BOX_ENV === 'test') return callback(null, '127.0.0.1');
async.retry({ times: 10, interval: 5000 }, function (callback) {
superagent.get(config.apiServerOrigin() + '/api/v1/helper/public_ip').timeout(30 * 1000).end(function (error, result) {
if (error || result.statusCode !== 200) {
+8 -13
View File
@@ -12,7 +12,7 @@ var appdb = require('../appdb.js'),
config = require('../config.js'),
constants = require('../constants.js'),
database = require('../database.js'),
domains = require('../domains.js'),
domaindb = require('../domaindb.js'),
expect = require('expect.js'),
groupdb = require('../groupdb.js'),
groups = require('../groups.js'),
@@ -27,7 +27,6 @@ describe('Apps', function () {
username: 'admin123',
password: 'secret',
email: 'admin@me.com',
fallbackEmail: 'admin@me.com',
salt: 'morton',
createdAt: 'sometime back',
modifiedAt: 'now',
@@ -40,7 +39,6 @@ describe('Apps', function () {
username: 'uuid213',
password: 'secret',
email: 'safe@me.com',
fallbackEmail: 'safe@me.com',
salt: 'morton',
createdAt: 'sometime back',
modifiedAt: 'now',
@@ -53,7 +51,6 @@ describe('Apps', function () {
username: 'uuid2134',
password: 'secret',
email: 'safe1@me.com',
fallbackEmail: 'safe1@me.com',
salt: 'morton',
createdAt: 'sometime back',
modifiedAt: 'now',
@@ -74,16 +71,14 @@ describe('Apps', function () {
domain: 'example.com',
zoneName: 'example.com',
provider: 'manual',
config: { },
tlsConfig: { provider: 'fallback' }
config: { }
};
const DOMAIN_1 = {
domain: 'example2.com',
zoneName: 'example2.com',
provider: 'manual',
config: { },
tlsConfig: { provider: 'fallback' }
config: { }
};
var APP_0 = {
@@ -142,8 +137,8 @@ describe('Apps', function () {
async.series([
database.initialize,
database._clear,
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0.zoneName, DOMAIN_0.provider, DOMAIN_0.config, null, DOMAIN_0.tlsConfig),
domains.add.bind(null, DOMAIN_1.domain, DOMAIN_1.zoneName, DOMAIN_1.provider, DOMAIN_1.config, null, DOMAIN_1.tlsConfig),
// DOMAIN_0 already added for test through domaindb.addDefaultDomain()
domaindb.add.bind(null, DOMAIN_1.domain, DOMAIN_1.zoneName, DOMAIN_1.provider, DOMAIN_1.config),
userdb.add.bind(null, ADMIN_0.id, ADMIN_0),
userdb.add.bind(null, USER_0.id, USER_0),
userdb.add.bind(null, USER_1.id, USER_1),
@@ -226,7 +221,7 @@ describe('Apps', function () {
describe('getters', function () {
it('cannot get invalid app', function (done) {
apps.get('nope', function (error) {
apps.get('nope', function (error, app) {
expect(error).to.be.ok();
expect(error.reason).to.be(AppsError.NOT_FOUND);
done();
@@ -238,7 +233,7 @@ describe('Apps', function () {
expect(error).to.be(null);
expect(app).to.be.ok();
expect(app.iconUrl).to.be(null);
expect(app.fqdn).to.eql(APP_0.location + '.' + DOMAIN_0.domain);
expect(app.fqdn).to.eql(APP_0.location + '.' + config.fqdn());
expect(app.memoryLimit).to.eql(0);
done();
});
@@ -250,7 +245,7 @@ describe('Apps', function () {
expect(apps).to.be.an(Array);
expect(apps[0].id).to.be(APP_0.id);
expect(apps[0].iconUrl).to.be(null);
expect(apps[0].fqdn).to.eql(APP_0.location + '.' + DOMAIN_0.domain);
expect(apps[0].fqdn).to.eql(APP_0.location + '.' + config.fqdn());
done();
});
});
+32 -32
View File
@@ -22,31 +22,31 @@ var addons = require('../addons.js'),
_ = require('underscore');
var MANIFEST = {
'id': 'io.cloudron.test',
'author': 'The Presidents Of the United States Of America',
'title': 'test title',
'description': 'test description',
'tagline': 'test rocks',
'website': 'http://test.cloudron.io',
'contactEmail': 'support@cloudron.io',
'version': '0.1.0',
'manifestVersion': 1,
'dockerImage': 'cloudron/test:25.2.0',
'healthCheckPath': '/',
'httpPort': 7777,
'tcpPorts': {
'ECHO_SERVER_PORT': {
'title': 'Echo Server Port',
'description': 'Echo server',
'containerPort': 7778
}
},
'addons': {
'oauth': { },
'redis': { },
'mysql': { },
'postgresql': { }
"id": "io.cloudron.test",
"author": "The Presidents Of the United States Of America",
"title": "test title",
"description": "test description",
"tagline": "test rocks",
"website": "http://test.cloudron.io",
"contactEmail": "support@cloudron.io",
"version": "0.1.0",
"manifestVersion": 1,
"dockerImage": "cloudron/test:25.2.0",
"healthCheckPath": "/",
"httpPort": 7777,
"tcpPorts": {
"ECHO_SERVER_PORT": {
"title": "Echo Server Port",
"description": "Echo server",
"containerPort": 7778
}
},
"addons": {
"oauth": { },
"redis": { },
"mysql": { },
"postgresql": { }
}
};
const DOMAIN_0 = {
@@ -57,8 +57,7 @@ const DOMAIN_0 = {
accessKeyId: 'accessKeyId',
secretAccessKey: 'secretAccessKey',
endpoint: 'http://localhost:5353'
},
tlsConfig: { provider: 'caas' }
}
};
var APP = {
@@ -90,7 +89,7 @@ describe('apptask', function () {
awsHostedZones = {
HostedZones: [{
Id: '/hostedzone/ZONEID',
Name: `${DOMAIN_0.domain}.`,
Name: config.fqdn() + '.',
CallerReference: '305AFD59-9D73-4502-B020-F4E6F889CB30',
ResourceRecordSetCount: 2,
ChangeInfo: {
@@ -105,9 +104,10 @@ describe('apptask', function () {
async.series([
database.initialize,
database._clear,
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0.zoneName, DOMAIN_0.provider, DOMAIN_0.config, null, DOMAIN_0.tlsConfig),
domains.update.bind(null, DOMAIN_0.domain, DOMAIN_0.provider, DOMAIN_0.config, null),
appdb.add.bind(null, APP.id, APP.appStoreId, APP.manifest, APP.location, APP.domain, APP.portBindings, APP),
settings.initialize
settings.initialize,
settings.setTlsConfig.bind(null, { provider: 'caas' })
], done);
});
@@ -133,7 +133,7 @@ describe('apptask', function () {
});
it('configure nginx correctly', function (done) {
apptask._configureReverseProxy(APP, function (error) {
apptask._configureNginx(APP, function (error) {
expect(fs.existsSync(paths.NGINX_APPCONFIG_DIR + '/' + APP.id + '.conf'));
// expect(error).to.be(null); // this fails because nginx cannot be restarted
done();
@@ -141,7 +141,7 @@ describe('apptask', function () {
});
it('unconfigure nginx', function (done) {
apptask._unconfigureReverseProxy(APP, function (error) {
apptask._unconfigureNginx(APP, function (error) {
expect(!fs.existsSync(paths.NGINX_APPCONFIG_DIR + '/' + APP.id + '.conf'));
// expect(error).to.be(null); // this fails because nginx cannot be restarted
done();
@@ -243,7 +243,7 @@ describe('apptask', function () {
.get('/2013-04-01/hostedzone')
.times(2)
.reply(200, js2xml('ListHostedZonesResponse', awsHostedZones, { wrapHandlers: { HostedZones: () => 'HostedZone'} }))
.get('/2013-04-01/hostedzone/ZONEID/rrset?maxitems=1&name=applocation.' + DOMAIN_0.domain + '.&type=A')
.get('/2013-04-01/hostedzone/ZONEID/rrset?maxitems=1&name=applocation.' + config.fqdn() + '.&type=A')
.reply(200, js2xml('ListResourceRecordSetsResponse', { ResourceRecordSets: [ ] }, { 'Content-Type': 'application/xml' }))
.post('/2013-04-01/hostedzone/ZONEID/rrset/')
.reply(200, js2xml('ChangeResourceRecordSetsResponse', { ChangeInfo: { Id: 'RRID', Status: 'INSYNC' } }));
@@ -6,27 +6,18 @@
'use strict';
var async = require('async'),
certificates = require('../certificates.js'),
config = require('../config.js'),
database = require('../database.js'),
domains = require('../domains.js'),
expect = require('expect.js'),
reverseProxy = require('../reverseproxy.js'),
settings = require('../settings.js');
const DOMAIN_0 = {
domain: 'example-reverseproxy-test.com',
zoneName: 'example-reverseproxy-test.com',
provider: 'noop',
config: {},
fallbackCertificate: null,
tlsConfig: { provider: 'fallback' }
};
function setup(done) {
async.series([
database.initialize,
settings.initialize,
database._clear,
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0.zoneName, DOMAIN_0.provider, DOMAIN_0.config, DOMAIN_0.fallbackCertificate, DOMAIN_0.tlsConfig)
certificates.initialize,
database._clear
], done);
}
@@ -58,66 +49,76 @@ describe('Certificates', function () {
var validCert2 = '-----BEGIN CERTIFICATE-----\nMIIBwjCCAWwCCQCZjm6jL50XfTANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJE\nRTEPMA0GA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4xEDAOBgNVBAoMB05l\nYnVsb24xDDAKBgNVBAsMA0NUTzEXMBUGA1UEAwwOYmF6LmZvb2Jhci5jb20wHhcN\nMTYxMTA4MDgyMDE1WhcNMjAxMTA3MDgyMDE1WjBoMQswCQYDVQQGEwJERTEPMA0G\nA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4xEDAOBgNVBAoMB05lYnVsb24x\nDDAKBgNVBAsMA0NUTzEXMBUGA1UEAwwOYmF6LmZvb2Jhci5jb20wXDANBgkqhkiG\n9w0BAQEFAANLADBIAkEAtKoyTPrf2DjKbnW7Xr1HbRvV+quHTcGmUq5anDI7G4w/\nabqDXGYyakHHlPyZxYp7FWQxCm83rHUuDT1LiLIBZQIDAQABMA0GCSqGSIb3DQEB\nCwUAA0EAVaD2Q6bF9hcUUBev5NyjaMdDYURuWfjuwWUkb8W50O2ed3O+MATKrDdS\nyVaBy8W02KJ4Y1ym4je/MF8nilPurA==\n-----END CERTIFICATE-----';
var validKey2 = '-----BEGIN RSA PRIVATE KEY-----\nMIIBPQIBAAJBALSqMkz639g4ym51u169R20b1fqrh03BplKuWpwyOxuMP2m6g1xm\nMmpBx5T8mcWKexVkMQpvN6x1Lg09S4iyAWUCAwEAAQJBAJXu7YHPbjfuoalcUZzF\nbuKRCFtZQRf5z0Os6QvZ8A3iR0SzYJzx+c2ibp7WdifMXp3XaKm4tHSOfumrjUIq\nt10CIQDrs9Xo7bq0zuNjUV5IshNfaiYKZRfQciRVW2O8xBP9VwIhAMQ5CCEDZy+u\nsaF9RtmB0bjbe6XonBlAzoflfH/MAwWjAiEA50hL+ohr0MfCMM7DKaozgEj0kvan\n645VQLywnaX5x3kCIQDCwjinS9FnKmV0e/uOd6PJb0/S5IXLKt/TUpu33K5DMQIh\nAM9peu3B5t9pO59MmeUGZwI+bEJfEb+h03WTptBxS3pO\n-----END RSA PRIVATE KEY-----';
it('allows both null', function () {
expect(certificates.validateCertificate(null, null, 'foobar.com')).to.be(null);
});
it('does not allow only cert', function () {
expect(certificates.validateCertificate('cert', null, 'foobar.com')).to.be.an(Error);
});
it('does not allow only key', function () {
expect(certificates.validateCertificate(null, 'key', 'foobar.com')).to.be.an(Error);
});
it('does not allow empty string for cert', function () {
expect(reverseProxy.validateCertificate('foobar.com', '', 'key')).to.be.an(Error);
expect(certificates.validateCertificate('', 'key', 'foobar.com')).to.be.an(Error);
});
it('does not allow empty string for key', function () {
expect(reverseProxy.validateCertificate('foobar.com', 'cert', '')).to.be.an(Error);
expect(certificates.validateCertificate('cert', '', 'foobar.com')).to.be.an(Error);
});
it('does not allow invalid cert', function () {
expect(reverseProxy.validateCertificate('foobar.com', 'someinvalidcert', validKey0)).to.be.an(Error);
expect(certificates.validateCertificate('someinvalidcert', validKey0, 'foobar.com')).to.be.an(Error);
});
it('does not allow invalid key', function () {
expect(reverseProxy.validateCertificate('foobar.com', validCert0, 'invalidkey')).to.be.an(Error);
expect(certificates.validateCertificate(validCert0, 'invalidkey', 'foobar.com')).to.be.an(Error);
});
it('does not allow cert without matching domain', function () {
expect(reverseProxy.validateCertificate('cloudron.io', validCert0, validKey0)).to.be.an(Error);
expect(certificates.validateCertificate(validCert0, validKey0, 'cloudron.io')).to.be.an(Error);
});
it('allows valid cert with matching domain', function () {
expect(reverseProxy.validateCertificate('foobar.com', validCert0, validKey0)).to.be(null);
expect(certificates.validateCertificate(validCert0, validKey0, 'foobar.com')).to.be(null);
});
it('allows valid cert with matching domain (wildcard)', function () {
expect(reverseProxy.validateCertificate('abc.foobar.com', validCert1, validKey1)).to.be(null);
expect(certificates.validateCertificate(validCert1, validKey1, 'abc.foobar.com')).to.be(null);
});
it('does now allow cert without matching domain (wildcard)', function () {
expect(reverseProxy.validateCertificate('foobar.com', validCert1, validKey1)).to.be.an(Error);
expect(reverseProxy.validateCertificate('bar.abc.foobar.com', validCert1, validKey1)).to.be.an(Error);
expect(certificates.validateCertificate(validCert1, validKey1, 'foobar.com')).to.be.an(Error);
expect(certificates.validateCertificate(validCert1, validKey1, 'bar.abc.foobar.com')).to.be.an(Error);
});
it('allows valid cert with matching domain (subdomain)', function () {
expect(reverseProxy.validateCertificate('baz.foobar.com', validCert2, validKey2)).to.be(null);
expect(certificates.validateCertificate(validCert2, validKey2, 'baz.foobar.com')).to.be(null);
});
it('does not allow cert without matching domain (subdomain)', function () {
expect(reverseProxy.validateCertificate('baz.foobar.com', validCert0, validKey0)).to.be.an(Error);
expect(certificates.validateCertificate(validCert0, validKey0, 'baz.foobar.com')).to.be.an(Error);
});
it('does not allow invalid cert/key tuple', function () {
expect(reverseProxy.validateCertificate('foobar.com', validCert0, validKey1)).to.be.an(Error);
expect(certificates.validateCertificate(validCert0, validKey1, 'foobar.com')).to.be.an(Error);
});
});
describe('getApi - caas', function () {
before(function (done) {
DOMAIN_0.tlsConfig = { provider: 'caas' };
async.series([
setup,
domains.update.bind(null, DOMAIN_0.domain, DOMAIN_0.provider, DOMAIN_0.config, DOMAIN_0.fallbackCertificate, DOMAIN_0.tlsConfig)
settings.setTlsConfig.bind(null, { provider: 'caas' })
], done);
});
after(cleanup);
it('returns prod caas for prod cloudron', function (done) {
reverseProxy._getApi({ domain: DOMAIN_0.domain }, function (error, api, options) {
certificates._getApi({ }, function (error, api, options) {
expect(error).to.be(null);
expect(api._name).to.be('caas');
expect(options.prod).to.be(true);
@@ -126,7 +127,7 @@ describe('Certificates', function () {
});
it('returns prod caas for dev cloudron', function (done) {
reverseProxy._getApi({ domain: DOMAIN_0.domain }, function (error, api, options) {
certificates._getApi({ }, function (error, api, options) {
expect(error).to.be(null);
expect(api._name).to.be('caas');
expect(options.prod).to.be(true);
@@ -135,7 +136,7 @@ describe('Certificates', function () {
});
it('returns prod-acme with altDomain in prod cloudron', function (done) {
reverseProxy._getApi({ domain: DOMAIN_0.domain, altDomain: 'foo.something.com' }, function (error, api, options) {
certificates._getApi({ altDomain: 'foo.something.com' }, function (error, api, options) {
expect(error).to.be(null);
expect(api._name).to.be('acme');
expect(options.prod).to.be(true);
@@ -144,7 +145,7 @@ describe('Certificates', function () {
});
it('returns prod acme with altDomain in dev cloudron', function (done) {
reverseProxy._getApi({ domain: DOMAIN_0.domain, altDomain: 'foo.something.com' }, function (error, api, options) {
certificates._getApi({ altDomain: 'foo.something.com' }, function (error, api, options) {
expect(error).to.be(null);
expect(api._name).to.be('acme');
expect(options.prod).to.be(true);
@@ -153,20 +154,18 @@ describe('Certificates', function () {
});
});
describe('getApi - letsencrypt-prod', function () {
describe('getApi - le-prod', function () {
before(function (done) {
DOMAIN_0.tlsConfig = { provider: 'letsencrypt-prod' };
async.series([
setup,
domains.update.bind(null, DOMAIN_0.domain, DOMAIN_0.provider, DOMAIN_0.config, DOMAIN_0.fallbackCertificate, DOMAIN_0.tlsConfig)
settings.setTlsConfig.bind(null, { provider: 'le-prod' })
], done);
});
after(cleanup);
it('returns prod acme in prod cloudron', function (done) {
reverseProxy._getApi({ domain: DOMAIN_0.domain }, function (error, api, options) {
certificates._getApi({ }, function (error, api, options) {
expect(error).to.be(null);
expect(api._name).to.be('acme');
expect(options.prod).to.be(true);
@@ -175,7 +174,7 @@ describe('Certificates', function () {
});
it('returns prod acme with altDomain in prod cloudron', function (done) {
reverseProxy._getApi({ domain: DOMAIN_0.domain, altDomain: 'foo.bar.com' }, function (error, api, options) {
certificates._getApi({ altDomain: 'foo.bar.com' }, function (error, api, options) {
expect(error).to.be(null);
expect(api._name).to.be('acme');
expect(options.prod).to.be(true);
@@ -184,7 +183,7 @@ describe('Certificates', function () {
});
it('returns prod acme in dev cloudron', function (done) {
reverseProxy._getApi({ domain: DOMAIN_0.domain }, function (error, api, options) {
certificates._getApi({ }, function (error, api, options) {
expect(error).to.be(null);
expect(api._name).to.be('acme');
expect(options.prod).to.be(true);
@@ -193,20 +192,18 @@ describe('Certificates', function () {
});
});
describe('getApi - letsencrypt-staging', function () {
describe('getApi - le-staging', function () {
before(function (done) {
DOMAIN_0.tlsConfig = { provider: 'letsencrypt-staging' };
async.series([
setup,
domains.update.bind(null, DOMAIN_0.domain, DOMAIN_0.provider, DOMAIN_0.config, DOMAIN_0.fallbackCertificate, DOMAIN_0.tlsConfig)
settings.setTlsConfig.bind(null, { provider: 'le-staging' })
], done);
});
after(cleanup);
it('returns staging acme in prod cloudron', function (done) {
reverseProxy._getApi({ domain: DOMAIN_0.domain }, function (error, api, options) {
certificates._getApi({ }, function (error, api, options) {
expect(error).to.be(null);
expect(api._name).to.be('acme');
expect(options.prod).to.be(false);
@@ -215,7 +212,7 @@ describe('Certificates', function () {
});
it('returns staging acme in dev cloudron', function (done) {
reverseProxy._getApi({ domain: DOMAIN_0.domain }, function (error, api, options) {
certificates._getApi({ }, function (error, api, options) {
expect(error).to.be(null);
expect(api._name).to.be('acme');
expect(options.prod).to.be(false);
@@ -224,7 +221,7 @@ describe('Certificates', function () {
});
it('returns staging acme with altDomain in prod cloudron', function (done) {
reverseProxy._getApi({ domain: DOMAIN_0.domain, altDomain: 'foo.bar.com' }, function (error, api, options) {
certificates._getApi({ altDomain: 'foo.bar.com' }, function (error, api, options) {
expect(error).to.be(null);
expect(api._name).to.be('acme');
expect(options.prod).to.be(false);
+10 -7
View File
@@ -31,14 +31,15 @@ describe('config', function () {
});
it('did set default values', function () {
expect(config.adminDomain()).to.equal('');
expect(config.fqdn()).to.equal('');
expect(config.adminFqdn()).to.equal('');
expect(config.zoneName()).to.equal('');
expect(config.adminLocation()).to.equal('my');
});
it('set saves value in file', function (done) {
config.set('version', '1.3.0');
expect(JSON.parse(fs.readFileSync(path.join(config.baseDir(), 'configs/cloudron.conf'))).version).to.eql('1.3.0');
config.set('fqdn', 'example.com');
expect(JSON.parse(fs.readFileSync(path.join(config.baseDir(), 'configs/cloudron.conf'))).fqdn).to.eql('example.com');
done();
});
@@ -55,8 +56,8 @@ describe('config', function () {
});
it('set - object', function (done) {
config.set( { adminDomain: 'something.com' } );
expect(config.adminDomain()).to.eql('something.com');
config.set( { fqdn: 'something.com' } );
expect(config.fqdn()).to.eql('something.com');
done();
});
@@ -64,18 +65,20 @@ describe('config', function () {
config.setFqdn('example.com');
config.setAdminFqdn('my.example.com');
expect(config.adminDomain()).to.equal('example.com');
expect(config.fqdn()).to.equal('example.com');
expect(config.adminFqdn()).to.equal('my.example.com');
expect(config.adminOrigin()).to.equal('https://my.example.com');
expect(config.zoneName()).to.equal('example.com');
});
it('uses hyphen locations with non-custom domain', function () {
config.setFqdn('test.example.com');
config.setAdminFqdn('my-test.example.com');
expect(config.adminDomain()).to.equal('test.example.com');
expect(config.fqdn()).to.equal('test.example.com');
expect(config.adminLocation()).to.equal('my');
expect(config.adminOrigin()).to.equal('https://my-test.example.com');
expect(config.zoneName()).to.equal('example.com');
});
it('can set arbitrary values', function (done) {
+62 -130
View File
@@ -19,7 +19,7 @@ var appdb = require('../appdb.js'),
groupdb = require('../groupdb.js'),
hat = require('hat'),
mailboxdb = require('../mailboxdb.js'),
maildb = require('../maildb.js'),
path = require('path'),
settingsdb = require('../settingsdb.js'),
tokendb = require('../tokendb.js'),
userdb = require('../userdb.js'),
@@ -30,7 +30,6 @@ var USER_0 = {
username: 'uuid0',
password: 'secret',
email: 'safe@me.com',
fallbackEmail: 'safer@me.com',
salt: 'morton',
createdAt: 'sometime back',
modifiedAt: 'now',
@@ -43,7 +42,6 @@ var USER_1 = {
username: 'uuid1',
password: 'secret',
email: 'safe2@me.com',
fallbackEmail: 'safer2@me.com',
salt: 'tata',
createdAt: 'sometime back',
modifiedAt: 'now',
@@ -56,7 +54,6 @@ var USER_2 = {
username: 'uuid2',
password: 'secret',
email: 'safe3@me.com',
fallbackEmail: 'safer3@me.com',
salt: 'tata',
createdAt: 'sometime back',
modifiedAt: 'now',
@@ -64,28 +61,12 @@ var USER_2 = {
displayName: 'Herbert 2'
};
const DOMAIN_0 = {
domain: 'foobar.com',
zoneName: 'foobar.com',
provider: 'digitalocean',
config: { token: 'abcd' },
tlsConfig: { provider: 'fallback' }
};
const DOMAIN_1 = {
domain: 'foo.cloudron.io',
zoneName: 'cloudron.io',
provider: 'manual',
config: null,
tlsConfig: { provider: 'fallback' }
};
const TEST_DOMAIN = {
domain: 'example.com',
zoneName: 'example.com',
provider: 'manual',
config: {},
tlsConfig: { provider: 'fallback' }
config: {
}
};
describe('database', function () {
@@ -107,16 +88,30 @@ describe('database', function () {
});
describe('domains', function () {
const DOMAIN_0 = {
domain: 'foobar.com',
zoneName: 'foobar.com',
provider: 'digitalocean',
config: { token: 'abcd' }
};
const DOMAIN_1 = {
domain: 'foo.cloudron.io',
zoneName: 'cloudron.io',
provider: 'manual',
config: null
};
it('can add domain', function (done) {
domaindb.add(DOMAIN_0.domain, { zoneName: DOMAIN_0.zoneName, provider: DOMAIN_0.provider, config: DOMAIN_0.config, tlsConfig: DOMAIN_0.tlsConfig }, done);
domaindb.add(DOMAIN_0.domain, DOMAIN_0.zoneName, DOMAIN_0.provider, DOMAIN_0.config, done);
});
it('can add another domain', function (done) {
domaindb.add(DOMAIN_1.domain, { zoneName: DOMAIN_1.zoneName, provider: DOMAIN_1.provider, config: DOMAIN_1.config, tlsConfig: DOMAIN_1.tlsConfig }, done);
domaindb.add(DOMAIN_1.domain, DOMAIN_1.zoneName, DOMAIN_1.provider, DOMAIN_1.config, done);
});
it('cannot add same domain twice', function (done) {
domaindb.add(DOMAIN_0.domain, { zoneName: DOMAIN_0.zoneName, provider: DOMAIN_0.provider, config: DOMAIN_0.config, tlsConfig: DOMAIN_0.tlsConfig }, function (error) {
domaindb.add(DOMAIN_0.domain, DOMAIN_0.zoneName, DOMAIN_0.provider, DOMAIN_0.config, function (error) {
expect(error).to.be.ok();
expect(error.reason).to.be(DatabaseError.ALREADY_EXISTS);
done();
@@ -137,9 +132,8 @@ describe('database', function () {
it('can update domain', function (done) {
const newConfig = { provider: 'manual' };
const newTlsConfig = { provider: 'foobar' };
domaindb.update(DOMAIN_1.domain, { provider: DOMAIN_1.provider, config: newConfig, tlsConfig: newTlsConfig }, function (error) {
domaindb.update(DOMAIN_1.domain, DOMAIN_1.provider, newConfig, function (error) {
expect(error).to.equal(null);
domaindb.get(DOMAIN_1.domain, function (error, result) {
@@ -149,10 +143,8 @@ describe('database', function () {
expect(result.zoneName).to.equal(DOMAIN_1.zoneName);
expect(result.provider).to.equal(DOMAIN_1.provider);
expect(result.config).to.eql(newConfig);
expect(result.tlsConfig).to.eql(newTlsConfig);
DOMAIN_1.config = newConfig;
DOMAIN_1.tlsConfig = newTlsConfig;
done();
});
@@ -163,20 +155,23 @@ describe('database', function () {
domaindb.getAll(function (error, result) {
expect(error).to.equal(null);
expect(result).to.be.an('array');
expect(result.length).to.equal(2);
expect(result.length).to.equal(3); // includes the TEST_DOMAIN
// sorted by domain
expect(result[0].domain).to.equal(DOMAIN_1.domain);
expect(result[0].zoneName).to.equal(DOMAIN_1.zoneName);
expect(result[0].provider).to.equal(DOMAIN_1.provider);
expect(result[0].config).to.eql(DOMAIN_1.config);
expect(result[0].tlsConfig).to.eql(DOMAIN_1.tlsConfig);
expect(result[0].domain).to.equal(TEST_DOMAIN.domain);
expect(result[0].zoneName).to.equal(TEST_DOMAIN.zoneName);
expect(result[0].provider).to.equal(TEST_DOMAIN.provider);
expect(result[0].config).to.eql(TEST_DOMAIN.config);
expect(result[1].domain).to.equal(DOMAIN_0.domain);
expect(result[1].zoneName).to.equal(DOMAIN_0.zoneName);
expect(result[1].provider).to.equal(DOMAIN_0.provider);
expect(result[1].config).to.eql(DOMAIN_0.config);
expect(result[1].tlsConfig).to.eql(DOMAIN_0.tlsConfig);
expect(result[1].domain).to.equal(DOMAIN_1.domain);
expect(result[1].zoneName).to.equal(DOMAIN_1.zoneName);
expect(result[1].provider).to.equal(DOMAIN_1.provider);
expect(result[1].config).to.eql(DOMAIN_1.config);
expect(result[2].domain).to.equal(DOMAIN_0.domain);
expect(result[2].zoneName).to.equal(DOMAIN_0.zoneName);
expect(result[2].provider).to.equal(DOMAIN_0.provider);
expect(result[2].config).to.eql(DOMAIN_0.config);
done();
});
@@ -364,7 +359,7 @@ describe('database', function () {
});
});
it('can update the user', function (done) {
it('can update the user', function (done) {
userdb.update(USER_0.id, { email: 'some@thing.com', displayName: 'Heiter' }, function (error) {
expect(error).to.not.be.ok();
userdb.get(USER_0.id, function (error, user) {
@@ -705,7 +700,7 @@ describe('database', function () {
installationProgress: null,
runState: null,
location: 'some-location-0',
domain: DOMAIN_0.domain,
domain: 'example.com',
manifest: { version: '0.1', dockerImage: 'docker/app0', healthCheckPath: '/', httpPort: 80, title: 'app0' },
httpPort: null,
containerId: null,
@@ -723,7 +718,6 @@ describe('database', function () {
robotsTxt: null,
enableBackup: true
};
var APP_1 = {
id: 'appid-1',
appStoreId: 'appStoreId-1',
@@ -732,7 +726,7 @@ describe('database', function () {
installationProgress: null,
runState: null,
location: 'some-location-1',
domain: DOMAIN_0.domain,
domain: 'example.com',
manifest: { version: '0.2', dockerImage: 'docker/app1', healthCheckPath: '/', httpPort: 80, title: 'app1' },
httpPort: null,
containerId: null,
@@ -751,14 +745,6 @@ describe('database', function () {
enableBackup: true
};
before(function (done) {
domaindb.add(DOMAIN_0.domain, { zoneName: DOMAIN_0.zoneName, provider: DOMAIN_0.provider, config: DOMAIN_0.config, tlsConfig: DOMAIN_0.tlsConfig }, done);
});
after(function (done) {
database._clear(done);
});
it('add fails due to missing arguments', function () {
expect(function () { appdb.add(APP_0.id, APP_0.manifest, APP_0.installationState, function () {}); }).to.throwError();
expect(function () { appdb.add(APP_0.id, function () {}); }).to.throwError();
@@ -1423,7 +1409,7 @@ describe('database', function () {
eventlogdb.getAllPaged(null, null, 1, 100, function (error, results) {
expect(error).to.be(null);
expect(results.length).to.be(2);
results = results.sort(function (x, y) { return x.action > y.action; }); // because equal timestamp gives random ordering
results = results.sort(function (x, y) { return x.action > y.action }); // because equal timestamp gives random ordering
expect(results[1].action).to.be.eql('persistent.event');
expect(results[0].action).to.be.eql('anotherpersistent.event');
@@ -1593,40 +1579,42 @@ describe('database', function () {
describe('mailboxes', function () {
before(function (done) {
domaindb.add(DOMAIN_0.domain, { zoneName: DOMAIN_0.zoneName, provider: DOMAIN_0.provider, config: DOMAIN_0.config, tlsConfig: DOMAIN_0.tlsConfig }, done);
});
config._reset();
config.setFqdn(TEST_DOMAIN.domain);
after(function (done) {
database._clear(done);
async.series([
database.initialize,
database._clear
], done);
});
it('add user mailbox succeeds', function (done) {
mailboxdb.add('girish', DOMAIN_0.domain, 'uid-0', mailboxdb.TYPE_USER, function (error, mailbox) {
mailboxdb.add('girish', TEST_DOMAIN.domain, 'uid-0', mailboxdb.TYPE_USER, function (error, mailbox) {
expect(error).to.be(null);
done();
});
});
it('cannot add dup entry', function (done) {
mailboxdb.add('girish', DOMAIN_0.domain, 'uid-1', mailboxdb.TYPE_APP, function (error, mailbox) {
mailboxdb.add('girish', TEST_DOMAIN.domain, 'uid-1', mailboxdb.TYPE_APP, function (error, mailbox) {
expect(error.reason).to.be(DatabaseError.ALREADY_EXISTS);
done();
});
});
it('add app mailbox succeeds', function (done) {
mailboxdb.add('support', DOMAIN_0.domain, 'osticket', mailboxdb.TYPE_APP, function (error, mailbox) {
mailboxdb.add('support', TEST_DOMAIN.domain, 'osticket', mailboxdb.TYPE_APP, function (error, mailbox) {
expect(error).to.be(null);
done();
});
});
it('get succeeds', function (done) {
mailboxdb.getMailbox('support', DOMAIN_0.domain, function (error, mailbox) {
mailboxdb.getMailbox('support', TEST_DOMAIN.domain, function (error, mailbox) {
expect(error).to.be(null);
expect(mailbox.name).to.equal('support');
expect(mailbox.ownerId).to.equal('osticket');
expect(mailbox.domain).to.equal(DOMAIN_0.domain);
expect(mailbox.domain).to.equal(TEST_DOMAIN.domain);
expect(mailbox.creationTime).to.be.a(Date);
done();
@@ -1634,7 +1622,7 @@ describe('database', function () {
});
it('list mailboxes succeeds', function (done) {
mailboxdb.listMailboxes(DOMAIN_0.domain, function (error, mailboxes) {
mailboxdb.listMailboxes(TEST_DOMAIN.domain, function (error, mailboxes) {
expect(error).to.be(null);
expect(mailboxes.length).to.be(2);
expect(mailboxes[0].name).to.be('girish');
@@ -1646,14 +1634,14 @@ describe('database', function () {
});
it('can set alias', function (done) {
mailboxdb.setAliasesForName('support', DOMAIN_0.domain, [ 'support2', 'help' ], function (error) {
mailboxdb.setAliasesForName('support', TEST_DOMAIN.domain, [ 'support2', 'help' ], function (error) {
expect(error).to.be(null);
done();
});
});
it('can get aliases of name', function (done) {
mailboxdb.getAliasesForName('support', DOMAIN_0.domain, function (error, results) {
mailboxdb.getAliasesForName('support', TEST_DOMAIN.domain, function (error, results) {
expect(error).to.be(null);
expect(results.length).to.be(2);
expect(results[0]).to.be('help');
@@ -1663,7 +1651,7 @@ describe('database', function () {
});
it('can get alias', function (done) {
mailboxdb.getAlias('support2', DOMAIN_0.domain, function (error, result) {
mailboxdb.getAlias('support2', TEST_DOMAIN.domain, function (error, result) {
expect(error).to.be(null);
expect(result.name).to.be('support2');
expect(result.aliasTarget).to.be('support');
@@ -1672,7 +1660,7 @@ describe('database', function () {
});
it('can list aliases', function (done) {
mailboxdb.listAliases(DOMAIN_0.domain, function (error, results) {
mailboxdb.listAliases(TEST_DOMAIN.domain, function (error, results) {
expect(error).to.be(null);
expect(results.length).to.be(2);
expect(results[0].name).to.be('help');
@@ -1694,22 +1682,22 @@ describe('database', function () {
});
it('cannot get non-existing group', function (done) {
mailboxdb.getGroup('random', DOMAIN_0.domain, function (error) {
mailboxdb.getGroup('random', TEST_DOMAIN.domain, function (error) {
expect(error.reason).to.be(DatabaseError.NOT_FOUND);
done();
});
});
it('can change name', function (done) {
mailboxdb.updateName('support', DOMAIN_0.domain, 'support3', DOMAIN_0.domain, function (error) {
mailboxdb.updateName('support', TEST_DOMAIN.domain, 'support3', TEST_DOMAIN.domain, function (error) {
expect(error).to.be(null);
mailboxdb.updateName('support3', DOMAIN_0.domain, 'support', DOMAIN_0.domain, done);
mailboxdb.updateName('support3', TEST_DOMAIN.domain, 'support', TEST_DOMAIN.domain, done);
});
});
it('cannot change name to existing one', function (done) {
mailboxdb.updateName('support', DOMAIN_0.domain, 'support2', DOMAIN_0.domain, function (error) {
mailboxdb.updateName('support', TEST_DOMAIN.domain, 'support2', TEST_DOMAIN.domain, function (error) {
expect(error).to.be.ok();
expect(error.reason).to.eql(DatabaseError.ALREADY_EXISTS);
@@ -1718,10 +1706,10 @@ describe('database', function () {
});
it('unset aliases', function (done) {
mailboxdb.setAliasesForName('support', DOMAIN_0.domain, [], function (error) {
mailboxdb.setAliasesForName('support', TEST_DOMAIN.domain, [], function (error) {
expect(error).to.be(null);
mailboxdb.getAliasesForName('support', DOMAIN_0.domain, function (error, results) {
mailboxdb.getAliasesForName('support', TEST_DOMAIN.domain, function (error, results) {
expect(error).to.be(null);
expect(results.length).to.be(0);
done();
@@ -1730,7 +1718,7 @@ describe('database', function () {
});
it('del succeeds', function (done) {
mailboxdb.del('girish', DOMAIN_0.domain, function (error) {
mailboxdb.del('girish', TEST_DOMAIN.domain, function (error) {
expect(error).to.be(null);
done();
});
@@ -1748,60 +1736,4 @@ describe('database', function () {
});
});
});
describe('mail', function () {
const MAIL_DOMAIN_0 = {
domain: DOMAIN_0.domain,
enabled: false,
relay: { provider: 'cloudron-smtp' },
catchAll: [ ],
mailFromValidation: true
};
before(function (done) {
domaindb.add(DOMAIN_0.domain, { zoneName: DOMAIN_0.zoneName, provider: DOMAIN_0.provider, config: DOMAIN_0.config, tlsConfig: DOMAIN_0.tlsConfig }, done);
});
after(function (done) {
database._clear(done);
});
it('cannot add non-existing domain', function (done) {
maildb.add(MAIL_DOMAIN_0.domain + 'nope', function (error) {
expect(error).to.be.ok();
expect(error.reason).to.be(DatabaseError.NOT_FOUND);
done();
});
});
it('can add domain', function (done) {
maildb.add(MAIL_DOMAIN_0.domain, function (error) {
expect(error).to.equal(null);
done();
});
});
it('can get all domains', function (done) {
maildb.getAll(function (error, result) {
expect(error).to.equal(null);
expect(result).to.be.an(Array);
expect(result[0]).to.be.an('object');
expect(result[0].domain).to.eql(MAIL_DOMAIN_0.domain);
done();
});
});
it('can get domain', function (done) {
maildb.get(MAIL_DOMAIN_0.domain, function (error, result) {
expect(error).to.equal(null);
expect(result).to.be.an('object');
expect(result).to.eql(MAIL_DOMAIN_0);
done();
});
});
});
});

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