Compare commits

..

1 Commits

Author SHA1 Message Date
Girish Ramakrishnan 15cf0c3c19 Do not allow dns setup and restore to run in parallel
In the e2e, we did not check the webadminStatus after a dnsSetup
and immediately rushed into restore. This ended up mangling the
cert/key files of the admin domain.
2018-02-04 15:08:48 -08:00
102 changed files with 7883 additions and 4492 deletions
+1 -2
View File
@@ -1,7 +1,6 @@
# following files are skipped when exporting using git archive
test export-ignore
.jshintrc export-ignore
.gitlab export-ignore
docs export-ignore
.gitattributes export-ignore
.gitignore export-ignore
-6
View File
@@ -1,6 +0,0 @@
Please do not use this issue tracker for support requests and bug reports.
This issue tracker is used by the Cloudron development team to track actual
bugs in the code.
Please use the forum at https://forum.cloudron.io to report bugs. For
confidential issues, please email us at support@cloudron.io.
-7
View File
@@ -1,7 +0,0 @@
Please do not use this issue tracker for support requests and feature reports.
This issue tracker is used by the Cloudron development team to track issues in
the code.
Please use the forum at https://forum.cloudron.io to report bugs. For
confidential issues, please email us at support@cloudron.io.
-1
View File
@@ -2,7 +2,6 @@
"node": true,
"browser": true,
"unused": true,
"multistr": true,
"globalstrict": true,
"predef": [ "angular", "$" ],
"esnext": true
-58
View File
@@ -1190,61 +1190,3 @@
* Add DigitalOcean Spaces region Singapore 1 (SGP1)
* Configure Exoscale SOS to use new SOS NG endpoint
* Fix S3 storage backend CopySource encoding rules
[1.10.1]
* Migrate mailboxes to support multiple domains
* Update addon containers to latest versions
* Add DigitalOcean Spaces region Singapore 1 (SGP1)
* Configure Exoscale SOS to use new SOS NG endpoint
* Fix S3 storage backend CopySource encoding rules
[1.10.2]
* Migrate mailboxes to support multiple domains
* Update addon containers to latest versions
* Add DigitalOcean Spaces region Singapore 1 (SGP1)
* Configure Exoscale SOS to use new SOS NG endpoint
* Fix S3 storage backend CopySource encoding rules
[1.11.0]
* Update Haraka to 2.8.17 to fix various crashes
* Report dependency error for clone if backup or domain was not found
* Enable auto-updates for major versions
[2.0.0]
* Multi-domain support
* Update Haraka to 2.8.18
* Split box and app autoupdate pattern settings
* Stop and disable any pre-installed postfix server
* Migrate altDomain as a manual DNS provider
* Use node's native dns resolve instead of dig
* DNS records can now be a A record or a CNAME record
* Fix generation of fallback certificates to include naked domain
* Merge multi-string DKIM records
* scheduler: do not start cron jobs all at once
* scheduler: give cron jobs a grace period of 30 minutes to complete
[2.0.1]
* Multi-domain support
* Update Haraka to 2.8.18
* Split box and app autoupdate pattern settings
* Stop and disable any pre-installed postfix server
* Migrate altDomain as a manual DNS provider
* Use node's native dns resolve instead of dig
* DNS records can now be a A record or a CNAME record
* Fix generation of fallback certificates to include naked domain
* Merge multi-string DKIM records
* scheduler: do not start cron jobs all at once
* scheduler: give cron jobs a grace period of 30 minutes to complete
* Rework the eventlog view
* App clone now clones the robotsTxt and backup settings
[2.1.0]
* Make S3 backend work reliably with slow internet connections
* Update docker to 18.03.0-ce
* Finalize the Email and Mailbox API
* Move mailbox settings from users to email view
* mail: fix issue where hosts with valid SPF for a Cloudron domain are unable to send mail to Cloudron
* mail: fix crash when bounce emails have a null sender
* Add CSP header for dashboard
* Add support for installing private docker images
+1 -6
View File
@@ -48,11 +48,6 @@ apps up-to-date and secure.
* [Selfhosting](https://cloudron.io/documentation/installation/) - [Pricing](https://cloudron.io/pricing.html)
* [Managed Hosting](https://cloudron.io/managed.html)
**Note:** This repo is a small part of what gets installed on your server - there is
the dashboard, database addons, graph container, base image etc. Cloudron also relies
on external services such as the App Store for apps to be installed. As such, don't
clone this repo and npm install and expect something to work.
## Documentation
* [Documentation](https://cloudron.io/documentation/)
@@ -64,6 +59,6 @@ the containers in the Cloudron.
## Community
* [Forum](https://forum.cloudron.io/)
* [Chat](https://chat.cloudron.io/)
* [Support](mailto:support@cloudron.io)
-4
View File
@@ -105,7 +105,3 @@ systemctl disable bind9 || true
systemctl stop dnsmasq || true
systemctl disable dnsmasq || true
# on ssdnodes postfix seems to run by default
systemctl stop postfix || true
systemctl disable postfix || true
@@ -11,7 +11,6 @@ exports.up = function(db, callback) {
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) {
@@ -22,14 +21,13 @@ exports.up = function(db, callback) {
defaultEmail = user.email;
fallbackEmail = user.email;
} else {
defaultEmail = user.username ? (user.username + '@' + mailDomains[0].domain) : user.email;
defaultEmail = user.username + '@' + mailDomains[0].domain;
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);
});
@@ -1,24 +0,0 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
db.runSql('SELECT * FROM settings WHERE name=?', ['autoupdate_pattern'], function (error, results) {
if (error || results.length === 0) return callback(error); // will use defaults from box code
// migrate the 'daily' update pattern
var appUpdatePattern = results[0].value;
if (appUpdatePattern === '00 00 1,3,5,23 * * *') appUpdatePattern = '00 30 1,3,5,23 * * *';
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
db.runSql.bind(db, 'DELETE FROM settings WHERE name=?', ['autoupdate_pattern']),
db.runSql.bind(db, 'INSERT settings (name, value) VALUES(?, ?)', ['app_autoupdate_pattern', appUpdatePattern]),
db.runSql.bind(db, 'COMMIT')
], callback);
});
};
exports.down = function(db, callback) {
callback();
};
@@ -1,121 +0,0 @@
'use strict';
var async = require('async'),
crypto = require('crypto'),
fs = require('fs'),
os = require('os'),
path = require('path'),
safe = require('safetydance'),
tldjs = require('tldjs');
exports.up = function(db, callback) {
db.all('SELECT * FROM apps', function (error, apps) {
if (error) return callback(error);
async.eachSeries(apps, function (app, callback) {
if (!app.altDomain) {
console.log('App %s does not use altDomain, skip', app.id);
return callback();
}
const domain = tldjs.getDomain(app.altDomain);
const subdomain = tldjs.getSubdomain(app.altDomain);
const mailboxName = (subdomain ? subdomain : JSON.parse(app.manifestJson).title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app';
console.log('App %s is on domain %s and subdomain %s with mailbox', app.id, domain, subdomain, mailboxName);
async.series([
// Add domain if not exists
function (callback) {
const query = 'INSERT INTO domains (domain, zoneName, provider, configJson, tlsConfigJson) VALUES (?, ?, ?, ?, ?)';
const args = [ domain, domain, 'manual', JSON.stringify({}), JSON.stringify({ provider: 'letsencrypt-prod' }) ];
db.runSql(query, args, function (error) {
if (error && error.code !== 'ER_DUP_ENTRY') return callback(error);
console.log('Added domain %s', domain);
// ensure we have a fallback cert for the newly added domain. This is the same as in reverseproxy.js
// WARNING this will only work on the cloudron itself not during local testing!
const certFilePath = `/home/yellowtent/boxdata/certs/${domain}.host.cert`;
const keyFilePath = `/home/yellowtent/boxdata/certs/${domain}.host.key`;
if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) { // generate it
let opensslConf = safe.fs.readFileSync('/etc/ssl/openssl.cnf', 'utf8');
let opensslConfWithSan = `${opensslConf}\n[SAN]\nsubjectAltName=DNS:${domain}\n`;
let configFile = path.join(os.tmpdir(), 'openssl-' + crypto.randomBytes(4).readUInt32LE(0) + '.conf');
let certCommand = `openssl req -x509 -newkey rsa:2048 -keyout ${keyFilePath} -out ${certFilePath} -days 3650 -subj /CN=*.${domain} -extensions SAN -config ${configFile} -nodes`;
safe.fs.writeFileSync(configFile, opensslConfWithSan, 'utf8');
if (!safe.child_process.execSync(certCommand)) return callback(safe.error.message);
safe.fs.unlinkSync(configFile);
}
callback();
});
},
// Add domain to mail table if not exists
function (callback) {
const query = 'INSERT INTO mail (domain, enabled, mailFromValidation, catchAllJson, relayJson) VALUES (?, ?, ?, ?, ?)';
const args = [ domain, 0, 1, '[]', JSON.stringify({ provider: 'cloudron-smtp' }) ];
db.runSql(query, args, function (error) {
if (error && error.code !== 'ER_DUP_ENTRY') return callback(error);
console.log('Added domain %s to mail table', domain);
callback();
});
},
// Remove old mailbox record if any
function (callback) {
const query = 'DELETE FROM mailboxes WHERE ownerId=?';
const args = [ app.id ];
db.runSql(query, args, function (error) {
if (error) return callback(error);
console.log('Cleaned up mailbox record for app %s', app.id);
callback();
});
},
// Add new mailbox record
function (callback) {
const query = 'INSERT INTO mailboxes (name, domain, ownerId, ownerType) VALUES (?, ?, ?, ?)';
const args = [ mailboxName, domain, app.id, 'app' /* mailboxdb.TYPE_APP */ ];
db.runSql(query, args, function (error) {
if (error) return callback(error);
console.log('Added mailbox record for app %s', app.id);
callback();
});
},
// Update app record
function (callback) {
const query = 'UPDATE apps SET location=?, domain=?, altDomain=? WHERE id=?';
const args = [ subdomain, domain, '', app.id ];
db.runSql(query, args, function (error) {
if (error) return error;
console.log('Updated app %s with new domain', app.id);
callback();
});
}
], callback);
}, function (error) {
if (error) return callback(error);
// finally drop the altDomain db field
db.runSql('ALTER TABLE apps DROP COLUMN altDomain', [], callback);
});
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN altDomain VARCHAR(256)', [], callback);
};
@@ -1,19 +0,0 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
db.runSql.bind(db, 'ALTER TABLE mailboxes DROP FOREIGN KEY mailboxes_domain_constraint'),
db.runSql.bind(db, 'ALTER TABLE mailboxes ADD CONSTRAINT mailboxes_domain_constraint FOREIGN KEY(domain) REFERENCES mail(domain)'),
db.runSql.bind(db, 'COMMIT')
], callback);
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE mailboxes DROP FOREIGN KEY mailboxes_domain_constraint', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,51 +0,0 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
var users = { }, groupMembers = { };
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
db.runSql.bind(db, 'ALTER TABLE mailboxes ADD COLUMN membersJson TEXT'),
function getUsers(done) {
db.all('SELECT * from users', [ ], function (error, results) {
if (error) return done(error);
results.forEach(function (result) { users[result.id] = result; });
done();
});
},
function getGroups(done) {
db.all('SELECT id, name, GROUP_CONCAT(groupMembers.userId) AS userIds ' +
' FROM groups LEFT OUTER JOIN groupMembers ON groups.id = groupMembers.groupId ' +
' GROUP BY groups.id', [ ], function (error, results) {
if (error) return done(error);
results.forEach(function (result) {
var userIds = result.userIds ? result.userIds.split(',') : [];
var members = userIds.map(function (id) { return users[id].username; });
groupMembers[result.id] = members;
});
done();
});
},
function removeGroupIdAndSetMembers(done) {
async.eachSeries(Object.keys(groupMembers), function (gid, iteratorDone) {
console.log(`Migrating group id ${gid} to ${JSON.stringify(groupMembers[gid])}`);
db.runSql('UPDATE mailboxes SET membersJson = ?, ownerId = ? WHERE ownerId = ?', [ JSON.stringify(groupMembers[gid]), 'admin', gid ], iteratorDone);
}, done);
},
db.runSql.bind(db, 'COMMIT')
], callback);
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE mailboxes DROP COLUMN membersJson', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,34 +0,0 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
db.runSql.bind(db, 'ALTER TABLE mailboxes ADD COLUMN type VARCHAR(16)'),
function addMailboxType(done) {
db.all('SELECT * from mailboxes', [ ], function (error, results) {
if (error) return done(error);
async.eachSeries(results, function (mailbox, iteratorCallback) {
let type = 'mailbox';
if (mailbox.aliasTarget) {
type = 'alias';
} else if (mailbox.membersJson) {
type = 'list';
}
db.runSql('UPDATE mailboxes SET type = ? WHERE name = ?', [ type, mailbox.name ], iteratorCallback);
}, done);
});
},
db.runSql.bind(db, 'ALTER TABLE mailboxes MODIFY type VARCHAR(16) NOT NULL'),
db.runSql.bind(db, 'COMMIT')
], callback);
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE mailboxes DROP COLUMN membersJson', function (error) {
if (error) console.error(error);
callback(error);
});
};
+19 -19
View File
@@ -72,6 +72,7 @@ CREATE TABLE IF NOT EXISTS apps(
createdAt TIMESTAMP(2) NOT NULL DEFAULT CURRENT_TIMESTAMP,
updatedAt TIMESTAMP(2) NOT NULL DEFAULT CURRENT_TIMESTAMP,
memoryLimit BIGINT DEFAULT 0,
altDomain VARCHAR(256),
xFrameOptions VARCHAR(512),
sso BOOLEAN DEFAULT 1, // whether user chose to enable SSO
debugModeJson TEXT, // options for development mode
@@ -80,8 +81,8 @@ CREATE TABLE IF NOT EXISTS apps(
// the following fields do not belong here, they can be removed when we use a queue for apptask
restoreConfigJson VARCHAR(256), // used to pass backupId to restore from to apptask
oldConfigJson TEXT, // used to pass old config to apptask (configure, restore)
updateConfigJson TEXT, // used to pass new config to apptask (update)
oldConfigJson TEXT, // used to pass old config for apptask (configure, restore)
updateConfigJson TEXT, // used to pass new config for apptask (update)
FOREIGN KEY(domain) REFERENCES domains(domain),
PRIMARY KEY(id));
@@ -129,10 +130,25 @@ CREATE TABLE IF NOT EXISTS eventlog(
action VARCHAR(128) NOT NULL,
source TEXT, /* { userId, username, ip }. userId can be null for cron,sysadmin */
data TEXT, /* free flowing json based on action */
createdAt TIMESTAMP(2) NOT NULL,
creationTime TIMESTAMP, /* FIXME: precision must be TIMESTAMP(2) */
PRIMARY KEY (id));
/* Future fields:
* accessRestriction - to determine who can access it. So this has foreign keys
* quota - per mailbox quota
*/
CREATE TABLE IF NOT EXISTS mailboxes(
name VARCHAR(128) NOT NULL,
ownerId VARCHAR(128) NOT NULL, /* app id or user id or group id */
ownerType VARCHAR(16) NOT NULL, /* 'app' or 'user' or 'group' */
aliasTarget VARCHAR(128), /* the target name type is an alias */
creationTime TIMESTAMP,
domain VARCHAR(128),
FOREIGN KEY(domain) REFERENCES domains(domain),
UNIQUE (name, domain));
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 */
@@ -158,20 +174,4 @@ CREATE TABLE IF NOT EXISTS mail(
CHARACTER SET utf8 COLLATE utf8_bin;
/* Future fields:
* accessRestriction - to determine who can access it. So this has foreign keys
* quota - per mailbox quota
*/
CREATE TABLE IF NOT EXISTS mailboxes(
name VARCHAR(128) NOT NULL,
type VARCHAR(16) NOT NULL, /* 'mailbox', 'alias', 'list' */
ownerId VARCHAR(128) NOT NULL, /* app id or user id or group id */
ownerType VARCHAR(16) NOT NULL, /* 'app' or 'user' or 'group' */
aliasTarget VARCHAR(128), /* the target name type is an alias */
membersJson TEXT, /* members of a group */
creationTime TIMESTAMP,
domain VARCHAR(128),
FOREIGN KEY(domain) REFERENCES mail(domain),
UNIQUE (name, domain));
+6212 -1693
View File
File diff suppressed because it is too large Load Diff
+26 -16
View File
@@ -14,11 +14,11 @@
"node": ">=4.0.0 <=4.1.1"
},
"dependencies": {
"@google-cloud/dns": "^0.7.1",
"@google-cloud/storage": "^1.6.0",
"@google-cloud/dns": "^0.7.0",
"@google-cloud/storage": "^1.2.1",
"@sindresorhus/df": "^2.1.0",
"async": "^2.6.0",
"aws-sdk": "^2.201.0",
"aws-sdk": "^2.151.0",
"body-parser": "^1.18.2",
"cloudron-manifestformat": "^2.11.0",
"connect-ensure-login": "^0.1.1",
@@ -28,24 +28,24 @@
"cookie-session": "^1.3.2",
"cron": "^1.3.0",
"csurf": "^1.6.6",
"db-migrate": "^0.10.5",
"db-migrate": "^0.10.0-beta.24",
"db-migrate-mysql": "^1.1.10",
"debug": "^3.1.0",
"dockerode": "^2.5.4",
"dockerode": "^2.5.3",
"ejs": "^2.5.7",
"ejs-cli": "^2.0.0",
"express": "^4.16.2",
"express-session": "^1.15.6",
"hat": "0.0.3",
"json": "^9.0.3",
"ldapjs": "^1.0.2",
"ldapjs": "^1.0.0",
"lodash.chunk": "^4.2.0",
"mime": "^2.2.0",
"mime": "^2.0.3",
"moment-timezone": "^0.5.14",
"morgan": "^1.9.0",
"multiparty": "^4.1.2",
"mysql": "^2.15.0",
"nodemailer": "^4.6.0",
"nodemailer": "^4.4.0",
"nodemailer-smtp-transport": "^2.7.4",
"oauth2orize": "^1.11.0",
"once": "^1.3.2",
@@ -62,31 +62,41 @@
"request": "^2.83.0",
"s3-block-read-stream": "^0.2.0",
"safetydance": "^0.7.1",
"semver": "^5.5.0",
"semver": "^5.4.1",
"showdown": "^1.8.2",
"split": "^1.0.0",
"superagent": "^3.8.1",
"supererror": "^0.7.1",
"tar-fs": "^1.16.0",
"tar-stream": "^1.5.5",
"tldjs": "^2.3.1",
"tldjs": "^2.2.0",
"underscore": "^1.7.0",
"uuid": "^3.2.1",
"uuid": "^3.1.0",
"valid-url": "^1.0.9",
"validator": "^9.4.1",
"ws": "^3.3.3"
"validator": "^9.1.1",
"ws": "^3.3.1"
},
"devDependencies": {
"bootstrap-sass": "^3.3.3",
"expect.js": "*",
"gulp": "^3.9.1",
"gulp-autoprefixer": "^4.0.0",
"gulp-concat": "^2.4.3",
"gulp-cssnano": "^2.1.0",
"gulp-ejs": "^3.1.0",
"gulp-sass": "^3.1.0",
"gulp-serve": "^1.0.0",
"gulp-sourcemaps": "^2.6.1",
"gulp-uglify": "^3.0.0",
"hock": "^1.3.2",
"istanbul": "*",
"js2xmlparser": "^3.0.0",
"mocha": "^5.0.1",
"mocha": "*",
"mock-aws-s3": "git+https://github.com/cloudron-io/mock-aws-s3.git",
"nock": "^9.0.14",
"node-sass": "^4.6.1",
"readdirp": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz",
"rimraf": "^2.6.2"
"yargs": "^10.0.3"
},
"scripts": {
"migrate_local": "DATABASE_URL=mysql://root:@localhost/box node_modules/.bin/db-migrate up",
@@ -96,6 +106,6 @@
"postmerge": "/bin/true",
"precommit": "/bin/true",
"prepush": "npm test",
"dashboard": "node_modules/.bin/gulp"
"webadmin": "node_modules/.bin/gulp"
}
}
+13 -20
View File
@@ -2,6 +2,16 @@
set -eu -o pipefail
if [[ ${EUID} -ne 0 ]]; then
echo "This script should be run as root." > /dev/stderr
exit 1
fi
if [[ $(lsb_release -rs) != "16.04" ]]; then
echo "Cloudron requires Ubuntu 16.04" > /dev/stderr
exit 1
fi
# change this to a hash when we make a upgrade release
readonly LOG_FILE="/var/log/cloudron-setup.log"
readonly DATA_FILE="/root/cloudron-install-data.json"
@@ -16,10 +26,6 @@ readonly physical_memory=$(LC_ALL=C free -m | awk '/Mem:/ { print $2 }')
readonly disk_size_bytes=$(LC_ALL=C df --output=size / | tail -n1)
readonly disk_size_gb=$((${disk_size_bytes}/1024/1024))
readonly RED='\033[31m'
readonly GREEN='\033[32m'
readonly DONE='\033[m'
# verify the system has minimum requirements met
if [[ "${rootfs_type}" != "ext4" ]]; then
echo "Error: Cloudron requires '/' to be ext4" # see #364
@@ -75,18 +81,6 @@ while true; do
esac
done
# Only --help works as non-root
if [[ ${EUID} -ne 0 ]]; then
echo "This script should be run as root." > /dev/stderr
exit 1
fi
# Only --help works with mismatched ubuntu
if [[ $(lsb_release -rs) != "16.04" ]]; then
echo "Cloudron requires Ubuntu 16.04" > /dev/stderr
exit 1
fi
# validate arguments in the absence of data
if [[ -z "${provider}" ]]; then
echo "--provider is required (azure, cloudscale, digitalocean, ec2, exoscale, hetzner, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic)"
@@ -94,7 +88,6 @@ if [[ -z "${provider}" ]]; then
elif [[ \
"${provider}" != "ami" && \
"${provider}" != "azure" && \
"${provider}" != "caas" && \
"${provider}" != "cloudscale" && \
"${provider}" != "digitalocean" && \
"${provider}" != "ec2" && \
@@ -126,7 +119,7 @@ echo ""
echo " Follow setup logs in a second terminal with:"
echo " $ tail -f ${LOG_FILE}"
echo ""
echo " Join us at https://forum.cloudron.io for any questions."
echo " Join us at https://chat.cloudron.io for any questions."
echo ""
if [[ "${initBaseImage}" == "true" ]]; then
@@ -205,10 +198,10 @@ while true; do
sleep 10
done
echo -e "\n\n${GREEN}Visit https://<IP> and accept the self-signed certificate to finish setup.${DONE}"
echo -e "\n\nVisit https://<IP> to finish setup once the server has rebooted.\n"
if [[ "${rebootServer}" == "true" ]]; then
echo -e "\n${RED}Rebooting this server now to let changes take effect.${DONE}\n"
echo -e "\n\nRebooting this server now to let bootloader changes take effect.\n"
systemctl stop mysql # sometimes mysql ends up having corrupt privilege tables
systemctl reboot
fi
+39 -27
View File
@@ -29,8 +29,8 @@ if ! $(cd "${SOURCE_DIR}" && git diff --exit-code >/dev/null); then
exit 1
fi
if ! $(cd "${SOURCE_DIR}/../dashboard" && git diff --exit-code >/dev/null); then
echo "You have local changes in dashboard, stash or commit them to proceed"
if ! $(cd "${SOURCE_DIR}/../webadmin" && git diff --exit-code >/dev/null); then
echo "You have local changes in webadmin, stash or commit them to proceed"
exit 1
fi
@@ -40,41 +40,53 @@ if [[ "$(node --version)" != "v8.9.3" ]]; then
fi
box_version=$(cd "${SOURCE_DIR}" && git rev-parse "HEAD")
branch=$(git rev-parse --abbrev-ref HEAD)
if [[ "${branch}" == "master" ]]; then
dashboard_version=$(cd "${SOURCE_DIR}/../dashboard" && git rev-parse "${branch}")
else
dashboard_version=$(cd "${SOURCE_DIR}/../dashboard" && git fetch && git rev-parse "origin/${branch}")
fi
webadmin_version=$(cd "${SOURCE_DIR}/../webadmin" && git rev-parse "HEAD")
bundle_dir=$(mktemp -d -t box 2>/dev/null || mktemp -d box-XXXXXXXXXX --tmpdir=$TMPDIR)
[[ -z "$bundle_file" ]] && bundle_file="${TMPDIR}/box-${box_version:0:10}-${dashboard_version:0:10}.tar.gz"
[[ -z "$bundle_file" ]] && bundle_file="${TMPDIR}/box-${box_version:0:10}-${webadmin_version:0:10}.tar.gz"
chmod "o+rx,g+rx" "${bundle_dir}" # otherwise extracted tarball director won't be readable by others/group
echo "==> Checking out code box version [${box_version}] and dashboard version [${dashboard_version}] into ${bundle_dir}"
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}/../dashboard" && git archive --format=tar ${dashboard_version} | (mkdir -p "${bundle_dir}/dashboard.build" && cd "${bundle_dir}/dashboard.build" && tar xf -))
(cp "${SOURCE_DIR}/../dashboard/LICENSE" "${bundle_dir}")
(cd "${SOURCE_DIR}/../webadmin" && git archive --format=tar ${webadmin_version} | (cd "${bundle_dir}" && tar xf -))
(cp "${SOURCE_DIR}/../webadmin/LICENSE" "${bundle_dir}")
echo "==> Installing modules for dashboard asset generation"
(cd "${bundle_dir}/dashboard.build" && npm install --production)
if diff "${TMPDIR}/boxtarball.cache/package-lock.json.all" "${bundle_dir}/package-lock.json" >/dev/null 2>&1; then
echo "Reusing dev modules from cache"
cp -r "${TMPDIR}/boxtarball.cache/node_modules-all/." "${bundle_dir}/node_modules"
else
echo "Installing modules with dev dependencies"
(cd "${bundle_dir}" && npm install)
echo "==> Building dashboard assets"
(cd "${bundle_dir}/dashboard.build" && ./node_modules/.bin/gulp --revision ${dashboard_version})
echo "Caching dev dependencies"
mkdir -p "${TMPDIR}/boxtarball.cache/node_modules-all"
rsync -a --delete "${bundle_dir}/node_modules/" "${TMPDIR}/boxtarball.cache/node_modules-all/"
cp "${bundle_dir}/package-lock.json" "${TMPDIR}/boxtarball.cache/package-lock.json.all"
fi
echo "==> Move built dashboard assets into destination"
mkdir -p "${bundle_dir}/dashboard"
mv "${bundle_dir}/dashboard.build/dist" "${bundle_dir}/dashboard/"
echo "Building webadmin assets"
(cd "${bundle_dir}" && ./node_modules/.bin/gulp)
echo "==> Cleanup dashboard build artifacts"
rm -rf "${bundle_dir}/dashboard.build"
echo "Remove intermediate files required at build-time only"
rm -rf "${bundle_dir}/node_modules/"
rm -rf "${bundle_dir}/webadmin/src"
rm -rf "${bundle_dir}/gulpfile.js"
echo "==> Installing toplevel node modules"
(cd "${bundle_dir}" && npm install --production --no-optional)
if diff "${TMPDIR}/boxtarball.cache/package-lock.json.prod" "${bundle_dir}/package-lock.json" >/dev/null 2>&1; then
echo "Reusing prod modules from cache"
cp -r "${TMPDIR}/boxtarball.cache/node_modules-prod/." "${bundle_dir}/node_modules"
else
echo "Installing modules for production"
(cd "${bundle_dir}" && npm install --production --no-optional)
echo "==> Create final tarball"
echo "Caching prod dependencies"
mkdir -p "${TMPDIR}/boxtarball.cache/node_modules-prod"
rsync -a --delete "${bundle_dir}/node_modules/" "${TMPDIR}/boxtarball.cache/node_modules-prod/"
cp "${bundle_dir}/package-lock.json" "${TMPDIR}/boxtarball.cache/package-lock.json.prod"
fi
echo "Create final tarball"
(cd "${bundle_dir}" && tar czf "${bundle_file}" .)
echo "==> Cleaning up ${bundle_dir}"
echo "Cleaning up ${bundle_dir}"
rm -rf "${bundle_dir}"
echo "==> Tarball saved at ${bundle_file}"
echo "Tarball saved at ${bundle_file}"
+3 -3
View File
@@ -35,11 +35,11 @@ while true; do
done
echo "==> installer: updating docker"
if [[ $(docker version --format {{.Client.Version}}) != "18.03.0-ce" ]]; then
$curl -sL https://download.docker.com/linux/ubuntu/dists/xenial/pool/stable/amd64/docker-ce_18.03.0~ce-0~ubuntu_amd64.deb -o /tmp/docker.deb
if [[ $(docker version --format {{.Client.Version}}) != "17.09.0-ce" ]]; then
$curl -sL https://download.docker.com/linux/ubuntu/dists/xenial/pool/stable/amd64/docker-ce_17.09.0~ce-0~ubuntu_amd64.deb -o /tmp/docker.deb
# https://download.docker.com/linux/ubuntu/dists/xenial/stable/binary-amd64/Packages
if [[ $(sha256sum /tmp/docker.deb | cut -d' ' -f1) != "1f7315b5723b849fe542fe973b0edb4164a0200e926d386ac14363a968f9e4fc" ]]; then
if [[ $(sha256sum /tmp/docker.deb | cut -d' ' -f1) != "d33f6eb134f0ab0876148bd96de95ea47d583d7f2cddfdc6757979453f9bd9bf" ]]; then
echo "==> installer: docker binary download is corrupt"
exit 5
fi
+2 -2
View File
@@ -214,8 +214,8 @@ cat > "${CONFIG_DIR}/cloudron.conf" <<CONF_END
}
CONF_END
echo "==> Creating config.json for dashboard"
cat > "${BOX_SRC_DIR}/dashboard/dist/config.json" <<CONF_END
echo "==> Creating config.json for webadmin"
cat > "${BOX_SRC_DIR}/webadmin/dist/config.json" <<CONF_END
{
"webServerOrigin": "${arg_web_server_origin}"
}
+4 -10
View File
@@ -66,9 +66,8 @@ server {
# https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html
ssl_prefer_server_ciphers on;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # don't use SSLv3 ref: POODLE
# ciphers according to https://mozilla.github.io/server-side-tls/ssl-config-generator/?server=nginx-1.10.3&openssl=1.0.2g&hsts=yes&profile=modern
ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
# ciphers according to https://weakdh.org/sysadmin.html
ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';
ssl_dhparam /home/yellowtent/boxdata/dhparams.pem;
add_header Strict-Transport-Security "max-age=15768000";
@@ -90,11 +89,6 @@ server {
add_header Referrer-Policy "no-referrer-when-downgrade";
proxy_hide_header Referrer-Policy;
# CSP headers for the admin/dashboard resources
<% if ( endpoint === 'admin' ) { -%>
add_header Content-Security-Policy "default-src 'none'; connect-src wss: https: 'self' *.cloudron.io; script-src https: 'self' 'unsafe-inline' 'unsafe-eval'; img-src * data:; style-src https: 'unsafe-inline'; object-src 'none'; font-src https: 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self';";
<% } -%>
proxy_http_version 1.1;
proxy_intercept_errors on;
proxy_read_timeout 3500;
@@ -112,7 +106,7 @@ server {
proxy_set_header Connection $connection_upgrade;
# only serve up the status page if we get proxy gateway errors
root <%= sourceDir %>/dashboard/dist;
root <%= sourceDir %>/webadmin/dist;
error_page 502 503 504 /appstatus.html;
location /appstatus.html {
internal;
@@ -166,7 +160,7 @@ server {
# }
location / {
root <%= sourceDir %>/dashboard/dist;
root <%= sourceDir %>/webadmin/dist;
index index.html index.htm;
}
<% } else if ( endpoint === 'app' ) { %>
+18 -53
View File
@@ -28,7 +28,6 @@ var appdb = require('./appdb.js'),
generatePassword = require('password-generator'),
hat = require('hat'),
infra = require('./infra_version.js'),
mail = require('./mail.js'),
mailboxdb = require('./mailboxdb.js'),
once = require('once'),
path = require('path'),
@@ -113,9 +112,10 @@ var KNOWN_ADDONS = {
var RMAPPDIR_CMD = path.join(__dirname, 'scripts/rmappdir.sh');
function debugApp(app, args) {
assert(typeof app === 'object');
assert(!app || typeof app === 'object');
debug(app.fqdn + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
var prefix = app ? app.intrinsicFqdn : '(no app)';
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
}
function setupAddons(app, addons, callback) {
@@ -250,7 +250,7 @@ function setupOauth(app, options, callback) {
if (!app.sso) return callback(null);
var appId = app.id;
var redirectURI = 'https://' + app.fqdn;
var redirectURI = 'https://' + (app.altDomain || app.intrinsicFqdn);
var scope = 'profile';
clients.delByAppIdAndType(appId, clients.TYPE_OAUTH, function (error) { // remove existing creds
@@ -291,27 +291,20 @@ function setupEmail(app, options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
mail.getDomains(function (error, mailDomains) {
if (error) return callback(error);
// note that "external" access info can be derived from MAIL_DOMAIN (since it's part of user documentation)
var env = [
{ name: 'MAIL_SMTP_SERVER', value: 'mail' },
{ name: 'MAIL_SMTP_PORT', value: '2525' },
{ name: 'MAIL_IMAP_SERVER', value: 'mail' },
{ name: 'MAIL_IMAP_PORT', value: '9993' },
{ name: 'MAIL_SIEVE_SERVER', value: 'mail' },
{ name: 'MAIL_SIEVE_PORT', value: '4190' },
{ name: 'MAIL_DOMAIN', value: app.domain }
];
const mailInDomains = mailDomains.filter(function (d) { return d.enabled; }).map(function (d) { return d.domain; }).join(',');
debugApp(app, 'Setting up Email');
// note that "external" access info can be derived from MAIL_DOMAIN (since it's part of user documentation)
var env = [
{ name: 'MAIL_SMTP_SERVER', value: 'mail' },
{ name: 'MAIL_SMTP_PORT', value: '2525' },
{ name: 'MAIL_IMAP_SERVER', value: 'mail' },
{ name: 'MAIL_IMAP_PORT', value: '9993' },
{ name: 'MAIL_SIEVE_SERVER', value: 'mail' },
{ name: 'MAIL_SIEVE_PORT', value: '4190' },
{ name: 'MAIL_DOMAIN', value: app.domain },
{ name: 'MAIL_DOMAINS', value: mailInDomains }
];
debugApp(app, 'Setting up Email');
appdb.setAddonConfig(app.id, 'email', env, callback);
});
appdb.setAddonConfig(app.id, 'email', env, callback);
}
function teardownEmail(app, options, callback) {
@@ -467,10 +460,6 @@ function teardownMySql(app, options, callback) {
}
function backupMySql(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'Backing up mysql');
callback = once(callback); // ChildProcess exit may or may not be called after error
@@ -484,10 +473,6 @@ function backupMySql(app, options, callback) {
}
function restoreMySql(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
callback = once(callback); // ChildProcess exit may or may not be called after error
setupMySql(app, options, function (error) {
@@ -540,10 +525,6 @@ function teardownPostgreSql(app, options, callback) {
}
function backupPostgreSql(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'Backing up postgresql');
callback = once(callback); // ChildProcess exit may or may not be called after error
@@ -557,10 +538,6 @@ function backupPostgreSql(app, options, callback) {
}
function restorePostgreSql(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
callback = once(callback);
setupPostgreSql(app, options, function (error) {
@@ -614,10 +591,6 @@ function teardownMongoDb(app, options, callback) {
}
function backupMongoDb(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'Backing up mongodb');
callback = once(callback); // ChildProcess exit may or may not be called after error
@@ -631,10 +604,6 @@ function backupMongoDb(app, options, callback) {
}
function restoreMongoDb(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
callback = once(callback); // ChildProcess exit may or may not be called after error
setupMongoDb(app, options, function (error) {
@@ -678,7 +647,7 @@ function setupRedis(app, options, callback) {
}
const tag = infra.images.redis.tag, redisName = 'redis-' + app.id;
const label = app.fqdn;
const label = app.intrinsicFqdn;
// note that we do not add appId label because this interferes with the stop/start app logic
const cmd = `docker run --restart=always -d --name=${redisName} \
--label=location=${label} \
@@ -728,7 +697,7 @@ function teardownRedis(app, options, callback) {
safe.fs.unlinkSync(paths.ADDON_CONFIG_DIR, 'redis-' + app.id + '_vars.sh');
shell.sudo('teardownRedis', [ RMAPPDIR_CMD, app.id + '/redis', true /* delete directory */ ], function (error /* ,stdout , stderr*/) {
shell.sudo('teardownRedis', [ RMAPPDIR_CMD, app.id + '/redis', true /* delete directory */ ], function (error, stdout, stderr) {
if (error) return callback(new Error('Error removing redis data:' + error));
appdb.unsetAddonConfig(app.id, 'redis', callback);
@@ -737,10 +706,6 @@ function teardownRedis(app, options, callback) {
}
function backupRedis(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'Backing up redis');
var cmd = [ '/addons/redis/service.sh', 'backup' ]; // the redis dir is volume mounted
+6 -7
View File
@@ -61,7 +61,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.xFrameOptions', 'apps.sso', 'apps.debugModeJson', 'apps.robotsTxt', 'apps.enableBackup',
'apps.altDomain', 'apps.xFrameOptions', 'apps.sso', 'apps.debugModeJson', 'apps.robotsTxt', 'apps.enableBackup',
'apps.creationTime', 'apps.updateTime' ].join(',');
var PORT_BINDINGS_FIELDS = [ 'hostPort', 'environmentVariable', 'appId' ].join(',');
@@ -196,18 +196,17 @@ function add(id, appStoreId, manifest, location, domain, portBindings, data, cal
var accessRestriction = data.accessRestriction || null;
var accessRestrictionJson = JSON.stringify(accessRestriction);
var memoryLimit = data.memoryLimit || 0;
var altDomain = data.altDomain || null;
var xFrameOptions = data.xFrameOptions || '';
var installationState = data.installationState || exports.ISTATE_PENDING_INSTALL;
var restoreConfigJson = data.restoreConfig ? JSON.stringify(data.restoreConfig) : null; // used when cloning
var sso = 'sso' in data ? data.sso : null;
var robotsTxt = 'robotsTxt' in data ? data.robotsTxt : null;
var debugModeJson = data.debugMode ? JSON.stringify(data.debugMode) : null;
var queries = [];
queries.push({
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, location, domain, accessRestrictionJson, memoryLimit, xFrameOptions, restoreConfigJson, sso, debugModeJson, robotsTxt) ' +
' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, manifestJson, installationState, location, domain, accessRestrictionJson, memoryLimit, xFrameOptions, restoreConfigJson, sso, debugModeJson, robotsTxt ]
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, location, domain, accessRestrictionJson, memoryLimit, altDomain, xFrameOptions, restoreConfigJson, sso, debugModeJson) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, manifestJson, installationState, location, domain, accessRestrictionJson, memoryLimit, altDomain, xFrameOptions, restoreConfigJson, sso, debugModeJson ]
});
Object.keys(portBindings).forEach(function (env) {
@@ -220,8 +219,8 @@ function add(id, appStoreId, manifest, location, domain, portBindings, data, cal
// only allocate a mailbox if mailboxName is set
if (data.mailboxName) {
queries.push({
query: 'INSERT INTO mailboxes (name, type, domain, ownerId, ownerType) VALUES (?, ?, ?, ?, ?)',
args: [ data.mailboxName, mailboxdb.TYPE_MAILBOX, domain, id, mailboxdb.OWNER_TYPE_APP ]
query: 'INSERT INTO mailboxes (name, domain, ownerId, ownerType) VALUES (?, ?, ?, ?)',
args: [ data.mailboxName, domain, id, mailboxdb.TYPE_APP ]
});
}
+7 -5
View File
@@ -5,6 +5,7 @@ var appdb = require('./appdb.js'),
assert = require('assert'),
async = require('async'),
DatabaseError = require('./databaseerror.js'),
config = require('./config.js'),
debug = require('debug')('box:apphealthmonitor'),
docker = require('./docker.js').connection,
mailer = require('./mailer.js'),
@@ -23,9 +24,13 @@ var gRunTimeout = null;
var gDockerEventStream = null;
function debugApp(app) {
assert(typeof app === 'object');
assert(!app || typeof app === 'object');
debug(app.fqdn + ' ' + app.manifest.id + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)) + ' - ' + app.id);
var prefix = app ? app.intrinsicFqdn : '(no app)';
var manifestAppId = app ? app.manifest.id : '';
var id = app ? app.id : '';
debug(prefix + ' ' + manifestAppId + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)) + ' - ' + id);
}
function setHealth(app, health, callback) {
@@ -66,9 +71,6 @@ function setHealth(app, health, callback) {
// callback is called with error for fatal errors and not if health check failed
function checkAppHealth(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
if (app.installationState !== appdb.ISTATE_INSTALLED || app.runState !== appdb.RSTATE_RUNNING) {
debugApp(app, 'skipped. istate:%s rstate:%s', app.installationState, app.runState);
return callback(null);
+83 -76
View File
@@ -172,14 +172,14 @@ function validatePortBindings(portBindings, tcpPorts) {
993, /* imaps */
2003, /* graphite (lo) */
2004, /* graphite (lo) */
2020, /* mail server */
2020, /* install server */
config.get('port'), /* app server (lo) */
config.get('sysadminPort'), /* sysadmin app server (lo) */
config.get('smtpPort'), /* internal smtp port (lo) */
config.get('ldapPort'), /* ldap server (lo) */
3306, /* mysql (lo) */
4190, /* managesieve */
8000, /* graphite (lo) */
8000 /* graphite (lo) */
];
if (!portBindings) return null;
@@ -306,19 +306,17 @@ function getDuplicateErrorDetails(location, portBindings, error) {
return new AppsError(AppsError.ALREADY_EXISTS);
}
// app configs that is useful for 'archival' into the app backup config.json
function getAppConfig(app) {
return {
manifest: app.manifest,
location: app.location,
domain: app.domain,
fqdn: app.fqdn,
intrinsicFqdn: app.intrinsicFqdn,
accessRestriction: app.accessRestriction,
portBindings: app.portBindings,
memoryLimit: app.memoryLimit,
xFrameOptions: app.xFrameOptions || 'SAMEORIGIN',
robotsTxt: app.robotsTxt,
sso: app.sso
altDomain: app.altDomain
};
}
@@ -364,8 +362,10 @@ function get(appId, callback) {
domaindb.get(app.domain, function (error, result) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
app.intrinsicFqdn = domains.fqdn(app.location, app.domain, result.provider);
app.iconUrl = getIconUrlSync(app);
app.fqdn = domains.fqdn(app.location, app.domain, result.provider);
app.fqdn = app.altDomain || app.intrinsicFqdn;
app.cnameTarget = app.altDomain ? app.intrinsicFqdn : null;
callback(null, app);
});
@@ -386,8 +386,10 @@ function getByIpAddress(ip, callback) {
domaindb.get(app.domain, function (error, result) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
app.intrinsicFqdn = domains.fqdn(app.location, app.domain, result.provider);
app.iconUrl = getIconUrlSync(app);
app.fqdn = domains.fqdn(app.location, app.domain, result.provider);
app.fqdn = app.altDomain || app.intrinsicFqdn;
app.cnameTarget = app.altDomain ? app.intrinsicFqdn : null;
callback(null, app);
});
@@ -405,8 +407,10 @@ function getAll(callback) {
domaindb.get(app.domain, function (error, result) {
if (error) return iteratorDone(new AppsError(AppsError.INTERNAL_ERROR, error));
app.intrinsicFqdn = domains.fqdn(app.location, app.domain, result.provider);
app.iconUrl = getIconUrlSync(app);
app.fqdn = domains.fqdn(app.location, app.domain, result.provider);
app.fqdn = app.altDomain || app.intrinsicFqdn;
app.cnameTarget = app.altDomain ? app.intrinsicFqdn : null;
iteratorDone();
});
@@ -464,6 +468,7 @@ function install(data, auditSource, callback) {
cert = data.cert || null,
key = data.key || null,
memoryLimit = data.memoryLimit || 0,
altDomain = data.altDomain || null,
xFrameOptions = data.xFrameOptions || 'SAMEORIGIN',
sso = 'sso' in data ? data.sso : null,
debugMode = data.debugMode || null,
@@ -508,6 +513,8 @@ function install(data, auditSource, callback) {
// if sso was unspecified, enable it by default if possible
if (sso === null) sso = !!manifest.addons['ldap'] || !!manifest.addons['oauth'];
if (altDomain !== null && !validator.isFQDN(altDomain)) return callback(new AppsError(AppsError.BAD_FIELD, 'Invalid external domain'));
var appId = uuid.v4();
if (icon) {
@@ -522,13 +529,13 @@ function install(data, auditSource, callback) {
if (error && error.reason === DomainError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such domain'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Could not get domain info:' + error.message));
var fqdn = domains.fqdn(location, domain, domainObject.provider);
var intrinsicFqdn = domains.fqdn(location, domain, domainObject.provider);
error = validateHostname(location, domain, fqdn);
error = validateHostname(location, domain, intrinsicFqdn);
if (error) return callback(error);
if (cert && key) {
error = reverseProxy.validateCertificate(fqdn, cert, key);
error = reverseProxy.validateCertificate(intrinsicFqdn, cert, key);
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
}
@@ -543,36 +550,32 @@ function install(data, auditSource, callback) {
var data = {
accessRestriction: accessRestriction,
memoryLimit: memoryLimit,
altDomain: altDomain,
xFrameOptions: xFrameOptions,
sso: sso,
debugMode: debugMode,
mailboxName: (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app',
restoreConfig: backupId ? { backupId: backupId, backupFormat: backupFormat } : null,
enableBackup: enableBackup,
robotsTxt: robotsTxt
robotsTxt: robotsTxt,
intrinsicFqdn: intrinsicFqdn
};
appdb.add(appId, appStoreId, manifest, location, domain, portBindings, data, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location, portBindings, error));
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, error.message));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
// save cert to boxdata/certs
if (cert && key) {
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, fqdn + '.user.cert'), cert)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving cert: ' + safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, fqdn + '.user.key'), key)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving key: ' + safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, intrinsicFqdn + '.user.cert'), cert)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving cert: ' + safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, intrinsicFqdn + '.user.key'), key)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving key: ' + safe.error.message));
}
taskmanager.restartAppTask(appId);
// fetch fresh app object for eventlog
get(appId, function (error, result) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_APP_INSTALL, auditSource, { appId: appId, location: location, domain: domain, manifest: manifest, backupId: backupId });
eventlog.add(eventlog.ACTION_APP_INSTALL, auditSource, { appId: appId, app: result });
callback(null, { id : appId });
});
callback(null, { id : appId });
});
});
});
@@ -585,8 +588,9 @@ function configure(appId, data, auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
get(appId, function (error, app) {
if (error) return callback(error);
appdb.get(appId, function (error, app) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
var domain, location, portBindings, values = { };
if ('location' in data) location = values.location = data.location.toLowerCase();
@@ -601,6 +605,11 @@ function configure(appId, data, auditSource, callback) {
if (error) return callback(error);
}
if ('altDomain' in data) {
values.altDomain = data.altDomain;
if (values.altDomain !== null && !validator.isFQDN(values.altDomain)) return callback(new AppsError(AppsError.BAD_FIELD, 'Invalid external domain'));
}
if ('portBindings' in data) {
portBindings = values.portBindings = data.portBindings;
error = validatePortBindings(values.portBindings, app.manifest.tcpPorts);
@@ -637,22 +646,24 @@ function configure(appId, data, auditSource, callback) {
if (error && error.reason === DomainError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such domain'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Could not get domain info:' + error.message));
var fqdn = domains.fqdn(location, domain, domainObject.provider);
var intrinsicFqdn = domains.fqdn(location, domain, domainObject.provider);
error = validateHostname(location, domain, fqdn);
error = validateHostname(location, domain, intrinsicFqdn);
if (error) return callback(error);
// save cert to boxdata/certs. TODO: move this to apptask when we have a real task queue
if ('cert' in data && 'key' in data) {
if (data.cert && data.key) {
error = reverseProxy.validateCertificate(fqdn, data.cert, data.key);
var vhost = values.altDomain || intrinsicFqdn;
error = reverseProxy.validateCertificate(vhost, data.cert, data.key);
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${fqdn}.user.cert`), data.cert)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving cert: ' + safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${fqdn}.user.key`), data.key)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving key: ' + safe.error.message));
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));
} else { // remove existing cert/key
if (!safe.fs.unlinkSync(path.join(paths.APP_CERTS_DIR, `${fqdn}.user.cert`))) debug('Error removing cert: ' + safe.error.message);
if (!safe.fs.unlinkSync(path.join(paths.APP_CERTS_DIR, `${fqdn}..user.key`))) debug('Error removing key: ' + safe.error.message);
if (!safe.fs.unlinkSync(path.join(paths.APP_CERTS_DIR, `${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);
}
}
@@ -676,14 +687,9 @@ function configure(appId, data, auditSource, callback) {
taskmanager.restartAppTask(appId);
// fetch fresh app object for eventlog
get(appId, function (error, result) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: appId });
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: appId, app: result });
callback(null);
});
callback(null);
});
});
});
@@ -723,8 +729,9 @@ function update(appId, data, auditSource, callback) {
}
}
get(appId, function (error, app) {
if (error) return callback(error);
appdb.get(appId, function (error, app) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
// prevent user from installing a app with different manifest id over an existing app
// this allows cloudron install -f --app <appid> for an app installed from the appStore
@@ -749,7 +756,7 @@ function update(appId, data, auditSource, callback) {
taskmanager.restartAppTask(appId);
eventlog.add(eventlog.ACTION_APP_UPDATE, auditSource, { appId: appId, toManifest: manifest, fromManifest: app.manifest, force: data.force, app: app });
eventlog.add(eventlog.ACTION_APP_UPDATE, auditSource, { appId: appId, toManifest: manifest, fromManifest: app.manifest, force: data.force });
// clear update indicator, if update fails, it will come back through the update checker
updateChecker.resetAppUpdateInfo(appId);
@@ -773,8 +780,10 @@ function getLogs(appId, options, callback) {
debug('Getting logs for %s', appId);
get(appId, function (error, app) {
if (error) return callback(error);
appdb.get(appId, function (error, app) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
var lines = options.lines || 100,
follow = !!options.follow,
@@ -818,8 +827,9 @@ function restore(appId, data, auditSource, callback) {
debug('Will restore app with id:%s', appId);
get(appId, function (error, app) {
if (error) return callback(error);
appdb.get(appId, function (error, app) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
// for empty or null backupId, use existing manifest to mimic a reinstall
var func = data.backupId ? backups.get.bind(null, data.backupId) : function (next) { return next(null, { manifest: app.manifest }); };
@@ -848,7 +858,7 @@ function restore(appId, data, auditSource, callback) {
taskmanager.restartAppTask(appId);
eventlog.add(eventlog.ACTION_APP_RESTORE, auditSource, { appId: appId, app: app });
eventlog.add(eventlog.ACTION_APP_RESTORE, auditSource, { appId: appId });
callback(null);
});
@@ -874,12 +884,13 @@ function clone(appId, data, auditSource, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof portBindings, 'object');
get(appId, function (error, app) {
if (error) return callback(error);
appdb.get(appId, function (error, app) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
backups.get(backupId, function (error, backupInfo) {
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
if (error && error.reason === BackupsError.NOT_FOUND) return callback(new AppsError(AppsError.EXTERNAL_ERROR, 'Backup not found'));
if (error && error.reason === BackupsError.NOT_FOUND) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
if (!backupInfo.manifest) callback(new AppsError(AppsError.EXTERNAL_ERROR, 'Could not get restore config'));
@@ -892,16 +903,18 @@ function clone(appId, data, auditSource, callback) {
if (error) return callback(error);
domains.get(domain, function (error, domainObject) {
if (error && error.reason === DomainError.NOT_FOUND) return callback(new AppsError(AppsError.EXTERNAL_ERROR, 'No such domain'));
if (error && error.reason === DomainError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such domain'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Could not get domain info:' + error.message));
error = validateHostname(location, domain, domains.fqdn(location, domain, domainObject.provider));
var intrinsicFqdn = domains.fqdn(location, domain, domainObject.provider);
error = validateHostname(location, domain, intrinsicFqdn);
if (error) return callback(error);
var newAppId = uuid.v4(), manifest = backupInfo.manifest;
appstore.purchase(newAppId, app.appStoreId, function (error) {
if (error && error.reason === AppstoreError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, error.message));
if (error && error.reason === AppstoreError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
if (error && error.reason === AppstoreError.BILLING_REQUIRED) return callback(new AppsError(AppsError.BILLING_REQUIRED, error.message));
if (error && error.reason === AppstoreError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
@@ -913,9 +926,7 @@ function clone(appId, data, auditSource, callback) {
xFrameOptions: app.xFrameOptions,
restoreConfig: { backupId: backupId, backupFormat: backupInfo.format },
sso: !!app.sso,
mailboxName: (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app',
enableBackup: app.enableBackup,
robotsTxt: app.robotsTxt
mailboxName: (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app'
};
appdb.add(newAppId, app.appStoreId, manifest, location, domain, portBindings, data, function (error) {
@@ -924,14 +935,9 @@ function clone(appId, data, auditSource, callback) {
taskmanager.restartAppTask(newAppId);
// fetch fresh app object for eventlog
get(appId, function (error, result) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_APP_CLONE, auditSource, { appId: newAppId, oldAppId: appId, backupId: backupId, location: location, manifest: manifest });
eventlog.add(eventlog.ACTION_APP_CLONE, auditSource, { appId: newAppId, oldAppId: appId, backupId: backupId, oldApp: app, newApp: result });
callback(null, { id : newAppId });
});
callback(null, { id : newAppId });
});
});
});
@@ -946,10 +952,10 @@ function uninstall(appId, auditSource, callback) {
debug('Will uninstall app with id:%s', appId);
get(appId, function (error, app) {
get(appId, function (error, result) {
if (error) return callback(error);
appstore.unpurchase(appId, app.appStoreId, function (error) {
appstore.unpurchase(appId, result.appStoreId, function (error) {
if (error && error.reason === AppstoreError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
if (error && error.reason === AppstoreError.BILLING_REQUIRED) return callback(new AppsError(AppsError.BILLING_REQUIRED, error.message));
if (error && error.reason === AppstoreError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
@@ -960,7 +966,7 @@ function uninstall(appId, auditSource, callback) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_APP_UNINSTALL, auditSource, { appId: appId, app: app });
eventlog.add(eventlog.ACTION_APP_UNINSTALL, auditSource, { appId: appId });
taskmanager.startAppTask(appId, callback);
});
@@ -1011,7 +1017,7 @@ function checkManifestConstraints(manifest) {
}
if (semver.valid(manifest.minBoxVersion) && semver.gt(manifest.minBoxVersion, config.version())) {
return new AppsError(AppsError.BAD_FIELD, 'App version requires a new platform version');
return new AppsError(AppsError.BAD_FIELD, 'minBoxVersion exceeds Box version');
}
return null;
@@ -1025,8 +1031,9 @@ function exec(appId, options, callback) {
var cmd = options.cmd || [ '/bin/bash' ];
assert(util.isArray(cmd) && cmd.length > 0);
get(appId, function (error, app) {
if (error) return callback(error);
appdb.get(appId, function (error, app) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
if (app.installationState !== appdb.ISTATE_INSTALLED || app.runState !== appdb.RSTATE_RUNNING) {
return callback(new AppsError(AppsError.BAD_STATE, 'App not installed or running'));
@@ -1165,17 +1172,17 @@ function listBackups(page, perPage, appId, callback) {
function restoreInstalledApps(callback) {
assert.strictEqual(typeof callback, 'function');
getAll(function (error, apps) {
appdb.getAll(function (error, apps) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
async.map(apps, function (app, iteratorDone) {
debug('marking %s for restore', app.intrinsicFqdn);
backups.getByAppIdPaged(1, 1, app.id, function (error, results) {
var restoreConfig = !error && results.length ? { backupId: results[0].id, backupFormat: results[0].format } : null;
debug(`marking ${app.fqdn} for restore using restore config ${JSON.stringify(restoreConfig)}`);
appdb.setInstallationCommand(app.id, appdb.ISTATE_PENDING_RESTORE, { restoreConfig: restoreConfig, oldConfig: null }, function (error) {
if (error) debug(`Error marking ${app.fqdn} for restore: ${JSON.stringify(error)}`);
if (error) debug('did not mark %s for restore', app.intrinsicFqdn, error);
iteratorDone(); // always succeed
});
@@ -1187,14 +1194,14 @@ function restoreInstalledApps(callback) {
function configureInstalledApps(callback) {
assert.strictEqual(typeof callback, 'function');
getAll(function (error, apps) {
appdb.getAll(function (error, apps) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
async.map(apps, function (app, iteratorDone) {
debug(`marking ${app.fqdn} for reconfigure`);
debug('marking %s for reconfigure', app.intrinsicFqdn);
appdb.setInstallationCommand(app.id, appdb.ISTATE_PENDING_CONFIGURE, { oldConfig: null }, function (error) {
if (error) debug(`Error marking ${app.fqdn} for reconfigure: ${JSON.stringify(error)}`);
if (error) debug('did not mark %s for reconfigure', app.intrinsicFqdn, error);
iteratorDone(); // always succeed
});
+56 -97
View File
@@ -18,12 +18,9 @@ exports = module.exports = {
AppstoreError: AppstoreError
};
var apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
var assert = require('assert'),
config = require('./config.js'),
debug = require('debug')('box:appstore'),
domains = require('./domains.js'),
eventlog = require('./eventlog.js'),
mail = require('./mail.js'),
os = require('os'),
@@ -130,7 +127,7 @@ function unpurchase(appId, appstoreId, callback) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
if (result.statusCode === 403 || result.statusCode === 401) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED));
if (result.statusCode === 404) return callback(null); // was never purchased
if (result.statusCode !== 201 && result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App unpurchase failed. %s %j', result.status, result.body)));
if (result.statusCode !== 201 && result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App purchase failed. %s %j', result.status, result.body)));
superagent.del(url).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
@@ -143,87 +140,62 @@ function unpurchase(appId, appstoreId, callback) {
});
}
function sendAliveStatus(callback) {
function sendAliveStatus(data, callback) {
callback = callback || NOOP_CALLBACK;
var allSettings, allDomains, mailDomains, loginEvents;
settings.getAll(function (error, result) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
async.series([
function (callback) {
settings.getAll(function (error, result) {
mail.getAll(function (error, mailDomains) {
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));
allSettings = result;
callback();
});
},
function (callback) {
domains.getAll(function (error, result) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
allDomains = result;
callback();
});
},
function (callback) {
mail.getDomains(function (error, result) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
mailDomains = result;
callback();
});
},
function (callback) {
eventlog.getAllPaged([ eventlog.ACTION_USER_LOGIN ], null, 1, 1, function (error, result) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
loginEvents = result;
callback();
});
}
], function (error) {
if (error) return callback(error);
var backendSettings = {
backupConfig: {
provider: allSettings[settings.BACKUP_CONFIG_KEY].provider,
hardlinks: !allSettings[settings.BACKUP_CONFIG_KEY].noHardlinks
},
domainConfig: {
count: allDomains.length,
domains: Array.from(new Set(allDomains.map(function (d) { return { domain: d.domain, provider: d.provider }; })))
},
mailConfig: {
outboundCount: mailDomains.length,
inboundCount: mailDomains.filter(function (d) { return d.enabled; }).length,
catchAllCount: mailDomains.filter(function (d) { return d.catchAll.length !== 0; }).length,
relayProviders: Array.from(new Set(mailDomains.map(function (d) { return d.relay.provider; })))
},
appAutoupdatePattern: allSettings[settings.APP_AUTOUPDATE_PATTERN_KEY],
boxAutoupdatePattern: allSettings[settings.BOX_AUTOUPDATE_PATTERN_KEY],
timeZone: allSettings[settings.TIME_ZONE_KEY],
};
var backendSettings = {
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 = {
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);
});
});
});
});
});
@@ -270,12 +242,9 @@ function getAppUpdate(app, callback) {
const updateInfo = result.body;
// for the appstore, x.y.z is the same as x.y.z-0 but in semver, x.y.z > x.y.z-0
const curAppVersion = semver.prerelease(app.manifest.version) ? app.manifest.version : `${app.manifest.version}-0`;
// do some sanity checks
if (!safe.query(updateInfo, 'manifest.version') || semver.gt(curAppVersion, safe.query(updateInfo, 'manifest.version'))) {
debug('Skipping malformed update of app %s version: %s. got %j', app.id, curAppVersion, updateInfo);
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)));
}
@@ -312,26 +281,16 @@ function sendFeedback(info, callback) {
assert.strictEqual(typeof info.description, 'string');
assert.strictEqual(typeof callback, 'function');
function collectAppInfoIfNeeded(callback) {
if (!info.appId) return callback();
apps.get(info.appId, callback);
}
getAppstoreConfig(function (error, appstoreConfig) {
if (error) return callback(error);
collectAppInfoIfNeeded(function (error, result) {
if (error) console.error('Unable to get app info', error);
if (result) info.app = result;
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/feedback';
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/feedback';
superagent.post(url).query({ accessToken: appstoreConfig.token }).send(info).timeout(10 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
if (result.statusCode !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
superagent.post(url).query({ accessToken: appstoreConfig.token }).send(info).timeout(10 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
if (result.statusCode !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
callback(null);
});
callback(null);
});
});
}
+34 -9
View File
@@ -15,7 +15,8 @@ exports = module.exports = {
_verifyManifest: verifyManifest,
_registerSubdomain: registerSubdomain,
_unregisterSubdomain: unregisterSubdomain,
_waitForDnsPropagation: waitForDnsPropagation
_waitForDnsPropagation: waitForDnsPropagation,
_waitForAltDomainDnsPropagation: waitForAltDomainDnsPropagation
};
require('supererror')({ splatchError: true });
@@ -70,7 +71,8 @@ function initialize(callback) {
function debugApp(app) {
assert.strictEqual(typeof app, 'object');
debug(app.fqdn + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
var prefix = app ? (app.intrinsicFqdn || '(bare)') : '(no app)';
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
}
// updates the app object and the database
@@ -264,16 +266,16 @@ function registerSubdomain(app, overwrite, callback) {
if (error) return callback(error);
async.retry({ times: 200, interval: 5000 }, function (retryCallback) {
debugApp(app, 'Registering subdomain location [%s] overwrite: %s', app.fqdn, overwrite);
debugApp(app, 'Registering subdomain location [%s] overwrite: %s', app.intrinsicFqdn, overwrite);
// get the current record before updating it
domains.getDnsRecords(app.location, app.domain, 'A', function (error, values) {
domains.getDNSRecords(app.location, app.domain, 'A', function (error, values) {
if (error) return retryCallback(error);
// refuse to update any existing DNS record for custom domains that we did not create
if (values.length !== 0 && !overwrite) return retryCallback(null, new Error('DNS Record already exists'));
domains.upsertDnsRecords(app.location, app.domain, 'A', [ ip ], function (error, changeId) {
domains.upsertDNSRecords(app.location, app.domain, 'A', [ ip ], function (error, changeId) {
if (error && (error.reason === DomainError.STILL_BUSY || error.reason === DomainError.EXTERNAL_ERROR)) return retryCallback(error); // try again
retryCallback(null, error || changeId);
@@ -303,9 +305,9 @@ function unregisterSubdomain(app, location, domain, callback) {
if (error) return callback(error);
async.retry({ times: 30, interval: 5000 }, function (retryCallback) {
debugApp(app, 'Unregistering subdomain: %s', app.fqdn);
debugApp(app, 'Unregistering subdomain: %s', app.intrinsicFqdn);
domains.removeDnsRecords(location, domain, 'A', [ ip ], function (error) {
domains.removeDNSRecords(location, domain, 'A', [ ip ], function (error) {
if (error && error.reason === DomainError.NOT_FOUND) return retryCallback(null, null); // domain can be not found if oldConfig.domain or restoreConfig.domain was removed
if (error && (error.reason === DomainError.STILL_BUSY || error.reason === DomainError.EXTERNAL_ERROR)) return retryCallback(error); // try again
@@ -341,10 +343,27 @@ function waitForDnsPropagation(app, callback) {
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error);
domains.waitForDnsRecord(app.fqdn, app.domain, ip, { interval: 5000, times: 120 }, callback);
domains.waitForDNSRecord(app.intrinsicFqdn, app.domain, ip, 'A', { interval: 5000, times: 120 }, callback);
});
}
function waitForAltDomainDnsPropagation(app, callback) {
if (!app.altDomain) return callback(null);
// try for 10 minutes before giving up. this allows the user to "reconfigure" the app in the case where
// an app has an external domain and cloudron is migrated to custom domain.
var isNakedDomain = tld.getDomain(app.altDomain) === app.altDomain;
if (isNakedDomain) { // check naked domains with A record since CNAME records don't work there
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error);
domains.waitForDNSRecord(app.altDomain, tld.getDomain(app.altDomain), ip, 'A', { interval: 10000, times: 60 }, callback);
});
} else {
domains.waitForDNSRecord(app.altDomain, tld.getDomain(app.altDomain), app.intrinsicFqdn + '.', 'CNAME', { interval: 10000, times: 60 }, callback);
}
}
// Ordering is based on the following rationale:
// - configure nginx, icon, oauth
// - register subdomain.
@@ -427,6 +446,9 @@ function install(app, callback) {
updateApp.bind(null, app, { installationProgress: '85, Waiting for DNS propagation' }),
exports._waitForDnsPropagation.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Waiting for External Domain setup' }),
exports._waitForAltDomainDnsPropagation.bind(null, app), // required when restoring and !restoreConfig
updateApp.bind(null, app, { installationProgress: '95, Configuring reverse proxy' }),
configureReverseProxy.bind(null, app),
@@ -472,7 +494,7 @@ function configure(app, callback) {
assert.strictEqual(typeof callback, 'function');
// oldConfig can be null during an infra update
var locationChanged = app.oldConfig && (app.oldConfig.fqdn !== app.fqdn);
var locationChanged = app.oldConfig && (app.oldConfig.intrinsicFqdn !== app.intrinsicFqdn);
async.series([
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
@@ -519,6 +541,9 @@ function configure(app, callback) {
updateApp.bind(null, app, { installationProgress: '80, Waiting for DNS propagation' }),
exports._waitForDnsPropagation.bind(null, app),
updateApp.bind(null, app, { installationProgress: '85, Waiting for External Domain setup' }),
exports._waitForAltDomainDnsPropagation.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Configuring reverse proxy' }),
configureReverseProxy.bind(null, app),
+18 -27
View File
@@ -68,9 +68,10 @@ var NOOP_CALLBACK = function (error) { if (error) debug(error); };
var BACKUPTASK_CMD = path.join(__dirname, 'backuptask.js');
function debugApp(app) {
assert(typeof app === 'object');
assert(!app || typeof app === 'object');
debug(app.fqdn + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
var prefix = app ? app.intrinsicFqdn : '(no app)';
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
}
function BackupsError(reason, errorOrMessage) {
@@ -231,11 +232,6 @@ function sync(backupConfig, backupId, dataDir, callback) {
assert.strictEqual(typeof dataDir, 'string');
assert.strictEqual(typeof callback, 'function');
function setBackupProgress(message) {
debug('%s: %s', (new Date()).toISOString(), message);
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, message);
}
syncer.sync(dataDir, function processTask(task, iteratorCallback) {
debug('sync: processing task: %j', task);
var backupFilePath = path.join(getBackupFilePath(backupConfig, backupId, backupConfig.format), task.path);
@@ -243,33 +239,28 @@ function sync(backupConfig, backupId, dataDir, callback) {
if (task.operation === 'removedir') {
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, `Removing directory ${task.path}`);
return api(backupConfig.provider).removeDir(backupConfig, backupFilePath)
.on('progress', setBackupProgress)
.on('progress', function (detail) {
debug(`sync: ${detail}`);
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, detail);
})
.on('done', iteratorCallback);
} else if (task.operation === 'remove') {
setBackupProgress(`Removing ${task.path}`);
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, `Removing ${task.path}`);
return api(backupConfig.provider).remove(backupConfig, backupFilePath, iteratorCallback);
}
var retryCount = 0;
async.retry({ times: 5, interval: 20000 }, function (retryCallback) {
retryCallback = once(retryCallback); // protect again upload() erroring much later after read stream error
++retryCount;
debug(`${task.operation} ${task.path} try ${retryCount}`);
if (task.operation === 'add') {
setBackupProgress(`Adding ${task.path} position ${task.position} try ${retryCount}`);
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, `Adding ${task.path}`);
var stream = fs.createReadStream(path.join(dataDir, task.path));
stream.on('error', function (error) {
setBackupProgress(`read stream error for ${task.path}: ${error.message}`);
retryCallback();
}); // ignore error if file disappears
api(backupConfig.provider).upload(backupConfig, backupFilePath, stream, function (error) {
setBackupProgress(error ? `Error uploading ${task.path} try ${retryCount}: ${error.message}` : `Uploaded ${task.path}`);
retryCallback(error);
});
stream.on('error', function () { return retryCallback(); }); // ignore error if file disappears
api(backupConfig.provider).upload(backupConfig, backupFilePath, stream, retryCallback);
}
}, iteratorCallback);
}, backupConfig.syncConcurrency || 10 /* concurrency */, function (error) {
}, 10 /* concurrency */, function (error) {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
callback();
@@ -303,6 +294,8 @@ function upload(backupId, format, dataDir, callback) {
assert.strictEqual(typeof dataDir, 'string');
assert.strictEqual(typeof callback, 'function');
callback = once(callback);
debug('upload: id %s format %s dataDir %s', backupId, format, dataDir);
settings.getBackupConfig(function (error, backupConfig) {
@@ -310,8 +303,6 @@ function upload(backupId, format, dataDir, callback) {
if (format === 'tgz') {
async.retry({ times: 5, interval: 20000 }, function (retryCallback) {
retryCallback = once(retryCallback); // protect again upload() erroring much later after tar stream error
var tarStream = createTarPackStream(dataDir, backupConfig.key || null);
tarStream.on('error', retryCallback); // already returns BackupsError
@@ -729,7 +720,7 @@ function backupApp(app, callback) {
const timestamp = (new Date()).toISOString().replace(/[T.]/g, '-').replace(/[:Z]/g,'');
safe.fs.unlinkSync(paths.BACKUP_LOG_FILE); // start fresh log file
progress.set(progress.BACKUP, 10, 'Backing up ' + app.fqdn);
progress.set(progress.BACKUP, 10, 'Backing up ' + (app.altDomain || app.intrinsicFqdn));
backupAppWithTimestamp(app, timestamp, function (error) {
progress.set(progress.BACKUP, 100, error ? error.message : '');
@@ -756,12 +747,12 @@ function backupBoxAndApps(auditSource, callback) {
var step = 100/(allApps.length+2);
async.mapSeries(allApps, function iterator(app, iteratorCallback) {
progress.set(progress.BACKUP, step * processed, 'Backing up ' + app.fqdn);
progress.set(progress.BACKUP, step * processed, 'Backing up ' + (app.altDomain || app.intrinsicFqdn));
++processed;
if (!app.enableBackup) {
progress.set(progress.BACKUP, step * processed, 'Skipped backup ' + app.fqdn);
progress.set(progress.BACKUP, step * processed, 'Skipped backup ' + (app.altDomain || app.intrinsicFqdn));
return iteratorCallback(null, null); // nothing to backup
}
@@ -771,7 +762,7 @@ function backupBoxAndApps(auditSource, callback) {
return iteratorCallback(error);
}
progress.set(progress.BACKUP, step * processed, 'Backed up ' + app.fqdn);
progress.set(progress.BACKUP, step * processed, 'Backed up ' + (app.altDomain || app.intrinsicFqdn));
iteratorCallback(null, backupId || null); // clear backupId if is in BAD_STATE and never backed up
});
+2 -2
View File
@@ -44,9 +44,9 @@ initialize(function (error) {
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, '');
backups.upload(backupId, format, dataDir, function resultHandler(error) {
if (error) debug('upload completed with error', error);
if (error) debug('completed with error', error);
debug('upload completed');
debug('completed');
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, error ? error.message : '');
+1 -1
View File
@@ -191,7 +191,7 @@ function getAll(callback) {
if (record.type === exports.TYPE_PROXY) record.name = result.manifest.title + ' Website Proxy';
if (record.type === exports.TYPE_OAUTH) record.name = result.manifest.title + ' OAuth';
record.domain = result.fqdn;
record.domain = result.altDomain || result.intrinsicFqdn;
tmp.push(record);
+20 -1
View File
@@ -234,11 +234,30 @@ function updateToLatest(auditSource, callback) {
if (!boxUpdateInfo) return callback(new CloudronError(CloudronError.ALREADY_UPTODATE, 'No update available'));
if (!boxUpdateInfo.sourceTarballUrl) return callback(new CloudronError(CloudronError.BAD_STATE, 'No automatic update available'));
// check if this is just a version number change
if (config.version().match(/[-+]/) !== null && config.version().replace(/[-+].*/, '') === boxUpdateInfo.version) {
doShortCircuitUpdate(boxUpdateInfo, function (error) {
if (error) debug('Short-circuit update failed', error);
});
return callback(null);
}
if (boxUpdateInfo.upgrade && config.provider() !== 'caas') return callback(new CloudronError(CloudronError.SELF_UPGRADE_NOT_SUPPORTED));
update(boxUpdateInfo, auditSource, callback);
}
function doShortCircuitUpdate(boxUpdateInfo, callback) {
assert(boxUpdateInfo !== null && typeof boxUpdateInfo === 'object');
debug('Starting short-circuit from prerelease version %s to release version %s', config.version(), boxUpdateInfo.version);
config.setVersion(boxUpdateInfo.version);
progress.clear(progress.UPDATE);
updateChecker.resetUpdateInfo();
callback();
}
function doUpdate(boxUpdateInfo, callback) {
assert(boxUpdateInfo && typeof boxUpdateInfo === 'object');
@@ -275,7 +294,7 @@ function doUpdate(boxUpdateInfo, callback) {
debug('updating box %s %j', boxUpdateInfo.sourceTarballUrl, _.omit(data, 'tlsCert', 'tlsKey', 'token', 'appstore', 'caas'));
progress.set(progress.UPDATE, 5, 'Downloading and installing new version');
progress.set(progress.UPDATE, 5, 'Downloading and extracting new version');
shell.sudo('update', [ UPDATE_CMD, boxUpdateInfo.sourceTarballUrl, JSON.stringify(data) ], function (error) {
if (error) return updateError(error);
+17 -39
View File
@@ -22,12 +22,12 @@ var apps = require('./apps.js'),
reverseProxy = require('./reverseproxy.js'),
scheduler = require('./scheduler.js'),
settings = require('./settings.js'),
semver = require('semver'),
updateChecker = require('./updatechecker.js');
var gJobs = {
alive: null, // send periodic stats
appAutoUpdater: null,
boxAutoUpdater: null,
autoUpdater: null,
appUpdateChecker: null,
backup: null,
boxUpdateChecker: null,
@@ -78,16 +78,14 @@ function initialize(callback) {
});
settings.events.on(settings.TIME_ZONE_KEY, recreateJobs);
settings.events.on(settings.APP_AUTOUPDATE_PATTERN_KEY, appAutoupdatePatternChanged);
settings.events.on(settings.BOX_AUTOUPDATE_PATTERN_KEY, boxAutoupdatePatternChanged);
settings.events.on(settings.AUTOUPDATE_PATTERN_KEY, autoupdatePatternChanged);
settings.events.on(settings.DYNAMIC_DNS_KEY, dynamicDnsChanged);
settings.getAll(function (error, allSettings) {
if (error) return callback(error);
recreateJobs(allSettings[settings.TIME_ZONE_KEY]);
appAutoupdatePatternChanged(allSettings[settings.APP_AUTOUPDATE_PATTERN_KEY]);
boxAutoupdatePatternChanged(allSettings[settings.BOX_AUTOUPDATE_PATTERN_KEY]);
autoupdatePatternChanged(allSettings[settings.AUTOUPDATE_PATTERN_KEY]);
dynamicDnsChanged(allSettings[settings.DYNAMIC_DNS_KEY]);
callback();
@@ -191,51 +189,32 @@ function recreateJobs(tz) {
});
}
function boxAutoupdatePatternChanged(pattern) {
function autoupdatePatternChanged(pattern) {
assert.strictEqual(typeof pattern, 'string');
assert(gJobs.boxUpdateCheckerJob);
debug('Box auto update pattern changed to %s', pattern);
debug('Auto update pattern changed to %s', pattern);
if (gJobs.boxAutoUpdater) gJobs.boxAutoUpdater.stop();
if (gJobs.autoUpdater) gJobs.autoUpdater.stop();
if (pattern === constants.AUTOUPDATE_PATTERN_NEVER) return;
gJobs.boxAutoUpdater = new CronJob({
gJobs.autoUpdater = new CronJob({
cronTime: pattern,
onTick: function() {
var updateInfo = updateChecker.getUpdateInfo();
if (updateInfo.box) {
debug('Starting autoupdate to %j', updateInfo.box);
cloudron.updateToLatest(AUDIT_SOURCE, NOOP_CALLBACK);
} else {
debug('No box auto updates available');
}
},
start: true,
timeZone: gJobs.boxUpdateCheckerJob.cronTime.zone // hack
});
}
function appAutoupdatePatternChanged(pattern) {
assert.strictEqual(typeof pattern, 'string');
assert(gJobs.boxUpdateCheckerJob);
debug('Apps auto update pattern changed to %s', pattern);
if (gJobs.appAutoUpdater) gJobs.appAutoUpdater.stop();
if (pattern === constants.AUTOUPDATE_PATTERN_NEVER) return;
gJobs.appAutoUpdater = new CronJob({
cronTime: pattern,
onTick: function() {
var updateInfo = updateChecker.getUpdateInfo();
if (updateInfo.apps) {
if (semver.major(updateInfo.box.version) === semver.major(config.version())) {
debug('Starting autoupdate to %j', updateInfo.box);
cloudron.updateToLatest(AUDIT_SOURCE, NOOP_CALLBACK);
} else {
debug('Block automatic update for major version');
}
} else if (updateInfo.apps) {
debug('Starting app update to %j', updateInfo.apps);
apps.autoupdateApps(updateInfo.apps, AUDIT_SOURCE, NOOP_CALLBACK);
} else {
debug('No app auto updates available');
debug('No auto updates available');
}
},
start: true,
@@ -266,8 +245,7 @@ function uninitialize(callback) {
assert.strictEqual(typeof callback, 'function');
settings.events.removeListener(settings.TIME_ZONE_KEY, recreateJobs);
settings.events.removeListener(settings.APP_AUTOUPDATE_PATTERN_KEY, appAutoupdatePatternChanged);
settings.events.removeListener(settings.BOX_AUTOUPDATE_PATTERN_KEY, boxAutoupdatePatternChanged);
settings.events.removeListener(settings.AUTOUPDATE_PATTERN_KEY, autoupdatePatternChanged);
settings.events.removeListener(settings.DYNAMIC_DNS_KEY, dynamicDnsChanged);
for (var job in gJobs) {
+14 -2
View File
@@ -6,6 +6,10 @@ exports = module.exports = {
query: query,
transaction: transaction,
beginTransaction: beginTransaction,
rollback: rollback,
commit: commit,
importFromFile: importFromFile,
exportToFile: exportToFile,
@@ -23,13 +27,21 @@ var assert = require('assert'),
var gConnectionPool = null,
gDefaultConnection = null;
function initialize(callback) {
function initialize(options, callback) {
if (typeof options === 'function') {
callback = options;
options = {
connectionLimit: 5
};
}
assert.strictEqual(typeof options.connectionLimit, 'number');
assert.strictEqual(typeof callback, 'function');
if (gConnectionPool !== null) return callback(null);
gConnectionPool = mysql.createPool({
connectionLimit: 5, // this has to be > 1 since we store one connection as 'default'. the rest for transactions
connectionLimit: options.connectionLimit,
host: config.database().hostname,
user: config.database().username,
password: config.database().password,
+5 -6
View File
@@ -13,7 +13,6 @@ var assert = require('assert'),
constants = require('./constants.js'),
eventlog = require('./eventlog.js'),
tokendb = require('./tokendb.js'),
user = require('./user.js'),
util = require('util');
function DeveloperError(reason, errorOrMessage) {
@@ -38,19 +37,19 @@ util.inherits(DeveloperError, Error);
DeveloperError.INTERNAL_ERROR = 'Internal Error';
DeveloperError.EXTERNAL_ERROR = 'External Error';
function issueDeveloperToken(userObject, ip, callback) {
assert.strictEqual(typeof userObject, 'object');
assert.strictEqual(typeof ip, 'string');
function issueDeveloperToken(user, auditSource, callback) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
var token = tokendb.generateToken();
var expiresAt = Date.now() + constants.DEFAULT_TOKEN_EXPIRATION;
var scopes = '*,' + clients.SCOPE_ROLE_SDK;
tokendb.add(token, userObject.id, 'cid-cli', expiresAt, scopes, function (error) {
tokendb.add(token, user.id, 'cid-cli', expiresAt, scopes, function (error) {
if (error) return callback(new DeveloperError(DeveloperError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'cli', ip: ip }, { userId: userObject.id, user: user.removePrivateFields(userObject) });
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource, { authType: 'cli', userId: user.id, username: user.username });
callback(null, { token: token, expiresAt: new Date(expiresAt).toISOString() });
});
+46
View File
@@ -0,0 +1,46 @@
'use strict';
exports = module.exports = {
resolve: resolve
};
var assert = require('assert'),
child_process = require('child_process'),
debug = require('debug')('box:dig');
function resolve(domain, type, options, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
// dig @server cloudron.io TXT +short
var args = [ ];
if (options.server) args.push('@' + options.server);
if (type === 'PTR') {
args.push('-x', domain);
} else {
args.push(domain, type);
}
args.push('+short');
child_process.execFile('/usr/bin/dig', args, { encoding: 'utf8', killSignal: 'SIGKILL', timeout: options.timeout || 0 }, function (error, stdout, stderr) {
if (error && error.killed) error.code = 'ETIMEDOUT';
if (error || stderr) debug('resolve error (%j): %j %s %s', args, error, stdout, stderr);
if (error) return callback(error);
debug('resolve (%j): %s', args, stdout);
if (!stdout) return callback(); // timeout or no result
var lines = stdout.trim().split('\n');
if (type === 'MX') {
lines = lines.map(function (line) {
var parts = line.split(' ');
return { priority: parts[0], exchange: parts[1] };
});
}
return callback(null, lines);
});
}
+6 -6
View File
@@ -11,7 +11,7 @@ exports = module.exports = {
var assert = require('assert'),
async = require('async'),
debug = require('debug')('box:dns/cloudflare'),
dns = require('../native-dns.js'),
dns = require('dns'),
DomainError = require('../domains.js').DomainError,
superagent = require('superagent'),
util = require('util'),
@@ -58,7 +58,7 @@ function getZoneByName(dnsConfig, zoneName, callback) {
});
}
function getDnsRecordsByZoneId(dnsConfig, zoneId, zoneName, subdomain, type, callback) {
function getDNSRecordsByZoneId(dnsConfig, zoneId, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneId, 'string');
assert.strictEqual(typeof zoneName, 'string');
@@ -100,7 +100,7 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
var zoneId = result.id;
getDnsRecordsByZoneId(dnsConfig, zoneId, zoneName, subdomain, type, function (error, result) {
getDNSRecordsByZoneId(dnsConfig, zoneId, zoneName, subdomain, type, function (error, result) {
if (error) return callback(error);
var dnsRecords = result;
@@ -171,7 +171,7 @@ function get(dnsConfig, zoneName, subdomain, type, callback) {
getZoneByName(dnsConfig, zoneName, function(error, result){
if (error) return callback(error);
getDnsRecordsByZoneId(dnsConfig, result.id, zoneName, subdomain, type, function(error, result) {
getDNSRecordsByZoneId(dnsConfig, result.id, zoneName, subdomain, type, function(error, result) {
if (error) return callback(error);
var tmp = result.map(function (record) { return record.content; });
@@ -193,7 +193,7 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
getZoneByName(dnsConfig, zoneName, function(error, result){
if (error) return callback(error);
getDnsRecordsByZoneId(dnsConfig, result.id, zoneName, subdomain, type, function(error, result) {
getDNSRecordsByZoneId(dnsConfig, result.id, zoneName, subdomain, type, function(error, result) {
if (error) return callback(error);
if (result.length === 0) return callback(null);
@@ -243,7 +243,7 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
dns.resolveNs(zoneName, function (error, nameservers) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainError(DomainError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
if (error || !nameservers) return callback(new DomainError(DomainError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
+2 -2
View File
@@ -11,7 +11,7 @@ exports = module.exports = {
var assert = require('assert'),
async = require('async'),
debug = require('debug')('box:dns/digitalocean'),
dns = require('../native-dns.js'),
dns = require('dns'),
DomainError = require('../domains.js').DomainError,
safe = require('safetydance'),
superagent = require('superagent'),
@@ -210,7 +210,7 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
dns.resolveNs(zoneName, function (error, nameservers) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainError(DomainError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
if (error || !nameservers) return callback(new DomainError(DomainError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
+2 -2
View File
@@ -10,7 +10,7 @@ exports = module.exports = {
var assert = require('assert'),
debug = require('debug')('box:dns/gcdns'),
dns = require('../native-dns.js'),
dns = require('dns'),
DomainError = require('../domains.js').DomainError,
GCDNS = require('@google-cloud/dns'),
util = require('util'),
@@ -172,7 +172,7 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
var credentials = getDnsCredentials(dnsConfig);
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
dns.resolveNs(zoneName, function (error, resolvedNS) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainError(DomainError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
if (error || !resolvedNS) return callback(new DomainError(DomainError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
+2 -2
View File
@@ -10,7 +10,7 @@ exports = module.exports = {
var assert = require('assert'),
debug = require('debug')('box:dns/manual'),
dns = require('../native-dns.js'),
dns = require('dns'),
DomainError = require('../domains.js').DomainError,
util = require('util');
@@ -56,7 +56,7 @@ function verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback) {
assert.strictEqual(typeof callback, 'function');
// Very basic check if the nameservers can be fetched
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
dns.resolveNs(zoneName, function (error, nameservers) {
if (error || !nameservers) return callback(new DomainError(DomainError.BAD_FIELD, 'Unable to get nameservers'));
callback(null, { wildcard: !!dnsConfig.wildcard });
+3 -2
View File
@@ -46,10 +46,11 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
return callback();
}
function waitForDns(domain, zoneName, value, options, callback) {
function waitForDns(domain, zoneName, value, type, options, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof value, 'string');
assert(typeof value === 'string' || util.isRegExp(value));
assert(type === 'A' || type === 'CNAME' || type === 'TXT');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
+4 -3
View File
@@ -13,8 +13,9 @@ exports = module.exports = {
var assert = require('assert'),
AWS = require('aws-sdk'),
config = require('../config.js'),
debug = require('debug')('box:dns/route53'),
dns = require('../native-dns.js'),
dns = require('dns'),
DomainError = require('../domains.js').DomainError,
util = require('util'),
_ = require('underscore');
@@ -192,7 +193,7 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
};
var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
route53.changeResourceRecordSets(params, function(error) {
route53.changeResourceRecordSets(params, function(error, result) {
if (error && error.code === 'AccessDenied') return callback(new DomainError(DomainError.ACCESS_DENIED, error.message));
if (error && error.code === 'InvalidClientTokenId') return callback(new DomainError(DomainError.ACCESS_DENIED, error.message));
if (error && error.message && error.message.indexOf('it was not found') !== -1) {
@@ -233,7 +234,7 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
dns.resolveNs(zoneName, function (error, nameservers) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainError(DomainError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
if (error || !nameservers) return callback(new DomainError(DomainError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
+44 -45
View File
@@ -5,59 +5,53 @@ exports = module.exports = waitForDns;
var assert = require('assert'),
async = require('async'),
debug = require('debug')('box:dns/waitfordns'),
dns = require('../native-dns.js'),
DomainError = require('../domains.js').DomainError;
dig = require('../dig.js'),
dns = require('dns'),
DomainError = require('../domains.js').DomainError,
util = require('util');
function resolveIp(hostname, options, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
// try A record at authoritative server
debug(`resolveIp: Checking if ${hostname} has A record at ${options.server}`);
dns.resolve(hostname, 'A', options, function (error, results) {
if (!error && results.length !== 0) return callback(null, results);
// try CNAME record at authoritative server
debug(`resolveIp: Checking if ${hostname} has CNAME record at ${options.server}`);
dns.resolve(hostname, 'CNAME', options, function (error, results) {
if (error || results.length === 0) return callback(error, results);
// recurse lookup the CNAME record
debug(`resolveIp: Resolving ${hostname}'s CNAME record ${results[0]}`);
dns.resolve(results[0], 'A', { server: '127.0.0.1', timeout: options.timeout }, callback);
});
});
}
function isChangeSynced(domain, value, nameserver, callback) {
function isChangeSynced(domain, value, type, nameserver, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof value, 'string');
assert(util.isRegExp(value));
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof nameserver, 'string');
assert.strictEqual(typeof callback, 'function');
// ns records cannot have cname
dns.resolve(nameserver, 'A', { timeout: 5000 }, function (error, nsIps) {
dns.resolve4(nameserver, function (error, nsIps) {
if (error || !nsIps || nsIps.length === 0) {
debug(`isChangeSynced: cannot resolve NS ${nameserver}`); // it's fine if one or more ns are dead
return callback(null, true);
debug('nameserver %s does not resolve. assuming it stays bad.', nameserver); // it's fine if one or more ns are dead
return callback(true);
}
async.every(nsIps, function (nsIp, iteratorCallback) {
resolveIp(domain, { server: nsIp, timeout: 5000 }, function (error, answer) {
if (error && error.code === 'TIMEOUT') {
debug(`isChangeSynced: NS ${nameserver} (${nsIp}) timed out when resolving ${domain}`);
dig.resolve(domain, type, { server: nsIp, timeout: 5000 }, function (error, answer) {
if (error && error.code === 'ETIMEDOUT') {
debug('nameserver %s (%s) timed out when trying to resolve %s', nameserver, nsIp, domain);
return iteratorCallback(null, true); // should be ok if dns server is down
}
if (error) {
debug(`isChangeSynced: NS ${nameserver} (${nsIp}) errored when resolve ${domain}: ${error}`);
debug('nameserver %s (%s) returned error trying to resolve %s: %s', nameserver, nsIp, domain, error);
return iteratorCallback(null, false);
}
debug(`isChangeSynced: ${domain} was resolved to ${answer} at NS ${nameserver} (${nsIp}). Expecting ${value}`);
if (!answer || answer.length === 0) {
debug('bad answer from nameserver %s (%s) resolving %s (%s)', nameserver, nsIp, domain, type);
return iteratorCallback(null, false);
}
iteratorCallback(null, answer.length === 1 && answer[0] === value);
debug('isChangeSynced: ns: %s (%s), name:%s Actual:%j Expecting:%s', nameserver, nsIp, domain, answer, value);
var match = answer.some(function (a) {
return ((type === 'A' && value.test(a)) ||
(type === 'CNAME' && value.test(a)) ||
(type === 'TXT' && value.test(a)));
});
if (match) return iteratorCallback(null, true); // done!
iteratorCallback(null, false);
});
}, callback);
@@ -65,25 +59,30 @@ function isChangeSynced(domain, value, nameserver, callback) {
}
// check if IP change has propagated to every nameserver
function waitForDns(domain, zoneName, value, options, callback) {
function waitForDns(domain, zoneName, value, type, options, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof value, 'string');
assert(typeof value === 'string' || util.isRegExp(value));
assert(type === 'A' || type === 'CNAME' || type === 'TXT');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
debug('waitForDns: domain %s to be %s in zone %s.', domain, value, zoneName);
if (typeof value === 'string') {
// http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript
value = new RegExp('^' + value.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') + '$');
}
var attempt = 0;
debug('waitForIp: domain %s to be %s in zone %s.', domain, value, zoneName);
var attempt = 1;
async.retry(options, function (retryCallback) {
++attempt;
debug(`waitForDns (try ${attempt}): ${domain} to be ${value} in zone ${zoneName}`);
debug('waitForDNS: %s (zone: %s) attempt %s.', domain, zoneName, attempt++);
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
dns.resolveNs(zoneName, function (error, nameservers) {
if (error || !nameservers) return retryCallback(error || new DomainError(DomainError.EXTERNAL_ERROR, 'Unable to get nameservers'));
async.every(nameservers, isChangeSynced.bind(null, domain, value), function (error, synced) {
debug('waitForDns: %s %s ns: %j', domain, synced ? 'done' : 'not done', nameservers);
async.every(nameservers, isChangeSynced.bind(null, domain, value, type), function (error, synced) {
debug('waitForIp: %s %s ns: %j', domain, synced ? 'done' : 'not done', nameservers);
retryCallback(synced ? null : new DomainError(DomainError.EXTERNAL_ERROR, 'ETRYAGAIN'));
});
@@ -91,7 +90,7 @@ function waitForDns(domain, zoneName, value, options, callback) {
}, function retryDone(error) {
if (error) return callback(error);
debug(`waitForDns: ${domain} has propagated`);
debug('waitForDNS: %s done.', domain);
callback(null);
});
+37 -24
View File
@@ -15,7 +15,6 @@ exports = module.exports = {
createSubcontainer: createSubcontainer,
getContainerIdByIp: getContainerIdByIp,
inspect: inspect,
inspectByName: inspect,
execContainer: execContainer
};
@@ -45,38 +44,56 @@ var addons = require('./addons.js'),
debug = require('debug')('box:docker.js'),
once = require('once'),
safe = require('safetydance'),
shell = require('./shell.js'),
spawn = child_process.spawn,
util = require('util'),
_ = require('underscore');
function debugApp(app, args) {
assert(typeof app === 'object');
assert(!app || typeof app === 'object');
debug(app.fqdn + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
var prefix = app ? app.intrinsicFqdn : '(no app)';
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
}
function pullImage(manifest, callback) {
var docker = exports.connection;
// Use docker CLI here to support downloading of private repos. for dockerode, we have to use
// https://github.com/apocas/dockerode#pull-from-private-repos
shell.exec('pullImage', '/usr/bin/docker', [ 'pull', manifest.dockerImage ], { }, function (error) {
if (error) {
debug(`pullImage: Error pulling image ${manifest.dockerImage} of ${manifest.id}: ${error.message}`);
return callback(new Error('Failed to pull image'));
}
docker.pull(manifest.dockerImage, function (err, stream) {
if (err) return callback(new Error('Error connecting to docker. statusCode: ' + err.statusCode));
var image = docker.getImage(manifest.dockerImage);
// https://github.com/dotcloud/docker/issues/1074 says each status message
// is emitted as a chunk
stream.on('data', function (chunk) {
var data = safe.JSON.parse(chunk) || { };
debug('pullImage %s: %j', manifest.id, data);
image.inspect(function (err, data) {
if (err) return callback(new Error('Error inspecting image:' + err.message));
if (!data || !data.Config) return callback(new Error('Missing Config in image:' + JSON.stringify(data, null, 4)));
if (!data.Config.Entrypoint && !data.Config.Cmd) return callback(new Error('Only images with entry point are allowed'));
// The information here is useless because this is per layer as opposed to per image
if (data.status) {
} else if (data.error) {
debug('pullImage error %s: %s', manifest.id, data.errorDetail.message);
}
});
if (data.Config.ExposedPorts) debug('This image of %s exposes ports: %j', manifest.id, data.Config.ExposedPorts);
stream.on('end', function () {
debug('downloaded image %s of %s successfully', manifest.dockerImage, manifest.id);
callback(null);
var image = docker.getImage(manifest.dockerImage);
image.inspect(function (err, data) {
if (err) return callback(new Error('Error inspecting image:' + err.message));
if (!data || !data.Config) return callback(new Error('Missing Config in image:' + JSON.stringify(data, null, 4)));
if (!data.Config.Entrypoint && !data.Config.Cmd) return callback(new Error('Only images with entry point are allowed'));
if (data.Config.ExposedPorts) debug('This image of %s exposes ports: %j', manifest.id, data.Config.ExposedPorts);
callback(null);
});
});
stream.on('error', function (error) {
debug('error pulling image %s of %s: %j', manifest.dockerImage, manifest.id, error);
callback(error);
});
});
}
@@ -112,7 +129,7 @@ function createSubcontainer(app, name, cmd, options, callback) {
var manifest = app.manifest;
var exposedPorts = {}, dockerPortBindings = { };
var domain = app.fqdn;
var domain = app.altDomain || app.intrinsicFqdn;
var stdEnv = [
'CLOUDRON=1',
'WEBADMIN_ORIGIN=' + config.adminOrigin(),
@@ -146,10 +163,6 @@ function createSubcontainer(app, name, cmd, options, callback) {
memoryLimit = constants.DEFAULT_MEMORY_LIMIT;
}
// give scheduler tasks twice the memory limit since background jobs take more memory
// if required, we can make this a manifest and runtime argument later
if (!isAppContainer) memoryLimit *= 2;
// apparmor is disabled on few servers
var enableSecurityOpt = config.CLOUDRON && safe(function () { return child_process.spawnSync('aa-enabled').status === 0; }, false);
@@ -173,7 +186,7 @@ function createSubcontainer(app, name, cmd, options, callback) {
'/run': {}
},
Labels: {
'fqdn': app.fqdn,
'fqdn': app.intrinsicFqdn,
'appId': app.id,
'isSubcontainer': String(!isAppContainer)
},
+19 -15
View File
@@ -10,11 +10,11 @@ module.exports = exports = {
fqdn: fqdn,
setAdmin: setAdmin,
getDnsRecords: getDnsRecords,
upsertDnsRecords: upsertDnsRecords,
removeDnsRecords: removeDnsRecords,
getDNSRecords: getDNSRecords,
upsertDNSRecords: upsertDNSRecords,
removeDNSRecords: removeDNSRecords,
waitForDnsRecord: waitForDnsRecord,
waitForDNSRecord: waitForDNSRecord,
DomainError: DomainError
};
@@ -116,7 +116,7 @@ function add(domain, zoneName, provider, config, fallbackCertificate, tlsConfig,
}
if (fallbackCertificate) {
let error = reverseProxy.validateCertificate(`test.${domain}`, fallbackCertificate.cert, fallbackCertificate.key);
let error = reverseProxy.validateCertificate(fallbackCertificate.cert, fallbackCertificate.key, domain);
if (error) return callback(new DomainError(DomainError.BAD_FIELD, error.message));
}
@@ -164,7 +164,7 @@ function get(domain, callback) {
var cert = safe.fs.readFileSync(bundle.certFilePath, 'utf-8');
var key = safe.fs.readFileSync(bundle.keyFilePath, 'utf-8');
if (!cert || !key) return callback(new DomainError(DomainError.INTERNAL_ERROR, 'unable to read certificates from disk'));
if (!cert || !key) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
result.fallbackCertificate = { cert: cert, key: key };
@@ -196,7 +196,7 @@ function update(domain, provider, config, fallbackCertificate, tlsConfig, callba
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
if (fallbackCertificate) {
let error = reverseProxy.validateCertificate(`test.${domain}`, fallbackCertificate.cert, fallbackCertificate.key);
let error = reverseProxy.validateCertificate(fallbackCertificate.cert, fallbackCertificate.key, domain);
if (error) return callback(new DomainError(DomainError.BAD_FIELD, error.message));
}
@@ -256,7 +256,7 @@ function getName(domain, subdomain) {
return subdomain === '' ? part : subdomain + '.' + part;
}
function getDnsRecords(subdomain, domain, type, callback) {
function getDNSRecords(subdomain, domain, type, callback) {
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof type, 'string');
@@ -273,7 +273,7 @@ function getDnsRecords(subdomain, domain, type, callback) {
});
}
function upsertDnsRecords(subdomain, domain, type, values, callback) {
function upsertDNSRecords(subdomain, domain, type, values, callback) {
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof type, 'string');
@@ -293,7 +293,7 @@ function upsertDnsRecords(subdomain, domain, type, values, callback) {
});
}
function removeDnsRecords(subdomain, domain, type, values, callback) {
function removeDNSRecords(subdomain, domain, type, values, callback) {
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof type, 'string');
@@ -313,18 +313,22 @@ function removeDnsRecords(subdomain, domain, type, values, callback) {
});
}
// only wait for A record
function waitForDnsRecord(fqdn, domain, value, options, callback) {
function waitForDNSRecord(fqdn, domain, value, type, options, callback) {
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof value, 'string');
assert(typeof value === 'string' || util.isRegExp(value));
assert(type === 'A' || type === 'CNAME' || type === 'TXT');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
get(domain, function (error, result) {
if (error) return callback(error);
// domain can be not found when waiting for altDomain. When we migrate altDomain, this can never happen
if (error && error.reason !== DomainError.NOT_FOUND) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
api(result.provider).waitForDns(fqdn, result ? result.zoneName : domain, value, options, callback);
// hack for lack of provider with altDomain. When we migrate altDomain, this will be automatically "manual"
const provider = result ? result.provider : 'manual';
api(provider).waitForDns(fqdn, result ? result.zoneName : domain, value, type, options, callback);
});
}
+2 -2
View File
@@ -23,7 +23,7 @@ function sync(callback) {
debug('refreshDNS: current ip %s', ip);
domains.upsertDnsRecords(config.adminLocation(), config.adminDomain(), 'A', [ ip ], function (error) {
domains.upsertDNSRecords(config.adminLocation(), config.adminDomain(), 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('refreshDNS: done for admin location');
@@ -35,7 +35,7 @@ function sync(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);
domains.upsertDNSRecords(app.location, app.domain, 'A', [ ip ], callback);
}, function (error) {
if (error) return callback(error);
+12 -4
View File
@@ -22,6 +22,7 @@ exports = module.exports = {
ACTION_BACKUP_START: 'backup.start',
ACTION_BACKUP_CLEANUP: 'backup.cleanup',
ACTION_CERTIFICATE_RENEWAL: 'certificate.renew',
ACTION_CLI_MODE: 'settings.climode',
ACTION_START: 'cloudron.start',
ACTION_UPDATE: 'cloudron.update',
ACTION_USER_ADD: 'user.add',
@@ -90,14 +91,14 @@ function get(id, callback) {
});
}
function getAllPaged(actions, search, page, perPage, callback) {
assert(Array.isArray(actions));
function getAllPaged(action, search, page, perPage, callback) {
assert(typeof action === 'string' || action === null);
assert(typeof search === 'string' || search === null);
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function');
eventlogdb.getAllPaged(actions, search, page, perPage, function (error, events) {
eventlogdb.getAllPaged(action, search, page, perPage, function (error, events) {
if (error) return callback(new EventLogError(EventLogError.INTERNAL_ERROR, error));
callback(null, events);
@@ -121,7 +122,14 @@ function cleanup(callback) {
var d = new Date();
d.setDate(d.getDate() - 10); // 10 days ago
eventlogdb.delByCreationTime(d, function (error) {
// only cleanup high frequency events
var actions = [
exports.ACTION_USER_LOGIN,
exports.ACTION_BACKUP_START,
exports.ACTION_BACKUP_FINISH
];
eventlogdb.delByCreationTime(d, actions, function (error) {
if (error) return callback(new EventLogError(EventLogError.INTERNAL_ERROR, error));
callback(null);
+13 -12
View File
@@ -40,8 +40,8 @@ function get(eventId, callback) {
});
}
function getAllPaged(actions, search, page, perPage, callback) {
assert(Array.isArray(actions));
function getAllPaged(action, search, page, perPage, callback) {
assert(typeof action === 'string' || action === null);
assert(typeof search === 'string' || search === null);
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
@@ -50,15 +50,14 @@ function getAllPaged(actions, search, page, perPage, callback) {
var data = [];
var query = 'SELECT ' + EVENTLOGS_FIELDS + ' FROM eventlog';
if (actions.length || search) query += ' WHERE';
if (action || search) query += ' WHERE';
if (search) query += ' (source LIKE ' + mysql.escape('%' + search + '%') + ' OR data LIKE ' + mysql.escape('%' + search + '%') + ')';
if (action && search) query += ' AND ';
if (actions.length && search) query += ' AND ( ';
actions.forEach(function (action, i) {
query += ' (action LIKE ' + mysql.escape(`%${action}%`) + ') ';
if (i < actions.length-1) query += ' OR ';
});
if (actions.length && search) query += ' ) ';
if (action) {
query += ' action=?';
data.push(action);
}
query += ' ORDER BY creationTime DESC LIMIT ?,?';
@@ -121,13 +120,15 @@ function clear(callback) {
});
}
function delByCreationTime(creationTime, callback) {
function delByCreationTime(creationTime, actions, callback) {
assert(util.isDate(creationTime));
assert(Array.isArray(actions));
assert.strictEqual(typeof callback, 'function');
var query = 'DELETE FROM eventlog WHERE creationTime < ?';
var query = 'DELETE FROM eventlog WHERE creationTime < ? ';
if (actions.length) query += ' AND ( ' + actions.map(function () { return 'action != ?'; }).join(' AND ') + ' ) ';
database.query(query, [ creationTime ], function (error) {
database.query(query, [ creationTime ].concat(actions), function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(error);
+8 -3
View File
@@ -24,6 +24,7 @@ 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');
@@ -99,11 +100,15 @@ function remove(id, callback) {
// never allow admin group to be deleted
if (id === constants.ADMIN_GROUP_ID) return callback(new GroupError(GroupError.NOT_ALLOWED));
groupdb.del(id, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupError(GroupError.NOT_FOUND));
mailboxdb.delByOwnerId(id, function (error) {
if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, error));
callback(null);
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);
});
});
}
+1 -1
View File
@@ -18,7 +18,7 @@ exports = module.exports = {
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:1.0.0' },
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:1.0.1' },
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:1.0.0' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:1.2.2' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:1.0.0' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:1.0.0' }
}
};
+6 -8
View File
@@ -410,7 +410,7 @@ function authorizeUserForApp(req, res, next) {
// we return no such object, to avoid leakage of a users existence
if (!result) return next(new ldap.NoSuchObjectError(req.dn.toString()));
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', appId: app.id, app: app }, { userId: req.user.id, user: user.removePrivateFields(req.user) });
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', appId: app.id }, { userId: req.user.id });
res.end();
});
@@ -418,8 +418,6 @@ function authorizeUserForApp(req, res, next) {
}
function authenticateMailbox(req, res, next) {
debug('mailbox auth: %s (from %s)', req.dn.toString(), req.connection.ldap.id);
if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString()));
var email = req.dn.rdns[0].attrs.cn.value.toLowerCase();
@@ -430,11 +428,11 @@ function authenticateMailbox(req, res, next) {
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.message));
mail.getDomain(parts[1], function (error, domain) {
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.OWNER_TYPE_APP) {
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';
@@ -448,15 +446,15 @@ function authenticateMailbox(req, res, next) {
eventlog.add(eventlog.ACTION_APP_LOGIN, { authType: 'ldap', mailboxId: name }, { appId: mailbox.ownerId, addonId: addonId });
return res.end();
});
} else if (mailbox.ownerType === mailboxdb.OWNER_TYPE_USER) {
} else if (mailbox.ownerType === mailboxdb.TYPE_USER) {
if (!domain.enabled) return next(new ldap.NoSuchObjectError(req.dn.toString()));
user.verify(mailbox.ownerId, req.credentials || '', function (error, result) {
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: result.id, user: user.removePrivateFields(result) });
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: email }, { userId: user.username });
res.end();
});
} else {
+264 -358
View File
@@ -3,14 +3,11 @@
exports = module.exports = {
getStatus: getStatus,
getDomains: getDomains,
get: get,
getAll: getAll,
getDomain: getDomain,
addDomain: addDomain,
removeDomain: removeDomain,
updateDomain: updateDomain,
addDnsRecords: addDnsRecords,
add: add,
del: del,
setMailFromValidation: setMailFromValidation,
setCatchAllAddress: setCatchAllAddress,
@@ -22,20 +19,16 @@ exports = module.exports = {
sendTestMail: sendTestMail,
getMailboxes: getMailboxes,
removeMailboxes: removeMailboxes,
getMailbox: getMailbox,
addMailbox: addMailbox,
updateMailbox: updateMailbox,
removeMailbox: removeMailbox,
getUserMailbox: getUserMailbox,
enableUserMailbox: enableUserMailbox,
disableUserMailbox: disableUserMailbox,
listAliases: listAliases,
getAliases: getAliases,
setAliases: setAliases,
getLists: getLists,
getList: getList,
addList: addList,
updateList: updateList,
removeList: removeList,
_readDkimPublicKeySync: readDkimPublicKeySync,
@@ -49,8 +42,10 @@ var assert = require('assert'),
constants = require('./constants.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:mail'),
dns = require('./native-dns.js'),
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'),
@@ -66,10 +61,11 @@ var assert = require('assert'),
smtpTransport = require('nodemailer-smtp-transport'),
sysinfo = require('./sysinfo.js'),
user = require('./user.js'),
UserError = user.UserError,
util = require('util'),
_ = require('underscore');
const DNS_OPTIONS = { server: '127.0.0.1', timeout: 5000 }; // unbound runs on 127.0.0.1
const digOptions = { server: '127.0.0.1', port: 53, timeout: 5000 };
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
function MailError(reason, errorOrMessage) {
@@ -95,21 +91,20 @@ MailError.INTERNAL_ERROR = 'Internal Error';
MailError.BAD_FIELD = 'Bad Field';
MailError.ALREADY_EXISTS = 'Already Exists';
MailError.NOT_FOUND = 'Not Found';
MailError.IN_USE = 'In Use';
function validateName(name) {
assert.strictEqual(typeof name, 'string');
function validateAlias(alias) {
assert.strictEqual(typeof alias, 'string');
if (name.length < 1) return new MailError(MailError.BAD_FIELD, 'mailbox name must be atleast 1 char');
if (name.length >= 200) return new MailError(MailError.BAD_FIELD, 'mailbox name too long');
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(name) !== -1) return new MailError(MailError.BAD_FIELD, `mailbox name ${name} is reserved`);
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(name)) return new MailError(MailError.BAD_FIELD, 'mailbox name can only contain alphanumerals and dot');
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 (name.indexOf('.app') !== -1) return new MailError(MailError.BAD_FIELD, 'mailbox name pattern is reserved for apps');
if (alias.indexOf('.app') !== -1) return new MailError(MailError.BAD_FIELD, 'alias pattern is reserved for apps');
return null;
}
@@ -123,7 +118,7 @@ function checkOutboundPort25(callback) {
'smtp.mail.yahoo.com',
'smtp.o2.ie',
'smtp.comcast.net',
'smtp.1und1.de',
'outgoing.verizon.net'
]);
var relay = {
@@ -184,11 +179,9 @@ function verifyRelay(relay, callback) {
assert.strictEqual(typeof relay, 'object');
assert.strictEqual(typeof callback, 'function');
// we used to verify cloudron-smtp with checkOutboundPort25 but that is unreliable given that we just
// randomly select some smtp server
if (relay.provider === 'cloudron-smtp') return callback();
var verifier = relay.provider === 'cloudron-smtp' ? checkOutboundPort25 : checkSmtpRelay.bind(null, relay);
checkSmtpRelay(relay, function (error) {
verifier(function (error) {
if (error) return callback(new MailError(MailError.BAD_FIELD, error.message));
callback();
@@ -207,13 +200,14 @@ function checkDkim(domain, callback) {
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;
dkim.expected = '"v=DKIM1; t=s; p=' + dkimKey + '"';
dns.resolve(dkim.domain, dkim.type, DNS_OPTIONS, function (error, txtRecords) {
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 (txtRecords.length !== 0) {
dkim.value = txtRecords[0].join('');
if (Array.isArray(txtRecords) && txtRecords.length !== 0) {
dkim.value = txtRecords[0];
dkim.status = (dkim.value === dkim.expected);
}
@@ -226,18 +220,21 @@ function checkSpf(domain, callback) {
domain: domain,
type: 'TXT',
value: null,
expected: 'v=spf1 a:' + config.mailFqdn() + ' ~all',
expected: '"v=spf1 a:' + config.mailFqdn() + ' ~all"',
status: false
};
dns.resolve(spf.domain, spf.type, DNS_OPTIONS, function (error, txtRecords) {
// 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++) {
let txtRecord = txtRecords[i].join(''); // https://agari.zendesk.com/hc/en-us/articles/202952749-How-long-can-my-SPF-record-be-
if (txtRecord.indexOf('v=spf1 ') !== 0) continue; // not SPF
spf.value = txtRecord;
if (txtRecords[i].indexOf('"v=spf1 ') !== 0) continue; // not SPF
spf.value = txtRecords[i];
spf.status = spf.value.indexOf(' a:' + config.adminFqdn()) !== -1;
break;
}
@@ -245,7 +242,7 @@ function checkSpf(domain, callback) {
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);
spf.expected = '"v=spf1 a:' + config.adminFqdn() + ' ' + spf.value.slice('"v=spf1 '.length);
}
callback(null, spf);
@@ -261,12 +258,13 @@ function checkMx(domain, callback) {
status: false
};
dns.resolve(mx.domain, mx.type, DNS_OPTIONS, function (error, mxRecords) {
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 (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(' ');
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);
@@ -278,15 +276,16 @@ function checkDmarc(domain, callback) {
domain: '_dmarc.' + domain,
type: 'TXT',
value: null,
expected: 'v=DMARC1; p=reject; pct=100',
expected: '"v=DMARC1; p=reject; pct=100"',
status: false
};
dns.resolve(dmarc.domain, dmarc.type, DNS_OPTIONS, function (error, txtRecords) {
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 (txtRecords.length !== 0) {
dmarc.value = txtRecords[0].join('');
if (Array.isArray(txtRecords) && txtRecords.length !== 0) {
dmarc.value = txtRecords[0];
dmarc.status = (dmarc.value === dmarc.expected);
}
@@ -299,7 +298,7 @@ function checkPtr(callback) {
domain: null,
type: 'PTR',
value: null,
expected: config.mailFqdn(), // any trailing '.' is added by client software (https://lists.gt.net/spf/devel/7918)
expected: config.mailFqdn() + '.',
status: false
};
@@ -308,10 +307,11 @@ function checkPtr(callback) {
ptr.domain = ip.split('.').reverse().join('.') + '.in-addr.arpa';
dns.resolve(ptr.domain, 'PTR', DNS_OPTIONS, function (error, ptrRecords) {
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 (ptrRecords.length !== 0) {
if (Array.isArray(ptrRecords) && ptrRecords.length !== 0) {
ptr.value = ptrRecords.join(' ');
ptr.status = ptrRecords.some(function (v) { return v === ptr.expected; });
}
@@ -323,31 +323,15 @@ function checkPtr(callback) {
// https://raw.githubusercontent.com/jawsome/node-dnsbl/master/list.json
const RBL_LIST = [
{
'name': 'Abuse.ch',
'dns': 'spam.abuse.ch',
'site': 'http://abuse.ch/'
},
{
'name': 'Barracuda',
'dns': 'b.barracudacentral.org',
'site': 'http://www.barracudacentral.org/rbl/removal-request'
},
{
'name': 'Composite Blocking List',
'dns': 'cbl.abuseat.org',
'site': 'http://www.abuseat.org'
},
{
'name': 'Multi SURBL',
'dns': 'multi.surbl.org',
'site': 'http://www.surbl.org'
},
{
'name': 'Passive Spam Block List',
'dns': 'psbl.surriel.com',
'site': 'https://psbl.org'
'name': 'SpamCop',
'dns': 'bl.spamcop.net',
'site': 'http://spamcop.net'
},
{
'name': 'Sorbs Aggregate Zone',
@@ -360,20 +344,30 @@ const RBL_LIST = [
'site': 'http://sorbs.net'
},
{
'name': 'Spam Cannibal',
'dns': 'bl.spamcannibal.org',
'site': 'http://www.spamcannibal.org/cannibal.cgi'
},
{
'name': 'SpamCop',
'dns': 'bl.spamcop.net',
'site': 'http://spamcop.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 ',
@@ -397,15 +391,15 @@ function checkRblStatus(domain, callback) {
// https://tools.ietf.org/html/rfc5782
async.map(RBL_LIST, function (rblServer, iteratorDone) {
dns.resolve(flippedIp + '.' + rblServer.dns, 'A', DNS_OPTIONS, function (error, records) {
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);
dns.resolve(flippedIp + '.' + rblServer.dns, 'TXT', DNS_OPTIONS, function (error, txtRecords) {
result.txtRecords = error || !txtRecords ? 'No txt record' : txtRecords.map(x => x.join(''));
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);
@@ -445,7 +439,7 @@ function getStatus(domain, callback) {
};
}
getDomain(domain, function (error, result) {
get(domain, function (error, result) {
if (error) return callback(error);
var checks = [
@@ -475,59 +469,45 @@ function getStatus(domain, callback) {
function createMailConfig(callback) {
assert.strictEqual(typeof callback, 'function');
const mailFqdn = config.mailFqdn();
debug('createMailConfig: generating mail config');
getDomains(function (error, mailDomains) {
maildb.getAll(function (error, mailOutDomains) {
if (error) return callback(error);
user.getOwner(function (error, owner) {
const mailFqdn = config.mailFqdn();
const defaultDomain = config.adminDomain();
const alertsFrom = `no-reply@${defaultDomain}`;
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 mailOutDomains = mailDomains.map(function (d) { return d.domain; }).join(',');
const mailInDomains = mailDomains.filter(function (d) { return d.enabled; }).map(function (d) { return d.domain; }).join(',');
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(path.join(paths.ADDON_CONFIG_DIR, 'mail/mail.ini'),
`mail_in_domains=${mailInDomains}\nmail_out_domains=${mailOutDomains}\nmail_default_domain=${defaultDomain}\nmail_server_name=${mailFqdn}\nalerts_from=${alertsFrom}\nalerts_to=${alertsTo}\n\n`, 'utf8')) {
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));
}
// enable_outbound makes plugin forward email for relayed mail. non-relayed mail always hits LMTP plugin first
if (!safe.fs.writeFileSync(path.join(paths.ADDON_CONFIG_DIR, 'mail/smtp_forward.ini'), 'enable_outbound=false\ndomain_selector=mail_from\n', 'utf8')) {
return callback(new Error('Could not create smtp forward 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));
}
// create sections for per-domain configuration
mailDomains.forEach(function (domain) {
const catchAll = domain.catchAll.map(function (c) { return `${c}@${domain.domain}`; }).join(',');
const mailFromValidation = domain.mailFromValidation;
if (!safe.fs.appendFileSync(path.join(paths.ADDON_CONFIG_DIR, 'mail/mail.ini'),
`[${domain.domain}]\ncatch_all=${catchAll}\nmail_from_validation=${mailFromValidation}\n\n`, 'utf8')) {
return callback(new Error('Could not create mail var file:' + safe.error.message));
}
const relay = domain.relay;
const enableRelay = relay.provider !== 'cloudron-smtp',
host = relay.host || '',
port = relay.port || 25,
username = relay.username || '',
password = relay.password || '';
if (!enableRelay) return;
if (!safe.fs.appendFileSync(paths.ADDON_CONFIG_DIR + '/mail/smtp_forward.ini',
`[${domain.domain}]\nenable_outbound=true\nhost=${host}\nport=${port}\nenable_tls=true\nauth_type=plain\nauth_user=${username}\nauth_pass=${password}\n\n`, 'utf8')) {
return callback(new Error('Could not create mail var file:' + safe.error.message));
}
});
callback(null, mailInDomains.length !== 0 /* allowInbound */);
callback(null, mailInDomain.length !== 0);
});
});
}
@@ -544,7 +524,7 @@ function restartMail(callback) {
const memoryLimit = Math.max((1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 128, 256);
// admin and mail share the same certificate
reverseProxy.getCertificate({ fqdn: config.adminFqdn(), domain: config.adminDomain() }, function (error, bundle) {
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
@@ -571,7 +551,6 @@ function restartMail(callback) {
-v "${paths.MAIL_DATA_DIR}:/app/data" \
-v "${paths.PLATFORM_DATA_DIR}/addons/mail:/etc/mail" \
${ports} \
-p 127.0.0.1:2020:2020 \
--read-only -v /run -v /tmp ${tag}`;
shell.execSync('startMail', cmd);
@@ -581,7 +560,7 @@ function restartMail(callback) {
});
}
function getDomain(domain, callback) {
function get(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -593,7 +572,7 @@ function getDomain(domain, callback) {
});
}
function getDomains(callback) {
function getAll(callback) {
assert.strictEqual(typeof callback, 'function');
maildb.getAll(function (error, results) {
@@ -603,57 +582,16 @@ function getDomains(callback) {
});
}
// https://agari.zendesk.com/hc/en-us/articles/202952749-How-long-can-my-SPF-record-be-
function txtRecordsWithSpf(domain, callback) {
function ensureDkimKey(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
domains.getDnsRecords('', domain, 'TXT', function (error, txtRecords) {
if (error) return callback(error);
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('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.mailFqdn()) !== -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.mailFqdn() + ' ~all"');
debug('txtRecordsWithSpf: adding txt record');
} else { // just add ourself
txtRecords[i] = matches[1] + ' a:' + config.mailFqdn() + txtRecords[i].slice(matches[1].length);
debug('txtRecordsWithSpf: inserting txt record');
}
return callback(null, txtRecords);
});
}
function ensureDkimKeySync(domain) {
assert.strictEqual(typeof domain, 'string');
const dkimPath = path.join(paths.MAIL_DATA_DIR, `dkim/${domain}`);
const dkimPrivateKeyFile = path.join(dkimPath, 'private');
const dkimPublicKeyFile = path.join(dkimPath, 'public');
const dkimSelectorFile = path.join(dkimPath, 'selector');
if (safe.fs.existsSync(dkimPublicKeyFile) &&
safe.fs.existsSync(dkimPublicKeyFile) &&
safe.fs.existsSync(dkimPublicKeyFile)) {
debug(`Reusing existing DKIM keys for ${domain}`);
return null;
}
debug(`Generating new DKIM keys for ${domain}`);
debug('Generating new DKIM keys');
if (!safe.fs.mkdirSync(dkimPath) && safe.error.code !== 'EEXIST') {
debug('Error creating dkim.', safe.error);
@@ -665,7 +603,41 @@ function ensureDkimKeySync(domain) {
if (!safe.fs.writeFileSync(dkimSelectorFile, config.dkimSelector(), 'utf8')) return new MailError(MailError.INTERNAL_ERROR, safe.error);
return null;
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) {
@@ -691,9 +663,6 @@ function addDnsRecords(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
var error = ensureDkimKeySync(domain);
if (error) return callback(error);
if (process.env.BOX_ENV === 'test') return callback();
var dkimKey = readDkimPublicKeySync(domain);
@@ -707,67 +676,58 @@ function addDnsRecords(domain, callback) {
debug('addDnsRecords: %j', records);
txtRecordsWithSpf(domain, function (error, txtRecords) {
if (error) return callback(error);
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 });
if (txtRecords) records.push({ subdomain: '', domain: domain, type: 'TXT', values: txtRecords });
debug('addDnsRecords: will update %j', records);
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);
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);
callback(error);
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 addDomain(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
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));
async.series([
addDnsRecords.bind(null, domain), // do this first to ensure DKIM keys
restartMail
], NOOP_CALLBACK); // do these asynchronously
callback();
});
}
// this is just a way to resync the mail "dns" records via the UI
function updateDomain(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
getDomain(domain, function (error) {
if (error) return callback(error);
addDnsRecords(domain, NOOP_CALLBACK);
callback();
});
}
function removeDomain(domain, 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.IN_USE) return callback(new MailError(MailError.IN_USE));
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));
restartMail(NOOP_CALLBACK);
callback();
});
}
@@ -781,22 +741,22 @@ function setMailFromValidation(domain, enabled, callback) {
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)
createMailConfig(NOOP_CALLBACK);
callback(null);
});
}
function setCatchAllAddress(domain, addresses, callback) {
function setCatchAllAddress(domain, address, callback) {
assert.strictEqual(typeof domain, 'string');
assert(Array.isArray(addresses));
assert(Array.isArray(address));
assert.strictEqual(typeof callback, 'function');
maildb.update(domain, { catchAll: addresses }, function (error) {
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)
createMailConfig(NOOP_CALLBACK);
callback(null);
});
@@ -840,7 +800,7 @@ function setMailEnabled(domain, enabled, callback) {
];
async.mapSeries(records, function (record, iteratorCallback) {
domains.upsertDnsRecords(record.subdomain, domain, record.type, record.values, iteratorCallback);
domains.upsertDNSRecords(record.subdomain, domain, record.type, record.values, iteratorCallback);
}, NOOP_CALLBACK);
callback(null);
@@ -852,7 +812,7 @@ function sendTestMail(domain, to, callback) {
assert.strictEqual(typeof to, 'string');
assert.strictEqual(typeof callback, 'function');
getDomain(domain, function (error, result) {
get(domain, function (error, result) {
if (error) return callback(error);
mailer.sendTestMail(result.domain, to);
@@ -872,102 +832,72 @@ function getMailboxes(domain, callback) {
});
}
function removeMailboxes(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
mailboxdb.delByDomain(domain, function (error) {
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
callback();
});
}
function getMailbox(name, domain, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
mailboxdb.getMailbox(name, 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 addMailbox(name, domain, userId, callback) {
assert.strictEqual(typeof name, 'string');
function getUserMailbox(domain, userId, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof callback, 'function');
name = name.toLowerCase();
var error = validateName(name);
if (error) return callback(error);
mailboxdb.addMailbox(name, domain, userId, mailboxdb.OWNER_TYPE_USER, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new MailError(MailError.ALREADY_EXISTS, `mailbox ${name} already exists`));
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));
callback(null);
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 updateMailbox(name, domain, userId, callback) {
assert.strictEqual(typeof name, 'string');
function enableUserMailbox(domain, userId, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof callback, 'function');
name = name.toLowerCase();
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));
var error = validateName(name);
if (error) return callback(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));
mailboxdb.updateMailbox(name, domain, userId, mailboxdb.OWNER_TYPE_USER, 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);
callback(null);
});
});
}
function removeMailbox(name, domain, callback) {
function disableUserMailbox(domain, userId, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof callback, 'function');
mailboxdb.del(name, domain, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, 'no such mailbox'));
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));
callback(null);
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 listAliases(domain, callback) {
function getAliases(domain, userId, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof callback, 'function');
mailboxdb.listAliases(domain, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, error.message));
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));
callback(null, result);
});
}
if (!result.username) return callback(null, []);
function getAliases(name, domain, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
getMailbox(name, domain, function (error) {
if (error) return callback(error);
mailboxdb.getAliasesForName(name, domain, function (error, aliases) {
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));
@@ -976,30 +906,30 @@ function getAliases(name, domain, callback) {
});
}
function setAliases(name, domain, aliases, callback) {
assert.strictEqual(typeof name, 'string');
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 = validateName(aliases[i]);
var error = validateAlias(aliases[i]);
if (error) return callback(error);
}
mailboxdb.setAliasesForName(name, domain, aliases, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS && error.message.indexOf('mailboxes_name_domain_unique_index') !== -1) {
var aliasMatch = error.message.match(new RegExp(`^ER_DUP_ENTRY: Duplicate entry '(.*)-${domain}' for key 'mailboxes_name_domain_unique_index'$`))
if (!aliasMatch) return callback(new MailError(MailError.ALREADY_EXISTS, error.message));
return callback(new MailError(MailError.ALREADY_EXISTS, `Mailbox, mailinglist or alias for ${aliasMatch[1]} already exists`));
}
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'));
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));
callback(null);
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);
});
});
}
@@ -1014,80 +944,56 @@ function getLists(domain, callback) {
});
}
function getList(domain, listName, callback) {
function getList(domain, groupId, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof listName, 'string');
assert.strictEqual(typeof groupId, 'string');
assert.strictEqual(typeof callback, 'function');
mailboxdb.getGroup(listName, domain, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, 'no such list'));
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));
callback(null, result);
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(name, domain, members, callback) {
function addList(domain, groupId, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof name, 'string');
assert(Array.isArray(members));
assert.strictEqual(typeof groupId, 'string');
assert.strictEqual(typeof callback, 'function');
name = name.toLowerCase();
var error = validateName(name);
if (error) return callback(error);
for (var i = 0; i < members.length; i++) {
members[i] = members[i].toLowerCase();
error = validateName(members[i]);
if (error) return callback(error);
}
mailboxdb.addGroup(name, domain, members, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new MailError(MailError.ALREADY_EXISTS, 'list already exits'));
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));
callback();
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 updateList(name, domain, members, callback) {
assert.strictEqual(typeof name, 'string');
function removeList(domain, groupId, callback) {
assert.strictEqual(typeof domain, 'string');
assert(Array.isArray(members));
assert.strictEqual(typeof groupId, 'string');
assert.strictEqual(typeof callback, 'function');
name = name.toLowerCase();
var error = validateName(name);
if (error) return callback(error);
for (var i = 0; i < members.length; i++) {
members[i] = members[i].toLowerCase();
error = validateName(members[i]);
if (error) return callback(error);
}
mailboxdb.updateList(name, domain, members, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, 'no such mailbox'));
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));
callback(null);
});
}
function removeList(domain, listName, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof listName, 'string');
assert.strictEqual(typeof callback, 'function');
mailboxdb.del(listName, 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();
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();
});
});
}
+1 -1
View File
@@ -9,7 +9,7 @@ This is most likely a problem in the application.
To resolve this, you can try the following:
* Restart the app in the app configuration dialog
* Restore the app to the latest backup
* Contact us via support@cloudron.io or https://forum.cloudron.io
* Contact us via support@cloudron.io or https://chat.cloudron.io
Powered by https://cloudron.io
+56 -132
View File
@@ -1,11 +1,7 @@
'use strict';
exports = module.exports = {
addMailbox: addMailbox,
addGroup: addGroup,
updateMailbox: updateMailbox,
updateList: updateList,
add: add,
del: del,
listAliases: listAliases,
@@ -21,44 +17,31 @@ exports = module.exports = {
getByOwnerId: getByOwnerId,
delByOwnerId: delByOwnerId,
delByDomain: delByDomain,
updateName: updateName,
_clear: clear,
TYPE_MAILBOX: 'mailbox',
TYPE_LIST: 'list',
TYPE_ALIAS: 'alias',
OWNER_TYPE_USER: 'user',
OWNER_TYPE_APP: 'app',
OWNER_TYPE_GROUP: 'group' // obsolete
TYPE_USER: 'user',
TYPE_APP: 'app',
TYPE_GROUP: 'group'
};
var assert = require('assert'),
database = require('./database.js'),
DatabaseError = require('./databaseerror.js'),
safe = require('safetydance'),
util = require('util');
var MAILBOX_FIELDS = [ 'name', 'type', 'ownerId', 'ownerType', 'aliasTarget', 'creationTime', 'membersJson', 'domain' ].join(',');
var MAILBOX_FIELDS = [ 'name', 'ownerId', 'ownerType', 'aliasTarget', 'creationTime', 'domain' ].join(',');
function postProcess(data) {
data.members = safe.JSON.parse(data.membersJson) || [ ];
delete data.membersJson;
return data;
}
function addMailbox(name, domain, ownerId, ownerType, callback) {
function add(name, domain, ownerId, ownerType, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof ownerId, 'string');
assert.strictEqual(typeof ownerType, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('INSERT INTO mailboxes (name, type, domain, ownerId, ownerType) VALUES (?, ?, ?, ?, ?)', [ name, exports.TYPE_MAILBOX, domain, ownerId, ownerType ], function (error) {
database.query('INSERT INTO mailboxes (name, domain, ownerId, ownerType) VALUES (?, ?, ?, ?)', [ name, domain, ownerId, ownerType ], function (error) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, 'mailbox already exists'));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
@@ -66,51 +49,6 @@ function addMailbox(name, domain, ownerId, ownerType, callback) {
});
}
function updateMailbox(name, domain, ownerId, ownerType, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof ownerId, 'string');
assert.strictEqual(typeof ownerType, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('UPDATE mailboxes SET ownerId = ? WHERE name = ? AND domain = ? AND ownerType = ?', [ ownerId, name, domain, ownerType ], 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 addGroup(name, domain, members, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert(Array.isArray(members));
assert.strictEqual(typeof callback, 'function');
database.query('INSERT INTO mailboxes (name, type, domain, ownerId, ownerType, membersJson) VALUES (?, ?, ?, ?, ?, ?)',
[ name, exports.TYPE_LIST, domain, 'admin', exports.OWNER_TYPE_GROUP, JSON.stringify(members) ], function (error) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, 'mailbox already exists'));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null);
});
}
function updateList(name, domain, members, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert(Array.isArray(members));
assert.strictEqual(typeof callback, 'function');
database.query('UPDATE mailboxes SET membersJson = ? WHERE name = ? AND domain = ?',
[ JSON.stringify(members), name, domain ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
callback(null);
});
}
function clear(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -134,21 +72,11 @@ function del(name, domain, callback) {
});
}
function delByDomain(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('DELETE FROM mailboxes WHERE domain = ?', [ domain ], function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null);
});
}
function delByOwnerId(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
// deletes aliases as well
database.query('DELETE FROM mailboxes WHERE ownerId=?', [ id ], function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
@@ -180,41 +108,35 @@ function getMailbox(name, domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE name = ? AND type = ? AND domain = ?',
[ name, exports.TYPE_MAILBOX, 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));
database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE name = ? AND domain = ? AND (ownerType = ? OR ownerType = ?) AND aliasTarget IS NULL', [ name, domain, exports.TYPE_APP, exports.TYPE_USER ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
callback(null, postProcess(results[0]));
});
callback(null, results[0]);
});
}
function listMailboxes(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE type = ? AND domain = ? ORDER BY name',
[ exports.TYPE_MAILBOX, domain ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE domain = ? AND (ownerType = ? OR ownerType = ?) AND aliasTarget IS NULL ORDER BY name', [ domain, exports.TYPE_APP, exports.TYPE_USER ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
results.forEach(function (result) { postProcess(result); });
callback(null, results);
});
callback(null, results);
});
}
function listGroups(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE type = ? AND domain = ?',
[ exports.TYPE_LIST, domain ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
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));
results.forEach(function (result) { postProcess(result); });
callback(null, results);
});
callback(null, results);
});
}
function getGroup(name, domain, callback) {
@@ -222,13 +144,25 @@ function getGroup(name, domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE type = ? AND name = ? AND domain = ?',
[ exports.TYPE_LIST, name, domain ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
// This can be merged into a single query but cannot get 'not found' information
// SELECT users.username FROM mailboxes
// INNER JOIN groupMembers ON mailboxes.ownerId = groupMembers.groupId
// INNER JOIN users ON groupMembers.userId = users.id
// WHERE mailboxes.name = <name>
callback(null, postProcess(results[0]));
database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE name = ? AND domain = ? AND ownerType = ? AND aliasTarget IS NULL', [ name, domain, exports.TYPE_GROUP ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
// username can be null if the user has not signed up with the invite yet
database.query('SELECT users.username FROM groupMembers INNER JOIN users ON groupMembers.userId = users.id WHERE groupMembers.groupId = ? AND users.username IS NOT NULL', [ results[0].ownerId ], function (error, memberList) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
results[0].members = memberList.map(function (m) { return m.username; });
callback(null, results[0]);
});
});
}
function getByOwnerId(ownerId, callback) {
@@ -239,8 +173,6 @@ function getByOwnerId(ownerId, callback) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
results.forEach(function (result) { postProcess(result); });
callback(null, results);
});
}
@@ -256,11 +188,10 @@ function setAliasesForName(name, domain, aliases, callback) {
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
var queries = [];
// clear existing aliases
queries.push({ query: 'DELETE FROM mailboxes WHERE aliasTarget = ? AND domain = ? AND type = ?', args: [ name, domain, exports.TYPE_ALIAS ] });
queries.push({ query: 'DELETE FROM mailboxes WHERE aliasTarget = ? AND domain = ?', args: [ name, domain ] });
aliases.forEach(function (alias) {
queries.push({ query: 'INSERT INTO mailboxes (name, type, domain, aliasTarget, ownerId, ownerType) VALUES (?, ?, ?, ?, ?, ?)',
args: [ alias, exports.TYPE_ALIAS, domain, name, results[0].ownerId, results[0].ownerType ] });
queries.push({ query: 'INSERT INTO mailboxes (name, domain, aliasTarget, ownerId, ownerType) VALUES (?, ?, ?, ?, ?)',
args: [ alias, domain, name, results[0].ownerId, results[0].ownerType ] });
});
database.transaction(queries, function (error) {
@@ -277,27 +208,23 @@ function getAliasesForName(name, domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT name FROM mailboxes WHERE type = ? AND aliasTarget = ? AND domain = ? ORDER BY name',
[ exports.TYPE_ALIAS, name, domain ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
database.query('SELECT name FROM mailboxes WHERE aliasTarget = ? AND domain = ? ORDER BY name', [ name, domain ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
results = results.map(function (r) { return r.name; });
callback(null, results);
});
results = results.map(function (r) { return r.name; });
callback(null, results);
});
}
function listAliases(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE domain = ? AND type = ? ORDER BY name',
[ domain, exports.TYPE_ALIAS ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE domain = ? AND aliasTarget IS NOT NULL ORDER BY name', [ domain ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
results.forEach(function (result) { postProcess(result); });
callback(null, results);
});
callback(null, results);
});
}
function getAlias(name, domain, callback) {
@@ -305,13 +232,10 @@ function getAlias(name, domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE name = ? AND type = ? AND domain = ?',
[ name, exports.TYPE_ALIAS, 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));
database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE name = ? AND domain = ? AND aliasTarget IS NOT NULL', [ name, domain ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
results.forEach(function (result) { postProcess(result); });
callback(null, results[0]);
});
callback(null, results[0]);
});
}
-1
View File
@@ -61,7 +61,6 @@ function del(domain, callback) {
// deletes aliases as well
database.query('DELETE FROM mail WHERE domain=?', [ domain ], function (error, result) {
if (error && error.code === 'ER_ROW_IS_REFERENCED_2') return callback(new DatabaseError(DatabaseError.IN_USE));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
+1 -1
View File
@@ -74,7 +74,7 @@ function getMailConfig(callback) {
cloudronName = 'Cloudron';
}
mail.getDomains(function (error, domains) {
mail.getAll(function (error, domains) {
if (error) return callback(error);
if (domains.length === 0) return callback('No domains configured');
-35
View File
@@ -1,35 +0,0 @@
'use strict';
exports = module.exports = {
resolve: resolve
};
var assert = require('assert'),
dns = require('dns');
// a note on TXT records. It doesn't have quotes ("") at the DNS level. Those quotes
// are added for DNS server software to enclose spaces. Such quotes may also be returned
// by the DNS REST API of some providers
function resolve(hostname, rrtype, options, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof rrtype, 'string');
assert(options && typeof options === 'object');
assert.strictEqual(typeof callback, 'function');
const resolver = new dns.Resolver();
if (options.server) resolver.setServers([ options.server ]);
// should callback with ECANCELLED but looks like we might hit https://github.com/nodejs/node/issues/14814
const timerId = setTimeout(resolver.cancel.bind(resolver), options.timeout || 5000);
resolver.resolve(hostname, rrtype, function (error, result) {
clearTimeout(timerId);
if (error && error.code === 'ECANCELLED') error.code = 'TIMEOUT';
// result is an empty array if there was no error but there is no record. when you query a random
// domain, it errors with ENOTFOUND. But if you query an existing domain (A record) but with different
// type (CNAME) it is not an error and empty array
callback(error, result);
});
}
+1 -1
View File
@@ -33,7 +33,7 @@
</div>
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Sign in"/>
</form>
<a href="/api/v1/session/password/resetRequest.html">Reset password</a>
<a href="/api/v1/session/password/resetRequest.html">Reset your password</a>
</div>
</div>
</div>
+1 -1
View File
@@ -5,7 +5,7 @@
<div class="layout-content">
<center>
<h2>Reset password</h2>
<h2>Reset your password</h2>
</center>
<br/>
+1 -1
View File
@@ -41,7 +41,7 @@ function setDetail(tag, detail) {
assert.strictEqual(typeof tag, 'string');
assert.strictEqual(typeof detail, 'string');
if (!progress[tag]) return debug('[%s] %s', tag, detail);
if (!progress[tag]) return debug('unable to set detail %s', detail);
progress[tag].detail = detail;
}
+38 -28
View File
@@ -32,7 +32,6 @@ var acme = require('./cert/acme.js'),
caas = require('./cert/caas.js'),
config = require('./config.js'),
constants = require('./constants.js'),
crypto = require('crypto'),
debug = require('debug')('box:certificates'),
domains = require('./domains.js'),
ejs = require('ejs'),
@@ -40,12 +39,12 @@ var acme = require('./cert/acme.js'),
fallback = require('./cert/fallback.js'),
fs = require('fs'),
mailer = require('./mailer.js'),
os = require('os'),
path = require('path'),
paths = require('./paths.js'),
platform = require('./platform.js'),
safe = require('safetydance'),
shell = require('./shell.js'),
tld = require('tldjs'),
user = require('./user.js'),
util = require('util');
@@ -85,11 +84,12 @@ function getApi(app, callback) {
if (domain.tlsConfig.provider === 'fallback') return callback(null, fallback, {});
var api = domain.tlsConfig.provider === 'caas' ? caas : acme;
// 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;
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'
}
@@ -138,11 +138,22 @@ function validateCertificate(domain, cert, key) {
if (!cert && key) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'missing cert');
if (cert && !key) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'missing key');
// -checkhost checks for SAN or CN exclusively. SAN takes precedence and if present, ignores the CN.
var result = safe.child_process.execSync(`openssl x509 -noout -checkhost "${domain}"`, { encoding: 'utf8', input: cert });
var result = safe.child_process.execSync('openssl x509 -noout -checkhost "' + domain + '"', { encoding: 'utf8', input: cert });
if (!result) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'Unable to get certificate subject.');
if (result.indexOf('does match certificate') === -1) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, `Certificate is not valid for this domain. Expecting ${domain}`);
// 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 });
@@ -175,14 +186,8 @@ function setFallbackCertificate(domain, fallback, callback) {
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${domain}.host.cert`), fallback.cert)) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${domain}.host.key`), fallback.key)) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, safe.error.message));
} else if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) { // generate it
let opensslConf = safe.fs.readFileSync('/etc/ssl/openssl.cnf', 'utf8');
// SAN must contain all the domains since CN check is based on implementation if SAN is found. -checkhost also checks only SAN if present!
let opensslConfWithSan = `${opensslConf}\n[SAN]\nsubjectAltName=DNS:${domain},DNS:*.${domain}\n`;
let configFile = path.join(os.tmpdir(), 'openssl-' + crypto.randomBytes(4).readUInt32LE(0) + '.conf');
safe.fs.writeFileSync(configFile, opensslConfWithSan, 'utf8');
let certCommand = util.format(`openssl req -x509 -newkey rsa:2048 -keyout ${keyFilePath} -out ${certFilePath} -days 3650 -subj /CN=*.${domain} -extensions SAN -config ${configFile} -nodes`);
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));
safe.fs.unlinkSync(configFile);
}
platform.handleCertChanged('*.' + domain);
@@ -215,13 +220,15 @@ function getCertificate(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
var certFilePath = path.join(paths.APP_CERTS_DIR, `${app.fqdn}.user.cert`);
var keyFilePath = path.join(paths.APP_CERTS_DIR, `${app.fqdn}.user.key`);
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, `${app.fqdn}.cert`);
keyFilePath = path.join(paths.APP_CERTS_DIR, `${app.fqdn}.key`);
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 });
@@ -233,7 +240,7 @@ function ensureCertificate(app, auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
const vhost = app.fqdn;
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`);
@@ -271,7 +278,7 @@ function ensureCertificate(app, auditSource, callback) {
eventlog.add(eventlog.ACTION_CERTIFICATE_RENEWAL, auditSource, { domain: vhost, errorMessage: errorMessage });
// if no cert was returned use fallback. the fallback/caas provider will not provide any for example
if (!certFilePath || !keyFilePath) return getFallbackCertificate(app.domain, callback);
if (!certFilePath || !keyFilePath) return getFallbackCertificate(app.altDomain ? tld.getDomain(app.altDomain) : app.domain, callback);
callback(null, { certFilePath, keyFilePath, reason: 'new-le' });
});
@@ -307,7 +314,7 @@ function configureAdmin(auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
var adminApp = { domain: config.adminDomain(), fqdn: config.adminFqdn() };
var adminApp = { domain: config.adminDomain(), intrinsicFqdn: config.adminFqdn() };
ensureCertificate(adminApp, auditSource, function (error, bundle) {
if (error) return callback(error);
@@ -322,11 +329,12 @@ function configureAppInternal(app, bundle, callback) {
var sourceDir = path.resolve(__dirname, '..');
var endpoint = 'app';
var vhost = app.altDomain || app.intrinsicFqdn;
var data = {
sourceDir: sourceDir,
adminOrigin: config.adminOrigin(),
vhost: app.fqdn,
vhost: vhost,
hasIPv6: config.hasIPv6(),
port: app.httpPort,
endpoint: endpoint,
@@ -338,10 +346,10 @@ function configureAppInternal(app, bundle, callback) {
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, app.id + '.conf');
debug('writing config for "%s" to %s with options %j', app.fqdn, nginxConfigFilename, data);
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', app.fqdn, safe.error.message);
debug('Error creating nginx config for "%s" : %s', vhost, safe.error.message);
return callback(safe.error);
}
@@ -364,9 +372,11 @@ 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', app.fqdn, safe.error.message);
if (safe.error.code !== 'ENOENT') debug('Error removing nginx configuration of "%s": %s', vhost, safe.error.message);
return callback(null);
}
@@ -382,21 +392,21 @@ function renewAll(auditSource, callback) {
apps.getAll(function (error, allApps) {
if (error) return callback(error);
allApps.push({ domain: config.adminDomain(), fqdn: config.adminFqdn() }); // inject fake webadmin app
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.fqdn === config.adminFqdn() ?
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.fqdn);
platform.handleCertChanged(app.intrinsicFqdn);
iteratorCallback(); // move to next app
});
+6 -4
View File
@@ -58,6 +58,8 @@ function removeInternalAppFields(app) {
iconUrl: app.iconUrl,
fqdn: app.fqdn,
memoryLimit: app.memoryLimit,
altDomain: app.altDomain,
cnameTarget: app.cnameTarget,
xFrameOptions: app.xFrameOptions,
sso: app.sso,
debugMode: app.debugMode,
@@ -132,6 +134,9 @@ function installApp(req, res, next) {
if ('memoryLimit' in data && typeof data.memoryLimit !== 'number') return next(new HttpError(400, 'memoryLimit is not a number'));
// falsy value in altDomain unsets it
if (data.altDomain && typeof data.altDomain !== 'string') return next(new HttpError(400, 'altDomain must be a string'));
if (data.xFrameOptions && typeof data.xFrameOptions !== 'string') return next(new HttpError(400, 'xFrameOptions must be a string'));
if ('sso' in data && typeof data.sso !== 'boolean') return next(new HttpError(400, 'sso must be a boolean'));
@@ -176,6 +181,7 @@ function configureApp(req, res, next) {
if (!data.cert && data.key) return next(new HttpError(400, 'cert must be provided'));
if ('memoryLimit' in data && typeof data.memoryLimit !== 'number') return next(new HttpError(400, 'memoryLimit is not a number'));
if (data.altDomain && typeof data.altDomain !== 'string') return next(new HttpError(400, 'altDomain must be a string'));
if (data.xFrameOptions && typeof data.xFrameOptions !== 'string') return next(new HttpError(400, 'xFrameOptions must be a string'));
if ('enableBackup' in data && typeof data.enableBackup !== 'boolean') return next(new HttpError(400, 'enableBackup must be a boolean'));
@@ -237,13 +243,9 @@ function cloneApp(req, res, next) {
apps.clone(req.params.id, data, auditSource(req), function (error, result) {
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
if (error && error.reason === AppsError.PORT_RESERVED) return next(new HttpError(409, 'Port ' + error.message + ' is reserved.'));
if (error && error.reason === AppsError.PORT_CONFLICT) return next(new HttpError(409, 'Port ' + error.message + ' is already in use.'));
if (error && error.reason === AppsError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
if (error && error.reason === AppsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message));
if (error && error.reason === AppsError.BILLING_REQUIRED) return next(new HttpError(402, 'Billing required'));
if (error && error.reason === AppsError.BAD_CERTIFICATE) return next(new HttpError(400, error.message));
if (error && error.reason === AppsError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
if (error) return next(new HttpError(500, error));
-1
View File
@@ -89,7 +89,6 @@ function feedback(req, res, next) {
if (VALID_TYPES.indexOf(req.body.type) === -1) return next(new HttpError(400, 'unknown type'));
if (typeof req.body.subject !== 'string' || !req.body.subject) return next(new HttpError(400, 'subject must be string'));
if (typeof req.body.description !== 'string' || !req.body.description) return next(new HttpError(400, 'description must be string'));
if (req.body.appId && typeof req.body.appId !== 'string') return next(new HttpError(400, 'appId must be string'));
appstore.sendFeedback(_.extend(req.body, { email: req.user.email, displayName: req.user.displayName }), function (error) {
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'));
+6 -3
View File
@@ -9,14 +9,17 @@ var developer = require('../developer.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess;
function auditSource(req) {
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
return { ip: ip, username: req.user ? req.user.username : null, userId: req.user ? req.user.id : null };
}
function login(req, res, next) {
passport.authenticate('local', function (error, user) {
if (error) return next(new HttpError(500, error));
if (!user) return next(new HttpError(401, 'Invalid credentials'));
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
developer.issueDeveloperToken(user, ip, function (error, result) {
developer.issueDeveloperToken(user, auditSource(req), function (error, result) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { token: result.token, expiresAt: result.expiresAt }));
+1 -7
View File
@@ -29,9 +29,6 @@ function add(req, res, next) {
if ('tlsConfig' in req.body && typeof req.body.tlsConfig !== 'object') return next(new HttpError(400, 'tlsConfig must be a object with a provider string property'));
if (req.body.tlsConfig && (!req.body.tlsConfig.provider || typeof req.body.tlsConfig.provider !== 'string')) return next(new HttpError(400, 'tlsConfig.provider must be a string'));
// some DNS providers like DigitalOcean take a really long time to verify credentials (https://github.com/expressjs/timeout/issues/26)
req.clearTimeout();
domains.add(req.body.domain, req.body.zoneName || '', req.body.provider, req.body.config, req.body.fallbackCertificate || null, req.body.tlsConfig || { provider: 'letsencrypt-prod' }, function (error) {
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));
@@ -75,9 +72,6 @@ function update(req, res, next) {
if ('tlsConfig' in req.body && typeof req.body.tlsConfig !== 'object') return next(new HttpError(400, 'tlsConfig must be a object with a provider string property'));
if (req.body.tlsConfig && (!req.body.tlsConfig.provider || typeof req.body.tlsConfig.provider !== 'string')) return next(new HttpError(400, 'tlsConfig.provider must be a string'));
// some DNS providers like DigitalOcean take a really long time to verify credentials (https://github.com/expressjs/timeout/issues/26)
req.clearTimeout();
domains.update(req.params.domain, req.body.provider, req.body.config, req.body.fallbackCertificate || null, req.body.tlsConfig || { provider: 'letsencrypt-prod' }, function (error) {
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));
@@ -93,7 +87,7 @@ function del(req, res, next) {
domains.del(req.params.domain, function (error) {
if (error && error.reason === DomainError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === DomainError.IN_USE) return next(new HttpError(409, 'Domain is still in use. Remove all apps and mailboxes using this domain'));
if (error && error.reason === DomainError.IN_USE) return next(new HttpError(409, 'Domain is still in use'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(204));
+1 -5
View File
@@ -15,14 +15,10 @@ function get(req, res, next) {
var perPage = typeof req.query.per_page !== 'undefined'? parseInt(req.query.per_page) : 25;
if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a postive number'));
if (req.query.actions && typeof req.query.actions !== 'string') return next(new HttpError(400, 'actions must be a comma separated string'));
if (req.query.action && typeof req.query.action !== 'string') return next(new HttpError(400, 'action must be a string'));
if (req.query.search && typeof req.query.search !== 'string') return next(new HttpError(400, 'search must be a string'));
var actions = req.query.actions ? req.query.actions.split(',').map(function (s) { return s.trim(); }) : [];
if (req.query.action) actions.push(req.query.action);
eventlog.getAllPaged(actions, req.query.search || null, page, perPage, function (error, result) {
eventlog.getAllPaged(req.query.action || null, req.query.search || null, page, perPage, function (error, result) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { eventlogs: result }));
+39 -133
View File
@@ -1,11 +1,10 @@
'use strict';
exports = module.exports = {
getDomain: getDomain,
addDomain: addDomain,
getDomainStats: getDomainStats,
updateDomain: updateDomain,
removeDomain: removeDomain,
get: get,
add: add,
del: del,
getStatus: getStatus,
@@ -17,19 +16,16 @@ exports = module.exports = {
sendTestMail: sendTestMail,
getMailboxes: getMailboxes,
getMailbox: getMailbox,
addMailbox: addMailbox,
updateMailbox: updateMailbox,
removeMailbox: removeMailbox,
getUserMailbox: getUserMailbox,
enableUserMailbox: enableUserMailbox,
disableUserMailbox: disableUserMailbox,
listAliases: listAliases,
getAliases: getAliases,
setAliases: setAliases,
getLists: getLists,
getList: getList,
addList: addList,
updateList: updateList,
removeList: removeList
};
@@ -37,16 +33,12 @@ var assert = require('assert'),
mail = require('../mail.js'),
MailError = mail.MailError,
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
middleware = require('../middleware/index.js'),
url = require('url');
HttpSuccess = require('connect-lastmile').HttpSuccess;
var mailProxy = middleware.proxy(url.parse('http://127.0.0.1:2020'));
function getDomain(req, res, next) {
function get(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
mail.getDomain(req.params.domain, function (error, result) {
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));
@@ -54,12 +46,12 @@ function getDomain(req, res, next) {
});
}
function addDomain(req, res, next) {
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.addDomain(req.body.domain, function (error) {
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));
@@ -68,39 +60,13 @@ function addDomain(req, res, next) {
});
}
function getDomainStats(req, res, next) {
function del(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
var parsedUrl = url.parse(req.url, true /* parseQueryString */);
delete parsedUrl.query['access_token'];
delete req.headers['authorization'];
delete req.headers['cookies'];
req.url = url.format({ pathname: req.params.domain, query: parsedUrl.query });
mailProxy(req, res, next);
}
function updateDomain(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.domain, 'string');
mail.updateDomain(req.params.domain, function (error) {
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(202));
});
}
function removeDomain(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
mail.removeDomain(req.params.domain, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === MailError.IN_USE) return next(new HttpError(409, 'Mail domain is still in use. Remove existing mailboxes'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(204));
});
}
@@ -108,9 +74,6 @@ function removeDomain(req, res, next) {
function getStatus(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
// can take a while to query all the DNS entries
req.clearTimeout();
mail.getStatus(req.params.domain, function (error, records) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
@@ -138,14 +101,13 @@ function setCatchAllAddress(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.body, 'object');
if (!req.body.addresses) return next(new HttpError(400, 'addresses is required'));
if (!Array.isArray(req.body.addresses)) return next(new HttpError(400, 'addresses must be an array of strings'));
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.addresses.length; i++) {
if (typeof req.body.addresses[i] !== 'string') return next(new HttpError(400, 'addresses must be an array of strings'));
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.addresses, function (error) {
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));
@@ -213,11 +175,11 @@ function getMailboxes(req, res, next) {
});
}
function getMailbox(req, res, next) {
function getUserMailbox(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.params.name, 'string');
assert.strictEqual(typeof req.params.userId, 'string');
mail.getMailbox(req.params.name, req.params.domain, function (error, result) {
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));
@@ -225,65 +187,36 @@ function getMailbox(req, res, next) {
});
}
function addMailbox(req, res, next) {
function enableUserMailbox(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.params.userId, 'string');
if (typeof req.body.name !== 'string') return next(new HttpError(400, 'name must be a string'));
if (typeof req.body.userId !== 'string') return next(new HttpError(400, 'userId must be a string'));
mail.addMailbox(req.body.name, req.params.domain, req.body.userId, function (error) {
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 HttpError(409, error.message));
if (error && error.reason === MailError.BAD_FIELD) return next(new HttpError(400, 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 updateMailbox(req, res, next) {
function disableUserMailbox(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.params.name, 'string');
assert.strictEqual(typeof req.params.userId, 'string');
if (typeof req.body.userId !== 'string') return next(new HttpError(400, 'userId must be a string'));
mail.updateMailbox(req.params.name, req.params.domain, req.body.userId, 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(204));
});
}
function removeMailbox(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.params.name, 'string');
mail.removeMailbox(req.params.name, req.params.domain, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
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 listAliases(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
mail.listAliases(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, { aliases: result }));
});
}
function getAliases(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.params.name, 'string');
assert.strictEqual(typeof req.params.userId, 'string');
mail.getAliases(req.params.name, req.params.domain, function (error, result) {
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));
@@ -293,7 +226,7 @@ function getAliases(req, res, next) {
function setAliases(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.params.name, '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'));
@@ -302,10 +235,8 @@ function setAliases(req, res, next) {
if (typeof req.body.aliases[i] !== 'string') return next(new HttpError(400, 'alias must be a string'));
}
mail.setAliases(req.params.name, req.params.domain, req.body.aliases, function (error) {
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 && error.reason === MailError.ALREADY_EXISTS) return next(new HttpError(409, 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));
@@ -325,9 +256,9 @@ function getLists(req, res, next) {
function getList(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.params.name, 'string');
assert.strictEqual(typeof req.params.groupId, 'string');
mail.getList(req.params.domain, req.params.name, function (error, result) {
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));
@@ -339,47 +270,22 @@ function addList(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.name !== 'string') return next(new HttpError(400, 'name must be a string'));
if (!Array.isArray(req.body.members)) return next(new HttpError(400, 'members must be a string'));
if (typeof req.body.groupId !== 'string') return next(new HttpError(400, 'groupId must be a string'));
for (var i = 0; i < req.body.members.length; i++) {
if (typeof req.body.members[i] !== 'string') return next(new HttpError(400, 'member must be a string'));
}
mail.addList(req.body.name, req.params.domain, req.body.members, function (error) {
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 && error.reason === MailError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(201, {}));
});
}
function updateList(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.params.name, 'string');
if (!Array.isArray(req.body.members)) return next(new HttpError(400, 'members must be a string'));
for (var i = 0; i < req.body.members.length; i++) {
if (typeof req.body.members[i] !== 'string') return next(new HttpError(400, 'member must be a string'));
}
mail.updateList(req.params.name, req.params.domain, req.body.members, 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(204));
});
}
function removeList(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.params.name, 'string');
assert.strictEqual(typeof req.params.groupId, 'string');
mail.removeList(req.params.domain, req.params.name, function (error) {
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));
+5 -6
View File
@@ -26,10 +26,9 @@ var apps = require('../apps'),
util = require('util'),
_ = require('underscore');
// appObject is optional here
function auditSource(req, appId, appObject) {
function auditSource(req, appId) {
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
return { authType: 'oauth', ip: ip, appId: appId, app: appObject };
return { authType: 'oauth', ip: ip, appId: appId };
}
// create OAuth 2.0 server
@@ -240,7 +239,7 @@ function loginForm(req, res) {
apps.get(result.appId, function (error, result) {
if (error) return sendErrorPageOrRedirect(req, res, 'Unknown Application for those OAuth credentials');
var applicationName = result.fqdn;
var applicationName = result.altDomain || result.intrinsicFqdn;
render(applicationName, '/api/v1/apps/' + result.id + '/icon');
});
});
@@ -448,7 +447,7 @@ var authorization = [
var type = req.oauth2.client.type;
if (type === clients.TYPE_EXTERNAL || type === clients.TYPE_BUILT_IN) {
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource(req, req.oauth2.client.appId), { userId: req.oauth2.user.id, user: user.removePrivateFields(req.oauth2.user) });
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource(req, req.oauth2.client.appId), { userId: req.oauth2.user.id });
return next();
}
@@ -459,7 +458,7 @@ var authorization = [
if (error) return sendError(req, res, 'Internal error');
if (!access) return sendErrorPageOrRedirect(req, res, 'No access to this app.');
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource(req, appObject.id, appObject), { userId: req.oauth2.user.id, user: user.removePrivateFields(req.oauth2.user) });
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource(req, appObject.id), { userId: req.oauth2.user.id });
next();
});
+6 -34
View File
@@ -1,11 +1,8 @@
'use strict';
exports = module.exports = {
getAppAutoupdatePattern: getAppAutoupdatePattern,
setAppAutoupdatePattern: setAppAutoupdatePattern,
getBoxAutoupdatePattern: getBoxAutoupdatePattern,
setBoxAutoupdatePattern: setBoxAutoupdatePattern,
getAutoupdatePattern: getAutoupdatePattern,
setAutoupdatePattern: setAutoupdatePattern,
getCloudronName: getCloudronName,
setCloudronName: setCloudronName,
@@ -30,41 +27,20 @@ var assert = require('assert'),
settings = require('../settings.js'),
SettingsError = settings.SettingsError;
function getAppAutoupdatePattern(req, res, next) {
settings.getAppAutoupdatePattern(function (error, pattern) {
function getAutoupdatePattern(req, res, next) {
settings.getAutoupdatePattern(function (error, pattern) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { pattern: pattern }));
});
}
function setAppAutoupdatePattern(req, res, next) {
function setAutoupdatePattern(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.pattern !== 'string') return next(new HttpError(400, 'pattern is required'));
settings.setAppAutoupdatePattern(req.body.pattern, function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200));
});
}
function getBoxAutoupdatePattern(req, res, next) {
settings.getBoxAutoupdatePattern(function (error, pattern) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { pattern: pattern }));
});
}
function setBoxAutoupdatePattern(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.pattern !== 'string') return next(new HttpError(400, 'pattern is required'));
settings.setBoxAutoupdatePattern(req.body.pattern, function (error) {
settings.setAutoupdatePattern(req.body.pattern, function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
@@ -153,10 +129,6 @@ function setBackupConfig(req, res, next) {
if (typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider is required'));
if (typeof req.body.retentionSecs !== 'number') return next(new HttpError(400, 'retentionSecs is required'));
if ('key' in req.body && typeof req.body.key !== 'string') return next(new HttpError(400, 'key must be a string'));
if ('syncConcurrency' in req.body) {
if (typeof req.body.syncConcurrency !== 'number') return next(new HttpError(400, 'syncConcurrency must be a positive integer'));
if (req.body.syncConcurrency < 1) return next(new HttpError(400, 'syncConcurrency must be a positive integer'));
}
if (typeof req.body.format !== 'string') return next(new HttpError(400, 'format must be a string'));
if ('acceptSelfSignedCerts' in req.body && typeof req.body.acceptSelfSignedCerts !== 'boolean') return next(new HttpError(400, 'format must be a boolean'));
+76 -47
View File
@@ -9,23 +9,25 @@
var appdb = require('../../appdb.js'),
apps = require('../../apps.js'),
assert = require('assert'),
path = require('path'),
async = require('async'),
child_process = require('child_process'),
clients = require('../../clients.js'),
config = require('../../config.js'),
constants = require('../../constants.js'),
apphealthmonitor = require('../../apphealthmonitor.js'),
database = require('../../database.js'),
docker = require('../../docker.js').connection,
domains = require('../../domains.js'),
expect = require('expect.js'),
fs = require('fs'),
hock = require('hock'),
http = require('http'),
https = require('https'),
js2xml = require('js2xmlparser').parse,
ldap = require('../../ldap.js'),
mail = require('../../mail.js'),
net = require('net'),
nock = require('nock'),
path = require('path'),
paths = require('../../paths.js'),
safe = require('safetydance'),
server = require('../../server.js'),
@@ -41,8 +43,9 @@ var SERVER_URL = 'http://localhost:' + config.get('port');
// Test image information
var TEST_IMAGE_REPO = 'cloudron/test';
var TEST_IMAGE_TAG = '25.4.0';
var TEST_IMAGE_TAG = '25.2.0';
var TEST_IMAGE = TEST_IMAGE_REPO + ':' + TEST_IMAGE_TAG;
// var TEST_IMAGE_ID = child_process.execSync('docker inspect --format={{.Id}} ' + TEST_IMAGE).toString('utf8').trim();
const DOMAIN_0 = {
domain: 'example-apps-test.com',
@@ -57,6 +60,7 @@ 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_LOCATION_NEW = 'appslocationnew';
@@ -66,19 +70,14 @@ APP_MANIFEST.dockerImage = TEST_IMAGE;
var APP_MANIFEST_1 = JSON.parse(fs.readFileSync(__dirname + '/../../../../test-app/CloudronManifest.json', 'utf8'));
APP_MANIFEST_1.dockerImage = TEST_IMAGE;
const USERNAME = 'superadmin';
const PASSWORD = 'Foobar?1337';
const EMAIL ='admin@me.com';
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';
const USERNAME_1 = 'user';
const EMAIL_1 ='user@me.com';
var user_1_id = null;
// authentication token
var token = null;
var token = null; // authentication token
var token_1 = null;
var awsHostedZones;
function startDockerProxy(interceptor, callback) {
assert.strictEqual(typeof interceptor, 'function');
@@ -120,7 +119,6 @@ function checkAddons(appEntry, done) {
delete body.recvmail; // unclear why dovecot mail delivery won't work
delete body.stdenv; // cannot access APP_ORIGIN
delete body.email; // sieve will fail not sure why yet
for (var key in body) {
if (body[key] !== 'OK') return callback('Not done yet: ' + JSON.stringify(body));
@@ -166,6 +164,21 @@ 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'
// };
async.series([
// first clear, then start server. otherwise, taskmanager spins up tasks for obsolete appIds
database.initialize,
@@ -205,7 +218,7 @@ function startBox(done) {
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
user_1_id = res.body.id;
USER_1_ID = res.body.id;
callback(null);
});
@@ -215,7 +228,7 @@ function startBox(done) {
token_1 = tokendb.generateToken();
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
tokendb.add(token_1, user_1_id, 'test-client-id', Date.now() + 1000000, '*', callback);
tokendb.add(token_1, USER_1_ID, 'test-client-id', Date.now() + 100000, '*', callback);
},
function (callback) {
@@ -250,7 +263,6 @@ function stopBox(done) {
// db is not cleaned up here since it's too late to call it after server.stop. if called before server.stop taskmanager apptasks are unhappy :/
async.series([
apphealthmonitor.stop,
taskmanager.stopPendingTasks,
taskmanager.waitForPendingTasks,
appdb._clear,
@@ -444,11 +456,11 @@ describe('App API', function () {
});
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 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, { });
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) {
settings.setAppstoreConfig({ userId: USER_1_ID, token: USER_1_APPSTORE_TOKEN }, function (error) {
if (error) return done(error);
expect(fake1.isDone()).to.be.ok();
@@ -560,8 +572,8 @@ 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()).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, { });
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/uninstall')
.send({ password: PASSWORD })
@@ -576,7 +588,7 @@ describe('App API', function () {
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(function (uri) { return uri.indexOf('/api/v1/users/' + USER_1_ID + '/cloudrons/' + CLOUDRON_ID + '/apps/') >= 0; }, { 'appstoreId': APP_STORE_ID }).reply(201, { });
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
@@ -625,15 +637,17 @@ describe('App API', function () {
});
});
describe('App installation', function () {
xdescribe('App installation', function () {
this.timeout(100000);
var apiHockInstance = hock.createHock({ throwOnUnmatched: false }), apiHockServer;
var awsHockInstance = hock.createHock({ throwOnUnmatched: false }), awsHockServer;
// *.foobar.com
var validCert1, validKey1;
before(function (done) {
child_process.execSync('openssl req -subj "/CN=*.' + DOMAIN_0.domain + '/O=My Company Name LTD./C=US" -new -newkey rsa:2048 -days 365 -nodes -x509 -keyout /tmp/server.key -out /tmp/server.crt');
child_process.execSync('openssl req -subj "/CN=*.foobar.com/O=My Company Name LTD./C=US" -new -newkey rsa:2048 -days 365 -nodes -x509 -keyout /tmp/server.key -out /tmp/server.crt');
validKey1 = fs.readFileSync('/tmp/server.key', 'utf8');
validCert1 = fs.readFileSync('/tmp/server.crt', 'utf8');
@@ -641,37 +655,53 @@ describe('App installation', function () {
async.series([
startBox,
apphealthmonitor.start,
function (callback) {
apiHockInstance
.get('/api/v1/apps/' + APP_STORE_ID + '/versions/' + APP_MANIFEST.version + '/icon')
.replyWithFile(200, path.resolve(__dirname, '../../../assets/avatar.png'));
.replyWithFile(200, path.resolve(__dirname, '../../../webadmin/src/img/appicon_fallback.png'));
var port = parseInt(url.parse(config.apiServerOrigin()).port, 10);
apiHockServer = http.createServer(apiHockInstance.handler).listen(port, callback);
},
settings.setDnsConfig.bind(null, { provider: 'route53', accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey', endpoint: 'http://localhost:5353' }, config.adminDomain(), config.zoneName()),
settings.setTlsConfig.bind(null, { provider: 'caas' }),
function (callback) {
var fake1 = nock(config.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/users/' + user_1_id + '/cloudrons') >= 0; }, { 'domain': DOMAIN_0.domain }).reply(201, { cloudron: { id: CLOUDRON_ID } });
settings.setAppstoreConfig({ userId: user_1_id, token: USER_1_APPSTORE_TOKEN }, function (error) {
if (error) return callback(error);
awsHockInstance
.get('/2013-04-01/hostedzone')
.max(Infinity)
.reply(200, js2xml('ListHostedZonesResponse', awsHostedZones, { wrapHandlers: { HostedZones: () => 'HostedZone'} }), { 'Content-Type': 'application/xml' })
.filteringPathRegEx(/name=[^&]*/, 'name=location')
.get('/2013-04-01/hostedzone/ZONEID/rrset?maxitems=1&name=location&type=A')
.max(Infinity)
.reply(200, js2xml('ListResourceRecordSetsResponse', { ResourceRecordSets: [ ] }, { 'Content-Type': 'application/xml' }))
.filteringRequestBody(function (unusedBody) { return ''; }) // strip out body
.post('/2013-04-01/hostedzone/ZONEID/rrset/')
.max(Infinity)
.reply(200, js2xml('ChangeResourceRecordSetsResponse', { ChangeInfo: { Id: 'dnsrecordid', Status: 'INSYNC' } }), { 'Content-Type': 'application/xml' });
expect(fake1.isDone()).to.be.ok();
callback();
});
awsHockServer = http.createServer(awsHockInstance.handler).listen(5353, callback);
}
], done);
});
after(stopBox);
after(function (done) {
APP_ID = null;
async.series([
apiHockServer.close.bind(apiHockServer),
awsHockServer.close.bind(awsHockServer),
stopBox
], done);
});
var appResult = null, appEntry = null;
it('can install test app', function (done) {
var fake1 = nock(config.apiServerOrigin()).get('/api/v1/apps/' + APP_STORE_ID).reply(200, { manifest: APP_MANIFEST });
var fake2 = nock(config.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/users/' + user_1_id + '/cloudrons/' + CLOUDRON_ID + '/apps/') >= 0; }, { 'appstoreId': APP_STORE_ID }).reply(201, { });
var fake3 = nock(config.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/users/USER_ID/cloudrons/CLOUDRON_ID/apps/') >= 0; }, { 'appstoreId': APP_STORE_ID }).reply(201, { });
var count = 0;
function checkInstallStatus() {
@@ -688,11 +718,10 @@ describe('App installation', function () {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, location: APP_LOCATION, domain: DOMAIN_0.domain, portBindings: { ECHO_SERVER_PORT: 7171 }, accessRestriction: null })
.send({ appStoreId: APP_STORE_ID, location: APP_LOCATION, portBindings: { ECHO_SERVER_PORT: 7171 }, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
expect(fake1.isDone()).to.be.ok();
expect(fake2.isDone()).to.be.ok();
APP_ID = res.body.id;
checkInstallStatus();
});
@@ -721,8 +750,8 @@ describe('App installation', function () {
expect(data.Config.Env).to.contain('WEBADMIN_ORIGIN=' + config.adminOrigin());
expect(data.Config.Env).to.contain('API_ORIGIN=' + config.adminOrigin());
expect(data.Config.Env).to.contain('CLOUDRON=1');
expect(data.Config.Env).to.contain('APP_ORIGIN=https://' + APP_LOCATION + '.' + DOMAIN_0.domain);
expect(data.Config.Env).to.contain('APP_DOMAIN=' + APP_LOCATION + '.' + DOMAIN_0.domain);
expect(data.Config.Env).to.contain('APP_ORIGIN=https://' + APP_LOCATION + '.' + APP_DOMAIN);
expect(data.Config.Env).to.contain('APP_DOMAIN=' + APP_LOCATION + '.' + APP_DOMAIN);
// Hostname must not be set of app fqdn or app location!
expect(data.Config.Hostname).to.not.contain(APP_LOCATION);
expect(data.Config.Env).to.contain('ECHO_SERVER_PORT=7171');
@@ -1100,8 +1129,8 @@ describe('App installation', function () {
});
it('can uninstall app', function (done) {
var fake1 = nock(config.apiServerOrigin()).get(function (uri) { return uri.indexOf('/api/v1/users/' + user_1_id + '/cloudrons/' + CLOUDRON_ID + '/apps/') >= 0; }).reply(200, { });
var fake2 = nock(config.apiServerOrigin()).delete(function (uri) { return uri.indexOf('/api/v1/users/' + user_1_id + '/cloudrons/' + CLOUDRON_ID + '/apps/') >= 0; }).reply(204, { });
var fake2 = nock(config.apiServerOrigin()).get(function (uri) { return uri.indexOf('/api/v1/users/USER_ID/cloudrons/CLOUDRON_ID/apps/') >= 0; }).reply(200, { });
var fake3 = nock(config.apiServerOrigin()).delete(function (uri) { return uri.indexOf('/api/v1/users/USER_ID/cloudrons/CLOUDRON_ID/apps/') >= 0; }).reply(204, { });
var count = 0;
function checkUninstallStatus() {
@@ -1119,10 +1148,6 @@ describe('App installation', function () {
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
expect(fake1.isDone()).to.be.ok();
expect(fake2.isDone()).to.be.ok();
checkUninstallStatus();
});
});
@@ -1148,7 +1173,11 @@ describe('App installation', function () {
it('uninstalled - unregistered subdomain', function (done) {
apiHockInstance.done(function (error) { // checks if all the apiHockServer APIs were called
expect(!error).to.be.ok();
done();
awsHockInstance.done(function (error) {
expect(!error).to.be.ok();
done();
});
});
});
+1 -129
View File
@@ -6,16 +6,11 @@
/* global after:false */
var async = require('async'),
child_process = require('child_process'),
config = require('../../config.js'),
database = require('../../database.js'),
expect = require('expect.js'),
fs = require('fs'),
path = require('path'),
paths = require('../../paths.js'),
superagent = require('superagent'),
server = require('../../server.js'),
_ = require('underscore');
server = require('../../server.js');
var SERVER_URL = 'http://localhost:' + config.get('port');
@@ -133,17 +128,6 @@ describe('Domains API', function () {
});
});
it('fails without token', function (done) {
superagent.post(SERVER_URL + '/api/v1/domains')
.query({ })
.send(DOMAIN_0)
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/domains')
.query({ access_token: token })
@@ -268,116 +252,4 @@ describe('Domains API', function () {
});
});
});
describe('Certificates API', function () {
var validCert0, validKey0, // example.com
validCert1, validKey1; // *.example.com
before(function (done) {
child_process.execSync(`openssl req -subj "/CN=${DOMAIN_0.domain}/O=My Company Name LTD./C=US" -new -newkey rsa:2048 -days 365 -nodes -x509 -keyout /tmp/server.key -out /tmp/server.crt`);
validKey0 = fs.readFileSync('/tmp/server.key', 'utf8');
validCert0 = fs.readFileSync('/tmp/server.crt', 'utf8');
child_process.execSync(`openssl req -subj "/CN=*.${DOMAIN_0.domain}/O=My Company Name LTD./C=US" -new -newkey rsa:2048 -days 365 -nodes -x509 -keyout /tmp/server.key -out /tmp/server.crt`);
validKey1 = fs.readFileSync('/tmp/server.key', 'utf8');
validCert1 = fs.readFileSync('/tmp/server.crt', 'utf8');
superagent.post(SERVER_URL + '/api/v1/domains')
.query({ access_token: token })
.send(DOMAIN_0)
.end(function (error, result) {
expect(result.statusCode).to.equal(201);
done();
});
});
it('cannot set certificate without certificate', function (done) {
var d = _.extend({}, DOMAIN_0);
d.fallbackCertificate = { key: validKey1 };
superagent.put(`${SERVER_URL}/api/v1/domains/${DOMAIN_0.domain}`)
.query({ access_token: token })
.send(d)
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('cannot set certificate without key', function (done) {
var d = _.extend({}, DOMAIN_0);
d.fallbackCertificate = { cert: validCert1 };
superagent.put(`${SERVER_URL}/api/v1/domains/${DOMAIN_0.domain}`)
.query({ access_token: token })
.send(d)
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('cannot set certificate with cert not being a string', function (done) {
var d = _.extend({}, DOMAIN_0);
d.fallbackCertificate = { cert: 1234, key: validKey1 };
superagent.put(`${SERVER_URL}/api/v1/domains/${DOMAIN_0.domain}`)
.query({ access_token: token })
.send(d)
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('cannot set certificate with key not being a string', function (done) {
var d = _.extend({}, DOMAIN_0);
d.fallbackCertificate = { cert: validCert1, key: true };
superagent.put(`${SERVER_URL}/api/v1/domains/${DOMAIN_0.domain}`)
.query({ access_token: token })
.send(d)
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('cannot set non-fallback certificate', function (done) {
var d = _.extend({}, DOMAIN_0);
d.fallbackCertificate = { cert: validCert0, key: validKey0 };
superagent.put(`${SERVER_URL}/api/v1/domains/${DOMAIN_0.domain}`)
.query({ access_token: token })
.send(d)
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('can set fallback certificate', function (done) {
var d = _.extend({}, DOMAIN_0);
d.fallbackCertificate = { cert: validCert1, key: validKey1 };
superagent.put(`${SERVER_URL}/api/v1/domains/${DOMAIN_0.domain}`)
.query({ access_token: token })
.send(d)
.end(function (error, result) {
expect(result.statusCode).to.equal(204);
done();
});
});
it('did set the certificate', function (done) {
var cert = fs.readFileSync(path.join(paths.APP_CERTS_DIR, `${DOMAIN_0.domain}.host.cert`), 'utf-8');
expect(cert).to.eql(validCert1);
var key = fs.readFileSync(path.join(paths.APP_CERTS_DIR, `${DOMAIN_0.domain}.host.key`), 'utf-8');
expect(key).to.eql(validKey1);
done();
});
});
});
+2 -15
View File
@@ -77,8 +77,6 @@ function cleanup(done) {
}
describe('Eventlog API', function () {
this.timeout(10000);
before(setup);
after(cleanup);
@@ -113,7 +111,7 @@ describe('Eventlog API', function () {
});
});
it('succeeds with deprecated action', function (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) {
@@ -124,17 +122,6 @@ describe('Eventlog API', function () {
});
});
it('succeeds with actions', function (done) {
superagent.get(SERVER_URL + '/api/v1/cloudron/eventlog')
.query({ access_token: token, page: 1, per_page: 10, actions: 'cloudron.activate, user.add' })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.eventlogs.length).to.equal(3);
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 })
@@ -148,7 +135,7 @@ describe('Eventlog API', function () {
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, actions: 'cloudron.activate' })
.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);
+128 -143
View File
@@ -26,10 +26,11 @@ const DOMAIN_0 = {
fallbackCertificate: null,
tlsConfig: { provider: 'fallback' }
};
const USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com', MAILBOX_NAME = 'superman';
const LIST_NAME = 'devs';
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
const GROUP_NAME = 'maillistgroup';
var token = null;
var userId = '';
var groupObject = null;
function setup(done) {
config._reset();
@@ -78,7 +79,7 @@ function cleanup(done) {
}
describe('Mail API', function () {
this.timeout(10000);
this.timeout(5000);
before(setup);
after(cleanup);
@@ -197,18 +198,16 @@ describe('Mail API', function () {
this.timeout(10000);
before(function (done) {
var dns = require('../../native-dns.js');
var dig = require('../../dig.js');
// replace dns resolveTxt()
resolve = dns.resolve;
dns.resolve = function (hostname, type, options, callback) {
resolve = dig.resolve;
dig.resolve = function (hostname, type, options, callback) {
expect(hostname).to.be.a('string');
expect(callback).to.be.a('function');
if (!dnsAnswerQueue[hostname] || !(type in dnsAnswerQueue[hostname])) return callback(new Error('no mock answer'));
if (dnsAnswerQueue[hostname][type] === null) return callback(new Error({ code: 'ENODATA'} ));
callback(null, dnsAnswerQueue[hostname][type]);
};
@@ -223,13 +222,13 @@ describe('Mail API', function () {
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
done();
});
});
});
after(function (done) {
var dns = require('../../native-dns.js');
var dig = require('../../dig.js');
dns.resolve = resolve;
dig.resolve = resolve;
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.send({ password: PASSWORD })
@@ -269,20 +268,20 @@ describe('Mail API', function () {
expect(res.body.dns.dkim.domain).to.eql(dkimDomain);
expect(res.body.dns.dkim.type).to.eql('TXT');
expect(res.body.dns.dkim.value).to.eql(null);
expect(res.body.dns.dkim.expected).to.eql('v=DKIM1; t=s; p=' + mail._readDkimPublicKeySync(DOMAIN_0.domain));
expect(res.body.dns.dkim.expected).to.eql('"v=DKIM1; t=s; p=' + mail._readDkimPublicKeySync(DOMAIN_0.domain) + '"');
expect(res.body.dns.dkim.status).to.eql(false);
expect(res.body.dns.spf).to.be.an('object');
expect(res.body.dns.spf.domain).to.eql(spfDomain);
expect(res.body.dns.spf.type).to.eql('TXT');
expect(res.body.dns.spf.value).to.eql(null);
expect(res.body.dns.spf.expected).to.eql('v=spf1 a:' + config.adminFqdn() + ' ~all');
expect(res.body.dns.spf.expected).to.eql('"v=spf1 a:' + config.adminFqdn() + ' ~all"');
expect(res.body.dns.spf.status).to.eql(false);
expect(res.body.dns.dmarc).to.be.an('object');
expect(res.body.dns.dmarc.type).to.eql('TXT');
expect(res.body.dns.dmarc.value).to.eql(null);
expect(res.body.dns.dmarc.expected).to.eql('v=DMARC1; p=reject; pct=100');
expect(res.body.dns.dmarc.expected).to.eql('"v=DMARC1; p=reject; pct=100"');
expect(res.body.dns.dmarc.status).to.eql(false);
expect(res.body.dns.mx).to.be.an('object');
@@ -294,7 +293,7 @@ describe('Mail API', function () {
expect(res.body.dns.ptr).to.be.an('object');
expect(res.body.dns.ptr.type).to.eql('PTR');
// expect(res.body.ptr.value).to.eql(null); this will be anything random
expect(res.body.dns.ptr.expected).to.eql(config.mailFqdn());
expect(res.body.dns.ptr.expected).to.eql(config.mailFqdn() + '.');
expect(res.body.dns.ptr.status).to.eql(false);
done();
@@ -315,17 +314,17 @@ describe('Mail API', function () {
expect(res.statusCode).to.equal(200);
expect(res.body.dns.spf).to.be.an('object');
expect(res.body.dns.spf.expected).to.eql('v=spf1 a:' + config.adminFqdn() + ' ~all');
expect(res.body.dns.spf.expected).to.eql('"v=spf1 a:' + config.adminFqdn() + ' ~all"');
expect(res.body.dns.spf.status).to.eql(false);
expect(res.body.dns.spf.value).to.eql(null);
expect(res.body.dns.dkim).to.be.an('object');
expect(res.body.dns.dkim.expected).to.eql('v=DKIM1; t=s; p=' + mail._readDkimPublicKeySync(DOMAIN_0.domain));
expect(res.body.dns.dkim.expected).to.eql('"v=DKIM1; t=s; p=' + mail._readDkimPublicKeySync(DOMAIN_0.domain) + '"');
expect(res.body.dns.dkim.status).to.eql(false);
expect(res.body.dns.dkim.value).to.eql(null);
expect(res.body.dns.dmarc).to.be.an('object');
expect(res.body.dns.dmarc.expected).to.eql('v=DMARC1; p=reject; pct=100');
expect(res.body.dns.dmarc.expected).to.eql('"v=DMARC1; p=reject; pct=100"');
expect(res.body.dns.dmarc.status).to.eql(false);
expect(res.body.dns.dmarc.value).to.eql(null);
@@ -335,7 +334,7 @@ describe('Mail API', function () {
expect(res.body.dns.mx.value).to.eql(null);
expect(res.body.dns.ptr).to.be.an('object');
expect(res.body.dns.ptr.expected).to.eql(config.mailFqdn());
expect(res.body.dns.ptr.expected).to.eql(config.mailFqdn() + '.');
expect(res.body.dns.ptr.status).to.eql(false);
// expect(res.body.ptr.value).to.eql(null); this will be anything random
@@ -346,10 +345,10 @@ describe('Mail API', function () {
it('succeeds with all different spf, dkim, dmarc, mx, ptr records', function (done) {
clearDnsAnswerQueue();
dnsAnswerQueue[mxDomain].MX = [ { priority: '20', exchange: config.mailFqdn() }, { priority: '30', exchange: config.mailFqdn() } ];
dnsAnswerQueue[dmarcDomain].TXT = [['v=DMARC2; p=reject; pct=100']];
dnsAnswerQueue[dkimDomain].TXT = [['v=DKIM2; t=s; p=' + mail._readDkimPublicKeySync(DOMAIN_0.domain)]];
dnsAnswerQueue[spfDomain].TXT = [['v=spf1 a:random.com ~all']];
dnsAnswerQueue[mxDomain].MX = [ { priority: '20', exchange: config.mailFqdn() + '.' }, { priority: '30', exchange: config.mailFqdn() + '.'} ];
dnsAnswerQueue[dmarcDomain].TXT = ['"v=DMARC2; p=reject; pct=100"'];
dnsAnswerQueue[dkimDomain].TXT = ['"v=DKIM2; t=s; p=' + mail._readDkimPublicKeySync(DOMAIN_0.domain) + '"'];
dnsAnswerQueue[spfDomain].TXT = ['"v=spf1 a:random.com ~all"'];
superagent.get(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/status')
.query({ access_token: token })
@@ -357,19 +356,19 @@ describe('Mail API', function () {
expect(res.statusCode).to.equal(200);
expect(res.body.dns.spf).to.be.an('object');
expect(res.body.dns.spf.expected).to.eql('v=spf1 a:' + config.adminFqdn() + ' a:random.com ~all');
expect(res.body.dns.spf.expected).to.eql('"v=spf1 a:' + config.adminFqdn() + ' a:random.com ~all"');
expect(res.body.dns.spf.status).to.eql(false);
expect(res.body.dns.spf.value).to.eql('v=spf1 a:random.com ~all');
expect(res.body.dns.spf.value).to.eql('"v=spf1 a:random.com ~all"');
expect(res.body.dns.dkim).to.be.an('object');
expect(res.body.dns.dkim.expected).to.eql('v=DKIM1; t=s; p=' + mail._readDkimPublicKeySync(DOMAIN_0.domain));
expect(res.body.dns.dkim.expected).to.eql('"v=DKIM1; t=s; p=' + mail._readDkimPublicKeySync(DOMAIN_0.domain) + '"');
expect(res.body.dns.dkim.status).to.eql(false);
expect(res.body.dns.dkim.value).to.eql('v=DKIM2; t=s; p=' + mail._readDkimPublicKeySync(DOMAIN_0.domain));
expect(res.body.dns.dkim.value).to.eql('"v=DKIM2; t=s; p=' + mail._readDkimPublicKeySync(DOMAIN_0.domain) + '"');
expect(res.body.dns.dmarc).to.be.an('object');
expect(res.body.dns.dmarc.expected).to.eql('v=DMARC1; p=reject; pct=100');
expect(res.body.dns.dmarc.expected).to.eql('"v=DMARC1; p=reject; pct=100"');
expect(res.body.dns.dmarc.status).to.eql(false);
expect(res.body.dns.dmarc.value).to.eql('v=DMARC2; p=reject; pct=100');
expect(res.body.dns.dmarc.value).to.eql('"v=DMARC2; p=reject; pct=100"');
expect(res.body.dns.mx).to.be.an('object');
expect(res.body.dns.mx.status).to.eql(false);
@@ -377,7 +376,7 @@ describe('Mail API', function () {
expect(res.body.dns.mx.value).to.eql('20 ' + config.mailFqdn() + '. 30 ' + config.mailFqdn() + '.');
expect(res.body.dns.ptr).to.be.an('object');
expect(res.body.dns.ptr.expected).to.eql(config.mailFqdn());
expect(res.body.dns.ptr.expected).to.eql(config.mailFqdn() + '.');
expect(res.body.dns.ptr.status).to.eql(false);
// expect(res.body.ptr.value).to.eql(null); this will be anything random
@@ -390,7 +389,7 @@ describe('Mail API', function () {
it('succeeds with existing embedded spf', function (done) {
clearDnsAnswerQueue();
dnsAnswerQueue[spfDomain].TXT = [['v=spf1 a:example.com a:' + config.mailFqdn() + ' ~all']];
dnsAnswerQueue[spfDomain].TXT = ['"v=spf1 a:example.com a:' + config.mailFqdn() + ' ~all"'];
superagent.get(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/status')
.query({ access_token: token })
@@ -400,8 +399,8 @@ describe('Mail API', function () {
expect(res.body.dns.spf).to.be.an('object');
expect(res.body.dns.spf.domain).to.eql(spfDomain);
expect(res.body.dns.spf.type).to.eql('TXT');
expect(res.body.dns.spf.value).to.eql('v=spf1 a:example.com a:' + config.mailFqdn() + ' ~all');
expect(res.body.dns.spf.expected).to.eql('v=spf1 a:example.com a:' + config.mailFqdn() + ' ~all');
expect(res.body.dns.spf.value).to.eql('"v=spf1 a:example.com a:' + config.mailFqdn() + ' ~all"');
expect(res.body.dns.spf.expected).to.eql('"v=spf1 a:example.com a:' + config.mailFqdn() + ' ~all"');
expect(res.body.dns.spf.status).to.eql(true);
done();
@@ -411,10 +410,10 @@ describe('Mail API', function () {
it('succeeds with all correct records', function (done) {
clearDnsAnswerQueue();
dnsAnswerQueue[mxDomain].MX = [ { priority: '10', exchange: config.mailFqdn() } ];
dnsAnswerQueue[dmarcDomain].TXT = [['v=DMARC1; p=reject; pct=100']];
dnsAnswerQueue[dkimDomain].TXT = [['v=DKIM1; t=s; p=', mail._readDkimPublicKeySync(DOMAIN_0.domain) ]];
dnsAnswerQueue[spfDomain].TXT = [['v=spf1 a:' + config.adminFqdn() + ' ~all']];
dnsAnswerQueue[mxDomain].MX = [ { priority: '10', exchange: config.mailFqdn() + '.' } ];
dnsAnswerQueue[dmarcDomain].TXT = ['"v=DMARC1; p=reject; pct=100"'];
dnsAnswerQueue[dkimDomain].TXT = ['"v=DKIM1; t=s; p=' + mail._readDkimPublicKeySync(DOMAIN_0.domain) + '"'];
dnsAnswerQueue[spfDomain].TXT = ['"v=spf1 a:' + config.adminFqdn() + ' ~all"'];
superagent.get(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/status')
.query({ access_token: token })
@@ -424,21 +423,21 @@ describe('Mail API', function () {
expect(res.body.dns.dkim).to.be.an('object');
expect(res.body.dns.dkim.domain).to.eql(dkimDomain);
expect(res.body.dns.dkim.type).to.eql('TXT');
expect(res.body.dns.dkim.value).to.eql('v=DKIM1; t=s; p=' + mail._readDkimPublicKeySync(DOMAIN_0.domain));
expect(res.body.dns.dkim.expected).to.eql('v=DKIM1; t=s; p=' + mail._readDkimPublicKeySync(DOMAIN_0.domain));
expect(res.body.dns.dkim.value).to.eql('"v=DKIM1; t=s; p=' + mail._readDkimPublicKeySync(DOMAIN_0.domain) + '"');
expect(res.body.dns.dkim.expected).to.eql('"v=DKIM1; t=s; p=' + mail._readDkimPublicKeySync(DOMAIN_0.domain) + '"');
expect(res.body.dns.dkim.status).to.eql(true);
expect(res.body.dns.spf).to.be.an('object');
expect(res.body.dns.spf.domain).to.eql(spfDomain);
expect(res.body.dns.spf.type).to.eql('TXT');
expect(res.body.dns.spf.value).to.eql('v=spf1 a:' + config.adminFqdn() + ' ~all');
expect(res.body.dns.spf.expected).to.eql('v=spf1 a:' + config.adminFqdn() + ' ~all');
expect(res.body.dns.spf.value).to.eql('"v=spf1 a:' + config.adminFqdn() + ' ~all"');
expect(res.body.dns.spf.expected).to.eql('"v=spf1 a:' + config.adminFqdn() + ' ~all"');
expect(res.body.dns.spf.status).to.eql(true);
expect(res.body.dns.dmarc).to.be.an('object');
expect(res.body.dns.dmarc.expected).to.eql('v=DMARC1; p=reject; pct=100');
expect(res.body.dns.dmarc.expected).to.eql('"v=DMARC1; p=reject; pct=100"');
expect(res.body.dns.dmarc.status).to.eql(true);
expect(res.body.dns.dmarc.value).to.eql('v=DMARC1; p=reject; pct=100');
expect(res.body.dns.dmarc.value).to.eql('"v=DMARC1; p=reject; pct=100"');
expect(res.body.dns.mx).to.be.an('object');
expect(res.body.dns.mx.status).to.eql(true);
@@ -458,7 +457,7 @@ describe('Mail API', function () {
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
done();
});
});
});
after(function (done) {
@@ -510,7 +509,7 @@ describe('Mail API', function () {
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
done();
});
});
});
after(function (done) {
@@ -533,7 +532,7 @@ describe('Mail API', function () {
});
});
it('cannot set without addresses field', function (done) {
it('cannot set without address field', function (done) {
superagent.post(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/catch_all')
.query({ access_token: token })
.end(function (err, res) {
@@ -542,10 +541,10 @@ describe('Mail API', function () {
});
});
it('cannot set with bad addresses field', function (done) {
it('cannot set with bad address field', function (done) {
superagent.post(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/catch_all')
.query({ access_token: token })
.send({ addresses: [ 'user1', 123 ] })
.send({ address: [ 'user1', 123 ] })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
@@ -555,7 +554,7 @@ describe('Mail API', function () {
it('set succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/catch_all')
.query({ access_token: token })
.send({ addresses: [ 'user1' ] })
.send({ address: [ 'user1' ] })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
done();
@@ -581,7 +580,7 @@ describe('Mail API', function () {
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
done();
});
});
});
after(function (done) {
@@ -659,7 +658,7 @@ describe('Mail API', function () {
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
done();
});
});
});
after(function (done) {
@@ -672,9 +671,17 @@ describe('Mail API', function () {
});
});
it('add succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/mailboxes')
.send({ name: MAILBOX_NAME, userId: userId })
it('add fails if user does not exist', function (done) {
superagent.post(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/mailboxes/' + 'someuserdoesnotexist')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
done();
});
});
it('add/enable succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/mailboxes/' + userId)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
@@ -682,12 +689,11 @@ describe('Mail API', function () {
});
});
it('cannot add again', function (done) {
superagent.post(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/mailboxes')
.send({ name: MAILBOX_NAME, userId: userId })
it('enable again succeeds if already enabled', function (done) {
superagent.post(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/mailboxes/' + userId)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(409);
expect(res.statusCode).to.equal(201);
done();
});
});
@@ -702,12 +708,12 @@ describe('Mail API', function () {
});
it('get succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/mailboxes/' + MAILBOX_NAME)
superagent.get(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/mailboxes/' + userId)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.mailbox).to.be.an('object');
expect(res.body.mailbox.name).to.equal(MAILBOX_NAME);
expect(res.body.mailbox.name).to.equal(USERNAME);
expect(res.body.mailbox.ownerId).to.equal(userId);
expect(res.body.mailbox.ownerType).to.equal('user');
expect(res.body.mailbox.aliasTarget).to.equal(null);
@@ -723,7 +729,7 @@ describe('Mail API', function () {
expect(res.statusCode).to.equal(200);
expect(res.body.mailboxes.length).to.eql(1);
expect(res.body.mailboxes[0]).to.be.an('object');
expect(res.body.mailboxes[0].name).to.equal(MAILBOX_NAME);
expect(res.body.mailboxes[0].name).to.equal(USERNAME);
expect(res.body.mailboxes[0].ownerId).to.equal(userId);
expect(res.body.mailboxes[0].ownerType).to.equal('user');
expect(res.body.mailboxes[0].aliasTarget).to.equal(null);
@@ -732,21 +738,21 @@ describe('Mail API', function () {
});
});
it('disable fails even if not exist', function (done) {
it('disable succeeds even if not exist', function (done) {
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/mailboxes/' + 'someuserdoesnotexist')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
expect(res.statusCode).to.equal(201);
done();
});
});
it('disable succeeds', function (done) {
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/mailboxes/' + MAILBOX_NAME)
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/mailboxes/' + userId)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
superagent.get(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/mailboxes/' + MAILBOX_NAME)
superagent.get(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/mailboxes/' + userId)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
@@ -764,25 +770,21 @@ describe('Mail API', function () {
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
done();
});
});
after(function (done) {
mail.removeMailboxes(DOMAIN_0.domain, function (error) {
if (error) return done(error);
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
done();
});
});
});
after(function (done) {
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
done();
});
});
it('set fails if aliases is missing', function (done) {
superagent.put(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/aliases/' + MAILBOX_NAME)
superagent.put(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/aliases/' + userId)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
@@ -801,7 +803,7 @@ describe('Mail API', function () {
});
it('set fails if aliases is the wrong type', function (done) {
superagent.put(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/aliases/' + MAILBOX_NAME)
superagent.put(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/aliases/' + userId)
.send({ aliases: 'hello, there' })
.query({ access_token: token })
.end(function (err, res) {
@@ -811,7 +813,7 @@ describe('Mail API', function () {
});
it('set fails if user is not enabled', function (done) {
superagent.put(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/aliases/' + MAILBOX_NAME)
superagent.put(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/aliases/' + userId)
.send({ aliases: ['hello', 'there'] })
.query({ access_token: token })
.end(function (err, res) {
@@ -820,9 +822,8 @@ describe('Mail API', function () {
});
});
it('now add the mailbox', function (done) {
superagent.post(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/mailboxes')
.send({ name: MAILBOX_NAME, userId: userId })
it('now enable the user', function (done) {
superagent.post(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/mailboxes/' + userId)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
@@ -831,7 +832,7 @@ describe('Mail API', function () {
});
it('set succeeds', function (done) {
superagent.put(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/aliases/' + MAILBOX_NAME)
superagent.put(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/aliases/' + userId)
.send({ aliases: ['hello', 'there'] })
.query({ access_token: token })
.end(function (err, res) {
@@ -841,7 +842,7 @@ describe('Mail API', function () {
});
it('get succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/aliases/' + MAILBOX_NAME)
superagent.get(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/aliases/' + userId)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
@@ -850,27 +851,7 @@ describe('Mail API', function () {
});
});
it('listing succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/aliases')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.aliases.length).to.eql(2);
expect(res.body.aliases[0].name).to.equal('hello');
expect(res.body.aliases[0].ownerId).to.equal(userId);
expect(res.body.aliases[0].ownerType).to.equal('user');
expect(res.body.aliases[0].aliasTarget).to.equal(MAILBOX_NAME);
expect(res.body.aliases[0].domain).to.equal(DOMAIN_0.domain);
expect(res.body.aliases[1].name).to.equal('there');
expect(res.body.aliases[1].ownerId).to.equal(userId);
expect(res.body.aliases[1].ownerType).to.equal('user');
expect(res.body.aliases[1].aliasTarget).to.equal(MAILBOX_NAME);
expect(res.body.aliases[1].domain).to.equal(DOMAIN_0.domain);
done();
});
});
it('get fails if mailbox does not exist', function (done) {
it('get succeeds if mailbox does not exist', function (done) {
superagent.get(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/aliases/' + 'someuserdoesnotexist')
.query({ access_token: token })
.end(function (err, res) {
@@ -891,22 +872,37 @@ describe('Mail API', function () {
expect(res.statusCode).to.equal(201);
done();
});
},
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();
});
},
function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + userId + '/groups')
.query({ access_token: token })
.send({ groupIds: [ 'admin', groupObject.id ]})
.end(function (error, result) {
expect(result.statusCode).to.equal(204);
done();
});
}
], done);
});
after(function (done) {
mail.removeMailboxes(DOMAIN_0.domain, function (error) {
if (error) return done(error);
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
done();
});
});
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
done();
});
});
it('add fails without groupId', function (done) {
@@ -928,29 +924,19 @@ describe('Mail API', function () {
});
});
it('add fails without members array', function (done) {
it('add fails with non-existing groupId', function (done) {
superagent.post(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/lists')
.send({ name: LIST_NAME })
.send({ groupId: 'doesnotexist' })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('cannot add reserved group', function (done) {
superagent.post(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/lists')
.send({ name: LIST_NAME, members: [ 'Admin', USERNAME ]})
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.statusCode).to.equal(404);
done();
});
});
it('add succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/lists')
.send({ name: LIST_NAME, members: [ 'admin2', USERNAME ]})
.send({ groupId: groupObject.id })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
@@ -960,7 +946,7 @@ describe('Mail API', function () {
it('add twice fails', function (done) {
superagent.post(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/lists')
.send({ name: LIST_NAME, members: [ 'admin2', USERNAME ] })
.send({ groupId: groupObject.id })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(409);
@@ -978,17 +964,17 @@ describe('Mail API', function () {
});
it('get succeeds', function (done) {
superagent.get(SERVER_URL + `/api/v1/mail/${DOMAIN_0.domain}/lists/${LIST_NAME}`)
superagent.get(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/lists/' + groupObject.id)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.list).to.be.an('object');
expect(res.body.list.name).to.equal(LIST_NAME);
expect(res.body.list.ownerId).to.equal('admin');
expect(res.body.list.name).to.equal(GROUP_NAME);
expect(res.body.list.ownerId).to.equal(groupObject.id);
expect(res.body.list.ownerType).to.equal('group');
expect(res.body.list.aliasTarget).to.equal(null);
expect(res.body.list.domain).to.equal(DOMAIN_0.domain);
expect(res.body.list.members).to.eql([ 'admin2', 'superadmin' ]);
expect(res.body.list.members).to.eql([ 'superadmin' ]);
done();
});
});
@@ -1000,12 +986,11 @@ describe('Mail API', function () {
expect(res.statusCode).to.equal(200);
expect(res.body.lists).to.be.an(Array);
expect(res.body.lists.length).to.equal(1);
expect(res.body.lists[0].name).to.equal(LIST_NAME);
expect(res.body.lists[0].ownerId).to.equal('admin');
expect(res.body.lists[0].name).to.equal(GROUP_NAME);
expect(res.body.lists[0].ownerId).to.equal(groupObject.id);
expect(res.body.lists[0].ownerType).to.equal('group');
expect(res.body.lists[0].aliasTarget).to.equal(null);
expect(res.body.lists[0].domain).to.equal(DOMAIN_0.domain);
expect(res.body.lists[0].members).to.eql([ 'admin2', 'superadmin' ]);
done();
});
});
@@ -1020,12 +1005,12 @@ describe('Mail API', function () {
});
it('del succeeds', function (done) {
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/lists/' + LIST_NAME)
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/lists/' + groupObject.id)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
superagent.get(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/lists/' + LIST_NAME)
superagent.get(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/lists/' + groupObject.id)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
+8 -4
View File
@@ -167,7 +167,8 @@ describe('OAuth2', function () {
domain: DOMAIN_0.domain,
portBindings: {},
accessRestriction: null,
memoryLimit: 0
memoryLimit: 0,
altDomain: null
};
var APP_1 = {
@@ -178,7 +179,8 @@ describe('OAuth2', function () {
domain: DOMAIN_0.domain,
portBindings: {},
accessRestriction: { users: [ 'foobar' ] },
memoryLimit: 0
memoryLimit: 0,
altDomain: null
};
var APP_2 = {
@@ -189,7 +191,8 @@ describe('OAuth2', function () {
domain: DOMAIN_0.domain,
portBindings: {},
accessRestriction: { users: [ USER_0.id ] },
memoryLimit: 0
memoryLimit: 0,
altDomain: null
};
var APP_3 = {
@@ -200,7 +203,8 @@ describe('OAuth2', function () {
domain: DOMAIN_0.domain,
portBindings: {},
accessRestriction: { groups: [ 'someothergroup', 'admin', 'anothergroup' ] },
memoryLimit: 0
memoryLimit: 0,
altDomain: null
};
// unknown app
+25 -238
View File
@@ -14,262 +14,49 @@ var async = require('async'),
server = require('../../server.js');
var SERVER_URL = 'http://localhost:' + config.get('port');
var DOMAIN = 'example-server-test.com';
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var token = null;
function setup(done) {
config._reset();
config.setFqdn('example-server-test.com');
config.setVersion('1.2.3');
async.series([
server.start,
database._clear
server.start.bind(server),
database._clear,
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);
// stash token for further use
token = result.body.token;
callback();
});
}
], done);
}
function cleanup(done) {
async.series([
database._clear,
server.stop
], done);
database._clear(function (error) {
expect(!error).to.be.ok();
server.stop(done);
});
}
describe('REST API', function () {
before(setup);
after(cleanup);
it('dns setup fails without provider', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/dns_setup')
.send({ domain: DOMAIN, adminFqdn: 'my.' + DOMAIN, config: {} })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
done();
});
});
it('dns setup fails with invalid provider', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/dns_setup')
.send({ provider: 'foobar', domain: DOMAIN, adminFqdn: 'my.' + DOMAIN, config: {} })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
done();
});
});
it('dns setup fails with missing domain', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/dns_setup')
.send({ provider: 'noop', adminFqdn: 'my.' + DOMAIN, config: {} })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
done();
});
});
it('dns setup fails with invalid domain', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/dns_setup')
.send({ provider: 'noop', domain: '.foo', adminFqdn: 'my.' + DOMAIN, config: {} })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
done();
});
});
it('dns setup fails with missing adminFqdn', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/dns_setup')
.send({ provider: 'noop', domain: DOMAIN, config: {} })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
done();
});
});
it('dns setup fails with invalid adminFqdn', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/dns_setup')
.send({ provider: 'noop', domain: DOMAIN, adminFqdn: 'my', config: {} })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
done();
});
});
it('dns setup fails with invalid config', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/dns_setup')
.send({ provider: 'noop', domain: DOMAIN, adminFqdn: 'my' + DOMAIN, config: 'not an object' })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
done();
});
});
it('dns setup fails with invalid zoneName', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/dns_setup')
.send({ provider: 'noop', domain: DOMAIN, adminFqdn: 'my' + DOMAIN, config: {}, zoneName: 1337 })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
done();
});
});
it('dns setup fails with invalid tlsConfig', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/dns_setup')
.send({ provider: 'noop', domain: DOMAIN, adminFqdn: 'my' + DOMAIN, config: {}, tlsConfig: 'foobar' })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
done();
});
});
it('dns setup fails with invalid tlsConfig provider', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/dns_setup')
.send({ provider: 'noop', domain: DOMAIN, adminFqdn: 'my' + DOMAIN, config: {}, tlsConfig: { provider: 1337 } })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
done();
});
});
it('dns setup succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/dns_setup')
.send({ provider: 'noop', domain: DOMAIN, adminFqdn: 'my.' + DOMAIN, config: {} })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(200);
done();
});
});
it('dns setup twice fails', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/dns_setup')
.send({ provider: 'noop', domain: DOMAIN, adminFqdn: 'my.' + DOMAIN, config: {} })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(409);
done();
});
});
it('activation fails without username', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
done();
});
});
it('activation fails with invalid username', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: '?this.is-not!valid', password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
done();
});
});
it('activation fails without email', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
done();
});
});
it('activation fails with invalid email', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: 'notanemail' })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
done();
});
});
it('activation fails without password', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
done();
});
});
it('activation fails with invalid password', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: 'short', email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(400);
done();
});
});
it('activation succeeds', function (done) {
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);
// stash token for further use
token = result.body.token;
done();
});
});
it('activating twice fails', function (done) {
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(409);
done();
});
});
it('does not crash with invalid JSON', function (done) {
superagent.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
+109 -76
View File
@@ -2,16 +2,19 @@
/* global it:false */
/* global describe:false */
/* global xdescribe:false */
/* global before:false */
/* global after:false */
var async = require('async'),
child_process = require('child_process'),
config = require('../../config.js'),
constants = require('../../constants.js'),
database = require('../../database.js'),
expect = require('expect.js'),
fs = require('fs'),
nock = require('nock'),
path = require('path'),
paths = require('../../paths.js'),
server = require('../../server.js'),
settings = require('../../settings.js'),
@@ -60,9 +63,9 @@ describe('Settings API', function () {
before(setup);
after(cleanup);
describe('app_autoupdate_pattern', function () {
it('can get app auto update pattern (default)', function (done) {
superagent.get(SERVER_URL + '/api/v1/settings/app_autoupdate_pattern')
describe('autoupdate_pattern', function () {
it('can get auto update pattern (default)', function (done) {
superagent.get(SERVER_URL + '/api/v1/settings/autoupdate_pattern')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
@@ -71,8 +74,8 @@ describe('Settings API', function () {
});
});
it('cannot set app_autoupdate_pattern without pattern', function (done) {
superagent.post(SERVER_URL + '/api/v1/settings/app_autoupdate_pattern')
it('cannot set autoupdate_pattern without pattern', function (done) {
superagent.post(SERVER_URL + '/api/v1/settings/autoupdate_pattern')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
@@ -80,13 +83,13 @@ describe('Settings API', function () {
});
});
it('can set app_autoupdate_pattern', function (done) {
it('can set autoupdate_pattern', function (done) {
var eventPattern = null;
settings.events.on(settings.APP_AUTOUPDATE_PATTERN_KEY, function (pattern) {
settings.events.on(settings.AUTOUPDATE_PATTERN_KEY, function (pattern) {
eventPattern = pattern;
});
superagent.post(SERVER_URL + '/api/v1/settings/app_autoupdate_pattern')
superagent.post(SERVER_URL + '/api/v1/settings/autoupdate_pattern')
.query({ access_token: token })
.send({ pattern: '00 30 11 * * 1-5' })
.end(function (err, res) {
@@ -96,13 +99,13 @@ describe('Settings API', function () {
});
});
it('can set app_autoupdate_pattern to never', function (done) {
it('can set autoupdate_pattern to never', function (done) {
var eventPattern = null;
settings.events.on(settings.APP_AUTOUPDATE_PATTERN_KEY, function (pattern) {
settings.events.on(settings.AUTOUPDATE_PATTERN_KEY, function (pattern) {
eventPattern = pattern;
});
superagent.post(SERVER_URL + '/api/v1/settings/app_autoupdate_pattern')
superagent.post(SERVER_URL + '/api/v1/settings/autoupdate_pattern')
.query({ access_token: token })
.send({ pattern: constants.AUTOUPDATE_PATTERN_NEVER })
.end(function (err, res) {
@@ -112,71 +115,8 @@ describe('Settings API', function () {
});
});
it('cannot set invalid app_autoupdate_pattern', function (done) {
superagent.post(SERVER_URL + '/api/v1/settings/app_autoupdate_pattern')
.query({ access_token: token })
.send({ pattern: '1 3 x 5 6' })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
});
describe('box_autoupdate_pattern', function () {
it('can get app auto update pattern (default)', function (done) {
superagent.get(SERVER_URL + '/api/v1/settings/box_autoupdate_pattern')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.pattern).to.be.ok();
done();
});
});
it('cannot set box_autoupdate_pattern without pattern', function (done) {
superagent.post(SERVER_URL + '/api/v1/settings/box_autoupdate_pattern')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('can set box_autoupdate_pattern', function (done) {
var eventPattern = null;
settings.events.on(settings.BOX_AUTOUPDATE_PATTERN_KEY, function (pattern) {
eventPattern = pattern;
});
superagent.post(SERVER_URL + '/api/v1/settings/box_autoupdate_pattern')
.query({ access_token: token })
.send({ pattern: '00 30 11 * * 1-5' })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(eventPattern === '00 30 11 * * 1-5').to.be.ok();
done();
});
});
it('can set box_autoupdate_pattern to never', function (done) {
var eventPattern = null;
settings.events.on(settings.BOX_AUTOUPDATE_PATTERN_KEY, function (pattern) {
eventPattern = pattern;
});
superagent.post(SERVER_URL + '/api/v1/settings/box_autoupdate_pattern')
.query({ access_token: token })
.send({ pattern: constants.AUTOUPDATE_PATTERN_NEVER })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(eventPattern).to.eql(constants.AUTOUPDATE_PATTERN_NEVER);
done();
});
});
it('cannot set invalid box_autoupdate_pattern', function (done) {
superagent.post(SERVER_URL + '/api/v1/settings/box_autoupdate_pattern')
it('cannot set invalid autoupdate_pattern', function (done) {
superagent.post(SERVER_URL + '/api/v1/settings/autoupdate_pattern')
.query({ access_token: token })
.send({ pattern: '1 3 x 5 6' })
.end(function (err, res) {
@@ -280,6 +220,99 @@ describe('Settings API', function () {
});
});
xdescribe('Certificates API', function () {
var validCert0, validKey0, // example.com
validCert1, validKey1; // *.example.com
before(function () {
child_process.execSync('openssl req -subj "/CN=example.com/O=My Company Name LTD./C=US" -new -newkey rsa:2048 -days 365 -nodes -x509 -keyout /tmp/server.key -out /tmp/server.crt');
validKey0 = fs.readFileSync('/tmp/server.key', 'utf8');
validCert0 = fs.readFileSync('/tmp/server.crt', 'utf8');
child_process.execSync('openssl req -subj "/CN=*.example.com/O=My Company Name LTD./C=US" -new -newkey rsa:2048 -days 365 -nodes -x509 -keyout /tmp/server.key -out /tmp/server.crt');
validKey1 = fs.readFileSync('/tmp/server.key', 'utf8');
validCert1 = fs.readFileSync('/tmp/server.crt', 'utf8');
});
it('cannot set certificate without token', function (done) {
superagent.post(SERVER_URL + '/api/v1/settings/certificate')
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('cannot set certificate without certificate', function (done) {
superagent.post(SERVER_URL + '/api/v1/settings/certificate')
.query({ access_token: token })
.send({ key: validKey1 })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('cannot set certificate without key', function (done) {
superagent.post(SERVER_URL + '/api/v1/settings/certificate')
.query({ access_token: token })
.send({ cert: validCert1 })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('cannot set certificate with cert not being a string', function (done) {
superagent.post(SERVER_URL + '/api/v1/settings/certificate')
.query({ access_token: token })
.send({ cert: 1234, key: validKey1 })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('cannot set certificate with key not being a string', function (done) {
superagent.post(SERVER_URL + '/api/v1/settings/certificate')
.query({ access_token: token })
.send({ cert: validCert1, key: true })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('cannot set non wildcard certificate', function (done) {
superagent.post(SERVER_URL + '/api/v1/settings/certificate')
.query({ access_token: token })
.send({ cert: validCert0, key: validKey0 })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('can set certificate', function (done) {
superagent.post(SERVER_URL + '/api/v1/settings/certificate')
.query({ access_token: token })
.send({ cert: validCert1, key: validKey1 })
.end(function (error, result) {
expect(result.statusCode).to.equal(202);
done();
});
});
it('did set the certificate', function (done) {
var cert = fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.cert'), 'utf-8');
expect(cert).to.eql(validCert1);
var key = fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.key'), 'utf-8');
expect(key).to.eql(validKey1);
done();
});
});
describe('time_zone', function () {
it('succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/settings/time_zone')
+1 -1
View File
@@ -44,7 +44,7 @@ function setup(done) {
database._clear,
mailer._clearMailQueue,
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0.zoneName, DOMAIN_0.provider, DOMAIN_0.config, DOMAIN_0.fallbackCertificate, DOMAIN_0.tlsConfig),
mail.addDomain.bind(null, DOMAIN_0.domain)
mail.add.bind(null, DOMAIN_0.domain)
], function (error) {
expect(error).to.not.be.ok();
+14 -3
View File
@@ -20,7 +20,8 @@ var assert = require('assert'),
HttpSuccess = require('connect-lastmile').HttpSuccess,
oauth2 = require('./oauth2.js'),
user = require('../user.js'),
UserError = user.UserError;
UserError = user.UserError,
_ = require('underscore');
function auditSource(req) {
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
@@ -87,7 +88,9 @@ function list(req, res, next) {
user.list(function (error, results) {
if (error) return next(new HttpError(500, error));
var users = results.map(user.removePrivateFields);
var users = results.map(function (result) {
return _.pick(result, 'id', 'username', 'email', 'fallbackEmail', 'displayName', 'groupIds', 'admin');
});
next(new HttpSuccess(200, { users: users }));
});
@@ -103,7 +106,15 @@ function get(req, res, next) {
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, user.removePrivateFields(result)));
next(new HttpSuccess(200, {
id: result.id,
username: result.username,
displayName: result.displayName,
email: result.email,
fallbackEmail: result.fallbackEmail,
admin: result.admin,
groupIds: result.groupIds
}));
});
}
+27 -44
View File
@@ -24,7 +24,7 @@ function sync(callback) {
callback = callback || NOOP_CALLBACK;
debug('sync: synchronizing global state with installed app state');
debug('Syncing');
apps.getAll(function (error, allApps) {
if (error) return callback(error);
@@ -36,10 +36,11 @@ function sync(callback) {
async.eachSeries(removedAppIds, function (appId, iteratorDone) {
stopJobs(appId, gState[appId], iteratorDone);
}, function (error) {
if (error) debug('sync: error stopping jobs of removed apps', error);
if (error) debug('Error stopping jobs of removed apps', error);
gState = _.omit(gState, removedAppIds);
debug('sync: checking apps %j', allAppIds);
async.eachSeries(allApps, function (app, iteratorDone) {
var appState = gState[app.id] || null;
var schedulerConfig = app.manifest.addons ? app.manifest.addons.scheduler : null;
@@ -50,10 +51,9 @@ function sync(callback) {
return iteratorDone(); // nothing changed
}
debug(`sync: app ${app.fqdn} changed`);
debug('sync: app %s changed', app.id);
stopJobs(app.id, appState, function (error) {
if (error) debug(`sync: error stopping jobs of ${app.fqdn} : ${error.message}`);
if (error) debug('Error stopping jobs for %s : %s', app.id, error.message);
if (!schedulerConfig) {
delete gState[app.id];
@@ -62,21 +62,20 @@ function sync(callback) {
gState[app.id] = {
schedulerConfig: schedulerConfig,
cronJobs: createCronJobs(app, schedulerConfig)
cronJobs: createCronJobs(app.id, schedulerConfig)
};
iteratorDone();
});
});
debug('sync: done');
debug('Done syncing');
});
});
}
function killContainer(containerName, callback) {
assert.strictEqual(typeof containerName, 'string');
assert.strictEqual(typeof callback, 'function');
if (!containerName) return callback();
async.series([
docker.stopContainerByName.bind(null, containerName),
@@ -93,7 +92,7 @@ function stopJobs(appId, appState, callback) {
assert.strictEqual(typeof appState, 'object');
assert.strictEqual(typeof callback, 'function');
debug(`stopJobs: stopping jobs of ${appId}`);
debug('stopJobs for %s', appId);
if (!appState) return callback();
@@ -102,30 +101,29 @@ function stopJobs(appId, appState, callback) {
appState.cronJobs[taskName].stop();
}
killContainer(`${appId}-${taskName}`, iteratorDone);
var containerName = appId + '-' + taskName;
killContainer(containerName, iteratorDone);
}, callback);
}
function createCronJobs(app, schedulerConfig) {
assert.strictEqual(typeof app, 'object');
function createCronJobs(appId, schedulerConfig) {
assert.strictEqual(typeof appId, 'string');
assert(schedulerConfig && typeof schedulerConfig === 'object');
debug(`createCronJobs: creating cron jobs for app ${app.fqdn}`);
debug('creating cron jobs for app %s', appId);
var jobs = { };
Object.keys(schedulerConfig).forEach(function (taskName) {
var task = schedulerConfig[taskName];
const randomSecond = Math.floor(60*Math.random()); // don't start all crons to decrease memory pressure
var cronTime = (config.TEST ? '*/5 ' : '00 ') + task.schedule; // time ticks faster in tests
var cronTime = (config.TEST ? '*/5 ' : `${randomSecond} `) + task.schedule; // time ticks faster in tests
debug(`createCronJobs: ${app.fqdn} task ${taskName} scheduled at ${cronTime} with cmd ${task.command}`);
debug('scheduling task for %s/%s @ %s : %s', appId, taskName, cronTime, task.command);
var cronJob = new CronJob({
cronTime: cronTime, // at this point, the pattern has been validated
onTick: runTask.bind(null, app.id, taskName), // put the app id in closure, so we don't use the outdated app object by mistake
onTick: doTask.bind(null, appId, taskName),
start: true
});
@@ -135,50 +133,35 @@ function createCronJobs(app, schedulerConfig) {
return jobs;
}
function runTask(appId, taskName, callback) {
function doTask(appId, taskName, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof taskName, 'string');
assert(!callback || typeof callback === 'function');
const JOB_MAX_TIME = 30 * 60 * 1000; // 30 minutes
callback = callback || NOOP_CALLBACK;
debug(`runTask: running task ${taskName} of ${appId}`);
debug('Executing task %s/%s', appId, taskName);
apps.get(appId, function (error, app) {
if (error) return callback(error);
if (app.installationState !== appdb.ISTATE_INSTALLED || app.runState !== appdb.RSTATE_RUNNING || app.health !== appdb.HEALTH_HEALTHY) {
debug(`runTask: skipped task ${taskName} because app ${app.fqdn} has run state ${app.installationState}`);
debug('task %s skipped. app %s is not installed/running/healthy', taskName, app.id);
return callback();
}
const containerName = `${app.id}-${taskName}`;
var containerName = app.id + '-' + taskName;
docker.inspectByName(containerName, function (err, data) {
if (!err && data && data.State.Running === true) {
const jobStartTime = new Date(data.State.StartedAt); // iso 8601
if (new Date() - jobStartTime < JOB_MAX_TIME) {
debug(`runTask: skipped task ${taskName} of app ${app.fqdn} since it was started at ${jobStartTime}`);
return callback();
}
}
killContainer(containerName, function (error) {
if (error) return callback(error);
debug(`runTask: removing any old task ${taskName} of app ${app.fqdn}`);
debug('Creating subcontainer for %s/%s : %s', app.id, taskName, gState[appId].schedulerConfig[taskName].command);
killContainer(containerName, function (error) {
// NOTE: if you change container name here, fix addons.js to return correct container names
docker.createSubcontainer(app, containerName, [ '/bin/sh', '-c', gState[appId].schedulerConfig[taskName].command ], { } /* options */, function (error, container) {
if (error) return callback(error);
const cmd = gState[appId].schedulerConfig[taskName].command;
debug(`runTask: starting task ${taskName} of app ${app.fqdn} with cmd ${cmd}`);
// NOTE: if you change container name here, fix addons.js to return correct container names
docker.createSubcontainer(app, containerName, [ '/bin/sh', '-c', cmd ], { } /* options */, function (error, container) {
if (error) return callback(error);
docker.startContainer(container.id, callback);
});
docker.startContainer(container.id, callback);
});
});
});
+13 -22
View File
@@ -60,9 +60,7 @@ function initializeExpressSync() {
router.del = router.delete; // amend router.del for readability further on
app
// the timeout middleware will respond with a 503. the request itself cannot be 'aborted' and will continue
// search for req.clearTimeout in route handlers to see places where this timeout is reset
.use(middleware.timeout(REQUEST_TIMEOUT, { respond: true }))
.use(middleware.timeout(REQUEST_TIMEOUT))
.use(json)
.use(urlencoded)
.use(middleware.cookieParser())
@@ -197,10 +195,8 @@ function initializeExpressSync() {
router.post('/api/v1/apps/:id/upload', appsScope, routes.user.requireAdmin, multipart, routes.apps.uploadFile);
// settings routes (these are for the settings tab - avatar & name have public routes for normal users. see above)
router.get ('/api/v1/settings/app_autoupdate_pattern', settingsScope, routes.user.requireAdmin, routes.settings.getAppAutoupdatePattern);
router.post('/api/v1/settings/app_autoupdate_pattern', settingsScope, routes.user.requireAdmin, routes.settings.setAppAutoupdatePattern);
router.get ('/api/v1/settings/box_autoupdate_pattern', settingsScope, routes.user.requireAdmin, routes.settings.getBoxAutoupdatePattern);
router.post('/api/v1/settings/box_autoupdate_pattern', settingsScope, routes.user.requireAdmin, routes.settings.setBoxAutoupdatePattern);
router.get ('/api/v1/settings/autoupdate_pattern', settingsScope, routes.user.requireAdmin, routes.settings.getAutoupdatePattern);
router.post('/api/v1/settings/autoupdate_pattern', settingsScope, routes.user.requireAdmin, routes.settings.setAutoupdatePattern);
router.get ('/api/v1/settings/cloudron_name', settingsScope, routes.user.requireAdmin, routes.settings.getCloudronName);
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);
@@ -214,11 +210,9 @@ function initializeExpressSync() {
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.getDomain);
router.post('/api/v1/mail/:domain', settingsScope, routes.user.requireAdmin, routes.mail.updateDomain);
router.post('/api/v1/mail', settingsScope, routes.user.requireAdmin, routes.mail.addDomain);
router.get ('/api/v1/mail/:domain/stats', settingsScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.mail.getDomainStats);
router.del ('/api/v1/mail/:domain', settingsScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.mail.removeDomain);
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);
@@ -226,18 +220,15 @@ function initializeExpressSync() {
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/:name', settingsScope, routes.user.requireAdmin, routes.mail.getMailbox);
router.post('/api/v1/mail/:domain/mailboxes', settingsScope, routes.user.requireAdmin, routes.mail.addMailbox);
router.post('/api/v1/mail/:domain/mailboxes/:name', settingsScope, routes.user.requireAdmin, routes.mail.updateMailbox);
router.del ('/api/v1/mail/:domain/mailboxes/:name', settingsScope, routes.user.requireAdmin, routes.mail.removeMailbox);
router.get ('/api/v1/mail/:domain/aliases', settingsScope, routes.user.requireAdmin, routes.mail.listAliases);
router.get ('/api/v1/mail/:domain/aliases/:name', settingsScope, routes.user.requireAdmin, routes.mail.getAliases);
router.put ('/api/v1/mail/:domain/aliases/:name', settingsScope, routes.user.requireAdmin, routes.mail.setAliases);
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/:name', settingsScope, routes.user.requireAdmin, routes.mail.getList);
router.post('/api/v1/mail/:domain/lists/:name', settingsScope, routes.user.requireAdmin, routes.mail.updateList);
router.del ('/api/v1/mail/:domain/lists/:name', settingsScope, routes.user.requireAdmin, routes.mail.removeList);
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);
// feedback
router.post('/api/v1/feedback', usersScope, routes.cloudron.feedback);
+10 -44
View File
@@ -6,11 +6,8 @@ exports = module.exports = {
initialize: initialize,
uninitialize: uninitialize,
getAppAutoupdatePattern: getAppAutoupdatePattern,
setAppAutoupdatePattern: setAppAutoupdatePattern,
getBoxAutoupdatePattern: getBoxAutoupdatePattern,
setBoxAutoupdatePattern: setBoxAutoupdatePattern,
getAutoupdatePattern: getAutoupdatePattern,
setAutoupdatePattern: setAutoupdatePattern,
getTimeZone: getTimeZone,
setTimeZone: setTimeZone,
@@ -48,8 +45,7 @@ exports = module.exports = {
CAAS_CONFIG_KEY: 'caas_config',
// strings
APP_AUTOUPDATE_PATTERN_KEY: 'app_autoupdate_pattern',
BOX_AUTOUPDATE_PATTERN_KEY: 'box_autoupdate_pattern',
AUTOUPDATE_PATTERN_KEY: 'autoupdate_pattern',
TIME_ZONE_KEY: 'time_zone',
CLOUDRON_NAME_KEY: 'cloudron_name',
@@ -73,8 +69,7 @@ var assert = require('assert'),
var gDefaults = (function () {
var result = { };
result[exports.APP_AUTOUPDATE_PATTERN_KEY] = '00 30 1,3,5,23 * * *';
result[exports.BOX_AUTOUPDATE_PATTERN_KEY] = '00 00 1,3,5,23 * * *';
result[exports.AUTOUPDATE_PATTERN_KEY] = '00 00 1,3,5,23 * * *';
result[exports.TIME_ZONE_KEY] = 'America/Los_Angeles';
result[exports.CLOUDRON_NAME_KEY] = 'Cloudron';
result[exports.DYNAMIC_DNS_KEY] = false;
@@ -130,7 +125,7 @@ function uninitialize(callback) {
callback();
}
function setAppAutoupdatePattern(pattern, callback) {
function setAutoupdatePattern(pattern, callback) {
assert.strictEqual(typeof pattern, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -139,49 +134,20 @@ function setAppAutoupdatePattern(pattern, callback) {
if (!job) return callback(new SettingsError(SettingsError.BAD_FIELD, 'Invalid pattern'));
}
settingsdb.set(exports.APP_AUTOUPDATE_PATTERN_KEY, pattern, function (error) {
settingsdb.set(exports.AUTOUPDATE_PATTERN_KEY, pattern, function (error) {
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
exports.events.emit(exports.APP_AUTOUPDATE_PATTERN_KEY, pattern);
exports.events.emit(exports.AUTOUPDATE_PATTERN_KEY, pattern);
return callback(null);
});
}
function getAppAutoupdatePattern(callback) {
function getAutoupdatePattern(callback) {
assert.strictEqual(typeof callback, 'function');
settingsdb.get(exports.APP_AUTOUPDATE_PATTERN_KEY, function (error, pattern) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.APP_AUTOUPDATE_PATTERN_KEY]);
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
callback(null, pattern);
});
}
function setBoxAutoupdatePattern(pattern, callback) {
assert.strictEqual(typeof pattern, 'string');
assert.strictEqual(typeof callback, 'function');
if (pattern !== constants.AUTOUPDATE_PATTERN_NEVER) { // check if pattern is valid
var job = safe.safeCall(function () { return new CronJob(pattern); });
if (!job) return callback(new SettingsError(SettingsError.BAD_FIELD, 'Invalid pattern'));
}
settingsdb.set(exports.BOX_AUTOUPDATE_PATTERN_KEY, pattern, function (error) {
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
exports.events.emit(exports.BOX_AUTOUPDATE_PATTERN_KEY, pattern);
return callback(null);
});
}
function getBoxAutoupdatePattern(callback) {
assert.strictEqual(typeof callback, 'function');
settingsdb.get(exports.BOX_AUTOUPDATE_PATTERN_KEY, function (error, pattern) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.BOX_AUTOUPDATE_PATTERN_KEY]);
settingsdb.get(exports.AUTOUPDATE_PATTERN_KEY, function (error, pattern) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.AUTOUPDATE_PATTERN_KEY]);
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
callback(null, pattern);
+37 -21
View File
@@ -119,18 +119,38 @@ function configureWebadmin(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) {
debug('configureWebadmin: done error: %j', error || {});
gWebadminStatus.configuring = false;
if (error) return callback(error);
if (error) return done(error);
gWebadminStatus.tls = true;
callback();
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);
});
}
@@ -139,11 +159,10 @@ function configureWebadmin(callback) {
sysinfo.getPublicIp(function (error, ip) {
if (error) return configureReverseProxy(error);
domains.upsertDnsRecords(config.adminLocation(), config.adminDomain(), 'A', [ ip ], function (error) {
debug('addWebadminDnsRecord: updated records with error:', error);
addWebadminDnsRecord(ip, config.adminDomain(), function (error) {
if (error) return configureReverseProxy(error);
domains.waitForDnsRecord(config.adminFqdn(), config.adminDomain(), ip, { interval: 30000, times: 50000 }, function (error) {
domains.waitForDNSRecord(config.adminFqdn(), config.adminDomain(), ip, 'A', { interval: 30000, times: 50000 }, function (error) {
if (error) return configureReverseProxy(error);
gWebadminStatus.dns = true;
@@ -167,11 +186,9 @@ function dnsSetup(adminFqdn, domain, zoneName, provider, dnsConfig, tlsConfig, c
if (gWebadminStatus.configuring || gWebadminStatus.restoring) return callback(new SetupError(SetupError.BAD_STATE, 'Already restoring or configuring'));
if (!tld.isValid(adminFqdn) || !adminFqdn.endsWith(domain)) return callback(new SetupError(SetupError.BAD_FIELD, 'adminFqdn must be a subdomain of domain'));
if (!zoneName) zoneName = tld.getDomain(domain) || domain;
debug(`dnsSetup: Setting up Cloudron with domain ${domain} and zone ${zoneName} using admin fqdn ${adminFqdn}`);
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));
@@ -193,12 +210,14 @@ function dnsSetup(adminFqdn, domain, zoneName, provider, dnsConfig, tlsConfig, c
domains.get(domain, function (error, result) {
if (error && error.reason !== DomainError.NOT_FOUND) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
if (result) return callback(new SettingsError(SettingsError.ALREADY_EXISTS, 'domain already exists'));
async.series([
domains.add.bind(null, domain, zoneName, provider, dnsConfig, null /* cert */, tlsConfig),
mail.addDomain.bind(null, domain)
], done);
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);
}
});
}
@@ -285,7 +304,7 @@ function restore(backupConfig, backupId, version, callback) {
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} with format ${backupConfig.format}`);
debug(`restore: restoring from ${backupId} from provider ${backupConfig.provider}`);
gWebadminStatus.restoring = true;
@@ -294,9 +313,6 @@ function restore(backupConfig, backupId, version, callback) {
async.series([
backups.restore.bind(null, backupConfig, backupId),
autoprovision,
// currently, our suggested restore flow is after a dnsSetup. This re-creates DKIM keys and updates the DNS
// for this reason, we have to re-setup DNS after a restore. Once we have a 100% IP based restore, we can skip this
mail.addDnsRecords.bind(null, config.adminDomain()),
shell.sudo.bind(null, 'restart', [ RESTART_CMD ])
], function (error) {
debug('restore:', error);
+1 -1
View File
@@ -204,7 +204,7 @@ function copy(apiConfig, oldFilePath, newFilePath) {
var relativePath = path.relative(oldFilePath, file.name);
file.copy(path.join(newFilePath, relativePath), function(error) {
if (error && error.code === 404) return iteratorCallback(new BackupsError(BackupsError.NOT_FOUND, 'Old backup not found'));
if (error && error.code == 404) return iteratorCallback(new BackupsError(BackupsError.NOT_FOUND, 'Old backup not found'));
if (error) {
debug('copyBackup: gcs copy error', error);
return iteratorCallback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
-6
View File
@@ -6,13 +6,8 @@
// New backends can start from here
// -------------------------------------------
// Implementation note:
// retry logic for upload() comes from the syncer since it is stream based
// for the other API calls we leave it to the backend to retry. this allows
// them to tune the concurrency based on failures/rate limits accordingly
exports = module.exports = {
upload: upload,
download: download,
downloadDir: downloadDir,
copy: copy,
@@ -35,7 +30,6 @@ function upload(apiConfig, backupFilePath, sourceStream, callback) {
assert.strictEqual(typeof callback, 'function');
// Result: none
// sourceStream errors are handled upstream
callback(new Error('not implemented'));
}
+57 -65
View File
@@ -72,11 +72,7 @@ function getCaasConfig(apiConfig, callback) {
region: apiConfig.region || 'us-east-1',
maxRetries: 5,
retryDelayOptions: {
customBackoff: () => 20000 // constant backoff - https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Config.html#retryDelayOptions-property
},
httpOptions: {
connectTimeout: 10000, // https://github.com/aws/aws-sdk-js/pull/1446
timeout: 300 * 1000 // https://github.com/aws/aws-sdk-js/issues/1704 (allow 5MB chunk upload to take upto 5 minutes)
base: 20000 // 2^5 * 20 seconds
}
};
@@ -105,18 +101,16 @@ function getS3Config(apiConfig, callback) {
region: apiConfig.region || 'us-east-1',
maxRetries: 5,
retryDelayOptions: {
customBackoff: () => 20000 // constant backoff - https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Config.html#retryDelayOptions-property
},
httpOptions: {
connectTimeout: 10000, // https://github.com/aws/aws-sdk-js/pull/1446
timeout: 300 * 1000 // https://github.com/aws/aws-sdk-js/issues/1704 (allow 5MB chunk upload to take upto 5 minutes)
base: 20000 // 2^5 * 20 seconds
}
};
if (apiConfig.endpoint) credentials.endpoint = apiConfig.endpoint;
if (apiConfig.acceptSelfSignedCerts === true && credentials.endpoint && credentials.endpoint.startsWith('https://')) {
credentials.httpOptions.agent = new https.Agent({ rejectUnauthorized: false });
credentials.httpOptions.agent = {
agent: new https.Agent({ rejectUnauthorized: false })
};
}
callback(null, credentials);
}
@@ -128,6 +122,15 @@ function upload(apiConfig, backupFilePath, sourceStream, callback) {
assert.strictEqual(typeof sourceStream, 'object');
assert.strictEqual(typeof callback, 'function');
function done(error) {
if (error) {
debug('[%s] upload: s3 upload error.', backupFilePath, error);
return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, `Error uploading ${backupFilePath}. Message: ${error.message} HTTP Code: ${error.code}`));
}
callback(null);
}
getS3Config(apiConfig, function (error, credentials) {
if (error) return callback(error);
@@ -141,16 +144,7 @@ function upload(apiConfig, backupFilePath, sourceStream, callback) {
// 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.
s3.upload(params, { partSize: 10 * 1024 * 1024, queueSize: 1 }, function (error, data) {
if (error) {
debug('Error uploading [%s]: s3 upload error.', backupFilePath, error);
return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, `Error uploading ${backupFilePath}. Message: ${error.message} HTTP Code: ${error.code}`));
}
debug(`Uploaded ${backupFilePath}: ${JSON.stringify(data)}`);
callback(null);
});
return s3.upload(params, { partSize: 10 * 1024 * 1024, queueSize: 1 }, done);
});
}
@@ -187,14 +181,14 @@ function download(apiConfig, backupFilePath, callback) {
});
}
function listDir(apiConfig, dir, iteratorCallback, callback) {
function listDir(apiConfig, backupFilePath, iteratorCallback, callback) {
getS3Config(apiConfig, function (error, credentials) {
if (error) return callback(error);
var s3 = new AWS.S3(credentials);
var listParams = {
Bucket: apiConfig.bucket,
Prefix: dir
Prefix: backupFilePath
};
async.forever(function listAndDownload(foreverCallback) {
@@ -298,10 +292,11 @@ function copy(apiConfig, oldFilePath, newFilePath) {
var relativePath = path.relative(oldFilePath, content.Key);
function done(error) {
if (error) debug(`copy: s3 copy error when copying ${content.Key}: ${error}`);
if (error && error.code === 'NoSuchKey') return iteratorCallback(new BackupsError(BackupsError.NOT_FOUND, `Old backup not found: ${content.Key}`));
if (error) return iteratorCallback(new BackupsError(BackupsError.EXTERNAL_ERROR, `Error copying ${content.Key} : ${error.code} ${error}`));
if (error) {
debug('copy: s3 copy error when copying %s %s', content.Key, error);
return iteratorCallback(new BackupsError(BackupsError.EXTERNAL_ERROR, `Error copying ${content.Key} : ${error.message} ${error.code}`));
}
iteratorCallback(null);
}
@@ -312,30 +307,24 @@ function copy(apiConfig, oldFilePath, newFilePath) {
};
// S3 copyObject has a file size limit of 5GB so if we have larger files, we do a multipart copy
// Exoscale takes too long to copy 5GB
const largeFileLimit = apiConfig.provider === 'exoscale-sos' ? 1024 * 1024 * 1024 : 5 * 1024 * 1024 * 1024;
if (content.Size < largeFileLimit) {
events.emit('progress', `Copying ${relativePath || oldFilePath}`);
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);
s3.copyObject(copyParams, done).on('retry', function (response) {
++retryCount;
events.emit('progress', `Retrying (${response.retryCount+1}) copy of ${relativePath || oldFilePath}. Error: ${response.error} ${response.httpResponse.statusCode}`);
// on DO, we get a random 408. these are not retried by the SDK
if (response.error) response.error.retryable = true; // https://github.com/aws/aws-sdk-js/issues/412
events.emit('progress', `Retrying (${response.retryCount+1}) copy of ${relativePath}. Status code: ${response.httpResponse.statusCode}`);
});
return;
}
events.emit('progress', `Copying (multipart) ${relativePath || oldFilePath}`);
events.emit('progress', `Copying (multipart) ${relativePath}`);
s3.createMultipartUpload(copyParams, function (error, result) {
if (error) return done(error);
// Exoscale (96M) was suggested by exoscale. 1GB - rather random size for others
const chunkSize = apiConfig.provider === 'exoscale-sos' ? 96 * 1024 * 1024 : 1024 * 1024 * 1024;
const CHUNK_SIZE = 1024 * 1024 * 1024; // 1GB - rather random size
var uploadId = result.UploadId;
var uploadedParts = [];
var partNumber = 1;
@@ -344,7 +333,7 @@ function copy(apiConfig, oldFilePath, newFilePath) {
var size = content.Size-1;
function copyNextChunk() {
endBytes = startBytes + chunkSize;
endBytes = startBytes + CHUNK_SIZE;
if (endBytes > size) endBytes = size;
var params = {
@@ -377,7 +366,7 @@ function copy(apiConfig, oldFilePath, newFilePath) {
s3.completeMultipartUpload(params, done);
}).on('retry', function (response) {
++retryCount;
events.emit('progress', `Retrying (${response.retryCount+1}) multipart copy of ${relativePath || oldFilePath}. Error: ${response.error} ${response.httpResponse.statusCode}`);
events.emit('progress', `Retrying (${response.retryCount+1}) multipart copy of ${relativePath}. Status code: ${response.httpResponse.statusCode}`);
});
}
@@ -387,16 +376,16 @@ function copy(apiConfig, oldFilePath, newFilePath) {
var total = 0, concurrency = 4;
listDir(apiConfig, oldFilePath, function listDirIterator(s3, objects, done) {
listDir(apiConfig, oldFilePath, function (s3, objects, done) {
total += objects.length;
if (retryCount === 0) concurrency = Math.min(concurrency + 1, 10); else concurrency = Math.max(concurrency - 1, 5);
events.emit('progress', `Copying ${total-objects.length}-${total}. ${retryCount} errors so far. concurrency set to ${concurrency}`);
events.emit('progress', `${retryCount} errors. concurrency set to ${concurrency}`);
retryCount = 0;
async.eachLimit(objects, concurrency, copyFile.bind(null, s3), done);
}, function (error) {
events.emit('progress', `Copied ${total} files with error: ${error}`);
events.emit('progress', `Copied ${total} files`);
events.emit('done', error);
});
@@ -421,11 +410,10 @@ function remove(apiConfig, filename, callback) {
}
};
// deleteObjects does not return error if key is not found
s3.deleteObjects(deleteParams, function (error) {
if (error) debug(`remove: Unable to remove ${deleteParams.Key}. error: ${error.message}`);
if (error) debug('remove: Unable to remove %s. Not fatal.', deleteParams.Key, error);
callback(error);
callback(null);
});
});
}
@@ -437,29 +425,33 @@ function removeDir(apiConfig, pathPrefix) {
var events = new EventEmitter();
var total = 0;
listDir(apiConfig, pathPrefix, function listDirIterator(s3, objects, done) {
function deleteFiles(s3, contents, iteratorCallback) {
var deleteParams = {
Bucket: apiConfig.bucket,
Delete: {
Objects: contents.map(function (c) { return { Key: c.Key }; })
}
};
events.emit('progress', `Removing ${contents.length} files from ${contents[0].Key} to ${contents[contents.length-1].Key}`);
s3.deleteObjects(deleteParams, function (error /*, deleteData */) {
if (error) {
events.emit('progress', `Unable to remove ${deleteParams.Key} ${error.message}`);
return iteratorCallback(error);
}
iteratorCallback();
});
}
listDir(apiConfig, pathPrefix, function (s3, objects, done) {
total += objects.length;
const chunkSize = apiConfig.provider !== 'digitalocean-spaces' ? 1000 : 100; // throttle objects in each request
var chunks = chunk(objects, chunkSize);
const batchSize = apiConfig.provider !== 'digitalocean-spaces' ? 1000 : 100; // throttle objects in each request
var chunks = batchSize === 1 ? objects : chunk(objects, batchSize);
async.eachSeries(chunks, function deleteFiles(contents, iteratorCallback) {
var deleteParams = {
Bucket: apiConfig.bucket,
Delete: {
Objects: contents.map(function (c) { return { Key: c.Key }; })
}
};
events.emit('progress', `Removing ${contents.length} files from ${contents[0].Key} to ${contents[contents.length-1].Key}`);
// deleteObjects does not return error if key is not found
s3.deleteObjects(deleteParams, function (error /*, deleteData */) {
if (error) events.emit('progress', `Unable to remove ${deleteParams.Key} ${error.message}`);
iteratorCallback(error);
});
}, done);
async.eachSeries(chunks, deleteFiles.bind(null, s3), done);
}, function (error) {
events.emit('progress', `Removed ${total} files`);
+3 -3
View File
@@ -104,14 +104,14 @@ function sync(dir, taskProcessor, concurrency, callback) {
if (entryStat.isDirectory()) {
traverse(entryPath);
} else {
addQueue.push({ operation: 'add', path: entryPath, reason: 'new', position: addQueue.length });
addQueue.push({ operation: 'add', path: entryPath, reason: 'new' });
}
} else if (ISDIR(cacheStat.mode) && entryStat.isDirectory()) { // dir names match
++curCacheIndex;
traverse(entryPath);
} else if (ISFILE(cacheStat.mode) && entryStat.isFile()) { // file names match
if (entryStat.mtime.getTime() !== cacheStat.mtime || entryStat.size != cacheStat.size || entryStat.inode !== cacheStat.inode) { // file changed
addQueue.push({ operation: 'add', path: entryPath, reason: 'changed', position: addQueue.length });
addQueue.push({ operation: 'add', path: entryPath, reason: 'changed' });
}
++curCacheIndex;
} else if (entryStat.isDirectory()) { // was a file, now a directory
@@ -121,7 +121,7 @@ function sync(dir, taskProcessor, concurrency, callback) {
} else { // was a dir, now a file
delQueue.push({ operation: 'removedir', path: cachePath, reason: 'wasdir' });
while (curCacheIndex !== cache.length && cache[curCacheIndex].path.startsWith(cachePath)) ++curCacheIndex;
addQueue.push({ operation: 'add', path: entryPath, reason: 'wasdir', position: addQueue.length });
addQueue.push({ operation: 'add', path: entryPath, reason: 'wasdir' });
}
}
}
+2 -1
View File
@@ -16,6 +16,7 @@ var appdb = require('./appdb.js'),
assert = require('assert'),
async = require('async'),
child_process = require('child_process'),
config = require('./config.js'),
debug = require('debug')('box:taskmanager'),
locker = require('./locker.js'),
sendFailureLogs = require('./logcollector.js').sendFailureLogs,
@@ -47,7 +48,7 @@ function resumeTasks(callback) {
if (app.installationState === appdb.ISTATE_ERROR) return;
debug('Creating process for %s (%s) with state %s', app.fqdn, app.id, app.installationState);
debug('Creating process for %s (%s) with state %s', app.intrinsicFqdn, app.id, app.installationState);
restartAppTask(app.id, NOOP_CALLBACK); // restart because the auto-installer could have queued up tasks already
});
-176
View File
@@ -1,176 +0,0 @@
/* jslint node:true */
/* global it:false */
/* global describe:false */
/* global before:false */
/* global after:false */
/* global beforeEach:false */
'use strict';
var async = require('async'),
appstore = require('../appstore.js'),
AppstoreError = appstore.AppstoreError,
config = require('../config.js'),
database = require('../database.js'),
expect = require('expect.js'),
nock = require('nock'),
settings = require('../settings.js');
const DOMAIN = 'example-appstore-test.com';
const APPSTORE_USER_ID = 'appstoreuserid';
const APPSTORE_TOKEN = 'appstoretoken';
const CLOUDRON_ID = 'cloudronid';
const APP_ID = 'appid';
const APPSTORE_APP_ID = 'appstoreappid';
function setup(done) {
nock.cleanAll();
config.setFqdn(DOMAIN);
config.setAdminFqdn('my.' + DOMAIN);
async.series([
database.initialize,
database._clear,
settings.initialize
], done);
}
function cleanup(done) {
nock.cleanAll();
async.series([
settings.uninitialize,
database._clear,
database.uninitialize
], done);
}
describe('Appstore', function () {
before(setup);
after(cleanup);
beforeEach(nock.cleanAll);
it('cannot send alive status without appstore config', function (done) {
appstore.sendAliveStatus(function (error) {
expect(error).to.be.ok();
expect(error.reason).to.equal(AppstoreError.BILLING_REQUIRED);
done();
});
});
it('can set appstore config', function (done) {
var scope = nock('http://localhost:6060')
.post(`/api/v1/users/${APPSTORE_USER_ID}/cloudrons?accessToken=${APPSTORE_TOKEN}`, function () { return true; })
.reply(201, { cloudron: { id: CLOUDRON_ID }});
settings.setAppstoreConfig({ userId: APPSTORE_USER_ID, token: APPSTORE_TOKEN }, function (error) {
expect(error).to.not.be.ok();
expect(scope.isDone()).to.be.ok();
done();
});
});
it('can send alive status', function (done) {
var scope = nock('http://localhost:6060')
.post(`/api/v1/users/${APPSTORE_USER_ID}/cloudrons/${CLOUDRON_ID}/alive?accessToken=${APPSTORE_TOKEN}`, function (body) {
expect(body.version).to.be.a('string');
expect(body.adminFqdn).to.be.a('string');
expect(body.provider).to.be.a('string');
expect(body.backendSettings).to.be.an('object');
expect(body.backendSettings.backupConfig).to.be.an('object');
expect(body.backendSettings.backupConfig.provider).to.be.a('string');
expect(body.backendSettings.backupConfig.hardlinks).to.be.a('boolean');
expect(body.backendSettings.domainConfig).to.be.an('object');
expect(body.backendSettings.domainConfig.count).to.be.a('number');
expect(body.backendSettings.domainConfig.domains).to.be.an('array');
expect(body.backendSettings.mailConfig).to.be.an('object');
expect(body.backendSettings.mailConfig.outboundCount).to.be.a('number');
expect(body.backendSettings.mailConfig.inboundCount).to.be.a('number');
expect(body.backendSettings.mailConfig.catchAllCount).to.be.a('number');
expect(body.backendSettings.mailConfig.relayProviders).to.be.an('array');
expect(body.backendSettings.appAutoupdatePattern).to.be.a('string');
expect(body.backendSettings.boxAutoupdatePattern).to.be.a('string');
expect(body.backendSettings.timeZone).to.be.a('string');
expect(body.machine).to.be.an('object');
expect(body.machine.cpus).to.be.an('array');
expect(body.machine.totalmem).to.be.an('number');
expect(body.events).to.be.an('object');
expect(body.events.lastLogin).to.be.an('number');
return true;
})
.reply(201, { cloudron: { id: CLOUDRON_ID }});
appstore.sendAliveStatus(function (error) {
expect(error).to.not.be.ok();
expect(scope.isDone()).to.be.ok();
done();
});
});
it('can get account', function (done) {
var scope = nock('http://localhost:6060')
.get(`/api/v1/users/${APPSTORE_USER_ID}?accessToken=${APPSTORE_TOKEN}`)
.reply(200, { profile: { id: APPSTORE_USER_ID }});
appstore.getAccount(function (error, result) {
expect(error).to.not.be.ok();
expect(scope.isDone()).to.be.ok();
expect(result.id).to.equal(APPSTORE_USER_ID);
done();
});
});
it('can purchase an app', function (done) {
var scope = nock('http://localhost:6060')
.post(`/api/v1/users/${APPSTORE_USER_ID}/cloudrons/${CLOUDRON_ID}/apps/${APP_ID}?accessToken=${APPSTORE_TOKEN}`, function () { return true; })
.reply(201, {});
appstore.purchase(APP_ID, APPSTORE_APP_ID, function (error) {
expect(error).to.not.be.ok();
expect(scope.isDone()).to.be.ok();
done();
});
});
it('unpurchase succeeds if app was never purchased', function (done) {
var scope1 = nock('http://localhost:6060')
.get(`/api/v1/users/${APPSTORE_USER_ID}/cloudrons/${CLOUDRON_ID}/apps/${APP_ID}?accessToken=${APPSTORE_TOKEN}`)
.reply(404, {});
var scope2 = nock('http://localhost:6060')
.delete(`/api/v1/users/${APPSTORE_USER_ID}/cloudrons/${CLOUDRON_ID}/apps/${APP_ID}?accessToken=${APPSTORE_TOKEN}`, function () { return true; })
.reply(204, {});
appstore.unpurchase(APP_ID, APPSTORE_APP_ID, function (error) {
expect(error).to.not.be.ok();
expect(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.not.be.ok();
done();
});
});
it('can unpurchase an app', function (done) {
var scope1 = nock('http://localhost:6060')
.get(`/api/v1/users/${APPSTORE_USER_ID}/cloudrons/${CLOUDRON_ID}/apps/${APP_ID}?accessToken=${APPSTORE_TOKEN}`)
.reply(200, {});
var scope2 = nock('http://localhost:6060')
.delete(`/api/v1/users/${APPSTORE_USER_ID}/cloudrons/${CLOUDRON_ID}/apps/${APP_ID}?accessToken=${APPSTORE_TOKEN}`, function () { return true; })
.reply(204, {});
appstore.unpurchase(APP_ID, APPSTORE_APP_ID, function (error) {
expect(error).to.not.be.ok();
expect(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
done();
});
});
});
+1
View File
@@ -68,6 +68,7 @@ var APP = {
runState: null,
location: 'applocation',
domain: DOMAIN_0.domain,
intrinsicFqdn: DOMAIN_0.domain + '.' + 'applocation',
fqdn: DOMAIN_0.domain + '.' + 'applocation',
manifest: MANIFEST,
containerId: null,
+23 -18
View File
@@ -210,6 +210,7 @@ describe('database', function () {
oldConfig: null,
newConfig: null,
memoryLimit: 4294967296,
altDomain: null,
xFrameOptions: 'DENY',
sso: true,
debugMode: null,
@@ -681,7 +682,7 @@ describe('database', function () {
tokendb.add(TOKEN_0.accessToken, TOKEN_0.identifier, TOKEN_0.clientId, TOKEN_0.expires, TOKEN_0.scope, function (error) {
expect(error).to.be(null);
tokendb.delByClientId(TOKEN_0.clientId, function (error) {
tokendb.delByClientId(TOKEN_0.clientId, function (error, result) {
expect(error).to.not.be.ok();
tokendb.get(TOKEN_0.accessToken, function (error, result) {
@@ -715,6 +716,7 @@ describe('database', function () {
oldConfig: null,
updateConfig: null,
memoryLimit: 4294967296,
altDomain: null,
xFrameOptions: 'DENY',
sso: true,
debugMode: null,
@@ -741,6 +743,7 @@ describe('database', function () {
oldConfig: null,
updateConfig: null,
memoryLimit: 0,
altDomain: null,
xFrameOptions: 'SAMEORIGIN',
sso: true,
debugMode: null,
@@ -1016,7 +1019,7 @@ describe('database', function () {
});
it('getAddonConfigByName of unknown value succeeds', function (done) {
appdb.getAddonConfigByName(APP_1.id, 'addonid1', 'NOPE', function (error) {
appdb.getAddonConfigByName(APP_1.id, 'addonid1', 'NOPE', function (error, value) {
expect(error.reason).to.be(DatabaseError.NOT_FOUND);
done();
});
@@ -1362,7 +1365,7 @@ describe('database', function () {
});
it('getAllPaged succeeds', function (done) {
eventlogdb.getAllPaged([], null, 1, 1, function (error, results) {
eventlogdb.getAllPaged(null, null, 1, 1, function (error, results) {
expect(error).to.be(null);
expect(results).to.be.an(Array);
expect(results.length).to.be(1);
@@ -1377,7 +1380,7 @@ describe('database', function () {
});
it('getAllPaged succeeds with source search', function (done) {
eventlogdb.getAllPaged([], '1.2.3.4', 1, 1, function (error, results) {
eventlogdb.getAllPaged(null, '1.2.3.4', 1, 1, function (error, results) {
expect(error).to.be(null);
expect(results).to.be.an(Array);
expect(results.length).to.be(1);
@@ -1392,7 +1395,7 @@ describe('database', function () {
});
it('getAllPaged succeeds with data search', function (done) {
eventlogdb.getAllPaged([], 'thatapp', 1, 1, function (error, results) {
eventlogdb.getAllPaged(null, 'thatapp', 1, 1, function (error, results) {
expect(error).to.be(null);
expect(results).to.be.an(Array);
expect(results.length).to.be(1);
@@ -1412,12 +1415,17 @@ describe('database', function () {
}, function (error) {
expect(error).to.be(null);
eventlogdb.delByCreationTime(new Date(), function (error) {
var actions = [ 'anotherpersistent.event', 'persistent.event' ];
eventlogdb.delByCreationTime(new Date(), actions, function (error) {
expect(error).to.be(null);
eventlogdb.getAllPaged([], null, 1, 100, function (error, results) {
eventlogdb.getAllPaged(null, null, 1, 100, function (error, results) {
expect(error).to.be(null);
expect(results.length).to.be(0);
expect(results.length).to.be(2);
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');
done();
});
@@ -1443,7 +1451,7 @@ describe('database', function () {
var GROUP_ID_1 = 'foundersid';
it('can create a group', function (done) {
groupdb.add(GROUP_ID_1, 'founders', function (error) {
groupdb.add(GROUP_ID_1, 'founders', function (error, result) {
expect(error).to.be(null);
done();
});
@@ -1585,10 +1593,7 @@ describe('database', function () {
describe('mailboxes', function () {
before(function (done) {
async.series([
domaindb.add.bind(null, DOMAIN_0.domain, { zoneName: DOMAIN_0.zoneName, provider: DOMAIN_0.provider, config: DOMAIN_0.config, tlsConfig: DOMAIN_0.tlsConfig }),
maildb.add.bind(null, DOMAIN_0.domain)
], 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) {
@@ -1596,21 +1601,21 @@ describe('database', function () {
});
it('add user mailbox succeeds', function (done) {
mailboxdb.addMailbox('girish', DOMAIN_0.domain, 'uid-0', mailboxdb.OWNER_TYPE_USER, function (error) {
mailboxdb.add('girish', DOMAIN_0.domain, 'uid-0', mailboxdb.TYPE_USER, function (error, mailbox) {
expect(error).to.be(null);
done();
});
});
it('cannot add dup entry', function (done) {
mailboxdb.addMailbox('girish', DOMAIN_0.domain, 'uid-1', mailboxdb.OWNER_TYPE_APP, function (error) {
mailboxdb.add('girish', DOMAIN_0.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.addMailbox('support', DOMAIN_0.domain, 'osticket', mailboxdb.OWNER_TYPE_APP, function (error) {
mailboxdb.add('support', DOMAIN_0.domain, 'osticket', mailboxdb.TYPE_APP, function (error, mailbox) {
expect(error).to.be(null);
done();
});
@@ -1633,7 +1638,7 @@ describe('database', function () {
expect(error).to.be(null);
expect(mailboxes.length).to.be(2);
expect(mailboxes[0].name).to.be('girish');
expect(mailboxes[0].ownerType).to.be(mailboxdb.OWNER_TYPE_USER);
expect(mailboxes[0].ownerType).to.be(mailboxdb.TYPE_USER);
expect(mailboxes[1].name).to.be('support');
done();
@@ -1735,7 +1740,7 @@ describe('database', function () {
mailboxdb.delByOwnerId('osticket', function (error) {
expect(error).to.be(null);
mailboxdb.getByOwnerId('osticket', function (error) {
mailboxdb.getByOwnerId('osticket', function (error, results) {
expect(error).to.be.ok();
expect(error.reason).to.be(DatabaseError.NOT_FOUND);
done();
+1 -1
View File
@@ -72,7 +72,7 @@ describe('digest', function () {
database._clear,
settings.initialize,
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0.zoneName, DOMAIN_0.provider, DOMAIN_0.config, DOMAIN_0.fallbackCertificate, DOMAIN_0.tlsConfig),
mail.addDomain.bind(null, DOMAIN_0.domain),
mail.add.bind(null, DOMAIN_0.domain),
user.createOwner.bind(null, USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, AUDIT_SOURCE),
function (callback) {
userdb.getByUsername(USER_0.username, function (error, result) {
+18 -18
View File
@@ -55,7 +55,7 @@ describe('dns provider', function () {
});
it('upsert succeeds', function (done) {
domains.upsertDnsRecords('test', DOMAIN_0.domain, 'A', [ '1.2.3.4' ], function (error, result) {
domains.upsertDNSRecords('test', DOMAIN_0.domain, 'A', [ '1.2.3.4' ], function (error, result) {
expect(error).to.eql(null);
expect(result).to.eql('noop-record-id');
@@ -64,7 +64,7 @@ describe('dns provider', function () {
});
it('get succeeds', function (done) {
domains.getDnsRecords('test', DOMAIN_0.domain, 'A', function (error, result) {
domains.getDNSRecords('test', DOMAIN_0.domain, 'A', function (error, result) {
expect(error).to.eql(null);
expect(result).to.be.an(Array);
expect(result.length).to.eql(0);
@@ -74,7 +74,7 @@ describe('dns provider', function () {
});
it('del succeeds', function (done) {
domains.removeDnsRecords('test', DOMAIN_0.domain, 'A', [ '1.2.3.4' ], function (error) {
domains.removeDNSRecords('test', DOMAIN_0.domain, 'A', [ '1.2.3.4' ], function (error) {
expect(error).to.eql(null);
done();
@@ -115,7 +115,7 @@ describe('dns provider', function () {
.post('/v2/domains/' + DOMAIN_0.zoneName + '/records')
.reply(201, { domain_record: DOMAIN_RECORD_0 });
domains.upsertDnsRecords('test', DOMAIN_0.domain, 'A', [ '1.2.3.4' ], function (error, result) {
domains.upsertDNSRecords('test', DOMAIN_0.domain, 'A', [ '1.2.3.4' ], function (error, result) {
expect(error).to.eql(null);
expect(result).to.eql('3352892');
expect(req1.isDone()).to.be.ok();
@@ -165,7 +165,7 @@ describe('dns provider', function () {
.put('/v2/domains/' + DOMAIN_0.zoneName + '/records/' + DOMAIN_RECORD_1.id)
.reply(200, { domain_record: DOMAIN_RECORD_1_NEW });
domains.upsertDnsRecords('test', DOMAIN_0.domain, 'A', [ DOMAIN_RECORD_1_NEW.data ], function (error, result) {
domains.upsertDNSRecords('test', DOMAIN_0.domain, 'A', [ DOMAIN_RECORD_1_NEW.data ], function (error, result) {
expect(error).to.eql(null);
expect(result).to.eql('3352893');
expect(req1.isDone()).to.be.ok();
@@ -251,7 +251,7 @@ describe('dns provider', function () {
.post('/v2/domains/' + DOMAIN_0.zoneName + '/records')
.reply(201, { domain_record: DOMAIN_RECORD_2_NEW });
domains.upsertDnsRecords('', DOMAIN_0.domain, 'TXT', [ DOMAIN_RECORD_2_NEW.data, DOMAIN_RECORD_1_NEW.data, DOMAIN_RECORD_3_NEW.data ], function (error, result) {
domains.upsertDNSRecords('', DOMAIN_0.domain, 'TXT', [ DOMAIN_RECORD_2_NEW.data, DOMAIN_RECORD_1_NEW.data, DOMAIN_RECORD_3_NEW.data ], function (error, result) {
expect(error).to.eql(null);
expect(result).to.eql('3352893');
expect(req1.isDone()).to.be.ok();
@@ -290,7 +290,7 @@ describe('dns provider', function () {
.get('/v2/domains/' + DOMAIN_0.zoneName + '/records')
.reply(200, { domain_records: [ DOMAIN_RECORD_0, DOMAIN_RECORD_1 ] });
domains.getDnsRecords('test', DOMAIN_0.domain, 'A', function (error, result) {
domains.getDNSRecords('test', DOMAIN_0.domain, 'A', function (error, result) {
expect(error).to.eql(null);
expect(result).to.be.an(Array);
expect(result.length).to.eql(1);
@@ -331,7 +331,7 @@ describe('dns provider', function () {
.delete('/v2/domains/' + DOMAIN_0.zoneName + '/records/' + DOMAIN_RECORD_1.id)
.reply(204, {});
domains.removeDnsRecords('test', DOMAIN_0.domain, 'A', ['1.2.3.4'], function (error) {
domains.removeDNSRecords('test', DOMAIN_0.domain, 'A', ['1.2.3.4'], function (error) {
expect(error).to.eql(null);
expect(req1.isDone()).to.be.ok();
expect(req2.isDone()).to.be.ok();
@@ -437,7 +437,7 @@ describe('dns provider', function () {
}
}]);
domains.upsertDnsRecords('test', DOMAIN_0.domain, 'A', [ '1.2.3.4' ], function (error, result) {
domains.upsertDNSRecords('test', DOMAIN_0.domain, 'A', [ '1.2.3.4' ], function (error, result) {
expect(error).to.eql(null);
expect(result).to.eql('/change/C2QLKQIWEI0BZF');
expect(awsAnswerQueue.length).to.eql(0);
@@ -456,7 +456,7 @@ describe('dns provider', function () {
}
}]);
domains.upsertDnsRecords('test', DOMAIN_0.domain, 'A', [ '1.2.3.4' ], function (error, result) {
domains.upsertDNSRecords('test', DOMAIN_0.domain, 'A', [ '1.2.3.4' ], function (error, result) {
expect(error).to.eql(null);
expect(result).to.eql('/change/C2QLKQIWEI0BZF');
expect(awsAnswerQueue.length).to.eql(0);
@@ -475,7 +475,7 @@ describe('dns provider', function () {
}
}]);
domains.upsertDnsRecords('', DOMAIN_0.domain, 'TXT', [ 'first', 'second', 'third' ], function (error, result) {
domains.upsertDNSRecords('', DOMAIN_0.domain, 'TXT', [ 'first', 'second', 'third' ], function (error, result) {
expect(error).to.eql(null);
expect(result).to.eql('/change/C2QLKQIWEI0BZF');
expect(awsAnswerQueue.length).to.eql(0);
@@ -496,7 +496,7 @@ describe('dns provider', function () {
}]
}]);
domains.getDnsRecords('test', DOMAIN_0.domain, 'A', function (error, result) {
domains.getDNSRecords('test', DOMAIN_0.domain, 'A', function (error, result) {
expect(error).to.eql(null);
expect(result).to.be.an(Array);
expect(result.length).to.eql(1);
@@ -517,7 +517,7 @@ describe('dns provider', function () {
}
}]);
domains.removeDnsRecords('test', DOMAIN_0.domain, 'A', ['1.2.3.4'], function (error) {
domains.removeDNSRecords('test', DOMAIN_0.domain, 'A', ['1.2.3.4'], function (error) {
expect(error).to.eql(null);
expect(awsAnswerQueue.length).to.eql(0);
@@ -588,7 +588,7 @@ describe('dns provider', function () {
zoneQueue.push([null, [ ]]); // getRecords
zoneQueue.push([null, {id: '1'}]);
domains.upsertDnsRecords('test', DOMAIN_0.domain, 'A', [ '1.2.3.4' ], function (error, result) {
domains.upsertDNSRecords('test', DOMAIN_0.domain, 'A', [ '1.2.3.4' ], function (error, result) {
expect(error).to.eql(null);
expect(result).to.eql('1');
expect(zoneQueue.length).to.eql(0);
@@ -602,7 +602,7 @@ describe('dns provider', function () {
zoneQueue.push([null, [GCDNS().zone('test').record('A', {'name': 'test', data:['5.6.7.8'], ttl: 1})]]);
zoneQueue.push([null, {id: '2'}]);
domains.upsertDnsRecords('test', DOMAIN_0.domain, 'A', [ '1.2.3.4' ], function (error, result) {
domains.upsertDNSRecords('test', DOMAIN_0.domain, 'A', [ '1.2.3.4' ], function (error, result) {
expect(error).to.eql(null);
expect(result).to.eql('2');
expect(zoneQueue.length).to.eql(0);
@@ -616,7 +616,7 @@ describe('dns provider', function () {
zoneQueue.push([null, [ ]]); // getRecords
zoneQueue.push([null, {id: '3'}]);
domains.upsertDnsRecords('', DOMAIN_0.domain, 'TXT', [ 'first', 'second', 'third' ], function (error, result) {
domains.upsertDNSRecords('', DOMAIN_0.domain, 'TXT', [ 'first', 'second', 'third' ], function (error, result) {
expect(error).to.eql(null);
expect(result).to.eql('3');
expect(zoneQueue.length).to.eql(0);
@@ -629,7 +629,7 @@ describe('dns provider', function () {
zoneQueue.push([null, HOSTED_ZONES]);
zoneQueue.push([null, [GCDNS().zone('test').record('A', {'name': 'test', data:['1.2.3.4', '5.6.7.8'], ttl: 1})]]);
domains.getDnsRecords('test', DOMAIN_0.domain, 'A', function (error, result) {
domains.getDNSRecords('test', DOMAIN_0.domain, 'A', function (error, result) {
expect(error).to.eql(null);
expect(result).to.be.an(Array);
expect(result.length).to.eql(2);
@@ -645,7 +645,7 @@ describe('dns provider', function () {
zoneQueue.push([null, [GCDNS().zone('test').record('A', {'name': 'test', data:['5.6.7.8'], ttl: 1})]]);
zoneQueue.push([null, {id: '5'}]);
domains.removeDnsRecords('test', DOMAIN_0.domain, 'A', ['1.2.3.4'], function (error) {
domains.removeDNSRecords('test', DOMAIN_0.domain, 'A', ['1.2.3.4'], function (error) {
expect(error).to.eql(null);
expect(zoneQueue.length).to.eql(0);
+1 -1
View File
@@ -69,7 +69,7 @@ describe('Eventlog', function () {
});
it('getAllPaged succeeds', function (done) {
eventlog.getAllPaged([], null, 1, 1, function (error, results) {
eventlog.getAllPaged(null, null, 1, 1, function (error, results) {
expect(error).to.be(null);
expect(results).to.be.an(Array);
expect(results.length).to.be(1);
+5 -5
View File
@@ -103,7 +103,7 @@ function setup(done) {
appdb.update.bind(null, APP_0.id, { containerId: APP_0.containerId }),
appdb.setAddonConfig.bind(null, APP_0.id, 'sendmail', [{ name: 'MAIL_SMTP_PASSWORD', value : 'sendmailpassword' }]),
appdb.setAddonConfig.bind(null, APP_0.id, 'recvmail', [{ name: 'MAIL_IMAP_PASSWORD', value : 'recvmailpassword' }]),
mailboxdb.addMailbox.bind(null, APP_0.location + '.app', APP_0.domain, APP_0.id, mailboxdb.OWNER_TYPE_APP),
mailboxdb.add.bind(null, APP_0.location + '.app', APP_0.domain, APP_0.id, mailboxdb.TYPE_APP),
function (callback) {
user.createOwner(USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, AUDIT_SOURCE, function (error, result) {
@@ -739,7 +739,7 @@ describe('Ldap', function () {
describe('search mailbox', function () {
before(function (done) {
mailboxdb.addMailbox(USER_0.username.toLowerCase(), DOMAIN_0.domain, USER_0.id, mailboxdb.OWNER_TYPE_USER, done);
mailboxdb.add(USER_0.username.toLowerCase(), DOMAIN_0.domain, USER_0.id, mailboxdb.TYPE_USER, done);
});
it('get specific mailbox by email', function (done) {
@@ -824,14 +824,14 @@ describe('Ldap', function () {
describe('search mailing list', function () {
before(function (done) {
mailboxdb.addGroup('devs', DOMAIN_0.domain, [ USER_0.username.toLowerCase(), USER_1.username.toLowerCase() ], done);
mailboxdb.add(GROUP_NAME, DOMAIN_0.domain, GROUP_ID, mailboxdb.TYPE_GROUP, done);
});
it('get specific list', function (done) {
ldapSearch('cn=devs@example.com,ou=mailinglists,dc=cloudron', 'objectclass=mailGroup', function (error, entries) {
ldapSearch('cn=developers@example.com,ou=mailinglists,dc=cloudron', 'objectclass=mailGroup', function (error, entries) {
if (error) return done(error);
expect(entries.length).to.equal(1);
expect(entries[0].cn).to.equal('devs@example.com');
expect(entries[0].cn).to.equal('developers@example.com');
expect(entries[0].mgrpRFC822MailMember).to.eql([ USER_0.username.toLowerCase() + '@example.com', USER_1.username.toLowerCase() + '@example.com' ]);
done();
});
+6 -6
View File
@@ -31,7 +31,7 @@ function setup(done) {
database.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),
mail.addDomain.bind(null, DOMAIN_0.domain)
mail.add.bind(null, DOMAIN_0.domain)
], done);
}
@@ -48,7 +48,7 @@ describe('Mail', function () {
describe('values', function () {
it('can get default', function (done) {
mail.getDomain(DOMAIN_0.domain, function (error, mailConfig) {
mail.get(DOMAIN_0.domain, function (error, mailConfig) {
expect(error).to.be(null);
expect(mailConfig.enabled).to.be(false);
expect(mailConfig.mailFromValidation).to.be(true);
@@ -62,7 +62,7 @@ describe('Mail', function () {
mail.setMailFromValidation(DOMAIN_0.domain, false, function (error) {
expect(error).to.be(null);
mail.getDomain(DOMAIN_0.domain, function (error, mailConfig) {
mail.get(DOMAIN_0.domain, function (error, mailConfig) {
expect(error).to.be(null);
expect(mailConfig.mailFromValidation).to.be(false);
@@ -75,7 +75,7 @@ describe('Mail', function () {
mail.setCatchAllAddress(DOMAIN_0.domain, [ 'user1', 'user2' ], function (error) {
expect(error).to.be(null);
mail.getDomain(DOMAIN_0.domain, function (error, mailConfig) {
mail.get(DOMAIN_0.domain, function (error, mailConfig) {
expect(error).to.be(null);
expect(mailConfig.catchAll).to.eql([ 'user1', 'user2' ]);
done();
@@ -89,7 +89,7 @@ describe('Mail', function () {
maildb.update(DOMAIN_0.domain, { relay: relay }, function (error) { // skip the mail server verify()
expect(error).to.be(null);
mail.getDomain(DOMAIN_0.domain, function (error, mailConfig) {
mail.get(DOMAIN_0.domain, function (error, mailConfig) {
expect(error).to.be(null);
expect(mailConfig.relay).to.eql(relay);
done();
@@ -101,7 +101,7 @@ describe('Mail', function () {
mail.setMailEnabled(DOMAIN_0.domain, true, function (error) {
expect(error).to.be(null);
mail.getDomain(DOMAIN_0.domain, function (error, mailConfig) {
mail.get(DOMAIN_0.domain, function (error, mailConfig) {
expect(error).to.be(null);
expect(mailConfig.enabled).to.be(true);
done();
+36 -11
View File
@@ -58,12 +58,6 @@ 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-----';
// cp /etc/ssl/openssl.cnf /tmp/openssl.cnf
// echo -e "[SAN]\nsubjectAltName=DNS:amazing.com,DNS:*.amazing.com\n" >> /tmp/openssl.cnf
// openssl req -x509 -newkey rsa:2048 -keyout amazing.key -out amazing.crt -days 3650 -subj /CN=*.amazing.com -nodes -extensions SAN -config /tmp/openssl.cnf
var validCert3 = '-----BEGIN CERTIFICATE-----\nMIIC3DCCAcSgAwIBAgIJALcStAD5sDWEMA0GCSqGSIb3DQEBCwUAMBgxFjAUBgNV\nBAMMDSouYW1hemluZy5jb20wHhcNMTgwMjA5MjIxMzM2WhcNMjgwMjA3MjIxMzM2\nWjAYMRYwFAYDVQQDDA0qLmFtYXppbmcuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOC\nAQ8AMIIBCgKCAQEAvp8dk13u4vmAfKfRNOO8+rVQ8q+vyR8scc9Euj0pTodLBflM\n2K6Zk0isirRzCL/jd4n1A6QrPeJ+r2J4xtHk2j+pavt8Sa2Go2MzpAe3OTuIqYJf\nUt7Im3f2Lb67itTPrpA2TR3A/dDFlazju+eBd3t3496Do8aBPpXAdOabfPsrv3nE\nx97vrr4tzeK3kG9u7GYuod5gyiwF2t5wSeMWbFk2oqkOCtHRXE77JDKVxIGiepnU\nTnkW9b7jIkiBQ1x0xHG4soewV2ymGHS2XrUHZ45FFMG7yVYpytKT9Iz9ty/z5VcL\nZ6NzgU/pKfQaIe8MpoDpVf5UNeB2DOAAEoJKKwIDAQABoykwJzAlBgNVHREEHjAc\nggthbWF6aW5nLmNvbYINKi5hbWF6aW5nLmNvbTANBgkqhkiG9w0BAQsFAAOCAQEA\nMULk6B9XrVPAole8W66o3WUUOrC7NVjbwZjr+Kp5oQTSo84qacaZS2C3ox/j/TZY\nUuNvoE6gIOHi+inN+G4P76K7NEvm8+Y1CeAyaPq01H4Qy2lk9F5wFMtPqvBZnF9C\nx1MvV30FruHXe5pDfnG1npKECpn2SgE3k6FRHM55u8rTMEm/O4TtsDq+fPqUvyWa\nZuRjPv4qVGGkoPyxA6iffxclpOAXs3JUgLcYoM2vxKC0YSOjHEa0p4uffX063Jgg\nybuy3OKvm+8L6moycX7J+LZK81dDTFDtF7PwrnRbpS4re0i/LSk23jDQvDOLnrAa\nSawRR8+1QHTENBo7dnP+NA==\n-----END CERTIFICATE-----';
var validKey3 = '-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC+nx2TXe7i+YB8\np9E047z6tVDyr6/JHyxxz0S6PSlOh0sF+UzYrpmTSKyKtHMIv+N3ifUDpCs94n6v\nYnjG0eTaP6lq+3xJrYajYzOkB7c5O4ipgl9S3sibd/YtvruK1M+ukDZNHcD90MWV\nrOO754F3e3fj3oOjxoE+lcB05pt8+yu/ecTH3u+uvi3N4reQb27sZi6h3mDKLAXa\n3nBJ4xZsWTaiqQ4K0dFcTvskMpXEgaJ6mdROeRb1vuMiSIFDXHTEcbiyh7BXbKYY\ndLZetQdnjkUUwbvJVinK0pP0jP23L/PlVwtno3OBT+kp9Boh7wymgOlV/lQ14HYM\n4AASgkorAgMBAAECggEAdVSVLMcNqlGuv4vAHtDq2lpOaAKxrZbtkWPlxsisqzRl\nfljT7y+RQfHimkG16LXL+iFFWadsIlxOY/+1nZNGTPwQeNQwzVzs2ZbPC3DgW28E\nkGm56NVOHzu4oLGc2DhjWOxVMCRXTSN66sUPK/K0YunxgqXM2zrtBKvCWXI0VLlo\nN/UWAwHf4i0GWRl8u8PvxgMXlSW9p9l6gSsivWRMag9ADwRQ/NSKrRYkiOoRe3vz\nLxXARBvzeZXvOPVLGVRX4SIR7OmS8cC6Ol/rp1/ZFFID7aN+wdzphPSL1UNUriw4\nDv1mxz73SNakgeYSFBoWRS5BsJI01JoCoILsnhVCiQKBgQDyW+k5+j4K17fzwsmi\nyxZ0Nz/ncpkqxVrWYZM3pn7OVkb2NDArimEk53kmJ0hrT84kKJUYDx55R2TpnzpV\nMLmjxgs9TUrzZzsL/DP2ppkfE3OrPS+06OGa5GbURxD6KPvqDtOmU3oFyJ3f4YJR\nVK7RW+zO4sXEpHIxwdBXbYov1QKBgQDJWbt+W5M0sA2D5LrUBNMTvMdNnKH0syc2\nZlcIOdj6HuUIveYpBRq64Jn9VJpXMxQanwE+IUjCpPTa8wF0OA6MZPy6cfovqb8a\ni1/M/lvCoYVS3KHLcTOvTGD3xej0EUj13xWGNu8y3i7Z9/Bl21hEyjd0q0I5OqJx\no9Qa5TGR/wKBgBPfkYpdiMTe14i3ik09FgRFm4nhDcpCEKbPrYC8uF03Ge6KbQDF\nAh5ClN6aDggurRqt8Tvd0YPkZNP7aI8fxbk2PimystiuuFrNPX2WP6warjt2cvkE\nt6s522zAvxWkUrPor1ZONg1PXBLFrSf6J7OnNA3q7oina23FFM52fwRZAoGAZ7l7\nFffU2IKNI9HT0N7/YZ6RSVEUOXuFCsgjs5AhT5BUynERPTZs87I6gb9wltUwWRpq\nSHhbBDJ4FMa0jAtIq1hmvSF0EdOvJ9x+qJqr6JLOnMYd7zDMwFRna5yfigPRgx+9\n9dsc1CaTGiRYyg/5484MTWTgA51KC6Kq5IQHSj8CgYBr9rWgqM8hVCKSt1cMguQV\nTPaV97+u3kV2jFd/aVgDtCDIVvp5TPuqfskE1v3MsSjJ8hfHdYvyxZB8h8T4LlTD\n2HdxwCjVh2qirAvkar2b1mfA6R8msmVaIxBu4MqDcIPqR823klF7A8jSD3MGzYcU\nbnnxMdwgWQkmx0/6/90ZCg==\n-----END PRIVATE KEY-----\n';
it('does not allow empty string for cert', function () {
expect(reverseProxy.validateCertificate('foobar.com', '', 'key')).to.be.an(Error);
});
@@ -108,11 +102,6 @@ describe('Certificates', function () {
it('does not allow invalid cert/key tuple', function () {
expect(reverseProxy.validateCertificate('foobar.com', validCert0, validKey1)).to.be.an(Error);
});
it('picks certificate in SAN', function () {
expect(reverseProxy.validateCertificate('amazing.com', validCert3, validKey3)).to.be(null);
expect(reverseProxy.validateCertificate('subdomain.amazing.com', validCert3, validKey3)).to.be(null);
});
});
describe('getApi - caas', function () {
@@ -144,6 +133,24 @@ describe('Certificates', function () {
done();
});
});
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) {
expect(error).to.be(null);
expect(api._name).to.be('acme');
expect(options.prod).to.be(true);
done();
});
});
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) {
expect(error).to.be(null);
expect(api._name).to.be('acme');
expect(options.prod).to.be(true);
done();
});
});
});
describe('getApi - letsencrypt-prod', function () {
@@ -167,6 +174,15 @@ 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) {
expect(error).to.be(null);
expect(api._name).to.be('acme');
expect(options.prod).to.be(true);
done();
});
});
it('returns prod acme in dev cloudron', function (done) {
reverseProxy._getApi({ domain: DOMAIN_0.domain }, function (error, api, options) {
expect(error).to.be(null);
@@ -206,5 +222,14 @@ describe('Certificates', function () {
done();
});
});
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) {
expect(error).to.be(null);
expect(api._name).to.be('acme');
expect(options.prod).to.be(false);
done();
});
});
});
});
+3 -13
View File
@@ -7,7 +7,6 @@
var async = require('async'),
config = require('../config.js'),
constants = require('../constants.js'),
database = require('../database.js'),
expect = require('expect.js'),
MockS3 = require('mock-aws-s3'),
@@ -68,16 +67,8 @@ describe('Settings', function () {
});
});
it('can get default app_autoupdate_pattern', function (done) {
settings.getAppAutoupdatePattern(function (error, pattern) {
expect(error).to.be(null);
expect(pattern).to.be('00 30 1,3,5,23 * * *');
done();
});
});
it('can get default box_autoupdate_pattern', function (done) {
settings.getBoxAutoupdatePattern(function (error, pattern) {
it('can get default autoupdate_pattern', function (done) {
settings.getAutoupdatePattern(function (error, pattern) {
expect(error).to.be(null);
expect(pattern).to.be('00 00 1,3,5,23 * * *');
done();
@@ -139,8 +130,7 @@ describe('Settings', function () {
settings.getAll(function (error, allSettings) {
expect(error).to.be(null);
expect(allSettings[settings.TIME_ZONE_KEY]).to.be.a('string');
expect(allSettings[settings.APP_AUTOUPDATE_PATTERN_KEY]).to.be.a('string');
expect(allSettings[settings.BOX_AUTOUPDATE_PATTERN_KEY]).to.be.a('string');
expect(allSettings[settings.AUTOUPDATE_PATTERN_KEY]).to.be.a('string');
expect(allSettings[settings.CLOUDRON_NAME_KEY]).to.be.a('string');
done();
});
+1 -1
View File
@@ -16,7 +16,7 @@ mkdir -p boxdata/appicons boxdata/mail boxdata/certs boxdata/mail/dkim/localhost
mkdir -p platformdata/addons/mail platformdata/nginx/cert platformdata/nginx/applications platformdata/collectd/collectd.conf.d platformdata/addons platformdata/logrotate.d platformdata/backup
# put cert
openssl req -x509 -newkey rsa:2048 -keyout platformdata/nginx/cert/host.key -out platformdata/nginx/cert/host.cert -days 3650 -subj '/CN=localhost' -nodes -config <(cat /etc/ssl/openssl.cnf <(printf "\n[SAN]\nsubjectAltName=DNS:*.localhost"))
openssl req -x509 -newkey rsa:2048 -keyout platformdata/nginx/cert/host.key -out platformdata/nginx/cert/host.cert -days 3650 -subj '/CN=localhost' -nodes
# create docker network (while the infra code does this, most tests skip infra setup)
docker network create --subnet=172.18.0.0/16 cloudron || true
+13 -13
View File
@@ -53,9 +53,9 @@ describe('Syncer', function () {
expect(error).to.not.be.ok();
expect(gTasks).to.eql([
{ operation: 'add', path: 'src/index.js', reason: 'new', position: 0 },
{ operation: 'add', path: 'test/test.js', reason: 'new', position: 1 },
{ operation: 'add', path: 'walrus', reason: 'new', position: 2 }
{ operation: 'add', path: 'src/index.js', reason: 'new' },
{ operation: 'add', path: 'test/test.js', reason: 'new' },
{ operation: 'add', path: 'walrus', reason: 'new' }
]);
done();
});
@@ -70,7 +70,7 @@ describe('Syncer', function () {
expect(error).to.not.be.ok();
expect(gTasks).to.eql([
{ operation: 'add', path: 'a/b/c/d/e', reason: 'new', position: 0 }
{ operation: 'add', path: 'a/b/c/d/e', reason: 'new' }
]);
done();
});
@@ -85,7 +85,7 @@ describe('Syncer', function () {
expect(error).to.not.be.ok();
expect(gTasks).to.eql([
{ operation: 'add', path: 'readme', reason: 'new', position: 0 }
{ operation: 'add', path: 'readme', reason: 'new' }
]);
done();
});
@@ -107,8 +107,8 @@ describe('Syncer', function () {
expect(error).to.not.be.ok();
expect(gTasks).to.eql([
{ operation: 'add', path: 'src/index.js', reason: 'changed', position: 0 },
{ operation: 'add', path: 'test/test.js', reason: 'changed', position: 1 }
{ operation: 'add', path: 'src/index.js', reason: 'changed' },
{ operation: 'add', path: 'test/test.js', reason: 'changed' }
]);
done();
@@ -209,7 +209,7 @@ describe('Syncer', function () {
expect(gTasks).to.eql([
{ operation: 'removedir', path: 'a/b', reason: 'missing' },
{ operation: 'add', path: 'a/f', reason: 'new', position: 0 }
{ operation: 'add', path: 'a/f', reason: 'new' }
]);
done();
@@ -234,7 +234,7 @@ describe('Syncer', function () {
expect(gTasks).to.eql([
{ operation: 'remove', path: 'data/test/test.js', reason: 'wasfile' },
{ operation: 'add', path: 'data/test/test.js/trick', reason: 'new', position: 0 }
{ operation: 'add', path: 'data/test/test.js/trick', reason: 'new' }
]);
done();
@@ -259,7 +259,7 @@ describe('Syncer', function () {
expect(gTasks).to.eql([
{ operation: 'removedir', path: 'test', reason: 'wasdir' },
{ operation: 'add', path: 'test', reason: 'wasdir', position: 0 }
{ operation: 'add', path: 'test', reason: 'wasdir' }
]);
done();
@@ -312,9 +312,9 @@ describe('Syncer', function () {
{ operation: 'removedir', path: 'j/l', reason: 'missing' },
{ operation: 'removedir', path: 'j/m', reason: 'missing' },
{ operation: 'add', path: 'a/file', reason: 'new', position: 0 },
{ operation: 'add', path: 'b', reason: 'changed', position: 1 },
{ operation: 'add', path: 'j/k/file', reason: 'new', position: 2 },
{ operation: 'add', path: 'a/file', reason: 'new' },
{ operation: 'add', path: 'b', reason: 'changed' },
{ operation: 'add', path: 'j/k/file', reason: 'new' },
]);
gTasks = [ ];
+8 -10
View File
@@ -76,9 +76,9 @@ describe('updatechecker - box - manual (email)', function () {
database._clear,
settings.initialize,
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0.zoneName, DOMAIN_0.provider, DOMAIN_0.config, DOMAIN_0.fallbackCertificate, DOMAIN_0.tlsConfig),
mail.addDomain.bind(null, DOMAIN_0.domain),
mail.add.bind(null, DOMAIN_0.domain),
user.createOwner.bind(null, USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, AUDIT_SOURCE),
settings.setBoxAutoupdatePattern.bind(null, constants.AUTOUPDATE_PATTERN_NEVER),
settings.setAutoupdatePattern.bind(null, constants.AUTOUPDATE_PATTERN_NEVER),
settingsdb.set.bind(null, settings.APPSTORE_CONFIG_KEY, JSON.stringify({ userId: 'uid', cloudronId: 'cid', token: 'token' })),
mailer._clearMailQueue
], done);
@@ -178,7 +178,7 @@ describe('updatechecker - box - automatic (no email)', function () {
database.initialize,
settings.initialize,
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0.zoneName, DOMAIN_0.provider, DOMAIN_0.config, DOMAIN_0.fallbackCertificate, DOMAIN_0.tlsConfig),
mail.addDomain.bind(null, DOMAIN_0.domain),
mail.add.bind(null, DOMAIN_0.domain),
mailer._clearMailQueue,
user.createOwner.bind(null, USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, AUDIT_SOURCE),
settingsdb.set.bind(null, settings.APPSTORE_CONFIG_KEY, JSON.stringify({ userId: 'uid', cloudronId: 'cid', token: 'token' }))
@@ -222,7 +222,7 @@ describe('updatechecker - box - automatic free (email)', function () {
database.initialize,
settings.initialize,
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0.zoneName, DOMAIN_0.provider, DOMAIN_0.config, DOMAIN_0.fallbackCertificate, DOMAIN_0.tlsConfig),
mail.addDomain.bind(null, DOMAIN_0.domain),
mail.add.bind(null, DOMAIN_0.domain),
mailer._clearMailQueue,
user.createOwner.bind(null, USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, AUDIT_SOURCE),
settingsdb.set.bind(null, settings.APPSTORE_CONFIG_KEY, JSON.stringify({ userId: 'uid', cloudronId: 'cid', token: 'token' }))
@@ -292,11 +292,11 @@ describe('updatechecker - app - manual (email)', function () {
database._clear,
settings.initialize,
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0.zoneName, DOMAIN_0.provider, DOMAIN_0.config, DOMAIN_0.fallbackCertificate, DOMAIN_0.tlsConfig),
mail.addDomain.bind(null, DOMAIN_0.domain),
mail.add.bind(null, DOMAIN_0.domain),
mailer._clearMailQueue,
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, APP_0.portBindings, APP_0),
user.createOwner.bind(null, USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, AUDIT_SOURCE),
settings.setAppAutoupdatePattern.bind(null, constants.AUTOUPDATE_PATTERN_NEVER),
settings.setAutoupdatePattern.bind(null, constants.AUTOUPDATE_PATTERN_NEVER),
settingsdb.set.bind(null, settings.APPSTORE_CONFIG_KEY, JSON.stringify({ userId: 'uid', cloudronId: 'cid', token: 'token' }))
], done);
});
@@ -408,11 +408,10 @@ describe('updatechecker - app - automatic (no email)', function () {
database._clear,
settings.initialize,
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0.zoneName, DOMAIN_0.provider, DOMAIN_0.config, DOMAIN_0.fallbackCertificate, DOMAIN_0.tlsConfig),
mail.addDomain.bind(null, DOMAIN_0.domain),
mail.add.bind(null, DOMAIN_0.domain),
mailer._clearMailQueue,
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, APP_0.portBindings, APP_0),
user.createOwner.bind(null, USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, AUDIT_SOURCE),
settings.setAppAutoupdatePattern.bind(null, '00 00 1,3,5,23 * * *'),
settingsdb.set.bind(null, settings.APPSTORE_CONFIG_KEY, JSON.stringify({ userId: 'uid', cloudronId: 'cid', token: 'token' }))
], done);
});
@@ -474,11 +473,10 @@ describe('updatechecker - app - automatic free (email)', function () {
database._clear,
settings.initialize,
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0.zoneName, DOMAIN_0.provider, DOMAIN_0.config, DOMAIN_0.fallbackCertificate, DOMAIN_0.tlsConfig),
mail.addDomain.bind(null, DOMAIN_0.domain),
mail.add.bind(null, DOMAIN_0.domain),
mailer._clearMailQueue,
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, APP_0.portBindings, APP_0),
user.createOwner.bind(null, USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, AUDIT_SOURCE),
settings.setAppAutoupdatePattern.bind(null, '00 00 1,3,5,23 * * *'),
settingsdb.set.bind(null, settings.APPSTORE_CONFIG_KEY, JSON.stringify({ userId: 'uid', cloudronId: 'cid', token: 'token' }))
], done);
});
+1 -1
View File
@@ -78,7 +78,7 @@ function setup(done) {
database.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),
mail.addDomain.bind(null, DOMAIN_0.domain),
mail.add.bind(null, DOMAIN_0.domain),
mailer._clearMailQueue
], done);
}

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