Compare commits

..

1 Commits

Author SHA1 Message Date
Girish Ramakrishnan 69d9f98190 Get the domain correctly from subject
(cherry picked from commit a3b1a2c781)
2018-11-15 15:19:05 -08:00
107 changed files with 3653 additions and 4288 deletions
+1 -2
View File
@@ -23,7 +23,6 @@
"semi": [
"error",
"always"
],
"no-console": "off"
]
}
}
-10
View File
@@ -1464,13 +1464,3 @@
* Add support for hyphenated subdomains
* Add domain, mail events to eventlog
[3.4.0]
* Improve error page
* Add system view to manage addons and view their status
* Fix iconset regression for account and Cloudron name edits
* Add server reboot button and warn if reboot is required for security updates
* Backup and update tasks are now cancelable
* Move graphite away from port 3000 (reserved by ESXi)
* Flexible mailbox management
* Automatic updates can be toggled per app
+5 -9
View File
@@ -2,17 +2,13 @@
'use strict';
// prefix all output with a timestamp
// debug() already prefixes and uses process.stderr NOT console.*
['log', 'info', 'warn', 'debug', 'error'].forEach(function (log) {
var orig = console[log];
console[log] = function () {
orig.apply(console, [new Date().toISOString()].concat(Array.prototype.slice.call(arguments)));
};
});
require('supererror')({ splatchError: true });
// remove timestamp from debug() based output
require('debug').formatArgs = function formatArgs(args) {
args[0] = this.namespace + ' ' + args[0];
};
let async = require('async'),
config = require('./src/config.js'),
ldap = require('./src/ldap.js'),
@@ -1,7 +1,7 @@
'use strict';
exports.up = function(db, callback) {
var cmd = "CREATE TABLE userGroups(" +
var cmd = "CREATE TABLE groups(" +
"id VARCHAR(128) NOT NULL UNIQUE," +
"name VARCHAR(128) NOT NULL UNIQUE," +
"PRIMARY KEY(id))";
@@ -13,7 +13,7 @@ exports.up = function(db, callback) {
};
exports.down = function(db, callback) {
db.runSql('DROP TABLE userGroups', function (error) {
db.runSql('DROP TABLE groups', function (error) {
if (error) console.error(error);
callback(error);
});
@@ -4,7 +4,7 @@ exports.up = function(db, callback) {
var cmd = "CREATE TABLE IF NOT EXISTS groupMembers(" +
"groupId VARCHAR(128) NOT NULL," +
"userId VARCHAR(128) NOT NULL," +
"FOREIGN KEY(groupId) REFERENCES userGroups(id)," +
"FOREIGN KEY(groupId) REFERENCES groups(id)," +
"FOREIGN KEY(userId) REFERENCES users(id));";
db.runSql(cmd, function (error) {
@@ -7,7 +7,7 @@ var ADMIN_GROUP_ID = 'admin'; // see constants.js
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
db.runSql.bind(db, 'INSERT INTO userGroups (id, name) VALUES (?, ?)', [ ADMIN_GROUP_ID, 'admin' ]),
db.runSql.bind(db, 'INSERT INTO groups (id, name) VALUES (?, ?)', [ ADMIN_GROUP_ID, 'admin' ]),
function migrateAdminFlag(done) {
db.all('SELECT * FROM users WHERE admin=1', function (error, results) {
if (error) return done(error);
@@ -10,7 +10,7 @@ exports.up = function(db, callback) {
function addGroupMailboxes(done) {
console.log('Importing group mailboxes');
db.all('SELECT id, name FROM userGroups', function (error, results) {
db.all('SELECT id, name FROM groups', function (error, results) {
if (error) return done(error);
async.eachSeries(results, function (g, next) {
@@ -16,7 +16,7 @@ exports.up = function(db, callback) {
db.runSql.bind(db, 'ALTER TABLE clients CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
db.runSql.bind(db, 'ALTER TABLE eventlog CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
db.runSql.bind(db, 'ALTER TABLE groupMembers CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
db.runSql.bind(db, 'ALTER TABLE userGroups CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
db.runSql.bind(db, 'ALTER TABLE groups CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
db.runSql.bind(db, 'ALTER TABLE mailboxes CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
db.runSql.bind(db, 'ALTER TABLE migrations CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
db.runSql.bind(db, 'ALTER TABLE settings CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
+1 -1
View File
@@ -29,7 +29,7 @@ exports.up = function(db, callback) {
// this will be finally created once we have a domain when we create the owner in user.js
const ADMIN_GROUP_ID = 'admin'; // see constants.js
db.runSql('DELETE FROM userGroups WHERE id = ?', [ ADMIN_GROUP_ID ], function (error) {
db.runSql('DELETE FROM groups WHERE id = ?', [ ADMIN_GROUP_ID ], function (error) {
if (error) return done(error);
db.runSql('DELETE FROM mailboxes WHERE ownerId = ?', [ ADMIN_GROUP_ID ], done);
@@ -19,8 +19,8 @@ exports.up = function(db, callback) {
},
function getGroups(done) {
db.all('SELECT id, name, GROUP_CONCAT(groupMembers.userId) AS userIds ' +
' FROM userGroups LEFT OUTER JOIN groupMembers ON userGroups.id = groupMembers.groupId ' +
' GROUP BY userGroups.id', [ ], function (error, results) {
' 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) {
+1 -1
View File
@@ -18,7 +18,7 @@ exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'DELETE FROM groupMembers WHERE groupId=?', [ 'admin' ]),
db.runSql.bind(db, 'DELETE FROM userGroups WHERE id=?', [ 'admin' ])
db.runSql.bind(db, 'DELETE FROM groups WHERE id=?', [ 'admin' ])
], callback);
});
});
@@ -1,27 +0,0 @@
'use strict';
exports.up = function(db, callback) {
var cmd = 'CREATE TABLE tasks(' +
'id int NOT NULL AUTO_INCREMENT,' +
'type VARCHAR(32) NOT NULL,' +
'argsJson TEXT,' +
'percent INTEGER DEFAULT 0,' +
'message TEXT,' +
'errorMessage TEXT,' +
'result TEXT,' +
'creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,' +
'ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,' +
'PRIMARY KEY (id))';
db.runSql(cmd, function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('DROP TABLE tasks', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,17 +0,0 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('SELECT 1 FROM groups LIMIT 1', function (error) {
if (error) return callback(); // groups table does not exist
db.runSql('RENAME TABLE groups TO userGroups', function (error) {
if (error) console.error(error);
callback(error);
});
});
};
exports.down = function(db, callback) {
// this is a one way renaming since the previous migration steps have been already updated to match the new name
callback();
};
@@ -1,17 +0,0 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'ALTER TABLE apps MODIFY creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP'),
db.runSql.bind(db, 'ALTER TABLE apps MODIFY updateTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP'),
db.runSql.bind(db, 'ALTER TABLE eventlog MODIFY creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP'),
db.runSql.bind(db, 'ALTER TABLE backups MODIFY creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP'),
db.runSql.bind(db, 'ALTER TABLE mailboxes MODIFY creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP'),
], callback);
};
exports.down = function(db, callback) {
callback();
};
@@ -1,28 +0,0 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'ALTER TABLE apps ADD COLUMN mailboxName VARCHAR(128)'),
db.runSql.bind(db, 'START TRANSACTION;'),
function migrateMailboxNames(done) {
db.all('SELECT * FROM mailboxes', function (error, mailboxes) {
if (error) return done(error);
async.eachSeries(mailboxes, function (mailbox, iteratorDone) {
if (mailbox.ownerType !== 'app') return iteratorDone();
db.runSql('UPDATE apps SET mailboxName = ? WHERE id = ?', [ mailbox.name, mailbox.ownerId ], iteratorDone);
}, done);
});
},
db.runSql.bind(db, 'COMMIT')
], callback);
};
exports.down = function(db, callback) {
callback();
};
@@ -1,28 +0,0 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
function migrateMailboxNames(done) {
db.all('SELECT * FROM mailboxes', function (error, mailboxes) {
if (error) return done(error);
async.eachSeries(mailboxes, function (mailbox, iteratorDone) {
if (mailbox.ownerType !== 'app') return iteratorDone();
db.runSql('DELETE FROM mailboxes WHERE name = ?', [ mailbox.name ], iteratorDone);
}, done);
});
},
db.runSql.bind(db, 'COMMIT'),
db.runSql.bind(db, 'ALTER TABLE mailboxes DROP COLUMN ownerType')
], callback);
};
exports.down = function(db, callback) {
callback();
};
@@ -1,16 +0,0 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN enableAutomaticUpdate BOOLEAN DEFAULT 1', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps DROP COLUMN enableAutomaticUpdate', function (error) {
if (error) console.error(error);
callback(error);
});
};
+10 -26
View File
@@ -29,7 +29,7 @@ CREATE TABLE IF NOT EXISTS users(
PRIMARY KEY(id));
CREATE TABLE IF NOT EXISTS userGroups(
CREATE TABLE IF NOT EXISTS groups(
id VARCHAR(128) NOT NULL UNIQUE,
name VARCHAR(254) NOT NULL UNIQUE,
PRIMARY KEY(id));
@@ -37,7 +37,7 @@ CREATE TABLE IF NOT EXISTS userGroups(
CREATE TABLE IF NOT EXISTS groupMembers(
groupId VARCHAR(128) NOT NULL,
userId VARCHAR(128) NOT NULL,
FOREIGN KEY(groupId) REFERENCES userGroups(id),
FOREIGN KEY(groupId) REFERENCES groups(id),
FOREIGN KEY(userId) REFERENCES users(id));
CREATE TABLE IF NOT EXISTS tokens(
@@ -71,8 +71,8 @@ CREATE TABLE IF NOT EXISTS apps(
location VARCHAR(128) NOT NULL,
domain VARCHAR(128) NOT NULL,
accessRestrictionJson TEXT, // { users: [ ], groups: [ ] }
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, // when the app was installed
updateTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, // when the last app update was done
creationTime TIMESTAMP(2) NOT NULL DEFAULT CURRENT_TIMESTAMP, // when the app was installed
updateTime TIMESTAMP(2) NOT NULL DEFAULT CURRENT_TIMESTAMP, // when the last app update was done
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, // when this db record was updated (useful for UI caching)
memoryLimit BIGINT DEFAULT 0,
xFrameOptions VARCHAR(512),
@@ -80,8 +80,6 @@ CREATE TABLE IF NOT EXISTS apps(
debugModeJson TEXT, // options for development mode
robotsTxt TEXT,
enableBackup BOOLEAN DEFAULT 1, // misnomer: controls automatic daily backups
enableAutomaticUpdate BOOLEAN DEFAULT 1,
mailboxName VARCHAR(128), // mailbox of this app
// 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
@@ -128,7 +126,7 @@ CREATE TABLE IF NOT EXISTS appEnvVars(
CREATE TABLE IF NOT EXISTS backups(
id VARCHAR(128) NOT NULL,
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
creationTime TIMESTAMP,
version VARCHAR(128) NOT NULL, /* app version or box version */
type VARCHAR(16) NOT NULL, /* 'box' or 'app' */
dependsOn TEXT, /* comma separate list of objects this backup depends on */
@@ -143,7 +141,7 @@ 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 */
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
createdAt TIMESTAMP(2) NOT NULL,
PRIMARY KEY (id));
@@ -175,17 +173,15 @@ CREATE TABLE IF NOT EXISTS mail(
/* Future fields:
* accessRestriction - to determine who can access it. So this has foreign keys
* quota - per mailbox quota
NOTE: this table exists only real mailboxes. And has unique constraint to handle
conflict with aliases and mailbox names
*/
CREATE TABLE IF NOT EXISTS mailboxes(
name VARCHAR(128) NOT NULL,
type VARCHAR(16) NOT NULL, /* 'mailbox', 'alias', 'list' */
ownerId VARCHAR(128) NOT NULL, /* user id */
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 NOT NULL DEFAULT CURRENT_TIMESTAMP,
creationTime TIMESTAMP,
domain VARCHAR(128),
FOREIGN KEY(domain) REFERENCES mail(domain),
@@ -199,18 +195,6 @@ CREATE TABLE IF NOT EXISTS subdomains(
FOREIGN KEY(domain) REFERENCES domains(domain),
FOREIGN KEY(appId) REFERENCES apps(id),
UNIQUE (subdomain, domain));
CREATE TABLE IF NOT EXISTS tasks(
id int NOT NULL AUTO_INCREMENT,
type VARCHAR(32) NOT NULL,
percent INTEGER DEFAULT 0,
message TEXT,
errorMessage TEXT,
result TEXT,
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id));
UNIQUE (subdomain, domain))
CHARACTER SET utf8 COLLATE utf8_bin;
+1473 -1640
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -27,7 +27,7 @@
"connect-timeout": "^1.9.0",
"cookie-parser": "^1.3.5",
"cookie-session": "^1.3.2",
"cron": "^1.5.1",
"cron": "^1.3.0",
"csurf": "^1.6.6",
"db-migrate": "^0.11.1",
"db-migrate-mysql": "^1.1.10",
@@ -92,7 +92,7 @@
"scripts": {
"migrate_local": "DATABASE_URL=mysql://root:@localhost/box node_modules/.bin/db-migrate up",
"migrate_test": "BOX_ENV=test DATABASE_URL=mysql://root:@localhost/boxtest node_modules/.bin/db-migrate up",
"test": "npm run migrate_test && src/test/setupTest && BOX_ENV=test ./node_modules/istanbul/lib/cli.js test $1 ./node_modules/mocha/bin/_mocha -- --no-timeouts --exit -R spec ./src/test ./src/routes/test",
"test": "npm run migrate_test && src/test/setupTest && BOX_ENV=test ./node_modules/istanbul/lib/cli.js test $1 ./node_modules/mocha/bin/_mocha -- --exit -R spec ./src/test ./src/routes/test",
"postmerge": "/bin/true",
"precommit": "/bin/true",
"prepush": "npm test",
+1 -1
View File
@@ -2,7 +2,7 @@
set -eu -o pipefail
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400 --http1.1"
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400"
ip=""
dns_config=""
+6 -18
View File
@@ -94,7 +94,7 @@ echo "Running cloudron-setup with args : $@" > "${LOG_FILE}"
# validate arguments in the absence of data
if [[ -z "${provider}" ]]; then
echo "--provider is required (azure, digitalocean, ec2, exoscale, gce, hetzner, lightsail, linode, netcup, ovh, rosehosting, scaleway, vultr or generic)"
echo "--provider is required (azure, digitalocean, ec2, exoscale, gce, hetzner, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic)"
exit 1
elif [[ \
"${provider}" != "ami" && \
@@ -110,14 +110,13 @@ elif [[ \
"${provider}" != "hetzner" && \
"${provider}" != "lightsail" && \
"${provider}" != "linode" && \
"${provider}" != "netcup" && \
"${provider}" != "ovh" && \
"${provider}" != "rosehosting" && \
"${provider}" != "scaleway" && \
"${provider}" != "vultr" && \
"${provider}" != "generic" \
]]; then
echo "--provider must be one of: azure, cloudscale.ch, digitalocean, ec2, exoscale, galaxygate, gce, hetzner, lightsail, linode, netcup, ovh, rosehosting, scaleway, vultr or generic"
echo "--provider must be one of: azure, cloudscale.ch, digitalocean, ec2, exoscale, galaxygate, gce, hetzner, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic"
exit 1
fi
@@ -138,12 +137,6 @@ echo " Join us at https://forum.cloudron.io for any questions."
echo ""
if [[ "${initBaseImage}" == "true" ]]; then
echo "=> Ensure required apt sources"
if ! add-apt-repository universe &>> "${LOG_FILE}"; then
echo "Could not add required apt sources (for nginx-full). See ${LOG_FILE}"
exit 1
fi
echo "=> Updating apt and installing script dependencies"
if ! apt-get update &>> "${LOG_FILE}"; then
echo "Could not update package repositories. See ${LOG_FILE}"
@@ -238,15 +231,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}\n"
echo -e "\n\n${GREEN}Visit https://<IP> and accept the self-signed certificate to finish setup.${DONE}"
if [[ "${rebootServer}" == "true" ]]; then
systemctl stop box mysql # sometimes mysql ends up having corrupt privilege tables
read -p "This server has to rebooted to apply all the settings. Reboot now ? [Y/n] " yn
yn=${yn:-y}
case $yn in
[Yy]* ) systemctl reboot;;
* ) exit;;
esac
echo -e "\n${RED}Rebooting this server now to let changes take effect.${DONE}\n"
systemctl stop mysql # sometimes mysql ends up having corrupt privilege tables
systemctl reboot
fi
+6 -32
View File
@@ -7,13 +7,6 @@ PASTEBIN="https://paste.cloudron.io"
OUT="/tmp/cloudron-support.log"
LINE="\n========================================================\n"
CLOUDRON_SUPPORT_PUBLIC_KEY="ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDQVilclYAIu+ioDp/sgzzFz6YU0hPcRYY7ze/LiF/lC7uQqK062O54BFXTvQ3ehtFZCx3bNckjlT2e6gB8Qq07OM66De4/S/g+HJW4TReY2ppSPMVNag0TNGxDzVH8pPHOysAm33LqT2b6L/wEXwC6zWFXhOhHjcMqXvi8Ejaj20H1HVVcf/j8qs5Thkp9nAaFTgQTPu8pgwD8wDeYX1hc9d0PYGesTADvo6HF4hLEoEnefLw7PaStEbzk2fD3j7/g5r5HcgQQXBe74xYZ/1gWOX2pFNuRYOBSEIrNfJEjFJsqk3NR1+ZoMGK7j+AZBR4k0xbrmncQLcQzl6MMDzkp support@cloudron.io"
HELP_MESSAGE="
This script collects diagnostic information to help debug server related issues
Options:
--enable-ssh Enable SSH access for the Cloudron support team
--help Show this message
"
# We require root
if [[ ${EUID} -ne 0 ]]; then
@@ -21,20 +14,6 @@ if [[ ${EUID} -ne 0 ]]; then
exit 1
fi
enableSSH="false"
args=$(getopt -o "" -l "help,enable-ssh" -n "$0" -- "$@")
eval set -- "${args}"
while true; do
case "$1" in
--help) echo -e "${HELP_MESSAGE}"; exit 0;;
--enable-ssh) enableSSH="true"; shift;;
--) break;;
*) echo "Unknown option $1"; exit 1;;
esac
done
# check if at least 10mb root partition space is available
if [[ "`df --output="avail" / | sed -n 2p`" -lt "10240" ]]; then
echo "No more space left on /"
@@ -87,9 +66,6 @@ df -h &>> $OUT
echo -e $LINE"System daemon status"$LINE >> $OUT
systemctl status --lines=100 cloudron.target box mysql unbound cloudron-syslog nginx collectd docker &>> $OUT
echo -e $LINE"Box logs"$LINE >> $OUT
tail -n 100 /home/yellowtent/platformdata/logs/box.log &>> $OUT
echo -e $LINE"Firewall chains"$LINE >> $OUT
iptables -L &>> $OUT
@@ -100,14 +76,12 @@ echo -n "Uploading information..."
paste_key=$(curl -X POST ${PASTEBIN}/documents --silent -d "$(cat $OUT)" | python3 -c "import sys, json; print(json.load(sys.stdin)['key'])")
echo "Done"
if [[ "${enableSSH}" == "true" ]]; then
echo -n "Enabling ssh access for the Cloudron support team..."
mkdir -p "${ssh_folder}"
echo "${CLOUDRON_SUPPORT_PUBLIC_KEY}" >> ${authorized_key_file}
chown -R ${ssh_user} "${ssh_folder}"
chmod 600 "${authorized_key_file}"
echo "Done"
fi
echo -n "Enabling ssh access for the Cloudron support team..."
mkdir -p "${ssh_folder}"
echo "${CLOUDRON_SUPPORT_PUBLIC_KEY}" >> ${authorized_key_file}
chown -R ${ssh_user} "${ssh_folder}"
chmod 600 "${authorized_key_file}"
echo "Done"
echo ""
echo "Please email the following link to support@cloudron.io"
+2 -8
View File
@@ -86,14 +86,8 @@ images=$(node -e "var i = require('${box_src_tmp_dir}/src/infra_version.js'); co
echo -e "\tPulling docker images: ${images}"
for image in ${images}; do
if ! docker pull "${image}"; then # this pulls the image using the sha256
echo "==> installer: Could not pull ${image}"
exit 5
fi
if ! docker pull "${image%@sha256:*}"; then # this will tag the image for readability
echo "==> installer: Could not pull ${image%@sha256:*}"
exit 6
fi
docker pull "${image}" # this pulls the image using the sha256
docker pull "${image%@sha256:*}" # this will tag the image for readability
done
echo "==> installer: update cloudron-syslog"
+10 -7
View File
@@ -16,7 +16,7 @@ readonly BOX_DATA_DIR="${HOME_DIR}/boxdata" # box data
readonly CONFIG_DIR="${HOME_DIR}/configs"
readonly script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly json="$(realpath ${script_dir}/../node_modules/.bin/json)"
readonly get_config="$(realpath ${script_dir}/../node_modules/.bin/json) -f /etc/cloudron/cloudron.conf"
echo "==> Configuring docker"
cp "${script_dir}/start/docker-cloudron-app.apparmor" /etc/apparmor.d/docker-cloudron-app
@@ -58,7 +58,7 @@ mkdir -p "${PLATFORM_DATA_DIR}/collectd/collectd.conf.d"
mkdir -p "${PLATFORM_DATA_DIR}/logrotate.d"
mkdir -p "${PLATFORM_DATA_DIR}/acme"
mkdir -p "${PLATFORM_DATA_DIR}/backup"
mkdir -p "${PLATFORM_DATA_DIR}/logs/backup" "${PLATFORM_DATA_DIR}/logs/updater" "${PLATFORM_DATA_DIR}/logs/tasks"
mkdir -p "${PLATFORM_DATA_DIR}/logs/backup"
mkdir -p "${PLATFORM_DATA_DIR}/update"
mkdir -p "${BOX_DATA_DIR}/appicons"
@@ -91,9 +91,12 @@ setfacl -n -m u:${USER}:r /var/log/journal/*/system.journal
echo "==> Creating config directory"
mkdir -p "${CONFIG_DIR}"
# remove old cloudron.conf. Can be removed after 3.4
rm -f "${CONFIG_DIR}/cloudron.conf"
$json -f /etc/cloudron/cloudron.conf -I -e "delete this.version" # remove the version field
# migration for cloudron.conf file. Can be removed after 3.3
if [[ ! -d /etc/cloudron ]]; then
echo "==> Migrating existing cloudron.conf to new location"
mkdir -p /etc/cloudron
cp "${CONFIG_DIR}/cloudron.conf" /etc/cloudron/cloudron.conf
fi
chown -R "${USER}" /etc/cloudron
echo "==> Setting up unbound"
@@ -139,8 +142,8 @@ echo "==> Configuring logrotate"
if ! grep -q "^include ${PLATFORM_DATA_DIR}/logrotate.d" /etc/logrotate.conf; then
echo -e "\ninclude ${PLATFORM_DATA_DIR}/logrotate.d\n" >> /etc/logrotate.conf
fi
cp "${script_dir}/start/box-logrotate" "${script_dir}/start/app-logrotate" "${PLATFORM_DATA_DIR}/logrotate.d/"
chown root:root "${PLATFORM_DATA_DIR}/logrotate.d/box-logrotate" "${PLATFORM_DATA_DIR}/logrotate.d/app-logrotate"
cp "${script_dir}/start/app-logrotate" "${PLATFORM_DATA_DIR}/logrotate.d/app-logrotate"
chown root:root "${PLATFORM_DATA_DIR}/logrotate.d/app-logrotate"
echo "==> Adding motd message for admins"
cp "${script_dir}/start/cloudron-motd" /etc/update-motd.d/92-cloudron
-9
View File
@@ -1,9 +0,0 @@
# logrotate config for box logs
/home/yellowtent/platformdata/logs/box.log {
rotate 10
size 10M
# we never compress so we can simply tail the files
nocompress
copytruncate
}
+1 -1
View File
@@ -162,7 +162,7 @@ server {
# graphite paths (uncomment block below and visit /graphite/index.html)
# remember to comment out the CSP policy as well to access the graphite dashboard
# location ~ ^/(graphite|content|metrics|dashboard|render|browser|composer)/ {
# proxy_pass http://127.0.0.1:8417;
# proxy_pass http://127.0.0.1:8000;
# client_max_body_size 1m;
# }
+4 -10
View File
@@ -4,8 +4,8 @@ Defaults !syslog
Defaults!/home/yellowtent/box/src/scripts/rmvolume.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/rmvolume.sh
Defaults!/home/yellowtent/box/src/scripts/rmaddondir.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/rmaddondir.sh
Defaults!/home/yellowtent/box/src/scripts/rmaddon.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/rmaddon.sh
Defaults!/home/yellowtent/box/src/scripts/reloadnginx.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/reloadnginx.sh
@@ -31,15 +31,9 @@ yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/authorized_keys
Defaults!/home/yellowtent/box/src/scripts/configurelogrotate.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/configurelogrotate.sh
Defaults!/home/yellowtent/box/src/scripts/backupupload.js env_keep="HOME BOX_ENV"
Defaults!/home/yellowtent/box/src/scripts/backupupload.js closefrom_override
yellowtent ALL=(root) NOPASSWD:SETENV: /home/yellowtent/box/src/scripts/backupupload.js
Defaults!/home/yellowtent/box/src/backuptask.js env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD:SETENV: /home/yellowtent/box/src/backuptask.js
Defaults!/home/yellowtent/box/src/scripts/restart.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/restart.sh
Defaults!/home/yellowtent/box/src/scripts/restartdocker.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/restartdocker.sh
Defaults!/home/yellowtent/box/src/scripts/restartunbound.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/restartunbound.sh
+1 -2
View File
@@ -12,8 +12,7 @@ Wants=cloudron-resize-fs.service
Type=idle
WorkingDirectory=/home/yellowtent/box
Restart=always
; Systemd does not append logs when logging to files, we spawn a shell first and exec to replace it after setting up the pipes
ExecStart=/bin/sh -c 'echo "Logging to /home/yellowtent/platformdata/logs/box.log"; exec /usr/bin/node --max_old_space_size=150 /home/yellowtent/box/box.js >> /home/yellowtent/platformdata/logs/box.log 2>&1'
ExecStart=/usr/bin/node --max_old_space_size=150 /home/yellowtent/box/box.js
Environment="HOME=/home/yellowtent" "USER=yellowtent" "DEBUG=box*,connect-lastmile" "BOX_ENV=cloudron" "NODE_ENV=production"
; kill apptask processes as well
KillMode=control-group
+180 -469
View File
@@ -1,16 +1,8 @@
'use strict';
exports = module.exports = {
AddonsError: AddonsError,
getServices: getServices,
getService: getService,
configureService: configureService,
getServiceLogs: getServiceLogs,
restartService: restartService,
startServices: startServices,
updateServiceConfig: updateServiceConfig,
startAddons: startAddons,
updateAddonConfig: updateAddonConfig,
setupAddons: setupAddons,
teardownAddons: teardownAddons,
@@ -24,11 +16,7 @@ exports = module.exports = {
// exported for testing
_setupOauth: setupOauth,
_teardownOauth: teardownOauth,
SERVICE_STATUS_STARTING: 'starting', // container up, waiting for healthcheck
SERVICE_STATUS_ACTIVE: 'active',
SERVICE_STATUS_STOPPED: 'stopped'
_teardownOauth: teardownOauth
};
var accesscontrol = require('./accesscontrol.js'),
@@ -43,11 +31,11 @@ var accesscontrol = require('./accesscontrol.js'),
debug = require('debug')('box:addons'),
docker = require('./docker.js'),
dockerConnection = docker.connection,
DockerError = docker.DockerError,
fs = require('fs'),
hat = require('./hat.js'),
infra = require('./infra_version.js'),
mail = require('./mail.js'),
mailboxdb = require('./mailboxdb.js'),
once = require('once'),
os = require('os'),
path = require('path'),
@@ -55,41 +43,13 @@ var accesscontrol = require('./accesscontrol.js'),
rimraf = require('rimraf'),
safe = require('safetydance'),
semver = require('semver'),
settings = require('./settings.js'),
shell = require('./shell.js'),
spawn = require('child_process').spawn,
split = require('split'),
request = require('request'),
util = require('util');
// http://dustinsenos.com/articles/customErrorsInNode
// http://code.google.com/p/v8/wiki/JavaScriptStackTraceApi
function AddonsError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
Error.call(this);
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.reason = reason;
if (typeof errorOrMessage === 'undefined') {
this.message = reason;
} else if (typeof errorOrMessage === 'string') {
this.message = errorOrMessage;
} else {
this.message = 'Internal error';
this.nestedError = errorOrMessage;
}
}
util.inherits(AddonsError, Error);
AddonsError.INTERNAL_ERROR = 'Internal Error';
AddonsError.NOT_FOUND = 'Not Found';
AddonsError.NOT_ACTIVE = 'Not Active';
const NOOP = function (app, options, callback) { return callback(); };
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
const RMADDONDIR_CMD = path.join(__dirname, 'scripts/rmaddondir.sh');
const RMADDON_CMD = path.join(__dirname, 'scripts/rmaddon.sh');
// setup can be called multiple times for the same app (configure crash restart) and existing data must not be lost
// teardown is destructive. app data stored with the addon is lost
@@ -99,117 +59,84 @@ var KNOWN_ADDONS = {
teardown: teardownEmail,
backup: NOOP,
restore: setupEmail,
clear: NOOP,
clear: NOOP
},
ldap: {
setup: setupLdap,
teardown: teardownLdap,
backup: NOOP,
restore: setupLdap,
clear: NOOP,
clear: NOOP
},
localstorage: {
setup: setupLocalStorage, // docker creates the directory for us
teardown: teardownLocalStorage,
backup: NOOP, // no backup because it's already inside app data
restore: NOOP,
clear: clearLocalStorage,
clear: clearLocalStorage
},
mongodb: {
setup: setupMongoDb,
teardown: teardownMongoDb,
backup: backupMongoDb,
restore: restoreMongoDb,
clear: clearMongodb,
clear: clearMongodb
},
mysql: {
setup: setupMySql,
teardown: teardownMySql,
backup: backupMySql,
restore: restoreMySql,
clear: clearMySql,
clear: clearMySql
},
oauth: {
setup: setupOauth,
teardown: teardownOauth,
backup: NOOP,
restore: setupOauth,
clear: NOOP,
clear: NOOP
},
postgresql: {
setup: setupPostgreSql,
teardown: teardownPostgreSql,
backup: backupPostgreSql,
restore: restorePostgreSql,
clear: clearPostgreSql,
clear: clearPostgreSql
},
recvmail: {
setup: setupRecvMail,
teardown: teardownRecvMail,
backup: NOOP,
restore: setupRecvMail,
clear: NOOP,
clear: NOOP
},
redis: {
setup: setupRedis,
teardown: teardownRedis,
backup: backupRedis,
restore: restoreRedis,
clear: clearRedis,
clear: clearRedis
},
sendmail: {
setup: setupSendMail,
teardown: teardownSendMail,
backup: NOOP,
restore: setupSendMail,
clear: NOOP,
clear: NOOP
},
scheduler: {
setup: NOOP,
teardown: NOOP,
backup: NOOP,
restore: NOOP,
clear: NOOP,
clear: NOOP
},
docker: {
setup: NOOP,
teardown: NOOP,
backup: NOOP,
restore: NOOP,
clear: NOOP,
}
};
const KNOWN_SERVICES = {
mail: {
status: containerStatus.bind(null, 'mail', 'CLOUDRON_MAIL_TOKEN'),
restart: restartContainer.bind(null, 'mail'),
defaultMemoryLimit: Math.max((1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 128, 256) * 1024 * 1024
},
mongodb: {
status: containerStatus.bind(null, 'mongodb', 'CLOUDRON_MONGODB_TOKEN'),
restart: restartContainer.bind(null, 'mongodb'),
defaultMemoryLimit: (1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 200 * 1024 * 1024
},
mysql: {
status: containerStatus.bind(null, 'mysql', 'CLOUDRON_MYSQL_TOKEN'),
restart: restartContainer.bind(null, 'mysql'),
defaultMemoryLimit: (1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 256 * 1024 * 1024
},
postgresql: {
status: containerStatus.bind(null, 'postgresql', 'CLOUDRON_POSTGRESQL_TOKEN'),
restart: restartContainer.bind(null, 'postgresql'),
defaultMemoryLimit: (1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 256 * 1024 * 1024
},
docker: {
status: statusDocker,
restart: restartDocker,
defaultMemoryLimit: 0
},
unbound: {
status: statusUnbound,
restart: restartUnbound,
defaultMemoryLimit: 0
clear: NOOP
}
};
@@ -243,233 +170,41 @@ function dumpPath(addon, appId) {
}
}
function restartContainer(serviceName, callback) {
assert.strictEqual(typeof serviceName, 'string');
assert.strictEqual(typeof callback, 'function');
assert(KNOWN_SERVICES[serviceName], `Unknown service ${serviceName}`);
docker.stopContainer(serviceName, function (error) {
if (error) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error));
docker.startContainer(serviceName, function (error) {
if (error) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error));
callback(null);
});
});
}
function containerStatus(addonName, addonTokenName, callback) {
assert.strictEqual(typeof addonName, 'string');
assert.strictEqual(typeof addonTokenName, 'string');
assert.strictEqual(typeof callback, 'function');
getServiceDetails(addonName, addonTokenName, function (error, addonDetails) {
if (error && error.reason === AddonsError.NOT_ACTIVE) return callback(null, { status: exports.SERVICE_STATUS_STOPPED });
if (error) return callback(error);
request.get(`https://${addonDetails.ip}:3000/healthcheck?access_token=${addonDetails.token}`, { json: true, rejectUnauthorized: false }, function (error, response) {
if (error) return callback(null, { status: exports.SERVICE_STATUS_STARTING, error: `Error waiting for ${addonName}: ${error.message}` });
if (response.statusCode !== 200 || !response.body.status) return callback(null, { status: exports.SERVICE_STATUS_STARTING, error: `Error waiting for ${addonName}. Status code: ${response.statusCode} message: ${response.body.message}` });
docker.memoryUsage(addonName, function (error, result) {
if (error) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error));
var tmp = {
status: addonDetails.state.Running ? exports.SERVICE_STATUS_ACTIVE : exports.SERVICE_STATUS_STOPPED,
memoryUsed: result.memory_stats.usage,
memoryPercent: parseInt(100 * result.memory_stats.usage / result.memory_stats.limit)
};
callback(null, tmp);
});
});
});
}
function getServices(callback) {
assert.strictEqual(typeof callback, 'function');
let services = Object.keys(KNOWN_SERVICES);
callback(null, services);
}
function getService(serviceName, callback) {
assert.strictEqual(typeof serviceName, 'string');
assert.strictEqual(typeof callback, 'function');
if (!KNOWN_SERVICES[serviceName]) return callback(new AddonsError(AddonsError.NOT_FOUND));
var tmp = {
name: serviceName,
status: null,
config: {
// If a property is not set then we cannot change it through the api, see below
// memory: 0,
// memorySwap: 0
}
};
settings.getPlatformConfig(function (error, platformConfig) {
if (error) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error));
if (platformConfig[serviceName] && platformConfig[serviceName].memory && platformConfig[serviceName].memorySwap) {
tmp.config.memory = platformConfig[serviceName].memory;
tmp.config.memorySwap = platformConfig[serviceName].memorySwap;
} else if (KNOWN_SERVICES[serviceName].defaultMemoryLimit) {
tmp.config.memory = KNOWN_SERVICES[serviceName].defaultMemoryLimit;
tmp.config.memorySwap = tmp.config.memory * 2;
}
KNOWN_SERVICES[serviceName].status(function (error, result) {
if (error) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error));
tmp.status = result.status;
tmp.memoryUsed = result.memoryUsed;
tmp.memoryPercent = result.memoryPercent;
tmp.error = result.error || null;
callback(null, tmp);
});
});
}
function configureService(serviceName, data, callback) {
assert.strictEqual(typeof serviceName, 'string');
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof callback, 'function');
if (!KNOWN_SERVICES[serviceName]) return callback(new AddonsError(AddonsError.NOT_FOUND));
settings.getPlatformConfig(function (error, platformConfig) {
if (error) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error));
if (!platformConfig[serviceName]) platformConfig[serviceName] = {};
// if not specified we clear the entry and use defaults
if (!data.memory || !data.memorySwap) {
delete platformConfig[serviceName];
} else {
platformConfig[serviceName].memory = data.memory;
platformConfig[serviceName].memorySwap = data.memorySwap;
}
settings.setPlatformConfig(platformConfig, function (error) {
if (error) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error));
callback(null);
});
});
}
function getServiceLogs(serviceName, options, callback) {
assert.strictEqual(typeof serviceName, 'string');
assert(options && typeof options === 'object');
assert.strictEqual(typeof callback, 'function');
if (!KNOWN_SERVICES[serviceName]) return callback(new AddonsError(AddonsError.NOT_FOUND));
debug(`Getting logs for ${serviceName}`);
var lines = options.lines || 100,
format = options.format || 'json',
follow = !!options.follow;
assert.strictEqual(typeof lines, 'number');
assert.strictEqual(typeof format, 'string');
var cmd;
var args = [ '--lines=' + lines ];
// docker and unbound use journald
if (serviceName === 'docker' || serviceName === 'unbound') {
cmd = 'journalctl';
args.push(`--unit=${serviceName}`);
args.push('--no-pager');
args.push('--output=short-iso');
if (follow) args.push('--follow');
} else {
cmd = '/usr/bin/tail';
if (follow) args.push('--follow', '--retry', '--quiet'); // same as -F. to make it work if file doesn't exist, --quiet to not output file headers, which are no logs
args.push(path.join(paths.LOG_DIR, serviceName, 'app.log'));
}
var cp = spawn(cmd, args);
var transformStream = split(function mapper(line) {
if (format !== 'json') return line + '\n';
var data = line.split(' '); // logs are <ISOtimestamp> <msg>
var timestamp = (new Date(data[0])).getTime();
if (isNaN(timestamp)) timestamp = 0;
var message = line.slice(data[0].length+1);
// ignore faulty empty logs
if (!timestamp && !message) return;
return JSON.stringify({
realtimeTimestamp: timestamp * 1000,
message: message,
source: serviceName
}) + '\n';
});
transformStream.close = cp.kill.bind(cp, 'SIGKILL'); // closing stream kills the child process
cp.stdout.pipe(transformStream);
callback(null, transformStream);
}
function restartService(serviceName, callback) {
assert.strictEqual(typeof serviceName, 'string');
assert.strictEqual(typeof callback, 'function');
if (!KNOWN_SERVICES[serviceName]) return callback(new AddonsError(AddonsError.NOT_FOUND));
KNOWN_SERVICES[serviceName].restart(callback);
}
function getServiceDetails(containerName, tokenEnvName, callback) {
function getAddonDetails(containerName, tokenEnvName, callback) {
assert.strictEqual(typeof containerName, 'string');
assert.strictEqual(typeof tokenEnvName, 'string');
assert.strictEqual(typeof callback, 'function');
docker.inspect(containerName, function (error, result) {
if (error && error.reason === DockerError.NOT_FOUND) return callback(new AddonsError(AddonsError.NOT_ACTIVE, error));
if (error) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error));
var container = dockerConnection.getContainer(containerName);
container.inspect(function (error, result) {
if (error) return callback(new Error(`Error inspecting ${containerName} container: ` + error));
const ip = safe.query(result, 'NetworkSettings.Networks.cloudron.IPAddress', null);
if (!ip) return callback(new AddonsError(AddonsError.NOT_ACTIVE, `Error getting ${containerName} container ip`));
if (!ip) return callback(new Error(`Error getting ${containerName} container ip`));
// extract the cloudron token for auth
const env = safe.query(result, 'Config.Env', null);
if (!env) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, `Error getting ${containerName} env`));
if (!env) return callback(new Error(`Error getting ${containerName} env`));
const tmp = env.find(function (e) { return e.indexOf(tokenEnvName) === 0; });
if (!tmp) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, `Error getting ${containerName} cloudron token env var`));
if (!tmp) return callback(new Error(`Error getting ${containerName} cloudron token env var`));
const token = tmp.slice(tokenEnvName.length + 1); // +1 for the = sign
if (!token) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, `Error getting ${containerName} cloudron token`));
if (!token) return callback(new Error(`Error getting ${containerName} cloudron token`));
callback(null, { ip: ip, token: token, state: result.State });
callback(null, { ip: ip, token: token });
});
}
function waitForService(containerName, tokenEnvName, callback) {
function waitForAddon(containerName, tokenEnvName, callback) {
assert.strictEqual(typeof containerName, 'string');
assert.strictEqual(typeof tokenEnvName, 'string');
assert.strictEqual(typeof callback, 'function');
debug(`Waiting for ${containerName}`);
getServiceDetails(containerName, tokenEnvName, function (error, result) {
getAddonDetails(containerName, tokenEnvName, function (error, result) {
if (error) return callback(error);
async.retry({ times: 10, interval: 15000 }, function (retryCallback) {
async.retry({ times: 10, interval: 5000 }, function (retryCallback) {
request.get(`https://${result.ip}:3000/healthcheck?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) {
if (error) return retryCallback(new Error(`Error waiting for ${containerName}: ${error.message}`));
if (response.statusCode !== 200 || !response.body.status) return retryCallback(new Error(`Error waiting for ${containerName}. Status code: ${response.statusCode} message: ${response.body.message}`));
@@ -608,29 +343,37 @@ function importDatabase(addon, callback) {
});
}
function updateServiceConfig(platformConfig, callback) {
function updateAddonConfig(platformConfig, callback) {
callback = callback || NOOP_CALLBACK;
debug('updateServiceConfig: %j', platformConfig);
// TODO: maybe derive these defaults based on how many apps are using them
const defaultMemoryLimits = {
mysql: (1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 256 * 1024 * 1024,
mongodb: (1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 200 * 1024 * 1024,
postgresql: (1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 256 * 1024 * 1024,
mail: Math.max((1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 128, 256) * 1024 * 1024
};
debug('updateAddonConfig: %j', platformConfig);
// TODO: this should possibly also rollback memory to default
async.eachSeries([ 'mysql', 'postgresql', 'mail', 'mongodb' ], function iterator(serviceName, iteratorCallback) {
const containerConfig = platformConfig[serviceName];
async.eachSeries([ 'mysql', 'postgresql', 'mail', 'mongodb' ], function iterator(containerName, iteratorCallback) {
const containerConfig = platformConfig[containerName];
let memory, memorySwap;
if (containerConfig && containerConfig.memory && containerConfig.memorySwap) {
memory = containerConfig.memory;
memorySwap = containerConfig.memorySwap;
} else {
memory = KNOWN_SERVICES[serviceName].defaultMemoryLimit;
memory = defaultMemoryLimits[containerName];
memorySwap = memory * 2;
}
const args = `update --memory ${memory} --memory-swap ${memorySwap} ${serviceName}`.split(' ');
shell.spawn(`update${serviceName}`, '/usr/bin/docker', args, { }, iteratorCallback);
const args = `update --memory ${memory} --memory-swap ${memorySwap} ${containerName}`.split(' ');
shell.exec(`update${containerName}`, '/usr/bin/docker', args, { }, iteratorCallback);
}, callback);
}
function startServices(existingInfra, callback) {
function startAddons(existingInfra, callback) {
assert.strictEqual(typeof existingInfra, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -638,7 +381,7 @@ function startServices(existingInfra, callback) {
// always start addons on any infra change, regardless of minor or major update
if (existingInfra.version !== infra.version) {
debug(`startServices: ${existingInfra.version} -> ${infra.version}. starting all services`);
debug(`startAddons: ${existingInfra.version} -> ${infra.version}. starting all addons`);
startFuncs.push(
startMysql.bind(null, existingInfra),
startPostgresql.bind(null, existingInfra),
@@ -654,7 +397,7 @@ function startServices(existingInfra, callback) {
if (infra.images.mail.tag !== existingInfra.images.mail.tag) startFuncs.push(mail.startMail);
if (infra.images.redis.tag !== existingInfra.images.redis.tag) startFuncs.push(startRedis.bind(null, existingInfra));
debug('startServices: existing infra. incremental service create %j', startFuncs.map(function (f) { return f.name; }));
debug('startAddons: existing infra. incremental addon create %j', startFuncs.map(function (f) { return f.name; }));
}
async.series(startFuncs, callback);
@@ -878,17 +621,23 @@ function setupSendMail(app, options, callback) {
var password = error ? hat(4 * 48) : existingPassword; // see box#565 for password length
var env = [
{ name: 'MAIL_SMTP_SERVER', value: 'mail' },
{ name: 'MAIL_SMTP_PORT', value: '2525' },
{ name: 'MAIL_SMTPS_PORT', value: '2465' },
{ name: 'MAIL_SMTP_USERNAME', value: app.mailboxName + '@' + app.domain },
{ name: 'MAIL_SMTP_PASSWORD', value: password },
{ name: 'MAIL_FROM', value: app.mailboxName + '@' + app.domain },
{ name: 'MAIL_DOMAIN', value: app.domain }
];
debugApp(app, 'Setting sendmail addon config to %j', env);
appdb.setAddonConfig(app.id, 'sendmail', env, callback);
mailboxdb.getByOwnerId(app.id, function (error, results) {
if (error) return callback(error);
var mailbox = results.filter(function (r) { return !r.aliasTarget; })[0];
var env = [
{ name: 'MAIL_SMTP_SERVER', value: 'mail' },
{ name: 'MAIL_SMTP_PORT', value: '2525' },
{ name: 'MAIL_SMTPS_PORT', value: '2465' },
{ name: 'MAIL_SMTP_USERNAME', value: mailbox.name + '@' + app.domain },
{ name: 'MAIL_SMTP_PASSWORD', value: password },
{ name: 'MAIL_FROM', value: mailbox.name + '@' + app.domain },
{ name: 'MAIL_DOMAIN', value: app.domain }
];
debugApp(app, 'Setting sendmail addon config to %j', env);
appdb.setAddonConfig(app.id, 'sendmail', env, callback);
});
});
}
@@ -914,17 +663,23 @@ function setupRecvMail(app, options, callback) {
var password = error ? hat(4 * 48) : existingPassword; // see box#565 for password length
var env = [
{ name: 'MAIL_IMAP_SERVER', value: 'mail' },
{ name: 'MAIL_IMAP_PORT', value: '9993' },
{ name: 'MAIL_IMAP_USERNAME', value: app.mailboxName + '@' + app.domain },
{ name: 'MAIL_IMAP_PASSWORD', value: password },
{ name: 'MAIL_TO', value: app.mailboxName + '@' + app.domain },
{ name: 'MAIL_DOMAIN', value: app.domain }
];
mailboxdb.getByOwnerId(app.id, function (error, results) {
if (error) return callback(error);
debugApp(app, 'Setting sendmail addon config to %j', env);
appdb.setAddonConfig(app.id, 'recvmail', env, callback);
var mailbox = results.filter(function (r) { return !r.aliasTarget; })[0];
var env = [
{ name: 'MAIL_IMAP_SERVER', value: 'mail' },
{ name: 'MAIL_IMAP_PORT', value: '9993' },
{ name: 'MAIL_IMAP_USERNAME', value: mailbox.name + '@' + app.domain },
{ name: 'MAIL_IMAP_PASSWORD', value: password },
{ name: 'MAIL_TO', value: mailbox.name + '@' + app.domain },
{ name: 'MAIL_DOMAIN', value: app.domain }
];
debugApp(app, 'Setting sendmail addon config to %j', env);
appdb.setAddonConfig(app.id, 'recvmail', env, callback);
});
});
}
@@ -958,40 +713,36 @@ function startMysql(existingInfra, callback) {
const upgrading = existingInfra.version !== 'none' && requiresUpgrade(existingInfra.images.mysql.tag, tag);
if (upgrading) debug('startMysql: mysql will be upgraded');
const upgradeFunc = upgrading ? shell.sudo.bind(null, 'startMysql', [ RMADDONDIR_CMD, 'mysql' ], {}) : (next) => next();
if (upgrading) {
debug('startMysql: mysql will be upgraded');
shell.sudoSync('startMysql', `${RMADDON_CMD} mysql`);
}
upgradeFunc(function (error) {
const cmd = `docker run --restart=always -d --name="mysql" \
--net cloudron \
--net-alias mysql \
--log-driver syslog \
--log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \
--log-opt tag=mysql \
-m ${memoryLimit}m \
--memory-swap ${memoryLimit * 2}m \
--dns 172.18.0.1 \
--dns-search=. \
-e CLOUDRON_MYSQL_TOKEN=${cloudronToken} \
-e CLOUDRON_MYSQL_ROOT_HOST=172.18.0.1 \
-e CLOUDRON_MYSQL_ROOT_PASSWORD=${rootPassword} \
-v "${dataDir}/mysql:/var/lib/mysql" \
--label isCloudronManaged=true \
--read-only -v /tmp -v /run "${tag}"`;
shell.execSync('startMysql', cmd);
waitForAddon('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error) {
if (error) return callback(error);
if (!upgrading) return callback(null);
const cmd = `docker run --restart=always -d --name="mysql" \
--net cloudron \
--net-alias mysql \
--log-driver syslog \
--log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \
--log-opt tag=mysql \
-m ${memoryLimit}m \
--memory-swap ${memoryLimit * 2}m \
--dns 172.18.0.1 \
--dns-search=. \
-e CLOUDRON_MYSQL_TOKEN=${cloudronToken} \
-e CLOUDRON_MYSQL_ROOT_HOST=172.18.0.1 \
-e CLOUDRON_MYSQL_ROOT_PASSWORD=${rootPassword} \
-v "${dataDir}/mysql:/var/lib/mysql" \
--label isCloudronManaged=true \
--read-only -v /tmp -v /run "${tag}"`;
shell.exec('startMysql', cmd, function (error) {
if (error) return callback(error);
waitForService('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error) {
if (error) return callback(error);
if (!upgrading) return callback(null);
importDatabase('mysql', callback);
});
});
importDatabase('mysql', callback);
});
}
@@ -1014,7 +765,7 @@ function setupMySql(app, options, callback) {
password: error ? hat(4 * 48) : existingPassword // see box#362 for password length
};
getServiceDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
getAddonDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
if (error) return callback(error);
request.post(`https://${result.ip}:3000/` + (options.multipleDatabases ? 'prefixes' : 'databases') + `?access_token=${result.token}`, { rejectUnauthorized: false, json: data }, function (error, response) {
@@ -1051,7 +802,7 @@ function clearMySql(app, options, callback) {
const database = mysqlDatabaseName(app.id);
getServiceDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
getAddonDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
if (error) return callback(error);
request.post(`https://${result.ip}:3000/` + (options.multipleDatabases ? 'prefixes' : 'databases') + `/${database}/clear?access_token=${result.token}`, { rejectUnauthorized: false }, function (error, response) {
@@ -1070,7 +821,7 @@ function teardownMySql(app, options, callback) {
const database = mysqlDatabaseName(app.id);
const username = database;
getServiceDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
getAddonDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
if (error) return callback(error);
request.delete(`https://${result.ip}:3000/` + (options.multipleDatabases ? 'prefixes' : 'databases') + `/${database}?access_token=${result.token}&username=${username}`, { rejectUnauthorized: false }, function (error, response) {
@@ -1117,7 +868,7 @@ function backupMySql(app, options, callback) {
debugApp(app, 'Backing up mysql');
getServiceDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
getAddonDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
if (error) return callback(error);
const url = `https://${result.ip}:3000/` + (options.multipleDatabases ? 'prefixes' : 'databases') + `/${database}/backup?access_token=${result.token}`;
@@ -1136,7 +887,7 @@ function restoreMySql(app, options, callback) {
callback = once(callback); // protect from multiple returns with streams
getServiceDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
getAddonDetails('mysql', 'CLOUDRON_MYSQL_TOKEN', function (error, result) {
if (error) return callback(error);
var input = fs.createReadStream(dumpPath('mysql', app.id));
@@ -1170,39 +921,35 @@ function startPostgresql(existingInfra, callback) {
const upgrading = existingInfra.version !== 'none' && requiresUpgrade(existingInfra.images.postgresql.tag, tag);
if (upgrading) debug('startPostgresql: postgresql will be upgraded');
const upgradeFunc = upgrading ? shell.sudo.bind(null, 'startPostgresql', [ RMADDONDIR_CMD, 'postgresql' ], {}) : (next) => next();
if (upgrading) {
debug('startPostgresql: postgresql will be upgraded');
shell.sudoSync('startPostgresql', `${RMADDON_CMD} postgresql`);
}
upgradeFunc(function (error) {
const cmd = `docker run --restart=always -d --name="postgresql" \
--net cloudron \
--net-alias postgresql \
--log-driver syslog \
--log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \
--log-opt tag=postgresql \
-m ${memoryLimit}m \
--memory-swap ${memoryLimit * 2}m \
--dns 172.18.0.1 \
--dns-search=. \
-e CLOUDRON_POSTGRESQL_ROOT_PASSWORD="${rootPassword}" \
-e CLOUDRON_POSTGRESQL_TOKEN="${cloudronToken}" \
-v "${dataDir}/postgresql:/var/lib/postgresql" \
--label isCloudronManaged=true \
--read-only -v /tmp -v /run "${tag}"`;
shell.execSync('startPostgresql', cmd);
waitForAddon('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error) {
if (error) return callback(error);
if (!upgrading) return callback(null);
const cmd = `docker run --restart=always -d --name="postgresql" \
--net cloudron \
--net-alias postgresql \
--log-driver syslog \
--log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \
--log-opt tag=postgresql \
-m ${memoryLimit}m \
--memory-swap ${memoryLimit * 2}m \
--dns 172.18.0.1 \
--dns-search=. \
-e CLOUDRON_POSTGRESQL_ROOT_PASSWORD="${rootPassword}" \
-e CLOUDRON_POSTGRESQL_TOKEN="${cloudronToken}" \
-v "${dataDir}/postgresql:/var/lib/postgresql" \
--label isCloudronManaged=true \
--read-only -v /tmp -v /run "${tag}"`;
shell.exec('startPostgresql', cmd, function (error) {
if (error) return callback(error);
waitForService('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error) {
if (error) return callback(error);
if (!upgrading) return callback(null);
importDatabase('postgresql', callback);
});
});
importDatabase('postgresql', callback);
});
}
@@ -1224,7 +971,7 @@ function setupPostgreSql(app, options, callback) {
password: error ? hat(4 * 128) : existingPassword
};
getServiceDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
getAddonDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
if (error) return callback(error);
request.post(`https://${result.ip}:3000/databases?access_token=${result.token}`, { rejectUnauthorized: false, json: data }, function (error, response) {
@@ -1256,7 +1003,7 @@ function clearPostgreSql(app, options, callback) {
debugApp(app, 'Clearing postgresql');
getServiceDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
getAddonDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
if (error) return callback(error);
request.post(`https://${result.ip}:3000/databases/${database}/clear?access_token=${result.token}&username=${username}`, { rejectUnauthorized: false }, function (error, response) {
@@ -1275,7 +1022,7 @@ function teardownPostgreSql(app, options, callback) {
const { database, username } = postgreSqlNames(app.id);
getServiceDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
getAddonDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
if (error) return callback(error);
request.delete(`https://${result.ip}:3000/databases/${database}?access_token=${result.token}&username=${username}`, { rejectUnauthorized: false }, function (error, response) {
@@ -1296,7 +1043,7 @@ function backupPostgreSql(app, options, callback) {
const { database } = postgreSqlNames(app.id);
getServiceDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
getAddonDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
if (error) return callback(error);
const url = `https://${result.ip}:3000/databases/${database}/backup?access_token=${result.token}`;
@@ -1315,7 +1062,7 @@ function restorePostgreSql(app, options, callback) {
callback = once(callback); // protect from multiple returns with streams
getServiceDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
getAddonDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
if (error) return callback(error);
var input = fs.createReadStream(dumpPath('postgresql', app.id));
@@ -1344,39 +1091,35 @@ function startMongodb(existingInfra, callback) {
const upgrading = existingInfra.version !== 'none' && requiresUpgrade(existingInfra.images.mongodb.tag, tag);
if (upgrading) debug('startMongodb: mongodb will be upgraded');
const upgradeFunc = upgrading ? shell.sudo.bind(null, 'startMongodb', [ RMADDONDIR_CMD, 'mongodb' ], {}) : (next) => next();
if (upgrading) {
debug('startMongodb: mongodb will be upgraded');
shell.sudoSync('startMongodb', `${RMADDON_CMD} mongodb`);
}
upgradeFunc(function (error) {
const cmd = `docker run --restart=always -d --name="mongodb" \
--net cloudron \
--net-alias mongodb \
--log-driver syslog \
--log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \
--log-opt tag=mongodb \
-m ${memoryLimit}m \
--memory-swap ${memoryLimit * 2}m \
--dns 172.18.0.1 \
--dns-search=. \
-e CLOUDRON_MONGODB_ROOT_PASSWORD="${rootPassword}" \
-e CLOUDRON_MONGODB_TOKEN="${cloudronToken}" \
-v "${dataDir}/mongodb:/var/lib/mongodb" \
--label isCloudronManaged=true \
--read-only -v /tmp -v /run "${tag}"`;
shell.execSync('startMongodb', cmd);
waitForAddon('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error) {
if (error) return callback(error);
if (!upgrading) return callback(null);
const cmd = `docker run --restart=always -d --name="mongodb" \
--net cloudron \
--net-alias mongodb \
--log-driver syslog \
--log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \
--log-opt tag=mongodb \
-m ${memoryLimit}m \
--memory-swap ${memoryLimit * 2}m \
--dns 172.18.0.1 \
--dns-search=. \
-e CLOUDRON_MONGODB_ROOT_PASSWORD="${rootPassword}" \
-e CLOUDRON_MONGODB_TOKEN="${cloudronToken}" \
-v "${dataDir}/mongodb:/var/lib/mongodb" \
--label isCloudronManaged=true \
--read-only -v /tmp -v /run "${tag}"`;
shell.exec('startMongodb', cmd, function (error) {
if (error) return callback(error);
waitForService('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error) {
if (error) return callback(error);
if (!upgrading) return callback(null);
importDatabase('mongodb', callback);
});
});
importDatabase('mongodb', callback);
});
}
@@ -1396,7 +1139,7 @@ function setupMongoDb(app, options, callback) {
password: error ? hat(4 * 128) : existingPassword
};
getServiceDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
getAddonDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
if (error) return callback(error);
request.post(`https://${result.ip}:3000/databases?access_token=${result.token}`, { rejectUnauthorized: false, json: data }, function (error, response) {
@@ -1426,7 +1169,7 @@ function clearMongodb(app, options, callback) {
debugApp(app, 'Clearing mongodb');
getServiceDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
getAddonDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
if (error) return callback(error);
request.post(`https://${result.ip}:3000/databases/${app.id}/clear?access_token=${result.token}`, { rejectUnauthorized: false }, function (error, response) {
@@ -1445,7 +1188,7 @@ function teardownMongoDb(app, options, callback) {
debugApp(app, 'Tearing down mongodb');
getServiceDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
getAddonDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
if (error) return callback(error);
request.delete(`https://${result.ip}:3000/databases/${app.id}?access_token=${result.token}`, { rejectUnauthorized: false }, function (error, response) {
@@ -1464,7 +1207,7 @@ function backupMongoDb(app, options, callback) {
debugApp(app, 'Backing up mongodb');
getServiceDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
getAddonDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
if (error) return callback(error);
const url = `https://${result.ip}:3000/databases/${app.id}/backup?access_token=${result.token}`;
@@ -1481,7 +1224,7 @@ function restoreMongoDb(app, options, callback) {
debugApp(app, 'restoreMongoDb');
getServiceDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
getAddonDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
if (error) return callback(error);
const readStream = fs.createReadStream(dumpPath('mongodb', app.id));
@@ -1570,9 +1313,9 @@ function setupRedis(app, options, callback) {
];
async.series([
shell.exec.bind(null, 'startRedis', cmd),
shell.execSync.bind(null, 'startRedis', cmd),
appdb.setAddonConfig.bind(null, app.id, 'redis', env),
waitForService.bind(null, 'redis-' + app.id, 'CLOUDRON_REDIS_TOKEN')
waitForAddon.bind(null, 'redis-' + app.id, 'CLOUDRON_REDIS_TOKEN')
], function (error) {
if (error) debug('Error setting up redis: ', error);
callback(error);
@@ -1588,7 +1331,7 @@ function clearRedis(app, options, callback) {
debugApp(app, 'Clearing redis');
getServiceDetails('redis-' + app.id, 'CLOUDRON_REDIS_TOKEN', function (error, result) {
getAddonDetails('redis-' + app.id, 'CLOUDRON_REDIS_TOKEN', function (error, result) {
if (error) return callback(error);
request.post(`https://${result.ip}:3000/clear?access_token=${result.token}`, { rejectUnauthorized: false }, function (error, response) {
@@ -1615,7 +1358,7 @@ function teardownRedis(app, options, callback) {
container.remove(removeOptions, function (error) {
if (error && error.statusCode !== 404) return callback(new Error('Error removing container:' + error));
shell.sudo('removeVolume', [ RMADDONDIR_CMD, 'redis', app.id ], {}, function (error) {
shell.sudo('removeVolume', [ RMADDON_CMD, 'redis', app.id ], function (error) {
if (error) return callback(new Error('Error removing redis data:' + error));
rimraf(path.join(paths.LOG_DIR, `redis-${app.id}`), function (error) {
@@ -1634,7 +1377,7 @@ function backupRedis(app, options, callback) {
debugApp(app, 'Backing up redis');
getServiceDetails('redis-' + app.id, 'CLOUDRON_REDIS_TOKEN', function (error, result) {
getAddonDetails('redis-' + app.id, 'CLOUDRON_REDIS_TOKEN', function (error, result) {
if (error) return callback(error);
const url = `https://${result.ip}:3000/backup?access_token=${result.token}`;
@@ -1649,7 +1392,7 @@ function restoreRedis(app, options, callback) {
debugApp(app, 'Restoring redis');
getServiceDetails('redis-' + app.id, 'CLOUDRON_REDIS_TOKEN', function (error, result) {
getAddonDetails('redis-' + app.id, 'CLOUDRON_REDIS_TOKEN', function (error, result) {
if (error) return callback(error);
let input;
@@ -1671,35 +1414,3 @@ function restoreRedis(app, options, callback) {
input.pipe(restoreReq);
});
}
function statusDocker(callback) {
assert.strictEqual(typeof callback, 'function');
docker.ping(function (error) {
callback(null, { status: error ? exports.SERVICE_STATUS_STOPPED: exports.SERVICE_STATUS_ACTIVE });
});
}
function restartDocker(callback) {
assert.strictEqual(typeof callback, 'function');
shell.sudo('restartdocker', [ path.join(__dirname, 'scripts/restartdocker.sh') ], {}, NOOP_CALLBACK);
callback(null);
}
function statusUnbound(callback) {
assert.strictEqual(typeof callback, 'function');
shell.exec('statusUnbound', 'systemctl is-active unbound', function (error) {
callback(null, { status: error ? exports.SERVICE_STATUS_STOPPED : exports.SERVICE_STATUS_ACTIVE });
});
}
function restartUnbound(callback) {
assert.strictEqual(typeof callback, 'function');
shell.sudo('restartunbound', [ path.join(__dirname, 'scripts/restartunbound.sh') ], {}, NOOP_CALLBACK);
callback(null);
}
+21 -24
View File
@@ -18,7 +18,6 @@ exports = module.exports = {
getAddonConfigByName: getAddonConfigByName,
unsetAddonConfig: unsetAddonConfig,
unsetAddonConfigByAppId: unsetAddonConfigByAppId,
getAppIdByAddonConfigValue: getAppIdByAddonConfigValue,
setHealth: setHealth,
setInstallationCommand: setInstallationCommand,
@@ -62,6 +61,7 @@ var assert = require('assert'),
async = require('async'),
database = require('./database.js'),
DatabaseError = require('./databaseerror'),
mailboxdb = require('./mailboxdb.js'),
safe = require('safetydance'),
util = require('util');
@@ -69,7 +69,7 @@ var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationSta
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.httpPort', 'subdomains.subdomain AS location', 'subdomains.domain',
'apps.accessRestrictionJson', 'apps.restoreConfigJson', 'apps.oldConfigJson', 'apps.updateConfigJson', 'apps.memoryLimit',
'apps.xFrameOptions', 'apps.sso', 'apps.debugModeJson', 'apps.robotsTxt', 'apps.enableBackup',
'apps.creationTime', 'apps.updateTime', 'apps.ownerId', 'apps.mailboxName', 'apps.enableAutomaticUpdate', 'apps.ts' ].join(',');
'apps.creationTime', 'apps.updateTime', 'apps.ownerId', 'apps.ts' ].join(',');
var PORT_BINDINGS_FIELDS = [ 'hostPort', 'type', 'environmentVariable', 'appId' ].join(',');
@@ -120,7 +120,6 @@ function postProcess(result) {
result.sso = !!result.sso; // make it bool
result.enableBackup = !!result.enableBackup; // make it bool
result.enableAutomaticUpdate = !!result.enableAutomaticUpdate; // make it bool
assert(result.debugModeJson === null || typeof result.debugModeJson === 'string');
result.debugMode = safe.JSON.parse(result.debugModeJson);
@@ -277,14 +276,13 @@ function add(id, appStoreId, manifest, location, domain, ownerId, portBindings,
var robotsTxt = 'robotsTxt' in data ? data.robotsTxt : null;
var debugModeJson = data.debugMode ? JSON.stringify(data.debugMode) : null;
var env = data.env || {};
const mailboxName = data.mailboxName || null;
var queries = [];
queries.push({
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, accessRestrictionJson, memoryLimit, xFrameOptions, restoreConfigJson, sso, debugModeJson, robotsTxt, ownerId, mailboxName) ' +
' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, manifestJson, installationState, accessRestrictionJson, memoryLimit, xFrameOptions, restoreConfigJson, sso, debugModeJson, robotsTxt, ownerId, mailboxName ]
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, accessRestrictionJson, memoryLimit, xFrameOptions, restoreConfigJson, sso, debugModeJson, robotsTxt, ownerId) ' +
' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, manifestJson, installationState, accessRestrictionJson, memoryLimit, xFrameOptions, restoreConfigJson, sso, debugModeJson, robotsTxt, ownerId ]
});
queries.push({
@@ -306,6 +304,14 @@ function add(id, appStoreId, manifest, location, domain, ownerId, portBindings,
});
});
// only allocate a mailbox if mailboxName is set
if (data.mailboxName) {
queries.push({
query: 'INSERT INTO mailboxes (name, type, domain, ownerId, ownerType) VALUES (?, ?, ?, ?, ?)',
args: [ data.mailboxName, mailboxdb.TYPE_MAILBOX, domain, id, mailboxdb.OWNER_TYPE_APP ]
});
}
if (data.alternateDomains) {
data.alternateDomains.forEach(function (d) {
queries.push({
@@ -370,6 +376,7 @@ function del(id, callback) {
var queries = [
{ query: 'DELETE FROM subdomains WHERE appId = ?', args: [ id ] },
{ query: 'DELETE FROM mailboxes WHERE ownerId=?', args: [ id ] },
{ query: 'DELETE FROM appPortBindings WHERE appId = ?', args: [ id ] },
{ query: 'DELETE FROM appEnvVars WHERE appId = ?', args: [ id ] },
{ query: 'DELETE FROM apps WHERE id = ?', args: [ id ] }
@@ -377,7 +384,7 @@ function del(id, callback) {
database.transaction(queries, function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results[3].affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (results[4].affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
callback(null);
});
@@ -435,8 +442,12 @@ function updateWithConstraints(id, app, constraints, callback) {
});
}
if ('location' in app && 'domain' in app) { // must be updated together as they are unique together
queries.push({ query: 'UPDATE subdomains SET subdomain = ?, domain = ? WHERE appId = ? AND type = ?', args: [ app.location, app.domain, id, exports.SUBDOMAIN_TYPE_PRIMARY ]});
if ('location' in app) {
queries.push({ query: 'UPDATE subdomains SET subdomain = ? WHERE appId = ? AND type = ?', args: [ app.location, id, exports.SUBDOMAIN_TYPE_PRIMARY ]});
}
if ('domain' in app) {
queries.push({ query: 'UPDATE subdomains SET domain = ? WHERE appId = ? AND type = ?', args: [ app.domain, id, exports.SUBDOMAIN_TYPE_PRIMARY ]});
}
if ('alternateDomains' in app) {
@@ -610,20 +621,6 @@ function getAddonConfigByAppId(appId, callback) {
});
}
function getAppIdByAddonConfigValue(addonId, name, value, callback) {
assert.strictEqual(typeof addonId, 'string');
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof value, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT appId FROM appAddonConfigs WHERE addonId = ? AND name = ? AND value = ?', [ addonId, name, value ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
callback(null, results[0].appId);
});
}
function getAddonConfigByName(appId, addonId, name, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof addonId, 'string');
+59 -56
View File
@@ -72,6 +72,7 @@ var appdb = require('./appdb.js'),
eventlog = require('./eventlog.js'),
fs = require('fs'),
mail = require('./mail.js'),
mailboxdb = require('./mailboxdb.js'),
manifestFormat = require('cloudron-manifestformat'),
os = require('os'),
path = require('path'),
@@ -153,8 +154,7 @@ function validatePortBindings(portBindings, manifest) {
config.get('ldapPort'), /* ldap server (lo) */
3306, /* mysql (lo) */
4190, /* managesieve */
8000, /* ESXi monitoring */
8417, /* graphite (lo) */
8000, /* graphite (lo) */
];
if (!portBindings) return null;
@@ -346,7 +346,7 @@ function removeInternalFields(app) {
'location', 'domain', 'fqdn', 'mailboxName',
'accessRestriction', 'manifest', 'portBindings', 'iconUrl', 'memoryLimit', 'xFrameOptions',
'sso', 'debugMode', 'robotsTxt', 'enableBackup', 'creationTime', 'updateTime', 'ts',
'alternateDomains', 'ownerId', 'env', 'enableAutomaticUpdate');
'alternateDomains', 'ownerId', 'env');
}
function removeRestrictedFields(app) {
@@ -399,7 +399,13 @@ function get(appId, callback) {
app.fqdn = domains.fqdn(app.location, domainObjectMap[app.domain]);
app.alternateDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
callback(null, app);
mailboxdb.getByOwnerId(app.id, function (error, mailboxes) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
if (!error) app.mailboxName = mailboxes[0].name;
callback(null, app);
});
});
});
}
@@ -433,7 +439,13 @@ function getByIpAddress(ip, callback) {
app.fqdn = domains.fqdn(app.location, domainObjectMap[app.domain]);
app.alternateDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
callback(null, app);
mailboxdb.getByOwnerId(app.id, function (error, mailboxes) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
if (!error) app.mailboxName = mailboxes[0].name;
callback(null, app);
});
});
});
});
@@ -459,7 +471,13 @@ function getAll(callback) {
app.fqdn = domains.fqdn(app.location, domainObjectMap[app.domain]);
app.alternateDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
iteratorDone(null, app);
mailboxdb.getByOwnerId(app.id, function (error, mailboxes) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return iteratorDone(new AppsError(AppsError.INTERNAL_ERROR, error));
if (!error) app.mailboxName = mailboxes[0].name;
iteratorDone(null, app);
});
}, function (error) {
if (error) return callback(error);
@@ -525,13 +543,11 @@ function install(data, user, auditSource, callback) {
debugMode = data.debugMode || null,
robotsTxt = data.robotsTxt || null,
enableBackup = 'enableBackup' in data ? data.enableBackup : true,
enableAutomaticUpdate = 'enableAutomaticUpdate' in data ? data.enableAutomaticUpdate : true,
backupId = data.backupId || null,
backupFormat = data.backupFormat || 'tgz',
ownerId = data.ownerId,
alternateDomains = data.alternateDomains || [],
env = data.env || {},
mailboxName = data.mailboxName || '';
env = data.env || {};
assert(data.appStoreId || data.manifest); // atleast one of them is required
@@ -572,13 +588,6 @@ function install(data, user, auditSource, callback) {
error = validateEnv(env);
if (error) return callback(error);
if (mailboxName) {
error = mail.validateName(mailboxName);
if (error) return callback(error);
} else {
mailboxName = mailboxNameForLocation(location, manifest);
}
var appId = uuid.v4();
if (icon) {
@@ -612,10 +621,9 @@ function install(data, user, auditSource, callback) {
xFrameOptions: xFrameOptions,
sso: sso,
debugMode: debugMode,
mailboxName: mailboxName,
mailboxName: mailboxNameForLocation(location, manifest),
restoreConfig: backupId ? { backupId: backupId, backupFormat: backupFormat } : null,
enableBackup: enableBackup,
enableAutomaticUpdate: enableAutomaticUpdate,
robotsTxt: robotsTxt,
alternateDomains: alternateDomains,
env: env
@@ -674,14 +682,12 @@ function configure(appId, data, user, auditSource, callback) {
get(appId, function (error, app) {
if (error) return callback(error);
let domain, location, portBindings, values = { };
if ('location' in data && 'domain' in data) {
location = values.location = data.location.toLowerCase();
domain = values.domain = data.domain.toLowerCase();
} else {
location = app.location;
domain = app.domain;
}
let domain, location, portBindings, values = { }, mailboxName;
if ('location' in data) location = values.location = data.location.toLowerCase();
else location = app.location;
if ('domain' in data) domain = values.domain = data.domain.toLowerCase();
else domain = app.domain;
if ('accessRestriction' in data) {
values.accessRestriction = data.accessRestriction;
@@ -723,15 +729,15 @@ function configure(appId, data, user, auditSource, callback) {
}
if ('mailboxName' in data) {
if (data.mailboxName) {
if (data.mailboxName === '') { // special case to reset back to .app
mailboxName = mailboxNameForLocation(location, app.manifest);
} else {
error = mail.validateName(data.mailboxName);
if (error) return callback(error);
values.mailboxName = data.mailboxName;
} else {
values.mailboxName = mailboxNameForLocation(location, app.manifest);
mailboxName = data.mailboxName;
}
} else { // keep existing name or follow the new location
values.mailboxName = app.mailboxName.endsWith('.app') ? mailboxNameForLocation(location, app.manifest) : app.mailboxName;
mailboxName = app.mailboxName.endsWith('.app') ? mailboxNameForLocation(location, app.manifest) : app.mailboxName;
}
if ('alternateDomains' in data) {
@@ -767,26 +773,34 @@ function configure(appId, data, user, auditSource, callback) {
}
if ('enableBackup' in data) values.enableBackup = data.enableBackup;
if ('enableAutomaticUpdate' in data) values.enableAutomaticUpdate = data.enableAutomaticUpdate;
values.oldConfig = getAppConfig(app);
debug('Will configure app with id:%s values:%j', appId, values);
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_CONFIGURE, values, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location, portBindings, error));
// make the mailbox name follow the apps new location, if the user did not set it explicitly
mailboxdb.updateName(app.mailboxName /* old */, values.oldConfig.domain, mailboxName, domain, function (error) {
if (mailboxName.endsWith('.app')) error = null; // ignore internal mailbox conflict errors since we want to show location conflict errors in the UI
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new AppsError(AppsError.ALREADY_EXISTS, 'This mailbox is already taken'));
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
taskmanager.restartAppTask(appId);
// fetch fresh app object for eventlog
get(appId, function (error, result) {
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_CONFIGURE, values, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location, portBindings, error));
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: appId, app: result });
taskmanager.restartAppTask(appId);
callback(null);
// fetch fresh app object for eventlog
get(appId, function (error, result) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: appId, app: result });
callback(null);
});
});
});
});
@@ -973,8 +987,7 @@ function clone(appId, data, user, auditSource, callback) {
domain = data.domain.toLowerCase(),
portBindings = data.portBindings || null,
backupId = data.backupId,
ownerId = data.ownerId,
mailboxName = data.mailboxName || '';
ownerId = data.ownerId;
assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof location, 'string');
@@ -992,22 +1005,13 @@ function clone(appId, data, user, auditSource, callback) {
if (!backupInfo.manifest) callback(new AppsError(AppsError.EXTERNAL_ERROR, 'Could not get restore config'));
const manifest = backupInfo.manifest;
// re-validate because this new box version may not accept old configs
error = checkManifestConstraints(manifest);
error = checkManifestConstraints(backupInfo.manifest);
if (error) return callback(error);
error = validatePortBindings(portBindings, manifest);
error = validatePortBindings(portBindings, backupInfo.manifest);
if (error) return callback(error);
if (mailboxName) {
error = mail.validateName(mailboxName);
if (error) return callback(error);
} else {
mailboxName = mailboxNameForLocation(location, manifest);
}
domains.get(domain, function (error, domainObject) {
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new AppsError(AppsError.EXTERNAL_ERROR, 'No such domain'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Could not get domain info:' + error.message));
@@ -1016,7 +1020,7 @@ function clone(appId, data, user, auditSource, callback) {
error = domains.validateHostname(location, domainObject);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Bad location: ' + error.message));
var newAppId = uuid.v4();
var newAppId = uuid.v4(), manifest = backupInfo.manifest;
var data = {
installationState: appdb.ISTATE_PENDING_CLONE,
@@ -1025,7 +1029,7 @@ function clone(appId, data, user, auditSource, callback) {
xFrameOptions: app.xFrameOptions,
restoreConfig: { backupId: backupId, backupFormat: backupInfo.format },
sso: !!app.sso,
mailboxName: mailboxName,
mailboxName: (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app',
enableBackup: app.enableBackup,
robotsTxt: app.robotsTxt,
env: app.env
@@ -1216,7 +1220,6 @@ function autoupdateApps(updateInfo, auditSource, callback) { // updateInfo is {
assert.strictEqual(typeof callback, 'function');
function canAutoupdateApp(app, newManifest) {
if (!app.enableAutomaticUpdate) return new Error('Automatic update disabled');
if ((semver.major(app.manifest.version) !== 0) && (semver.major(app.manifest.version) !== semver.major(newManifest.version))) return new Error('Major version change'); // major changes are blocking
const newTcpPorts = newManifest.tcpPorts || { };
+7 -9
View File
@@ -55,8 +55,6 @@ var COLLECTD_CONFIG_EJS = fs.readFileSync(__dirname + '/collectd.config.ejs', {
LOGROTATE_CONFIG_EJS = fs.readFileSync(__dirname + '/logrotate.ejs', { encoding: 'utf8' }),
CONFIGURE_LOGROTATE_CMD = path.join(__dirname, 'scripts/configurelogrotate.sh');
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -212,7 +210,7 @@ function addCollectdProfile(app, callback) {
var collectdConf = ejs.render(COLLECTD_CONFIG_EJS, { appId: app.id, containerId: app.containerId });
fs.writeFile(path.join(paths.COLLECTD_APPCONFIG_DIR, app.id + '.conf'), collectdConf, function (error) {
if (error) return callback(error);
shell.sudo('addCollectdProfile', [ CONFIGURE_COLLECTD_CMD, 'add', app.id ], {}, callback);
shell.sudo('addCollectdProfile', [ CONFIGURE_COLLECTD_CMD, 'add', app.id ], callback);
});
}
@@ -222,7 +220,7 @@ function removeCollectdProfile(app, callback) {
fs.unlink(path.join(paths.COLLECTD_APPCONFIG_DIR, app.id + '.conf'), function (error) {
if (error && error.code !== 'ENOENT') debugApp(app, 'Error removing collectd profile', error);
shell.sudo('removeCollectdProfile', [ CONFIGURE_COLLECTD_CMD, 'remove', app.id ], {}, callback);
shell.sudo('removeCollectdProfile', [ CONFIGURE_COLLECTD_CMD, 'remove', app.id ], callback);
});
}
@@ -241,7 +239,7 @@ function addLogrotateConfig(app, callback) {
var tmpFilePath = path.join(os.tmpdir(), app.id + '.logrotate');
fs.writeFile(tmpFilePath, logrotateConf, function (error) {
if (error) return callback(error);
shell.sudo('addLogrotateConfig', [ CONFIGURE_LOGROTATE_CMD, 'add', app.id, tmpFilePath ], {}, callback);
shell.sudo('addLogrotateConfig', [ CONFIGURE_LOGROTATE_CMD, 'add', app.id, tmpFilePath ], callback);
});
});
}
@@ -250,7 +248,7 @@ function removeLogrotateConfig(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
shell.sudo('removeLogrotateConfig', [ CONFIGURE_LOGROTATE_CMD, 'remove', app.id ], {}, callback);
shell.sudo('removeLogrotateConfig', [ CONFIGURE_LOGROTATE_CMD, 'remove', app.id ], callback);
}
function verifyManifest(manifest, callback) {
@@ -541,7 +539,7 @@ function install(app, callback) {
updateApp.bind(null, app, { installationProgress: '60, Download backup and restoring addons' }),
addons.setupAddons.bind(null, app, app.manifest.addons),
addons.clearAddons.bind(null, app, app.manifest.addons),
backups.restoreApp.bind(null, app, app.manifest.addons, restoreConfig, (progress) => updateApp(app, { installationProgress: progress.message }, NOOP_CALLBACK))
backups.restoreApp.bind(null, app, app.manifest.addons, restoreConfig),
], next);
}
},
@@ -583,7 +581,7 @@ function backup(app, callback) {
async.series([
updateApp.bind(null, app, { installationProgress: '10, Backing up' }),
backups.backupApp.bind(null, app, (progress) => updateApp(app, { installationProgress: progress.message }, NOOP_CALLBACK)),
backups.backupApp.bind(null, app),
// done!
function (callback) {
@@ -696,7 +694,7 @@ function update(app, callback) {
async.series([
updateApp.bind(null, app, { installationProgress: '15, Backing up app' }),
backups.backupApp.bind(null, app, (progress) => updateApp(app, { installationProgress: progress.message }, NOOP_CALLBACK))
backups.backupApp.bind(null, app)
], function (error) {
if (error) error.backupError = true;
next(error);
+157 -118
View File
@@ -10,9 +10,9 @@ exports = module.exports = {
get: get,
startBackupTask: startBackupTask,
ensureBackup: ensureBackup,
backup: backup,
restore: restore,
backupApp: backupApp,
@@ -53,6 +53,7 @@ var addons = require('./addons.js'),
once = require('once'),
path = require('path'),
paths = require('./paths.js'),
progress = require('./progress.js'),
progressStream = require('progress-stream'),
safe = require('safetydance'),
shell = require('./shell.js'),
@@ -60,12 +61,12 @@ var addons = require('./addons.js'),
superagent = require('superagent'),
syncer = require('./syncer.js'),
tar = require('tar-fs'),
tasks = require('./tasks.js'),
util = require('util'),
zlib = require('zlib');
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
const BACKUP_UPLOAD_CMD = path.join(__dirname, 'scripts/backupupload.js');
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
var BACKUPTASK_CMD = path.join(__dirname, 'backuptask.js');
function debugApp(app) {
assert(typeof app === 'object');
@@ -180,6 +181,11 @@ function getBackupFilePath(backupConfig, backupId, format) {
}
}
function log(detail) {
safe.fs.appendFileSync(paths.BACKUP_LOG_FILE, detail + '\n', 'utf8');
progress.setDetail(progress.BACKUP, detail);
}
function encryptFilePath(filePath, key) {
assert.strictEqual(typeof filePath, 'string');
assert.strictEqual(typeof key, 'string');
@@ -232,6 +238,10 @@ function createReadStream(sourceFile, key) {
ps.emit('error', new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
});
ps.on('progress', function(progress) {
debug('createReadStream: %s@%s (%s)', Math.round(progress.transferred/1024/1024) + 'M', Math.round(progress.speed/1024/1024) + 'Mbps', sourceFile);
});
if (key !== null) {
var encrypt = crypto.createCipher('aes-256-cbc', key);
encrypt.on('error', function (error) {
@@ -277,7 +287,7 @@ function createTarPackStream(sourceDir, key) {
});
var gzip = zlib.createGzip({});
var ps = progressStream({ time: 10000 }); // emit 'pgoress' every 10 seconds
var ps = progressStream({ time: 10000 }); // display a progress every 10 seconds
pack.on('error', function (error) {
debug('createTarPackStream: tar stream error.', error);
@@ -289,6 +299,10 @@ function createTarPackStream(sourceDir, key) {
ps.emit('error', new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
});
ps.on('progress', function(progress) {
debug('createTarPackStream: %s@%s', Math.round(progress.transferred/1024/1024) + 'M', Math.round(progress.speed/1024/1024) + 'Mbps');
});
if (key !== null) {
var encrypt = crypto.createCipher('aes-256-cbc', key);
encrypt.on('error', function (error) {
@@ -301,13 +315,17 @@ function createTarPackStream(sourceDir, key) {
}
}
function sync(backupConfig, backupId, dataDir, progressCallback, callback) {
function sync(backupConfig, backupId, dataDir, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof dataDir, 'string');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
function setBackupProgress(message) {
debug('%s', message);
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, message);
}
syncer.sync(dataDir, function processTask(task, iteratorCallback) {
debug('sync: processing task: %j', task);
// the empty task.path is special to signify the directory
@@ -315,12 +333,12 @@ function sync(backupConfig, backupId, dataDir, progressCallback, callback) {
const backupFilePath = path.join(getBackupFilePath(backupConfig, backupId, backupConfig.format), destPath);
if (task.operation === 'removedir') {
debug(`Removing directory ${backupFilePath}`);
setBackupProgress(`Removing directory ${backupFilePath}`);
return api(backupConfig.provider).removeDir(backupConfig, backupFilePath)
.on('progress', (message) => progressCallback({ message }))
.on('progress', setBackupProgress)
.on('done', iteratorCallback);
} else if (task.operation === 'remove') {
debug(`Removing ${backupFilePath}`);
setBackupProgress(`Removing ${backupFilePath}`);
return api(backupConfig.provider).remove(backupConfig, backupFilePath, iteratorCallback);
}
@@ -329,19 +347,16 @@ function sync(backupConfig, backupId, dataDir, progressCallback, callback) {
retryCallback = once(retryCallback); // protect again upload() erroring much later after read stream error
++retryCount;
progressCallback({ message: `${task.operation} ${task.path} try ${retryCount}` });
debug(`${task.operation} ${task.path} try ${retryCount}`);
if (task.operation === 'add') {
debug(`Adding ${task.path} position ${task.position} try ${retryCount}`);
setBackupProgress(`Adding ${task.path} position ${task.position} try ${retryCount}`);
var stream = createReadStream(path.join(dataDir, task.path), backupConfig.key || null);
stream.on('error', function (error) {
debug(`read stream error for ${task.path}: ${error.message}`);
setBackupProgress(`read stream error for ${task.path}: ${error.message}`);
retryCallback();
}); // ignore error if file disappears
stream.on('progress', function(progress) {
progressCallback({ message: `Uploading ${task.path}: ${Math.round(progress.transferred/1024/1024)}M@${Math.round(progress.speed/1024/1024)}` });
});
api(backupConfig.provider).upload(backupConfig, backupFilePath, stream, function (error) {
debug(error ? `Error uploading ${task.path} try ${retryCount}: ${error.message}` : `Uploaded ${task.path}`);
setBackupProgress(error ? `Error uploading ${task.path} try ${retryCount}: ${error.message}` : `Uploaded ${task.path}`);
retryCallback(error);
});
}
@@ -373,15 +388,14 @@ function saveFsMetadata(appDataDir, callback) {
callback();
}
// this function is called via backupupload (since it needs root to traverse app's directory)
function upload(backupId, format, dataDir, progressCallback, callback) {
// this function is called via backuptask (since it needs root to traverse app's directory)
function upload(backupId, format, dataDir, callback) {
assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof format, 'string');
assert.strictEqual(typeof dataDir, 'string');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
debug(`upload: id ${backupId} format ${format} dataDir ${dataDir}`);
debug('upload: id %s format %s dataDir %s', backupId, format, dataDir);
settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
@@ -391,9 +405,6 @@ function upload(backupId, format, dataDir, progressCallback, callback) {
retryCallback = once(retryCallback); // protect again upload() erroring much later after tar stream error
var tarStream = createTarPackStream(dataDir, backupConfig.key || null);
tarStream.on('progress', function(progress) {
progressCallback({ message: `Uploading ${Math.round(progress.transferred/1024/1024)}M@${Math.round(progress.speed/1024/1024)}Mbps` });
});
tarStream.on('error', retryCallback); // already returns BackupsError
api(backupConfig.provider).upload(backupConfig, getBackupFilePath(backupConfig, backupId, format), tarStream, retryCallback);
@@ -401,7 +412,7 @@ function upload(backupId, format, dataDir, progressCallback, callback) {
} else {
async.series([
saveFsMetadata.bind(null, dataDir),
sync.bind(null, backupConfig, backupId, dataDir, progressCallback)
sync.bind(null, backupConfig, backupId, dataDir)
], callback);
}
});
@@ -424,6 +435,10 @@ function tarExtract(inStream, destination, key, callback) {
callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
});
ps.on('progress', function(progress) {
debug('tarExtract: %s@%s', Math.round(progress.transferred/1024/1024) + 'M', Math.round(progress.speed/1024/1024) + 'Mbps');
});
gunzip.on('error', function (error) {
debug('tarExtract: gunzip stream error.', error);
callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
@@ -449,15 +464,13 @@ function tarExtract(inStream, destination, key, callback) {
} else {
inStream.pipe(ps).pipe(gunzip).pipe(extract);
}
return ps;
}
function restoreFsMetadata(appDataDir, callback) {
assert.strictEqual(typeof appDataDir, 'string');
assert.strictEqual(typeof callback, 'function');
debug(`Recreating empty directories in ${appDataDir}`);
log('Recreating empty directories');
var metadataJson = safe.fs.readFileSync(path.join(appDataDir, 'fsmetadata.json'), 'utf8');
if (metadataJson === null) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, 'Error loading fsmetadata.txt:' + safe.error.message));
@@ -479,11 +492,10 @@ function restoreFsMetadata(appDataDir, callback) {
});
}
function downloadDir(backupConfig, backupFilePath, destDir, progressCallback, callback) {
function downloadDir(backupConfig, backupFilePath, destDir, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof backupFilePath, 'string');
assert.strictEqual(typeof destDir, 'string');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
debug(`downloadDir: ${backupFilePath} to ${destDir}`);
@@ -507,7 +519,7 @@ function downloadDir(backupConfig, backupFilePath, destDir, progressCallback, ca
let destStream = createWriteStream(destFilePath, backupConfig.key || null);
destStream.on('error', callback);
progressCallback({ message: `Downloading ${entry.fullPath} to ${destFilePath}` });
debug(`downloadDir: Copying ${entry.fullPath} to ${destFilePath}`);
sourceStream.pipe(destStream, { end: true }).on('finish', callback);
});
@@ -519,27 +531,25 @@ function downloadDir(backupConfig, backupFilePath, destDir, progressCallback, ca
}, callback);
}
function download(backupConfig, backupId, format, dataDir, progressCallback, callback) {
function download(backupConfig, backupId, format, dataDir, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof format, 'string');
assert.strictEqual(typeof dataDir, 'string');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
debug(`download - Downloading ${backupId} of format ${format} to ${dataDir}`);
safe.fs.unlinkSync(paths.BACKUP_LOG_FILE); // start fresh log file
log(`Downloading ${backupId} of format ${format} to ${dataDir}`);
if (format === 'tgz') {
api(backupConfig.provider).download(backupConfig, getBackupFilePath(backupConfig, backupId, format), function (error, sourceStream) {
if (error) return callback(error);
let ps = tarExtract(sourceStream, dataDir, backupConfig.key || null, callback);
ps.on('progress', function (progress) {
progressCallback({ message: `Downloading ${Math.round(progress.transferred/1024/1024)}M@${Math.round(progress.speed/1024/1024)}Mbps` });
});
tarExtract(sourceStream, dataDir, backupConfig.key || null, callback);
});
} else {
downloadDir(backupConfig, getBackupFilePath(backupConfig, backupId, format), dataDir, progressCallback, function (error) {
downloadDir(backupConfig, getBackupFilePath(backupConfig, backupId, format), dataDir, function (error) {
if (error) return callback(error);
restoreFsMetadata(dataDir, callback);
@@ -547,13 +557,12 @@ function download(backupConfig, backupId, format, dataDir, progressCallback, cal
}
}
function restore(backupConfig, backupId, progressCallback, callback) {
function restore(backupConfig, backupId, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
download(backupConfig, backupId, backupConfig.format, paths.BOX_DATA_DIR, progressCallback, function (error) {
download(backupConfig, backupId, backupConfig.format, paths.BOX_DATA_DIR, function (error) {
if (error) return callback(error);
debug('restore: download completed, importing database');
@@ -568,11 +577,10 @@ function restore(backupConfig, backupId, progressCallback, callback) {
});
}
function restoreApp(app, addonsToRestore, restoreConfig, progressCallback, callback) {
function restoreApp(app, addonsToRestore, restoreConfig, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof addonsToRestore, 'object');
assert.strictEqual(typeof restoreConfig, 'object');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
var appDataDir = safe.fs.realpathSync(path.join(paths.APPS_DATA_DIR, app.id));
@@ -583,7 +591,7 @@ function restoreApp(app, addonsToRestore, restoreConfig, progressCallback, callb
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
async.series([
download.bind(null, backupConfig, restoreConfig.backupId, restoreConfig.backupFormat, appDataDir, progressCallback),
download.bind(null, backupConfig, restoreConfig.backupId, restoreConfig.backupFormat, appDataDir),
addons.restoreAddons.bind(null, app, addonsToRestore)
], function (error) {
debug('restoreApp: time: %s', (new Date() - startTime)/1000);
@@ -593,27 +601,44 @@ function restoreApp(app, addonsToRestore, restoreConfig, progressCallback, callb
});
}
function runBackupUpload(backupId, format, dataDir, progressCallback, callback) {
function runBackupTask(backupId, format, dataDir, callback) {
assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof format, 'string');
assert.strictEqual(typeof dataDir, 'string');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
let result = '';
var killTimerId = null, progressTimerId = null;
var logStream = fs.createWriteStream(paths.BACKUP_LOG_FILE, { flags: 'a' });
var cp = shell.sudo(`backup-${backupId}`, [ BACKUPTASK_CMD, backupId, format, dataDir ], { env: process.env, logStream: logStream }, function (error) {
clearTimeout(killTimerId);
clearInterval(progressTimerId);
cp = null;
shell.sudo(`backup-${backupId}`, [ BACKUP_UPLOAD_CMD, backupId, format, dataDir ], { preserveEnv: true, ipc: true }, function (error) {
if (error && (error.code === null /* signal */ || (error.code !== 0 && error.code !== 50))) { // backuptask crashed
return callback(new BackupsError(BackupsError.INTERNAL_ERROR, 'Backuptask crashed'));
} else if (error && error.code === 50) { // exited with error
var result = safe.fs.readFileSync(paths.BACKUP_RESULT_FILE, 'utf8') || safe.error.message;
return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, result));
}
callback();
}).on('message', function (message) {
if (!message.result) return progressCallback(message);
debug(`runBackupUpload: result - ${message}`);
result = message.result;
});
progressTimerId = setInterval(function () {
var result = safe.fs.readFileSync(paths.BACKUP_RESULT_FILE, 'utf8');
if (result) progress.setDetail(progress.BACKUP, result);
}, 1000); // every second
killTimerId = setTimeout(function () {
debug('runBackupTask: backup task taking too long. killing');
cp.kill();
}, 4 * 60 * 60 * 1000); // 4 hours
logStream.on('error', function (error) {
debug('runBackupTask: error in logging stream', error);
cp.kill();
});
}
@@ -639,11 +664,10 @@ function setSnapshotInfo(id, info, callback) {
callback();
}
function snapshotBox(progressCallback, callback) {
assert.strictEqual(typeof progressCallback, 'function');
function snapshotBox(callback) {
assert.strictEqual(typeof callback, 'function');
progressCallback({ message: 'Snapshotting box' });
log('Snapshotting box');
database.exportToFile(`${paths.BOX_DATA_DIR}/box.mysqldump`, function (error) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
@@ -652,17 +676,16 @@ function snapshotBox(progressCallback, callback) {
});
}
function uploadBoxSnapshot(backupConfig, progressCallback, callback) {
function uploadBoxSnapshot(backupConfig, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
var startTime = new Date();
snapshotBox(progressCallback, function (error) {
snapshotBox(function (error) {
if (error) return callback(error);
runBackupUpload('snapshot/box', backupConfig.format, paths.BOX_DATA_DIR, progressCallback, function (error) {
runBackupTask('snapshot/box', backupConfig.format, paths.BOX_DATA_DIR, function (error) {
if (error) return callback(error);
debug('uploadBoxSnapshot: time: %s secs', (new Date() - startTime)/1000);
@@ -700,11 +723,10 @@ function backupDone(apiConfig, backupId, appBackupIds, callback) {
});
}
function rotateBoxBackup(backupConfig, timestamp, appBackupIds, progressCallback, callback) {
function rotateBoxBackup(backupConfig, timestamp, appBackupIds, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof timestamp, 'string');
assert(Array.isArray(appBackupIds));
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
var snapshotInfo = getSnapshotInfo('box');
@@ -714,13 +736,13 @@ function rotateBoxBackup(backupConfig, timestamp, appBackupIds, progressCallback
var backupId = util.format('%s/box_%s_v%s', timestamp, snapshotTime, config.version());
const format = backupConfig.format;
debug(`Rotating box backup to id ${backupId}`);
log(`Rotating box backup to id ${backupId}`);
backupdb.add({ id: backupId, version: config.version(), type: backupdb.BACKUP_TYPE_BOX, dependsOn: appBackupIds, manifest: null, format: format }, function (error) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
var copy = api(backupConfig.provider).copy(backupConfig, getBackupFilePath(backupConfig, 'snapshot/box', format), getBackupFilePath(backupConfig, backupId, format));
copy.on('progress', (message) => progressCallback({ message }));
copy.on('progress', log);
copy.on('done', function (copyBackupError) {
const state = copyBackupError ? backupdb.BACKUP_STATE_ERROR : backupdb.BACKUP_STATE_NORMAL;
@@ -728,7 +750,7 @@ function rotateBoxBackup(backupConfig, timestamp, appBackupIds, progressCallback
if (copyBackupError) return callback(copyBackupError);
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
debug(`Rotated box backup successfully as id ${backupId}`);
log(`Rotated box backup successfully as id ${backupId}`);
backupDone(backupConfig, backupId, appBackupIds, function (error) {
if (error) return callback(error);
@@ -740,19 +762,18 @@ function rotateBoxBackup(backupConfig, timestamp, appBackupIds, progressCallback
});
}
function backupBoxWithAppBackupIds(appBackupIds, timestamp, progressCallback, callback) {
function backupBoxWithAppBackupIds(appBackupIds, timestamp, callback) {
assert(Array.isArray(appBackupIds));
assert.strictEqual(typeof timestamp, 'string');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
uploadBoxSnapshot(backupConfig, progressCallback, function (error) {
uploadBoxSnapshot(backupConfig, function (error) {
if (error) return callback(error);
rotateBoxBackup(backupConfig, timestamp, appBackupIds, progressCallback, callback);
rotateBoxBackup(backupConfig, timestamp, appBackupIds, callback);
});
});
}
@@ -766,12 +787,11 @@ function canBackupApp(app) {
app.installationState === appdb.ISTATE_PENDING_UPDATE; // called from apptask
}
function snapshotApp(app, progressCallback, callback) {
function snapshotApp(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
progressCallback({ message: `Snapshotting app ${app.id}` });
log(`Snapshotting app ${app.id}`);
if (!safe.fs.writeFileSync(path.join(paths.APPS_DATA_DIR, app.id + '/config.json'), JSON.stringify(apps.getAppConfig(app)))) {
return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, 'Error creating config.json: ' + safe.error.message));
@@ -784,11 +804,10 @@ function snapshotApp(app, progressCallback, callback) {
});
}
function rotateAppBackup(backupConfig, app, timestamp, progressCallback, callback) {
function rotateAppBackup(backupConfig, app, timestamp, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof timestamp, 'string');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
var snapshotInfo = getSnapshotInfo(app.id);
@@ -799,13 +818,13 @@ function rotateAppBackup(backupConfig, app, timestamp, progressCallback, callbac
var backupId = util.format('%s/app_%s_%s_v%s', timestamp, app.id, snapshotTime, manifest.version);
const format = backupConfig.format;
debug(`Rotating app backup of ${app.id} to id ${backupId}`);
log(`Rotating app backup of ${app.id} to id ${backupId}`);
backupdb.add({ id: backupId, version: manifest.version, type: backupdb.BACKUP_TYPE_APP, dependsOn: [ ], manifest: manifest, format: format }, function (error) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
var copy = api(backupConfig.provider).copy(backupConfig, getBackupFilePath(backupConfig, `snapshot/app_${app.id}`, format), getBackupFilePath(backupConfig, backupId, format));
copy.on('progress', (message) => progressCallback({ message }));
copy.on('progress', log);
copy.on('done', function (copyBackupError) {
const state = copyBackupError ? backupdb.BACKUP_STATE_ERROR : backupdb.BACKUP_STATE_NORMAL;
@@ -813,7 +832,7 @@ function rotateAppBackup(backupConfig, app, timestamp, progressCallback, callbac
if (copyBackupError) return callback(copyBackupError);
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
debug(`Rotated app backup of ${app.id} successfully to id ${backupId}`);
log(`Rotated app backup of ${app.id} successfully to id ${backupId}`);
callback(null, backupId);
});
@@ -821,22 +840,21 @@ function rotateAppBackup(backupConfig, app, timestamp, progressCallback, callbac
});
}
function uploadAppSnapshot(backupConfig, app, progressCallback, callback) {
function uploadAppSnapshot(backupConfig, app, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
if (!canBackupApp(app)) return callback(); // nothing to do
var startTime = new Date();
snapshotApp(app, progressCallback, function (error) {
snapshotApp(app, function (error) {
if (error) return callback(error);
var backupId = util.format('snapshot/app_%s', app.id);
var appDataDir = safe.fs.realpathSync(path.join(paths.APPS_DATA_DIR, app.id));
runBackupUpload(backupId, backupConfig.format, appDataDir, progressCallback, function (error) {
runBackupTask(backupId, backupConfig.format, appDataDir, function (error) {
if (error) return callback(error);
debugApp(app, 'uploadAppSnapshot: %s done time: %s secs', backupId, (new Date() - startTime)/1000);
@@ -846,10 +864,9 @@ function uploadAppSnapshot(backupConfig, app, progressCallback, callback) {
});
}
function backupAppWithTimestamp(app, timestamp, progressCallback, callback) {
function backupAppWithTimestamp(app, timestamp, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof timestamp, 'string');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
if (!canBackupApp(app)) return callback(); // nothing to do
@@ -857,88 +874,110 @@ function backupAppWithTimestamp(app, timestamp, progressCallback, callback) {
settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
uploadAppSnapshot(backupConfig, app, progressCallback, function (error) {
uploadAppSnapshot(backupConfig, app, function (error) {
if (error) return callback(error);
rotateAppBackup(backupConfig, app, timestamp, progressCallback, callback);
rotateAppBackup(backupConfig, app, timestamp, callback);
});
});
}
function backupApp(app, progressCallback, callback) {
function backupApp(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
const timestamp = (new Date()).toISOString().replace(/[T.]/g, '-').replace(/[:Z]/g,'');
safe.fs.unlinkSync(paths.BACKUP_LOG_FILE); // start fresh log file
debug(`backupApp - Backing up ${app.fqdn} with timestamp ${timestamp}`);
progress.set(progress.BACKUP, 10, 'Backing up ' + app.fqdn);
backupAppWithTimestamp(app, timestamp, progressCallback, callback);
backupAppWithTimestamp(app, timestamp, function (error) {
progress.set(progress.BACKUP, 100, error ? error.message : '');
callback(error);
});
}
// this function expects you to have a lock. Unlike other progressCallback this also has a progress field
function backupBoxAndApps(progressCallback, callback) {
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
// this function expects you to have a lock
function backupBoxAndApps(auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
callback = callback || NOOP_CALLBACK;
var timestamp = (new Date()).toISOString().replace(/[T.]/g, '-').replace(/[:Z]/g,'');
safe.fs.unlinkSync(paths.BACKUP_LOG_FILE); // start fresh log file
eventlog.add(eventlog.ACTION_BACKUP_START, auditSource, { });
apps.getAll(function (error, allApps) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
let percent = 1;
let step = 100/(allApps.length+2);
var processed = 1;
var step = 100/(allApps.length+2);
async.mapSeries(allApps, function iterator(app, iteratorCallback) {
progressCallback({ percent: percent, message: `Backing up ${app.fqdn}` });
percent += step;
progress.set(progress.BACKUP, step * processed, 'Backing up ' + app.fqdn);
++processed;
if (!app.enableBackup) {
debug(`Skipped backup ${app.fqdn}`);
progress.set(progress.BACKUP, step * processed, 'Skipped backup ' + app.fqdn);
return iteratorCallback(null, null); // nothing to backup
}
backupAppWithTimestamp(app, timestamp, (progress) => progressCallback({ percent: percent, message: progress.message }), function (error, backupId) {
backupAppWithTimestamp(app, timestamp, function (error, backupId) {
if (error && error.reason !== BackupsError.BAD_STATE) {
debugApp(app, 'Unable to backup', error);
return iteratorCallback(error);
}
debugApp(app, 'Backed up');
progress.set(progress.BACKUP, step * processed, 'Backed up ' + app.fqdn);
iteratorCallback(null, backupId || null); // clear backupId if is in BAD_STATE and never backed up
});
}, function appsBackedUp(error, backupIds) {
if (error) return callback(error);
if (error) {
progress.set(progress.BACKUP, 100, error.message);
return callback(error);
}
backupIds = backupIds.filter(function (id) { return id !== null; }); // remove apps in bad state that were never backed up
progressCallback({ percent: percent, message: 'Backing up system data' });
percent += step;
progress.set(progress.BACKUP, step * processed, 'Backing up system data');
backupBoxWithAppBackupIds(backupIds, timestamp, (progress) => progressCallback({ percent: percent, message: progress.message }), callback);
backupBoxWithAppBackupIds(backupIds, timestamp, function (error, backupId) {
progress.set(progress.BACKUP, 100, error ? error.message : '');
eventlog.add(eventlog.ACTION_BACKUP_FINISH, auditSource, { errorMessage: error ? error.message : null, backupId: backupId, timestamp: timestamp });
callback(error, backupId);
});
});
});
}
function startBackupTask(auditSource, callback) {
let error = locker.lock(locker.OP_FULL_BACKUP);
if (error) return callback(error);
function backup(auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
var error = locker.lock(locker.OP_FULL_BACKUP);
if (error) return callback(new BackupsError(BackupsError.BAD_STATE, error.message));
var startTime = new Date();
progress.set(progress.BACKUP, 0, 'Starting'); // ensure tools can 'wait' on progress
backupBoxAndApps(auditSource, function (error) { // start the backup operation in the background
if (error) {
debug('backup failed.', error);
mailer.backupFailed(error);
}
let task = tasks.startTask(tasks.TASK_BACKUP, [], auditSource);
task.on('error', (error) => callback(new BackupsError(BackupsError.INTERNAL_ERROR, error)));
task.on('start', (taskId) => {
eventlog.add(eventlog.ACTION_BACKUP_START, auditSource, { taskId });
callback(null, taskId);
});
task.on('finish', (error, result) => {
locker.unlock(locker.OP_FULL_BACKUP);
if (error) mailer.backupFailed(error);
eventlog.add(eventlog.ACTION_BACKUP_FINISH, auditSource, { errorMessage: error ? error.message : null, backupId: result });
debug('backup took %s seconds', (new Date() - startTime)/1000);
});
callback(null);
}
function ensureBackup(auditSource, callback) {
@@ -960,7 +999,7 @@ function ensureBackup(auditSource, callback) {
return callback(null);
}
startBackupTask(auditSource, callback);
backup(auditSource, callback);
});
});
}
@@ -9,10 +9,17 @@ if (process.argv[2] === '--check') return console.log('OK');
require('supererror')({ splatchError: true });
// remove timestamp from debug() based output
require('debug').formatArgs = function formatArgs(args) {
args[0] = this.namespace + ' ' + args[0];
};
var assert = require('assert'),
backups = require('../backups.js'),
database = require('../database.js'),
debug = require('debug')('box:backupupload');
backups = require('./backups.js'),
database = require('./database.js'),
debug = require('debug')('box:backuptask'),
paths = require('./paths.js'),
safe = require('safetydance');
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -31,21 +38,17 @@ process.on('SIGTERM', function () {
process.exit(0);
});
// this can happen when the backup task is terminated (not box code)
process.on('disconnect', function () {
debug('parent process died');
process.exit(0);
});
initialize(function (error) {
if (error) throw error;
backups.upload(backupId, format, dataDir, (progress) => process.send(progress), function resultHandler(error) {
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, '');
backups.upload(backupId, format, dataDir, function resultHandler(error) {
if (error) debug('upload completed with error', error);
debug('upload completed');
process.send({ result: error ? error.message : '' });
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, error ? error.message : '');
// https://nodejs.org/api/process.html are exit codes used by node. apps.js uses the value below
// to check apptask crashes
+137 -1
View File
@@ -4,18 +4,29 @@ exports = module.exports = {
verifySetupToken: verifySetupToken,
setupDone: setupDone,
changePlan: changePlan,
upgrade: upgrade,
sendHeartbeat: sendHeartbeat,
getBoxAndUserDetails: getBoxAndUserDetails,
setPtrRecord: setPtrRecord,
CaasError: CaasError
};
var assert = require('assert'),
backups = require('./backups.js'),
config = require('./config.js'),
debug = require('debug')('box:caas'),
locker = require('./locker.js'),
path = require('path'),
progress = require('./progress.js'),
settings = require('./settings.js'),
shell = require('./shell.js'),
superagent = require('superagent'),
util = require('util');
util = require('util'),
_ = require('underscore');
const RETIRE_CMD = path.join(__dirname, 'scripts/retire.sh');
function CaasError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
@@ -42,6 +53,20 @@ CaasError.INVALID_TOKEN = 'Invalid Token';
CaasError.INTERNAL_ERROR = 'Internal Error';
CaasError.EXTERNAL_ERROR = 'External Error';
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
function retire(reason, info, callback) {
assert(reason === 'migrate' || reason === 'upgrade');
info = info || { };
callback = callback || NOOP_CALLBACK;
var data = {
apiServerOrigin: config.apiServerOrigin(),
adminFqdn: config.adminFqdn()
};
shell.sudo('retire', [ RETIRE_CMD, reason, JSON.stringify(info), JSON.stringify(data) ], callback);
}
function getCaasConfig(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -92,6 +117,96 @@ function setupDone(setupToken, callback) {
});
});
}
function doMigrate(options, caasConfig, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof caasConfig, 'object');
assert.strictEqual(typeof callback, 'function');
var error = locker.lock(locker.OP_MIGRATE);
if (error) return callback(new CaasError(CaasError.BAD_STATE, error.message));
function unlock(error) {
debug('Failed to migrate', error);
locker.unlock(locker.OP_MIGRATE);
progress.set(progress.MIGRATE, -1, 'Backup failed: ' + error.message);
}
progress.set(progress.MIGRATE, 10, 'Backing up for migration');
// initiate the migration in the background
backups.backupBoxAndApps({ userId: null, username: 'migrator' }, function (error) {
if (error) return unlock(error);
debug('migrate: domain: %s size %s region %s', options.domain, options.size, options.region);
superagent
.post(config.apiServerOrigin() + '/api/v1/boxes/' + caasConfig.boxId + '/migrate')
.query({ token: caasConfig.token })
.send(options)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return unlock(error); // network error
if (result.statusCode === 409) return unlock(new CaasError(CaasError.BAD_STATE));
if (result.statusCode === 404) return unlock(new CaasError(CaasError.NOT_FOUND));
if (result.statusCode !== 202) return unlock(new CaasError(CaasError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
progress.set(progress.MIGRATE, 10, 'Migrating');
retire('migrate', _.pick(options, 'domain', 'size', 'region'));
});
});
callback(null);
}
function changePlan(options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
if (config.isDemo()) return callback(new CaasError(CaasError.BAD_FIELD, 'Not allowed in demo mode'));
getCaasConfig(function (error, result) {
if (error) return callback(error);
doMigrate(options, result, callback);
});
}
// this function expects a lock
function upgrade(boxUpdateInfo, callback) {
assert(boxUpdateInfo !== null && typeof boxUpdateInfo === 'object');
function upgradeError(e) {
progress.set(progress.UPDATE, -1, e.message);
callback(e);
}
progress.set(progress.UPDATE, 5, 'Backing up for upgrade');
backups.backupBoxAndApps({ userId: null, username: 'upgrader' }, function (error) {
if (error) return upgradeError(error);
getCaasConfig(function (error, result) {
if (error) return upgradeError(error);
superagent.post(config.apiServerOrigin() + '/api/v1/boxes/' + result.boxId + '/upgrade')
.query({ token: result.token })
.send({ version: boxUpdateInfo.version })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return upgradeError(new Error('Network error making upgrade request: ' + error));
if (result.statusCode !== 202) return upgradeError(new Error(util.format('Server not ready to upgrade. statusCode: %s body: %j', result.status, result.body)));
progress.set(progress.UPDATE, 10, 'Updating base system');
// no need to unlock since this is the last thing we ever do on this box
callback();
retire('upgrade');
});
});
});
}
function sendHeartbeat() {
assert(config.provider() === 'caas', 'Heartbeat is only sent for managed cloudrons');
@@ -108,6 +223,27 @@ function sendHeartbeat() {
});
}
function getBoxAndUserDetails(callback) {
assert.strictEqual(typeof callback, 'function');
if (config.provider() !== 'caas') return callback(null, {});
getCaasConfig(function (error, caasConfig) {
if (error) return callback(error);
superagent
.get(config.apiServerOrigin() + '/api/v1/boxes/' + caasConfig.boxId)
.query({ token: caasConfig.token })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(new CaasError(CaasError.EXTERNAL_ERROR, 'Cannot reach appstore'));
if (result.statusCode !== 200) return callback(new CaasError(CaasError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
return callback(null, result.body);
});
});
}
function setPtrRecord(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
+481
View File
@@ -0,0 +1,481 @@
'use strict';
var assert = require('assert'),
async = require('async'),
crypto = require('crypto'),
debug = require('debug')('box:cert/acme1'),
execSync = require('safetydance').child_process.execSync,
fs = require('fs'),
parseLinks = require('parse-links'),
path = require('path'),
paths = require('../paths.js'),
safe = require('safetydance'),
superagent = require('superagent'),
util = require('util'),
_ = require('underscore');
var CA_PROD = 'https://acme-v01.api.letsencrypt.org',
CA_STAGING = 'https://acme-staging.api.letsencrypt.org',
LE_AGREEMENT = 'https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf';
exports = module.exports = {
getCertificate: getCertificate,
// testing
_name: 'acme'
};
function Acme1Error(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
Error.call(this);
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.reason = reason;
if (typeof errorOrMessage === 'undefined') {
this.message = reason;
} else if (typeof errorOrMessage === 'string') {
this.message = errorOrMessage;
} else {
this.message = 'Internal error';
this.nestedError = errorOrMessage;
}
}
util.inherits(Acme1Error, Error);
Acme1Error.INTERNAL_ERROR = 'Internal Error';
Acme1Error.EXTERNAL_ERROR = 'External Error';
Acme1Error.ALREADY_EXISTS = 'Already Exists';
Acme1Error.NOT_COMPLETED = 'Not Completed';
Acme1Error.FORBIDDEN = 'Forbidden';
// http://jose.readthedocs.org/en/latest/
// https://www.ietf.org/proceedings/92/slides/slides-92-acme-1.pdf
// https://community.letsencrypt.org/t/list-of-client-implementations/2103
function Acme1(options) {
assert.strictEqual(typeof options, 'object');
this.caOrigin = options.prod ? CA_PROD : CA_STAGING;
this.accountKeyPem = null; // Buffer
this.email = options.email;
}
Acme1.prototype.getNonce = function (callback) {
superagent.get(this.caOrigin + '/directory').timeout(30 * 1000).end(function (error, response) {
if (error && !error.response) return callback(error);
if (response.statusCode !== 200) return callback(new Error('Invalid response code when fetching nonce : ' + response.statusCode));
return callback(null, response.headers['Replay-Nonce'.toLowerCase()]);
});
};
// urlsafe base64 encoding (jose)
function urlBase64Encode(string) {
return string.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}
function b64(str) {
var buf = util.isBuffer(str) ? str : new Buffer(str);
return urlBase64Encode(buf.toString('base64'));
}
function getModulus(pem) {
assert(util.isBuffer(pem));
var stdout = execSync('openssl rsa -modulus -noout', { input: pem, encoding: 'utf8' });
if (!stdout) return null;
var match = stdout.match(/Modulus=([0-9a-fA-F]+)$/m);
if (!match) return null;
return Buffer.from(match[1], 'hex');
}
Acme1.prototype.sendSignedRequest = function (url, payload, callback) {
assert.strictEqual(typeof url, 'string');
assert.strictEqual(typeof payload, 'string');
assert.strictEqual(typeof callback, 'function');
assert(util.isBuffer(this.accountKeyPem));
var that = this;
var header = {
alg: 'RS256',
jwk: {
e: b64(Buffer.from([0x01, 0x00, 0x01])), // exponent - 65537
kty: 'RSA',
n: b64(getModulus(this.accountKeyPem))
}
};
var payload64 = b64(payload);
this.getNonce(function (error, nonce) {
if (error) return callback(error);
debug('sendSignedRequest: using nonce %s for url %s', nonce, url);
var protected64 = b64(JSON.stringify(_.extend({ }, header, { nonce: nonce })));
var signer = crypto.createSign('RSA-SHA256');
signer.update(protected64 + '.' + payload64, 'utf8');
var signature64 = urlBase64Encode(signer.sign(that.accountKeyPem, 'base64'));
var data = {
header: header,
protected: protected64,
payload: payload64,
signature: signature64
};
superagent.post(url).set('Content-Type', 'application/x-www-form-urlencoded').send(JSON.stringify(data)).timeout(30 * 1000).end(function (error, res) {
if (error && !error.response) return callback(error); // network errors
callback(null, res);
});
});
};
Acme1.prototype.updateContact = function (registrationUri, callback) {
assert.strictEqual(typeof registrationUri, 'string');
assert.strictEqual(typeof callback, 'function');
debug('updateContact: %s %s', registrationUri, this.email);
// https://github.com/ietf-wg-acme/acme/issues/30
var payload = {
resource: 'reg',
contact: [ 'mailto:' + this.email ],
agreement: LE_AGREEMENT
};
var that = this;
this.sendSignedRequest(registrationUri, JSON.stringify(payload), function (error, result) {
if (error) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when registering user: ' + error.message));
if (result.statusCode !== 202) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, util.format('Failed to update contact. Expecting 202, got %s %s', result.statusCode, result.text)));
debug('updateContact: contact of user updated to %s', that.email);
callback();
});
};
Acme1.prototype.registerUser = function (callback) {
assert.strictEqual(typeof callback, 'function');
var payload = {
resource: 'new-reg',
contact: [ 'mailto:' + this.email ],
agreement: LE_AGREEMENT
};
debug('registerUser: %s', this.email);
var that = this;
this.sendSignedRequest(this.caOrigin + '/acme/new-reg', JSON.stringify(payload), function (error, result) {
if (error) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when registering user: ' + error.message));
if (result.statusCode === 409) return that.updateContact(result.headers.location, callback); // already exists
if (result.statusCode !== 201) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, util.format('Failed to register user. Expecting 201, got %s %s', result.statusCode, result.text)));
debug('registerUser: registered user %s', that.email);
callback(null);
});
};
Acme1.prototype.registerDomain = function (domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
var payload = {
resource: 'new-authz',
identifier: {
type: 'dns',
value: domain
}
};
debug('registerDomain: %s', domain);
this.sendSignedRequest(this.caOrigin + '/acme/new-authz', JSON.stringify(payload), function (error, result) {
if (error) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when registering domain: ' + error.message));
if (result.statusCode === 403) return callback(new Acme1Error(Acme1Error.FORBIDDEN, result.body.detail));
if (result.statusCode !== 201) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, util.format('Failed to register user. Expecting 201, got %s %s', result.statusCode, result.text)));
debug('registerDomain: registered %s', domain);
callback(null, result.body);
});
};
Acme1.prototype.prepareHttpChallenge = function (challenge, callback) {
assert.strictEqual(typeof challenge, 'object');
assert.strictEqual(typeof callback, 'function');
debug('prepareHttpChallenge: preparing for challenge %j', challenge);
var token = challenge.token;
assert(util.isBuffer(this.accountKeyPem));
var jwk = {
e: b64(Buffer.from([0x01, 0x00, 0x01])), // Exponent - 65537
kty: 'RSA',
n: b64(getModulus(this.accountKeyPem))
};
var shasum = crypto.createHash('sha256');
shasum.update(JSON.stringify(jwk));
var thumbprint = urlBase64Encode(shasum.digest('base64'));
var keyAuthorization = token + '.' + thumbprint;
debug('prepareHttpChallenge: writing %s to %s', keyAuthorization, path.join(paths.ACME_CHALLENGES_DIR, token));
fs.writeFile(path.join(paths.ACME_CHALLENGES_DIR, token), token + '.' + thumbprint, function (error) {
if (error) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, error));
callback();
});
};
Acme1.prototype.notifyChallengeReady = function (challenge, callback) {
assert.strictEqual(typeof challenge, 'object');
assert.strictEqual(typeof callback, 'function');
debug('notifyChallengeReady: %s was met', challenge.uri);
var keyAuthorization = fs.readFileSync(path.join(paths.ACME_CHALLENGES_DIR, challenge.token), 'utf8');
var payload = {
resource: 'challenge',
keyAuthorization: keyAuthorization
};
this.sendSignedRequest(challenge.uri, JSON.stringify(payload), function (error, result) {
if (error) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when notifying challenge: ' + error.message));
if (result.statusCode !== 202) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, util.format('Failed to notify challenge. Expecting 202, got %s %s', result.statusCode, result.text)));
callback();
});
};
Acme1.prototype.waitForChallenge = function (challenge, callback) {
assert.strictEqual(typeof challenge, 'object');
assert.strictEqual(typeof callback, 'function');
debug('waitingForChallenge: %j', challenge);
async.retry({ times: 10, interval: 5000 }, function (retryCallback) {
debug('waitingForChallenge: getting status');
superagent.get(challenge.uri).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) {
debug('waitForChallenge: network error getting uri %s', challenge.uri);
return retryCallback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, error.message)); // network error
}
if (result.statusCode !== 202) {
debug('waitForChallenge: invalid response code getting uri %s', result.statusCode);
return retryCallback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Bad response code:' + result.statusCode));
}
debug('waitForChallenge: status is "%s %j', result.body.status, result.body);
if (result.body.status === 'pending') return retryCallback(new Acme1Error(Acme1Error.NOT_COMPLETED));
else if (result.body.status === 'valid') return retryCallback();
else return retryCallback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Unexpected status: ' + result.body.status));
});
}, function retryFinished(error) {
// async.retry will pass 'undefined' as second arg making it unusable with async.waterfall()
callback(error);
});
};
// https://community.letsencrypt.org/t/public-beta-rate-limits/4772 for rate limits
Acme1.prototype.signCertificate = function (domain, csrDer, callback) {
assert.strictEqual(typeof domain, 'string');
assert(util.isBuffer(csrDer));
assert.strictEqual(typeof callback, 'function');
var outdir = paths.APP_CERTS_DIR;
var payload = {
resource: 'new-cert',
csr: b64(csrDer)
};
debug('signCertificate: sending new-cert request');
this.sendSignedRequest(this.caOrigin + '/acme/new-cert', JSON.stringify(payload), function (error, result) {
if (error) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when signing certificate: ' + error.message));
// 429 means we reached the cert limit for this domain
if (result.statusCode !== 201) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, util.format('Failed to sign certificate. Expecting 201, got %s %s', result.statusCode, result.text)));
var certUrl = result.headers.location;
if (!certUrl) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Missing location in downloadCertificate'));
safe.fs.writeFileSync(path.join(outdir, domain + '.url'), certUrl, 'utf8'); // maybe use for renewal
return callback(null, result.headers.location);
});
};
Acme1.prototype.createKeyAndCsr = function (domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
var outdir = paths.APP_CERTS_DIR;
var csrFile = path.join(outdir, domain + '.csr');
var privateKeyFile = path.join(outdir, domain + '.key');
if (safe.fs.existsSync(privateKeyFile)) {
// in some old releases, csr file was corrupt. so always regenerate it
debug('createKeyAndCsr: reuse the key for renewal at %s', privateKeyFile);
} else {
var key = execSync('openssl genrsa 4096');
if (!key) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error));
if (!safe.fs.writeFileSync(privateKeyFile, key)) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error));
debug('createKeyAndCsr: key file saved at %s', privateKeyFile);
}
var csrDer = execSync(util.format('openssl req -new -key %s -outform DER -subj /CN=%s', privateKeyFile, domain));
if (!csrDer) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error));
if (!safe.fs.writeFileSync(csrFile, csrDer)) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error)); // bookkeeping
debug('createKeyAndCsr: csr file (DER) saved at %s', csrFile);
callback(null, csrDer);
};
// TODO: download the chain in a loop following 'up' header
Acme1.prototype.downloadChain = function (linkHeader, callback) {
if (!linkHeader) return new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Empty link header when downloading certificate chain');
debug('downloadChain: linkHeader %s', linkHeader);
var linkInfo = parseLinks(linkHeader);
if (!linkInfo || !linkInfo.up) return new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Failed to parse link header when downloading certificate chain');
var intermediateCertUrl = linkInfo.up.startsWith('https://') ? linkInfo.up : (this.caOrigin + linkInfo.up);
debug('downloadChain: downloading from %s', intermediateCertUrl);
superagent.get(intermediateCertUrl).buffer().parse(function (res, done) {
var data = [ ];
res.on('data', function(chunk) { data.push(chunk); });
res.on('end', function () { res.text = Buffer.concat(data); done(); });
}).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when downloading certificate'));
if (result.statusCode !== 200) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, util.format('Failed to get cert. Expecting 200, got %s %s', result.statusCode, result.text)));
var chainDer = result.text;
var chainPem = execSync('openssl x509 -inform DER -outform PEM', { input: chainDer }); // this is really just base64 encoding with header
if (!chainPem) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error));
callback(null, chainPem);
});
};
Acme1.prototype.downloadCertificate = function (domain, certUrl, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof certUrl, 'string');
assert.strictEqual(typeof callback, 'function');
var outdir = paths.APP_CERTS_DIR;
var that = this;
superagent.get(certUrl).buffer().parse(function (res, done) {
var data = [ ];
res.on('data', function(chunk) { data.push(chunk); });
res.on('end', function () { res.text = Buffer.concat(data); done(); });
}).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when downloading certificate'));
if (result.statusCode === 202) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, 'Retry not implemented yet'));
if (result.statusCode !== 200) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, util.format('Failed to get cert. Expecting 200, got %s %s', result.statusCode, result.text)));
var certificateDer = result.text;
safe.fs.writeFileSync(path.join(outdir, domain + '.der'), certificateDer);
debug('downloadCertificate: cert der file for %s saved', domain);
var certificatePem = execSync('openssl x509 -inform DER -outform PEM', { input: certificateDer }); // this is really just base64 encoding with header
if (!certificatePem) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error));
that.downloadChain(result.header['link'], function (error, chainPem) {
if (error) return callback(error);
var certificateFile = path.join(outdir, domain + '.cert');
var fullChainPem = Buffer.concat([certificatePem, chainPem]);
if (!safe.fs.writeFileSync(certificateFile, fullChainPem)) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error));
debug('downloadCertificate: cert file for %s saved at %s', domain, certificateFile);
callback();
});
});
};
Acme1.prototype.acmeFlow = function (domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
if (!fs.existsSync(paths.ACME_ACCOUNT_KEY_FILE)) {
debug('getCertificate: generating acme account key on first run');
this.accountKeyPem = safe.child_process.execSync('openssl genrsa 4096');
if (!this.accountKeyPem) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error));
safe.fs.writeFileSync(paths.ACME_ACCOUNT_KEY_FILE, this.accountKeyPem);
} else {
debug('getCertificate: using existing acme account key');
this.accountKeyPem = fs.readFileSync(paths.ACME_ACCOUNT_KEY_FILE);
}
var that = this;
this.registerUser(function (error) {
if (error) return callback(error);
that.registerDomain(domain, function (error, result) {
if (error) return callback(error);
debug('acmeFlow: challenges: %j', result);
var httpChallenges = result.challenges.filter(function(x) { return x.type === 'http-01'; });
if (httpChallenges.length === 0) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'no http challenges'));
var challenge = httpChallenges[0];
async.waterfall([
that.prepareHttpChallenge.bind(that, challenge),
that.notifyChallengeReady.bind(that, challenge),
that.waitForChallenge.bind(that, challenge),
that.createKeyAndCsr.bind(that, domain),
that.signCertificate.bind(that, domain),
that.downloadCertificate.bind(that, domain)
], callback);
});
});
};
Acme1.prototype.getCertificate = function (hostname, domain, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
debug('getCertificate: start acme flow for %s from %s', hostname, this.caOrigin);
this.acmeFlow(hostname, function (error) {
if (error) return callback(error);
var outdir = paths.APP_CERTS_DIR;
callback(null, path.join(outdir, hostname + '.cert'), path.join(outdir, hostname + '.key'));
});
};
function getCertificate(hostname, domain, options, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var acme = new Acme1(options || { });
acme.getCertificate(hostname, domain, callback);
}
+4 -3
View File
@@ -5,6 +5,7 @@ var assert = require('assert'),
crypto = require('crypto'),
debug = require('debug')('box:cert/acme2'),
domains = require('../domains.js'),
execSync = require('safetydance').child_process.execSync,
fs = require('fs'),
path = require('path'),
paths = require('../paths.js'),
@@ -87,7 +88,7 @@ function b64(str) {
function getModulus(pem) {
assert(util.isBuffer(pem));
var stdout = safe.child_process.execSync('openssl rsa -modulus -noout', { input: pem, encoding: 'utf8' });
var stdout = execSync('openssl rsa -modulus -noout', { input: pem, encoding: 'utf8' });
if (!stdout) return null;
var match = stdout.match(/Modulus=([0-9a-fA-F]+)$/m);
if (!match) return null;
@@ -350,14 +351,14 @@ Acme2.prototype.createKeyAndCsr = function (hostname, callback) {
// in some old releases, csr file was corrupt. so always regenerate it
debug('createKeyAndCsr: reuse the key for renewal at %s', privateKeyFile);
} else {
var key = safe.child_process.execSync('openssl genrsa 4096');
var key = execSync('openssl genrsa 4096');
if (!key) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error));
if (!safe.fs.writeFileSync(privateKeyFile, key)) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error));
debug('createKeyAndCsr: key file saved at %s', privateKeyFile);
}
var csrDer = safe.child_process.execSync(`openssl req -new -key ${privateKeyFile} -outform DER -subj /CN=${hostname}`);
var csrDer = execSync(`openssl req -new -key ${privateKeyFile} -outform DER -subj /CN=${hostname}`);
if (!csrDer) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error));
if (!safe.fs.writeFileSync(csrFile, csrDer)) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error)); // bookkeeping
+44 -66
View File
@@ -11,12 +11,9 @@ exports = module.exports = {
getStatus: getStatus,
reboot: reboot,
isRebootRequired: isRebootRequired,
onActivated: onActivated,
setDashboardDomain: setDashboardDomain,
renewCerts: renewCerts,
checkDiskSpace: checkDiskSpace,
@@ -26,26 +23,24 @@ exports = module.exports = {
var assert = require('assert'),
async = require('async'),
clients = require('./clients.js'),
config = require('./config.js'),
cron = require('./cron.js'),
debug = require('debug')('box:cloudron'),
domains = require('./domains.js'),
DomainsError = require('./domains.js').DomainsError,
df = require('@sindresorhus/df'),
fs = require('fs'),
mailer = require('./mailer.js'),
os = require('os'),
path = require('path'),
paths = require('./paths.js'),
platform = require('./platform.js'),
progress = require('./progress.js'),
reverseProxy = require('./reverseproxy.js'),
safe = require('safetydance'),
settings = require('./settings.js'),
shell = require('./shell.js'),
spawn = require('child_process').spawn,
split = require('split'),
sysinfo = require('./sysinfo.js'),
tasks = require('./tasks.js'),
users = require('./users.js'),
util = require('util');
@@ -87,6 +82,8 @@ CloudronError.INTERNAL_ERROR = 'Internal Error';
CloudronError.EXTERNAL_ERROR = 'External Error';
CloudronError.BAD_STATE = 'Bad state';
CloudronError.ALREADY_UPTODATE = 'No Update Available';
CloudronError.NOT_FOUND = 'Not found';
CloudronError.SELF_UPGRADE_NOT_SUPPORTED = 'Self upgrade not supported';
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -174,6 +171,7 @@ function getConfig(callback) {
adminFqdn: config.adminFqdn(),
mailFqdn: config.mailFqdn(),
version: config.version(),
progress: progress.getAll(),
isDemo: config.isDemo(),
edition: config.edition(),
memory: os.totalmem(),
@@ -184,14 +182,7 @@ function getConfig(callback) {
}
function reboot(callback) {
shell.sudo('reboot', [ REBOOT_CMD ], {}, callback);
}
function isRebootRequired(callback) {
assert.strictEqual(typeof callback, 'function');
// https://serverfault.com/questions/92932/how-does-ubuntu-keep-track-of-the-system-restart-required-flag-in-motd
callback(null, fs.existsSync('/var/run/reboot-required'));
shell.sudo('reboot', [ REBOOT_CMD ], callback);
}
function checkDiskSpace(callback) {
@@ -253,28 +244,49 @@ function getLogs(unit, options, callback) {
debug('Getting logs for %s as %s', unit, format);
let args = [ '--lines=' + lines ];
if (follow) args.push('--follow');
var cp, transformStream;
if (unit === 'box') {
let args = [ '--no-pager', `--lines=${lines}` ];
if (format === 'short') args.push('--output=short', '-a'); else args.push('--output=json');
if (follow) args.push('--follow');
args.push('--unit=box');
args.push('--unit=cloudron-updater');
cp = spawn('/bin/journalctl', args);
// need to handle box.log without subdir
if (unit === 'box') args.push(path.join(paths.LOG_DIR, 'box.log'));
else args.push(path.join(paths.LOG_DIR, unit, 'app.log'));
transformStream = split(function mapper(line) {
if (format !== 'json') return line + '\n';
var cp = spawn('/usr/bin/tail', args);
var obj = safe.JSON.parse(line);
if (!obj) return undefined;
var transformStream = split(function mapper(line) {
if (format !== 'json') return line + '\n';
return JSON.stringify({
realtimeTimestamp: obj.__REALTIME_TIMESTAMP,
monotonicTimestamp: obj.__MONOTONIC_TIMESTAMP,
message: obj.MESSAGE,
source: obj.SYSLOG_IDENTIFIER || ''
}) + '\n';
});
} else { // mail, mongodb, mysql, postgresql, backup
let args = [ '--lines=' + lines ];
if (follow) args.push('--follow');
args.push(path.join(paths.LOG_DIR, unit, 'app.log'));
var data = line.split(' '); // logs are <ISOtimestamp> <msg>
var timestamp = (new Date(data[0])).getTime();
if (isNaN(timestamp)) timestamp = 0;
cp = spawn('/usr/bin/tail', args);
return JSON.stringify({
realtimeTimestamp: timestamp * 1000,
message: line.slice(data[0].length+1),
source: unit
}) + '\n';
});
transformStream = split(function mapper(line) {
if (format !== 'json') return line + '\n';
var data = line.split(' '); // logs are <ISOtimestamp> <msg>
var timestamp = (new Date(data[0])).getTime();
if (isNaN(timestamp)) timestamp = 0;
return JSON.stringify({
realtimeTimestamp: timestamp * 1000,
message: line.slice(data[0].length+1),
source: unit
}) + '\n';
});
}
transformStream.close = cp.kill.bind(cp, 'SIGKILL'); // closing stream kills the child process
@@ -353,37 +365,3 @@ function getStatus(callback) {
});
});
}
function setDashboardDomain(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
debug(`setDashboardDomain: ${domain}`);
domains.get(domain, function (error, result) {
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new CloudronError(CloudronError.BAD_FIELD, 'No such domain'));
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
config.setAdminDomain(result.domain);
config.setAdminLocation('my');
config.setAdminFqdn('my' + (result.config.hyphenatedSubdomains ? '-' : '.') + result.domain);
clients.addDefaultClients(config.adminOrigin(), function (error) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
callback(null);
configureWebadmin(NOOP_CALLBACK); // ## trigger as task
});
});
}
function renewCerts(options, auditSource, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
let task = tasks.startTask(tasks.TASK_RENEW_CERTS, [ options, auditSource ]);
task.on('error', (error) => callback(new CloudronError(CloudronError.INTERNAL_ERROR, error)));
task.on('start', (taskId) => callback(null, taskId));
}
+1 -1
View File
@@ -186,7 +186,7 @@ function recreateJobs(tz) {
if (gJobs.certificateRenew) gJobs.certificateRenew.stop();
gJobs.certificateRenew = new CronJob({
cronTime: '00 00 */12 * * *', // every 12 hours
onTick: cloudron.renewCerts.bind(null, {}, AUDIT_SOURCE, NOOP_CALLBACK),
onTick: reverseProxy.renewAll.bind(null, AUDIT_SOURCE, NOOP_CALLBACK),
start: true,
timeZone: tz
});
+11 -47
View File
@@ -1,13 +1,9 @@
'use strict';
exports = module.exports = {
DockerError: DockerError,
connection: connectionInstance(),
setRegistryConfig: setRegistryConfig,
ping: ping,
downloadImage: downloadImage,
createContainer: createContainer,
startContainer: startContainer,
@@ -22,26 +18,24 @@ exports = module.exports = {
getContainerIdByIp: getContainerIdByIp,
inspect: inspect,
inspectByName: inspect,
memoryUsage: memoryUsage,
execContainer: execContainer,
createVolume: createVolume,
removeVolume: removeVolume,
clearVolume: clearVolume
};
// timeout is optional
function connectionInstance(timeout) {
function connectionInstance() {
var Docker = require('dockerode');
var docker;
if (process.env.BOX_ENV === 'test') {
// test code runs a docker proxy on this port
docker = new Docker({ host: 'http://localhost', port: 5687, timeout: timeout });
docker = new Docker({ host: 'http://localhost', port: 5687 });
// proxy code uses this to route to the real docker
docker.options = { socketPath: '/var/run/docker.sock' };
} else {
docker = new Docker({ socketPath: '/var/run/docker.sock', timeout: timeout });
docker = new Docker({ socketPath: '/var/run/docker.sock' });
}
return docker;
@@ -86,7 +80,6 @@ function DockerError(reason, errorOrMessage) {
}
util.inherits(DockerError, Error);
DockerError.INTERNAL_ERROR = 'Internal Error';
DockerError.NOT_FOUND = 'Not found';
DockerError.BAD_FIELD = 'Bad field';
function debugApp(app, args) {
@@ -111,26 +104,12 @@ function setRegistryConfig(auth, callback) {
});
}
function ping(callback) {
assert.strictEqual(typeof callback, 'function');
// do not let the request linger
var docker = connectionInstance(1000);
docker.ping(function (error, result) {
if (error) return callback(new DockerError(DockerError.INTERNAL_ERROR, error));
if (result !== 'OK') return callback(new DockerError(DockerError.INTERNAL_ERROR, 'Unable to ping the docker daemon'));
callback(null);
});
}
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.spawn('pullImage', '/usr/bin/docker', [ 'pull', manifest.dockerImage ], {}, function (error) {
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'));
@@ -182,10 +161,8 @@ function createSubcontainer(app, name, cmd, options, callback) {
var manifest = app.manifest;
var exposedPorts = {}, dockerPortBindings = { };
var domain = app.fqdn;
// TODO: these should all have the CLOUDRON_ prefix
var stdEnv = [
'CLOUDRON=1',
'CLOUDRON_PROXY_IP=172.18.0.1',
'WEBADMIN_ORIGIN=' + config.adminOrigin(),
'API_ORIGIN=' + config.adminOrigin(),
'APP_ORIGIN=https://' + domain,
@@ -227,6 +204,9 @@ function createSubcontainer(app, name, cmd, options, callback) {
// 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);
addons.getEnvironment(app, function (error, addonEnv) {
if (error) return callback(new Error('Error getting addon environment : ' + error));
@@ -277,7 +257,7 @@ function createSubcontainer(app, name, cmd, options, callback) {
NetworkMode: 'cloudron',
Dns: ['172.18.0.1'], // use internal dns
DnsSearch: ['.'], // use internal dns
SecurityOpt: [ 'apparmor=docker-cloudron-app' ]
SecurityOpt: enableSecurityOpt ? [ 'apparmor=docker-cloudron-app' ] : null // profile available only on cloudron
}
};
@@ -465,23 +445,7 @@ function inspect(containerId, callback) {
var container = exports.connection.getContainer(containerId);
container.inspect(function (error, result) {
if (error && error.statusCode === 404) return callback(new DockerError(DockerError.NOT_FOUND));
if (error) return callback(new DockerError(DockerError.INTERNAL_ERROR, error));
callback(null, result);
});
}
function memoryUsage(containerId, callback) {
assert.strictEqual(typeof containerId, 'string');
assert.strictEqual(typeof callback, 'function');
var container = exports.connection.getContainer(containerId);
container.stats({ stream: false }, function (error, result) {
if (error && error.statusCode === 404) return callback(new DockerError(DockerError.NOT_FOUND));
if (error) return callback(new DockerError(DockerError.INTERNAL_ERROR, error));
if (error) return callback(error);
callback(null, result);
});
}
@@ -558,7 +522,7 @@ function clearVolume(app, name, subdir, callback) {
assert.strictEqual(typeof subdir, 'string');
assert.strictEqual(typeof callback, 'function');
shell.sudo('removeVolume', [ RMVOLUME_CMD, app.id, subdir ], {}, callback);
shell.sudo('removeVolume', [ RMVOLUME_CMD, app.id, subdir ], callback);
}
function removeVolume(app, name, subdir, callback) {
@@ -576,6 +540,6 @@ function removeVolume(app, name, subdir, callback) {
callback(error);
}
shell.sudo('removeVolume', [ RMVOLUME_CMD, app.id, subdir ], {}, callback);
shell.sudo('removeVolume', [ RMVOLUME_CMD, app.id, subdir ], callback);
});
}
+2 -1
View File
@@ -8,7 +8,8 @@ exports = module.exports = {
getAll: getAll,
update: update,
del: del,
clear: clear
_clear: clear
};
var assert = require('assert'),
+47 -11
View File
@@ -6,10 +6,12 @@ module.exports = exports = {
getAll: getAll,
update: update,
del: del,
clear: clear,
isLocked: isLocked,
renewCerts: renewCerts,
fqdn: fqdn,
setAdmin: setAdmin,
getDnsRecords: getDnsRecords,
upsertDnsRecords: upsertDnsRecords,
@@ -33,20 +35,26 @@ module.exports = exports = {
};
var assert = require('assert'),
caas = require('./caas.js'),
config = require('./config.js'),
constants = require('./constants.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:domains'),
domaindb = require('./domaindb.js'),
eventlog = require('./eventlog.js'),
path = require('path'),
reverseProxy = require('./reverseproxy.js'),
ReverseProxyError = reverseProxy.ReverseProxyError,
safe = require('safetydance'),
shell = require('./shell.js'),
sysinfo = require('./sysinfo.js'),
tld = require('tldjs'),
util = require('util'),
_ = require('underscore');
var RESTART_CMD = path.join(__dirname, 'scripts/restart.sh');
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
function DomainsError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
@@ -361,16 +369,6 @@ function del(domain, auditSource, callback) {
});
}
function clear(callback) {
assert.strictEqual(typeof callback, 'function');
domaindb.clear(function (error) {
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
return callback(null);
});
}
// returns the 'name' that needs to be inserted into zone
function getName(domain, subdomain, type) {
// hack for supporting special caas domains. if we want to remove this, we have to fix the appstore domain API first
@@ -470,6 +468,31 @@ function waitForDnsRecord(subdomain, domain, type, value, options, callback) {
});
}
function setAdmin(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
debug('setAdmin domain:%s', domain);
get(domain, function (error, result) {
if (error) return callback(error);
var setPtrRecord = config.provider() === 'caas' ? caas.setPtrRecord : function (d, next) { next(); };
setPtrRecord(domain, function (error) {
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, 'Error setting PTR record:' + error.message));
config.setAdminDomain(result.domain);
config.setAdminLocation('my');
config.setAdminFqdn('my' + (result.config.hyphenatedSubdomains ? '-' : '.') + result.domain);
callback();
shell.sudo('restart', [ RESTART_CMD ], NOOP_CALLBACK);
});
});
}
// removes all fields that are strictly private and should never be returned by API calls
function removePrivateFields(domain) {
var result = _.pick(domain, 'domain', 'zoneName', 'provider', 'config', 'tlsConfig', 'fallbackCertificate', 'locked');
@@ -494,3 +517,16 @@ function makeWildcard(hostname) {
parts[0] = '*';
return parts.join('.');
}
function renewCerts(domain, auditSource, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
// trigger renewal in the background
reverseProxy.renewCerts({ domain: domain }, auditSource, function (error) {
debug('renewCerts', error);
});
callback();
}
+4 -2
View File
@@ -31,10 +31,12 @@ function startGraphite(existingInfra, callback) {
--dns-search=. \
-p 127.0.0.1:2003:2003 \
-p 127.0.0.1:2004:2004 \
-p 127.0.0.1:8417:8000 \
-p 127.0.0.1:8000:8000 \
-v "${dataDir}/graphite:/var/lib/graphite" \
--label isCloudronManaged=true \
--read-only -v /tmp -v /run "${tag}"`;
shell.exec('startGraphite', cmd, callback);
shell.execSync('startGraphite', cmd);
callback();
}
+14 -14
View File
@@ -34,7 +34,7 @@ function get(groupId, callback) {
assert.strictEqual(typeof groupId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + GROUPS_FIELDS + ' FROM userGroups WHERE id = ? ORDER BY name', [ groupId ], function (error, result) {
database.query('SELECT ' + GROUPS_FIELDS + ' FROM groups WHERE id = ? ORDER BY name', [ groupId ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
@@ -47,9 +47,9 @@ function getWithMembers(groupId, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + GROUPS_FIELDS + ',GROUP_CONCAT(groupMembers.userId) AS userIds ' +
' FROM userGroups LEFT OUTER JOIN groupMembers ON userGroups.id = groupMembers.groupId ' +
' WHERE userGroups.id = ? ' +
' GROUP BY userGroups.id', [ groupId ], function (error, results) {
' FROM groups LEFT OUTER JOIN groupMembers ON groups.id = groupMembers.groupId ' +
' WHERE groups.id = ? ' +
' GROUP BY groups.id', [ groupId ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
@@ -63,7 +63,7 @@ function getWithMembers(groupId, callback) {
function getAll(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + GROUPS_FIELDS + ' FROM userGroups', function (error, results) {
database.query('SELECT ' + GROUPS_FIELDS + ' FROM groups', function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null, results);
@@ -72,8 +72,8 @@ function getAll(callback) {
function getAllWithMembers(callback) {
database.query('SELECT ' + GROUPS_FIELDS + ',GROUP_CONCAT(groupMembers.userId) AS userIds ' +
' FROM userGroups LEFT OUTER JOIN groupMembers ON userGroups.id = groupMembers.groupId ' +
' GROUP BY userGroups.id', function (error, results) {
' FROM groups LEFT OUTER JOIN groupMembers ON groups.id = groupMembers.groupId ' +
' GROUP BY groups.id', function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
@@ -88,7 +88,7 @@ function add(id, name, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('INSERT INTO userGroups (id, name) VALUES (?, ?)', [ id, name ], function (error, result) {
database.query('INSERT INTO groups (id, name) VALUES (?, ?)', [ id, name ], function (error, result) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error));
if (error || result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
@@ -112,8 +112,8 @@ function update(id, data, callback) {
}
args.push(id);
database.query('UPDATE userGroups SET ' + fields.join(', ') + ' WHERE id = ?', args, function (error, result) {
if (error && error.code === 'ER_DUP_ENTRY' && error.sqlMessage.indexOf('userGroups_name') !== -1) return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, 'name already exists'));
database.query('UPDATE groups SET ' + fields.join(', ') + ' WHERE id = ?', args, function (error, result) {
if (error && error.code === 'ER_DUP_ENTRY' && error.sqlMessage.indexOf('groups_name') !== -1) return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, 'name already exists'));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
@@ -128,7 +128,7 @@ function del(id, callback) {
// also cleanup the groupMembers table
var queries = [];
queries.push({ query: 'DELETE FROM groupMembers WHERE groupId = ?', args: [ id ] });
queries.push({ query: 'DELETE FROM userGroups WHERE id = ?', args: [ id ] });
queries.push({ query: 'DELETE FROM groups WHERE id = ?', args: [ id ] });
database.transaction(queries, function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
@@ -141,7 +141,7 @@ function del(id, callback) {
function count(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT COUNT(*) AS total FROM userGroups', function (error, result) {
database.query('SELECT COUNT(*) AS total FROM groups', function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
return callback(null, result[0].total);
@@ -152,7 +152,7 @@ function clear(callback) {
database.query('DELETE FROM groupMembers', function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
database.query('DELETE FROM userGroups', function (error) {
database.query('DELETE FROM groups', function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(error);
@@ -266,7 +266,7 @@ function getGroups(userId, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + GROUPS_FIELDS + ' ' +
' FROM userGroups INNER JOIN groupMembers ON userGroups.id = groupMembers.groupId AND groupMembers.userId = ?', [ userId ], function (error, results) {
' FROM groups INNER JOIN groupMembers ON groups.id = groupMembers.groupId AND groupMembers.userId = ?', [ userId ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null, results);
+2 -2
View File
@@ -19,7 +19,7 @@ exports = module.exports = {
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:2.0.2@sha256:6dcee0731dfb9b013ed94d56205eee219040ee806c7e251db3b3886eaa4947ff' },
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:2.0.2@sha256:95e006390ddce7db637e1672eb6f3c257d3c2652747424f529b1dee3cbe6728c' },
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:2.0.0@sha256:8a88dd334b62b578530a014ca1a2425a54cb9df1e475f5d3a36806e5cfa22121' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:2.0.1@sha256:deee3739011670d45abd8997a8a0b8d3c4cd577a93f235417614dea58338e0f9' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:2.0.2@sha256:454f035d60b768153d4f31210380271b5ba1c09367c9d95c7fa37f9e39d2f59c' }
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:2.0.0@sha256:3c0fbb2a042ac471940ac3e9f6ffa900c8a294941fb7de509b2e3309b09fbffd' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:2.0.0@sha256:454f035d60b768153d4f31210380271b5ba1c09367c9d95c7fa37f9e39d2f59c' }
}
};
+31 -23
View File
@@ -271,6 +271,7 @@ function mailboxSearch(req, res, next) {
cn: `${mailbox.name}@${mailbox.domain}`,
uid: `${mailbox.name}@${mailbox.domain}`,
mail: `${mailbox.name}@${mailbox.domain}`,
ownerType: mailbox.ownerType,
displayname: 'Max Mustermann',
givenName: 'Max',
username: 'mmustermann',
@@ -296,6 +297,9 @@ function mailboxSearch(req, res, next) {
var results = [];
// only send user mailboxes
result = result.filter(function (m) { return m.ownerType === mailboxdb.OWNER_TYPE_USER; });
// send mailbox objects
result.forEach(function (mailbox) {
var dn = ldap.parseDN(`cn=${mailbox.name}@${domain},domain=${domain},ou=mailboxes,dc=cloudron`);
@@ -307,7 +311,8 @@ function mailboxSearch(req, res, next) {
objectcategory: 'mailbox',
cn: `${mailbox.name}@${domain}`,
uid: `${mailbox.name}@${domain}`,
mail: `${mailbox.name}@${domain}`
mail: `${mailbox.name}@${domain}`,
ownerType: mailbox.ownerType
}
};
@@ -459,30 +464,30 @@ function authenticateMailbox(req, res, next) {
var parts = email.split('@');
if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString()));
const addonId = req.dn.rdns[1].attrs.ou.value.toLowerCase(); // 'sendmail' or 'recvmail'
mail.getDomain(parts[1], function (error, domain) {
if (error && error.reason === MailError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
mailboxdb.getMailbox(parts[0], parts[1], function (error, mailbox) {
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.message));
if (addonId === 'recvmail' && !domain.enabled) return next(new ldap.NoSuchObjectError(req.dn.toString()));
mail.getDomain(parts[1], function (error, domain) {
if (error && error.reason === MailError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.message));
let name;
if (addonId === 'sendmail') name = 'MAIL_SMTP_PASSWORD';
else if (addonId === 'recvmail') name = 'MAIL_IMAP_PASSWORD';
else return next(new ldap.OperationsError('Invalid DN'));
if (mailbox.ownerType === mailboxdb.OWNER_TYPE_APP) {
var addonId = req.dn.rdns[1].attrs.ou.value.toLowerCase(); // 'sendmail' or 'recvmail'
var name;
if (addonId === 'sendmail') name = 'MAIL_SMTP_PASSWORD';
else if (addonId === 'recvmail') name = 'MAIL_IMAP_PASSWORD';
else return next(new ldap.OperationsError('Invalid DN'));
// note: with sendmail addon, apps can send mail without a mailbox (unlike users)
appdb.getAppIdByAddonConfigValue(addonId, name, req.credentials || '', function (error, appId) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return next(new ldap.OperationsError(error.message));
if (appId) { // matched app password
eventlog.add(eventlog.ACTION_APP_LOGIN, { authType: 'ldap', mailboxId: email }, { appId: appId, addonId: addonId });
return res.end();
}
appdb.getAddonConfigByName(mailbox.ownerId, addonId, name, function (error, value) {
if (error) return next(new ldap.OperationsError(error.message));
if (req.credentials !== value) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
mailboxdb.getMailbox(parts[0], parts[1], function (error, mailbox) {
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.message));
eventlog.add(eventlog.ACTION_APP_LOGIN, { authType: 'ldap', mailboxId: name }, { appId: mailbox.ownerId, addonId: addonId });
return res.end();
});
} else if (mailbox.ownerType === mailboxdb.OWNER_TYPE_USER) {
if (!domain.enabled) return next(new ldap.NoSuchObjectError(req.dn.toString()));
users.verify(mailbox.ownerId, req.credentials || '', function (error, result) {
if (error && error.reason === UsersError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
@@ -492,7 +497,9 @@ function authenticateMailbox(req, res, next) {
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: email }, { userId: result.id, user: users.removePrivateFields(result) });
res.end();
});
});
} else {
return next(new ldap.OperationsError('Unknown ownerType for mailbox'));
}
});
});
}
@@ -520,8 +527,9 @@ function start(callback) {
gServer.search('ou=mailaliases,dc=cloudron', mailAliasSearch);
gServer.search('ou=mailinglists,dc=cloudron', mailingListSearch);
gServer.bind('ou=recvmail,dc=cloudron', authenticateMailbox); // dovecot
gServer.bind('ou=sendmail,dc=cloudron', authenticateMailbox); // haraka
gServer.bind('ou=mailboxes,dc=cloudron', authenticateMailbox);
gServer.bind('ou=recvmail,dc=cloudron', authenticateMailbox);
gServer.bind('ou=sendmail,dc=cloudron', authenticateMailbox);
gServer.compare('cn=users,ou=groups,dc=cloudron', authenticateApp, groupUsersCompare);
gServer.compare('cn=admins,ou=groups,dc=cloudron', authenticateApp, groupAdminsCompare);
+1
View File
@@ -18,6 +18,7 @@ Locker.prototype.OP_BOX_UPDATE = 'box_update';
Locker.prototype.OP_PLATFORM_START = 'platform_start';
Locker.prototype.OP_FULL_BACKUP = 'full_backup';
Locker.prototype.OP_APPTASK = 'apptask';
Locker.prototype.OP_MIGRATE = 'migrate';
Locker.prototype.lock = function (operation) {
assert.strictEqual(typeof operation, 'string');
-2
View File
@@ -22,8 +22,6 @@ function collectLogs(unitName, callback) {
assert.strictEqual(typeof callback, 'function');
var logs = safe.child_process.execSync('sudo ' + COLLECT_LOGS_CMD + ' ' + unitName, { encoding: 'utf8' });
if (!logs) return callback(safe.error);
logs = logs + '\n\n=====================================\n\n';
callback(null, logs);
+37 -44
View File
@@ -8,7 +8,6 @@ exports = module.exports = {
getDomain: getDomain,
addDomain: addDomain,
removeDomain: removeDomain,
clearDomains: clearDomains,
setDnsRecords: setDnsRecords,
@@ -23,11 +22,11 @@ exports = module.exports = {
sendTestMail: sendTestMail,
listMailboxes: listMailboxes,
getMailboxes: getMailboxes,
removeMailboxes: removeMailboxes,
getMailbox: getMailbox,
addMailbox: addMailbox,
updateMailboxOwner: updateMailboxOwner,
updateMailbox: updateMailbox,
removeMailbox: removeMailbox,
listAliases: listAliases,
@@ -53,13 +52,13 @@ var assert = require('assert'),
dns = require('./native-dns.js'),
domains = require('./domains.js'),
eventlog = require('./eventlog.js'),
hat = require('./hat.js'),
infra = require('./infra_version.js'),
mailboxdb = require('./mailboxdb.js'),
maildb = require('./maildb.js'),
mailer = require('./mailer.js'),
net = require('net'),
nodemailer = require('nodemailer'),
os = require('os'),
path = require('path'),
paths = require('./paths.js'),
reverseProxy = require('./reverseproxy.js'),
@@ -110,6 +109,9 @@ function validateName(name) {
// 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');
// 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');
return null;
}
@@ -553,7 +555,6 @@ function restartMail(callback) {
const tag = infra.images.mail.tag;
const memoryLimit = 4 * 256;
const cloudronToken = hat(8 * 128);
// admin and mail share the same certificate
reverseProxy.getCertificate({ fqdn: config.adminFqdn(), domain: config.adminDomain() }, function (error, bundle) {
@@ -566,35 +567,34 @@ function restartMail(callback) {
if (!safe.child_process.execSync(`cp ${bundle.certFilePath} ${mailCertFilePath}`)) return callback(new Error('Could not create cert file:' + safe.error.message));
if (!safe.child_process.execSync(`cp ${bundle.keyFilePath} ${mailKeyFilePath}`)) return callback(new Error('Could not create key file:' + safe.error.message));
shell.exec('startMail', 'docker rm -f mail || true', function (error) {
shell.execSync('startMail', 'docker rm -f mail || true');
createMailConfig(function (error, allowInbound) {
if (error) return callback(error);
createMailConfig(function (error, allowInbound) {
if (error) return callback(error);
var ports = allowInbound ? '-p 587:2525 -p 993:9993 -p 4190:4190 -p 25:2525' : '';
var ports = allowInbound ? '-p 587:2525 -p 993:9993 -p 4190:4190 -p 25:2525' : '';
const cmd = `docker run --restart=always -d --name="mail" \
--net cloudron \
--net-alias mail \
--log-driver syslog \
--log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \
--log-opt tag=mail \
-m ${memoryLimit}m \
--memory-swap ${memoryLimit * 2}m \
--dns 172.18.0.1 \
--dns-search=. \
-v "${paths.MAIL_DATA_DIR}:/app/data" \
-v "${paths.PLATFORM_DATA_DIR}/addons/mail:/etc/mail" \
${ports} \
-p 127.0.0.1:2020:2020 \
--label isCloudronManaged=true \
--read-only -v /run -v /tmp ${tag}`;
const cmd = `docker run --restart=always -d --name="mail" \
--net cloudron \
--net-alias mail \
--log-driver syslog \
--log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \
--log-opt tag=mail \
-m ${memoryLimit}m \
--memory-swap ${memoryLimit * 2}m \
--dns 172.18.0.1 \
--dns-search=. \
-e CLOUDRON_MAIL_TOKEN="${cloudronToken}" \
-v "${paths.MAIL_DATA_DIR}:/app/data" \
-v "${paths.PLATFORM_DATA_DIR}/addons/mail:/etc/mail" \
${ports} \
-p 127.0.0.1:2020:2020 \
--label isCloudronManaged=true \
--read-only -v /run -v /tmp ${tag}`;
shell.execSync('startMail', cmd);
shell.exec('startMail', cmd, callback);
});
callback();
});
});
}
@@ -628,7 +628,7 @@ function getDomain(domain, callback) {
function getDomains(callback) {
assert.strictEqual(typeof callback, 'function');
maildb.list(function (error, results) {
maildb.getAll(function (error, results) {
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
return callback(null, results);
@@ -805,16 +805,6 @@ function removeDomain(domain, callback) {
});
}
function clearDomains(callback) {
assert.strictEqual(typeof callback, 'function');
maildb.clear(function (error) {
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
callback();
});
}
function setMailFromValidation(domain, enabled, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof enabled, 'boolean');
@@ -896,7 +886,7 @@ function sendTestMail(domain, to, callback) {
});
}
function listMailboxes(domain, callback) {
function getMailboxes(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -943,7 +933,7 @@ function addMailbox(name, domain, userId, auditSource, callback) {
var error = validateName(name);
if (error) return callback(error);
mailboxdb.addMailbox(name, domain, userId, function (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`));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
@@ -953,7 +943,7 @@ function addMailbox(name, domain, userId, auditSource, callback) {
});
}
function updateMailboxOwner(name, domain, userId, callback) {
function updateMailbox(name, domain, userId, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof userId, 'string');
@@ -961,7 +951,10 @@ function updateMailboxOwner(name, domain, userId, callback) {
name = name.toLowerCase();
mailboxdb.updateMailboxOwner(name, domain, userId, function (error) {
var error = validateName(name);
if (error) return callback(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));
+17 -11
View File
@@ -4,7 +4,7 @@ exports = module.exports = {
addMailbox: addMailbox,
addGroup: addGroup,
updateMailboxOwner: updateMailboxOwner,
updateMailbox: updateMailbox,
updateList: updateList,
del: del,
@@ -29,7 +29,11 @@ exports = module.exports = {
TYPE_MAILBOX: 'mailbox',
TYPE_LIST: 'list',
TYPE_ALIAS: 'alias'
TYPE_ALIAS: 'alias',
OWNER_TYPE_USER: 'user',
OWNER_TYPE_APP: 'app',
OWNER_TYPE_GROUP: 'group' // obsolete
};
var assert = require('assert'),
@@ -38,7 +42,7 @@ var assert = require('assert'),
safe = require('safetydance'),
util = require('util');
var MAILBOX_FIELDS = [ 'name', 'type', 'ownerId', 'aliasTarget', 'creationTime', 'membersJson', 'domain' ].join(',');
var MAILBOX_FIELDS = [ 'name', 'type', 'ownerId', 'ownerType', 'aliasTarget', 'creationTime', 'membersJson', 'domain' ].join(',');
function postProcess(data) {
data.members = safe.JSON.parse(data.membersJson) || [ ];
@@ -47,13 +51,14 @@ function postProcess(data) {
return data;
}
function addMailbox(name, domain, ownerId, callback) {
function addMailbox(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) VALUES (?, ?, ?, ?)', [ name, exports.TYPE_MAILBOX, domain, ownerId ], function (error) {
database.query('INSERT INTO mailboxes (name, type, domain, ownerId, ownerType) VALUES (?, ?, ?, ?, ?)', [ name, exports.TYPE_MAILBOX, 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));
@@ -61,13 +66,14 @@ function addMailbox(name, domain, ownerId, callback) {
});
}
function updateMailboxOwner(name, domain, ownerId, 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 = ?', [ ownerId, name, domain ], function (error, result) {
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));
@@ -81,8 +87,8 @@ function addGroup(name, domain, members, callback) {
assert(Array.isArray(members));
assert.strictEqual(typeof callback, 'function');
database.query('INSERT INTO mailboxes (name, type, domain, ownerId, membersJson) VALUES (?, ?, ?, ?, ?)',
[ name, exports.TYPE_LIST, domain, 'admin', JSON.stringify(members) ], function (error) {
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));
@@ -253,8 +259,8 @@ function setAliasesForName(name, domain, aliases, callback) {
// clear existing aliases
queries.push({ query: 'DELETE FROM mailboxes WHERE aliasTarget = ? AND domain = ? AND type = ?', args: [ name, domain, exports.TYPE_ALIAS ] });
aliases.forEach(function (alias) {
queries.push({ query: 'INSERT INTO mailboxes (name, type, domain, aliasTarget, ownerId) VALUES (?, ?, ?, ?, ?)',
args: [ alias, exports.TYPE_ALIAS, domain, name, results[0].ownerId ] });
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 ] });
});
database.transaction(queries, function (error) {
+4 -5
View File
@@ -4,10 +4,10 @@ exports = module.exports = {
add: add,
del: del,
get: get,
list: list,
getAll: getAll,
update: update,
clear: clear,
_clear: clear,
TYPE_USER: 'user',
TYPE_APP: 'app',
@@ -49,8 +49,7 @@ function add(domain, callback) {
function clear(callback) {
assert.strictEqual(typeof callback, 'function');
// using TRUNCATE makes it fail foreign key check
database.query('DELETE FROM mail', [], function (error) {
database.query('TRUNCATE TABLE mail', [], function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null);
});
@@ -82,7 +81,7 @@ function get(domain, callback) {
});
}
function list(callback) {
function getAll(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + MAILDB_FIELDS + ' FROM mail ORDER BY domain', function (error, results) {
+1 -3
View File
@@ -7,6 +7,7 @@ var config = require('./config.js'),
exports = module.exports = {
CLOUDRON_DEFAULT_AVATAR_FILE: path.join(__dirname + '/../assets/avatar.png'),
INFRA_VERSION_FILE: path.join(config.baseDir(), 'platformdata/INFRA_VERSION'),
BACKUP_RESULT_FILE: path.join(config.baseDir(), 'platformdata/backup/result.txt'),
OLD_DATA_DIR: path.join(config.baseDir(), 'data'),
PLATFORM_DATA_DIR: path.join(config.baseDir(), 'platformdata'),
@@ -33,9 +34,6 @@ exports = module.exports = {
UPDATE_CHECKER_FILE: path.join(config.baseDir(), 'boxdata/updatechecker.json'),
LOG_DIR: path.join(config.baseDir(), 'platformdata/logs'),
TASKS_LOG_DIR: path.join(config.baseDir(), 'platformdata/logs/tasks'),
// this pattern is for the cloudron logs API route to work
BACKUP_LOG_FILE: path.join(config.baseDir(), 'platformdata/logs/backup/app.log'),
UPDATER_LOG_FILE: path.join(config.baseDir(), 'platformdata/logs/updater/app.log')
};
+22 -26
View File
@@ -16,6 +16,7 @@ var addons = require('./addons.js'),
async = require('async'),
config = require('./config.js'),
debug = require('debug')('box:platform'),
execSync = require('child_process').execSync,
fs = require('fs'),
graphs = require('./graphs.js'),
infra = require('./infra_version.js'),
@@ -63,7 +64,8 @@ function start(callback) {
// mark app state before we start addons. this gives the db import logic a chance to mark an app as errored
startApps.bind(null, existingInfra),
graphs.startGraphite.bind(null, existingInfra),
addons.startServices.bind(null, existingInfra),
addons.startAddons.bind(null, existingInfra),
pruneInfraImages,
fs.writeFile.bind(fs, paths.INFRA_VERSION_FILE, JSON.stringify(infra, null, 4))
], function (error) {
if (error) return callback(error);
@@ -84,9 +86,7 @@ function onPlatformReady() {
debug('onPlatformReady: platform is ready');
exports._isReady = true;
taskmanager.resumeTasks();
applyPlatformConfig(NOOP_CALLBACK);
pruneInfraImages(NOOP_CALLBACK);
}
function applyPlatformConfig(callback) {
@@ -97,8 +97,8 @@ function applyPlatformConfig(callback) {
settings.getPlatformConfig(function (error, platformConfig) {
if (error) return retryCallback(error);
addons.updateServiceConfig(platformConfig, function (error) {
if (error) debug('Error updating services. Will rety in 5 minutes', platformConfig, error);
addons.updateAddonConfig(platformConfig, function (error) {
if (error) debug('Error updating addons. Will rety in 5 minutes', platformConfig, error);
retryCallback(error);
});
@@ -110,22 +110,21 @@ function pruneInfraImages(callback) {
debug('pruneInfraImages: checking existing images');
// cannot blindly remove all unused images since redis image may not be used
const images = infra.baseImages.concat(Object.keys(infra.images).map(function (addon) { return infra.images[addon]; }));
async.eachSeries(images, function (image, iteratorCallback) {
let output = safe.child_process.execSync(`docker images --digests ${image.repo} --format "{{.ID}} {{.Repository}}:{{.Tag}}@{{.Digest}}"`, { encoding: 'utf8' });
if (output === null) return iteratorCallback(safe.error);
let images = infra.baseImages.concat(Object.keys(infra.images).map(function (addon) { return infra.images[addon]; }));
for (let image of images) {
let output = execSync(`docker images --digests ${image.repo} --format "{{.ID}} {{.Repository}}:{{.Tag}}@{{.Digest}}"`, { encoding: 'utf8' });
let lines = output.trim().split('\n');
for (let line of lines) {
if (!line) continue;
let parts = line.split(' '); // [ ID, Repo:Tag@Digest ]
if (image.tag === parts[1]) continue; // keep
debug(`pruneInfraImages: removing unused image of ${image.repo}: ${line}`);
shell.exec('pruneInfraImages', `docker rmi ${parts[0]}`, iteratorCallback);
shell.execSync('pruneInfraImages', `docker rmi ${parts[0]}`);
}
}, callback);
}
callback();
}
function stopContainers(existingInfra, callback) {
@@ -133,10 +132,8 @@ function stopContainers(existingInfra, callback) {
if (existingInfra.version !== infra.version) {
// TODO: only nuke containers with isCloudronManaged=true
debug('stopping all containers for infra upgrade');
async.series([
shell.exec.bind(null, 'stopContainers', 'docker ps -qa | xargs --no-run-if-empty docker stop'),
shell.exec.bind(null, 'stopContainers', 'docker ps -qa | xargs --no-run-if-empty docker rm -f')
], callback);
shell.execSync('stopContainers', 'docker ps -qa | xargs --no-run-if-empty docker stop');
shell.execSync('stopContainers', 'docker ps -qa | xargs --no-run-if-empty docker rm -f');
} else {
assert(typeof infra.images, 'object');
var changedAddons = [ ];
@@ -147,11 +144,11 @@ function stopContainers(existingInfra, callback) {
debug('stopContainer: stopping addons for incremental infra update: %j', changedAddons);
let filterArg = changedAddons.map(function (c) { return `--filter 'name=${c}'`; }).join(' '); // name=c matches *c*. required for redis-{appid}
// ignore error if container not found (and fail later) so that this code works across restarts
async.series([
shell.exec.bind(null, 'stopContainers', `docker ps -qa ${filterArg} | xargs --no-run-if-empty docker stop || true`),
shell.exec.bind(null, 'stopContainers', `docker ps -qa ${filterArg} | xargs --no-run-if-empty docker rm -f || true`)
], callback);
shell.execSync('stopContainers', `docker ps -qa ${filterArg} | xargs --no-run-if-empty docker stop || true`);
shell.execSync('stopContainers', `docker ps -qa ${filterArg} | xargs --no-run-if-empty docker rm -f || true`);
}
callback();
}
function startApps(existingInfra, callback) {
@@ -168,13 +165,12 @@ function startApps(existingInfra, callback) {
}
}
function handleCertChanged(cn, callback) {
function handleCertChanged(cn) {
assert.strictEqual(typeof cn, 'string');
assert.strictEqual(typeof callback, 'function');
debug('handleCertChanged', cn);
if (cn === '*.' + config.adminDomain() || cn === config.adminFqdn()) return mail.startMail(callback);
callback();
if (cn === '*.' + config.adminDomain() || cn === config.adminFqdn()) {
mail.startMail(NOOP_CALLBACK);
}
}
+59
View File
@@ -0,0 +1,59 @@
'use strict';
exports = module.exports = {
set: set,
setDetail: setDetail,
clear: clear,
getAll: getAll,
UPDATE: 'update',
BACKUP: 'backup',
MIGRATE: 'migrate'
};
var assert = require('assert'),
debug = require('debug')('box:progress');
// if progress.update or progress.backup are object, they will contain 'percent' and 'message' properties
// otherwise no such operation is currently ongoing
var progress = {
update: null,
backup: null,
migrate: null
};
// We use -1 for percentage to indicate errors
function set(tag, percent, message) {
assert.strictEqual(typeof tag, 'string');
assert.strictEqual(typeof percent, 'number');
assert.strictEqual(typeof message, 'string');
progress[tag] = {
percent: percent,
message: message,
detail: ''
};
debug('%s: %s %s', tag, percent, message);
}
function setDetail(tag, detail) {
assert.strictEqual(typeof tag, 'string');
assert.strictEqual(typeof detail, 'string');
if (!progress[tag]) return debug('[%s] %s', tag, detail);
progress[tag].detail = detail;
}
function clear(tag) {
assert.strictEqual(typeof tag, 'string');
progress[tag] = null;
debug('clearing %s', tag);
}
function getAll() {
return progress;
}
+14 -28
View File
@@ -114,8 +114,6 @@ function isExpiringSync(certFilePath, hours) {
var result = safe.child_process.spawnSync('/usr/bin/openssl', [ 'x509', '-checkend', String(60 * 60 * hours), '-in', certFilePath ]);
if (!result) return 3; // some error
debug('isExpiringSync: %s %s %s', certFilePath, result.stdout.toString('utf8').trim(), result.status);
return result.status === 1; // 1 - expired 0 - not expired
@@ -132,7 +130,6 @@ function providerMatchesSync(domainObject, certFilePath, apiOptions) {
if (apiOptions.fallback) return certFilePath.includes('.host.cert');
const subjectAndIssuer = safe.child_process.execSync(`/usr/bin/openssl x509 -noout -subject -issuer -in "${certFilePath}"`, { encoding: 'utf8' });
if (!subjectAndIssuer) return false; // something bad happenned
const subject = subjectAndIssuer.match(/^subject=(.*)$/m)[1];
const domain = subject.substr(subject.indexOf('=') + 1).trim(); // subject can be /CN=, CN=, CN = and other forms
@@ -168,17 +165,13 @@ function validateCertificate(location, domainObject, certificate) {
const fqdn = domains.fqdn(location, domainObject);
var result = safe.child_process.execSync(`openssl x509 -noout -checkhost "${fqdn}"`, { encoding: 'utf8', input: cert });
if (result === null) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'Unable to get certificate subject:' + safe.error.message);
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 ${fqdn}`);
// 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 });
if (certModulus === null) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, `Unable to get cert modulus: ${safe.error.message}`);
var keyModulus = safe.child_process.execSync('openssl rsa -noout -modulus', { encoding: 'utf8', input: key });
if (keyModulus === null) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, `Unable to get key modulus: ${safe.error.message}`);
if (certModulus !== keyModulus) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'Key does not match the certificate.');
// check expiration
@@ -191,7 +184,7 @@ function validateCertificate(location, domainObject, certificate) {
function reload(callback) {
if (process.env.BOX_ENV === 'test') return callback();
shell.sudo('reload', [ RELOAD_NGINX_CMD ], {}, callback);
shell.sudo('reload', [ RELOAD_NGINX_CMD ], callback);
}
function generateFallbackCertificateSync(domainObject) {
@@ -242,14 +235,12 @@ function setFallbackCertificate(domain, fallback, callback) {
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));
}
platform.handleCertChanged('*.' + domain, function (error) {
platform.handleCertChanged('*.' + domain);
reload(function (error) {
if (error) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, error));
reload(function (error) {
if (error) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, error));
return callback(null);
});
return callback(null);
});
}
@@ -507,10 +498,9 @@ function unconfigureApp(app, callback) {
});
}
function renewCerts(options, auditSource, progressCallback, callback) {
function renewCerts(options, auditSource, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
apps.getAll(function (error, allApps) {
@@ -533,11 +523,7 @@ function renewCerts(options, auditSource, progressCallback, callback) {
if (options.domain) appDomains = appDomains.filter(function (appDomain) { return appDomain.domain === options.domain; });
let progress = 1;
async.eachSeries(appDomains, function (appDomain, iteratorCallback) {
progressCallback({ percent: progress, message: `Renewing certs of ${appDomain.fqdn}` });
progress += Math.round(100/appDomains.length);
ensureCertificate(appDomain.fqdn, appDomain.domain, auditSource, function (error, bundle) {
if (error) return iteratorCallback(error); // this can happen if cloudron is not setup yet
@@ -552,15 +538,17 @@ function renewCerts(options, auditSource, progressCallback, callback) {
if (appDomain.type === 'webadmin') configureFunc = writeAdminConfig.bind(null, bundle, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn());
else if (appDomain.type === 'main') configureFunc = writeAppConfig.bind(null, appDomain.app, bundle);
else if (appDomain.type === 'alternate') configureFunc = writeAppRedirectConfig.bind(null, appDomain.app, appDomain.fqdn, bundle);
else return iteratorCallback(new Error(`Unknown domain type for ${appDomain.fqdn}. This should never happen`));
else return callback(new Error(`Unknown domain type for ${appDomain.fqdn}. This should never happen`));
configureFunc(function (ignoredError) {
if (ignoredError) debug('renewAll: error reconfiguring app', ignoredError);
platform.handleCertChanged(appDomain.fqdn, iteratorCallback);
platform.handleCertChanged(appDomain.fqdn);
iteratorCallback(); // move to next domain
});
});
}, callback);
});
});
}
@@ -591,10 +579,8 @@ function configureDefaultServer(callback) {
debug('configureDefaultServer: create new cert');
var cn = 'cloudron-' + (new Date()).toISOString(); // randomize date a bit to keep firefox happy
if (!safe.child_process.execSync(`openssl req -x509 -newkey rsa:2048 -keyout ${keyFilePath} -out ${certFilePath} -days 3650 -subj /CN=${cn} -nodes`)) {
debug(`configureDefaultServer: could not generate certificate: ${safe.error.message}`);
return callback(safe.error);
}
var certCommand = util.format('openssl req -x509 -newkey rsa:2048 -keyout %s -out %s -days 3650 -subj /CN=%s -nodes', keyFilePath, certFilePath, cn);
safe.child_process.execSync(certCommand);
}
writeAdminConfig({ certFilePath, keyFilePath }, constants.NGINX_DEFAULT_CONFIG_FILE_NAME, '', function (error) {
-6
View File
@@ -134,7 +134,6 @@ function installApp(req, res, next) {
if ('sso' in data && typeof data.sso !== 'boolean') return next(new HttpError(400, 'sso must be a boolean'));
if ('enableBackup' in data && typeof data.enableBackup !== 'boolean') return next(new HttpError(400, 'enableBackup must be a boolean'));
if ('enableAutomaticUpdate' in data && typeof data.enableAutomaticUpdate !== 'boolean') return next(new HttpError(400, 'enableAutomaticUpdate must be a boolean'));
if (('debugMode' in data) && typeof data.debugMode !== 'object') return next(new HttpError(400, 'debugMode must be an object'));
@@ -175,10 +174,6 @@ function configureApp(req, res, next) {
if ('location' in data && typeof data.location !== 'string') return next(new HttpError(400, 'location must be string'));
if ('domain' in data && typeof data.domain !== 'string') return next(new HttpError(400, 'domain must be string'));
// domain, location must both be provided since they are unique together
if ('location' in data && !('domain' in data)) return next(new HttpError(400, 'domain must be provided'));
if (!('location' in data) && 'domain' in data) return next(new HttpError(400, 'location must be provided'));
if ('portBindings' in data && typeof data.portBindings !== 'object') return next(new HttpError(400, 'portBindings must be an object'));
if ('accessRestriction' in data && typeof data.accessRestriction !== 'object') return next(new HttpError(400, 'accessRestriction must be an object'));
@@ -192,7 +187,6 @@ function configureApp(req, res, next) {
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'));
if ('enableAutomaticUpdate' in data && typeof data.enableAutomaticUpdate !== 'boolean') return next(new HttpError(400, 'enableAutomaticUpdate must be a boolean'));
if (('debugMode' in data) && typeof data.debugMode !== 'object') return next(new HttpError(400, 'debugMode must be an object'));
+8 -6
View File
@@ -1,8 +1,8 @@
'use strict';
exports = module.exports = {
list: list,
startBackup: startBackup
get: get,
create: create
};
var backupdb = require('../backupdb.js'),
@@ -16,7 +16,7 @@ function auditSource(req) {
return { ip: ip, username: req.user ? req.user.username : null, userId: req.user ? req.user.id : null };
}
function list(req, res, next) {
function get(req, res, next) {
var page = typeof req.query.page !== 'undefined' ? parseInt(req.query.page) : 1;
if (!page || page < 0) return next(new HttpError(400, 'page query param has to be a postive number'));
@@ -31,11 +31,13 @@ function list(req, res, next) {
});
}
function startBackup(req, res, next) {
backups.startBackupTask(auditSource(req), function (error, taskId) {
function create(req, res, next) {
// note that cloudron.backup only waits for backup initiation and not for backup to complete
// backup progress can be checked up ny polling the progress api call
backups.backup(auditSource(req), function (error) {
if (error && error.reason === BackupsError.BAD_STATE) return next(new HttpError(409, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, { taskId }));
next(new HttpSuccess(202, {}));
});
}
+60
View File
@@ -0,0 +1,60 @@
'use strict';
exports = module.exports = {
getConfig: getConfig,
changePlan: changePlan
};
var caas = require('../caas.js'),
CaasError = require('../caas.js').CaasError,
config = require('../config.js'),
debug = require('debug')('box:routes/cloudron'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
_ = require('underscore');
function getConfig(req, res, next) {
if (config.provider() !== 'caas') return next(new HttpError(422, 'Cannot use this API with this provider'));
caas.getBoxAndUserDetails(function (error, result) {
if (error) return next(new HttpError(500, error));
// the result is { box: { region, size, plan }, user: { billing, currency } }
next(new HttpSuccess(200, {
region: result.box.region,
size: result.box.size,
billing: !!result.user.billing,
plan: result.box.plan,
currency: result.user.currency
}));
});
}
function changePlan(req, res, next) {
if (config.provider() !== 'caas') return next(new HttpError(422, 'Cannot use this API with this provider'));
if ('size' in req.body && typeof req.body.size !== 'string') return next(new HttpError(400, 'size must be string'));
if ('region' in req.body && typeof req.body.region !== 'string') return next(new HttpError(400, 'region must be string'));
if ('domain' in req.body) {
if (typeof req.body.domain !== 'string') return next(new HttpError(400, 'domain must be string'));
if (typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider must be string'));
}
if ('zoneName' in req.body && typeof req.body.zoneName !== 'string') return next(new HttpError(400, 'zoneName must be string'));
debug('Migration requested domain:%s size:%s region:%s', req.body.domain, req.body.size, req.body.region);
var options = _.pick(req.body, 'domain', 'size', 'region');
if (Object.keys(options).length === 0) return next(new HttpError(400, 'no migrate option provided'));
if (options.domain) options.domain = options.domain.toLowerCase();
caas.changePlan(req.body, function (error) { // pass req.body because 'domain' can have arbitrary options
if (error && error.reason === CaasError.BAD_STATE) return next(new HttpError(409, error.message));
if (error && error.reason === CaasError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, {}));
});
}
+11 -35
View File
@@ -2,7 +2,7 @@
exports = module.exports = {
reboot: reboot,
isRebootRequired: isRebootRequired,
getProgress: getProgress,
getConfig: getConfig,
getDisks: getDisks,
getUpdateInfo: getUpdateInfo,
@@ -12,8 +12,6 @@ exports = module.exports = {
getLogs: getLogs,
getLogStream: getLogStream,
getStatus: getStatus,
setDashboardDomain: setDashboardDomain,
renewCerts: renewCerts
};
var appstore = require('../appstore.js'),
@@ -24,6 +22,7 @@ var appstore = require('../appstore.js'),
CloudronError = cloudron.CloudronError,
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
progress = require('../progress.js'),
updater = require('../updater.js'),
updateChecker = require('../updatechecker.js'),
UpdaterError = require('../updater.js').UpdaterError,
@@ -34,19 +33,15 @@ function auditSource(req) {
return { ip: ip, username: req.user ? req.user.username : null, userId: req.user ? req.user.id : null };
}
function reboot(req, res, next) {
// Finish the request, to let the appstore know we triggered the reboot
next(new HttpSuccess(202, {}));
cloudron.reboot(function () {});
function getProgress(req, res, next) {
return next(new HttpSuccess(200, progress.getAll()));
}
function isRebootRequired(req, res, next) {
cloudron.isRebootRequired(function (error, result) {
if (error) return next(new HttpError(500, error));
function reboot(req, res, next) {
// Finish the request, to let the appstore know we triggered the restore it
next(new HttpSuccess(202, {}));
next(new HttpSuccess(200, { rebootRequired: result }));
});
cloudron.reboot(function () { });
}
function getConfig(req, res, next) {
@@ -66,12 +61,13 @@ function getDisks(req, res, next) {
function update(req, res, next) {
// this only initiates the update, progress can be checked via the progress route
updater.updateToLatest(auditSource(req), function (error, taskId) {
updater.updateToLatest(auditSource(req), function (error) {
if (error && error.reason === UpdaterError.ALREADY_UPTODATE) return next(new HttpError(422, error.message));
if (error && error.reason === UpdaterError.BAD_STATE) return next(new HttpError(409, error.message));
if (error && error.reason === UpdaterError.SELF_UPGRADE_NOT_SUPPORTED) return next(new HttpError(412, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, { taskId }));
next(new HttpSuccess(202, {}));
});
}
@@ -175,17 +171,6 @@ function getLogStream(req, res, next) {
});
}
function setDashboardDomain(req, res, next) {
if (!req.body.domain || typeof req.body.domain !== 'string') return next(new HttpError(400, 'domain must be a string'));
cloudron.setDashboardDomain(req.body.domain, function (error) {
if (error && error.reason === CloudronError.BAD_FIELD) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(204, {}));
});
}
function getStatus(req, res, next) {
cloudron.getStatus(function (error, status) {
if (error) return next(new HttpError(500, error));
@@ -193,12 +178,3 @@ function getStatus(req, res, next) {
next(new HttpSuccess(200, status));
});
}
function renewCerts(req, res, next) {
cloudron.renewCerts({ domain: req.body.domain || null }, auditSource(req), function (error, taskId) {
if (error && error.reason === CloudronError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, { taskId }));
});
}
+15
View File
@@ -7,6 +7,8 @@ exports = module.exports = {
update: update,
del: del,
renewCerts: renewCerts,
verifyDomainLock: verifyDomainLock
};
@@ -152,3 +154,16 @@ function del(req, res, next) {
next(new HttpSuccess(204));
});
}
function renewCerts(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
domains.renewCerts(req.params.domain, auditSource(req), function (error) {
if (error && error.reason === DomainsError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, {}));
});
next(new HttpSuccess(202, {}));
}
+1 -1
View File
@@ -7,7 +7,7 @@ exports = module.exports = {
var middleware = require('../middleware/index.js'),
url = require('url');
var graphiteProxy = middleware.proxy(url.parse('http://127.0.0.1:8417'));
var graphiteProxy = middleware.proxy(url.parse('http://127.0.0.1:8000'));
function getGraphs(req, res, next) {
var parsedUrl = url.parse(req.url, true /* parseQueryString */);
+3 -4
View File
@@ -4,6 +4,7 @@ exports = module.exports = {
accesscontrol: require('./accesscontrol.js'),
apps: require('./apps.js'),
backups: require('./backups.js'),
caas: require('./caas.js'),
clients: require('./clients.js'),
cloudron: require('./cloudron.js'),
developer: require('./developer.js'),
@@ -14,11 +15,9 @@ exports = module.exports = {
oauth2: require('./oauth2.js'),
mail: require('./mail.js'),
profile: require('./profile.js'),
provision: require('./provision.js'),
services: require('./services.js'),
settings: require('./settings.js'),
setup: require('./setup.js'),
sysadmin: require('./sysadmin.js'),
settings: require('./settings.js'),
ssh: require('./ssh.js'),
tasks: require('./tasks.js'),
users: require('./users.js')
};
+4 -4
View File
@@ -17,7 +17,7 @@ exports = module.exports = {
sendTestMail: sendTestMail,
listMailboxes: listMailboxes,
getMailboxes: getMailboxes,
getMailbox: getMailbox,
addMailbox: addMailbox,
updateMailbox: updateMailbox,
@@ -214,10 +214,10 @@ function sendTestMail(req, res, next) {
});
}
function listMailboxes(req, res, next) {
function getMailboxes(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
mail.listMailboxes(req.params.domain, function (error, result) {
mail.getMailboxes(req.params.domain, function (error, result) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
@@ -259,7 +259,7 @@ function updateMailbox(req, res, next) {
if (typeof req.body.userId !== 'string') return next(new HttpError(400, 'userId must be a string'));
mail.updateMailboxOwner(req.params.name, req.params.domain, req.body.userId, function (error) {
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));
-134
View File
@@ -1,134 +0,0 @@
'use strict';
exports = module.exports = {
getAll: getAll,
get: get,
configure: configure,
getLogs: getLogs,
getLogStream: getLogStream,
restart: restart
};
var addons = require('../addons.js'),
AddonsError = addons.AddonsError,
assert = require('assert'),
debug = require('debug')('box:routes/addons'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess;
function getAll(req, res, next) {
addons.getServices(function (error, result) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { services: result }));
});
}
function get(req, res, next) {
assert.strictEqual(typeof req.params.service, 'string');
addons.getService(req.params.service, function (error, result) {
if (error && error.reason === AddonsError.NOT_FOUND) return next(new HttpError(404, 'No such service'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { service: result }));
});
}
function configure(req, res, next) {
assert.strictEqual(typeof req.params.service, 'string');
if (typeof req.body.memory !== 'number') return next(new HttpError(400, 'memory must be a number'));
const data = {
memory: req.body.memory,
memorySwap: req.body.memory * 2
};
addons.configureService(req.params.service, data, function (error) {
if (error && error.reason === AddonsError.NOT_FOUND) return next(new HttpError(404, 'No such service'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, {}));
});
}
function getLogs(req, res, next) {
assert.strictEqual(typeof req.params.service, 'string');
var lines = req.query.lines ? parseInt(req.query.lines, 10) : 100;
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a number'));
debug(`Getting logs of service ${req.params.service}`);
var options = {
lines: lines,
follow: false,
format: req.query.format
};
addons.getServiceLogs(req.params.service, options, function (error, logStream) {
if (error && error.reason === AddonsError.NOT_FOUND) return next(new HttpError(404, 'No such service'));
if (error) return next(new HttpError(500, error));
res.writeHead(200, {
'Content-Type': 'application/x-logs',
'Content-Disposition': 'attachment; filename="log.txt"',
'Cache-Control': 'no-cache',
'X-Accel-Buffering': 'no' // disable nginx buffering
});
logStream.pipe(res);
});
}
// this route is for streaming logs
function getLogStream(req, res, next) {
assert.strictEqual(typeof req.params.service, 'string');
debug(`Getting logstream of service ${req.params.service}`);
var lines = req.query.lines ? parseInt(req.query.lines, 10) : -10; // we ignore last-event-id
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a valid number'));
function sse(id, data) { return 'id: ' + id + '\ndata: ' + data + '\n\n'; }
if (req.headers.accept !== 'text/event-stream') return next(new HttpError(400, 'This API call requires EventStream'));
var options = {
lines: lines,
follow: true
};
addons.getServiceLogs(req.params.service, options, function (error, logStream) {
if (error && error.reason === AddonsError.NOT_FOUND) return next(new HttpError(404, 'No such service'));
if (error) return next(new HttpError(500, error));
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'X-Accel-Buffering': 'no', // disable nginx buffering
'Access-Control-Allow-Origin': '*'
});
res.write('retry: 3000\n');
res.on('close', logStream.close);
logStream.on('data', function (data) {
var obj = JSON.parse(data);
res.write(sse(obj.monotonicTimestamp, JSON.stringify(obj))); // send timestamp as id
});
logStream.on('end', res.end.bind(res));
logStream.on('error', res.end.bind(res, null));
});
}
function restart(req, res, next) {
assert.strictEqual(typeof req.params.service, 'string');
debug(`Restarting service ${req.params.service}`);
addons.restartService(req.params.service, function (error) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, {}));
});
}
+16 -16
View File
@@ -3,7 +3,7 @@
exports = module.exports = {
providerTokenAuth: providerTokenAuth,
setupTokenAuth: setupTokenAuth,
setup: setup,
provision: provision,
activate: activate,
restore: restore
};
@@ -15,8 +15,8 @@ var assert = require('assert'),
debug = require('debug')('box:routes/setup'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
provision = require('../provision.js'),
ProvisionError = require('../provision.js').ProvisionError,
setup = require('../setup.js'),
SetupError = require('../setup.js').SetupError,
superagent = require('superagent');
function auditSource(req) {
@@ -61,7 +61,7 @@ function setupTokenAuth(req, res, next) {
});
}
function setup(req, res, next) {
function provision(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (!req.body.dnsConfig || typeof req.body.dnsConfig !== 'object') return next(new HttpError(400, 'dnsConfig is required'));
@@ -83,10 +83,10 @@ function setup(req, res, next) {
// it can take sometime to setup DNS, register cloudron
req.clearTimeout();
provision.setup(dnsConfig, req.body.autoconf || {}, auditSource(req), function (error) {
if (error && error.reason === ProvisionError.ALREADY_SETUP) return next(new HttpError(409, error.message));
if (error && error.reason === ProvisionError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === ProvisionError.BAD_STATE) return next(new HttpError(409, error.message));
setup.provision(dnsConfig, req.body.autoconf || {}, auditSource(req), function (error) {
if (error && error.reason === SetupError.ALREADY_SETUP) return next(new HttpError(409, error.message));
if (error && error.reason === SetupError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === SetupError.BAD_STATE) return next(new HttpError(409, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200));
@@ -109,9 +109,9 @@ function activate(req, res, next) {
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
debug('activate: username:%s ip:%s', username, ip);
provision.activate(username, password, email, displayName, ip, auditSource(req), function (error, info) {
if (error && error.reason === ProvisionError.ALREADY_PROVISIONED) return next(new HttpError(409, 'Already setup'));
if (error && error.reason === ProvisionError.BAD_FIELD) return next(new HttpError(400, error.message));
setup.activate(username, password, email, displayName, ip, auditSource(req), function (error, info) {
if (error && error.reason === SetupError.ALREADY_PROVISIONED) return next(new HttpError(409, 'Already setup'));
if (error && error.reason === SetupError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
// only in caas case do we have to notify the api server about activation
@@ -146,11 +146,11 @@ function restore(req, res, next) {
// TODO: validate subfields of these objects
if (req.body.autoconf && typeof req.body.autoconf !== 'object') return next(new HttpError(400, 'autoconf must be an object'));
provision.restore(backupConfig, req.body.backupId, req.body.version, req.body.autoconf || {}, auditSource(req), function (error) {
if (error && error.reason === ProvisionError.ALREADY_SETUP) return next(new HttpError(409, error.message));
if (error && error.reason === ProvisionError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === ProvisionError.BAD_STATE) return next(new HttpError(409, error.message));
if (error && error.reason === ProvisionError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
setup.restore(backupConfig, req.body.backupId, req.body.version, req.body.autoconf || {}, auditSource(req), function (error) {
if (error && error.reason === SetupError.ALREADY_SETUP) return next(new HttpError(409, error.message));
if (error && error.reason === SetupError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === SetupError.BAD_STATE) return next(new HttpError(409, error.message));
if (error && error.reason === SetupError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200));
+5 -4
View File
@@ -26,11 +26,11 @@ function backup(req, res, next) {
// note that cloudron.backup only waits for backup initiation and not for backup to complete
// backup progress can be checked up ny polling the progress api call
var auditSource = { userId: null, username: 'sysadmin' };
backups.startBackupTask(auditSource, function (error, taskId) {
backups.backup(auditSource, function (error) {
if (error && error.reason === BackupsError.BAD_STATE) return next(new HttpError(409, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, { taskId }));
next(new HttpSuccess(202, {}));
});
}
@@ -39,12 +39,13 @@ function update(req, res, next) {
// this only initiates the update, progress can be checked via the progress route
var auditSource = { userId: null, username: 'sysadmin' };
updater.updateToLatest(auditSource, function (error, taskId) {
updater.updateToLatest(auditSource, function (error) {
if (error && error.reason === UpdaterError.ALREADY_UPTODATE) return next(new HttpError(422, error.message));
if (error && error.reason === UpdaterError.BAD_STATE) return next(new HttpError(409, error.message));
if (error && error.reason === UpdaterError.SELF_UPGRADE_NOT_SUPPORTED) return next(new HttpError(412, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, { taskId }));
next(new HttpSuccess(202, {}));
});
}
-121
View File
@@ -1,121 +0,0 @@
'use strict';
exports = module.exports = {
get: get,
stopTask: stopTask,
list: list,
getLogs: getLogs,
getLogStream: getLogStream
};
let assert = require('assert'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
TaskError = require('../tasks.js').TaskError,
tasks = require('../tasks.js');
function stopTask(req, res, next) {
assert.strictEqual(typeof req.params.taskId, 'string');
tasks.stopTask(req.params.taskId, function (error) {
if (error && error.reason === TaskError.NOT_FOUND) return next(new HttpError(404, 'No such task'));
if (error && error.reason === TaskError.BAD_STATE) return next(new HttpError(409, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(204, {}));
});
}
function get(req, res, next) {
assert.strictEqual(typeof req.params.taskId, 'string');
tasks.get(req.params.taskId, function (error, task) {
if (error && error.reason === TaskError.NOT_FOUND) return next(new HttpError(404, 'No such task'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, tasks.removePrivateFields(task)));
});
}
function list(req, res, next) {
var page = typeof req.query.page !== 'undefined' ? parseInt(req.query.page) : 1;
if (!page || page < 0) return next(new HttpError(400, 'page query param has to be a postive number'));
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.type && typeof req.query.type !== 'string') return next(new HttpError(400, 'type must be a string'));
tasks.listByTypePaged(req.query.type || null, page, perPage, function (error, result) {
if (error) return next(new HttpError(500, error));
result = result.map(tasks.removePrivateFields);
next(new HttpSuccess(200, { tasks: result }));
});
}
function getLogs(req, res, next) {
assert.strictEqual(typeof req.params.taskId, 'string');
var lines = req.query.lines ? parseInt(req.query.lines, 10) : 100;
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a number'));
var options = {
lines: lines,
follow: false,
format: req.query.format
};
tasks.getLogs(req.params.taskId, options, function (error, logStream) {
if (error && error.reason === TaskError.NOT_FOUND) return next(new HttpError(404, 'No such task'));
if (error) return next(new HttpError(500, error));
res.writeHead(200, {
'Content-Type': 'application/x-logs',
'Content-Disposition': 'attachment; filename="log.txt"',
'Cache-Control': 'no-cache',
'X-Accel-Buffering': 'no' // disable nginx buffering
});
logStream.pipe(res);
});
}
// this route is for streaming logs
function getLogStream(req, res, next) {
assert.strictEqual(typeof req.params.taskId, 'string');
var lines = req.query.lines ? parseInt(req.query.lines, 10) : -10; // we ignore last-event-id
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a valid number'));
function sse(id, data) { return 'id: ' + id + '\ndata: ' + data + '\n\n'; }
if (req.headers.accept !== 'text/event-stream') return next(new HttpError(400, 'This API call requires EventStream'));
var options = {
lines: lines,
follow: true
};
tasks.getLogs(req.params.taskId, options, function (error, logStream) {
if (error && error.reason === TaskError.NOT_FOUND) return next(new HttpError(404, 'No such task'));
if (error) return next(new HttpError(500, error));
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'X-Accel-Buffering': 'no', // disable nginx buffering
'Access-Control-Allow-Origin': '*'
});
res.write('retry: 3000\n');
res.on('close', logStream.close);
logStream.on('data', function (data) {
var obj = JSON.parse(data);
res.write(sse(obj.monotonicTimestamp, JSON.stringify(obj))); // send timestamp as id
});
logStream.on('end', res.end.bind(res));
logStream.on('error', res.end.bind(res, null));
});
}
+9
View File
@@ -264,6 +264,8 @@ function stopBox(done) {
}
describe('App API', function () {
this.timeout(100000);
before(startBox);
after(stopBox);
@@ -616,7 +618,11 @@ describe('App API', function () {
});
describe('App installation', function () {
this.timeout(100000);
var apiHockInstance = hock.createHock({ throwOnUnmatched: false });
var apiHockServer;
var validCert1, validKey1;
before(function (done) {
@@ -814,6 +820,7 @@ describe('App installation', function () {
});
it('installation - app can check addons', function (done) {
this.timeout(120000);
console.log('This test can take a while as it waits for scheduler addon to tick 3');
checkAddons(appEntry, done);
});
@@ -942,6 +949,7 @@ describe('App installation', function () {
});
it('installation - app can check addons', function (done) {
this.timeout(120000);
console.log('This test can take a while as it waits for scheduler addon to tick 2');
checkAddons(appEntry, done);
});
@@ -1068,6 +1076,7 @@ describe('App installation', function () {
});
it('installation - app can check addons', function (done) {
this.timeout(120000);
console.log('This test can take a while as it waits for scheduler addon to tick 4');
checkAddons(appEntry, done);
});
+187
View File
@@ -145,6 +145,29 @@ describe('Caas', function () {
});
});
describe('get config', function () {
before(setup);
after(cleanup);
it('succeeds (admin)', function (done) {
var scope = nock(config.apiServerOrigin())
.get('/api/v1/boxes/BOX_ID?token=ACCESS_TOKEN2')
.reply(200, { box: { region: 'sfo', size: '1gb' }, user: { }});
superagent.get(SERVER_URL + '/api/v1/caas/config')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.size).to.eql('1gb');
expect(result.body.region).to.eql('sfo');
expect(scope.isDone()).to.be.ok();
done();
});
});
});
describe('Backups API', function () {
var scope1 = nock(config.apiServerOrigin()).post('/api/v1/boxes/BOX_ID/awscredentials?token=BACKUP_TOKEN')
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey' } }, { 'Content-Type': 'application/json' });
@@ -168,5 +191,169 @@ describe('Caas', function () {
});
});
});
xdescribe('migrate', function () {
before(function (done) {
async.series([
setup,
function (callback) {
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/BOX_ID/setup/verify?setupToken=somesetuptoken').reply(200, {});
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/BOX_ID/setup/done?setupToken=somesetuptoken').reply(201, {});
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
// stash token for further use
token = result.body.token;
callback();
});
}
], done);
});
after(function (done) {
locker.unlock(locker._operation); // migrate never unlocks
cleanup(done);
});
it('fails without token', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 'small', region: 'sfo'})
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails without password', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 'small', region: 'sfo'})
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('succeeds without size', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ region: 'sfo', password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(202);
done();
});
});
it('fails with wrong size type', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 4, region: 'sfo', password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('succeeds without region', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 'small', password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(202);
done();
});
});
it('fails with wrong region type', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 'small', region: 4, password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails when in wrong state', function (done) {
var scope2 = nock(config.apiServerOrigin())
.post('/api/v1/boxes/BOX_ID/awscredentials?token=BACKUP_TOKEN')
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } });
var scope3 = nock(config.apiServerOrigin())
.post('/api/v1/boxes/BOX_ID/backupDone?token=APPSTORE_TOKEN', function (body) {
return body.boxVersion && body.restoreKey && !body.appId && !body.appVersion && body.appBackupIds.length === 0;
})
.reply(200, { id: 'someid' });
var scope1 = nock(config.apiServerOrigin())
.post('/api/v1/boxes/BOX_ID/migrate?token=APPSTORE_TOKEN', function (body) {
return body.size && body.region && body.restoreKey;
}).reply(409, {});
injectShellMock();
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 'small', region: 'sfo', password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(202);
function checkAppstoreServerCalled() {
if (scope1.isDone() && scope2.isDone() && scope3.isDone()) {
restoreShellMock();
return done();
}
setTimeout(checkAppstoreServerCalled, 100);
}
checkAppstoreServerCalled();
});
});
it('succeeds', function (done) {
var scope1 = nock(config.apiServerOrigin()).post('/api/v1/boxes/BOX_ID/migrate?token=APPSTORE_TOKEN', function (body) {
return body.size && body.region && body.restoreKey;
}).reply(202, {});
var scope2 = nock(config.apiServerOrigin())
.post('/api/v1/boxes/BOX_ID/backupDone?token=APPSTORE_TOKEN', function (body) {
return body.boxVersion && body.restoreKey && !body.appId && !body.appVersion && body.appBackupIds.length === 0;
})
.reply(200, { id: 'someid' });
var scope3 = nock(config.apiServerOrigin())
.post('/api/v1/boxes/BOX_ID/awscredentials?token=BACKUP_TOKEN')
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } });
injectShellMock();
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 'small', region: 'sfo', password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(202);
function checkAppstoreServerCalled() {
if (scope1.isDone() && scope2.isDone() && scope3.isDone()) {
restoreShellMock();
return done();
}
setTimeout(checkAppstoreServerCalled, 100);
}
checkAppstoreServerCalled();
});
});
});
});
+2
View File
@@ -483,6 +483,8 @@ describe('Clients', function () {
});
describe('delete tokens by client', function () {
this.timeout(5000);
before(setup2);
after(cleanup);
+1
View File
@@ -191,6 +191,7 @@ describe('Cloudron', function () {
expect(result.body.apiServerOrigin).to.eql('http://localhost:6060');
expect(result.body.webServerOrigin).to.eql(null);
expect(result.body.adminFqdn).to.eql(config.adminFqdn());
expect(result.body.progress).to.be.an('object');
expect(result.body.version).to.eql(config.version());
expect(result.body.memory).to.eql(os.totalmem());
expect(result.body.cloudronName).to.be.a('string');
+2
View File
@@ -37,6 +37,8 @@ function cleanup(done) {
}
describe('Developer API', function () {
this.timeout(20000);
describe('login', function () {
before(function (done) {
async.series([
+2
View File
@@ -43,6 +43,8 @@ var DOMAIN_1 = {
};
describe('Domains API', function () {
this.timeout(10000);
before(function (done) {
config._reset();
config.setFqdn(DOMAIN);
+2
View File
@@ -78,6 +78,8 @@ function cleanup(done) {
}
describe('Eventlog API', function () {
this.timeout(10000);
before(setup);
after(cleanup);
+10
View File
@@ -105,6 +105,8 @@ function cleanup(done) {
}
describe('Mail API', function () {
this.timeout(10000);
before(setup);
after(cleanup);
@@ -229,6 +231,8 @@ describe('Mail API', function () {
var dnsAnswerQueue = [];
var dkimDomain, spfDomain, mxDomain, dmarcDomain;
this.timeout(10000);
before(function (done) {
var dns = require('../../native-dns.js');
@@ -761,6 +765,7 @@ describe('Mail API', function () {
expect(res.body.mailbox).to.be.an('object');
expect(res.body.mailbox.name).to.equal(MAILBOX_NAME);
expect(res.body.mailbox.ownerId).to.equal(userId);
expect(res.body.mailbox.ownerType).to.equal('user');
expect(res.body.mailbox.aliasTarget).to.equal(null);
expect(res.body.mailbox.domain).to.equal(DOMAIN_0.domain);
done();
@@ -776,6 +781,7 @@ describe('Mail API', function () {
expect(res.body.mailboxes[0]).to.be.an('object');
expect(res.body.mailboxes[0].name).to.equal(MAILBOX_NAME);
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);
expect(res.body.mailboxes[0].domain).to.equal(DOMAIN_0.domain);
done();
@@ -908,10 +914,12 @@ describe('Mail API', function () {
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();
@@ -1023,6 +1031,7 @@ describe('Mail API', function () {
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.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' ]);
@@ -1039,6 +1048,7 @@ describe('Mail API', function () {
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].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' ]);
+2
View File
@@ -24,6 +24,8 @@ const EMAIL_0_NEW_FALLBACK = 'stupIDfallback@me.com';
const DISPLAY_NAME_0_NEW = 'New Name';
describe('Profile API', function () {
this.timeout(5000);
var user_0 = null;
var token_0;
+2 -13
View File
@@ -137,12 +137,12 @@ describe('REST API', function () {
});
});
it('dns setup twice succeeds', function (done) {
it('dns setup twice fails', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/setup')
.send({ dnsConfig: { provider: 'noop', domain: DOMAIN, DOMAIN, config: {} } })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(200);
expect(result.statusCode).to.eql(409);
done();
});
@@ -247,17 +247,6 @@ describe('REST API', function () {
});
});
it('dns setup after activation fails', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/setup')
.send({ dnsConfig: { provider: 'noop', domain: DOMAIN, DOMAIN, config: {} } })
.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 })
+2
View File
@@ -61,6 +61,8 @@ function cleanup(done) {
}
describe('SSH API', function () {
this.timeout(10000);
before(setup);
after(cleanup);
+13 -11
View File
@@ -65,27 +65,29 @@ function cleanup(done) {
}
describe('Internal API', function () {
this.timeout(5000);
before(setup);
after(cleanup);
describe('backup', function () {
it('succeeds', function (done) {
superagent.post(config.sysadminOrigin() + '/api/v1/backup')
.end(function (error, result) {
expect(result.statusCode).to.equal(202);
.end(function (error, result) {
expect(result.statusCode).to.equal(202);
function checkBackupStartEvent() {
eventlog.getAllPaged([ eventlog.ACTION_BACKUP_START ], '', 1, 100, function (error, result) {
expect(error).to.equal(null);
function checkBackupStartEvent() {
eventlog.getAllPaged([ eventlog.ACTION_BACKUP_START ], '', 1, 100, function (error, result) {
expect(error).to.equal(null);
if (result.length === 0) return setTimeout(checkBackupStartEvent, 1000);
if (result.length === 0) return setTimeout(checkBackupStartEvent, 1000);
done();
});
}
done();
});
}
checkBackupStartEvent();
});
checkBackupStartEvent();
});
});
});
});
-138
View File
@@ -1,138 +0,0 @@
'use strict';
/* global it:false */
/* global describe:false */
/* global before:false */
/* global after:false */
var async = require('async'),
config = require('../../config.js'),
database = require('../../database.js'),
expect = require('expect.js'),
server = require('../../server.js'),
superagent = require('superagent'),
tasks = require('../../tasks.js');
var SERVER_URL = 'http://localhost:' + config.get('port');
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var token = null;
function setup(done) {
config._reset();
config.setFqdn('example-tasks-test.com');
config.setAdminFqdn('my.example-tasks-test.com');
async.series([
server.start.bind(null),
database._clear.bind(null),
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) {
database._clear(function (error) {
expect(!error).to.be.ok();
server.stop(done);
});
}
describe('Tasks API', function () {
before(setup);
after(cleanup);
it('can get task', function (done) {
let taskId = null;
let task = tasks.startTask(tasks._TASK_IDENTITY, [ 'ping' ]);
task.on('error', done);
task.on('start', (tid) => { taskId = tid; });
task.on('finish', function () {
superagent.get(SERVER_URL + '/api/v1/tasks/' + taskId)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.percent).to.be(100);
expect(res.body.args).to.be(undefined);
expect(res.body.active).to.be(false); // finished
expect(res.body.result).to.be('ping');
expect(res.body.errorMessage).to.be(null);
done();
});
});
});
it('can get logs', function (done) {
let taskId = null;
let task = tasks.startTask(tasks._TASK_CRASH, [ 'ping' ]);
task.on('error', done);
task.on('start', (tid) => { taskId = tid; });
task.on('finish', function () {
superagent.get(SERVER_URL + '/api/v1/tasks/' + taskId + '/logs')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
done();
});
});
});
it('cannot stop inactive task', function (done) {
let taskId = null;
let task = tasks.startTask(tasks._TASK_IDENTITY, [ 'ping' ]);
task.on('error', done);
task.on('start', (tid) => { taskId = tid; });
task.on('finish', function () {
superagent.post(SERVER_URL + '/api/v1/tasks/' + taskId + '/stop')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(409);
done();
});
});
});
it('can stop task', function (done) {
let taskId = null;
let task = tasks.startTask(tasks._TASK_SLEEP, [ 10000 ]);
task.on('error', done);
task.on('start', (tid) => {
taskId = tid;
superagent.post(SERVER_URL + '/api/v1/tasks/' + taskId + '/stop')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
});
});
task.on('finish', () => {
superagent.get(SERVER_URL + '/api/v1/tasks/' + taskId)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.percent).to.be(100);
expect(res.body.active).to.be(false); // finished
expect(res.body.result).to.be(null);
expect(res.body.errorMessage).to.contain('signal SIGTERM');
done();
});
});
});
});
+2
View File
@@ -82,6 +82,8 @@ function checkMails(number, done) {
}
describe('Users API', function () {
this.timeout(5000);
var user_0, user_1, user_2, user_4;
var token = null, userToken = null;
var token_1 = tokendb.generateToken();
-18
View File
@@ -1,18 +0,0 @@
#!/bin/bash
set -eu -o pipefail
if [[ ${EUID} -ne 0 ]]; then
echo "This script should be run as root." > /dev/stderr
exit 1
fi
if [[ $# == 1 && "$1" == "--check" ]]; then
echo "OK"
exit 0
fi
if [[ "${BOX_ENV}" == "cloudron" ]]; then
systemctl restart docker
fi
-19
View File
@@ -1,19 +0,0 @@
#!/bin/bash
set -eu -o pipefail
if [[ ${EUID} -ne 0 ]]; then
echo "This script should be run as root." > /dev/stderr
exit 1
fi
if [[ $# == 1 && "$1" == "--check" ]]; then
echo "OK"
exit 0
fi
if [[ "${BOX_ENV}" == "cloudron" ]]; then
unbound-anchor -a /var/lib/unbound/root.key
systemctl restart unbound
fi
+7 -18
View File
@@ -30,32 +30,21 @@ if systemctl reset-failed "${UPDATER_SERVICE}"; then
echo "=> service has failed earlier"
fi
# StandardError will follow StandardOutput in default inherit mode. https://www.freedesktop.org/software/systemd/man/systemd.exec.html
echo "=> Run installer.sh as ${UPDATER_SERVICE}."
if [[ "$(systemd --version | head -n1)" != "systemd 22"* ]]; then
readonly DATETIME=`date '+%Y-%m-%d_%H-%M-%S'`
readonly LOG_FILE="/home/yellowtent/platformdata/logs/updater/cloudron-updater-${DATETIME}.log"
update_service_options="-p StandardOutput=file:${LOG_FILE}"
echo "=> starting service (ubuntu 18.04) ${UPDATER_SERVICE}. see logs at ${LOG_FILE}"
else
update_service_options=""
echo "=> starting service (ubuntu 16.04) ${UPDATER_SERVICE}. see logs using journalctl -u ${UPDATER_SERVICE}"
fi
if ! systemd-run --unit "${UPDATER_SERVICE}" $update_service_options ${installer_path}; then
echo "Failed to install cloudron. See log for details"
echo "=> Run installer.sh as cloudron-updater.service"
if ! systemd-run --unit "${UPDATER_SERVICE}" ${installer_path}; then
echo "Failed to install cloudron. See ${LOG_FILE} for details"
exit 1
fi
echo "=> service ${UPDATER_SERVICE} started."
echo "=> See logs with journalctl -u ${UPDATER_SERVICE} -f"
while true; do
if systemctl is-failed "${UPDATER_SERVICE}" >/dev/null 2>&1; then
if systemctl is-failed "${UPDATER_SERVICE}"; then
echo "=> ${UPDATER_SERVICE} has failed"
exit 1
fi
echo "${UPDATER_SERVICE} is still active. will check in 5 seconds"
sleep 5
# this loop will stop once the update process stopped the box unit and thus terminating this child process
done
+13 -25
View File
@@ -108,11 +108,12 @@ function initializeExpressSync() {
var csrf = routes.oauth2.csrf();
// public routes
router.post('/api/v1/cloudron/setup', routes.provision.providerTokenAuth, routes.provision.setup); // only available until no-domain
router.post('/api/v1/cloudron/restore', routes.provision.restore); // only available until activated
router.post('/api/v1/cloudron/activate', routes.provision.setupTokenAuth, routes.provision.activate);
router.post('/api/v1/cloudron/setup', routes.setup.providerTokenAuth, routes.setup.provision); // only available until no-domain
router.post('/api/v1/cloudron/restore', routes.setup.restore); // only available until activated
router.post('/api/v1/cloudron/activate', routes.setup.setupTokenAuth, routes.setup.activate);
router.get ('/api/v1/cloudron/status', routes.cloudron.getStatus);
router.get ('/api/v1/cloudron/progress', routes.cloudron.getProgress);
router.get ('/api/v1/cloudron/avatar', routes.settings.getCloudronAvatar); // this is a public alias for /api/v1/settings/cloudron_avatar
// developer routes
@@ -121,10 +122,7 @@ function initializeExpressSync() {
// cloudron routes
router.get ('/api/v1/cloudron/update', cloudronScope, routes.cloudron.getUpdateInfo);
router.post('/api/v1/cloudron/update', cloudronScope, routes.cloudron.update);
router.post('/api/v1/cloudron/set_dashboard_domain', cloudronScope, routes.cloudron.setDashboardDomain);
router.post('/api/v1/cloudron/renew_certs', cloudronScope, routes.cloudron.renewCerts);
router.post('/api/v1/cloudron/check_for_updates', cloudronScope, routes.cloudron.checkForUpdates);
router.get ('/api/v1/cloudron/reboot', cloudronScope, routes.cloudron.isRebootRequired);
router.post('/api/v1/cloudron/reboot', cloudronScope, routes.cloudron.reboot);
router.get ('/api/v1/cloudron/graphs', cloudronScope, routes.graphs.getGraphs);
router.get ('/api/v1/cloudron/disks', cloudronScope, routes.cloudron.getDisks);
@@ -136,17 +134,6 @@ function initializeExpressSync() {
router.del ('/api/v1/cloudron/ssh/authorized_keys/:identifier', cloudronScope, isUnmanaged, routes.ssh.delAuthorizedKey);
router.get ('/api/v1/cloudron/eventlog', cloudronScope, routes.eventlog.get);
// tasks
router.get ('/api/v1/tasks', settingsScope, routes.tasks.list);
router.get ('/api/v1/tasks/:taskId', settingsScope, routes.tasks.get);
router.get ('/api/v1/tasks/:taskId/logs', cloudronScope, routes.tasks.getLogs);
router.get ('/api/v1/tasks/:taskId/logstream', cloudronScope, routes.tasks.getLogStream);
router.post('/api/v1/tasks/:taskId/stop', settingsScope, routes.tasks.stopTask);
// backups
router.get ('/api/v1/backups', settingsScope, routes.backups.list);
router.post('/api/v1/backups', settingsScope, routes.backups.startBackup);
// config route (for dashboard)
router.get ('/api/v1/config', profileScope, routes.cloudron.getConfig);
@@ -265,7 +252,7 @@ function initializeExpressSync() {
router.post('/api/v1/mail/:domain/enable', mailScope, routes.mail.setMailEnabled);
router.post('/api/v1/mail/:domain/dns', mailScope, routes.mail.setDnsRecords);
router.post('/api/v1/mail/:domain/send_test_mail', mailScope, routes.mail.sendTestMail);
router.get ('/api/v1/mail/:domain/mailboxes', mailScope, routes.mail.listMailboxes);
router.get ('/api/v1/mail/:domain/mailboxes', mailScope, routes.mail.getMailboxes);
router.get ('/api/v1/mail/:domain/mailboxes/:name', mailScope, routes.mail.getMailbox);
router.post('/api/v1/mail/:domain/mailboxes', mailScope, routes.mail.addMailbox);
router.post('/api/v1/mail/:domain/mailboxes/:name', mailScope, routes.mail.updateMailbox);
@@ -282,20 +269,21 @@ function initializeExpressSync() {
// feedback
router.post('/api/v1/feedback', cloudronScope, isUnmanaged, routes.cloudron.feedback);
// backup routes
router.get ('/api/v1/backups', settingsScope, routes.backups.get);
router.post('/api/v1/backups', settingsScope, routes.backups.create);
// domain routes
router.post('/api/v1/domains', domainsManageScope, routes.domains.add);
router.get ('/api/v1/domains', domainsReadScope, routes.domains.getAll);
router.get ('/api/v1/domains/:domain', domainsManageScope, verifyDomainLock, routes.domains.get); // this is manage scope because it returns non-restricted fields
router.put ('/api/v1/domains/:domain', domainsManageScope, verifyDomainLock, routes.domains.update);
router.post ('/api/v1/domains/:domain/renew_certs', domainsManageScope, verifyDomainLock, routes.domains.renewCerts);
router.del ('/api/v1/domains/:domain', domainsManageScope, verifyDomainLock, routes.users.verifyPassword, routes.domains.del);
// addon routes
router.get ('/api/v1/services', cloudronScope, routes.services.getAll);
router.get ('/api/v1/services/:service', cloudronScope, routes.services.get);
router.post('/api/v1/services/:service', cloudronScope, routes.services.configure);
router.get ('/api/v1/services/:service/logs', cloudronScope, routes.services.getLogs);
router.get ('/api/v1/services/:service/logstream', cloudronScope, routes.services.getLogStream);
router.post('/api/v1/services/:service/restart', cloudronScope, routes.services.restart);
// caas routes
router.get('/api/v1/caas/config', cloudronScope, routes.caas.getConfig);
router.post('/api/v1/caas/change_plan', cloudronScope, routes.users.verifyPassword, routes.caas.changePlan);
// disable server socket "idle" timeout. we use the timeout middleware to handle timeouts on a route level
// we rely on nginx for timeouts on the TCP level (see client_header_timeout)
+1 -2
View File
@@ -86,7 +86,6 @@ var gDefaults = (function () {
provider: 'filesystem',
key: '',
backupFolder: '/var/backups',
format: 'tgz',
retentionSecs: 2 * 24 * 60 * 60, // 2 days
intervalSecs: 24 * 60 * 60 // ~1 day
};
@@ -394,7 +393,7 @@ function setPlatformConfig(platformConfig, callback) {
settingsdb.set(exports.PLATFORM_CONFIG_KEY, JSON.stringify(platformConfig), function (error) {
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
addons.updateServiceConfig(platformConfig, callback);
addons.updateAddonConfig(platformConfig, callback);
});
}
+64 -72
View File
@@ -1,11 +1,11 @@
'use strict';
exports = module.exports = {
setup: setup,
provision: provision,
restore: restore,
activate: activate,
ProvisionError: ProvisionError
SetupError: SetupError
};
var assert = require('assert'),
@@ -16,7 +16,7 @@ var assert = require('assert'),
constants = require('./constants.js'),
clients = require('./clients.js'),
cloudron = require('./cloudron.js'),
debug = require('debug')('box:provision'),
debug = require('debug')('box:setup'),
domains = require('./domains.js'),
DomainsError = domains.DomainsError,
eventlog = require('./eventlog.js'),
@@ -37,7 +37,7 @@ var RESTART_CMD = path.join(__dirname, 'scripts/restart.sh');
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
function ProvisionError(reason, errorOrMessage) {
function SetupError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
@@ -55,13 +55,13 @@ function ProvisionError(reason, errorOrMessage) {
this.nestedError = errorOrMessage;
}
}
util.inherits(ProvisionError, Error);
ProvisionError.BAD_FIELD = 'Field error';
ProvisionError.BAD_STATE = 'Bad State';
ProvisionError.ALREADY_SETUP = 'Already Setup';
ProvisionError.INTERNAL_ERROR = 'Internal Error';
ProvisionError.EXTERNAL_ERROR = 'External Error';
ProvisionError.ALREADY_PROVISIONED = 'Already Provisioned';
util.inherits(SetupError, Error);
SetupError.BAD_FIELD = 'Field error';
SetupError.BAD_STATE = 'Bad State';
SetupError.ALREADY_SETUP = 'Already Setup';
SetupError.INTERNAL_ERROR = 'Internal Error';
SetupError.EXTERNAL_ERROR = 'External Error';
SetupError.ALREADY_PROVISIONED = 'Already Provisioned';
function autoprovision(autoconf, callback) {
assert.strictEqual(typeof autoconf, 'object');
@@ -89,72 +89,64 @@ function autoprovision(autoconf, callback) {
return iteratorDone();
}
}, function (error) {
if (error) return callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
if (error) return callback(new SetupError(SetupError.INTERNAL_ERROR, error));
callback(null);
});
}
function unprovision(callback) {
assert.strictEqual(typeof callback, 'function');
debug('unprovision');
config.setAdminDomain('');
config.setAdminFqdn('');
config.setAdminLocation('my');
// TODO: also cancel any existing configureWebadmin task
async.series([
mail.clearDomains,
domains.clear
], callback);
}
function setup(dnsConfig, autoconf, auditSource, callback) {
function provision(dnsConfig, autoconf, auditSource, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof autoconf, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
users.isActivated(function (error, activated) {
if (error) return callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
if (activated) return callback(new ProvisionError(ProvisionError.ALREADY_SETUP));
if (config.adminDomain()) return callback(new SetupError(SetupError.ALREADY_SETUP));
unprovision(function (error) {
if (error) return callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
let webadminStatus = cloudron.getWebadminStatus();
let webadminStatus = cloudron.getWebadminStatus();
if (webadminStatus.configuring || webadminStatus.restore.active) return callback(new SetupError(SetupError.BAD_STATE, 'Already restoring or configuring'));
if (webadminStatus.configuring || webadminStatus.restore.active) return callback(new ProvisionError(ProvisionError.BAD_STATE, 'Already restoring or configuring'));
const domain = dnsConfig.domain.toLowerCase();
const zoneName = dnsConfig.zoneName ? dnsConfig.zoneName : (tld.getDomain(domain) || domain);
const domain = dnsConfig.domain.toLowerCase();
const zoneName = dnsConfig.zoneName ? dnsConfig.zoneName : (tld.getDomain(domain) || domain);
const adminFqdn = 'my' + (dnsConfig.config.hyphenatedSubdomains ? '-' : '.') + domain;
const adminFqdn = 'my' + (dnsConfig.config.hyphenatedSubdomains ? '-' : '.') + domain;
debug(`provision: Setting up Cloudron with domain ${domain} and zone ${zoneName} using admin fqdn ${adminFqdn}`);
debug(`provision: Setting up Cloudron with domain ${domain} and zone ${zoneName} using admin fqdn ${adminFqdn}`);
domains.get(domain, function (error, result) {
if (error && error.reason !== DomainsError.NOT_FOUND) return callback(new SetupError(SetupError.INTERNAL_ERROR, error));
let data = {
zoneName: zoneName,
provider: dnsConfig.provider,
config: dnsConfig.config,
fallbackCertificate: dnsConfig.fallbackCertificate || null,
tlsConfig: dnsConfig.tlsConfig || { provider: 'letsencrypt-prod' }
};
if (result) return callback(new SetupError(SetupError.BAD_STATE, 'Domain already exists'));
domains.add(domain, data, auditSource, function (error) {
if (error && error.reason === DomainsError.BAD_FIELD) return callback(new ProvisionError(ProvisionError.BAD_FIELD, error.message));
if (error && error.reason === DomainsError.ALREADY_EXISTS) return callback(new ProvisionError(ProvisionError.BAD_FIELD, error.message));
if (error) return callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
let data = {
zoneName: zoneName,
provider: dnsConfig.provider,
config: dnsConfig.config,
fallbackCertificate: dnsConfig.fallbackCertificate || null,
tlsConfig: dnsConfig.tlsConfig || { provider: 'letsencrypt-prod' }
};
async.series([
mail.addDomain.bind(null, domain),
cloudron.setDashboardDomain.bind(null, domain), // triggers task to setup my. dns/cert/reverseproxy
autoprovision.bind(null, autoconf),
eventlog.add.bind(null, eventlog.ACTION_PROVISION, auditSource, { })
], callback);
});
async.series([
domains.add.bind(null, domain, data, auditSource),
mail.addDomain.bind(null, domain)
], function (error) {
if (error && error.reason === DomainsError.BAD_FIELD) return callback(new SetupError(SetupError.BAD_FIELD, error.message));
if (error && error.reason === DomainsError.ALREADY_EXISTS) return callback(new SetupError(SetupError.BAD_FIELD, error.message));
if (error) return callback(new SetupError(SetupError.INTERNAL_ERROR, error));
config.setAdminDomain(domain); // set fqdn only after dns config is valid, otherwise cannot re-setup if we failed
config.setAdminFqdn(adminFqdn);
config.setAdminLocation('my');
eventlog.add(eventlog.ACTION_PROVISION, auditSource, { });
clients.addDefaultClients(config.adminOrigin(), callback);
async.series([
autoprovision.bind(null, autoconf),
cloudron.configureWebadmin
], NOOP_CALLBACK);
});
});
}
@@ -198,12 +190,12 @@ function activate(username, password, email, displayName, ip, auditSource, callb
setTimeZone(ip, function () { }); // TODO: get this from user. note that timezone is detected based on the browser location and not the cloudron region
users.createOwner(username, password, email, displayName, auditSource, function (error, userObject) {
if (error && error.reason === UsersError.ALREADY_EXISTS) return callback(new ProvisionError(ProvisionError.ALREADY_PROVISIONED, 'Already activated'));
if (error && error.reason === UsersError.BAD_FIELD) return callback(new ProvisionError(ProvisionError.BAD_FIELD, error.message));
if (error) return callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
if (error && error.reason === UsersError.ALREADY_EXISTS) return callback(new SetupError(SetupError.ALREADY_PROVISIONED));
if (error && error.reason === UsersError.BAD_FIELD) return callback(new SetupError(SetupError.BAD_FIELD, error.message));
if (error) return callback(new SetupError(SetupError.INTERNAL_ERROR, error));
clients.addTokenByUserId('cid-webadmin', userObject.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) {
if (error) return callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
if (error) return callback(new SetupError(SetupError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_ACTIVATE, auditSource, { });
@@ -226,21 +218,21 @@ function restore(backupConfig, backupId, version, autoconf, auditSource, callbac
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
if (!semver.valid(version)) return callback(new ProvisionError(ProvisionError.BAD_STATE, 'version is not a valid semver'));
if (semver.major(config.version()) !== semver.major(version) || semver.minor(config.version()) !== semver.minor(version)) return callback(new ProvisionError(ProvisionError.BAD_STATE, `Run cloudron-setup with --version ${version} to restore from this backup`));
if (!semver.valid(version)) return callback(new SetupError(SetupError.BAD_STATE, 'version is not a valid semver'));
if (semver.major(config.version()) !== semver.major(version) || semver.minor(config.version()) !== semver.minor(version)) return callback(new SetupError(SetupError.BAD_STATE, `Run cloudron-setup with --version ${version} to restore from this backup`));
let webadminStatus = cloudron.getWebadminStatus();
if (webadminStatus.configuring || webadminStatus.restore.active) return callback(new ProvisionError(ProvisionError.BAD_STATE, 'Already restoring or configuring'));
if (webadminStatus.configuring || webadminStatus.restore.active) return callback(new SetupError(SetupError.BAD_STATE, 'Already restoring or configuring'));
users.isActivated(function (error, activated) {
if (error) return callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
if (activated) return callback(new ProvisionError(ProvisionError.ALREADY_PROVISIONED, 'Already activated'));
if (error) return callback(new SetupError(SetupError.INTERNAL_ERROR, error));
if (activated) return callback(new SetupError(SetupError.ALREADY_PROVISIONED, 'Already activated'));
backups.testConfig(backupConfig, function (error) {
if (error && error.reason === BackupsError.BAD_FIELD) return callback(new ProvisionError(ProvisionError.BAD_FIELD, error.message));
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new ProvisionError(ProvisionError.EXTERNAL_ERROR, error.message));
if (error) return callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
if (error && error.reason === BackupsError.BAD_FIELD) return callback(new SetupError(SetupError.BAD_FIELD, error.message));
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new SetupError(SetupError.EXTERNAL_ERROR, error.message));
if (error) return callback(new SetupError(SetupError.INTERNAL_ERROR, error));
debug(`restore: restoring from ${backupId} from provider ${backupConfig.provider} with format ${backupConfig.format}`);
@@ -250,14 +242,14 @@ function restore(backupConfig, backupId, version, autoconf, auditSource, callbac
callback(null); // do no block
async.series([
backups.restore.bind(null, backupConfig, backupId, (progress) => debug(`restore: ${progress}`)),
backups.restore.bind(null, backupConfig, backupId),
eventlog.add.bind(null, eventlog.ACTION_RESTORE, auditSource, { backupId }),
autoprovision.bind(null, autoconf),
// currently, our suggested restore flow is after a dnsSetup. The dnSetup creates DKIM keys and updates the DNS
// for this reason, we have to re-setup DNS after a restore so it has DKIm from the backup
// Once we have a 100% IP based restore, we can skip this
mail.setDnsRecords.bind(null, config.adminDomain()),
shell.sudo.bind(null, 'restart', [ RESTART_CMD ], {})
shell.sudo.bind(null, 'restart', [ RESTART_CMD ])
], function (error) {
debug('restore:', error);
if (error) webadminStatus.restore.error = error.message;
+40 -23
View File
@@ -1,9 +1,10 @@
'use strict';
exports = module.exports = {
spawn: spawn,
exec: exec,
sudo: sudo
execSync: execSync,
sudo: sudo,
sudoSync: sudoSync
};
var assert = require('assert'),
@@ -14,22 +15,21 @@ var assert = require('assert'),
var SUDO = '/usr/bin/sudo';
function exec(tag, cmd, callback) {
function execSync(tag, cmd, callback) {
assert.strictEqual(typeof tag, 'string');
assert.strictEqual(typeof cmd, 'string');
assert.strictEqual(typeof callback, 'function');
debug(`${tag} exec: ${cmd}`);
child_process.exec(cmd, function (error, stdout, stderr) {
debug(`${tag} (stdout): %s`, stdout.toString('utf8'));
debug(`${tag} (stderr): %s`, stderr.toString('utf8'));
callback(error);
});
debug(cmd);
try {
child_process.execSync(cmd, { stdio: 'inherit' });
} catch (e) {
if (callback) return callback(e);
throw e;
}
if (callback) callback();
}
function spawn(tag, file, args, options, callback) {
function exec(tag, file, args, options, callback) {
assert.strictEqual(typeof tag, 'string');
assert.strictEqual(typeof file, 'string');
assert(util.isArray(args));
@@ -38,9 +38,7 @@ function spawn(tag, file, args, options, callback) {
callback = once(callback); // exit may or may not be called after an 'error'
debug(tag + ' spawn: %s %s', file, args.join(' '));
if (options.ipc) options.stdio = ['pipe', 'pipe', 'pipe', 'ipc'];
debug(tag + ' execFile: %s %s', file, args.join(' '));
var cp = child_process.spawn(file, args, options);
if (options.logStream) {
@@ -52,7 +50,7 @@ function spawn(tag, file, args, options, callback) {
});
cp.stderr.on('data', function (data) {
debug(tag + ' (stdout): %s', data.toString('utf8'));
debug(tag + ' (stderr): %s', data.toString('utf8'));
});
}
@@ -77,14 +75,33 @@ function spawn(tag, file, args, options, callback) {
function sudo(tag, args, options, callback) {
assert.strictEqual(typeof tag, 'string');
assert(util.isArray(args));
if (typeof options === 'function') {
callback = options;
options = { };
}
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
let sudoArgs = [ '-S' ]; // -S makes sudo read stdin for password
if (options.preserveEnv) sudoArgs.push('-E'); // -E preserves environment
if (options.ipc) sudoArgs.push('--close-from=4'); // keep the ipc open. requires closefrom_override in sudoers file
var cp = spawn(tag, SUDO, sudoArgs.concat(args), options, callback);
// -S makes sudo read stdin for password. -E preserves environment
var cp = exec(tag, SUDO, [ options.env ? '-SE' : '-S' ].concat(args), options, callback);
cp.stdin.end();
return cp;
}
function sudoSync(tag, cmd, callback) {
assert.strictEqual(typeof tag, 'string');
assert.strictEqual(typeof cmd, 'string');
// -S makes sudo read stdin for password
cmd = 'sudo -S ' + cmd;
debug(cmd);
try {
child_process.execSync(cmd, { stdio: 'inherit' });
} catch (e) {
if (callback) return callback(e);
throw e;
}
if (callback) callback();
}
+40 -60
View File
@@ -14,6 +14,7 @@ exports = module.exports = {
var assert = require('assert'),
config = require('./config.js'),
debug = require('debug')('box:ssh'),
fs = require('fs'),
path = require('path'),
safe = require('safetydance'),
shell = require('./shell.js'),
@@ -55,72 +56,61 @@ function clear(callback) {
callback();
}
function saveKeys(keys, callback) {
function saveKeys(keys) {
assert(Array.isArray(keys));
assert.strictEqual(typeof callback, 'function');
if (!safe.fs.writeFileSync(AUTHORIZED_KEYS_TMP_FILEPATH, keys.map(function (k) { return k.key; }).join('\n'))) {
debug('Error writing to temporary file', safe.error);
return callback(safe.error);
return false;
}
if (!safe.fs.chmodSync(AUTHORIZED_KEYS_TMP_FILEPATH, '600')) { // 600 = rw-------
debug('Failed to adjust permissions of %s %s', AUTHORIZED_KEYS_TMP_FILEPATH, safe.error);
return callback(safe.error);
try {
// 600 = rw-------
fs.chmodSync(AUTHORIZED_KEYS_TMP_FILEPATH, '600');
} catch (e) {
debug('Failed to adjust permissions of %s %j', AUTHORIZED_KEYS_TMP_FILEPATH, e);
return false;
}
var user = config.TEST ? process.env.USER : ((config.provider() === 'ec2' || config.provider() === 'lightsail' || config.provider() === 'ami') ? 'ubuntu' : 'root');
shell.sudo('authorized_keys', [ AUTHORIZED_KEYS_CMD, user, AUTHORIZED_KEYS_TMP_FILEPATH, AUTHORIZED_KEYS_FILEPATH ], {}, function (error) {
if (error) return callback(error);
shell.sudoSync('authorized_keys', util.format('%s %s %s %s', AUTHORIZED_KEYS_CMD, user, AUTHORIZED_KEYS_TMP_FILEPATH, AUTHORIZED_KEYS_FILEPATH));
callback(null);
});
return true;
}
function getKeys(callback) {
assert.strictEqual(typeof callback, 'function');
function getKeys() {
shell.sudoSync('authorized_keys', util.format('%s %s %s %s', AUTHORIZED_KEYS_CMD, process.env.USER, AUTHORIZED_KEYS_FILEPATH, AUTHORIZED_KEYS_TMP_FILEPATH));
shell.sudo('authorized_keys', [ AUTHORIZED_KEYS_CMD, process.env.USER, AUTHORIZED_KEYS_FILEPATH, AUTHORIZED_KEYS_TMP_FILEPATH ], {}, function (error) {
if (error) return callback(error);
var content = safe.fs.readFileSync(AUTHORIZED_KEYS_TMP_FILEPATH, 'utf8');
if (!content) return [];
var content = safe.fs.readFileSync(AUTHORIZED_KEYS_TMP_FILEPATH, 'utf8');
if (!content) return callback(null, []);
var keys = content.split('\n')
.filter(function (k) { return !!k.trim(); })
.map(function (k) { return { identifier: k.split(' ')[2], key: k }; })
.filter(function (k) { return k.identifier && k.key; });
var keys = content.split('\n')
.filter(function (k) { return !!k.trim(); })
.map(function (k) { return { identifier: k.split(' ')[2], key: k }; })
.filter(function (k) { return k.identifier && k.key; });
safe.fs.unlinkSync(AUTHORIZED_KEYS_TMP_FILEPATH);
safe.fs.unlinkSync(AUTHORIZED_KEYS_TMP_FILEPATH);
return callback(null, keys);
});
return keys;
}
function getAuthorizedKeys(callback) {
assert.strictEqual(typeof callback, 'function');
getKeys(function (error, keys) {
if (error) return callback(new SshError(SshError.INTERNAL_ERROR, error));
return callback(null, keys.sort(function (a, b) { return a.identifier.localeCompare(b.identifier); }));
});
return callback(null, getKeys().sort(function (a, b) { return a.identifier.localeCompare(b.identifier); }));
}
function getAuthorizedKey(identifier, callback) {
assert.strictEqual(typeof identifier, 'string');
assert.strictEqual(typeof callback, 'function');
getKeys(function (error, keys) {
if (error) return callback(new SshError(SshError.INTERNAL_ERROR, error));
var keys = getKeys();
if (keys.length === 0) return callback(new SshError(SshError.NOT_FOUND));
if (keys.length === 0) return callback(new SshError(SshError.NOT_FOUND));
var key = keys.find(function (k) { return k.identifier === identifier; });
if (!key) return callback(new SshError(SshError.NOT_FOUND));
var key = keys.find(function (k) { return k.identifier === identifier; });
if (!key) return callback(new SshError(SshError.NOT_FOUND));
callback(null, key);
});
callback(null, key);
}
function addAuthorizedKey(key, callback) {
@@ -134,38 +124,28 @@ function addAuthorizedKey(key, callback) {
var identifier = tmp[2];
getKeys(function (error, keys) {
if (error) return callback(new SshError(SshError.INTERNAL_ERROR, error));
var keys = getKeys();
var index = keys.findIndex(function (k) { return k.identifier === identifier; });
if (index !== -1) keys[index] = { identifier: identifier, key: key };
else keys.push({ identifier: identifier, key: key });
var index = keys.findIndex(function (k) { return k.identifier === identifier; });
if (index !== -1) keys[index] = { identifier: identifier, key: key };
else keys.push({ identifier: identifier, key: key });
if (!saveKeys(keys)) return callback(new SshError(SshError.INTERNAL_ERROR));
saveKeys(keys, function (error) {
if (error) return callback(new SshError(SshError.INTERNAL_ERROR, error));
callback(null);
});
});
callback();
}
function delAuthorizedKey(identifier, callback) {
assert.strictEqual(typeof identifier, 'string');
assert.strictEqual(typeof callback, 'function');
getKeys(function (error, keys) {
if (error) return callback(new SshError(SshError.INTERNAL_ERROR, error));
var keys = getKeys();
var index = keys.findIndex(function (k) { return k.identifier === identifier; });
if (index === -1) return callback(new SshError(SshError.NOT_FOUND));
let index = keys.findIndex(function (k) { return k.identifier === identifier; });
if (index === -1) return callback(new SshError(SshError.NOT_FOUND));
// now remove the key
keys.splice(index, 1);
// now remove the key
keys.splice(index, 1);
if (!saveKeys(keys)) return callback(new SshError(SshError.INTERNAL_ERROR));
saveKeys(keys, function (error) {
if (error) return callback(new SshError(SshError.INTERNAL_ERROR, error));
callback(null);
});
});
callback();
}
+2 -2
View File
@@ -120,7 +120,7 @@ function copy(apiConfig, oldFilePath, newFilePath) {
// this will hardlink backups saving space
var cpOptions = apiConfig.noHardlinks ? '-a' : '-al';
shell.spawn('copy', '/bin/cp', [ cpOptions, oldFilePath, newFilePath ], { }, function (error) {
shell.exec('copy', '/bin/cp', [ cpOptions, oldFilePath, newFilePath ], { }, function (error) {
if (error) return events.emit('done', new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
events.emit('done', null);
@@ -155,7 +155,7 @@ function removeDir(apiConfig, pathPrefix) {
events.emit('progress', `removeDir: ${pathPrefix}`);
shell.spawn('removeDir', '/bin/rm', [ '-rf', pathPrefix ], { }, function (error) {
shell.exec('removeDir', '/bin/rm', [ '-rf', pathPrefix ], { }, function (error) {
if (error) return events.emit('done', new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
events.emit('done', null);
-115
View File
@@ -1,115 +0,0 @@
'use strict';
exports = module.exports = {
get: get,
add: add,
update: update,
del: del,
listByTypePaged: listByTypePaged
};
let assert = require('assert'),
database = require('./database.js'),
DatabaseError = require('./databaseerror'),
safe = require('safetydance');
const TASKS_FIELDS = [ 'id', 'type', 'argsJson', 'percent', 'message', 'errorMessage', 'creationTime', 'result', 'ts' ];
function postProcess(result) {
assert.strictEqual(typeof result, 'object');
assert(result.argsJson === null || typeof result.argsJson === 'string');
result.args = safe.JSON.parse(result.argsJson) || [];
delete result.argsJson;
result.id = String(result.id);
}
function add(task, callback) {
assert.strictEqual(typeof task, 'object');
assert.strictEqual(typeof callback, 'function');
const query = 'INSERT INTO tasks (type, argsJson, percent, message) VALUES (?, ?, ?, ?)';
const args = [ task.type, JSON.stringify(task.args), task.percent, task.message ];
database.query(query, args, function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null, String(result.insertId));
});
}
function update(id, data, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof callback, 'function');
let args = [ ];
let fields = [ ];
for (let k in data) {
fields.push(k + ' = ?');
args.push(data[k]);
}
args.push(id);
database.query('UPDATE tasks SET ' + fields.join(', ') + ' WHERE id = ?', args, function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
return callback(null);
});
}
function get(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + TASKS_FIELDS + ' FROM tasks WHERE id = ?', [ id ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
postProcess(result[0]);
callback(null, result[0]);
});
}
function del(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('DELETE FROM tasks WHERE id = ?', [ id ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
callback(null);
});
}
function listByTypePaged(type, page, perPage, callback) {
assert(typeof type === 'string' || type === null);
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function');
var data = [];
var query = 'SELECT ' + TASKS_FIELDS + ' FROM tasks';
if (type) {
query += ' WHERE TYPE=?';
data.push(type);
}
query += ' ORDER BY creationTime DESC LIMIT ?,?';
data.push((page-1)*perPage);
data.push(perPage);
database.query(query, data, function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
results.forEach(postProcess);
callback(null, results);
});
}
-223
View File
@@ -1,223 +0,0 @@
'use strict';
exports = module.exports = {
get: get,
update: update,
listByTypePaged: listByTypePaged,
getLogs: getLogs,
startTask: startTask,
stopTask: stopTask,
removePrivateFields: removePrivateFields,
TaskError: TaskError,
// task types. if you add a task here, fill up the function table in taskworker
TASK_BACKUP: 'backup',
TASK_UPDATE: 'update',
TASK_RENEW_CERTS: 'renewcerts',
// testing
_TASK_IDENTITY: '_identity',
_TASK_CRASH: '_crash',
_TASK_ERROR: '_error',
_TASK_SLEEP: '_sleep'
};
let assert = require('assert'),
child_process = require('child_process'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:tasks'),
EventEmitter = require('events'),
paths = require('./paths.js'),
safe = require('safetydance'),
spawn = require('child_process').spawn,
split = require('split'),
taskdb = require('./taskdb.js'),
util = require('util'),
_ = require('underscore');
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
let gTasks = {}; // indexed by task id
function TaskError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
Error.call(this);
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.reason = reason;
if (typeof errorOrMessage === 'undefined') {
this.message = reason;
} else if (typeof errorOrMessage === 'string') {
this.message = errorOrMessage;
} else {
this.message = 'Internal error';
this.nestedError = errorOrMessage;
}
}
util.inherits(TaskError, Error);
TaskError.INTERNAL_ERROR = 'Internal Error';
TaskError.BAD_STATE = 'Bad State';
TaskError.NOT_FOUND = 'Not Found';
function get(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
taskdb.get(id, function (error, task) {
if (error && error.reason == DatabaseError.NOT_FOUND) return callback(new TaskError(TaskError.NOT_FOUND));
if (error) return callback(new TaskError(TaskError.INTERNAL_ERROR, error));
task.active = !!gTasks[id];
callback(null, task);
});
}
function update(id, task, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof task, 'object');
assert.strictEqual(typeof callback, 'function');
debug(`${id}: ${JSON.stringify(task)}`);
taskdb.update(id, task, function (error) {
if (error && error.reason == DatabaseError.NOT_FOUND) return callback(new TaskError(TaskError.NOT_FOUND));
if (error) return callback(new TaskError(TaskError.INTERNAL_ERROR, error));
callback();
});
}
function startTask(type, args) {
assert.strictEqual(typeof type, 'string');
assert(Array.isArray(args));
let events = new EventEmitter();
taskdb.add({ type: type, percent: 0, message: 'Starting', args: args }, function (error, taskId) {
if (error) return events.emit('error', new TaskError(TaskError.INTERNAL_ERROR, error));
const logFile = `${paths.TASKS_LOG_DIR}/${taskId}.log`;
let fd = safe.fs.openSync(logFile, 'a'); // will autoclose
if (!fd) {
debug(`startTask: unable to get log filedescriptor ${safe.error.message}`);
return events.emit('error', new TaskError(TaskError.INTERNAL_ERROR, error.message));
}
debug(`startTask - starting task ${type}. logs at ${logFile} id ${taskId}`);
gTasks[taskId] = child_process.fork(`${__dirname}/taskworker.js`, [ taskId ], { stdio: [ 'pipe', fd, fd, 'ipc' ]}); // fork requires ipc
gTasks[taskId].once('exit', function (code, signal) {
debug(`startTask: ${taskId} completed with code ${code} and signal ${signal}`);
get(taskId, function (error, task) {
if (!error && task.percent !== 100) { // task crashed or was killed by us (code 50)
error = code === 0 ? new Error(`${taskId} task stopped`) : new Error(`${taskId} task crashed with code ${code} and signal ${signal}`);
update(taskId, { percent: 100, errorMessage: error.message }, NOOP_CALLBACK);
} else if (!error && task.errorMessage) {
error = new Error(task.errorMessage);
} else if (!task) { // db got cleared in tests
error = new Error(`No such task ${taskId}`);
}
gTasks[taskId] = null;
events.emit('finish', error, task ? task.result : null);
debug(`startTask: ${taskId} done`);
});
});
events.emit('start', taskId);
});
return events;
}
function stopTask(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
if (!gTasks[id]) return callback(new TaskError(TaskError.BAD_STATE, 'task is not active'));
debug(`stopTask: stopping task ${id}`);
gTasks[id].kill('SIGTERM'); // this will end up calling the 'exit' signal handler
callback(null);
}
function listByTypePaged(type, page, perPage, callback) {
assert(typeof type === 'string' || type === null);
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function');
taskdb.listByTypePaged(type, page, perPage, function (error, tasks) {
if (error) return callback(new TaskError(TaskError.INTERNAL_ERROR, error));
tasks.forEach((task) => { task.active = !!gTasks[task.id]; });
callback(null, tasks);
});
}
function getLogs(taskId, options, callback) {
assert.strictEqual(typeof taskId, 'string');
assert(options && typeof options === 'object');
assert.strictEqual(typeof callback, 'function');
debug(`Getting logs for ${taskId}`);
var lines = options.lines || 100,
format = options.format || 'json',
follow = !!options.follow;
assert.strictEqual(typeof lines, 'number');
assert.strictEqual(typeof format, 'string');
let cmd = '/usr/bin/tail';
var args = [ '--lines=' + lines ];
if (follow) args.push('--follow', '--retry', '--quiet'); // same as -F. to make it work if file doesn't exist, --quiet to not output file headers, which are no logs
args.push(`${paths.TASKS_LOG_DIR}/${taskId}.log`);
var cp = spawn(cmd, args);
var transformStream = split(function mapper(line) {
if (format !== 'json') return line + '\n';
var data = line.split(' '); // logs are <ISOtimestamp> <msg>
var timestamp = (new Date(data[0])).getTime();
if (isNaN(timestamp)) timestamp = 0;
var message = line.slice(data[0].length+1);
// ignore faulty empty logs
if (!timestamp && !message) return;
return JSON.stringify({
realtimeTimestamp: timestamp * 1000,
message: message,
source: taskId
}) + '\n';
});
transformStream.close = cp.kill.bind(cp, 'SIGKILL'); // closing stream kills the child process
cp.stdout.pipe(transformStream);
callback(null, transformStream);
}
// removes all fields that are strictly private and should never be returned by API calls
function removePrivateFields(task) {
var result = _.pick(task, 'id', 'type', 'percent', 'message', 'errorMessage', 'active', 'creationTime', 'result', 'ts');
return result;
}
-51
View File
@@ -1,51 +0,0 @@
'use strict';
require('supererror')({ splatchError: true });
var assert = require('assert'),
backups = require('./backups.js'),
database = require('./database.js'),
debug = require('debug')('box:taskworker'),
reverseProxy = require('./reverseproxy.js'),
tasks = require('./tasks.js'),
updater = require('./updater.js');
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
const TASKS = { // indexed by task type
backup: backups.backupBoxAndApps,
update: updater.update,
renewcerts: reverseProxy.renewCerts,
_identity: (arg, progressCallback, callback) => callback(null, arg),
_error: (arg, progressCallback, callback) => callback(new Error(`Failed for arg: ${arg}`)),
_crash: (arg) => { throw new Error(`Crashing for arg: ${arg}`); },
_sleep: (arg) => setTimeout(process.exit, arg)
};
process.on('SIGTERM', function () {
process.exit(0);
});
assert.strictEqual(process.argv.length, 3, 'Pass the taskid as argument');
const taskId = process.argv[2];
// Main process starts here
debug(`Staring task ${taskId}`);
database.initialize(function (error) {
if (error) return process.exit(50);
tasks.get(taskId, function (error, task) {
if (error) return process.exit(50);
const progressCallback = (progress) => tasks.update(taskId, progress, NOOP_CALLBACK);
const resultCallback = (error, result) => {
const progress = { percent: 100, result: result || null, errorMessage: error ? error.message : null };
tasks.update(taskId, progress, () => process.exit(error ? 50 : 0));
};
TASKS[task.type].apply(null, task.args.concat(progressCallback).concat(resultCallback));
});
});
+64 -15
View File
@@ -16,31 +16,73 @@ var async = require('async'),
fs = require('fs'),
os = require('os'),
mkdirp = require('mkdirp'),
readdirp = require('readdirp'),
path = require('path'),
progress = require('../progress.js'),
rimraf = require('rimraf'),
settings = require('../settings.js'),
SettingsError = require('../settings.js').SettingsError,
tasks = require('../tasks.js');
SettingsError = require('../settings.js').SettingsError;
function compareDirectories(one, two, callback) {
readdirp({ root: one }, function (error, treeOne) {
if (error) return callback(error);
readdirp({ root: two }, function (error, treeTwo) {
if (error) return callback(error);
var mismatch = [];
function compareDirs(a, b) {
a.forEach(function (tmpA) {
var found = b.find(function (tmpB) {
return tmpA.path === tmpB.path;
});
if (!found) mismatch.push(tmpA);
});
}
function compareFiles(a, b) {
a.forEach(function (tmpA) {
var found = b.find(function (tmpB) {
// TODO check file or symbolic link
return tmpA.path === tmpB.path && tmpA.mode === tmpB.mode;
});
if (!found) mismatch.push(tmpA);
});
}
compareDirs(treeOne.directories, treeTwo.directories);
compareDirs(treeTwo.directories, treeOne.directories);
compareFiles(treeOne.files, treeTwo.files);
compareFiles(treeTwo.files, treeOne.files);
if (mismatch.length) {
console.error('Files not found in both: %j', mismatch);
return callback(new Error('file mismatch'));
}
callback(null);
});
});
}
function createBackup(callback) {
backups.startBackupTask({ username: 'test' }, function (error, taskId) { // this call does not wait for the backup!
backups.backup({ username: 'test' }, function (error) { // this call does not wait for the backup!
if (error) return callback(error);
function waitForBackup() {
tasks.get(taskId, function (error, p) {
var p = progress.getAll();
if (p.backup.percent !== 100) return setTimeout(waitForBackup, 1000);
if (p.backup.message) return callback(new Error('backup failed:' + p.backup.message));
backups.getByStatePaged(backupdb.BACKUP_STATE_NORMAL, 1, 1, function (error, result) {
if (error) return callback(error);
if (result.length !== 1) return callback(new Error('result is not of length 1'));
if (p.percent !== 100) return setTimeout(waitForBackup, 1000);
if (p.errorMessage) return callback(new Error('backup failed:' + p));
if (!p.result) return callback(new Error('backup has no result:' + p));
backups.getByStatePaged(backupdb.BACKUP_STATE_NORMAL, 1, 1, function (error, result) {
if (error) return callback(error);
if (result.length !== 1) return callback(new Error('result is not of length 1'));
callback(null, result[0]);
});
callback(null, result[0]);
});
}
@@ -73,6 +115,8 @@ describe('backups', function () {
});
describe('cleanup', function () {
this.timeout(20000);
var BACKUP_0 = {
id: 'backup-box-0',
version: '1.0.0',
@@ -261,6 +305,7 @@ describe('backups', function () {
after(function (done) {
rimraf.sync(gBackupConfig.backupFolder);
progress.clear(progress.BACKUP);
done();
});
@@ -284,6 +329,8 @@ describe('backups', function () {
});
it('can backup', function (done) {
this.timeout(6000);
createBackup(function (error, result) {
expect(error).to.be(null);
expect(fs.statSync(path.join(gBackupConfig.backupFolder, 'snapshot/box.tar.gz')).nlink).to.be(2); // hard linked to a rotated backup
@@ -296,6 +343,8 @@ describe('backups', function () {
});
it('can take another backup', function (done) {
this.timeout(6000);
createBackup(function (error, result) {
expect(error).to.be(null);
expect(fs.statSync(path.join(gBackupConfig.backupFolder, 'snapshot/box.tar.gz')).nlink).to.be(2); // hard linked to a rotated backup
+2 -5
View File
@@ -10,17 +10,14 @@ sudo -k || sudo --reset-timestamp
# checks if all scripts are sudo access
scripts=("${SOURCE_DIR}/src/scripts/rmvolume.sh" \
"${SOURCE_DIR}/src/scripts/rmaddondir.sh" \
"${SOURCE_DIR}/src/scripts/rmaddon.sh" \
"${SOURCE_DIR}/src/scripts/reloadnginx.sh" \
"${SOURCE_DIR}/src/scripts/reboot.sh" \
"${SOURCE_DIR}/src/scripts/restart.sh" \
"${SOURCE_DIR}/src/scripts/restartdocker.sh" \
"${SOURCE_DIR}/src/scripts/restartunbound.sh" \
"${SOURCE_DIR}/src/scripts/update.sh" \
"${SOURCE_DIR}/src/scripts/collectlogs.sh" \
"${SOURCE_DIR}/src/scripts/configurecollectd.sh" \
"${SOURCE_DIR}/src/scripts/authorized_keys.sh" \
"${SOURCE_DIR}/src/scripts/backupupload.js" \
"${SOURCE_DIR}/src/backuptask.js" \
"${SOURCE_DIR}/src/scripts/configurelogrotate.sh")
for script in "${scripts[@]}"; do
+10 -91
View File
@@ -21,7 +21,6 @@ var appdb = require('../appdb.js'),
mailboxdb = require('../mailboxdb.js'),
maildb = require('../maildb.js'),
settingsdb = require('../settingsdb.js'),
taskdb = require('../taskdb.js'),
tokendb = require('../tokendb.js'),
userdb = require('../userdb.js'),
_ = require('underscore');
@@ -99,6 +98,8 @@ const TEST_DOMAIN = {
};
describe('database', function () {
this.timeout(5000);
before(function (done) {
config._reset();
config.setFqdn(TEST_DOMAIN.domain);
@@ -233,9 +234,7 @@ describe('database', function () {
robotsTxt: null,
enableBackup: true,
ownerId: USER_0.id,
env: {},
mailboxName: 'talktome',
enableAutomaticUpdate: true
env: {}
};
it('cannot delete referenced domain', function (done) {
@@ -756,9 +755,7 @@ describe('database', function () {
alternateDomains: [],
env: {
'CUSTOM_KEY': 'CUSTOM_VALUE'
},
mailboxName: 'talktome',
enableAutomaticUpdate: true
}
};
var APP_1 = {
@@ -786,9 +783,7 @@ describe('database', function () {
enableBackup: true,
ownerId: USER_0.id,
alternateDomains: [],
env: {},
mailboxName: 'callme',
enableAutomaticUpdate: true
env: {}
};
before(function (done) {
@@ -876,7 +871,6 @@ describe('database', function () {
var data = {
installationState: APP_0.installationState,
location: APP_0.location,
domain: APP_0.domain,
manifest: APP_0.manifest,
accessRestriction: APP_0.accessRestriction,
httpPort: APP_0.httpPort,
@@ -1100,82 +1094,6 @@ describe('database', function () {
});
});
describe('tasks', function () {
let taskId;
let TASK = {
type: 'tasktype',
args: { x: 1 },
percent: 0,
message: 'starting task'
};
it('add succeeds', function (done) {
taskdb.add(TASK, function (error, id) {
expect(error).to.be(null);
expect(id).to.be.ok();
taskId = id;
done();
});
});
it('get succeeds', function (done) {
taskdb.get(taskId, function (error, task) {
expect(error).to.be(null);
expect(_.pick(task, Object.keys(TASK))).to.eql(TASK);
done();
});
});
it('update succeeds', function (done) {
TASK.percent = 34;
TASK.message = 'almost ther';
taskdb.update(taskId, { percent: TASK.percent, message: TASK.message }, function (error) {
expect(error).to.be(null);
taskdb.get(taskId, function (error, task) {
expect(_.pick(task, Object.keys(TASK))).to.eql(TASK);
done();
});
});
});
it('list succeeds - does not exist', function (done) {
taskdb.listByTypePaged('randomtask', 1, 1, function (error, tasks) {
expect(error).to.be(null);
expect(tasks.length).to.be(0);
done();
});
});
it('list succeeds - by type', function (done) {
taskdb.listByTypePaged(TASK.type, 1, 1, function (error, tasks) {
expect(error).to.be(null);
expect(tasks.length).to.be(1);
expect(_.pick(tasks[0], Object.keys(TASK))).to.eql(TASK);
done();
});
});
it('list succeeds - all', function (done) {
taskdb.listByTypePaged(null, 1, 1, function (error, tasks) {
expect(error).to.be(null);
expect(tasks.length).to.be(1);
expect(_.pick(tasks[0], Object.keys(TASK))).to.eql(TASK);
done();
});
});
it('del succeeds', function (done) {
taskdb.del(taskId, function (error) {
expect(error).to.be(null);
taskdb.get(taskId, function (error) {
expect(error.reason).to.be(DatabaseError.NOT_FOUND);
done();
});
});
});
});
describe('client', function () {
var CLIENT_0 = {
id: 'cid-0',
@@ -1716,21 +1634,21 @@ describe('database', function () {
});
it('add user mailbox succeeds', function (done) {
mailboxdb.addMailbox('girish', DOMAIN_0.domain, 'uid-0', function (error) {
mailboxdb.addMailbox('girish', DOMAIN_0.domain, 'uid-0', mailboxdb.OWNER_TYPE_USER, function (error) {
expect(error).to.be(null);
done();
});
});
it('cannot add dup entry', function (done) {
mailboxdb.addMailbox('girish', DOMAIN_0.domain, 'uid-1', function (error) {
mailboxdb.addMailbox('girish', DOMAIN_0.domain, 'uid-1', mailboxdb.OWNER_TYPE_APP, function (error) {
expect(error.reason).to.be(DatabaseError.ALREADY_EXISTS);
done();
});
});
it('add app mailbox succeeds', function (done) {
mailboxdb.addMailbox('support', DOMAIN_0.domain, 'osticket', function (error) {
mailboxdb.addMailbox('support', DOMAIN_0.domain, 'osticket', mailboxdb.OWNER_TYPE_APP, function (error) {
expect(error).to.be(null);
done();
});
@@ -1753,6 +1671,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[1].name).to.be('support');
done();
@@ -1898,7 +1817,7 @@ describe('database', function () {
});
it('can get all domains', function (done) {
maildb.list(function (error, result) {
maildb.getAll(function (error, result) {
expect(error).to.equal(null);
expect(result).to.be.an(Array);
expect(result[0]).to.be.an('object');
+1
View File
@@ -42,6 +42,7 @@ describe('Dockerproxy', function () {
});
// uncomment this to run the proxy for manual testing
// this.timeout(1000000);
// it('wait', function (done) {} );
it('can get info', function (done) {
+8 -7
View File
@@ -79,8 +79,7 @@ var APP_0 = {
restoreConfig: null,
oldConfig: null,
memoryLimit: 4294967296,
ownerId: null,
mailboxName: 'some-location-0.app'
ownerId: null
};
var dockerProxy;
@@ -113,7 +112,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.addMailbox.bind(null, APP_0.location + '.app', APP_0.domain, APP_0.id, mailboxdb.OWNER_TYPE_APP),
function (callback) {
users.create(USER_1.username, USER_1.password, USER_1.email, USER_0.displayName, { invitor: USER_0 }, AUDIT_SOURCE, function (error, result) {
@@ -203,6 +202,8 @@ function cleanup(done) {
}
describe('Ldap', function () {
this.timeout(10000);
before(setup);
after(cleanup);
@@ -813,7 +814,7 @@ describe('Ldap', function () {
describe('search mailbox', function () {
before(function (done) {
mailboxdb.addMailbox(USER_0.username.toLowerCase(), DOMAIN_0.domain, USER_0.id, done);
mailboxdb.addMailbox(USER_0.username.toLowerCase(), DOMAIN_0.domain, USER_0.id, mailboxdb.OWNER_TYPE_USER, done);
});
it('get specific mailbox by email', function (done) {
@@ -924,7 +925,7 @@ describe('Ldap', function () {
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
client.bind('cn=' + USER_0.username + '@example.com,ou=sendmail,dc=cloudron', USER_0.password + 'nope', function (error) {
expect(error).to.be.a(ldap.InvalidCredentialsError);
expect(error).to.be.a(ldap.NoSuchObjectError);
client.unbind(done);
});
});
@@ -992,7 +993,7 @@ describe('Ldap', function () {
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
client.bind('cn=' + APP_0.location + '.app@example.com,ou=sendmail,dc=cloudron', 'nope', function (error) {
expect(error).to.be.a(ldap.NoSuchObjectError);
expect(error).to.be.a(ldap.InvalidCredentialsError);
client.unbind(done);
});
});
@@ -1083,7 +1084,7 @@ describe('Ldap', function () {
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
client.bind('cn=' + APP_0.location + '.app@example.com,ou=recvmail,dc=cloudron', 'nope', function (error) {
expect(error).to.be.a(ldap.NoSuchObjectError);
expect(error).to.be.a(ldap.InvalidCredentialsError);
client.unbind(done);
});
});

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