Compare commits

..

6 Commits

Author SHA1 Message Date
Girish Ramakrishnan c0c5561aac DO DNS API break means this value must atleast be 30 2019-06-17 20:13:09 -07:00
Girish Ramakrishnan 23bfc1a3b8 Fix mail auth with manifest v2
(cherry picked from commit 8cd5c15c2b)
2019-06-17 11:14:16 -07:00
Girish Ramakrishnan 73a44d1fb2 4.1.4 changes 2019-06-16 17:58:56 -07:00
Girish Ramakrishnan a1970f3b65 Prefix mysql url/database variables
(cherry picked from commit c5f6e6b028)
2019-06-15 11:22:58 -07:00
Girish Ramakrishnan c69f4e4a48 Add 4.1.3 changes 2019-06-14 16:56:02 -07:00
Girish Ramakrishnan 417a8de823 Update manifestformat (for v2) 2019-06-14 16:55:11 -07:00
140 changed files with 4526 additions and 6431 deletions
-43
View File
@@ -1631,46 +1631,3 @@
[4.1.4]
* Add CLOUDRON_ prefix to MySQL addon variables
[4.1.5]
* Make the terminal addon button inject variables based on manifest version
* Preserve addon passwords correctly when using v2 manifest
* Show error message instead of logging out user when invalid 2FA token is provided
* Ensure redis vars are renamed with manifest v2
* Add missing Scaleway Object Storage to restore UI
* Fix Exoscale endpoints in restore UI
* Reset the app icon when showing the configure UI
[4.1.6]
* Fix issue where CLOUDRON_APP_HOSTNAME was incorrectly set
* Remove chat link from the footer of login screen
* Add support for oplog tailing in mongodb
* Fix LDAP not accessible via scheduler containers
[4.1.7]
* Fix issue where login looped when admin bit was removed
[4.2.0]
* Fix issue where tar backups with files > 8GB was corrupt
* Add SparkPost as mail relay backend
* Add Wasabi storage backend
* TOTP tokens are now checked for with +- 60 seconds
* IP based restore
* Fix issue where task logs were not getting rotated correctly
* Add notification for box update
* User enable/disable flag
* Check disk space before various operations like install, update, backup etc
* Collect per app du information
* Set Cloudron specific UA for healthchecks
* Show message why an app task is 'pending'
* Rework app task system so that we can now pass dynamic arguments
* Add external LDAP server integration
[4.2.1]
* Rework the app configuration routes & UI
* Fine grained eventlog for app configuration
* Update Haraka to 2.8.24
* Set sieve_max_redirects to 64
* SRS support for mail forwarding
* Fix issue where sieve responses were not sent via the relay
* File based session store
* Fix API token error reporting for namecheap backend
-2
View File
@@ -33,7 +33,6 @@ gpg_package=$([[ "${ubuntu_version}" == "16.04" ]] && echo "gnupg" || echo "gpg"
apt-get -y install \
acl \
build-essential \
cifs-utils \
cron \
curl \
debconf-utils \
@@ -41,7 +40,6 @@ apt-get -y install \
$gpg_package \
iptables \
libpython2.7 \
linux-generic \
logrotate \
mysql-server-5.7 \
nginx-full \
+14 -3
View File
@@ -14,14 +14,25 @@
require('supererror')({ splatchError: true });
let async = require('async'),
constants = require('./src/constants.js'),
dockerProxy = require('./src/dockerproxy.js'),
config = require('./src/config.js'),
ldap = require('./src/ldap.js'),
dockerProxy = require('./src/dockerproxy.js'),
server = require('./src/server.js');
console.log();
console.log('==========================================');
console.log(` Cloudron ${constants.VERSION} `);
console.log(' Cloudron will use the following settings ');
console.log('==========================================');
console.log();
console.log(' Environment: ', config.CLOUDRON ? 'CLOUDRON' : 'TEST');
console.log(' Version: ', config.version());
console.log(' Admin Origin: ', config.adminOrigin());
console.log(' Appstore API server origin: ', config.apiServerOrigin());
console.log(' Appstore Web server origin: ', config.webServerOrigin());
console.log(' SysAdmin Port: ', config.get('sysadminPort'));
console.log(' LDAP Server Port: ', config.get('ldapPort'));
console.log(' Docker Proxy Port: ', config.get('dockerProxyPort'));
console.log();
console.log('==========================================');
console.log();
@@ -1,14 +0,0 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'ALTER TABLE apps DROP FOREIGN KEY apps_owner_constraint'),
db.runSql.bind(db, 'ALTER TABLE apps DROP COLUMN ownerId')
], callback);
};
exports.down = function(db, callback) {
callback();
};
@@ -1,29 +0,0 @@
'use strict';
var async = require('async'),
fs = require('fs'),
superagent = require('superagent');
exports.up = function(db, callback) {
if (!fs.existsSync('/etc/cloudron/cloudron.conf')) {
console.log('Unable to locate cloudron.conf');
return callback();
}
const config = JSON.parse(fs.readFileSync('/etc/cloudron/cloudron.conf', 'utf8'));
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
// we use replace instead of insert because the cloudron-setup adds api/web_server_origin even for legacy setups
db.runSql.bind(db, 'REPLACE INTO settings (name, value) VALUES(?, ?)', [ 'api_server_origin', config.apiServerOrigin ]),
db.runSql.bind(db, 'REPLACE INTO settings (name, value) VALUES(?, ?)', [ 'web_server_origin', config.webServerOrigin ]),
db.runSql.bind(db, 'REPLACE INTO settings (name, value) VALUES(?, ?)', [ 'admin_domain', config.adminDomain ]),
db.runSql.bind(db, 'REPLACE INTO settings (name, value) VALUES(?, ?)', [ 'admin_fqdn', config.adminFqdn ]),
db.runSql.bind(db, 'REPLACE INTO settings (name, value) VALUES(?, ?)', [ 'demo', config.isDemo ]),
db.runSql.bind(db, 'COMMIT')
], callback);
};
exports.down = function(db, callback) {
callback();
};
@@ -1,17 +0,0 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE users ADD COLUMN active BOOLEAN DEFAULT 1', function (error) {
if (error) return callback(error);
callback();
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE users DROP COLUMN active', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,17 +0,0 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'ALTER TABLE apps ADD COLUMN taskId INTEGER'),
db.runSql.bind(db, 'ALTER TABLE apps ADD CONSTRAINT apps_task_constraint FOREIGN KEY(taskId) REFERENCES tasks(id)')
], callback);
};
exports.down = function(db, callback) {
async.series([
db.runSql.bind(db, 'ALTER TABLE app DROP FOREIGN KEY apps_task_constraint'),
db.runSql.bind(db, 'ALTER TABLE apps DROP COLUMN taskId'),
], callback);
};
@@ -1,12 +0,0 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps DROP updateConfigJson, DROP restoreConfigJson, DROP oldConfigJson', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
callback();
};
@@ -1,15 +0,0 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps CHANGE installationProgress errorJson TEXT', [], function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps CHANGE errorJson installationProgress TEXT', [], function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,17 +0,0 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE users ADD COLUMN source VARCHAR(128) DEFAULT ""', function (error) {
if (error) return callback(error);
callback();
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE users DROP COLUMN source', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,26 +0,0 @@
'use strict';
let async = require('async');
exports.up = function(db, callback) {
db.runSql('ALTER TABLE tasks CHANGE errorMessage errorJson TEXT', [], function (error) {
if (error) console.error(error);
// convert error messages into json
db.all('SELECT id, errorJson FROM apps', function (error, apps) {
async.eachSeries(apps, function (app, iteratorDone) {
if (app.errorJson === 'null') return iteratorDone();
if (app.errorJson === null) return iteratorDone();
db.runSql('UPDATE apps SET errorJson = ? WHERE id = ?', [ JSON.stringify({ message: app.errorJson }, app.id)], iteratorDone);
}, callback);
});
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE tasks CHANGE errorJson errorMessage TEXT', [], function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,21 +0,0 @@
'use strict';
var async = require('async');
// imports mailbox entries for existing users
exports.up = function(db, callback) {
db.all('SELECT * FROM mailboxes', function (error, mailboxes) {
async.eachSeries(mailboxes, function (mailbox, iteratorDone) {
if (!mailbox.membersJson) return iteratorDone();
let members = JSON.parse(mailbox.membersJson);
members = members.map((m) => m.indexOf('@') === -1 ? `${m}@${mailbox.domain}` : m); // only because we don't do things in a xction
db.runSql('UPDATE mailboxes SET memberJson=? WHERE name=? AND domain=?', [ JSON.stringify(members), mailbox.name, mailbox.domain ], iteratorDone);
}, callback);
});
};
exports.down = function(db, callback) {
callback();
};
@@ -1,19 +0,0 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('UPDATE apps SET runState=? WHERE runState IS NULL', [ 'running' ], function (error) {
if (error) return callback(error);
db.runSql('ALTER TABLE apps MODIFY runState VARCHAR(512) NOT NULL', [], function (error) {
if (error) console.error(error);
callback(error);
});
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE app MODIFY runState VARCHAR(512)', [], function (error) {
if (error) console.error(error);
callback(error);
});
};
+15 -11
View File
@@ -27,7 +27,6 @@ CREATE TABLE IF NOT EXISTS users(
twoFactorAuthenticationSecret VARCHAR(128) DEFAULT "",
twoFactorAuthenticationEnabled BOOLEAN DEFAULT false,
admin BOOLEAN DEFAULT false,
source VARCHAR(128) DEFAULT "",
PRIMARY KEY(id));
@@ -64,8 +63,9 @@ CREATE TABLE IF NOT EXISTS clients(
CREATE TABLE IF NOT EXISTS apps(
id VARCHAR(128) NOT NULL UNIQUE,
appStoreId VARCHAR(128) NOT NULL,
installationState VARCHAR(512) NOT NULL, // the active task on the app
runState VARCHAR(512) NOT NULL, // if the app is stopped
installationState VARCHAR(512) NOT NULL,
installationProgress TEXT,
runState VARCHAR(512),
health VARCHAR(128),
healthTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, // when the app last responded
containerId VARCHAR(128),
@@ -87,11 +87,15 @@ CREATE TABLE IF NOT EXISTS apps(
mailboxName VARCHAR(128), // mailbox of this app. default allocated as '.app'
label VARCHAR(128), // display name
tagsJson VARCHAR(2048), // array of tags
dataDir VARCHAR(256) UNIQUE,
taskId INTEGER, // current task
errorJson TEXT,
FOREIGN KEY(taskId) REFERENCES tasks(id),
// the following fields do not belong here, they can be removed when we use a queue for apptask
restoreConfigJson VARCHAR(256), // used to pass backupId to restore from to apptask
oldConfigJson TEXT, // used to pass old config to apptask (configure, restore)
updateConfigJson TEXT, // used to pass new config to apptask (update)
ownerId VARCHAR(128),
FOREIGN KEY(ownerId) REFERENCES users(id),
PRIMARY KEY(id));
CREATE TABLE IF NOT EXISTS appPortBindings(
@@ -189,7 +193,7 @@ CREATE TABLE IF NOT EXISTS mailboxes(
type VARCHAR(16) NOT NULL, /* 'mailbox', 'alias', 'list' */
ownerId VARCHAR(128) NOT NULL, /* user id */
aliasTarget VARCHAR(128), /* the target name type is an alias */
membersJson TEXT, /* members of a group. fully qualified */
membersJson TEXT, /* members of a group */
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
domain VARCHAR(128),
@@ -200,7 +204,7 @@ CREATE TABLE IF NOT EXISTS subdomains(
appId VARCHAR(128) NOT NULL,
domain VARCHAR(128) NOT NULL,
subdomain VARCHAR(128) NOT NULL,
type VARCHAR(128) NOT NULL, /* primary or redirect */
type VARCHAR(128) NOT NULL,
FOREIGN KEY(domain) REFERENCES domains(domain),
FOREIGN KEY(appId) REFERENCES apps(id),
@@ -211,8 +215,8 @@ CREATE TABLE IF NOT EXISTS tasks(
type VARCHAR(32) NOT NULL,
percent INTEGER DEFAULT 0,
message TEXT,
errorJson TEXT,
resultJson TEXT,
errorMessage TEXT,
result TEXT,
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id));
+318 -601
View File
File diff suppressed because it is too large Load Diff
+20 -22
View File
@@ -14,40 +14,40 @@
"node": ">=4.0.0 <=4.1.1"
},
"dependencies": {
"@google-cloud/dns": "^1.1.0",
"@google-cloud/dns": "^0.9.2",
"@google-cloud/storage": "^2.5.0",
"@sindresorhus/df": "git+https://github.com/cloudron-io/df.git#type",
"@sindresorhus/df": "^3.1.0",
"async": "^2.6.2",
"aws-sdk": "^2.476.0",
"body-parser": "^1.19.0",
"aws-sdk": "^2.441.0",
"body-parser": "^1.18.3",
"cloudron-manifestformat": "^2.15.0",
"connect": "^3.7.0",
"connect": "^3.6.6",
"connect-ensure-login": "^0.1.1",
"connect-lastmile": "^1.2.1",
"connect-lastmile": "^1.0.2",
"connect-timeout": "^1.9.0",
"cookie-parser": "^1.4.4",
"cookie-session": "^1.3.3",
"cron": "^1.7.1",
"csurf": "^1.10.0",
"db-migrate": "^0.11.6",
"cron": "^1.7.0",
"csurf": "^1.9.0",
"db-migrate": "^0.11.5",
"db-migrate-mysql": "^1.1.10",
"debug": "^4.1.1",
"dockerode": "^2.5.8",
"ejs": "^2.6.1",
"ejs-cli": "^2.0.1",
"express": "^4.17.1",
"express-session": "^1.16.2",
"express": "^4.16.4",
"express-session": "^1.16.1",
"js-yaml": "^3.13.1",
"json": "^9.0.6",
"ldapjs": "^1.0.2",
"lodash": "^4.17.11",
"lodash.chunk": "^4.2.0",
"mime": "^2.4.4",
"mime": "^2.4.2",
"moment-timezone": "^0.5.25",
"morgan": "^1.9.1",
"multiparty": "^4.2.1",
"mysql": "^2.17.1",
"nodemailer": "^6.2.1",
"nodemailer": "^6.1.1",
"nodemailer-smtp-transport": "^2.7.4",
"oauth2orize": "^1.11.0",
"once": "^1.4.0",
@@ -57,30 +57,28 @@
"passport-http-bearer": "^1.0.1",
"passport-local": "^1.0.0",
"passport-oauth2-client-password": "^0.1.2",
"pretty-bytes": "^5.3.0",
"progress-stream": "^2.0.0",
"proxy-middleware": "^0.15.0",
"qrcode": "^1.3.3",
"readdirp": "^3.0.2",
"readdirp": "^3.0.0",
"request": "^2.88.0",
"rimraf": "^2.6.3",
"s3-block-read-stream": "^0.5.0",
"safetydance": "^0.7.1",
"semver": "^6.1.1",
"session-file-store": "^1.3.1",
"semver": "^6.0.0",
"showdown": "^1.9.0",
"speakeasy": "^2.0.0",
"split": "^1.0.1",
"superagent": "^5.0.9",
"superagent": "^5.0.2",
"supererror": "^0.7.2",
"tar-fs": "github:cloudron-io/tar-fs#ignore_stat_error",
"tar-stream": "^2.1.0",
"tar-stream": "^2.0.1",
"tldjs": "^2.3.1",
"underscore": "^1.9.1",
"uuid": "^3.3.2",
"valid-url": "^1.0.9",
"validator": "^11.0.0",
"ws": "^7.0.0",
"validator": "^10.11.0",
"ws": "^6.2.1",
"xml2js": "^0.4.19"
},
"devDependencies": {
@@ -90,7 +88,7 @@
"mocha": "^6.1.4",
"mock-aws-s3": "git+https://github.com/cloudron-io/mock-aws-s3.git",
"nock": "^10.0.6",
"node-sass": "^4.12.0",
"node-sass": "^4.11.0",
"recursive-readdir": "^2.2.2"
},
"scripts": {
+4 -18
View File
@@ -92,9 +92,8 @@ fi
echo "Running cloudron-setup with args : $@" > "${LOG_FILE}"
# validate arguments in the absence of data
readonly AVAILABLE_PROVIDERS="azure, caas, cloudscale, contabo, digitalocean, ec2, exoscale, galaxygate, gce, hetzner, interox, lightsail, linode, netcup, ovh, rosehosting, scaleway, skysilk, time4vps, upcloud, vultr or generic"
if [[ -z "${provider}" ]]; then
echo "--provider is required ($AVAILABLE_PROVIDERS)"
echo "--provider is required (azure, contabo, digitalocean, ec2, exoscale, galaxygate, gce, hetzner, lightsail, linode, netcup, ovh, rosehosting, scaleway, upcloud, vultr or generic)"
exit 1
elif [[ \
"${provider}" != "ami" && \
@@ -107,10 +106,9 @@ elif [[ \
"${provider}" != "ec2" && \
"${provider}" != "exoscale" && \
"${provider}" != "galaxygate" && \
"${provider}" != "digitalocean" && \
"${provider}" != "gce" && \
"${provider}" != "hetzner" && \
"${provider}" != "interox" && \
"${provider}" != "interox-image" && \
"${provider}" != "lightsail" && \
"${provider}" != "linode" && \
"${provider}" != "linode-stackscript" && \
@@ -119,16 +117,12 @@ elif [[ \
"${provider}" != "ovh" && \
"${provider}" != "rosehosting" && \
"${provider}" != "scaleway" && \
"${provider}" != "skysilk" && \
"${provider}" != "skysilk-image" && \
"${provider}" != "time4vps" && \
"${provider}" != "time4vps-image" && \
"${provider}" != "upcloud" && \
"${provider}" != "upcloud-image" && \
"${provider}" != "vultr" && \
"${provider}" != "generic" \
]]; then
echo "--provider must be one of: $AVAILABLE_PROVIDERS"
echo "--provider must be one of: azure, cloudscale.ch, contabo, digitalocean, ec2, exoscale, galaxygate, gce, hetzner, lightsail, linode, netcup, ovh, rosehosting, scaleway, upcloud, vultr or generic"
exit 1
fi
@@ -162,7 +156,7 @@ if [[ "${initBaseImage}" == "true" ]]; then
exit 1
fi
if ! DEBIAN_FRONTEND=noninteractive apt-get -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" -y install curl python3 ubuntu-standard -y &>> "${LOG_FILE}"; then
if ! apt-get install curl python3 ubuntu-standard -y &>> "${LOG_FILE}"; then
echo "Could not install setup dependencies (curl). See ${LOG_FILE}"
exit 1
fi
@@ -205,10 +199,6 @@ fi
# NOTE: this install script only supports 3.x and above
echo "=> Installing version ${version} (this takes some time) ..."
mkdir -p /etc/cloudron
# this file is used >= 4.2
echo "${provider}" > /etc/cloudron/PROVIDER
# this file is unused <= 4.2 and exists to make legacy installations work. the start script will remove this file anyway
cat > "/etc/cloudron/cloudron.conf" <<CONF_END
{
"apiServerOrigin": "${apiServerOrigin}",
@@ -224,10 +214,6 @@ if ! /bin/bash "${box_src_tmp_dir}/scripts/installer.sh" &>> "${LOG_FILE}"; then
exit 1
fi
# only needed for >= 4.2
mysql -uroot -ppassword -e "REPLACE INTO box.settings (name, value) VALUES ('api_server_origin', '${apiServerOrigin}');" 2>/dev/null
mysql -uroot -ppassword -e "REPLACE INTO box.settings (name, value) VALUES ('web_server_origin', '${webServerOrigin}');" 2>/dev/null
echo -n "=> Waiting for cloudron to be ready (this takes some time) ..."
while true; do
echo -n "."
+5 -13
View File
@@ -13,7 +13,6 @@ HELP_MESSAGE="
This script collects diagnostic information to help debug server related issues
Options:
--admin-login Login as administrator
--enable-ssh Enable SSH access for the Cloudron support team
--help Show this message
"
@@ -26,20 +25,13 @@ fi
enableSSH="false"
args=$(getopt -o "" -l "help,enable-ssh,admin-login" -n "$0" -- "$@")
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;;
--admin-login)
admin_username=$(mysql -NB -uroot -ppassword -e "SELECT username FROM box.users WHERE admin=1 LIMIT 1" 2>/dev/null)
admin_password=$(pwgen -1s 12)
printf '{"%s":"%s"}\n' "${admin_username}" "${admin_password}" > /tmp/cloudron_ghost.json
echo "Login as ${admin_username} / ${admin_password} . Remove /tmp/cloudron_ghost.json when done."
exit 0
;;
--) break;;
*) echo "Unknown option $1"; exit 1;;
esac
@@ -52,7 +44,7 @@ if [[ "`df --output="avail" / | sed -n 2p`" -lt "10240" ]]; then
echo ""
df -h
echo ""
echo "To recover from a full disk, follow the guide at https://cloudron.io/documentation/troubleshooting/#recovery-after-disk-full"
echo "To recover from a full disk, follow the guide at https://cloudron.io/documentation/server/#recovery-after-disk-full"
exit 1
fi
@@ -68,8 +60,8 @@ echo -n "Generating Cloudron Support stats..."
# clear file
rm -rf $OUT
echo -e $LINE"PROVIDER"$LINE >> $OUT
cat /etc/cloudron/PROVIDER &>> $OUT || true
echo -e $LINE"cloudron.conf"$LINE >> $OUT
cat /etc/cloudron/cloudron.conf &>> $OUT
echo -e $LINE"Docker container"$LINE >> $OUT
if ! timeout --kill-after 10s 15s docker ps -a &>> $OUT 2>&1; then
@@ -107,7 +99,7 @@ if [[ "${enableSSH}" == "true" ]]; then
permit_root_login=$(grep -q ^PermitRootLogin.*yes /etc/ssh/sshd_config && echo "yes" || echo "no")
# support.js uses similar logic
if $(grep -q "ec2\|lightsail\|ami" /etc/cloudron/PROVIDER); then
if $(grep -q "ec2\|lightsail\|ami" /etc/cloudron/cloudron.conf); then
ssh_user="ubuntu"
keys_file="/home/ubuntu/.ssh/authorized_keys"
else
+3 -3
View File
@@ -108,6 +108,8 @@ systemctl restart unbound
# ensure cloudron-syslog runs
systemctl restart cloudron-syslog
$json -f /etc/cloudron/cloudron.conf -I -e "delete this.edition" # can be removed after 4.0
echo "==> Configuring sudoers"
rm -f /etc/sudoers.d/${USER}
cp "${script_dir}/start/sudoers" /etc/sudoers.d/${USER}
@@ -122,8 +124,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
rm -f "${PLATFORM_DATA_DIR}/logrotate.d/"*
cp "${script_dir}/start/logrotate/"* "${PLATFORM_DATA_DIR}/logrotate.d/"
rm -f "${PLATFORM_DATA_DIR}/logrotate.d/box-logrotate" "${PLATFORM_DATA_DIR}/logrotate.d/app-logrotate" # remove pre 3.6 config files
# logrotate files have to be owned by root, this is here to fixup existing installations where we were resetting the owner to yellowtent
chown root:root "${PLATFORM_DATA_DIR}/logrotate.d/"
@@ -171,8 +173,6 @@ mysql -u root -p${mysql_root_password} -e 'CREATE DATABASE IF NOT EXISTS box'
echo "==> Migrating data"
(cd "${BOX_SRC_DIR}" && BOX_ENV=cloudron DATABASE_URL=mysql://root:${mysql_root_password}@127.0.0.1/box "${BOX_SRC_DIR}/node_modules/.bin/db-migrate" up)
rm -f /etc/cloudron/cloudron.conf
if [[ ! -f "${BOX_DATA_DIR}/dhparams.pem" ]]; then
echo "==> Generating dhparams (takes forever)"
openssl dhparam -out "${BOX_DATA_DIR}/dhparams.pem" 2048
+2 -17
View File
@@ -240,23 +240,8 @@ LoadPlugin write_graphite
Interactive false
Import "df"
Import "du"
<Module du>
<Path>
Instance maildata
Dir "/home/yellowtent/boxdata/mail"
</Path>
<Path>
Instance boxdata
Dir "/home/yellowtent/boxdata"
Exclude "mail"
</Path>
<Path>
Instance platformdata
Dir "/home/yellowtent/platformdata"
</Path>
</Module>
# <Module df>
# </Module>
</Plugin>
<Plugin write_graphite>
-1
View File
@@ -21,7 +21,6 @@ def read():
except:
continue
# type comes from https://github.com/collectd/collectd/blob/master/src/types.db
val = collectd.Values(type='df_complex', plugin='df', plugin_instance=instance)
free = st.f_bavail * st.f_frsize # bavail is for non-root user. bfree is total
-79
View File
@@ -1,79 +0,0 @@
import collectd,os,subprocess,sys,re,time
# https://www.programcreek.com/python/example/106897/collectd.register_read
PATHS = [] # { name, dir, exclude }
INTERVAL = 60 * 60 * 12 # twice a day. change values in docker-graphite if you change this
def du(pathinfo):
cmd = 'timeout 1800 du -Dsb "{}"'.format(pathinfo['dir'])
if pathinfo['exclude'] != '':
cmd += ' --exclude "{}"'.format(pathinfo['exclude'])
collectd.info('computing size with command: %s' % cmd);
try:
size = subprocess.check_output(cmd, shell=True).split()[0].decode('utf-8')
collectd.info('\tsize of %s is %s (time: %i)' % (pathinfo['dir'], size, int(time.time())))
return size
except Exception as e:
collectd.info('\terror getting the size of %s: %s' % (pathinfo['dir'], str(e)))
return 0
def parseSize(size):
units = {"B": 1, "KB": 10**3, "MB": 10**6, "GB": 10**9, "TB": 10**12}
number, unit, _ = re.split('([a-zA-Z]+)', size.upper())
return int(float(number)*units[unit])
def dockerSize():
# use --format '{{json .}}' to dump the string. '{{if eq .Type "Images"}}{{.Size}}{{end}}' still creates newlines
cmd = 'timeout 1800 docker system df --format "{{.Size}}" | head -n1'
try:
size = subprocess.check_output(cmd, shell=True).strip().decode('utf-8')
collectd.info('size of docker images is %s (%s) (time: %i)' % (size, parseSize(size), int(time.time())))
return parseSize(size)
except Exception as e:
collectd.info('error getting docker images size : %s' % str(e))
return 0
# configure is called for each module block. this is called before init
def configure(config):
global PATHS
for child in config.children:
if child.key != 'Path':
collectd.info('du plugin: Unknown config key "%s"' % key)
continue
pathinfo = { 'name': '', 'dir': '', 'exclude': '' }
for node in child.children:
if node.key == 'Instance':
pathinfo['name'] = node.values[0]
elif node.key == 'Dir':
pathinfo['dir'] = node.values[0]
elif node.key == 'Exclude':
pathinfo['exclude'] = node.values[0]
PATHS.append(pathinfo);
collectd.info('du plugin: monitoring %s' % pathinfo['dir']);
def init():
global PATHS
collectd.info('custom du plugin initialized with %s %s' % (PATHS, sys.version))
def read():
for pathinfo in PATHS:
size = du(pathinfo)
# type comes from https://github.com/collectd/collectd/blob/master/src/types.db
val = collectd.Values(type='capacity', plugin='du', plugin_instance=pathinfo['name'])
val.dispatch(values=[size], type_instance='usage')
size = dockerSize()
val = collectd.Values(type='capacity', plugin='du', plugin_instance='docker')
val.dispatch(values=[size], type_instance='usage')
collectd.register_init(init)
collectd.register_config(configure)
collectd.register_read(read, INTERVAL)
+18
View File
@@ -0,0 +1,18 @@
# logrotate config for app, crash, addon and task logs
# man 7 glob
/home/yellowtent/platformdata/logs/[!t][!a][!s][!k][!s]/*.log {
# only keep one rotated file, we currently do not send that over the api
rotate 1
size 10M
# we never compress so we can simply tail the files
nocompress
copytruncate
}
/home/yellowtent/platformdata/logs/tasks/*.log {
monthly
rotate 0
missingok
}
+1 -2
View File
@@ -1,8 +1,7 @@
# logrotate config for box logs
# keep upto 5 logs of size 10M each
/home/yellowtent/platformdata/logs/box.log {
rotate 5
rotate 10
size 10M
# we never compress so we can simply tail the files
nocompress
-31
View File
@@ -1,31 +0,0 @@
# logrotate config for app, crash, addon and task logs
# man 7 glob
/home/yellowtent/platformdata/logs/graphite/*.log
/home/yellowtent/platformdata/logs/mail/*.log
/home/yellowtent/platformdata/logs/mysql/*.log
/home/yellowtent/platformdata/logs/mongodb/*.log
/home/yellowtent/platformdata/logs/postgresql/*.log
/home/yellowtent/platformdata/logs/sftp/*.log
/home/yellowtent/platformdata/logs/redis-*/*.log
/home/yellowtent/platformdata/logs/crash/*.log
/home/yellowtent/platformdata/logs/updater/*.log {
# only keep one rotated file, we currently do not send that over the api
rotate 1
size 10M
missingok
# we never compress so we can simply tail the files
nocompress
copytruncate
}
# keep task logs for a week. the 'nocreate' option ensures empty log files are not
# created post rotation
/home/yellowtent/platformdata/logs/tasks/*.log {
minage 7
daily
rotate 0
missingok
nocreate
}
+1 -2
View File
@@ -27,6 +27,7 @@ exports = module.exports = {
};
var assert = require('assert'),
config = require('./config.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:accesscontrol'),
tokendb = require('./tokendb.js'),
@@ -129,8 +130,6 @@ function validateToken(accessToken, callback) {
if (error && error.reason === UsersError.NOT_FOUND) return callback(null, null /* user */, 'Invalid Token'); // will end up as a 401
if (error) return callback(error);
if (!user.active) return callback(null, null /* user */, 'Invalid Token'); // will end up as a 401
scopesForUser(user, function (error, userScopes) {
if (error) return callback(error);
+76 -108
View File
@@ -36,17 +36,16 @@ var accesscontrol = require('./accesscontrol.js'),
apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
BoxError = require('./boxerror.js'),
clients = require('./clients.js'),
constants = require('./constants.js'),
config = require('./config.js'),
ClientsError = clients.ClientsError,
crypto = require('crypto'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:addons'),
docker = require('./docker.js'),
dockerConnection = docker.connection,
DockerError = docker.DockerError,
fs = require('fs'),
graphs = require('./graphs.js'),
hat = require('./hat.js'),
infra = require('./infra_version.js'),
mail = require('./mail.js'),
@@ -58,7 +57,6 @@ var accesscontrol = require('./accesscontrol.js'),
safe = require('safetydance'),
semver = require('semver'),
settings = require('./settings.js'),
sftp = require('./sftp.js'),
shell = require('./shell.js'),
spawn = require('child_process').spawn,
split = require('split'),
@@ -271,10 +269,6 @@ function restartContainer(serviceName, callback) {
if (error) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error));
docker.startContainer(serviceName, function (error) {
if (error && error.reason === BoxError.NOT_FOUND) {
callback(null); // callback early since rebuilding takes long
return rebuildService(serviceName, function (error) { if (error) console.error(`Unable to rebuild service ${serviceName}`, error); });
}
if (error) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error));
callback(null);
@@ -282,31 +276,13 @@ function restartContainer(serviceName, callback) {
});
}
function rebuildService(serviceName, callback) {
assert.strictEqual(typeof serviceName, 'string');
assert.strictEqual(typeof callback, 'function');
assert(KNOWN_SERVICES[serviceName], `Unknown service ${serviceName}`);
// this attempts to recreate the service docker container if they don't exist but platform infra version is unchanged
// passing an infra version of 'none' will not attempt to purge existing data, not sure if this is good or bad
if (serviceName === 'mongodb') return startMongodb({ version: 'none' }, callback);
if (serviceName === 'postgresql') return startPostgresql({ version: 'none' }, callback);
if (serviceName === 'mysql') return startMysql({ version: 'none' }, callback);
if (serviceName === 'sftp') return sftp.startSftp({ version: 'none' }, callback);
if (serviceName === 'graphite') return graphs.startGraphite({ version: 'none' }, callback);
// nothing to rebuild for now
callback();
}
function getServiceDetails(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 === BoxError.NOT_FOUND) return callback(new AddonsError(AddonsError.NOT_ACTIVE, error));
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));
const ip = safe.query(result, 'NetworkSettings.Networks.cloudron.IPAddress', null);
@@ -388,7 +364,7 @@ function getService(serviceName, callback) {
}
KNOWN_SERVICES[serviceName].status(function (error, result) {
if (error) return callback(error);
if (error) return callback(new AddonsError(AddonsError.INTERNAL_ERROR, error));
tmp.status = result.status;
tmp.memoryUsed = result.memoryUsed;
@@ -644,9 +620,7 @@ function importDatabase(addon, callback) {
if (!error) return iteratorCallback();
debug(`importDatabase: Error importing ${addon} of app ${app.id}. Marking as errored`, error);
// FIXME: there is no way to 'repair' if we are here. we need to make a separate apptask that re-imports db
// not clear, if repair workflow should be part of addon or per-app
appdb.update(app.id, { installationState: apps.ISTATE_ERROR, error: { message: error.message } }, iteratorCallback);
appdb.update(app.id, { installationState: appdb.ISTATE_ERROR, installationProgress: error.message }, iteratorCallback);
});
}, callback);
});
@@ -711,7 +685,7 @@ function getEnvironment(app, callback) {
appdb.getAddonConfigByAppId(app.id, function (error, result) {
if (error) return callback(error);
if (app.manifest.addons['docker']) result.push({ name: 'DOCKER_HOST', value: `tcp://172.18.0.1:${constants.DOCKER_PROXY_PORT}` });
if (app.manifest.addons['docker']) result.push({ name: 'DOCKER_HOST', value: `tcp://172.18.0.1:${config.get('dockerProxyPort')}` });
return callback(null, result.map(function (e) { return e.name + '=' + e.value; }));
});
@@ -826,7 +800,7 @@ function setupOauth(app, options, callback) {
var env = [
{ name: `${envPrefix}OAUTH_CLIENT_ID`, value: result.id },
{ name: `${envPrefix}OAUTH_CLIENT_SECRET`, value: result.clientSecret },
{ name: `${envPrefix}OAUTH_ORIGIN`, value: settings.adminOrigin() }
{ name: `${envPrefix}OAUTH_ORIGIN`, value: config.adminOrigin() }
];
debugApp(app, 'Setting oauth addon config to %j', env);
@@ -902,8 +876,8 @@ function setupLdap(app, options, callback) {
var env = [
{ name: `${envPrefix}LDAP_SERVER`, value: '172.18.0.1' },
{ name: `${envPrefix}LDAP_PORT`, value: '' + constants.LDAP_PORT },
{ name: `${envPrefix}LDAP_URL`, value: 'ldap://172.18.0.1:' + constants.LDAP_PORT },
{ name: `${envPrefix}LDAP_PORT`, value: '' + config.get('ldapPort') },
{ name: `${envPrefix}LDAP_URL`, value: 'ldap://172.18.0.1:' + config.get('ldapPort') },
{ name: `${envPrefix}LDAP_USERS_BASE_DN`, value: 'ou=users,dc=cloudron' },
{ name: `${envPrefix}LDAP_GROUPS_BASE_DN`, value: 'ou=groups,dc=cloudron' },
{ name: `${envPrefix}LDAP_BIND_DN`, value: 'cn='+ app.id + ',ou=apps,dc=cloudron' },
@@ -932,7 +906,7 @@ function setupSendMail(app, options, callback) {
debugApp(app, 'Setting up SendMail');
appdb.getAddonConfigByName(app.id, 'sendmail', '%MAIL_SMTP_PASSWORD', function (error, existingPassword) {
appdb.getAddonConfigByName(app.id, 'sendmail', 'MAIL_SMTP_PASSWORD', function (error, existingPassword) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
var password = error ? hat(4 * 48) : existingPassword; // see box#565 for password length
@@ -970,7 +944,7 @@ function setupRecvMail(app, options, callback) {
debugApp(app, 'Setting up recvmail');
appdb.getAddonConfigByName(app.id, 'recvmail', '%MAIL_IMAP_PASSWORD', function (error, existingPassword) {
appdb.getAddonConfigByName(app.id, 'recvmail', 'MAIL_IMAP_PASSWORD', function (error, existingPassword) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
var password = error ? hat(4 * 48) : existingPassword; // see box#565 for password length
@@ -1066,7 +1040,7 @@ function setupMySql(app, options, callback) {
debugApp(app, 'Setting up mysql');
appdb.getAddonConfigByName(app.id, 'mysql', '%MYSQL_PASSWORD', function (error, existingPassword) {
appdb.getAddonConfigByName(app.id, 'mysql', 'MYSQL_PASSWORD', function (error, existingPassword) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
const tmp = mysqlDatabaseName(app.id);
@@ -1282,7 +1256,7 @@ function setupPostgreSql(app, options, callback) {
const { database, username } = postgreSqlNames(app.id);
appdb.getAddonConfigByName(app.id, 'postgresql', '%POSTGRESQL_PASSWORD', function (error, existingPassword) {
appdb.getAddonConfigByName(app.id, 'postgresql', 'POSTGRESQL_PASSWORD', function (error, existingPassword) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
const data = {
@@ -1457,14 +1431,13 @@ function setupMongoDb(app, options, callback) {
debugApp(app, 'Setting up mongodb');
appdb.getAddonConfigByName(app.id, 'mongodb', '%MONGODB_PASSWORD', function (error, existingPassword) {
appdb.getAddonConfigByName(app.id, 'mongodb', 'MONGODB_PASSWORD', function (error, existingPassword) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
const data = {
database: app.id,
username: app.id,
password: error ? hat(4 * 128) : existingPassword,
oplog: !!options.oplog
password: error ? hat(4 * 128) : existingPassword
};
getServiceDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
@@ -1477,7 +1450,7 @@ function setupMongoDb(app, options, callback) {
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
var env = [
{ name: `${envPrefix}MONGODB_URL`, value : `mongodb://${data.username}:${data.password}@mongodb:27017/${data.database}` },
{ name: `${envPrefix}MONGODB_URL`, value : `mongodb://${data.username}:${data.password}@mongodb/${data.database}` },
{ name: `${envPrefix}MONGODB_USERNAME`, value : data.username },
{ name: `${envPrefix}MONGODB_PASSWORD`, value: data.password },
{ name: `${envPrefix}MONGODB_HOST`, value : 'mongodb' },
@@ -1485,10 +1458,6 @@ function setupMongoDb(app, options, callback) {
{ name: `${envPrefix}MONGODB_DATABASE`, value : data.database }
];
if (options.oplog) {
env.push({ name: `${envPrefix}MONGODB_OPLOG_URL`, value : `mongodb://${data.username}:${data.password}@mongodb:27017/local?authSource=${data.database}` });
}
debugApp(app, 'Setting mongodb addon config to %j', env);
appdb.setAddonConfig(app.id, 'mongodb', env, callback);
});
@@ -1595,69 +1564,68 @@ function setupRedis(app, options, callback) {
const redisName = 'redis-' + app.id;
appdb.getAddonConfigByName(app.id, 'redis', '%REDIS_PASSWORD', function (error, existingPassword) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
const redisPassword = error ? hat(4 * 48) : existingPassword; // see box#362 for password length
const redisServiceToken = hat(4 * 48);
// Compute redis memory limit based on app's memory limit (this is arbitrary)
var memoryLimit = app.memoryLimit || app.manifest.memoryLimit || 0;
if (memoryLimit === -1) { // unrestricted (debug mode)
memoryLimit = 0;
} else if (memoryLimit === 0 || memoryLimit <= (2 * 1024 * 1024 * 1024)) { // less than 2G (ram+swap)
memoryLimit = 150 * 1024 * 1024; // 150m
} else {
memoryLimit = 600 * 1024 * 1024; // 600m
docker.inspect(redisName, function (error, result) {
if (!error) {
debug(`Re-using existing redis container with state: ${result.State}`);
return callback();
}
const tag = infra.images.redis.tag;
const label = app.fqdn;
// note that we do not add appId label because this interferes with the stop/start app logic
const cmd = `docker run --restart=always -d --name=${redisName} \
--hostname ${redisName} \
--label=location=${label} \
--net cloudron \
--net-alias ${redisName} \
--log-driver syslog \
--log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \
--log-opt tag="${redisName}" \
-m ${memoryLimit/2} \
--memory-swap ${memoryLimit} \
--dns 172.18.0.1 \
--dns-search=. \
-e CLOUDRON_REDIS_PASSWORD="${redisPassword}" \
-e CLOUDRON_REDIS_TOKEN="${redisServiceToken}" \
-v "${paths.PLATFORM_DATA_DIR}/redis/${app.id}:/var/lib/redis" \
--label isCloudronManaged=true \
--read-only -v /tmp -v /run ${tag}`;
appdb.getAddonConfigByName(app.id, 'redis', 'REDIS_PASSWORD', function (error, existingPassword) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
const redisPassword = error ? hat(4 * 48) : existingPassword; // see box#362 for password length
const redisServiceToken = hat(4 * 48);
var env = [
{ name: `${envPrefix}REDIS_URL`, value: 'redis://redisuser:' + redisPassword + '@redis-' + app.id },
{ name: `${envPrefix}REDIS_PASSWORD`, value: redisPassword },
{ name: `${envPrefix}REDIS_HOST`, value: redisName },
{ name: `${envPrefix}REDIS_PORT`, value: '6379' }
];
// Compute redis memory limit based on app's memory limit (this is arbitrary)
var memoryLimit = app.memoryLimit || app.manifest.memoryLimit || 0;
async.series([
(next) => {
docker.inspect(redisName, function (inspectError, result) {
if (!inspectError) {
debug(`Re-using existing redis container with state: ${JSON.stringify(result.State)}`);
return next();
}
shell.exec('startRedis', cmd, next);
});
},
appdb.setAddonConfig.bind(null, app.id, 'redis', env),
waitForService.bind(null, 'redis-' + app.id, 'CLOUDRON_REDIS_TOKEN')
], function (error) {
if (error) debug('Error setting up redis: ', error);
callback(error);
if (memoryLimit === -1) { // unrestricted (debug mode)
memoryLimit = 0;
} else if (memoryLimit === 0 || memoryLimit <= (2 * 1024 * 1024 * 1024)) { // less than 2G (ram+swap)
memoryLimit = 150 * 1024 * 1024; // 150m
} else {
memoryLimit = 600 * 1024 * 1024; // 600m
}
const tag = infra.images.redis.tag;
const label = app.fqdn;
// note that we do not add appId label because this interferes with the stop/start app logic
const cmd = `docker run --restart=always -d --name=${redisName} \
--hostname ${redisName} \
--label=location=${label} \
--net cloudron \
--net-alias ${redisName} \
--log-driver syslog \
--log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \
--log-opt tag="${redisName}" \
-m ${memoryLimit/2} \
--memory-swap ${memoryLimit} \
--dns 172.18.0.1 \
--dns-search=. \
-e CLOUDRON_REDIS_PASSWORD="${redisPassword}" \
-e CLOUDRON_REDIS_TOKEN="${redisServiceToken}" \
-v "${paths.PLATFORM_DATA_DIR}/redis/${app.id}:/var/lib/redis" \
--label isCloudronManaged=true \
--read-only -v /tmp -v /run ${tag}`;
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
var env = [
{ name: `${envPrefix}REDIS_URL`, value: 'redis://redisuser:' + redisPassword + '@redis-' + app.id },
{ name: `${envPrefix}REDIS_PASSWORD`, value: redisPassword },
{ name: `${envPrefix}REDIS_HOST`, value: redisName },
{ name: `${envPrefix}REDIS_PORT`, value: '6379' }
];
async.series([
shell.exec.bind(null, 'startRedis', cmd),
appdb.setAddonConfig.bind(null, app.id, 'redis', env),
waitForService.bind(null, 'redis-' + app.id, 'CLOUDRON_REDIS_TOKEN')
], function (error) {
if (error) debug('Error setting up redis: ', error);
callback(error);
});
});
});
}
@@ -1805,7 +1773,7 @@ function statusSftp(callback) {
assert.strictEqual(typeof callback, 'function');
docker.inspect('sftp', function (error, container) {
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, { status: exports.SERVICE_STATUS_STOPPED });
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));
docker.memoryUsage('sftp', function (error, result) {
@@ -1826,10 +1794,10 @@ function statusGraphite(callback) {
assert.strictEqual(typeof callback, 'function');
docker.inspect('graphite', function (error, container) {
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, { status: exports.SERVICE_STATUS_STOPPED });
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));
request.get('http://127.0.0.1:8417/graphite-web/dashboard', { timeout: 3000 }, function (error, response) {
request.get('http://127.0.0.1:8417', { timeout: 3000 }, function (error, response) {
if (error) return callback(null, { status: exports.SERVICE_STATUS_STARTING, error: `Error waiting for graphite: ${error.message}` });
if (response.statusCode !== 200) return callback(null, { status: exports.SERVICE_STATUS_STARTING, error: `Error waiting for graphite. Status code: ${response.statusCode} message: ${response.body.message}` });
+3 -3
View File
@@ -96,7 +96,7 @@ server {
<% if ( endpoint === 'admin' ) { -%>
# CSP headers for the admin/dashboard resources
add_header Content-Security-Policy "default-src 'none'; frame-src 'self' cloudron.io *.cloudron.io; connect-src wss: https: 'self' *.cloudron.io; script-src https: 'self' 'unsafe-inline' 'unsafe-eval'; img-src * data:; style-src https: 'unsafe-inline'; object-src 'none'; font-src https: 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self';";
add_header Content-Security-Policy "default-src 'none'; connect-src wss: https: 'self' *.cloudron.io; script-src https: 'self' 'unsafe-inline' 'unsafe-eval'; img-src * data:; style-src https: 'unsafe-inline'; object-src 'none'; font-src https: 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self';";
<% } -%>
proxy_http_version 1.1;
@@ -163,9 +163,9 @@ server {
client_max_body_size 0;
}
# graphite paths (uncomment block below and visit /graphite-web/dashboard)
# 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-web/ {
# location ~ ^/(graphite|content|metrics|dashboard|render|browser|composer)/ {
# proxy_pass http://127.0.0.1:8417;
# client_max_body_size 1m;
# }
+130 -32
View File
@@ -21,9 +21,36 @@ exports = module.exports = {
getAppIdByAddonConfigValue: getAppIdByAddonConfigValue,
setHealth: setHealth,
setTask: setTask,
setInstallationCommand: setInstallationCommand,
setRunCommand: setRunCommand,
getAppStoreIds: getAppStoreIds,
setOwner: setOwner,
transferOwnership: transferOwnership,
// installation codes (keep in sync in UI)
ISTATE_PENDING_INSTALL: 'pending_install', // installs and fresh reinstalls
ISTATE_PENDING_CLONE: 'pending_clone', // clone
ISTATE_PENDING_CONFIGURE: 'pending_configure', // config (location, port) changes and on infra update
ISTATE_PENDING_UNINSTALL: 'pending_uninstall', // uninstallation
ISTATE_PENDING_RESTORE: 'pending_restore', // restore to previous backup or on upgrade
ISTATE_PENDING_UPDATE: 'pending_update', // update from installed state preserving data
ISTATE_PENDING_FORCE_UPDATE: 'pending_force_update', // update from any state preserving data
ISTATE_PENDING_BACKUP: 'pending_backup', // backup the app
ISTATE_ERROR: 'error', // error executing last pending_* command
ISTATE_INSTALLED: 'installed', // app is installed
RSTATE_RUNNING: 'running',
RSTATE_PENDING_START: 'pending_start',
RSTATE_PENDING_STOP: 'pending_stop',
RSTATE_STOPPED: 'stopped', // app stopped by us
// run codes (keep in sync in UI)
HEALTH_HEALTHY: 'healthy',
HEALTH_UNHEALTHY: 'unhealthy',
HEALTH_ERROR: 'error',
HEALTH_DEAD: 'dead',
// subdomain table types
SUBDOMAIN_TYPE_PRIMARY: 'primary',
SUBDOMAIN_TYPE_REDIRECT: 'redirect',
@@ -38,12 +65,12 @@ var assert = require('assert'),
safe = require('safetydance'),
util = require('util');
var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationState', 'apps.errorJson', 'apps.runState',
var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationState', 'apps.installationProgress', 'apps.runState',
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.httpPort', 'subdomains.subdomain AS location', 'subdomains.domain',
'apps.accessRestrictionJson', 'apps.memoryLimit',
'apps.label', 'apps.tagsJson', 'apps.taskId',
'apps.accessRestrictionJson', 'apps.restoreConfigJson', 'apps.oldConfigJson', 'apps.updateConfigJson', 'apps.memoryLimit',
'apps.label', 'apps.tagsJson',
'apps.sso', 'apps.debugModeJson', 'apps.robotsTxt', 'apps.enableBackup',
'apps.creationTime', 'apps.updateTime', 'apps.mailboxName', 'apps.enableAutomaticUpdate',
'apps.creationTime', 'apps.updateTime', 'apps.ownerId', 'apps.mailboxName', 'apps.enableAutomaticUpdate',
'apps.dataDir', 'apps.ts', 'apps.healthTime' ].join(',');
var PORT_BINDINGS_FIELDS = [ 'hostPort', 'type', 'environmentVariable', 'appId' ].join(',');
@@ -57,6 +84,18 @@ function postProcess(result) {
result.manifest = safe.JSON.parse(result.manifestJson);
delete result.manifestJson;
assert(result.oldConfigJson === null || typeof result.oldConfigJson === 'string');
result.oldConfig = safe.JSON.parse(result.oldConfigJson);
delete result.oldConfigJson;
assert(result.updateConfigJson === null || typeof result.updateConfigJson === 'string');
result.updateConfig = safe.JSON.parse(result.updateConfigJson);
delete result.updateConfigJson;
assert(result.restoreConfigJson === null || typeof result.restoreConfigJson === 'string');
result.restoreConfig = safe.JSON.parse(result.restoreConfigJson);
delete result.restoreConfigJson;
assert(result.tagsJson === null || typeof result.tagsJson === 'string');
result.tags = safe.JSON.parse(result.tagsJson) || [];
delete result.tagsJson;
@@ -104,10 +143,8 @@ function postProcess(result) {
if (envNames[i]) result.env[envNames[i]] = envValues[i];
}
result.error = safe.JSON.parse(result.errorJson);
delete result.errorJson;
result.taskId = result.taskId ? String(result.taskId) : null;
// in the db, we store dataDir as unique/nullable
result.dataDir = result.dataDir || '';
}
function get(id, callback) {
@@ -220,13 +257,14 @@ function getAll(callback) {
});
}
function add(id, appStoreId, manifest, location, domain, portBindings, data, callback) {
function add(id, appStoreId, manifest, location, domain, ownerId, portBindings, data, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof appStoreId, 'string');
assert(manifest && typeof manifest === 'object');
assert.strictEqual(typeof manifest.version, 'string');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof ownerId, 'string');
assert.strictEqual(typeof portBindings, 'object');
assert(data && typeof data === 'object');
assert.strictEqual(typeof callback, 'function');
@@ -238,8 +276,8 @@ function add(id, appStoreId, manifest, location, domain, portBindings, data, cal
const accessRestriction = data.accessRestriction || null;
const accessRestrictionJson = JSON.stringify(accessRestriction);
const memoryLimit = data.memoryLimit || 0;
const installationState = data.installationState;
const runState = data.runState;
const installationState = data.installationState || exports.ISTATE_PENDING_INSTALL;
const restoreConfigJson = data.restoreConfig ? JSON.stringify(data.restoreConfig) : null; // used when cloning
const sso = 'sso' in data ? data.sso : null;
const robotsTxt = 'robotsTxt' in data ? data.robotsTxt : null;
const debugModeJson = data.debugMode ? JSON.stringify(data.debugMode) : null;
@@ -251,11 +289,11 @@ function add(id, appStoreId, manifest, location, domain, portBindings, data, cal
var queries = [];
queries.push({
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, runState, accessRestrictionJson, memoryLimit, '
+ 'sso, debugModeJson, robotsTxt, mailboxName, label, tagsJson) '
+ ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, manifestJson, installationState, runState, accessRestrictionJson, memoryLimit,
sso, debugModeJson, robotsTxt, mailboxName, label, tagsJson ]
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, accessRestrictionJson, memoryLimit, '
+ 'restoreConfigJson, sso, debugModeJson, robotsTxt, ownerId, mailboxName, label, tagsJson) '
+ ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, manifestJson, installationState, accessRestrictionJson, memoryLimit, restoreConfigJson,
sso, debugModeJson, robotsTxt, ownerId, mailboxName, label, tagsJson ]
});
queries.push({
@@ -420,7 +458,7 @@ function updateWithConstraints(id, app, constraints, callback) {
var fields = [ ], values = [ ];
for (var p in app) {
if (p === 'manifest' || p === 'tags' || p === 'accessRestriction' || p === 'debugMode' || p === 'error') {
if (p === 'manifest' || p === 'oldConfig' || p === 'updateConfig' || p === 'restoreConfig' || p === 'tags' || p === 'accessRestriction' || p === 'debugMode') {
fields.push(`${p}Json = ?`);
values.push(JSON.stringify(app[p]));
} else if (p !== 'portBindings' && p !== 'location' && p !== 'domain' && p !== 'alternateDomains' && p !== 'env') {
@@ -443,6 +481,7 @@ function updateWithConstraints(id, app, constraints, callback) {
});
}
// not sure if health should influence runState
function setHealth(appId, health, healthTime, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof health, 'string');
@@ -451,22 +490,53 @@ function setHealth(appId, health, healthTime, callback) {
var values = { health, healthTime };
updateWithConstraints(appId, values, '', callback);
var constraints = 'AND runState NOT LIKE "pending_%" AND installationState = "installed"';
updateWithConstraints(appId, values, constraints, callback);
}
function setTask(appId, values, options, callback) {
function setInstallationCommand(appId, installationState, values, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof values, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof installationState, 'string');
if (typeof values === 'function') {
callback = values;
values = { };
} else {
assert.strictEqual(typeof values, 'object');
assert.strictEqual(typeof callback, 'function');
}
values.installationState = installationState;
values.installationProgress = '';
// Rules are:
// uninstall is allowed in any state
// force update is allowed in any state including pending_uninstall! (for better or worse)
// restore is allowed from installed or error state or currently restoring
// configure is allowed in installed state or currently configuring or in error state
// update and backup are allowed only in installed state
if (installationState === exports.ISTATE_PENDING_UNINSTALL || installationState === exports.ISTATE_PENDING_FORCE_UPDATE) {
updateWithConstraints(appId, values, '', callback);
} else if (installationState === exports.ISTATE_PENDING_RESTORE) {
updateWithConstraints(appId, values, 'AND (installationState = "installed" OR installationState = "error" OR installationState = "pending_restore")', callback);
} else if (installationState === exports.ISTATE_PENDING_UPDATE || installationState === exports.ISTATE_PENDING_BACKUP) {
updateWithConstraints(appId, values, 'AND installationState = "installed"', callback);
} else if (installationState === exports.ISTATE_PENDING_CONFIGURE) {
updateWithConstraints(appId, values, 'AND (installationState = "installed" OR installationState = "pending_configure" OR installationState = "error")', callback);
} else {
callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, 'invalid installationState'));
}
}
function setRunCommand(appId, runState, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof runState, 'string');
assert.strictEqual(typeof callback, 'function');
if (!options.requireNullTaskId) return updateWithConstraints(appId, values, '', callback);
if (options.requiredState === null) {
updateWithConstraints(appId, values, 'AND taskId IS NULL', callback);
} else {
updateWithConstraints(appId, values, `AND taskId IS NULL AND installationState = "${options.requiredState}"`, callback);
}
var values = { runState: runState };
updateWithConstraints(appId, values, 'AND runState NOT LIKE "pending_%" AND installationState = "installed"', callback);
}
function getAppStoreIds(callback) {
@@ -565,16 +635,44 @@ function getAppIdByAddonConfigValue(addonId, namePattern, value, callback) {
});
}
function getAddonConfigByName(appId, addonId, namePattern, callback) {
function getAddonConfigByName(appId, addonId, name, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof addonId, 'string');
assert.strictEqual(typeof namePattern, 'string');
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT value FROM appAddonConfigs WHERE appId = ? AND addonId = ? AND name LIKE ?', [ appId, addonId, namePattern ], function (error, results) {
database.query('SELECT value FROM appAddonConfigs WHERE appId = ? AND addonId = ? AND name = ?', [ appId, addonId, name ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
callback(null, results[0].value);
});
}
function setOwner(appId, ownerId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof ownerId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('UPDATE apps SET ownerId=? WHERE appId=?', [ ownerId, appId ], function (error, results) {
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND, 'No such user'));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND, 'No such app'));
callback(null);
});
}
function transferOwnership(oldOwnerId, newOwnerId, callback) {
assert.strictEqual(typeof oldOwnerId, 'string');
assert.strictEqual(typeof newOwnerId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('UPDATE apps SET ownerId=? WHERE ownerId=?', [ newOwnerId, oldOwnerId ], function (error) {
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND, 'No such user'));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null);
});
}
+12 -12
View File
@@ -36,16 +36,16 @@ function setHealth(app, health, callback) {
let now = new Date(), healthTime = app.healthTime, curHealth = app.health;
if (health === apps.HEALTH_HEALTHY) {
if (health === appdb.HEALTH_HEALTHY) {
healthTime = now;
if (curHealth && curHealth !== apps.HEALTH_HEALTHY) { // app starts out with null health
if (curHealth && curHealth !== appdb.HEALTH_HEALTHY) { // app starts out with null health
debugApp(app, 'app switched from %s to healthy', curHealth);
// do not send mails for dev apps
if (!app.debugMode) eventlog.add(eventlog.ACTION_APP_UP, auditSource.HEALTH_MONITOR, { app: app });
}
} else if (Math.abs(now - healthTime) > UNHEALTHY_THRESHOLD) {
if (curHealth === apps.HEALTH_HEALTHY) {
if (curHealth === appdb.HEALTH_HEALTHY) {
debugApp(app, 'marking as unhealthy since not seen for more than %s minutes', UNHEALTHY_THRESHOLD/(60 * 1000));
// do not send mails for dev apps
@@ -72,7 +72,7 @@ function checkAppHealth(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
if (app.installationState !== apps.ISTATE_INSTALLED || app.runState !== apps.RSTATE_RUNNING) {
if (app.installationState !== appdb.ISTATE_INSTALLED || app.runState !== appdb.RSTATE_RUNNING) {
debugApp(app, 'skipped. istate:%s rstate:%s', app.installationState, app.runState);
return callback(null);
}
@@ -82,34 +82,34 @@ function checkAppHealth(app, callback) {
docker.inspect(app.containerId, function (error, data) {
if (error || !data || !data.State) {
debugApp(app, 'Error inspecting container');
return setHealth(app, apps.HEALTH_ERROR, callback);
return setHealth(app, appdb.HEALTH_ERROR, callback);
}
if (data.State.Running !== true) {
debugApp(app, 'exited');
return setHealth(app, apps.HEALTH_DEAD, callback);
return setHealth(app, appdb.HEALTH_DEAD, callback);
}
// non-appstore apps may not have healthCheckPath
if (!manifest.healthCheckPath) return setHealth(app, apps.HEALTH_HEALTHY, callback);
if (!manifest.healthCheckPath) return setHealth(app, appdb.HEALTH_HEALTHY, callback);
// poll through docker network instead of nginx to bypass any potential oauth proxy
var healthCheckUrl = 'http://127.0.0.1:' + app.httpPort + manifest.healthCheckPath;
superagent
.get(healthCheckUrl)
.set('Host', app.fqdn) // required for some apache configs with rewrite rules
.set('User-Agent', 'Mozilla (CloudronHealth)') // required for some apps (e.g. minio)
.set('User-Agent', 'Mozilla') // required for some apps (e.g. minio)
.redirects(0)
.timeout(HEALTHCHECK_INTERVAL)
.end(function (error, res) {
if (error && !error.response) {
debugApp(app, 'not alive (network error): %s', error.message);
setHealth(app, apps.HEALTH_UNHEALTHY, callback);
setHealth(app, appdb.HEALTH_UNHEALTHY, callback);
} else if (res.statusCode >= 400) { // 2xx and 3xx are ok
debugApp(app, 'not alive : %s', error || res.status);
setHealth(app, apps.HEALTH_UNHEALTHY, callback);
setHealth(app, appdb.HEALTH_UNHEALTHY, callback);
} else {
setHealth(app, apps.HEALTH_HEALTHY, callback);
setHealth(app, appdb.HEALTH_HEALTHY, callback);
}
});
});
@@ -187,7 +187,7 @@ function processApp(callback) {
if (error) console.error(error);
var alive = result
.filter(function (a) { return a.installationState === apps.ISTATE_INSTALLED && a.runState === apps.RSTATE_RUNNING && a.health === apps.HEALTH_HEALTHY; })
.filter(function (a) { return a.installationState === appdb.ISTATE_INSTALLED && a.runState === appdb.RSTATE_RUNNING && a.health === appdb.HEALTH_HEALTHY; })
.map(function (a) { return (a.location || 'naked_domain') + '|' + a.manifest.id; }).join(', ');
debug('apps alive: [%s]', alive);
+306 -775
View File
File diff suppressed because it is too large Load Diff
+22 -41
View File
@@ -27,20 +27,17 @@ exports = module.exports = {
var apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
constants = require('./constants.js'),
config = require('./config.js'),
custom = require('./custom.js'),
debug = require('debug')('box:appstore'),
domains = require('./domains.js'),
eventlog = require('./eventlog.js'),
groups = require('./groups.js'),
mail = require('./mail.js'),
os = require('os'),
safe = require('safetydance'),
semver = require('semver'),
settings = require('./settings.js'),
superagent = require('superagent'),
sysinfo = require('./sysinfo.js'),
users = require('./users.js'),
util = require('util');
function AppstoreError(reason, errorOrMessage) {
@@ -98,7 +95,7 @@ function login(email, password, totpToken, callback) {
totpToken: totpToken
};
const url = settings.apiServerOrigin() + '/api/v1/login';
const url = config.apiServerOrigin() + '/api/v1/login';
superagent.post(url).send(data).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
if (result.statusCode === 401) return callback(new AppstoreError(AppstoreError.ACCESS_DENIED));
@@ -118,7 +115,7 @@ function registerUser(email, password, callback) {
password: password,
};
const url = settings.apiServerOrigin() + '/api/v1/register_user';
const url = config.apiServerOrigin() + '/api/v1/register_user';
superagent.post(url).send(data).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
if (result.statusCode === 409) return callback(new AppstoreError(AppstoreError.ALREADY_EXISTS));
@@ -134,7 +131,7 @@ function getSubscription(callback) {
getCloudronToken(function (error, token) {
if (error) return callback(error);
const url = settings.apiServerOrigin() + '/api/v1/subscription';
const url = config.apiServerOrigin() + '/api/v1/subscription';
superagent.get(url).query({ accessToken: token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
if (result.statusCode === 401) return callback(new AppstoreError(AppstoreError.INVALID_TOKEN));
@@ -161,7 +158,7 @@ function purchaseApp(data, callback) {
getCloudronToken(function (error, token) {
if (error) return callback(error);
const url = `${settings.apiServerOrigin()}/api/v1/cloudronapps`;
const url = `${config.apiServerOrigin()}/api/v1/cloudronapps`;
superagent.post(url).send(data).query({ accessToken: token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
@@ -186,7 +183,7 @@ function unpurchaseApp(appId, data, callback) {
getCloudronToken(function (error, token) {
if (error) return callback(error);
const url = `${settings.apiServerOrigin()}/api/v1/cloudronapps/${appId}`;
const url = `${config.apiServerOrigin()}/api/v1/cloudronapps/${appId}`;
superagent.get(url).query({ accessToken: token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
@@ -209,7 +206,7 @@ function unpurchaseApp(appId, data, callback) {
function sendAliveStatus(callback) {
callback = callback || NOOP_CALLBACK;
let allSettings, allDomains, mailDomains, loginEvents, userCount, groupCount;
var allSettings, allDomains, mailDomains, loginEvents;
async.series([
function (callback) {
@@ -239,20 +236,6 @@ function sendAliveStatus(callback) {
loginEvents = result;
callback();
});
},
function (callback) {
users.count(function (error, result) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
userCount = result;
callback();
});
},
function (callback) {
groups.count(function (error, result) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
groupCount = result;
callback();
});
}
], function (error) {
if (error) return callback(error);
@@ -272,17 +255,15 @@ function sendAliveStatus(callback) {
catchAllCount: mailDomains.filter(function (d) { return d.catchAll.length !== 0; }).length,
relayProviders: Array.from(new Set(mailDomains.map(function (d) { return d.relay.provider; })))
},
userCount: userCount,
groupCount: groupCount,
appAutoupdatePattern: allSettings[settings.APP_AUTOUPDATE_PATTERN_KEY],
boxAutoupdatePattern: allSettings[settings.BOX_AUTOUPDATE_PATTERN_KEY],
timeZone: allSettings[settings.TIME_ZONE_KEY],
};
var data = {
version: constants.VERSION,
adminFqdn: settings.adminFqdn(),
provider: sysinfo.provider(),
version: config.version(),
adminFqdn: config.adminFqdn(),
provider: config.provider(),
backendSettings: backendSettings,
machine: {
cpus: os.cpus(),
@@ -296,7 +277,7 @@ function sendAliveStatus(callback) {
getCloudronToken(function (error, token) {
if (error) return callback(error);
const url = `${settings.apiServerOrigin()}/api/v1/alive`;
const url = `${config.apiServerOrigin()}/api/v1/alive`;
superagent.post(url).send(data).query({ accessToken: token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
if (result.statusCode === 404) return callback(new AppstoreError(AppstoreError.NOT_FOUND));
@@ -316,9 +297,9 @@ function getBoxUpdate(callback) {
getCloudronToken(function (error, token) {
if (error) return callback(error);
const url = `${settings.apiServerOrigin()}/api/v1/boxupdate`;
const url = `${config.apiServerOrigin()}/api/v1/boxupdate`;
superagent.get(url).query({ accessToken: token, boxVersion: constants.VERSION }).timeout(10 * 1000).end(function (error, result) {
superagent.get(url).query({ accessToken: token, boxVersion: config.version() }).timeout(10 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
if (result.statusCode === 401) return callback(new AppstoreError(AppstoreError.INVALID_TOKEN));
if (result.statusCode === 422) return callback(new AppstoreError(AppstoreError.LICENSE_ERROR, result.body.message));
@@ -327,7 +308,7 @@ function getBoxUpdate(callback) {
var updateInfo = result.body;
if (!semver.valid(updateInfo.version) || semver.gt(constants.VERSION, updateInfo.version)) {
if (!semver.valid(updateInfo.version) || semver.gt(config.version(), updateInfo.version)) {
return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Invalid update version: %s %s', result.statusCode, result.text)));
}
@@ -351,9 +332,9 @@ function getAppUpdate(app, callback) {
getCloudronToken(function (error, token) {
if (error) return callback(error);
const url = `${settings.apiServerOrigin()}/api/v1/appupdate`;
const url = `${config.apiServerOrigin()}/api/v1/appupdate`;
superagent.get(url).query({ accessToken: token, boxVersion: constants.VERSION, appId: app.appStoreId, appVersion: app.manifest.version }).timeout(10 * 1000).end(function (error, result) {
superagent.get(url).query({ accessToken: token, boxVersion: config.version(), appId: app.appStoreId, appVersion: app.manifest.version }).timeout(10 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
if (result.statusCode === 401) return callback(new AppstoreError(AppstoreError.INVALID_TOKEN));
if (result.statusCode === 422) return callback(new AppstoreError(AppstoreError.LICENSE_ERROR, result.body.message));
@@ -381,7 +362,7 @@ function registerCloudron(data, callback) {
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof callback, 'function');
const url = `${settings.apiServerOrigin()}/api/v1/register_cloudron`;
const url = `${config.apiServerOrigin()}/api/v1/register_cloudron`;
superagent.post(url).send(data).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
@@ -437,7 +418,7 @@ function registerWithLoginCredentials(options, callback) {
login(options.email, options.password, options.totpToken || '', function (error, result) {
if (error) return callback(error);
registerCloudron({ domain: settings.adminDomain(), accessToken: result.accessToken }, callback);
registerCloudron({ domain: config.adminDomain(), accessToken: result.accessToken }, callback);
});
});
});
@@ -464,7 +445,7 @@ function createTicket(info, callback) {
if (error) console.error('Unable to get app info', error);
if (result) info.app = result;
let url = settings.apiServerOrigin() + '/api/v1/ticket';
let url = config.apiServerOrigin() + '/api/v1/ticket';
info.supportEmail = custom.spec().support.email; // destination address for tickets
@@ -488,8 +469,8 @@ function getApps(callback) {
settings.getUnstableAppsConfig(function (error, unstable) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
const url = `${settings.apiServerOrigin()}/api/v1/apps`;
superagent.get(url).query({ accessToken: token, boxVersion: constants.VERSION, unstable: unstable }).timeout(10 * 1000).end(function (error, result) {
const url = `${config.apiServerOrigin()}/api/v1/apps`;
superagent.get(url).query({ accessToken: token, boxVersion: config.version(), unstable: unstable }).timeout(10 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
if (result.statusCode === 403 || result.statusCode === 401) return callback(new AppstoreError(AppstoreError.INVALID_TOKEN));
if (result.statusCode === 422) return callback(new AppstoreError(AppstoreError.LICENSE_ERROR, result.body.message));
@@ -510,7 +491,7 @@ function getAppVersion(appId, version, callback) {
getCloudronToken(function (error, token) {
if (error) return callback(error);
let url = `${settings.apiServerOrigin()}/api/v1/apps/${appId}`;
let url = `${config.apiServerOrigin()}/api/v1/apps/${appId}`;
if (version !== 'latest') url += `/versions/${version}`;
superagent.get(url).query({ accessToken: token }).timeout(10 * 1000).end(function (error, result) {
+419 -537
View File
File diff suppressed because it is too large Load Diff
-86
View File
@@ -1,86 +0,0 @@
'use strict';
exports = module.exports = {
scheduleTask: scheduleTask
};
let assert = require('assert'),
debug = require('debug')('box:apptaskmanager'),
fs = require('fs'),
locker = require('./locker.js'),
safe = require('safetydance'),
path = require('path'),
paths = require('./paths.js'),
tasks = require('./tasks.js');
let gActiveTasks = { }; // indexed by app id
let gPendingTasks = [ ];
let gInitialized = false;
const TASK_CONCURRENCY = 3;
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
function waitText(lockOperation) {
if (lockOperation === locker.OP_BOX_UPDATE) return 'Waiting for Cloudron to finish updating. See the Settings view';
if (lockOperation === locker.OP_PLATFORM_START) return 'Waiting for Cloudron to initialize';
if (lockOperation === locker.OP_FULL_BACKUP) return 'Wait for Cloudron to finish backup. See the Backups view';
return ''; // cannot happen
}
function initializeSync() {
gInitialized = true;
locker.on('unlocked', startNextTask);
}
// callback is called when task is finished
function scheduleTask(appId, taskId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof taskId, 'string');
assert.strictEqual(typeof callback, 'function');
if (!gInitialized) initializeSync();
if (appId in gActiveTasks) {
return callback(new Error(`Task for %s is already active: ${appId}`));
}
if (Object.keys(gActiveTasks).length >= TASK_CONCURRENCY) {
debug(`Reached concurrency limit, queueing task id ${taskId}`);
tasks.update(taskId, { percent: 0, message: 'Waiting for other app tasks to complete' }, NOOP_CALLBACK);
gPendingTasks.push({ appId, taskId, callback });
return;
}
var lockError = locker.recursiveLock(locker.OP_APPTASK);
if (lockError) {
debug(`Could not get lock. ${lockError.message}, queueing task id ${taskId}`);
tasks.update(taskId, { percent: 0, message: waitText(lockError.operation) }, NOOP_CALLBACK);
gPendingTasks.push({ appId, taskId, callback });
return;
}
gActiveTasks[appId] = {};
const logFile = path.join(paths.LOG_DIR, appId, 'apptask.log');
if (!fs.existsSync(path.dirname(logFile))) safe.fs.mkdirSync(path.dirname(logFile)); // ensure directory
tasks.startTask(taskId, { logFile }, function (error, result) {
callback(error, result);
delete gActiveTasks[appId];
locker.unlock(locker.OP_APPTASK); // unlock event will trigger next task
});
}
function startNextTask() {
if (gPendingTasks.length === 0) return;
assert(Object.keys(gActiveTasks).length < TASK_CONCURRENCY);
const t = gPendingTasks.shift();
scheduleTask(t.appId, t.taskId, t.callback);
}
+2 -1
View File
@@ -3,8 +3,9 @@
exports = module.exports = {
CRON: { userId: null, username: 'cron' },
HEALTH_MONITOR: { userId: null, username: 'healthmonitor' },
SYSADMIN: { userId: null, username: 'sysadmin' },
TASK_MANAGER: { userId: null, username: 'taskmanager' },
APP_TASK: { userId: null, username: 'apptask' },
EXTERNAL_LDAP_TASK: { userId: null, username: 'externalldap' },
fromRequest: fromRequest
};
+51 -93
View File
@@ -40,18 +40,18 @@ exports = module.exports = {
};
var addons = require('./addons.js'),
appdb = require('./appdb.js'),
apps = require('./apps.js'),
AppsError = require('./apps.js').AppsError,
async = require('async'),
assert = require('assert'),
backupdb = require('./backupdb.js'),
constants = require('./constants.js'),
config = require('./config.js'),
crypto = require('crypto'),
database = require('./database.js'),
DatabaseError = require('./databaseerror.js'),
DataLayout = require('./datalayout.js'),
debug = require('debug')('box:backups'),
df = require('@sindresorhus/df'),
eventlog = require('./eventlog.js'),
fs = require('fs'),
locker = require('./locker.js'),
@@ -60,7 +60,6 @@ var addons = require('./addons.js'),
path = require('path'),
paths = require('./paths.js'),
progressStream = require('progress-stream'),
prettyBytes = require('pretty-bytes'),
safe = require('safetydance'),
shell = require('./shell.js'),
settings = require('./settings.js'),
@@ -113,7 +112,6 @@ function api(provider) {
case 's3-v4-compat': return require('./storage/s3.js');
case 'digitalocean-spaces': return require('./storage/s3.js');
case 'exoscale-sos': return require('./storage/s3.js');
case 'wasabi': return require('./storage/s3.js');
case 'scaleway-objectstorage': return require('./storage/s3.js');
case 'noop': return require('./storage/noop.js');
default: return null;
@@ -293,9 +291,6 @@ function tarPack(dataLayout, key, callback) {
},
map: function(header) {
header.name = dataLayout.toRemotePath(header.name);
// the tar pax format allows us to encode filenames > 100 and size > 8GB (see #640)
// https://www.systutorials.com/docs/linux/man/5-star/
if (header.size > 8589934590 || header.name > 99) header.pax = { size: header.size };
return header;
},
strict: false // do not error for unknown types (skip fifo, char/block devices)
@@ -413,34 +408,6 @@ function saveFsMetadata(dataLayout, metadataFile, callback) {
callback();
}
// the du call in the function below requires root
function checkFreeDiskSpace(backupConfig, dataLayout, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout');
assert.strictEqual(typeof callback, 'function');
if (backupConfig.provider !== 'filesystem') return callback();
let used = 0;
for (let localPath of dataLayout.localPaths()) {
debug(`checkFreeDiskSpace: getting disk usage of ${localPath}`);
let result = safe.child_process.execSync(`du -Dsb ${localPath}`, { encoding: 'utf8' });
if (!result) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, safe.error));
used += parseInt(result, 10);
}
debug(`checkFreeDiskSpace: ${used} bytes`);
df.file(backupConfig.backupFolder).then(function (diskUsage) {
const needed = used + (1024 * 1024 * 1024); // check if there is atleast 1GB left afterwards
if (diskUsage.available <= needed) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, `Not enough disk space for backup. Needed: ${prettyBytes(needed)} Available: ${prettyBytes(diskUsage.available)}`));
callback(null);
}).catch(function (error) {
callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
});
}
// this function is called via backupupload (since it needs root to traverse app's directory)
function upload(backupId, format, dataLayoutString, progressCallback, callback) {
assert.strictEqual(typeof backupId, 'string');
@@ -456,33 +423,29 @@ function upload(backupId, format, dataLayoutString, progressCallback, callback)
settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
checkFreeDiskSpace(backupConfig, dataLayout, function (error) {
if (error) return callback(error);
if (format === 'tgz') {
async.retry({ times: 5, interval: 20000 }, function (retryCallback) {
retryCallback = once(retryCallback); // protect again upload() erroring much later after tar stream error
if (format === 'tgz') {
async.retry({ times: 5, interval: 20000 }, function (retryCallback) {
retryCallback = once(retryCallback); // protect again upload() erroring much later after tar stream error
tarPack(dataLayout, backupConfig.key || null, function (error, tarStream) {
if (error) return retryCallback(error);
tarPack(dataLayout, backupConfig.key || null, function (error, tarStream) {
if (error) return retryCallback(error);
tarStream.on('progress', function(progress) {
const transferred = Math.round(progress.transferred/1024/1024), speed = Math.round(progress.speed/1024/1024);
if (!transferred && !speed) return progressCallback({ message: 'Uploading backup' }); // 0M@0Mbps looks wrong
progressCallback({ message: `Uploading backup ${transferred}M@${speed}Mbps` });
});
tarStream.on('error', retryCallback); // already returns BackupsError
api(backupConfig.provider).upload(backupConfig, getBackupFilePath(backupConfig, backupId, format), tarStream, retryCallback);
tarStream.on('progress', function(progress) {
const transferred = Math.round(progress.transferred/1024/1024), speed = Math.round(progress.speed/1024/1024);
if (!transferred && !speed) return progressCallback({ message: 'Uploading backup' }); // 0M@0Mbps looks wrong
progressCallback({ message: `Uploading backup ${transferred}M@${speed}Mbps` });
});
}, callback);
} else {
async.series([
saveFsMetadata.bind(null, dataLayout, `${dataLayout.localRoot()}/fsmetadata.json`),
sync.bind(null, backupConfig, backupId, dataLayout, progressCallback)
], callback);
}
});
tarStream.on('error', retryCallback); // already returns BackupsError
api(backupConfig.provider).upload(backupConfig, getBackupFilePath(backupConfig, backupId, format), tarStream, retryCallback);
});
}, callback);
} else {
async.series([
saveFsMetadata.bind(null, dataLayout, `${dataLayout.localRoot()}/fsmetadata.json`),
sync.bind(null, backupConfig, backupId, dataLayout, progressCallback)
], callback);
}
});
}
@@ -675,7 +638,7 @@ function restore(backupConfig, backupId, progressCallback, callback) {
debug('restore: database imported');
settings.initCache(callback);
callback();
});
});
}
@@ -726,7 +689,7 @@ function runBackupUpload(backupId, format, dataLayout, progressCallback, callbac
callback();
}).on('message', function (message) {
if (!message.result) return progressCallback(message);
debug(`runBackupUpload: result - ${JSON.stringify(message)}`);
debug(`runBackupUpload: result - ${message}`);
result = message.result;
});
}
@@ -801,12 +764,12 @@ function rotateBoxBackup(backupConfig, tag, appBackupIds, progressCallback, call
if (!snapshotInfo) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, 'Snapshot info missing or corrupt'));
const snapshotTime = snapshotInfo.timestamp.replace(/[T.]/g, '-').replace(/[:Z]/g,''); // add this to filename to make it unique, so it's easy to download them
const backupId = util.format('%s/box_%s_v%s', tag, snapshotTime, constants.VERSION);
const backupId = util.format('%s/box_%s_v%s', tag, snapshotTime, config.version());
const format = backupConfig.format;
debug(`Rotating box backup to id ${backupId}`);
backupdb.add(backupId, { version: constants.VERSION, type: backupdb.BACKUP_TYPE_BOX, dependsOn: appBackupIds, manifest: null, format: format }, function (error) {
backupdb.add(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));
@@ -846,10 +809,10 @@ function backupBoxWithAppBackupIds(appBackupIds, tag, progressCallback, callback
function canBackupApp(app) {
// only backup apps that are installed or pending configure or called from apptask. Rest of them are in some
// state not good for consistent backup (i.e addons may not have been setup completely)
return (app.installationState === apps.ISTATE_INSTALLED && app.health === apps.HEALTH_HEALTHY) ||
app.installationState === apps.ISTATE_PENDING_CONFIGURE ||
app.installationState === apps.ISTATE_PENDING_BACKUP || // called from apptask
app.installationState === apps.ISTATE_PENDING_UPDATE; // called from apptask
return (app.installationState === appdb.ISTATE_INSTALLED && app.health === appdb.HEALTH_HEALTHY) ||
app.installationState === appdb.ISTATE_PENDING_CONFIGURE ||
app.installationState === appdb.ISTATE_PENDING_BACKUP || // called from apptask
app.installationState === appdb.ISTATE_PENDING_UPDATE; // called from apptask
}
function snapshotApp(app, progressCallback, callback) {
@@ -859,7 +822,7 @@ function snapshotApp(app, progressCallback, callback) {
progressCallback({ message: `Snapshotting app ${app.fqdn}` });
if (!safe.fs.writeFileSync(path.join(paths.APPS_DATA_DIR, app.id + '/config.json'), JSON.stringify(app))) {
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));
}
@@ -1019,21 +982,19 @@ function startBackupTask(auditSource, callback) {
let error = locker.lock(locker.OP_FULL_BACKUP);
if (error) return callback(new BackupsError(BackupsError.BAD_STATE, `Cannot backup now: ${error.message}`));
tasks.add(tasks.TASK_BACKUP, [ ], function (error, taskId) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
let task = tasks.startTask(tasks.TASK_BACKUP, []);
task.on('error', (error) => callback(new BackupsError(BackupsError.INTERNAL_ERROR, error)));
task.on('start', (taskId) => {
eventlog.add(eventlog.ACTION_BACKUP_START, auditSource, { taskId });
tasks.startTask(taskId, {}, function (error, result) {
locker.unlock(locker.OP_FULL_BACKUP);
const errorMessage = error ? error.message : '';
eventlog.add(eventlog.ACTION_BACKUP_FINISH, auditSource, { taskId: taskId, errorMessage: errorMessage, backupId: result });
});
callback(null, taskId);
});
task.on('finish', (error, result) => {
locker.unlock(locker.OP_FULL_BACKUP);
const errorMessage = error ? error.message : '';
eventlog.add(eventlog.ACTION_BACKUP_FINISH, auditSource, { taskId: task.id, errorMessage: errorMessage, backupId: result });
});
}
function ensureBackup(auditSource, callback) {
@@ -1257,22 +1218,19 @@ function cleanup(auditSource, progressCallback, callback) {
}
function startCleanupTask(auditSource, callback) {
tasks.add(tasks.TASK_CLEAN_BACKUPS, [ auditSource ], function (error, taskId) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
let task = tasks.startTask(tasks.TASK_CLEAN_BACKUPS, [ auditSource ]);
task.on('error', (error) => callback(new BackupsError(BackupsError.INTERNAL_ERROR, error)));
task.on('start', (taskId) => {
eventlog.add(eventlog.ACTION_BACKUP_CLEANUP_START, auditSource, { taskId });
tasks.startTask(taskId, {}, (error, result) => { // result is { removedBoxBackups, removedAppBackups }
eventlog.add(eventlog.ACTION_BACKUP_CLEANUP_FINISH, auditSource, {
errorMessage: error ? error.message : null,
removedBoxBackups: result ? result.removedBoxBackups : [],
removedAppBackups: result ? result.removedAppBackups : []
});
});
callback(null, taskId);
});
task.on('finish', (error, result) => { // result is { removedBoxBackups, removedAppBackups }
eventlog.add(eventlog.ACTION_BACKUP_CLEANUP_FINISH, auditSource, {
errorMessage: error ? error.message : null,
removedBoxBackups: result ? result.removedBoxBackups : [],
removedAppBackups: result ? result.removedAppBackups : []
});
});
}
function checkConfiguration(callback) {
-54
View File
@@ -1,54 +0,0 @@
/* jslint node:true */
'use strict';
const assert = require('assert'),
util = require('util'),
_ = require('underscore');
exports = module.exports = BoxError;
function BoxError(reason, errorOrMessage, details) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
assert(typeof details === 'object' || typeof details === 'undefined');
Error.call(this);
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.reason = reason;
this.details = details || {};
if (typeof errorOrMessage === 'undefined') {
this.message = reason;
} else if (typeof errorOrMessage === 'string') {
this.message = errorOrMessage;
} else { // error object
this.message = errorOrMessage.message;
this.nestedError = errorOrMessage;
_.extend(this.details, errorOrMessage); // copy enumerable properies
}
}
util.inherits(BoxError, Error);
BoxError.ACCESS_DENIED = 'Access Denied';
BoxError.ALREADY_EXISTS = 'Already Exists';
BoxError.BAD_FIELD = 'Bad Field';
BoxError.COLLECTD_ERROR = 'Collectd Error';
BoxError.CONFLICT = 'Conflict';
BoxError.DATABASE_ERROR = 'Database Error';
BoxError.DNS_ERROR = 'DNS Error';
BoxError.DOCKER_ERROR = 'Docker Error';
BoxError.EXTERNAL_ERROR = 'External Error';
BoxError.FS_ERROR = 'FileSystem Error';
BoxError.INTERNAL_ERROR = 'Internal Error';
BoxError.LOGROTATE_ERROR = 'Logrotate Error';
BoxError.NETWORK_ERROR = 'Network Error';
BoxError.NOT_FOUND = 'Not found';
BoxError.REVERSEPROXY_ERROR = 'ReverseProxy Error';
BoxError.TASK_ERROR = 'Task Error';
BoxError.UNKNOWN_ERROR = 'Unknown Error'; // only used for porting
BoxError.prototype.toPlainObject = function () {
return _.extend({}, { message: this.message, reason: this.reason }, this.details);
};
-38
View File
@@ -1,38 +0,0 @@
'use strict';
let assert = require('assert'),
fs = require('fs'),
path = require('path');
exports = module.exports = {
getChanges: getChanges
};
function getChanges(version) {
assert.strictEqual(typeof version, 'string');
let changelog = [ ];
const lines = fs.readFileSync(path.join(__dirname, '../CHANGES'), 'utf8').split('\n');
version = version.replace(/[+-].*/, ''); // strip prerelease
let i;
for (i = 0; i < lines.length; i++) {
if (lines[i] === '[' + version + ']') break;
}
for (i = i + 1; i < lines.length; i++) {
if (lines[i] === '') continue;
if (lines[i][0] === '[') break;
lines[i] = lines[i].trim();
// detect and remove list style - and * in changelog lines
if (lines[i].indexOf('-') === 0) lines[i] = lines[i].slice(1).trim();
if (lines[i].indexOf('*') === 0) lines[i] = lines[i].slice(1).trim();
changelog.push(lines[i]);
}
return changelog;
}
+92 -64
View File
@@ -6,6 +6,7 @@ exports = module.exports = {
initialize: initialize,
uninitialize: uninitialize,
getConfig: getConfig,
getDisks: getDisks,
getLogs: getLogs,
reboot: reboot,
@@ -18,22 +19,24 @@ exports = module.exports = {
setDashboardAndMailDomain: setDashboardAndMailDomain,
renewCerts: renewCerts,
setupDashboard: setupDashboard,
runSystemChecks: runSystemChecks,
// exposed for testing
_checkDiskSpace: checkDiskSpace
};
var apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
auditSource = require('./auditsource.js'),
backups = require('./backups.js'),
clients = require('./clients.js'),
config = require('./config.js'),
constants = require('./constants.js'),
cron = require('./cron.js'),
debug = require('debug')('box:cloudron'),
domains = require('./domains.js'),
DomainsError = require('./domains.js').DomainsError,
df = require('@sindresorhus/df'),
eventlog = require('./eventlog.js'),
custom = require('./custom.js'),
fs = require('fs'),
@@ -44,14 +47,11 @@ var apps = require('./apps.js'),
paths = require('./paths.js'),
platform = require('./platform.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'),
TaskError = require('./tasks.js').TaskError,
users = require('./users.js'),
util = require('util');
@@ -89,7 +89,7 @@ function initialize(callback) {
runStartupTasks();
notifyUpdate(callback);
callback();
}
function uninitialize(callback) {
@@ -113,32 +113,13 @@ function onActivated(callback) {
], callback);
}
function notifyUpdate(callback) {
assert.strictEqual(typeof callback, 'function');
const version = safe.fs.readFileSync(paths.VERSION_FILE, 'utf8');
if (version === constants.VERSION) return callback();
eventlog.add(eventlog.ACTION_UPDATE_FINISH, auditSource.CRON, { oldVersion: version || 'dev', newVersion: constants.VERSION }, function (error) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
tasks.setCompletedByType(tasks.TASK_UPDATE, { error: null }, function (error) {
if (error && error.reason !== TaskError.NOT_FOUND) return callback(error); // when hotfixing, task may not exist
safe.fs.writeFileSync(paths.VERSION_FILE, constants.VERSION, 'utf8');
callback();
});
});
}
// each of these tasks can fail. we will add some routes to fix/re-run them
function runStartupTasks() {
// configure nginx to be reachable by IP
reverseProxy.configureDefaultServer(NOOP_CALLBACK);
// always generate webadmin config since we have no versioning mechanism for the ejs
if (settings.adminDomain()) reverseProxy.writeAdminConfig(settings.adminDomain(), NOOP_CALLBACK);
if (config.adminDomain()) reverseProxy.writeAdminConfig(config.adminDomain(), NOOP_CALLBACK);
// check activation state and start the platform
users.isActivated(function (error, activated) {
@@ -149,6 +130,32 @@ function runStartupTasks() {
});
}
function getDisks(callback) {
assert.strictEqual(typeof callback, 'function');
var disks = {
boxDataDisk: null,
platformDataDisk: null,
appsDataDisk: null
};
df.file(paths.BOX_DATA_DIR).then(function (result) {
disks.boxDataDisk = result.filesystem;
return df.file(paths.PLATFORM_DATA_DIR);
}).then(function (result) {
disks.platformDataDisk = result.filesystem;
return df.file(paths.APPS_DATA_DIR);
}).then(function (result) {
disks.appsDataDisk = result.filesystem;
callback(null, disks);
}).catch(function (error) {
callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
});
}
function getConfig(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -157,15 +164,15 @@ function getConfig(callback) {
// be picky about what we send out here since this is sent for 'normal' users as well
callback(null, {
apiServerOrigin: settings.apiServerOrigin(),
webServerOrigin: settings.webServerOrigin(),
adminDomain: settings.adminDomain(),
adminFqdn: settings.adminFqdn(),
mailFqdn: settings.mailFqdn(),
version: constants.VERSION,
isDemo: settings.isDemo(),
apiServerOrigin: config.apiServerOrigin(),
webServerOrigin: config.webServerOrigin(),
adminDomain: config.adminDomain(),
adminFqdn: config.adminFqdn(),
mailFqdn: config.mailFqdn(),
version: config.version(),
isDemo: config.isDemo(),
memory: os.totalmem(),
provider: sysinfo.provider(),
provider: config.provider(),
cloudronName: allSettings[settings.CLOUDRON_NAME_KEY],
uiSpec: custom.uiSpec()
});
@@ -187,6 +194,7 @@ function isRebootRequired(callback) {
function runSystemChecks() {
async.parallel([
checkBackupConfiguration,
checkDiskSpace,
checkMailStatus,
checkRebootRequired
], function (error) {
@@ -206,6 +214,45 @@ function checkBackupConfiguration(callback) {
});
}
function checkDiskSpace(callback) {
assert.strictEqual(typeof callback, 'function');
debug('Checking disk space');
getDisks(function (error, disks) {
if (error) {
debug('df error %s', error.message);
return callback();
}
df().then(function (entries) {
/*
[{
filesystem: '/dev/disk1',
size: 499046809600,
used: 443222245376,
available: 55562420224,
capacity: 0.89,
mountpoint: '/'
}, ...]
*/
var oos = entries.some(function (entry) {
// ignore other filesystems but where box, app and platform data is
if (entry.filesystem !== disks.boxDataDisk && entry.filesystem !== disks.platformDataDisk && entry.filesystem !== disks.appsDataDisk) return false;
return (entry.available <= (1.25 * 1024 * 1024 * 1024)); // 1.5G
});
debug('Disk space checked. ok: %s', !oos);
notifications.alert(notifications.ALERT_DISK_SPACE, 'Server is running out of disk space', oos ? JSON.stringify(entries, null, 4) : '', callback);
}).catch(function (error) {
if (error) console.error(error);
callback();
});
});
}
function checkMailStatus(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -295,13 +342,9 @@ function prepareDashboardDomain(domain, auditSource, callback) {
const conflict = result.filter(app => app.fqdn === fqdn);
if (conflict.length) return callback(new CloudronError(CloudronError.BAD_STATE, 'Dashboard location conflicts with an existing app'));
tasks.add(tasks.TASK_PREPARE_DASHBOARD_DOMAIN, [ domain, auditSource ], function (error, taskId) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
tasks.startTask(taskId, {}, NOOP_CALLBACK);
callback(null, taskId);
});
let task = tasks.startTask(tasks.TASK_PREPARE_DASHBOARD_DOMAIN, [ domain, auditSource ]);
task.on('error', (error) => callback(new CloudronError(CloudronError.INTERNAL_ERROR, error)));
task.on('start', (taskId) => callback(null, taskId));
});
});
}
@@ -323,10 +366,10 @@ function setDashboardDomain(domain, auditSource, callback) {
const fqdn = domains.fqdn(constants.ADMIN_LOCATION, domainObject);
async.series([
(done) => settings.setAdmin(domain, fqdn, done),
(done) => clients.addDefaultClients(settings.adminOrigin(), done)
], function (error) {
config.setAdminDomain(domain);
config.setAdminFqdn(fqdn);
clients.addDefaultClients(config.adminOrigin(), function (error) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_DASHBOARD_DOMAIN_UPDATE, auditSource, { domain: domain, fqdn: fqdn });
@@ -354,27 +397,12 @@ function setDashboardAndMailDomain(domain, auditSource, callback) {
});
}
function setupDashboard(auditSource, progressCallback, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
async.series([
domains.prepareDashboardDomain.bind(null, settings.adminDomain(), auditSource, progressCallback),
setDashboardDomain.bind(null, settings.adminDomain(), auditSource)
], callback);
}
function renewCerts(options, auditSource, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
tasks.add(tasks.TASK_RENEW_CERTS, [ options, auditSource ], function (error, taskId) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
tasks.startTask(taskId, {}, NOOP_CALLBACK);
callback(null, taskId);
});
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));
}
-10
View File
@@ -30,13 +30,3 @@ LoadPlugin "table"
</Result>
</Table>
</Plugin>
<Plugin python>
<Module du>
<Path>
Instance "<%= appId %>"
Dir "<%= appDataDir %>"
</Path>
</Module>
</Plugin>
+203
View File
@@ -0,0 +1,203 @@
'use strict';
exports = module.exports = {
baseDir: baseDir,
// values set here will be lost after a upgrade/update. use the sqlite database
// for persistent values that need to be backed up
get: get,
set: set,
// ifdefs to check environment
CLOUDRON: process.env.BOX_ENV === 'cloudron',
TEST: process.env.BOX_ENV === 'test',
// convenience getters
provider: provider,
apiServerOrigin: apiServerOrigin,
webServerOrigin: webServerOrigin,
adminDomain: adminDomain,
setFqdn: setAdminDomain,
setAdminDomain: setAdminDomain,
setAdminFqdn: setAdminFqdn,
version: version,
database: database,
// these values are derived
adminOrigin: adminOrigin,
internalAdminOrigin: internalAdminOrigin,
sysadminOrigin: sysadminOrigin, // localhost routes
adminFqdn: adminFqdn,
mailFqdn: mailFqdn,
hasIPv6: hasIPv6,
isDemo: isDemo,
// for testing resets to defaults
_reset: _reset
};
var assert = require('assert'),
fs = require('fs'),
path = require('path'),
safe = require('safetydance'),
_ = require('underscore');
// assert on unknown environment can't proceed
assert(exports.CLOUDRON || exports.TEST, 'Unknown environment. This should not happen!');
var data = { };
function baseDir() {
const homeDir = process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE;
if (exports.CLOUDRON) return homeDir;
if (exports.TEST) return path.join(homeDir, '.cloudron_test');
// cannot reach
}
const cloudronConfigFileName = exports.CLOUDRON ? '/etc/cloudron/cloudron.conf' : path.join(baseDir(), 'cloudron.conf');
function saveSync() {
// only save values we want to have in the cloudron.conf, see start.sh
var conf = {
apiServerOrigin: data.apiServerOrigin,
webServerOrigin: data.webServerOrigin,
adminDomain: data.adminDomain,
adminFqdn: data.adminFqdn,
provider: data.provider,
isDemo: data.isDemo
};
fs.writeFileSync(cloudronConfigFileName, JSON.stringify(conf, null, 4)); // functions are ignored by JSON.stringify
}
function _reset(callback) {
safe.fs.unlinkSync(cloudronConfigFileName);
initConfig();
if (callback) callback();
}
function initConfig() {
// setup defaults
data.adminFqdn = '';
data.adminDomain = '';
data.port = 3000;
data.apiServerOrigin = null;
data.webServerOrigin = null;
data.provider = 'generic';
data.smtpPort = 2525; // this value comes from mail container
data.sysadminPort = 3001;
data.ldapPort = 3002;
data.dockerProxyPort = 3003;
// keep in sync with start.sh
data.database = {
hostname: '127.0.0.1',
username: 'root',
password: 'password',
port: 3306,
name: 'box'
};
// overrides for local testings
if (exports.TEST) {
data.port = 5454;
data.apiServerOrigin = 'http://localhost:6060'; // hock doesn't support https
// see setupTest script how the mysql-server is run
data.database.hostname = require('child_process').execSync('docker inspect -f "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}" mysql-server').toString().trim();
}
// overwrite defaults with saved config
var existingData = safe.JSON.parse(safe.fs.readFileSync(cloudronConfigFileName, 'utf8'));
_.extend(data, existingData);
}
initConfig();
// set(obj) or set(key, value)
function set(key, value) {
if (typeof key === 'object') {
var obj = key;
for (var k in obj) {
assert(k in data, 'config.js is missing key "' + k + '"');
data[k] = obj[k];
}
} else {
data = safe.set(data, key, value);
}
saveSync();
}
function get(key) {
assert.strictEqual(typeof key, 'string');
return safe.query(data, key);
}
function apiServerOrigin() {
return get('apiServerOrigin');
}
function webServerOrigin() {
return get('webServerOrigin');
}
function setAdminDomain(domain) {
set('adminDomain', domain);
}
function adminDomain() {
return get('adminDomain');
}
function setAdminFqdn(adminFqdn) {
set('adminFqdn', adminFqdn);
}
function adminFqdn() {
return get('adminFqdn');
}
function mailFqdn() {
return adminFqdn();
}
function adminOrigin() {
return 'https://' + adminFqdn();
}
function internalAdminOrigin() {
return 'http://127.0.0.1:' + get('port');
}
function sysadminOrigin() {
return 'http://127.0.0.1:' + get('sysadminPort');
}
function version() {
if (exports.TEST) return '3.0.0-test';
return fs.readFileSync(path.join(__dirname, '../VERSION'), 'utf8').trim();
}
function database() {
return get('database');
}
function isDemo() {
return get('isDemo') === true;
}
function provider() {
return get('provider');
}
function hasIPv6() {
const IPV6_PROC_FILE = '/proc/net/if_inet6';
// on contabo, /proc/net/if_inet6 is an empty file. so just exists is not enough
return fs.existsSync(IPV6_PROC_FILE) && fs.readFileSync(IPV6_PROC_FILE, 'utf8').trim().length !== 0;
}
+1 -18
View File
@@ -1,11 +1,5 @@
'use strict';
let fs = require('fs'),
path = require('path');
const CLOUDRON = process.env.BOX_ENV === 'cloudron',
TEST = process.env.BOX_ENV === 'test';
exports = module.exports = {
SMTP_LOCATION: 'smtp',
IMAP_LOCATION: 'imap',
@@ -24,12 +18,6 @@ exports = module.exports = {
ADMIN_LOCATION: 'my',
PORT: CLOUDRON ? 3000 : 5454,
INTERNAL_SMTP_PORT: 2525, // this value comes from the mail container
SYSADMIN_PORT: 3001, // unused
LDAP_PORT: 3002,
DOCKER_PROXY_PORT: 3003,
NGINX_DEFAULT_CONFIG_FILE_NAME: 'default.conf',
GHOST_USER_FILE: '/tmp/cloudron_ghost.json',
@@ -42,11 +30,6 @@ exports = module.exports = {
AUTOUPDATE_PATTERN_NEVER: 'never',
SECRET_PLACEHOLDER: String.fromCharCode(0x25CF).repeat(8),
CLOUDRON: CLOUDRON,
TEST: TEST,
VERSION: process.env.BOX_ENV === 'cloudron' ? fs.readFileSync(path.join(__dirname, '../VERSION'), 'utf8').trim() : '4.2.0-test'
SECRET_PLACEHOLDER: String.fromCharCode(0x25CF).repeat(8)
};
+3 -13
View File
@@ -15,10 +15,10 @@ var appHealthMonitor = require('./apphealthmonitor.js'),
auditSource = require('./auditsource.js'),
backups = require('./backups.js'),
cloudron = require('./cloudron.js'),
config = require('./config.js'),
constants = require('./constants.js'),
CronJob = require('cron').CronJob,
debug = require('debug')('box:cron'),
disks = require('./disks.js'),
dyndns = require('./dyndns.js'),
eventlog = require('./eventlog.js'),
janitor = require('./janitor.js'),
@@ -35,7 +35,6 @@ var gJobs = {
backup: null,
boxUpdateChecker: null,
systemChecks: null,
diskSpaceChecker: null,
certificateRenew: null,
cleanupBackups: null,
cleanupEventlog: null,
@@ -113,15 +112,6 @@ function recreateJobs(tz) {
timeZone: tz
});
if (gJobs.diskSpaceChecker) gJobs.diskSpaceChecker.stop();
gJobs.diskSpaceChecker = new CronJob({
cronTime: '00 30 * * * *', // every 30 minutes. if you change this interval, change the notification messages with correct duration
onTick: () => disks.checkDiskSpace(NOOP_CALLBACK),
start: true,
runOnInit: true, // run system check immediately
timeZone: tz
});
// randomized pattern per cloudron every hour
var randomMinute = Math.floor(60*Math.random());
@@ -175,7 +165,7 @@ function recreateJobs(tz) {
if (gJobs.schedulerSync) gJobs.schedulerSync.stop();
gJobs.schedulerSync = new CronJob({
cronTime: constants.TEST ? '*/10 * * * * *' : '00 */1 * * * *', // every minute
cronTime: config.TEST ? '*/10 * * * * *' : '00 */1 * * * *', // every minute
onTick: scheduler.sync,
start: true,
timeZone: tz
@@ -258,7 +248,7 @@ function dynamicDnsChanged(enabled) {
if (enabled) {
gJobs.dynamicDns = new CronJob({
cronTime: '5 * * * * *', // we only update the records if the ip has changed.
cronTime: '00 */10 * * * *',
onTick: dyndns.sync.bind(null, auditSource.CRON, NOOP_CALLBACK),
start: true,
timeZone: gJobs.boxUpdateCheckerJob.cronTime.zone // hack
+4 -3
View File
@@ -1,6 +1,7 @@
'use strict';
let debug = require('debug')('box:custom'),
let config = require('./config.js'),
debug = require('debug')('box:features'),
lodash = require('lodash'),
paths = require('./paths.js'),
safe = require('safetydance'),
@@ -31,8 +32,8 @@ const DEFAULT_SPEC = {
remoteSupport: true,
ticketFormBody:
'Use this form to open support tickets. You can also write directly to [support@cloudron.io](mailto:support@cloudron.io).\n\n'
+ '* [Knowledge Base & App Docs](https://cloudron.io/documentation/apps/?support_view)\n'
+ '* [Custom App Packaging & API](https://cloudron.io/developer/packaging/?support_view)\n'
+ `* [Knowledge Base & App Docs](${config.webServerOrigin()}/documentation/apps/?support_view)\n`
+ `* [Custom App Packaging & API](${config.webServerOrigin()}/developer/packaging/?support_view)\n`
+ '* [Forum](https://forum.cloudron.io/)\n\n',
submitTickets: true
},
+11 -24
View File
@@ -15,7 +15,7 @@ exports = module.exports = {
var assert = require('assert'),
async = require('async'),
child_process = require('child_process'),
constants = require('./constants.js'),
config = require('./config.js'),
mysql = require('mysql'),
once = require('once'),
util = require('util');
@@ -23,38 +23,25 @@ var assert = require('assert'),
var gConnectionPool = null,
gDefaultConnection = null;
const gDatabase = {
hostname: '127.0.0.1',
username: 'root',
password: 'password',
port: 3306,
name: 'box'
};
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
if (gConnectionPool !== null) return callback(null);
if (constants.TEST) {
// see setupTest script how the mysql-server is run
gDatabase.hostname = require('child_process').execSync('docker inspect -f "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}" mysql-server').toString().trim();
}
gConnectionPool = mysql.createPool({
connectionLimit: 5, // this has to be > 1 since we store one connection as 'default'. the rest for transactions
host: gDatabase.hostname,
user: gDatabase.username,
password: gDatabase.password,
port: gDatabase.port,
database: gDatabase.name,
host: config.database().hostname,
user: config.database().username,
password: config.database().password,
port: config.database().port,
database: config.database().name,
multipleStatements: false,
ssl: false,
timezone: 'Z' // mysql follows the SYSTEM timezone. on Cloudron, this is UTC
});
gConnectionPool.on('connection', function (connection) {
connection.query('USE ' + gDatabase.name);
connection.query('USE ' + config.database().name);
connection.query('SET SESSION sql_mode = \'strict_all_tables\'');
});
@@ -100,8 +87,8 @@ function clear(callback) {
assert.strictEqual(typeof callback, 'function');
var cmd = util.format('mysql --host="%s" --user="%s" --password="%s" -Nse "SHOW TABLES" %s | grep -v "^migrations$" | while read table; do mysql --host="%s" --user="%s" --password="%s" -e "SET FOREIGN_KEY_CHECKS = 0; TRUNCATE TABLE $table" %s; done',
gDatabase.hostname, gDatabase.username, gDatabase.password, gDatabase.name,
gDatabase.hostname, gDatabase.username, gDatabase.password, gDatabase.name);
config.database().hostname, config.database().username, config.database().password, config.database().name,
config.database().hostname, config.database().username, config.database().password, config.database().name);
async.series([
child_process.exec.bind(null, cmd),
@@ -191,7 +178,7 @@ function importFromFile(file, callback) {
assert.strictEqual(typeof file, 'string');
assert.strictEqual(typeof callback, 'function');
var cmd = `/usr/bin/mysql -h "${gDatabase.hostname}" -u ${gDatabase.username} -p${gDatabase.password} ${gDatabase.name} < ${file}`;
var cmd = `/usr/bin/mysql -h "${config.database().hostname}" -u ${config.database().username} -p${config.database().password} ${config.database().name} < ${file}`;
async.series([
query.bind(null, 'CREATE DATABASE IF NOT EXISTS box'),
@@ -203,7 +190,7 @@ function exportToFile(file, callback) {
assert.strictEqual(typeof file, 'string');
assert.strictEqual(typeof callback, 'function');
var cmd = `/usr/bin/mysqldump -h "${gDatabase.hostname}" -u root -p${gDatabase.password} --single-transaction --routines --triggers ${gDatabase.name} > "${file}"`;
var cmd = `/usr/bin/mysqldump -h "${config.database().hostname}" -u root -p${config.database().password} --single-transaction --routines --triggers ${config.database().name} > "${file}"`;
child_process.exec(cmd, callback);
}
-118
View File
@@ -1,118 +0,0 @@
'use strict';
exports = module.exports = {
getDisks: getDisks,
checkDiskSpace: checkDiskSpace
};
const apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
debug = require('debug')('box:disks'),
df = require('@sindresorhus/df'),
docker = require('./docker.js'),
notifications = require('./notifications.js'),
paths = require('./paths.js'),
util = require('util');
function DisksError(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(DisksError, Error);
DisksError.INTERNAL_ERROR = 'Internal Error';
DisksError.EXTERNAL_ERROR = 'External Error';
function getDisks(callback) {
assert.strictEqual(typeof callback, 'function');
const dfAsync = async.asyncify(df), dfFileAsync = async.asyncify(df.file);
docker.info(function (error, info) {
if (error) return callback(new DisksError(DisksError.INTERNAL_ERROR, error));
async.series([
dfAsync,
dfFileAsync.bind(null, paths.BOX_DATA_DIR),
dfFileAsync.bind(null, paths.PLATFORM_DATA_DIR),
dfFileAsync.bind(null, paths.APPS_DATA_DIR),
dfFileAsync.bind(null, info.DockerRootDir)
], function (error, values) {
if (error) return callback(new DisksError(DisksError.INTERNAL_ERROR, error));
// filter by ext4 and then sort to make sure root disk is first
const ext4Disks = values[0].filter((r) => r.type === 'ext4').sort((a, b) => a.mountpoint.localeCompare(b.mountpoint));
const disks = {
disks: ext4Disks, // root disk is first
boxDataDisk: values[1].filesystem,
mailDataDisk: values[1].filesystem,
platformDataDisk: values[2].filesystem,
appsDataDisk: values[3].filesystem,
dockerDataDisk: values[4].filesystem,
apps: {}
};
apps.getAll(function (error, allApps) {
if (error) return callback(new DisksError(DisksError.INTERNAL_ERROR, error));
async.eachSeries(allApps, function (app, iteratorDone) {
if (!app.dataDir) {
disks.apps[app.id] = disks.appsDataDisk;
return iteratorDone();
}
dfFileAsync(app.dataDir, function (error, result) {
disks.apps[app.id] = error ? disks.appsDataDisk : result.filesystem; // ignore any errors
iteratorDone();
});
}, function (error) {
if (error) return callback(new DisksError(DisksError.INTERNAL_ERROR, error));
callback(null, disks);
});
});
});
});
}
function checkDiskSpace(callback) {
assert.strictEqual(typeof callback, 'function');
debug('Checking disk space');
getDisks(function (error, disks) {
if (error) {
debug('checkDiskSpace: error getting disks %s', error.message);
return callback();
}
var oos = disks.disks.some(function (entry) {
// ignore other filesystems but where box, app and platform data is
if (entry.filesystem !== disks.boxDataDisk
&& entry.filesystem !== disks.platformDataDisk
&& entry.filesystem !== disks.appsDataDisk
&& entry.filesystem !== disks.dockerDataDisk) return false;
return (entry.available <= (1.25 * 1024 * 1024 * 1024)); // 1.5G
});
debug('checkDiskSpace: disk space checked. ok: %s', !oos);
notifications.alert(notifications.ALERT_DISK_SPACE, 'Server is running out of disk space', oos ? JSON.stringify(disks.disks, null, 4) : '', callback);
});
}
+4 -4
View File
@@ -11,10 +11,10 @@ exports = module.exports = {
};
var assert = require('assert'),
config = require('../config.js'),
debug = require('debug')('box:dns/caas'),
domains = require('../domains.js'),
DomainsError = require('../domains.js').DomainsError,
settings = require('../settings.js'),
superagent = require('superagent'),
util = require('util'),
waitForDns = require('./waitfordns.js');
@@ -58,7 +58,7 @@ function upsert(domainObject, location, type, values, callback) {
};
superagent
.post(settings.apiServerOrigin() + '/api/v1/caas/domains/' + fqdn)
.post(config.apiServerOrigin() + '/api/v1/caas/domains/' + fqdn)
.query({ token: dnsConfig.token })
.send(data)
.timeout(30 * 1000)
@@ -84,7 +84,7 @@ function get(domainObject, location, type, callback) {
debug('get: zoneName: %s subdomain: %s type: %s fqdn: %s', domainObject.domain, location, type, fqdn);
superagent
.get(settings.apiServerOrigin() + '/api/v1/caas/domains/' + fqdn)
.get(config.apiServerOrigin() + '/api/v1/caas/domains/' + fqdn)
.query({ token: dnsConfig.token, type: type })
.timeout(30 * 1000)
.end(function (error, result) {
@@ -111,7 +111,7 @@ function del(domainObject, location, type, values, callback) {
};
superagent
.del(settings.apiServerOrigin() + '/api/v1/caas/domains/' + getFqdn(location, domainObject.domain))
.del(config.apiServerOrigin() + '/api/v1/caas/domains/' + getFqdn(location, domainObject.domain))
.query({ token: dnsConfig.token })
.send(data)
.timeout(30 * 1000)
+2 -12
View File
@@ -71,12 +71,7 @@ function getInternal(dnsConfig, zoneName, subdomain, type, callback) {
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error));
var tmp = result.ApiResponse;
if (tmp['$'].Status !== 'OK') {
var errorMessage = safe.query(tmp, 'Errors[0].Error[0]._', 'Invalid response');
if (errorMessage === 'API Key is invalid or API access has not been enabled') return callback(new DomainsError(DomainsError.ACCESS_DENIED, errorMessage));
return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, errorMessage));
}
if (tmp['$'].Status !== 'OK') return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, safe.query(tmp, 'Errors[0].Error[0]._', 'Invalid response')));
if (!tmp.CommandResponse[0]) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, 'Invalid response'));
if (!tmp.CommandResponse[0].DomainDNSGetHostsResult[0]) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, 'Invalid response'));
@@ -125,12 +120,7 @@ function setInternal(dnsConfig, zoneName, hosts, callback) {
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error));
var tmp = result.ApiResponse;
if (tmp['$'].Status !== 'OK') {
var errorMessage = safe.query(tmp, 'Errors[0].Error[0]._', 'Invalid response');
if (errorMessage === 'API Key is invalid or API access has not been enabled') return callback(new DomainsError(DomainsError.ACCESS_DENIED, errorMessage));
return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, errorMessage));
}
if (tmp['$'].Status !== 'OK') return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, safe.query(tmp, 'Errors[0].Error[0]._', 'Invalid response')));
if (!tmp.CommandResponse[0]) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, 'Invalid response'));
if (!tmp.CommandResponse[0].DomainDNSSetHostsResult[0]) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, 'Invalid response'));
if (tmp.CommandResponse[0].DomainDNSSetHostsResult[0]['$'].IsSuccess !== 'true') return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, 'Invalid response'));
+70 -61
View File
@@ -1,12 +1,13 @@
'use strict';
exports = module.exports = {
DockerError: DockerError,
connection: connectionInstance(),
setRegistryConfig: setRegistryConfig,
ping: ping,
info: info,
downloadImage: downloadImage,
createContainer: createContainer,
startContainer: startContainer,
@@ -32,22 +33,31 @@ exports = module.exports = {
// timeout is optional
function connectionInstance(timeout) {
var Docker = require('dockerode');
var docker = new Docker({ socketPath: '/var/run/docker.sock', timeout: timeout });
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 });
// 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 });
}
return docker;
}
var addons = require('./addons.js'),
async = require('async'),
assert = require('assert'),
BoxError = require('./boxerror.js'),
child_process = require('child_process'),
config = require('./config.js'),
constants = require('./constants.js'),
debug = require('debug')('box:docker.js'),
once = require('once'),
path = require('path'),
settings = require('./settings.js'),
shell = require('./shell.js'),
safe = require('safetydance'),
spawn = child_process.spawn,
util = require('util'),
_ = require('underscore');
@@ -55,7 +65,30 @@ var addons = require('./addons.js'),
const CLEARVOLUME_CMD = path.join(__dirname, 'scripts/clearvolume.sh'),
MKDIRVOLUME_CMD = path.join(__dirname, 'scripts/mkdirvolume.sh');
function debugApp(app) {
function DockerError(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(DockerError, Error);
DockerError.INTERNAL_ERROR = 'Internal Error';
DockerError.NOT_FOUND = 'Not found';
DockerError.BAD_FIELD = 'Bad field';
function debugApp(app, args) {
assert(typeof app === 'object');
debug(app.fqdn + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
@@ -70,8 +103,8 @@ function setRegistryConfig(auth, callback) {
// currently, auth info is not stashed in the db but maybe it should for restore to work?
const cmd = isLogin ? `docker login ${auth.serveraddress} --username ${auth.username} --password ${auth.password}` : `docker logout ${auth.serveraddress}`;
child_process.exec(cmd, { }, function (error /*, stdout, stderr */) {
if (error) return callback(new BoxError(BoxError.ACCESS_DENIED, error.message));
child_process.exec(cmd, { }, function (error, stdout, stderr) {
if (error) return callback(new DockerError(DockerError.BAD_FIELD, stderr));
callback();
});
@@ -84,8 +117,8 @@ function ping(callback) {
var docker = connectionInstance(1000);
docker.ping(function (error, result) {
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
if (result !== 'OK') return callback(new BoxError(BoxError.DOCKER_ERROR, 'Unable to ping the docker daemon'));
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);
});
@@ -96,32 +129,23 @@ function pullImage(manifest, callback) {
// 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
docker.pull(manifest.dockerImage, function (error, stream) {
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, 'Unable to pull image. statusCode: ' + error.statusCode));
shell.spawn('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'));
}
// https://github.com/dotcloud/docker/issues/1074 says each status message
// is emitted as a chunk
stream.on('data', function (chunk) {
var data = safe.JSON.parse(chunk) || { };
debug('pullImage %s: %j', manifest.id, data);
var image = docker.getImage(manifest.dockerImage);
// The data.status here is useless because this is per layer as opposed to per image
if (!data.status && data.error) {
debug('pullImage error %s: %s', manifest.id, data.errorDetail.message);
}
});
image.inspect(function (err, data) {
if (err) return callback(new Error('Error inspecting image:' + err.message));
if (!data || !data.Config) return callback(new Error('Missing Config in image:' + JSON.stringify(data, null, 4)));
if (!data.Config.Entrypoint && !data.Config.Cmd) return callback(new Error('Only images with entry point are allowed'));
stream.on('end', function () {
debug('downloaded image %s of %s successfully', manifest.dockerImage, manifest.id);
if (data.Config.ExposedPorts) debug('This image of %s exposes ports: %j', manifest.id, data.Config.ExposedPorts);
callback(null);
});
stream.on('error', function (error) {
debug('error pulling image %s of %s: %j', manifest.dockerImage, manifest.id, error);
callback(new BoxError(BoxError.DOCKER_ERROR, error.message));
});
});
}
@@ -152,22 +176,20 @@ function createSubcontainer(app, name, cmd, options, callback) {
assert.strictEqual(typeof callback, 'function');
var docker = exports.connection,
isAppContainer = !cmd; // non app-containers are like scheduler and exec (terminal) containers
isAppContainer = !cmd; // non app-containers are like scheduler containers
var manifest = app.manifest;
var exposedPorts = {}, dockerPortBindings = { };
var domain = app.fqdn;
const hostname = isAppContainer ? app.id : name;
const envPrefix = manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
let stdEnv = [
'CLOUDRON=1',
'CLOUDRON_PROXY_IP=172.18.0.1',
`CLOUDRON_APP_HOSTNAME=${app.id}`,
`CLOUDRON_ADMIN_EMAIL=${app.adminEmail}`,
`${envPrefix}WEBADMIN_ORIGIN=${settings.adminOrigin()}`,
`${envPrefix}API_ORIGIN=${settings.adminOrigin()}`,
`CLOUDRON_APP_HOSTNAME=${name}`,
`${envPrefix}WEBADMIN_ORIGIN=${config.adminOrigin()}`,
`${envPrefix}API_ORIGIN=${config.adminOrigin()}`,
`${envPrefix}APP_ORIGIN=https://${domain}`,
`${envPrefix}APP_DOMAIN=${domain}`
];
@@ -218,7 +240,7 @@ function createSubcontainer(app, name, cmd, options, callback) {
var containerOptions = {
name: name, // for referencing containers
Tty: isAppContainer,
Hostname: hostname,
Hostname: name,
Image: app.manifest.dockerImage,
Cmd: (isAppContainer && app.debugMode && app.debugMode.cmd) ? app.debugMode.cmd : cmd,
Env: stdEnv.concat(addonEnv).concat(portEnv).concat(appEnv),
@@ -297,8 +319,7 @@ function startContainer(containerId, callback) {
debug('Starting container %s', containerId);
container.start(function (error) {
if (error && error.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND));
if (error && error.statusCode !== 304) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
if (error && error.statusCode !== 304) return callback(new Error('Error starting container :' + error));
return callback(null);
});
@@ -374,7 +395,7 @@ function deleteContainers(appId, options, callback) {
if (options.managedOnly) labels.push('isCloudronManaged=true');
docker.listContainers({ all: 1, filters: JSON.stringify({ label: labels }) }, function (error, containers) {
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
if (error) return callback(error);
async.eachSeries(containers, function (container, iteratorDone) {
deleteContainer(container.Id, iteratorDone);
@@ -391,7 +412,7 @@ function stopContainers(appId, callback) {
debug('stopping containers of %s', appId);
docker.listContainers({ all: 1, filters: JSON.stringify({ label: [ 'appId=' + appId ] }) }, function (error, containers) {
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
if (error) return callback(error);
async.eachSeries(containers, function (container, iteratorDone) {
stopContainer(container.Id, iteratorDone);
@@ -435,7 +456,7 @@ function getContainerIdByIp(ip, callback) {
docker.getNetwork('cloudron').inspect(function (error, bridge) {
if (error && error.statusCode === 404) return callback(new Error('Unable to find the cloudron network'));
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
if (error) return callback(error);
var containerId;
for (var id in bridge.Containers) {
@@ -457,8 +478,8 @@ function inspect(containerId, callback) {
var container = exports.connection.getContainer(containerId);
container.inspect(function (error, result) {
if (error && error.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND));
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
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);
});
@@ -471,7 +492,7 @@ function getEvents(options, callback) {
let docker = exports.connection;
docker.getEvents(options, function (error, stream) {
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
if (error) return callback(new DockerError(DockerError.INTERNAL_ERROR, error));
callback(null, stream);
});
@@ -484,8 +505,8 @@ function memoryUsage(containerId, callback) {
var container = exports.connection.getContainer(containerId);
container.stats({ stream: false }, function (error, result) {
if (error && error.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND));
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
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);
});
@@ -549,7 +570,7 @@ function createVolume(app, name, volumeDataDir, callback) {
if (error) return callback(new Error(`Error creating app data dir: ${error.message}`));
docker.createVolume(volumeOptions, function (error) {
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
if (error) return callback(error);
callback();
});
@@ -566,7 +587,7 @@ function clearVolume(app, name, options, callback) {
let volume = docker.getVolume(name);
volume.inspect(function (error, v) {
if (error && error.statusCode === 404) return callback();
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
if (error) return callback(error);
const volumeDataDir = v.Options.device;
shell.sudo('clearVolume', [ CLEARVOLUME_CMD, options.removeDirectory ? 'rmdir' : 'clear', volumeDataDir ], {}, callback);
@@ -588,15 +609,3 @@ function removeVolume(app, name, callback) {
callback();
});
}
function info(callback) {
assert.strictEqual(typeof callback, 'function');
let docker = exports.connection;
docker.info(function (error, result) {
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, 'Error connecting to docker'));
callback(null, result);
});
}
+6 -6
View File
@@ -8,7 +8,7 @@ exports = module.exports = {
var apps = require('./apps.js'),
AppsError = apps.AppsError,
assert = require('assert'),
constants = require('./constants.js'),
config = require('./config.js'),
express = require('express'),
debug = require('debug')('box:dockerproxy'),
http = require('http'),
@@ -29,7 +29,7 @@ function authorizeApp(req, res, next) {
// - only allow managing and inspection of containers belonging to the app
// make the tests pass for now
if (constants.TEST) {
if (config.TEST) {
req.app = { id: 'testappid' };
return next();
}
@@ -120,7 +120,7 @@ function start(callback) {
let proxyServer = express();
if (constants.TEST) {
if (config.TEST) {
proxyServer.use(function (req, res, next) {
debug('proxying: ' + req.method, req.url);
next();
@@ -135,9 +135,9 @@ function start(callback) {
.use(middleware.lastMile());
gHttpServer = http.createServer(proxyServer);
gHttpServer.listen(constants.DOCKER_PROXY_PORT, '0.0.0.0', callback);
gHttpServer.listen(config.get('dockerProxyPort'), '0.0.0.0', callback);
debug(`startDockerProxy: started proxy on port ${constants.DOCKER_PROXY_PORT}`);
debug(`startDockerProxy: started proxy on port ${config.get('dockerProxyPort')}`);
gHttpServer.on('upgrade', function (req, client, head) {
// Create a new tcp connection to the TCP server
@@ -150,7 +150,7 @@ function start(callback) {
if (req.headers['content-type'] === 'application/json') {
// TODO we have to parse the immediate upgrade request body, but I don't know how
let plainBody = '{"Detach":false,"Tty":false}\r\n';
upgradeMessage += 'Content-Type: application/json\r\n';
upgradeMessage += `Content-Type: application/json\r\n`;
upgradeMessage += `Content-Length: ${Buffer.byteLength(plainBody)}\r\n`;
upgradeMessage += '\r\n';
upgradeMessage += plainBody;
+8 -31
View File
@@ -26,8 +26,6 @@ module.exports = exports = {
parentDomain: parentDomain,
checkDnsRecords: checkDnsRecords,
prepareDashboardDomain: prepareDashboardDomain,
DomainsError: DomainsError,
@@ -37,6 +35,7 @@ module.exports = exports = {
var assert = require('assert'),
async = require('async'),
config = require('./config.js'),
constants = require('./constants.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:domains'),
@@ -45,7 +44,6 @@ var assert = require('assert'),
reverseProxy = require('./reverseproxy.js'),
ReverseProxyError = reverseProxy.ReverseProxyError,
safe = require('safetydance'),
settings = require('./settings.js'),
sysinfo = require('./sysinfo.js'),
tld = require('tldjs'),
util = require('util'),
@@ -78,7 +76,7 @@ DomainsError.BAD_FIELD = 'Bad Field';
DomainsError.STILL_BUSY = 'Still busy';
DomainsError.IN_USE = 'In Use';
DomainsError.INTERNAL_ERROR = 'Internal error';
DomainsError.ACCESS_DENIED = 'Access Denied';
DomainsError.ACCESS_DENIED = 'Access denied';
DomainsError.INVALID_PROVIDER = 'provider must be route53, gcdns, digitalocean, gandi, cloudflare, namecom, noop, wildcard, manual or caas';
// choose which subdomain backend we use for test purpose we use route53
@@ -152,7 +150,7 @@ function validateHostname(location, domainObject) {
];
if (RESERVED_LOCATIONS.indexOf(location) !== -1) return new DomainsError(DomainsError.BAD_FIELD, location + ' is reserved');
if (hostname === settings.adminFqdn()) return new DomainsError(DomainsError.BAD_FIELD, location + ' is reserved');
if (hostname === config.adminFqdn()) return new DomainsError(DomainsError.BAD_FIELD, location + ' is reserved');
// workaround https://github.com/oncletom/tld.js/issues/73
var tmp = hostname.replace('_', '-');
@@ -345,7 +343,7 @@ function del(domain, auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
if (domain === settings.adminDomain()) return callback(new DomainsError(DomainsError.IN_USE));
if (domain === config.adminDomain()) return callback(new DomainsError(DomainsError.IN_USE));
domaindb.del(domain, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DomainsError(DomainsError.NOT_FOUND));
@@ -396,7 +394,7 @@ function getDnsRecords(location, domain, type, callback) {
assert.strictEqual(typeof callback, 'function');
get(domain, function (error, domainObject) {
if (error) return callback(error);
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
api(domainObject.provider).get(domainObject, location, type, function (error, values) {
if (error) return callback(error);
@@ -406,25 +404,6 @@ function getDnsRecords(location, domain, type, callback) {
});
}
function checkDnsRecords(location, domain, callback) {
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
getDnsRecords(location, domain, 'A', function (error, values) {
if (error) return callback(error);
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error.message));
if (values.length === 0) return callback(null, { needsOverwrite: false }); // does not exist
if (values[0] === ip) return callback(null, { needsOverwrite: false }); // exists but in sync
callback(null, { needsOverwrite: true });
});
});
}
// note: for TXT records the values must be quoted
function upsertDnsRecords(location, domain, type, values, callback) {
assert.strictEqual(typeof location, 'string');
@@ -514,17 +493,15 @@ function prepareDashboardDomain(domain, auditSource, progressCallback, callback)
get(domain, function (error, domainObject) {
if (error) return callback(error);
const adminFqdn = fqdn(constants.ADMIN_LOCATION, domainObject);
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error.message));
async.series([
(done) => { progressCallback({ percent: 10, message: `Updating DNS of ${adminFqdn}` }); done(); },
(done) => { progressCallback({ percent: 10, message: 'Updating DNS' }); done(); },
upsertDnsRecords.bind(null, constants.ADMIN_LOCATION, domain, 'A', [ ip ]),
(done) => { progressCallback({ percent: 40, message: `Waiting for DNS of ${adminFqdn}` }); done(); },
(done) => { progressCallback({ percent: 40, message: 'Waiting for DNS' }); done(); },
waitForDnsRecord.bind(null, constants.ADMIN_LOCATION, domain, 'A', ip, { interval: 30000, times: 50000 }),
(done) => { progressCallback({ percent: 70, message: `Getting certificate of ${adminFqdn}` }); done(); },
(done) => { progressCallback({ percent: 70, message: 'Getting certificate' }); done(); },
reverseProxy.ensureCertificate.bind(null, fqdn(constants.ADMIN_LOCATION, domainObject), domain, auditSource)
], function (error) {
if (error) return callback(error);
+5 -4
View File
@@ -4,16 +4,17 @@ exports = module.exports = {
sync: sync
};
let apps = require('./apps.js'),
var appdb = require('./appdb.js'),
apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
config = require('./config.js'),
constants = require('./constants.js'),
debug = require('debug')('box:dyndns'),
domains = require('./domains.js'),
eventlog = require('./eventlog.js'),
paths = require('./paths.js'),
safe = require('safetydance'),
settings = require('./settings.js'),
sysinfo = require('./sysinfo.js');
// called for dynamic dns setups where we have to update the IP
@@ -32,7 +33,7 @@ function sync(auditSource, callback) {
debug(`refreshDNS: updating ip from ${info.ip} to ${ip}`);
domains.upsertDnsRecords(constants.ADMIN_LOCATION, settings.adminDomain(), 'A', [ ip ], function (error) {
domains.upsertDnsRecords(constants.ADMIN_LOCATION, config.adminDomain(), 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('refreshDNS: updated admin location');
@@ -42,7 +43,7 @@ function sync(auditSource, callback) {
async.each(result, function (app, callback) {
// do not change state of installing apps since apptask will error if dns record already exists
if (app.installationState !== apps.ISTATE_INSTALLED) return callback();
if (app.installationState !== appdb.ISTATE_INSTALLED) return callback();
domains.upsertDnsRecords(app.location, app.domain, 'A', [ ip ], callback);
}, function (error) {
+3 -2
View File
@@ -13,7 +13,6 @@ exports = module.exports = {
ACTION_ACTIVATE: 'cloudron.activate',
ACTION_APP_CLONE: 'app.clone',
ACTION_APP_CONFIGURE: 'app.configure',
ACTION_APP_REPAIR: 'app.repair',
ACTION_APP_INSTALL: 'app.install',
ACTION_APP_RESTORE: 'app.restore',
ACTION_APP_UNINSTALL: 'app.uninstall',
@@ -23,6 +22,9 @@ exports = module.exports = {
ACTION_APP_OOM: 'app.oom',
ACTION_APP_UP: 'app.up',
ACTION_APP_DOWN: 'app.down',
ACTION_APP_TASK_START: 'app.task.start',
ACTION_APP_TASK_CRASH: 'app.task.crash',
ACTION_APP_TASK_SUCCESS: 'app.task.success',
ACTION_BACKUP_FINISH: 'backup.finish',
ACTION_BACKUP_START: 'backup.start',
@@ -49,7 +51,6 @@ exports = module.exports = {
ACTION_RESTORE: 'cloudron.restore', // unused
ACTION_START: 'cloudron.start',
ACTION_UPDATE: 'cloudron.update',
ACTION_UPDATE_FINISH: 'cloudron.update.finish',
ACTION_USER_ADD: 'user.add',
ACTION_USER_LOGIN: 'user.login',
-231
View File
@@ -1,231 +0,0 @@
'use strict';
exports = module.exports = {
ExternalLdapError: ExternalLdapError,
verifyPassword: verifyPassword,
testConfig: testConfig,
startSyncer: startSyncer,
sync: sync
};
var assert = require('assert'),
async = require('async'),
auditsource = require('./auditsource.js'),
debug = require('debug')('box:externalldap'),
ldap = require('ldapjs'),
settings = require('./settings.js'),
tasks = require('./tasks.js'),
users = require('./users.js'),
UserError = users.UsersError,
util = require('util');
function ExternalLdapError(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(ExternalLdapError, Error);
ExternalLdapError.EXTERNAL_ERROR = 'external error';
ExternalLdapError.INTERNAL_ERROR = 'internal error';
ExternalLdapError.INVALID_CREDENTIALS = 'invalid credentials';
ExternalLdapError.BAD_STATE = 'bad state';
ExternalLdapError.BAD_FIELD = 'bad field';
ExternalLdapError.NOT_FOUND = 'not found';
// performs service bind if required
function getClient(externalLdapConfig, callback) {
assert.strictEqual(typeof callback, 'function');
// basic validation to not crash
try { ldap.parseDN(externalLdapConfig.baseDn); } catch (e) { return callback(new ExternalLdapError(ExternalLdapError.BAD_FIELD, 'invalid baseDn')); }
try { ldap.parseFilter(externalLdapConfig.filter); } catch (e) { return callback(new ExternalLdapError(ExternalLdapError.BAD_FIELD, 'invalid filter')); }
if (externalLdapConfig.bindDn) try { ldap.parseFilter(externalLdapConfig.bindDn); } catch (e) { return callback(new ExternalLdapError(ExternalLdapError.INVALID_CREDENTIALS)); }
var client;
try {
client = ldap.createClient({ url: externalLdapConfig.url });
} catch (e) {
if (e instanceof ldap.ProtocolError) return callback(new ExternalLdapError(ExternalLdapError.BAD_FIELD, 'url protocol is invalid'));
return callback(new ExternalLdapError(ExternalLdapError.INTERNAL_ERROR, e));
}
if (!externalLdapConfig.bindDn) return callback(null, client);
client.bind(externalLdapConfig.bindDn, externalLdapConfig.bindPassword, function (error) {
if (error instanceof ldap.InvalidCredentialsError) return callback(new ExternalLdapError(ExternalLdapError.INVALID_CREDENTIALS));
if (error) return callback(new ExternalLdapError(ExternalLdapError.EXTERNAL_ERROR, error));
callback(null, client, externalLdapConfig);
});
}
function testConfig(config, callback) {
assert.strictEqual(typeof config, 'object');
assert.strictEqual(typeof callback, 'function');
if (!config.enabled) return callback();
if (!config.url) return callback(new ExternalLdapError(ExternalLdapError.BAD_FIELD, 'url must not be empty'));
if (!config.baseDn) return callback(new ExternalLdapError(ExternalLdapError.BAD_FIELD, 'basedn must not be empty'));
if (!config.filter) return callback(new ExternalLdapError(ExternalLdapError.BAD_FIELD, 'filter must not be empty'));
getClient(config, function (error, client) {
if (error) return callback(error);
var opts = {
filter: config.filter,
scope: 'sub'
};
client.search(config.baseDn, opts, function (error, result) {
if (error) return callback(new ExternalLdapError(ExternalLdapError.EXTERNAL_ERROR, error));
result.on('searchEntry', function (entry) {});
result.on('error', function (error) { callback(new ExternalLdapError(ExternalLdapError.BAD_FIELD, 'Unable to search directory')); });
result.on('end', function (result) { callback(); });
});
});
}
function verifyPassword(user, password, callback) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof password, 'string');
assert.strictEqual(typeof callback, 'function');
settings.getExternalLdapConfig(function (error, externalLdapConfig) {
if (error) return callback(new ExternalLdapError(ExternalLdapError.INTERNAL_ERROR, error));
if (!externalLdapConfig.enabled) return callback(new ExternalLdapError(ExternalLdapError.BAD_STATE, 'not enabled'));
getClient(externalLdapConfig, function (error, client) {
if (error) return callback(error);
const dn = `uid=${user.username},${externalLdapConfig.baseDn}`;
client.bind(dn, password, function (error) {
if (error instanceof ldap.InvalidCredentialsError) return callback(new ExternalLdapError(ExternalLdapError.INVALID_CREDENTIALS));
if (error) return callback(new ExternalLdapError(ExternalLdapError.EXTERNAL_ERROR, error));
callback();
});
});
});
}
function startSyncer(callback) {
assert.strictEqual(typeof callback, 'function');
settings.getExternalLdapConfig(function (error, externalLdapConfig) {
if (error) return callback(new ExternalLdapError(ExternalLdapError.INTERNAL_ERROR, error));
if (!externalLdapConfig.enabled) return callback(new ExternalLdapError(ExternalLdapError.BAD_STATE, 'not enabled'));
tasks.add(tasks.TASK_SYNC_EXTERNAL_LDAP, [], function (error, taskId) {
if (error) return callback(new ExternalLdapError(ExternalLdapError.INTERNAL_ERROR, error));
tasks.startTask(taskId, {}, function (error, result) {
debug('sync: done', error, result);
});
callback(null, taskId);
});
});
}
function sync(progressCallback, callback) {
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
debug('Start user syncing ...');
settings.getExternalLdapConfig(function (error, externalLdapConfig) {
if (error) return callback(new ExternalLdapError(ExternalLdapError.INTERNAL_ERROR, error));
if (!externalLdapConfig.enabled) return callback(new ExternalLdapError(ExternalLdapError.BAD_STATE, 'not enabled'));
getClient(externalLdapConfig, function (error, client) {
if (error) return callback(error);
var opts = {
paged: true,
filter: externalLdapConfig.filter,
scope: 'sub' // We may have to make this configurable
};
debug(`Listing users at ${externalLdapConfig.baseDn} with filter ${externalLdapConfig.filter}`);
client.search(externalLdapConfig.baseDn, opts, function (error, result) {
if (error) return callback(new ExternalLdapError(ExternalLdapError.EXTERNAL_ERROR, error));
var ldapUsers = [];
result.on('searchEntry', function (entry) {
ldapUsers.push(entry.object);
});
result.on('error', function (error) {
callback(new ExternalLdapError(ExternalLdapError.EXTERNAL_ERROR, error));
});
result.on('end', function (result) {
if (result.status !== 0) return callback(new ExternalLdapError(ExternalLdapError.EXTERNAL_ERROR, 'Server returned status ' + result.status));
debug(`Found ${ldapUsers.length} users`);
// we ignore all errors here and just log them for now
async.eachSeries(ldapUsers, function (user, callback) {
// ignore the bindDn user if any
if (user.dn === externalLdapConfig.bindDn) return callback();
users.getByUsername(user.uid, function (error, result) {
if (error && error.reason !== UserError.NOT_FOUND) {
console.error(error);
return callback();
}
if (error) {
debug('[adding user] ', user.uid, user.mail, user.cn);
users.create(user.uid, null, user.mail, user.cn, { source: 'ldap' }, auditsource.EXTERNAL_LDAP_TASK, function (error) {
if (error) console.error('Failed to create user', user, error);
callback();
});
} else if (result.source !== 'ldap') {
debug('[conflicting user]', user.uid, user.mail, user.cn);
callback();
} else if (result.email !== user.mail || result.displayName !== user.cn) {
debug('[updating user] ', user.uid, user.mail, user.cn);
users.update(result.id, { email: user.mail, fallbackEmail: user.mail, displayName: user.cn }, auditsource.EXTERNAL_LDAP_TASK, function (error) {
if (error) console.error('Failed to update user', user, error);
callback();
});
} else {
// user known and up-to-date
callback();
}
});
}, function () {
debug('User sync done.');
callback();
});
});
});
});
});
}
+1 -13
View File
@@ -20,9 +20,7 @@ exports = module.exports = {
getGroups: getGroups,
setMembership: setMembership,
getMembership: getMembership,
count: count
getMembership: getMembership
};
var assert = require('assert'),
@@ -270,13 +268,3 @@ function getGroups(userId, callback) {
callback(null, results);
});
}
function count(callback) {
assert.strictEqual(typeof callback, 'function');
groupdb.count(function (error, count) {
if (error) return callback(new GroupsError(GroupsError.INTERNAL_ERROR, error));
callback(null, count);
});
}
+4 -4
View File
@@ -6,7 +6,7 @@
exports = module.exports = {
// a version change recreates all containers with latest docker config
'version': '48.16.0',
'version': '48.15.0',
'baseImages': [
{ repo: 'cloudron/base', tag: 'cloudron/base:1.0.0@sha256:147a648a068a2e746644746bbfb42eb7a50d682437cead3c67c933c546357617' }
@@ -17,10 +17,10 @@ exports = module.exports = {
'images': {
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:2.0.2@sha256:a28320f313785816be60e3f865e09065504170a3d20ed37de675c719b32b01eb' },
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:2.0.2@sha256:6dcee0731dfb9b013ed94d56205eee219040ee806c7e251db3b3886eaa4947ff' },
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:2.1.0@sha256:6d1bf221cfe6124957e2c58b57c0a47214353496009296acb16adf56df1da9d5' },
'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.4.0@sha256:209f76833ff8cce58be8a09897c378e3b706e1e318249870afb7294a4ee83cad' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:2.2.0@sha256:fc9ca69d16e6ebdbd98ed53143d4a0d2212eef60cb638dc71219234e6f427a2c' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:2.3.1@sha256:9693e3ae42a12a7ac8cf5df94d828d46f5b22b4e2e1c7d1bc614d6ee2a22c365' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:2.0.2@sha256:454f035d60b768153d4f31210380271b5ba1c09367c9d95c7fa37f9e39d2f59c' },
'sftp': { repo: 'cloudron/sftp', tag: 'cloudron/sftp:0.1.0@sha256:e177c5bf5f38c84ce1dea35649c22a1b05f96eec67a54a812c5a35e585670f0f' }
}
};
+1 -1
View File
@@ -63,7 +63,7 @@ function cleanupTmpVolume(containerInfo, callback) {
assert.strictEqual(typeof containerInfo, 'object');
assert.strictEqual(typeof callback, 'function');
var cmd = 'find /tmp -type f -mtime +10 -exec rm -rf {} +'.split(' '); // 10 day old files
var cmd = 'find /tmp -mtime +10 -exec rm -rf {} +'.split(' '); // 10 days old
debug('cleanupTmpVolume %j', containerInfo.Names);
+10 -10
View File
@@ -9,7 +9,7 @@ var assert = require('assert'),
appdb = require('./appdb.js'),
apps = require('./apps.js'),
async = require('async'),
constants = require('./constants.js'),
config = require('./config.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:ldap'),
eventlog = require('./eventlog.js'),
@@ -135,7 +135,7 @@ function userSearch(req, res, next) {
var dn = ldap.parseDN('cn=' + entry.id + ',ou=users,dc=cloudron');
var groups = [ GROUP_USERS_DN ];
if (entry.admin) groups.push(GROUP_ADMINS_DN);
if (entry.admin || req.app.ownerId === entry.id) groups.push(GROUP_ADMINS_DN);
var displayName = entry.displayName || entry.username || ''; // displayName can be empty and username can be null
var nameParts = displayName.split(' ');
@@ -155,7 +155,7 @@ function userSearch(req, res, next) {
givenName: firstName,
username: entry.username,
samaccountname: entry.username, // to support ActiveDirectory clients
isadmin: entry.admin,
isadmin: (entry.admin || req.app.ownerId === entry.id) ? 1 : 0,
memberof: groups
}
};
@@ -195,7 +195,7 @@ function groupSearch(req, res, next) {
groups.forEach(function (group) {
var dn = ldap.parseDN('cn=' + group.name + ',ou=groups,dc=cloudron');
var members = group.admin ? result.filter(function (entry) { return entry.admin; }) : result;
var members = group.admin ? result.filter(function (entry) { return entry.admin || req.app.ownerId === entry.id; }) : result;
var obj = {
dn: dn.toString(),
@@ -244,7 +244,7 @@ function groupAdminsCompare(req, res, next) {
// we only support memberuid here, if we add new group attributes later add them here
if (req.attribute === 'memberuid') {
var found = result.find(function (u) { return u.id === req.value; });
if (found && found.admin) return res.end(true);
if (found && (found.admin || req.app.ownerId == found.id)) return res.end(true);
}
res.end(false);
@@ -371,7 +371,7 @@ function mailingListSearch(req, res, next) {
var parts = email.split('@');
if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString()));
mailboxdb.getList(parts[0], parts[1], function (error, list) {
mailboxdb.getGroup(parts[0], parts[1], function (error, group) {
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.toString()));
@@ -382,9 +382,9 @@ function mailingListSearch(req, res, next) {
attributes: {
objectclass: ['mailGroup'],
objectcategory: 'mailGroup',
cn: `${list.name}@${list.domain}`, // fully qualified
mail: `${list.name}@${list.domain}`,
mgrpRFC822MailMember: list.members // fully qualified
cn: `${group.name}@${group.domain}`, // fully qualified
mail: `${group.name}@${group.domain}`,
mgrpRFC822MailMember: group.members.map(function (m) { return `${m}@${group.domain}`; })
}
};
@@ -640,7 +640,7 @@ function start(callback) {
res.end();
});
gServer.listen(constants.LDAP_PORT, '0.0.0.0', callback);
gServer.listen(config.get('ldapPort'), '0.0.0.0', callback);
}
function stop(callback) {
+1 -5
View File
@@ -22,11 +22,7 @@ Locker.prototype.OP_APPTASK = 'apptask';
Locker.prototype.lock = function (operation) {
assert.strictEqual(typeof operation, 'string');
if (this._operation !== null) {
let error = new Error(`Locked for ${this._operation}`);
error.operation = this._operation;
return error;
}
if (this._operation !== null) return new Error('Already locked for ' + this._operation);
this._operation = operation;
++this._lockDepth;
+8 -20
View File
@@ -1,23 +1,11 @@
# Generated by apptask
# Generated by apptask for the /run mount
# keep upto 7 rotated logs. rotation triggered daily or ahead of time if size is > 1M
<%= volumePath %>/*.log <%= volumePath %>/*/*.log <%= volumePath %>/*/*/*.log {
rotate 7
daily
compress
maxsize 1M
missingok
delaycompress
copytruncate
rotate 7
daily
compress
maxsize=1M
missingok
delaycompress
copytruncate
}
/home/yellowtent/platformdata/logs/<%= appId %>/*.log {
# only keep one rotated file, we currently do not send that over the api
rotate 1
size 10M
missingok
# we never compress so we can simply tail the files
nocompress
copytruncate
}
+28 -24
View File
@@ -53,6 +53,7 @@ exports = module.exports = {
var assert = require('assert'),
async = require('async'),
config = require('./config.js'),
constants = require('./constants.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:mail'),
@@ -70,13 +71,11 @@ var assert = require('assert'),
paths = require('./paths.js'),
reverseProxy = require('./reverseproxy.js'),
safe = require('safetydance'),
settings = require('./settings.js'),
shell = require('./shell.js'),
smtpTransport = require('nodemailer-smtp-transport'),
sysinfo = require('./sysinfo.js'),
users = require('./users.js'),
util = require('util'),
validator = require('validator'),
_ = require('underscore');
const DNS_OPTIONS = { timeout: 5000 };
@@ -127,6 +126,7 @@ function checkOutboundPort25(callback) {
'smtp.gmail.com',
'smtp.live.com',
'smtp.mail.yahoo.com',
'smtp.o2.ie',
'smtp.comcast.net',
'smtp.1und1.de',
]);
@@ -263,14 +263,14 @@ function checkSpf(domain, mailFqdn, callback) {
let txtRecord = txtRecords[i].join(''); // https://agari.zendesk.com/hc/en-us/articles/202952749-How-long-can-my-SPF-record-be-
if (txtRecord.indexOf('v=spf1 ') !== 0) continue; // not SPF
spf.value = txtRecord;
spf.status = spf.value.indexOf(' a:' + settings.adminFqdn()) !== -1;
spf.status = spf.value.indexOf(' a:' + config.adminFqdn()) !== -1;
break;
}
if (spf.status) {
spf.expected = spf.value;
} else if (i !== txtRecords.length) {
spf.expected = 'v=spf1 a:' + settings.adminFqdn() + ' ' + spf.value.slice('v=spf1 '.length);
spf.expected = 'v=spf1 a:' + config.adminFqdn() + ' ' + spf.value.slice('v=spf1 '.length);
}
callback(null, spf);
@@ -497,7 +497,7 @@ function getStatus(domain, callback) {
};
}
const mailFqdn = settings.mailFqdn();
const mailFqdn = config.mailFqdn();
getDomain(domain, function (error, mailDomain) {
if (error) return callback(error);
@@ -568,16 +568,15 @@ function checkConfiguration(callback) {
markdownMessage += '\n\n';
});
if (markdownMessage) markdownMessage += 'Email Status is checked every 30 minutes.\n See the [troubleshooting docs](https://cloudron.io/documentation/troubleshooting/#mail-dns) for more information.\n';
if (markdownMessage) markdownMessage += 'Email Status is checked every 30 minutes\n See the [troubleshooting docs](https://cloudron.io/documentation/troubleshooting/#mail-dns) for more information.\n';
callback(null, markdownMessage); // empty message means all status checks succeeded
});
});
}
function createMailConfig(mailFqdn, mailDomain, callback) {
function createMailConfig(mailFqdn, callback) {
assert.strictEqual(typeof mailFqdn, 'string');
assert.strictEqual(typeof mailDomain, 'string');
assert.strictEqual(typeof callback, 'function');
debug('createMailConfig: generating mail config');
@@ -588,9 +587,8 @@ function createMailConfig(mailFqdn, mailDomain, callback) {
const mailOutDomains = mailDomains.filter(d => d.relay.provider !== 'noop').map(d => d.domain).join(',');
const mailInDomains = mailDomains.filter(function (d) { return d.enabled; }).map(function (d) { return d.domain; }).join(',');
// mail_domain is used for SRS
if (!safe.fs.writeFileSync(path.join(paths.ADDON_CONFIG_DIR, 'mail/mail.ini'),
`mail_in_domains=${mailInDomains}\nmail_out_domains=${mailOutDomains}\nmail_server_name=${mailFqdn}\nmail_domain=${mailDomain}\n\n`, 'utf8')) {
`mail_in_domains=${mailInDomains}\nmail_out_domains=${mailOutDomains}\nmail_server_name=${mailFqdn}\n\n`, 'utf8')) {
return callback(new Error('Could not create mail var file:' + safe.error.message));
}
@@ -657,7 +655,7 @@ function configureMail(mailFqdn, mailDomain, callback) {
shell.exec('startMail', 'docker rm -f mail || true', function (error) {
if (error) return callback(error);
createMailConfig(mailFqdn, mailDomain, function (error, allowInbound) {
createMailConfig(mailFqdn, function (error, allowInbound) {
if (error) return callback(error);
var ports = allowInbound ? '-p 587:2525 -p 993:9993 -p 4190:4190 -p 25:2525' : '';
@@ -693,8 +691,8 @@ function restartMail(callback) {
if (process.env.BOX_ENV === 'test' && !process.env.TEST_CREATE_INFRA) return callback();
debug(`restartMail: restarting mail container with ${settings.mailFqdn()} ${settings.adminDomain()}`);
configureMail(settings.mailFqdn(), settings.adminDomain(), callback);
debug(`restartMail: restarting mail container with ${config.mailFqdn()} ${config.adminDomain()}`);
configureMail(config.mailFqdn(), config.adminDomain(), callback);
}
function restartMailIfActivated(callback) {
@@ -885,14 +883,14 @@ function setDnsRecords(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
upsertDnsRecords(domain, settings.mailFqdn(), callback);
upsertDnsRecords(domain, config.mailFqdn(), callback);
}
function onMailFqdnChanged(callback) {
assert.strictEqual(typeof callback, 'function');
const mailFqdn = settings.mailFqdn(),
mailDomain = settings.adminDomain();
const mailFqdn = config.mailFqdn(),
mailDomain = config.adminDomain();
domains.getAll(function (error, allDomains) {
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
@@ -911,7 +909,7 @@ function addDomain(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
const dkimSelector = domain === settings.adminDomain() ? 'cloudron' : ('cloudron-' + settings.adminDomain().replace(/\./g, ''));
const dkimSelector = domain === config.adminDomain() ? 'cloudron' : ('cloudron-' + config.adminDomain().replace(/\./g, ''));
maildb.add(domain, { dkimSelector }, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new MailError(MailError.ALREADY_EXISTS, 'Domain already exists'));
@@ -919,7 +917,7 @@ function addDomain(domain, callback) {
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
async.series([
upsertDnsRecords.bind(null, domain, settings.mailFqdn()), // do this first to ensure DKIM keys
upsertDnsRecords.bind(null, domain, config.mailFqdn()), // do this first to ensure DKIM keys
restartMailIfActivated
], NOOP_CALLBACK); // do these asynchronously
@@ -931,7 +929,7 @@ function removeDomain(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
if (domain === settings.adminDomain()) return callback(new MailError(MailError.IN_USE));
if (domain === config.adminDomain()) return callback(new MailError(MailError.IN_USE));
maildb.del(domain, function (error) {
if (error && error.reason === DatabaseError.IN_USE) return callback(new MailError(MailError.IN_USE));
@@ -1206,7 +1204,7 @@ function getLists(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
mailboxdb.getLists(domain, function (error, result) {
mailboxdb.listGroups(domain, function (error, result) {
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
callback(null, result);
@@ -1218,7 +1216,7 @@ function getList(domain, listName, callback) {
assert.strictEqual(typeof listName, 'string');
assert.strictEqual(typeof callback, 'function');
mailboxdb.getList(listName, domain, function (error, result) {
mailboxdb.getGroup(listName, domain, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, 'no such list'));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
@@ -1239,10 +1237,13 @@ function addList(name, domain, members, auditSource, callback) {
if (error) return callback(error);
for (var i = 0; i < members.length; i++) {
if (!validator.isEmail(members[i])) return callback(new MailError(MailError.BAD_FIELD, 'Invalid mail member: ' + members[i]));
members[i] = members[i].toLowerCase();
error = validateName(members[i]);
if (error) return callback(error);
}
mailboxdb.addList(name, domain, members, function (error) {
mailboxdb.addGroup(name, domain, members, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new MailError(MailError.ALREADY_EXISTS, 'list already exits'));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
@@ -1264,7 +1265,10 @@ function updateList(name, domain, members, callback) {
if (error) return callback(error);
for (var i = 0; i < members.length; i++) {
if (!validator.isEmail(members[i])) return callback(new MailError(MailError.BAD_FIELD, 'Invalid email: ' + members[i]));
members[i] = members[i].toLowerCase();
error = validateName(members[i]);
if (error) return callback(error);
}
mailboxdb.updateList(name, domain, members, function (error) {
+6 -6
View File
@@ -2,7 +2,7 @@
exports = module.exports = {
addMailbox: addMailbox,
addList: addList,
addGroup: addGroup,
updateMailboxOwner: updateMailboxOwner,
updateList: updateList,
@@ -10,10 +10,10 @@ exports = module.exports = {
listAliases: listAliases,
listMailboxes: listMailboxes,
getLists: getLists,
listGroups: listGroups,
getMailbox: getMailbox,
getList: getList,
getGroup: getGroup,
getAlias: getAlias,
getAliasesForName: getAliasesForName,
@@ -75,7 +75,7 @@ function updateMailboxOwner(name, domain, ownerId, callback) {
});
}
function addList(name, domain, members, callback) {
function addGroup(name, domain, members, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert(Array.isArray(members));
@@ -197,7 +197,7 @@ function listMailboxes(domain, callback) {
});
}
function getLists(domain, callback) {
function listGroups(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -211,7 +211,7 @@ function getLists(domain, callback) {
});
}
function getList(name, domain, callback) {
function getGroup(name, domain, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
+13 -13
View File
@@ -24,7 +24,7 @@ exports = module.exports = {
};
var assert = require('assert'),
constants = require('./constants.js'),
config = require('./config.js'),
custom = require('./custom.js'),
debug = require('debug')('box:mailer'),
docker = require('./docker.js').connection,
@@ -54,7 +54,7 @@ function getMailConfig(callback) {
callback(null, {
cloudronName: cloudronName,
notificationFrom: `"${cloudronName}" <no-reply@${settings.adminDomain()}>`
notificationFrom: `"${cloudronName}" <no-reply@${config.adminDomain()}>`
});
});
}
@@ -84,9 +84,9 @@ function sendMail(mailOptions, callback) {
var transport = nodemailer.createTransport(smtpTransport({
host: mailServerIp,
port: constants.INTERNAL_SMTP_PORT,
port: config.get('smtpPort'),
auth: {
user: mailOptions.authUser || `no-reply@${settings.adminDomain()}`,
user: mailOptions.authUser || `no-reply@${config.adminDomain()}`,
pass: relayToken
}
}));
@@ -146,11 +146,11 @@ function sendInvite(user, invitor) {
var templateData = {
user: user,
webadminUrl: settings.adminOrigin(),
setupLink: `${settings.adminOrigin()}/api/v1/session/account/setup.html?reset_token=${user.resetToken}&email=${encodeURIComponent(user.email)}`,
webadminUrl: config.adminOrigin(),
setupLink: `${config.adminOrigin()}/api/v1/session/account/setup.html?reset_token=${user.resetToken}&email=${encodeURIComponent(user.email)}`,
invitor: invitor,
cloudronName: mailConfig.cloudronName,
cloudronAvatarUrl: settings.adminOrigin() + '/api/v1/cloudron/avatar'
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
};
var templateDataText = JSON.parse(JSON.stringify(templateData));
@@ -183,7 +183,7 @@ function userAdded(mailTo, user) {
var templateData = {
user: user,
cloudronName: mailConfig.cloudronName,
cloudronAvatarUrl: settings.adminOrigin() + '/api/v1/cloudron/avatar'
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
};
var templateDataText = JSON.parse(JSON.stringify(templateData));
@@ -233,9 +233,9 @@ function passwordReset(user) {
var templateData = {
user: user,
resetLink: `${settings.adminOrigin()}/api/v1/session/password/reset.html?reset_token=${user.resetToken}&email=${encodeURIComponent(user.email)}`,
resetLink: `${config.adminOrigin()}/api/v1/session/password/reset.html?reset_token=${user.resetToken}&email=${encodeURIComponent(user.email)}`,
cloudronName: mailConfig.cloudronName,
cloudronAvatarUrl: settings.adminOrigin() + '/api/v1/cloudron/avatar'
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
};
var templateDataText = JSON.parse(JSON.stringify(templateData));
@@ -314,7 +314,7 @@ function appUpdated(mailTo, app, callback) {
changelog: app.manifest.changelog,
changelogHTML: converter.makeHtml(app.manifest.changelog),
cloudronName: mailConfig.cloudronName,
cloudronAvatarUrl: settings.adminOrigin() + '/api/v1/cloudron/avatar'
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
};
var templateDataText = JSON.parse(JSON.stringify(templateData));
@@ -350,11 +350,11 @@ function appUpdatesAvailable(mailTo, apps, hasSubscription, callback) {
});
var templateData = {
webadminUrl: settings.adminOrigin(),
webadminUrl: config.adminOrigin(),
hasSubscription: hasSubscription,
apps: apps,
cloudronName: mailConfig.cloudronName,
cloudronAvatarUrl: settings.adminOrigin() + '/api/v1/cloudron/avatar'
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
};
var templateDataText = JSON.parse(JSON.stringify(templateData));
+2 -2
View File
@@ -5,7 +5,7 @@ exports = module.exports = {
};
var assert = require('assert'),
constants = require('./constants.js'),
config = require('./config.js'),
dns = require('dns'),
_ = require('underscore');
@@ -24,7 +24,7 @@ function resolve(hostname, rrtype, options, callback) {
options = _.extend({ }, DEFAULT_OPTIONS, options);
// Only use unbound on a Cloudron
if (constants.CLOUDRON) resolver.setServers([ options.server ]);
if (config.CLOUDRON) resolver.setServers([ options.server ]);
// should callback with ECANCELLED but looks like we might hit https://github.com/nodejs/node/issues/14814
const timerId = setTimeout(resolver.cancel.bind(resolver), options.timeout || 5000);
+7 -33
View File
@@ -24,15 +24,13 @@ exports = module.exports = {
let assert = require('assert'),
async = require('async'),
auditsource = require('./auditsource.js'),
changelog = require('./changelog.js'),
config = require('./config.js'),
custom = require('./custom.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:notifications'),
eventlog = require('./eventlog.js'),
mailer = require('./mailer.js'),
notificationdb = require('./notificationdb.js'),
settings = require('./settings.js'),
users = require('./users.js'),
util = require('util');
@@ -146,7 +144,7 @@ function userAdded(performedBy, eventId, user, callback) {
actionForAllAdmins([ performedBy, user.id ], function (admin, done) {
mailer.userAdded(admin.email, user);
add(admin.id, eventId, `User '${user.displayName}' added`, `User '${user.username || user.email || user.fallbackEmail}' was added.`, done);
add(admin.id, eventId, 'User added', `User ${user.fallbackEmail} was added`, done);
}, callback);
}
@@ -158,7 +156,7 @@ function userRemoved(performedBy, eventId, user, callback) {
actionForAllAdmins([ performedBy, user.id ], function (admin, done) {
mailer.userRemoved(admin.email, user);
add(admin.id, eventId, `User '${user.displayName}' removed`, `User '${user.username || user.email || user.fallbackEmail}' was removed.`, done);
add(admin.id, eventId, 'User removed', `User ${user.username || user.email || user.fallbackEmail} was removed`, done);
}, callback);
}
@@ -169,7 +167,7 @@ function adminChanged(performedBy, eventId, user, callback) {
actionForAllAdmins([ performedBy, user.id ], function (admin, done) {
mailer.adminChanged(admin.email, user, user.admin);
add(admin.id, eventId, `User '${user.displayName} ' ${user.admin ? 'is now an admin' : 'is no more an admin'}`, `User '${user.username || user.email || user.fallbackEmail}' ${user.admin ? 'is now an admin' : 'is no more an admin'}.`, done);
add(admin.id, eventId, 'Admin status change', `User ${user.username || user.email || user.fallbackEmail} ${user.admin ? 'is now an admin' : 'is no more an admin'}`, done);
}, callback);
}
@@ -238,13 +236,8 @@ function appUpdated(eventId, app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
const tmp = app.manifest.description.match(/<upstream>(.*)<\/upstream>/i);
const upstreamVersion = (tmp && tmp[1]) ? tmp[1] : '';
const title = upstreamVersion ? `${app.manifest.title} at ${app.fqdn} updated to ${upstreamVersion} (package version ${app.manifest.version})`
: `${app.manifest.title} at ${app.fqdn} updated to package version ${app.manifest.version}`;
actionForAllAdmins([], function (admin, done) {
add(admin.id, eventId, title, `The application ${app.manifest.title} installed at https://${app.fqdn} was updated.\n\nChangelog:\n${app.manifest.changelog}\n`, function (error) {
add(admin.id, eventId, `App ${app.fqdn} updated`, `The application ${app.manifest.title} installed at https://${app.fqdn} was updated to package version ${app.manifest.version}.`, function (error) {
if (error) return callback(error);
mailer.appUpdated(admin.email, app, function (error) {
@@ -255,19 +248,6 @@ function appUpdated(eventId, app, callback) {
}, callback);
}
function boxUpdated(oldVersion, newVersion, callback) {
assert.strictEqual(typeof oldVersion, 'string');
assert.strictEqual(typeof newVersion, 'string');
assert.strictEqual(typeof callback, 'function');
const changes = changelog.getChanges(newVersion);
const changelogMarkdown = changes.map((m) => `* ${m}\n`).join('');
actionForAllAdmins([], function (admin, done) {
add(admin.id, null, `Cloudron updated to v${newVersion}`, `Cloudron was updated from v${oldVersion} to v${newVersion}.\n\nChangelog:\n${changelogMarkdown}\n`, done);
}, callback);
}
function certificateRenewalError(eventId, vhost, errorMessage, callback) {
assert.strictEqual(typeof eventId, 'string');
assert.strictEqual(typeof vhost, 'string');
@@ -289,11 +269,11 @@ function backupFailed(eventId, taskId, errorMessage, callback) {
assert.strictEqual(typeof errorMessage, 'string');
assert.strictEqual(typeof callback, 'function');
if (custom.spec().alerts.email) mailer.backupFailed(custom.spec().alerts.email, errorMessage, `${settings.adminOrigin()}/logs.html?taskId=${taskId}`);
if (custom.spec().alerts.email) mailer.backupFailed(custom.spec().alerts.email, errorMessage, `${config.adminOrigin()}/logs.html?taskId=${taskId}`);
if (!custom.spec().alerts.notifyCloudronAdmins) return callback();
actionForAllAdmins([], function (admin, callback) {
mailer.backupFailed(admin.email, errorMessage, `${settings.adminOrigin()}/logs.html?taskId=${taskId}`);
mailer.backupFailed(admin.email, errorMessage, `${config.adminOrigin()}/logs.html?taskId=${taskId}`);
add(admin.id, eventId, 'Failed to backup', `Backup failed: ${errorMessage}. Logs are available [here](/logs.html?taskId=${taskId}). Will be retried in 4 hours`, callback);
}, callback);
}
@@ -346,9 +326,6 @@ function onEvent(id, action, source, data, callback) {
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof callback, 'function');
// external ldap syncer does not generate notifications - FIXME username might be an issue here
if (source.username === auditsource.EXTERNAL_LDAP_TASK.username) return callback();
switch (action) {
case eventlog.ACTION_USER_ADD:
return userAdded(source.userId, id, data.user, callback);
@@ -381,9 +358,6 @@ function onEvent(id, action, source, data, callback) {
if (!data.errorMessage || source.username !== 'cron') return callback();
return backupFailed(id, data.taskId, data.errorMessage, callback); // only notify for automated backups
case eventlog.ACTION_UPDATE_FINISH:
return boxUpdated(data.oldVersion, data.newVersion, callback);
default:
return callback();
}
+1 -1
View File
@@ -61,7 +61,7 @@ app.controller('Controller', ['$scope', function ($scope) {
<div class="control-label" ng-show="setupForm.password.$dirty && setupForm.password.$invalid">
<small ng-show="setupForm.password.$dirty && setupForm.password.$invalid">Password must be atleast 8 characters</small>
</div>
<input type="password" class="form-control" ng-model="password" name="password" ng-pattern="/^.{8,}$/" required>
<input type="password" class="form-control" ng-model="password" name="password" ng-pattern="/^.{8,30}$/" required>
</div>
<div class="form-group" ng-class="{ 'has-error': (setupForm.passwordRepeat.$dirty && (password !== passwordRepeat)) }">
+2
View File
@@ -1,6 +1,8 @@
<footer class="text-center">
<span class="text-muted">&copy; 2016-19 <a href="https://cloudron.io" target="_blank">Cloudron</a></span>
<span class="text-muted"><a href="https://twitter.com/cloudron_io" target="_blank">Twitter <i class="fa fa-twitter"></i></a></span>
<span class="text-muted"><a href="https://chat.cloudron.io" target="_blank">Chat <i class="fa fa-comments"></i></a></span>
</footer>
</div>
+1 -1
View File
@@ -13,7 +13,7 @@
<link href="<%= adminOrigin %>/theme.css" rel="stylesheet">
<!-- Custom Fonts -->
<link href="<%= adminOrigin %>/3rdparty/fontawesome/css/all.min.css" rel="stylesheet" rel="stylesheet" type="text/css">
<link href="<%= adminOrigin %>/3rdparty/css/font-awesome.min.css" rel="stylesheet" rel="stylesheet" type="text/css">
<!-- jQuery-->
<script src="<%= adminOrigin %>/3rdparty/js/jquery.min.js"></script>
+28 -43
View File
@@ -1,60 +1,45 @@
'use strict';
var constants = require('./constants.js'),
var config = require('./config.js'),
path = require('path');
function baseDir() {
const homeDir = process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE;
if (constants.CLOUDRON) return homeDir;
if (constants.TEST) return path.join(homeDir, '.cloudron_test');
// cannot reach
}
// keep these values in sync with start.sh
exports = module.exports = {
baseDir: baseDir,
CLOUDRON_DEFAULT_AVATAR_FILE: path.join(__dirname + '/../assets/avatar.png'),
INFRA_VERSION_FILE: path.join(baseDir(), 'platformdata/INFRA_VERSION'),
INFRA_VERSION_FILE: path.join(config.baseDir(), 'platformdata/INFRA_VERSION'),
LICENSE_FILE: '/etc/cloudron/LICENSE',
PROVIDER_FILE: '/etc/cloudron/PROVIDER',
CUSTOM_FILE: '/etc/cloudron/custom.yml',
PLATFORM_DATA_DIR: path.join(baseDir(), 'platformdata'),
APPS_DATA_DIR: path.join(baseDir(), 'appsdata'),
BOX_DATA_DIR: path.join(baseDir(), 'boxdata'),
PLATFORM_DATA_DIR: path.join(config.baseDir(), 'platformdata'),
APPS_DATA_DIR: path.join(config.baseDir(), 'appsdata'),
BOX_DATA_DIR: path.join(config.baseDir(), 'boxdata'),
CUSTOM_FILE: path.join(baseDir(), 'boxdata/custom.yml'),
ACME_CHALLENGES_DIR: path.join(baseDir(), 'platformdata/acme'),
ADDON_CONFIG_DIR: path.join(baseDir(), 'platformdata/addons'),
COLLECTD_APPCONFIG_DIR: path.join(baseDir(), 'platformdata/collectd/collectd.conf.d'),
LOGROTATE_CONFIG_DIR: path.join(baseDir(), 'platformdata/logrotate.d'),
NGINX_CONFIG_DIR: path.join(baseDir(), 'platformdata/nginx'),
NGINX_APPCONFIG_DIR: path.join(baseDir(), 'platformdata/nginx/applications'),
NGINX_CERT_DIR: path.join(baseDir(), 'platformdata/nginx/cert'),
BACKUP_INFO_DIR: path.join(baseDir(), 'platformdata/backup'),
UPDATE_DIR: path.join(baseDir(), 'platformdata/update'),
SNAPSHOT_INFO_FILE: path.join(baseDir(), 'platformdata/backup/snapshot-info.json'),
DYNDNS_INFO_FILE: path.join(baseDir(), 'platformdata/dyndns-info.json'),
VERSION_FILE: path.join(baseDir(), 'platformdata/VERSION'),
SESSION_SECRET_FILE: path.join(baseDir(), 'boxdata/session.secret'),
SESSION_DIR: path.join(baseDir(), 'platformdata/sessions'),
ACME_CHALLENGES_DIR: path.join(config.baseDir(), 'platformdata/acme'),
ADDON_CONFIG_DIR: path.join(config.baseDir(), 'platformdata/addons'),
COLLECTD_APPCONFIG_DIR: path.join(config.baseDir(), 'platformdata/collectd/collectd.conf.d'),
LOGROTATE_CONFIG_DIR: path.join(config.baseDir(), 'platformdata/logrotate.d'),
NGINX_CONFIG_DIR: path.join(config.baseDir(), 'platformdata/nginx'),
NGINX_APPCONFIG_DIR: path.join(config.baseDir(), 'platformdata/nginx/applications'),
NGINX_CERT_DIR: path.join(config.baseDir(), 'platformdata/nginx/cert'),
BACKUP_INFO_DIR: path.join(config.baseDir(), 'platformdata/backup'),
UPDATE_DIR: path.join(config.baseDir(), 'platformdata/update'),
SNAPSHOT_INFO_FILE: path.join(config.baseDir(), 'platformdata/backup/snapshot-info.json'),
DYNDNS_INFO_FILE: path.join(config.baseDir(), 'platformdata/dyndns-info.json'),
// this is not part of appdata because an icon may be set before install
APP_ICONS_DIR: path.join(baseDir(), 'boxdata/appicons'),
MAIL_DATA_DIR: path.join(baseDir(), 'boxdata/mail'),
ACME_ACCOUNT_KEY_FILE: path.join(baseDir(), 'boxdata/acme/acme.key'),
APP_CERTS_DIR: path.join(baseDir(), 'boxdata/certs'),
CLOUDRON_AVATAR_FILE: path.join(baseDir(), 'boxdata/avatar.png'),
UPDATE_CHECKER_FILE: path.join(baseDir(), 'boxdata/updatechecker.json'),
APP_ICONS_DIR: path.join(config.baseDir(), 'boxdata/appicons'),
MAIL_DATA_DIR: path.join(config.baseDir(), 'boxdata/mail'),
ACME_ACCOUNT_KEY_FILE: path.join(config.baseDir(), 'boxdata/acme/acme.key'),
APP_CERTS_DIR: path.join(config.baseDir(), 'boxdata/certs'),
CLOUDRON_AVATAR_FILE: path.join(config.baseDir(), 'boxdata/avatar.png'),
UPDATE_CHECKER_FILE: path.join(config.baseDir(), 'boxdata/updatechecker.json'),
LOG_DIR: path.join(baseDir(), 'platformdata/logs'),
TASKS_LOG_DIR: path.join(baseDir(), 'platformdata/logs/tasks'),
CRASH_LOG_DIR: path.join(baseDir(), 'platformdata/logs/crash'),
LOG_DIR: path.join(config.baseDir(), 'platformdata/logs'),
TASKS_LOG_DIR: path.join(config.baseDir(), 'platformdata/logs/tasks'),
CRASH_LOG_DIR: path.join(config.baseDir(), 'platformdata/logs/crash'),
// this pattern is for the cloudron logs API route to work
BACKUP_LOG_FILE: path.join(baseDir(), 'platformdata/logs/backup/app.log'),
UPDATER_LOG_FILE: path.join(baseDir(), 'platformdata/logs/updater/app.log')
BACKUP_LOG_FILE: path.join(config.baseDir(), 'platformdata/logs/backup/app.log'),
UPDATER_LOG_FILE: path.join(config.baseDir(), 'platformdata/logs/updater/app.log')
};
+3 -4
View File
@@ -23,7 +23,7 @@ var addons = require('./addons.js'),
settings = require('./settings.js'),
sftp = require('./sftp.js'),
shell = require('./shell.js'),
tasks = require('./tasks.js'),
taskmanager = require('./taskmanager.js'),
_ = require('underscore');
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
@@ -75,14 +75,13 @@ function start(callback) {
}
function stop(callback) {
tasks.stopAllTasks(callback);
taskmanager.pauseTasks(callback);
}
function onPlatformReady() {
debug('onPlatformReady: platform is ready');
exports._isReady = true;
apps.schedulePendingTasks(NOOP_CALLBACK);
taskmanager.resumeTasks();
applyPlatformConfig(NOOP_CALLBACK);
pruneInfraImages(NOOP_CALLBACK);
+18 -12
View File
@@ -17,6 +17,7 @@ var appstore = require('./appstore.js'),
async = require('async'),
backups = require('./backups.js'),
BackupsError = require('./backups.js').BackupsError,
config = require('./config.js'),
constants = require('./constants.js'),
clients = require('./clients.js'),
cloudron = require('./cloudron.js'),
@@ -31,7 +32,6 @@ var appstore = require('./appstore.js'),
semver = require('semver'),
settings = require('./settings.js'),
superagent = require('superagent'),
sysinfo = require('./sysinfo.js'),
users = require('./users.js'),
UsersError = users.UsersError,
tld = require('tldjs'),
@@ -113,9 +113,11 @@ function unprovision(callback) {
debug('unprovision');
config.setAdminDomain('');
config.setAdminFqdn('');
// TODO: also cancel any existing configureWebadmin task
async.series([
settings.setAdmin.bind(null, '', ''),
mail.clearDomains,
domains.clear
], callback);
@@ -168,8 +170,9 @@ function setup(dnsConfig, backupConfig, auditSource, callback) {
async.series([
autoRegister.bind(null, domain),
domains.prepareDashboardDomain.bind(null, domain, auditSource, (progress) => setProgress('setup', progress.message, NOOP_CALLBACK)),
cloudron.setDashboardDomain.bind(null, domain, auditSource),
mail.addDomain.bind(null, domain), // this relies on settings.mailFqdn() and settings.adminDomain()
cloudron.setDashboardDomain.bind(null, domain, auditSource), // this sets up the config.fqdn()
mail.addDomain.bind(null, domain), // this relies on config.mailFqdn() and config.adminDomain()
setProgress.bind(null, 'setup', 'Applying auto-configuration'),
(next) => { if (!backupConfig) return next(); settings.setBackupConfig(backupConfig, next); },
setProgress.bind(null, 'setup', 'Done'),
eventlog.add.bind(null, eventlog.ACTION_PROVISION, auditSource, { })
@@ -249,7 +252,7 @@ function restore(backupConfig, backupId, version, auditSource, callback) {
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(constants.VERSION) !== semver.major(version) || semver.minor(constants.VERSION) !== semver.minor(version)) return callback(new ProvisionError(ProvisionError.BAD_STATE, `Run cloudron-setup with --version ${version} to restore from this backup`));
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 (gProvisionStatus.setup.active || gProvisionStatus.restore.active) return callback(new ProvisionError(ProvisionError.BAD_STATE, 'Already setting up or restoring'));
@@ -263,7 +266,7 @@ function restore(backupConfig, backupId, version, auditSource, callback) {
users.isActivated(function (error, activated) {
if (error) return done(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
if (activated) return done(new ProvisionError(ProvisionError.ALREADY_PROVISIONED, 'Already activated. Restore with a fresh Cloudron installation.'));
if (activated) return done(new ProvisionError(ProvisionError.ALREADY_PROVISIONED, 'Already activated'));
backups.testConfig(backupConfig, function (error) {
if (error && error.reason === BackupsError.BAD_FIELD) return done(new ProvisionError(ProvisionError.BAD_FIELD, error.message));
@@ -277,8 +280,11 @@ function restore(backupConfig, backupId, version, auditSource, callback) {
async.series([
setProgress.bind(null, 'restore', 'Downloading backup'),
backups.restore.bind(null, backupConfig, backupId, (progress) => setProgress('restore', progress.message, NOOP_CALLBACK)),
cloudron.setupDashboard.bind(null, auditSource, (progress) => setProgress('restore', progress.message, NOOP_CALLBACK)),
settings.setBackupConfig.bind(null, backupConfig), // update with the latest backupConfig
setProgress.bind(null, 'restore', 'Applying auto-configuration'),
// 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()),
eventlog.add.bind(null, eventlog.ACTION_RESTORE, auditSource, { backupId }),
], function (error) {
gProvisionStatus.restore.active = false;
@@ -300,11 +306,11 @@ function getStatus(callback) {
if (error) return callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
callback(null, _.extend({
version: constants.VERSION,
apiServerOrigin: settings.apiServerOrigin(), // used by CaaS tool
provider: sysinfo.provider(),
version: config.version(),
apiServerOrigin: config.apiServerOrigin(), // used by CaaS tool
provider: config.provider(),
cloudronName: cloudronName,
adminFqdn: settings.adminDomain() ? settings.adminFqdn() : null,
adminFqdn: config.adminDomain() ? config.adminFqdn() : null,
activated: activated,
}, gProvisionStatus));
});
+14 -37
View File
@@ -23,7 +23,6 @@ exports = module.exports = {
unconfigureApp: unconfigureApp,
writeAdminConfig: writeAdminConfig,
writeAppConfig: writeAppConfig,
reload: reload,
removeAppConfigs: removeAppConfigs,
@@ -37,6 +36,7 @@ var acme2 = require('./cert/acme2.js'),
assert = require('assert'),
async = require('async'),
caas = require('./cert/caas.js'),
config = require('./config.js'),
constants = require('./constants.js'),
crypto = require('crypto'),
debug = require('debug')('box:reverseproxy'),
@@ -51,9 +51,7 @@ var acme2 = require('./cert/acme2.js'),
paths = require('./paths.js'),
rimraf = require('rimraf'),
safe = require('safetydance'),
settings = require('./settings.js'),
shell = require('./shell.js'),
sysinfo = require('./sysinfo.js'),
users = require('./users.js'),
util = require('util');
@@ -92,8 +90,8 @@ function getCertApi(domainObject, callback) {
var api = domainObject.tlsConfig.provider === 'caas' ? caas : acme2;
var options = { prod: false, performHttpAuthorization: false, wildcard: false, email: '' };
if (domainObject.tlsConfig.provider !== 'caas') {
options.prod = domainObject.tlsConfig.provider.match(/.*-prod/) !== null; // matches 'le-prod' or 'letsencrypt-prod'
if (domainObject.tlsConfig.provider !== 'caas') { // matches 'le-prod' or 'letsencrypt-prod'
options.prod = domainObject.tlsConfig.provider.match(/.*-prod/) !== null;
options.performHttpAuthorization = domainObject.provider.match(/noop|manual|wildcard/) !== null;
options.wildcard = !!domainObject.tlsConfig.wildcard;
}
@@ -333,7 +331,7 @@ function notifyCertChanged(vhost, callback) {
assert.strictEqual(typeof vhost, 'string');
assert.strictEqual(typeof callback, 'function');
if (vhost !== settings.mailFqdn()) return callback();
if (vhost !== config.mailFqdn()) return callback();
mail.handleCertChanged(callback);
}
@@ -388,9 +386,9 @@ function writeAdminNginxConfig(bundle, configFileName, vhost, callback) {
var data = {
sourceDir: path.resolve(__dirname, '..'),
adminOrigin: settings.adminOrigin(),
adminOrigin: config.adminOrigin(),
vhost: vhost, // if vhost is empty it will become the default_server
hasIPv6: sysinfo.hasIPv6(),
hasIPv6: config.hasIPv6(),
endpoint: 'admin',
certFilePath: bundle.certFilePath,
keyFilePath: bundle.keyFilePath,
@@ -451,9 +449,9 @@ function writeAppNginxConfig(app, bundle, callback) {
var data = {
sourceDir: sourceDir,
adminOrigin: settings.adminOrigin(),
adminOrigin: config.adminOrigin(),
vhost: app.fqdn,
hasIPv6: sysinfo.hasIPv6(),
hasIPv6: config.hasIPv6(),
port: app.httpPort,
endpoint: endpoint,
certFilePath: bundle.certFilePath,
@@ -483,7 +481,7 @@ function writeAppRedirectNginxConfig(app, fqdn, bundle, callback) {
sourceDir: path.resolve(__dirname, '..'),
vhost: fqdn,
redirectTo: app.fqdn,
hasIPv6: sysinfo.hasIPv6(),
hasIPv6: config.hasIPv6(),
endpoint: 'redirect',
certFilePath: bundle.certFilePath,
keyFilePath: bundle.keyFilePath,
@@ -503,27 +501,6 @@ function writeAppRedirectNginxConfig(app, fqdn, bundle, callback) {
reload(callback);
}
function writeAppConfig(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
getCertificate(app.fqdn, app.domain, function (error, bundle) {
if (error) return callback(error);
writeAppNginxConfig(app, bundle, function (error) {
if (error) return callback(error);
async.eachSeries(app.alternateDomains, function (alternateDomain, iteratorDone) {
getCertificate(alternateDomain.fqdn, alternateDomain.domain, function (error, bundle) {
if (error) return iteratorDone(error);
writeAppRedirectNginxConfig(app, alternateDomain.fqdn, bundle, iteratorDone);
});
}, callback);
});
});
}
function configureApp(app, auditSource, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof auditSource, 'object');
@@ -535,11 +512,11 @@ function configureApp(app, auditSource, callback) {
writeAppNginxConfig(app, bundle, function (error) {
if (error) return callback(error);
async.eachSeries(app.alternateDomains, function (alternateDomain, iteratorDone) {
async.eachSeries(app.alternateDomains, function (alternateDomain, callback) {
ensureCertificate(alternateDomain.fqdn, alternateDomain.domain, auditSource, function (error, bundle) {
if (error) return iteratorDone(error);
if (error) return callback(error);
writeAppRedirectNginxConfig(app, alternateDomain.fqdn, bundle, iteratorDone);
writeAppRedirectNginxConfig(app, alternateDomain.fqdn, bundle, callback);
});
}, callback);
});
@@ -570,7 +547,7 @@ function renewCerts(options, auditSource, progressCallback, callback) {
var appDomains = [];
// add webadmin domain
appDomains.push({ domain: settings.adminDomain(), fqdn: settings.adminFqdn(), type: 'webadmin', nginxConfigFilename: path.join(paths.NGINX_APPCONFIG_DIR, `${settings.adminFqdn()}.conf`) });
appDomains.push({ domain: config.adminDomain(), fqdn: config.adminFqdn(), type: 'webadmin', nginxConfigFilename: path.join(paths.NGINX_APPCONFIG_DIR, `${config.adminFqdn()}.conf`) });
// add app main
allApps.forEach(function (app) {
@@ -600,7 +577,7 @@ function renewCerts(options, auditSource, progressCallback, callback) {
// reconfigure since the cert changed
var configureFunc;
if (appDomain.type === 'webadmin') configureFunc = writeAdminNginxConfig.bind(null, bundle, `${settings.adminFqdn()}.conf`, settings.adminFqdn());
if (appDomain.type === 'webadmin') configureFunc = writeAdminNginxConfig.bind(null, bundle, `${config.adminFqdn()}.conf`, config.adminFqdn());
else if (appDomain.type === 'main') configureFunc = writeAppNginxConfig.bind(null, appDomain.app, bundle);
else if (appDomain.type === 'alternate') configureFunc = writeAppRedirectNginxConfig.bind(null, appDomain.app, appDomain.fqdn, bundle);
else return iteratorCallback(new Error(`Unknown domain type for ${appDomain.fqdn}. This should never happen`));
+145 -280
View File
@@ -5,6 +5,7 @@ exports = module.exports = {
getApps: getApps,
getAppIcon: getAppIcon,
installApp: installApp,
configureApp: configureApp,
uninstallApp: uninstallApp,
restoreApp: restoreApp,
backupApp: backupApp,
@@ -12,22 +13,6 @@ exports = module.exports = {
getLogs: getLogs,
getLogStream: getLogStream,
listBackups: listBackups,
repairApp: repairApp,
setAccessRestriction: setAccessRestriction,
setLabel: setLabel,
setTags: setTags,
setIcon: setIcon,
setMemoryLimit: setMemoryLimit,
setAutomaticBackup: setAutomaticBackup,
setAutomaticUpdate: setAutomaticUpdate,
setRobotsTxt: setRobotsTxt,
setCertificate: setCertificate,
setDebugMode: setDebugMode,
setEnvironment: setEnvironment,
setMailbox: setMailbox,
setLocation: setLocation,
setDataDir: setDataDir,
stopApp: stopApp,
startApp: startApp,
@@ -36,6 +21,8 @@ exports = module.exports = {
cloneApp: cloneApp,
setOwner: setOwner,
uploadFile: uploadFile,
downloadFile: downloadFile
};
@@ -47,34 +34,17 @@ var apps = require('../apps.js'),
debug = require('debug')('box:routes/apps'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
paths = require('../paths.js'),
safe = require('safetydance'),
util = require('util'),
WebSocket = require('ws');
function toHttpError(appError) {
switch (appError.reason) {
case AppsError.NOT_FOUND:
return new HttpError(404, appError);
case AppsError.ALREADY_EXISTS:
case AppsError.BAD_STATE:
return new HttpError(409, appError);
case AppsError.BAD_FIELD:
return new HttpError(400, appError);
case AppsError.PLAN_LIMIT:
return new HttpError(402, appError);
case AppsError.EXTERNAL_ERROR:
return new HttpError(424, appError);
case AppsError.INTERNAL_ERROR:
default:
return new HttpError(500, appError);
}
}
function getApp(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
apps.get(req.params.id, function (error, app) {
if (error) return next(toHttpError(error));
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, apps.removeInternalFields(app)));
});
@@ -84,7 +54,7 @@ function getApps(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
apps.getAllByUser(req.user, function (error, allApps) {
if (error) return next(toHttpError(error));
if (error) return next(new HttpError(500, error));
allApps = allApps.map(apps.removeRestrictedFields);
@@ -95,17 +65,22 @@ function getApps(req, res, next) {
function getAppIcon(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
apps.getIconPath(req.params.id, { original: req.query.original }, function (error, iconPath) {
if (error) return next(toHttpError(error));
if (!req.query.original) {
const userIconPath = `${paths.APP_ICONS_DIR}/${req.params.id}.user.png`;
if (safe.fs.existsSync(userIconPath)) return res.sendFile(userIconPath);
}
res.sendFile(iconPath);
});
const appstoreIconPath = `${paths.APP_ICONS_DIR}/${req.params.id}.png`;
if (safe.fs.existsSync(appstoreIconPath)) return res.sendFile(appstoreIconPath);
return next(new HttpError(404, 'No such icon'));
}
function installApp(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
var data = req.body;
data.ownerId = req.user.id;
// atleast one
if ('manifest' in data && typeof data.manifest !== 'object') return next(new HttpError(400, 'manifest must be an object'));
@@ -152,241 +127,82 @@ function installApp(req, res, next) {
if (Object.keys(data.env).some(function (key) { return typeof data.env[key] !== 'string'; })) return next(new HttpError(400, 'env must contain values as strings'));
}
if ('overwriteDns' in req.body && typeof req.body.overwriteDns !== 'boolean') return next(new HttpError(400, 'overwriteDns must be boolean'));
debug('Installing app :%j', data);
apps.install(data, req.user, auditSource.fromRequest(req), function (error, result) {
if (error) return next(toHttpError(error));
apps.install(data, req.user, auditSource.fromRequest(req), function (error, app) {
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === AppsError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
if (error && error.reason === AppsError.PORT_RESERVED) return next(new HttpError(409, 'Port ' + error.message + ' is reserved.'));
if (error && error.reason === AppsError.PORT_CONFLICT) return next(new HttpError(409, 'Port ' + error.message + ' is already in use.'));
if (error && error.reason === AppsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === AppsError.PLAN_LIMIT) return next(new HttpError(402, error.message));
if (error && error.reason === AppsError.BAD_CERTIFICATE) return next(new HttpError(400, error.message));
if (error && error.reason === AppsError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, { id: result.id, taskId: result.taskId }));
next(new HttpSuccess(202, app));
});
}
function setAccessRestriction(req, res, next) {
function configureApp(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
if (typeof req.body.accessRestriction !== 'object') return next(new HttpError(400, 'accessRestriction must be an object'));
var data = req.body;
apps.setAccessRestriction(req.params.id, req.body.accessRestriction, auditSource.fromRequest(req), function (error) {
if (error) return next(toHttpError(error));
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'));
next(new HttpSuccess(200, {}));
});
}
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'));
function setLabel(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
// falsy values in cert and key unset the cert
if (data.key && typeof data.cert !== 'string') return next(new HttpError(400, 'cert must be a string'));
if (data.cert && typeof data.key !== 'string') return next(new HttpError(400, 'key must be a string'));
if (data.cert && !data.key) return next(new HttpError(400, 'key must be provided'));
if (!data.cert && data.key) return next(new HttpError(400, 'cert must be provided'));
if (typeof req.body.label !== 'string') return next(new HttpError(400, 'label must be a string'));
if ('memoryLimit' in data && typeof data.memoryLimit !== 'number') return next(new HttpError(400, 'memoryLimit is not a number'));
apps.setLabel(req.params.id, req.body.label, auditSource.fromRequest(req), function (error) {
if (error) return next(toHttpError(error));
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'));
next(new HttpSuccess(200, {}));
});
}
if (('debugMode' in data) && typeof data.debugMode !== 'object') return next(new HttpError(400, 'debugMode must be an object'));
function setTags(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
if (data.robotsTxt && typeof data.robotsTxt !== 'string') return next(new HttpError(400, 'robotsTxt must be a string'));
if (!Array.isArray(req.body.tags)) return next(new HttpError(400, 'tags must be an array'));
if (req.body.tags.some((t) => typeof t !== 'string')) return next(new HttpError(400, 'tags array must contain strings'));
apps.setTags(req.params.id, req.body.tags, auditSource.fromRequest(req), function (error) {
if (error) return next(toHttpError(error));
next(new HttpSuccess(200, {}));
});
}
function setIcon(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
if (req.body.icon !== null && typeof req.body.icon !== 'string') return next(new HttpError(400, 'icon is null or a base-64 image string'));
apps.setIcon(req.params.id, req.body.icon, auditSource.fromRequest(req), function (error) {
if (error) return next(toHttpError(error));
next(new HttpSuccess(200, {}));
});
}
function setMemoryLimit(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
if (typeof req.body.memoryLimit !== 'number') return next(new HttpError(400, 'memoryLimit is not a number'));
apps.setMemoryLimit(req.params.id, req.body.memoryLimit, auditSource.fromRequest(req), function (error, result) {
if (error) return next(toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
function setAutomaticBackup(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
if (typeof req.body.enable !== 'boolean') return next(new HttpError(400, 'enable must be a boolean'));
apps.setAutomaticBackup(req.params.id, req.body.enable, auditSource.fromRequest(req), function (error) {
if (error) return next(toHttpError(error));
next(new HttpSuccess(200, {}));
});
}
function setAutomaticUpdate(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
if (typeof req.body.enable !== 'boolean') return next(new HttpError(400, 'enable must be a boolean'));
apps.setAutomaticUpdate(req.params.id, req.body.enable, auditSource.fromRequest(req), function (error) {
if (error) return next(toHttpError(error));
next(new HttpSuccess(200, {}));
});
}
function setRobotsTxt(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
if (req.body.robotsTxt !== null && typeof req.body.robotsTxt !== 'string') return next(new HttpError(400, 'robotsTxt is not a string'));
apps.setRobotsTxt(req.params.id, req.body.robotsTxt, auditSource.fromRequest(req), function (error) {
if (error) return next(toHttpError(error));
next(new HttpSuccess(200, {}));
});
}
function setCertificate(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
if (req.body.key !== null && typeof req.body.cert !== 'string') return next(new HttpError(400, 'cert must be a string'));
if (req.body.cert !== null && typeof req.body.key !== 'string') return next(new HttpError(400, 'key must be a string'));
if (req.body.cert && !req.body.key) return next(new HttpError(400, 'key must be provided'));
if (!req.body.cert && req.body.key) return next(new HttpError(400, 'cert must be provided'));
apps.setCertificate(req.params.id, req.body, auditSource.fromRequest(req), function (error) {
if (error) return next(toHttpError(error));
next(new HttpSuccess(200, {}));
});
}
function setEnvironment(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
if (!req.body.env || typeof req.body.env !== 'object') return next(new HttpError(400, 'env must be an object'));
if (Object.keys(req.body.env).some((key) => typeof req.body.env[key] !== 'string')) return next(new HttpError(400, 'env must contain values as strings'));
apps.setEnvironment(req.params.id, req.body.env, auditSource.fromRequest(req), function (error, result) {
if (error) return next(toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
function setDebugMode(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
if (req.body.debugMode !== null && typeof req.body.debugMode !== 'object') return next(new HttpError(400, 'debugMode must be an object'));
apps.setDebugMode(req.params.id, req.body.debugMode, auditSource.fromRequest(req), function (error, result) {
if (error) return next(toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
function setMailbox(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
if (req.body.mailboxName !== null && typeof req.body.mailboxName !== 'string') return next(new HttpError(400, 'mailboxName must be a string'));
apps.setMailbox(req.params.id, req.body.mailboxName, auditSource.fromRequest(req), function (error, result) {
if (error) return next(toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
function setLocation(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
if (!req.body.location) return next(new HttpError(400, 'location is required'));
if (typeof req.body.location !== 'string') return next(new HttpError(400, 'location must be string'));
if (!req.body.domain) return next(new HttpError(400, 'domain is required'));
if (typeof req.body.domain !== 'string') return next(new HttpError(400, 'domain must be string'));
if ('portBindings' in req.body && typeof req.body.portBindings !== 'object') return next(new HttpError(400, 'portBindings must be an object'));
if ('alternateDomains' in req.body) {
if (!Array.isArray(req.body.alternateDomains)) return next(new HttpError(400, 'alternateDomains must be an array'));
if (req.body.alternateDomains.some(function (d) { return (typeof d.domain !== 'string' || typeof d.subdomain !== 'string'); })) return next(new HttpError(400, 'alternateDomains array must contain objects with domain and subdomain strings'));
}
if ('overwriteDns' in req.body && typeof req.body.overwriteDns !== 'boolean') return next(new HttpError(400, 'overwriteDns must be boolean'));
apps.setLocation(req.params.id, req.body, auditSource.fromRequest(req), function (error, result) {
if (error) return next(toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
function setDataDir(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
if (req.body.dataDir !== null && typeof req.body.dataDir !== 'string') return next(new HttpError(400, 'dataDir must be a string'));
apps.setDataDir(req.params.id, req.body.dataDir, auditSource.fromRequest(req), function (error, result) {
if (error) return next(toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
function repairApp(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
debug('Repair app id:%s', req.params.id);
const data = req.body;
if (data.backupId && typeof data.backupId !== 'string') return next(new HttpError(400, 'backupId must be string or null'));
if (data.backupFormat && typeof data.backupFormat !== 'string') return next(new HttpError(400, 'backupFormat must be string or null'));
if (data.location && typeof data.location !== 'string') return next(new HttpError(400, 'location is required'));
if (data.domain && typeof data.domain !== 'string') return next(new HttpError(400, 'domain is required'));
if ('mailboxName' in data && typeof data.mailboxName !== 'string') return next(new HttpError(400, 'mailboxName must be a string'));
if ('alternateDomains' in data) {
if (!Array.isArray(data.alternateDomains)) return next(new HttpError(400, 'alternateDomains must be an array'));
if (data.alternateDomains.some(function (d) { return (typeof d.domain !== 'string' || typeof d.subdomain !== 'string'); })) return next(new HttpError(400, 'alternateDomains array must contain objects with domain and subdomain strings'));
}
if ('overwriteDns' in req.body && typeof req.body.overwriteDns !== 'boolean') return next(new HttpError(400, 'overwriteDns must be boolean'));
if ('env' in data) {
if (!data.env || typeof data.env !== 'object') return next(new HttpError(400, 'env must be an object'));
if (Object.keys(data.env).some(function (key) { return typeof data.env[key] !== 'string'; })) return next(new HttpError(400, 'env must contain values as strings'));
}
apps.repair(req.params.id, data, auditSource.fromRequest(req), function (error, result) {
if (error) return next(toHttpError(error));
if ('label' in data && typeof data.label !== 'string') return next(new HttpError(400, 'label must be a string'));
if ('dataDir' in data && typeof data.dataDir !== 'string') return next(new HttpError(400, 'dataDir must be a string'));
if ('icon' in data && typeof data.icon !== 'string') return next(new HttpError(400, 'icon is not a string'));
next(new HttpSuccess(202, { taskId: result.taskId }));
debug('Configuring app id:%s data:%j', req.params.id, data);
apps.configure(req.params.id, data, req.user, auditSource.fromRequest(req), function (error) {
if (error && error.reason === AppsError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
if (error && error.reason === AppsError.PORT_RESERVED) return next(new HttpError(409, 'Port ' + error.message + ' is reserved.'));
if (error && error.reason === AppsError.PORT_CONFLICT) return next(new HttpError(409, 'Port ' + error.message + ' is already in use.'));
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message));
if (error && error.reason === AppsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === AppsError.BAD_CERTIFICATE) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, { }));
});
}
@@ -401,10 +217,14 @@ function restoreApp(req, res, next) {
if (!('backupId' in req.body)) return next(new HttpError(400, 'backupId is required'));
if (data.backupId !== null && typeof data.backupId !== 'string') return next(new HttpError(400, 'backupId must be string or null'));
apps.restore(req.params.id, data, auditSource.fromRequest(req), function (error, result) {
if (error) return next(toHttpError(error));
apps.restore(req.params.id, data, auditSource.fromRequest(req), function (error) {
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
if (error && error.reason === AppsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message));
if (error && error.reason === AppsError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, { taskId: result.taskId }));
next(new HttpSuccess(202, { }));
});
}
@@ -413,6 +233,7 @@ function cloneApp(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
var data = req.body;
data.ownerId = req.user.id;
debug('Clone app id:%s', req.params.id);
@@ -421,12 +242,19 @@ function cloneApp(req, res, next) {
if (typeof data.domain !== 'string') return next(new HttpError(400, 'domain is required'));
if (('portBindings' in data) && typeof data.portBindings !== 'object') return next(new HttpError(400, 'portBindings must be an object'));
if ('overwriteDns' in req.body && typeof req.body.overwriteDns !== 'boolean') return next(new HttpError(400, 'overwriteDns must be boolean'));
apps.clone(req.params.id, data, req.user, auditSource.fromRequest(req), function (error, result) {
if (error) return next(toHttpError(error));
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
if (error && error.reason === AppsError.PORT_RESERVED) return next(new HttpError(409, 'Port ' + error.message + ' is reserved.'));
if (error && error.reason === AppsError.PORT_CONFLICT) return next(new HttpError(409, 'Port ' + error.message + ' is already in use.'));
if (error && error.reason === AppsError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
if (error && error.reason === AppsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message));
if (error && error.reason === AppsError.PLAN_LIMIT) return next(new HttpError(402, error.message));
if (error && error.reason === AppsError.BAD_CERTIFICATE) return next(new HttpError(400, error.message));
if (error && error.reason === AppsError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(201, { id: result.id, taskId: result.taskId }));
next(new HttpSuccess(201, { id: result.id }));
});
}
@@ -435,10 +263,13 @@ function backupApp(req, res, next) {
debug('Backup app id:%s', req.params.id);
apps.backup(req.params.id, function (error, result) {
if (error) return next(toHttpError(error));
apps.backup(req.params.id, function (error) {
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message));
if (error && error.reason === AppsError.EXTERNAL_ERROR) return next(new HttpError(424, error));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, { taskId: result.taskId }));
next(new HttpSuccess(202, { }));
});
}
@@ -447,10 +278,12 @@ function uninstallApp(req, res, next) {
debug('Uninstalling app id:%s', req.params.id);
apps.uninstall(req.params.id, auditSource.fromRequest(req), function (error, result) {
if (error) return next(toHttpError(error));
apps.uninstall(req.params.id, auditSource.fromRequest(req), function (error) {
if (error && error.reason === AppsError.EXTERNAL_ERROR) return next(new HttpError(424, error));
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, { taskId: result.taskId }));
next(new HttpSuccess(202, { }));
});
}
@@ -459,10 +292,12 @@ function startApp(req, res, next) {
debug('Start app id:%s', req.params.id);
apps.start(req.params.id, function (error, result) {
if (error) return next(toHttpError(error));
apps.start(req.params.id, function (error) {
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, { taskId: result.taskId }));
next(new HttpSuccess(202, { }));
});
}
@@ -471,10 +306,12 @@ function stopApp(req, res, next) {
debug('Stop app id:%s', req.params.id);
apps.stop(req.params.id, function (error, result) {
if (error) return next(toHttpError(error));
apps.stop(req.params.id, function (error) {
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, { taskId: result.taskId }));
next(new HttpSuccess(202, { }));
});
}
@@ -493,10 +330,13 @@ function updateApp(req, res, next) {
debug('Update app id:%s to manifest:%j', req.params.id, data.manifest);
apps.update(req.params.id, req.body, auditSource.fromRequest(req), function (error, result) {
if (error) return next(toHttpError(error));
apps.update(req.params.id, req.body, auditSource.fromRequest(req), function (error) {
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
if (error && error.reason === AppsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, { taskId: result.taskId }));
next(new HttpSuccess(202, { }));
});
}
@@ -520,7 +360,9 @@ function getLogStream(req, res, next) {
};
apps.getLogs(req.params.id, options, function (error, logStream) {
if (error) return next(toHttpError(error));
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(412, error.message));
if (error) return next(new HttpError(500, error));
res.writeHead(200, {
'Content-Type': 'text/event-stream',
@@ -555,7 +397,9 @@ function getLogs(req, res, next) {
};
apps.getLogs(req.params.id, options, function (error, logStream) {
if (error) return next(toHttpError(error));
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(412, error.message));
if (error) return next(new HttpError(500, error));
res.writeHead(200, {
'Content-Type': 'application/x-logs',
@@ -609,7 +453,9 @@ function exec(req, res, next) {
var tty = req.query.tty === 'true' ? true : false;
apps.exec(req.params.id, { cmd: cmd, rows: rows, columns: columns, tty: tty }, function (error, duplexStream) {
if (error) return next(toHttpError(error));
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message));
if (error) return next(new HttpError(500, error));
if (req.headers['upgrade'] !== 'tcp') return next(new HttpError(404, 'exec requires TCP upgrade'));
@@ -649,7 +495,9 @@ function execWebSocket(req, res, next) {
var tty = req.query.tty === 'true' ? true : false;
apps.exec(req.params.id, { cmd: cmd, rows: rows, columns: columns, tty: tty }, function (error, duplexStream) {
if (error) return next(toHttpError(error));
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message));
if (error) return next(new HttpError(500, error));
debug('Connected to terminal');
@@ -689,7 +537,8 @@ function listBackups(req, res, next) {
if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a postive number'));
apps.listBackups(page, perPage, req.params.id, function (error, result) {
if (error) return next(toHttpError(error));
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { backups: result }));
});
@@ -704,7 +553,8 @@ function uploadFile(req, res, next) {
if (!req.files.file) return next(new HttpError(400, 'file must be provided as multipart'));
apps.uploadFile(req.params.id, req.files.file.path, req.query.file, function (error) {
if (error) return next(toHttpError(error));
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
debug('uploadFile: done');
@@ -720,7 +570,8 @@ function downloadFile(req, res, next) {
if (typeof req.query.file !== 'string' || !req.query.file) return next(new HttpError(400, 'file query argument must be provided'));
apps.downloadFile(req.params.id, req.query.file, function (error, stream, info) {
if (error) return next(toHttpError(error));
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
var headers = {
'Content-Type': 'application/octet-stream',
@@ -733,3 +584,17 @@ function downloadFile(req, res, next) {
stream.pipe(res);
});
}
function setOwner(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.ownerId !== 'string') return next(new HttpError(400, 'ownerId must be a string'));
apps.setOwner(req.params.id, req.body.ownerId, function (error) {
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { }));
});
}
+2 -13
View File
@@ -12,8 +12,7 @@ exports = module.exports = {
getLogStream: getLogStream,
setDashboardAndMailDomain: setDashboardAndMailDomain,
prepareDashboardDomain: prepareDashboardDomain,
renewCerts: renewCerts,
syncExternalLdap: syncExternalLdap
renewCerts: renewCerts
};
let assert = require('assert'),
@@ -22,8 +21,6 @@ let assert = require('assert'),
cloudron = require('../cloudron.js'),
CloudronError = cloudron.CloudronError,
custom = require('../custom.js'),
disks = require('../disks.js'),
externalldap = require('../externalldap.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
updater = require('../updater.js'),
@@ -54,7 +51,7 @@ function getConfig(req, res, next) {
}
function getDisks(req, res, next) {
disks.getDisks(function (error, result) {
cloudron.getDisks(function (error, result) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, result));
});
@@ -188,11 +185,3 @@ function renewCerts(req, res, next) {
next(new HttpSuccess(202, { taskId }));
});
}
function syncExternalLdap(req, res, next) {
externalldap.startSyncer(function (error, taskId) {
if (error) return next(new HttpError(500, error.message));
next(new HttpSuccess(202, { taskId: taskId }));
});
}
+1 -1
View File
@@ -20,7 +20,7 @@ function login(req, res, next) {
if (!user.ghost && user.twoFactorAuthenticationEnabled) {
if (!req.body.totpToken) return next(new HttpError(401, 'A totpToken must be provided'));
let verified = speakeasy.totp.verify({ secret: user.twoFactorAuthenticationSecret, encoding: 'base32', token: req.body.totpToken, window: 2 });
let verified = speakeasy.totp.verify({ secret: user.twoFactorAuthenticationSecret, encoding: 'base32', token: req.body.totpToken });
if (!verified) return next(new HttpError(401, 'Invalid totpToken'));
}
-20
View File
@@ -7,8 +7,6 @@ exports = module.exports = {
update: update,
del: del,
checkDnsRecords: checkDnsRecords,
verifyDomainLock: verifyDomainLock
};
@@ -154,21 +152,3 @@ function del(req, res, next) {
next(new HttpSuccess(204));
});
}
function checkDnsRecords(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
if (!('subdomain' in req.query)) return next(new HttpError(400, 'subdomain is required'));
// some DNS providers like DigitalOcean take a really long time to verify credentials (https://github.com/expressjs/timeout/issues/26)
req.clearTimeout();
domains.checkDnsRecords(req.query.subdomain, req.params.domain, function (error, result) {
if (error && error.reason === DomainsError.NOT_FOUND) return next(new HttpError(404, error.message)); // domain (and not record!) not found
if (error && error.reason === DomainsError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
if (error && error.reason === DomainsError.ACCESS_DENIED) return next(new HttpSuccess(200, { error: { reason: error.reason, message: error.message }}));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { needsOverwrite: result.needsOverwrite }));
});
}
+3 -16
View File
@@ -5,34 +5,21 @@ exports = module.exports = {
};
var middleware = require('../middleware/index.js'),
HttpError = require('connect-lastmile').HttpError,
url = require('url');
// for testing locally: curl 'http://127.0.0.1:8417/graphite-web/render?format=json&from=-1min&target=absolute(collectd.localhost.du-docker.capacity-usage)'
// the datapoint is (value, timestamp) https://buildmedia.readthedocs.org/media/pdf/graphite/0.9.16/graphite.pdf
const graphiteProxy = middleware.proxy(url.parse('http://127.0.0.1:8417'));
var graphiteProxy = middleware.proxy(url.parse('http://127.0.0.1:8417'));
function getGraphs(req, res, next) {
var parsedUrl = url.parse(req.url, true /* parseQueryString */);
delete parsedUrl.query['access_token'];
delete req.headers['authorization'];
delete req.headers['cookies'];
// 'graphite-web' is the URL_PREFIX in docker-graphite
req.url = url.format({ pathname: 'graphite-web/render', query: parsedUrl.query });
req.url = url.format({ pathname: 'render', query: parsedUrl.query });
// graphs may take very long to respond so we run into headers already sent issues quite often
// nginx still has a request timeout which can deal with this then.
req.clearTimeout();
graphiteProxy(req, res, function (error) {
if (!error) return next();
if (error.code === 'ECONNREFUSED') return next(new HttpError(424, 'Unable to connect to graphite'));
// ECONNRESET here is most likely because of a bug in the query or the uwsgi buffer size is too small
if (error.code === 'ECONNRESET') return next(new HttpError(424, 'Unable to query graphite'));
next(new HttpError(500, error));
});
graphiteProxy(req, res, next);
}
+1
View File
@@ -20,6 +20,7 @@ exports = module.exports = {
services: require('./services.js'),
settings: require('./settings.js'),
support: require('./support.js'),
sysadmin: require('./sysadmin.js'),
tasks: require('./tasks.js'),
users: require('./users.js')
};
-2
View File
@@ -350,7 +350,6 @@ function addList(req, res, next) {
if (typeof req.body.name !== 'string') return next(new HttpError(400, 'name must be a string'));
if (!Array.isArray(req.body.members)) return next(new HttpError(400, 'members must be a string'));
if (req.body.members.length === 0) return next(new HttpError(400, 'list must have atleast one member'));
for (var i = 0; i < req.body.members.length; i++) {
if (typeof req.body.members[i] !== 'string') return next(new HttpError(400, 'member must be a string'));
@@ -371,7 +370,6 @@ function updateList(req, res, next) {
assert.strictEqual(typeof req.params.name, 'string');
if (!Array.isArray(req.body.members)) return next(new HttpError(400, 'members must be a string'));
if (req.body.members.length === 0) return next(new HttpError(400, 'list must have atleast one member'));
for (var i = 0; i < req.body.members.length; i++) {
if (typeof req.body.members[i] !== 'string') return next(new HttpError(400, 'member must be a string'));
+7 -6
View File
@@ -24,6 +24,7 @@ var apps = require('../apps.js'),
authcodedb = require('../authcodedb.js'),
clients = require('../clients'),
ClientsError = clients.ClientsError,
config = require('../config.js'),
constants = require('../constants.js'),
DatabaseError = require('../databaseerror.js'),
debug = require('debug')('box:routes/oauth2'),
@@ -153,7 +154,7 @@ function renderTemplate(res, template, data) {
// amend template properties, for example used in the header
data.title = data.title || 'Cloudron';
data.adminOrigin = settings.adminOrigin();
data.adminOrigin = config.adminOrigin();
data.cloudronName = cloudronName;
res.render(template, data);
@@ -213,8 +214,8 @@ function loginForm(req, res) {
applicationName: applicationName,
applicationLogo: applicationLogo,
error: error,
username: settings.isDemo() ? constants.DEMO_USERNAME : '',
password: settings.isDemo() ? 'cloudron' : '',
username: config.isDemo() ? constants.DEMO_USERNAME : '',
password: config.isDemo() ? 'cloudron' : '',
title: applicationName + ' Login'
});
}
@@ -262,7 +263,7 @@ function login(req, res) {
return res.redirect('/api/v1/session/login?' + failureQuery);
}
let verified = speakeasy.totp.verify({ secret: req.user.twoFactorAuthenticationSecret, encoding: 'base32', token: req.body.totpToken, window: 2 });
let verified = speakeasy.totp.verify({ secret: req.user.twoFactorAuthenticationSecret, encoding: 'base32', token: req.body.totpToken });
if (!verified) {
let failureQuery = querystring.stringify({ error: 'The 2FA token is invalid', returnTo: returnTo });
return res.redirect('/api/v1/session/login?' + failureQuery);
@@ -372,7 +373,7 @@ function accountSetup(req, res, next) {
clients.addTokenByUserId('cid-webadmin', userObject.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) {
if (error) return next(new HttpError(500, error));
res.redirect(`${settings.adminOrigin()}?accessToken=${result.accessToken}&expiresAt=${result.expires}`);
res.redirect(`${config.adminOrigin()}?accessToken=${result.accessToken}&expiresAt=${result.expires}`);
});
});
});
@@ -420,7 +421,7 @@ function passwordReset(req, res, next) {
clients.addTokenByUserId('cid-webadmin', userObject.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) {
if (error) return next(new HttpError(500, error));
res.redirect(`${settings.adminOrigin()}?accessToken=${result.accessToken}&expiresAt=${result.expires}`);
res.redirect(`${config.adminOrigin()}?accessToken=${result.accessToken}&expiresAt=${result.expires}`);
});
});
});
+2 -3
View File
@@ -27,8 +27,7 @@ function get(req, res, next) {
fallbackEmail: req.user.fallbackEmail,
displayName: req.user.displayName,
twoFactorAuthenticationEnabled: req.user.twoFactorAuthenticationEnabled,
admin: req.user.admin,
source: req.user.source
admin: req.user.admin
}));
}
@@ -86,7 +85,7 @@ function enableTwoFactorAuthentication(req, res, next) {
users.enableTwoFactorAuthentication(req.user.id, req.body.totpToken, function (error) {
if (error && error.reason === UsersError.NOT_FOUND) return next(new HttpError(404, 'User not found'));
if (error && error.reason === UsersError.BAD_TOKEN) return next(new HttpError(412, 'Invalid token'));
if (error && error.reason === UsersError.BAD_TOKEN) return next(new HttpError(403, 'Invalid token'));
if (error && error.reason === UsersError.ALREADY_EXISTS) return next(new HttpError(409, 'TwoFactor Authentication is already enabled'));
if (error) return next(new HttpError(500, error));
+2 -2
View File
@@ -10,18 +10,18 @@ exports = module.exports = {
var assert = require('assert'),
auditSource = require('../auditsource'),
config = require('../config.js'),
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,
sysinfo = require('../sysinfo.js'),
superagent = require('superagent');
function providerTokenAuth(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (sysinfo.provider() === 'ami') {
if (config.provider() === 'ami') {
if (typeof req.body.providerToken !== 'string' || !req.body.providerToken) return next(new HttpError(400, 'providerToken must be a non empty string'));
superagent.get('http://169.254.169.254/latest/meta-data/instance-id').timeout(30 * 1000).end(function (error, result) {
+5 -34
View File
@@ -11,7 +11,7 @@ var assert = require('assert'),
backups = require('../backups.js'),
custom = require('../custom.js'),
docker = require('../docker.js'),
BoxError = require('../boxerror.js'),
DockerError = docker.DockerError,
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
safe = require('safetydance'),
@@ -128,15 +128,15 @@ function getCloudronAvatar(req, res, next) {
}
function getBackupConfig(req, res, next) {
settings.getBackupConfig(function (error, backupConfig) {
settings.getBackupConfig(function (error, config) {
if (error) return next(new HttpError(500, error));
// always send provider as it is used by the UI to figure if backups are disabled ('noop' backend)
if (!custom.spec().backups.configurable) {
return next(new HttpSuccess(200, { provider: backupConfig.provider }));
return next(new HttpSuccess(200, { provider: config.provider }));
}
next(new HttpSuccess(200, backups.removePrivateFields(backupConfig)));
next(new HttpSuccess(200, backups.removePrivateFields(config)));
});
}
@@ -196,33 +196,6 @@ function setPlatformConfig(req, res, next) {
});
}
function getExternalLdapConfig(req, res, next) {
settings.getExternalLdapConfig(function (error, config) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, config));
});
}
function setExternalLdapConfig(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.enabled !== 'boolean') return next(new HttpError(400, 'enabled must be a boolean'));
if (typeof req.body.url !== 'string' || req.body.url === '') return next(new HttpError(400, 'url must be a non empty string'));
if (typeof req.body.baseDn !== 'string' || req.body.baseDn === '') return next(new HttpError(400, 'baseDn must be a non empty string'));
if (typeof req.body.filter !== 'string' || req.body.filter === '') return next(new HttpError(400, 'filter must be a non empty string'));
if ('bindDn' in req.body && (typeof req.body.bindDn !== 'string' || req.body.bindDn === '')) return next(new HttpError(400, 'bindDn must be a non empty string'));
if ('bindPassword' in req.body && typeof req.body.bindPassword !== 'string') return next(new HttpError(400, 'bindPassword must be a string'));
settings.setExternalLdapConfig(req.body, function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === SettingsError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, {}));
});
}
function getDynamicDnsConfig(req, res, next) {
settings.getDynamicDnsConfig(function (error, enabled) {
if (error) return next(new HttpError(500, error));
@@ -275,7 +248,7 @@ function setRegistryConfig(req, res, next) {
if ('password' in req.body && typeof req.body.password !== 'string') return next(new HttpError(400, 'password is required'));
docker.setRegistryConfig(req.body, function (error) {
if (error && error.reason === BoxError.ACCESS_DENIED) return next(new HttpError(424, error.message));
if (error && error.reason === DockerError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200));
@@ -289,7 +262,6 @@ function get(req, res, next) {
case settings.DYNAMIC_DNS_KEY: return getDynamicDnsConfig(req, res, next);
case settings.BACKUP_CONFIG_KEY: return getBackupConfig(req, res, next);
case settings.PLATFORM_CONFIG_KEY: return getPlatformConfig(req, res, next);
case settings.EXTERNAL_LDAP_KEY: return getExternalLdapConfig(req, res, next);
case settings.UNSTABLE_APPS_KEY: return getUnstableAppsConfig(req, res, next);
case settings.APP_AUTOUPDATE_PATTERN_KEY: return getAppAutoupdatePattern(req, res, next);
@@ -310,7 +282,6 @@ function set(req, res, next) {
case settings.DYNAMIC_DNS_KEY: return setDynamicDnsConfig(req, res, next);
case settings.BACKUP_CONFIG_KEY: return setBackupConfig(req, res, next);
case settings.PLATFORM_CONFIG_KEY: return setPlatformConfig(req, res, next);
case settings.EXTERNAL_LDAP_KEY: return setExternalLdapConfig(req, res, next);
case settings.UNSTABLE_APPS_KEY: return setUnstableAppsConfig(req, res, next);
case settings.APP_AUTOUPDATE_PATTERN_KEY: return setAppAutoupdatePattern(req, res, next);
+71
View File
@@ -0,0 +1,71 @@
'use strict';
exports = module.exports = {
backup: backup,
update: update,
retire: retire,
importAppDatabase: importAppDatabase
};
var apps = require('../apps.js'),
AppsError = apps.AppsError,
addons = require('../addons.js'),
auditSource = require('../auditsource.js'),
backups = require('../backups.js'),
BackupsError = require('../backups.js').BackupsError,
cloudron = require('../cloudron.js'),
debug = require('debug')('box:routes/sysadmin'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
updater = require('../updater.js'),
UpdaterError = require('../updater.js').UpdaterError;
function backup(req, res, next) {
debug('triggering backup');
// 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.startBackupTask(auditSource.SYSADMIN, function (error, taskId) {
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 }));
});
}
function update(req, res, next) {
debug('triggering update');
// this only initiates the update, progress can be checked via the progress route
updater.updateToLatest({ skipBackup: false }, auditSource.SYSADMIN, function (error, taskId) {
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) return next(new HttpError(500, error));
next(new HttpSuccess(202, { taskId }));
});
}
function retire(req, res, next) {
debug('triggering retire');
cloudron.retire('migrate', { }, function (error) {
if (error) debug('Retire failed.', error);
});
next(new HttpSuccess(202, {}));
}
function importAppDatabase(req, res, next) {
apps.get(req.params.id, function (error, app) {
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
if (error) return next(new HttpError(500, error));
addons.importAppDatabase(app, req.query.addon || '', function (error) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, {}));
});
});
}
+1 -1
View File
@@ -94,4 +94,4 @@ describe('scopes middleware', function () {
done();
});
});
});
});
+908 -1415
View File
File diff suppressed because it is too large Load Diff
+22 -18
View File
@@ -6,30 +6,32 @@
'use strict';
var async = require('async'),
constants = require('../../constants.js'),
config = require('../../config.js'),
database = require('../../database.js'),
expect = require('expect.js'),
nock = require('nock'),
settings = require('../../settings.js'),
path = require('path'),
safe = require('safetydance'),
superagent = require('superagent'),
server = require('../../server.js');
var SERVER_URL = 'http://localhost:' + constants.PORT;
var SERVER_URL = 'http://localhost:' + config.get('port');
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var AUTHORIZED_KEYS_FILE = path.join(config.baseDir(), 'authorized_keys');
var token = null;
function setup(done) {
nock.cleanAll();
config._reset();
config.setFqdn('example-ssh-test.com');
safe.fs.unlinkSync(AUTHORIZED_KEYS_FILE);
async.series([
server.start.bind(server),
database._clear,
settings._setApiServerOrigin.bind(null, 'http://localhost:6060'),
settings.setAdmin.bind(null, 'appstore-test.example.com', 'my.appstore-test.example.com'),
function createAdmin(callback) {
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
@@ -51,6 +53,8 @@ function cleanup(done) {
database._clear(function (error) {
expect(error).to.not.be.ok();
config._reset();
server.stop(done);
});
}
@@ -78,11 +82,11 @@ describe('Appstore Apps API', function () {
});
it('register cloudron', function (done) {
var scope1 = nock(settings.apiServerOrigin())
var scope1 = nock(config.apiServerOrigin())
.post('/api/v1/login', (body) => body.email && body.password)
.reply(200, { userId: 'userId', accessToken: 'SECRET_TOKEN' });
var scope2 = nock(settings.apiServerOrigin())
var scope2 = nock(config.apiServerOrigin())
.post('/api/v1/register_cloudron', (body) => !!body.domain && body.accessToken === 'SECRET_TOKEN')
.reply(201, { cloudronId: 'cid', cloudronToken: 'CLOUDRON_TOKEN', licenseKey: 'lkey' });
@@ -98,8 +102,8 @@ describe('Appstore Apps API', function () {
});
it('can list apps', function (done) {
var scope1 = nock(settings.apiServerOrigin())
.get(`/api/v1/apps?accessToken=CLOUDRON_TOKEN&boxVersion=${constants.VERSION}&unstable=false`, () => true)
var scope1 = nock(config.apiServerOrigin())
.get(`/api/v1/apps?accessToken=CLOUDRON_TOKEN&boxVersion=${config.version()}&unstable=false`, () => true)
.reply(200, { apps: [] });
superagent.get(SERVER_URL + '/api/v1/appstore/apps')
@@ -112,7 +116,7 @@ describe('Appstore Apps API', function () {
});
it('can get app', function (done) {
var scope1 = nock(settings.apiServerOrigin())
var scope1 = nock(config.apiServerOrigin())
.get('/api/v1/apps/org.wordpress.cloudronapp?accessToken=CLOUDRON_TOKEN', () => true)
.reply(200, { apps: [] });
@@ -126,7 +130,7 @@ describe('Appstore Apps API', function () {
});
it('can get app version', function (done) {
var scope1 = nock(settings.apiServerOrigin())
var scope1 = nock(config.apiServerOrigin())
.get('/api/v1/apps/org.wordpress.cloudronapp/versions/3.4.2?accessToken=CLOUDRON_TOKEN', () => true)
.reply(200, { apps: [] });
@@ -146,11 +150,11 @@ describe('Subscription API - no signup', function () {
after(cleanup);
it('can setup subscription', function (done) {
var scope1 = nock(settings.apiServerOrigin())
var scope1 = nock(config.apiServerOrigin())
.post('/api/v1/login', (body) => body.email && body.password)
.reply(200, { userId: 'userId', accessToken: 'SECRET_TOKEN' });
var scope2 = nock(settings.apiServerOrigin())
var scope2 = nock(config.apiServerOrigin())
.post('/api/v1/register_cloudron', (body) => !!body.domain && body.accessToken === 'SECRET_TOKEN')
.reply(201, { cloudronId: 'cid', cloudronToken: 'CLOUDRON_TOKEN', licenseKey: 'lkey' });
@@ -181,15 +185,15 @@ describe('Subscription API - signup', function () {
after(cleanup);
it('can setup subscription', function (done) {
var scope1 = nock(settings.apiServerOrigin())
var scope1 = nock(config.apiServerOrigin())
.post('/api/v1/register_user', (body) => body.email && body.password)
.reply(201, { });
var scope2 = nock(settings.apiServerOrigin())
var scope2 = nock(config.apiServerOrigin())
.post('/api/v1/login', (body) => body.email && body.password)
.reply(200, { userId: 'userId', accessToken: 'SECRET_TOKEN' });
var scope3 = nock(settings.apiServerOrigin())
var scope3 = nock(config.apiServerOrigin())
.post('/api/v1/register_cloudron', (body) => !!body.domain && body.accessToken === 'SECRET_TOKEN')
.reply(201, { cloudronId: 'cid', cloudronToken: 'CLOUDRON_TOKEN', licenseKey: 'lkey' });
@@ -206,7 +210,7 @@ describe('Subscription API - signup', function () {
});
it('can get subscription', function (done) {
var scope1 = nock(settings.apiServerOrigin())
var scope1 = nock(config.apiServerOrigin())
.get('/api/v1/subscription?accessToken=CLOUDRON_TOKEN', () => true)
.reply(200, { subscription: { plan: { id: 'free' } }, email: 'test@cloudron.io' });
+6 -4
View File
@@ -7,7 +7,7 @@
var appdb = require('../../appdb.js'),
async = require('async'),
constants = require('../../constants.js'),
config = require('../../config.js'),
database = require('../../database.js'),
domains = require('../../domains.js'),
expect = require('expect.js'),
@@ -16,7 +16,7 @@ var appdb = require('../../appdb.js'),
server = require('../../server.js'),
settings = require('../../settings.js');
const SERVER_URL = 'http://localhost:' + constants.PORT;
const SERVER_URL = 'http://localhost:' + config.get('port');
const USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
@@ -31,10 +31,11 @@ const DOMAIN_0 = {
let AUDIT_SOURCE = { ip: '1.2.3.4' };
var token = null;
var token = null, ownerId = null;
function setup(done) {
nock.cleanAll();
config._reset();
async.series([
server.start,
@@ -50,6 +51,7 @@ function setup(done) {
expect(result.statusCode).to.eql(201);
// stash token for further use
ownerId = result.body.userId;
token = result.body.token;
callback();
@@ -58,7 +60,7 @@ function setup(done) {
function addApp(callback) {
var manifest = { version: '0.0.1', manifestVersion: 1, dockerImage: 'foo', healthCheckPath: '/', httpPort: 3, title: 'ok', addons: { } };
appdb.add('appid', 'appStoreId', manifest, 'location', DOMAIN_0.domain, [ ] /* portBindings */, { installationState: 'installed', runState: 'running' }, callback);
appdb.add('appid', 'appStoreId', manifest, 'location', DOMAIN_0.domain, ownerId, [ ] /* portBindings */, { }, callback);
},
function createSettings(callback) {
+5 -2
View File
@@ -7,7 +7,7 @@
var accesscontrol = require('../../accesscontrol.js'),
async = require('async'),
constants = require('../../constants.js'),
config = require('../../config.js'),
clients = require('../../clients.js'),
database = require('../../database.js'),
oauth2 = require('../oauth2.js'),
@@ -17,12 +17,15 @@ var accesscontrol = require('../../accesscontrol.js'),
superagent = require('superagent'),
server = require('../../server.js');
var SERVER_URL = 'http://localhost:' + constants.PORT;
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-clients-test.com');
async.series([
server.start,
database._clear,
+11 -7
View File
@@ -6,7 +6,7 @@
/* global after:false */
let async = require('async'),
constants = require('../../constants.js'),
config = require('../../config.js'),
database = require('../../database.js'),
expect = require('expect.js'),
hat = require('../../hat.js'),
@@ -18,7 +18,7 @@ let async = require('async'),
settings = require('../../settings.js'),
tokendb = require('../../tokendb.js');
var SERVER_URL = 'http://localhost:' + constants.PORT;
var SERVER_URL = 'http://localhost:' + config.get('port');
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var token = null; // authentication token
@@ -26,11 +26,13 @@ var USERNAME_1 = 'userTheFirst', EMAIL_1 = 'taO@zen.mac', userId_1, token_1;
function setup(done) {
nock.cleanAll();
config._reset();
config.setFqdn('example-cloudron-test.com');
config.setAdminFqdn('my.example-cloudron-test.com');
async.series([
server.start.bind(server),
database._clear,
settings._setApiServerOrigin.bind(null, 'http://localhost:6060'),
settings.setBackupConfig.bind(null, { provider: 'filesystem', backupFolder: '/tmp', format: 'tgz' })
], done);
}
@@ -39,6 +41,8 @@ function cleanup(done) {
database._clear(function (error) {
expect(error).to.not.be.ok();
config._reset();
server.stop(done);
});
}
@@ -183,9 +187,9 @@ describe('Cloudron', function () {
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.apiServerOrigin).to.eql('http://localhost:6060');
expect(result.body.webServerOrigin).to.eql('https://cloudron.io');
expect(result.body.adminFqdn).to.eql(settings.adminFqdn());
expect(result.body.version).to.eql(constants.VERSION);
expect(result.body.webServerOrigin).to.eql(null);
expect(result.body.adminFqdn).to.eql(config.adminFqdn());
expect(result.body.version).to.eql(config.version());
expect(result.body.memory).to.eql(os.totalmem());
expect(result.body.cloudronName).to.be.a('string');
@@ -238,7 +242,7 @@ describe('Cloudron', function () {
it('logStream - stream logs', function (done) {
var options = {
host: 'localhost',
port: constants.PORT,
port: config.get('port'),
path: '/api/v1/cloudron/logstream/box?lines=10&access_token=' + token,
headers: { 'Accept': 'text/event-stream', 'Connection': 'keep-alive' }
};
+8 -5
View File
@@ -7,18 +7,21 @@
/* global after:false */
var async = require('async'),
constants = require('../../constants.js'),
config = require('../../config.js'),
database = require('../../database.js'),
expect = require('expect.js'),
speakeasy = require('speakeasy'),
superagent = require('superagent'),
server = require('../../server.js');
var SERVER_URL = 'http://localhost:' + constants.PORT;
var SERVER_URL = 'http://localhost:' + config.get('port');
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
function setup(done) {
config._reset();
config.setFqdn('example-developer-test.com');
async.series([
server.start.bind(server),
database._clear
@@ -176,7 +179,7 @@ describe('Developer API', function () {
async.series([
setup,
function (callback) {
superagent.post(`${SERVER_URL}/api/v1/cloudron/activate`).query({ setupToken: 'somesetuptoken' }).send({ username: USERNAME, password: PASSWORD, email: EMAIL }).end(function (error) {
superagent.post(`${SERVER_URL}/api/v1/cloudron/activate`).query({ setupToken: 'somesetuptoken' }).send({ username: USERNAME, password: PASSWORD, email: EMAIL }).end(function (error, result) {
callback(error);
});
},
@@ -198,7 +201,7 @@ describe('Developer API', function () {
encoding: 'base32'
});
superagent.post(`${SERVER_URL}/api/v1/profile/twofactorauthentication/enable`).query({ access_token: accessToken }).send({ totpToken: totpToken }).end(function (error) {
superagent.post(`${SERVER_URL}/api/v1/profile/twofactorauthentication/enable`).query({ access_token: accessToken }).send({ totpToken: totpToken }).end(function (error, result) {
callback(error);
});
}
@@ -208,7 +211,7 @@ describe('Developer API', function () {
after(function (done) {
async.series([
function (callback) {
superagent.post(`${SERVER_URL}/api/v1/profile/twofactorauthentication/disable`).query({ access_token: accessToken }).send({ password: PASSWORD }).end(function (error) {
superagent.post(`${SERVER_URL}/api/v1/profile/twofactorauthentication/disable`).query({ access_token: accessToken }).send({ password: PASSWORD }).end(function (error, result) {
callback(error);
});
},
+6 -2
View File
@@ -7,7 +7,7 @@
var async = require('async'),
child_process = require('child_process'),
constants = require('../../constants.js'),
config = require('../../config.js'),
database = require('../../database.js'),
domaindb = require('../../domaindb.js'),
expect = require('expect.js'),
@@ -18,10 +18,11 @@ var async = require('async'),
server = require('../../server.js'),
_ = require('underscore');
var SERVER_URL = 'http://localhost:' + constants.PORT;
var SERVER_URL = 'http://localhost:' + config.get('port');
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var token = null;
var DOMAIN = 'example-domains-test.com';
var DOMAIN_0 = {
domain: 'cloudron.com',
@@ -44,6 +45,9 @@ var DOMAIN_1 = {
describe('Domains API', function () {
before(function (done) {
config._reset();
config.setFqdn(DOMAIN);
async.series([
server.start.bind(null),
database._clear.bind(null),
+5 -2
View File
@@ -8,7 +8,7 @@
var accesscontrol = require('../../accesscontrol.js'),
async = require('async'),
constants = require('../../constants.js'),
config = require('../../config.js'),
database = require('../../database.js'),
eventlogdb = require('../../eventlogdb.js'),
expect = require('expect.js'),
@@ -17,7 +17,7 @@ var accesscontrol = require('../../accesscontrol.js'),
server = require('../../server.js'),
tokendb = require('../../tokendb.js');
var SERVER_URL = 'http://localhost:' + constants.PORT;
var SERVER_URL = 'http://localhost:' + config.get('port');
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var token = null;
@@ -36,6 +36,9 @@ var EVENT_0 = {
};
function setup(done) {
config._reset();
config.setFqdn('example-eventlog-test.com');
async.series([
server.start.bind(server),
+6 -3
View File
@@ -8,7 +8,7 @@
var accesscontrol = require('../../accesscontrol.js'),
async = require('async'),
constants = require('../../constants.js'),
config = require('../../config.js'),
database = require('../../database.js'),
expect = require('expect.js'),
hat = require('../../hat.js'),
@@ -16,7 +16,7 @@ var accesscontrol = require('../../accesscontrol.js'),
superagent = require('superagent'),
tokendb = require('../../tokendb.js');
var SERVER_URL = 'http://localhost:' + constants.PORT;
var SERVER_URL = 'http://localhost:' + config.get('port');
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var USERNAME_1 = 'user', PASSWORD_1 = 'Foobar?1337', EMAIL_1 ='happy@me.com';
@@ -27,6 +27,9 @@ var GROUP_NAME = 'externals';
var groupObject, group1Object;
function setup(done) {
config._reset();
config.setFqdn('example-groups-test.com');
async.series([
server.start.bind(server),
@@ -124,7 +127,7 @@ describe('Groups API', function () {
group1Object = result.body;
done();
});
});
})
it('cannot add user to invalid group', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + userId + '/groups')
+28 -27
View File
@@ -6,18 +6,17 @@
/* global after:false */
var async = require('async'),
constants = require('../../constants.js'),
config = require('../../config.js'),
database = require('../../database.js'),
expect = require('expect.js'),
mail = require('../../mail.js'),
maildb = require('../../maildb.js'),
server = require('../../server.js'),
settings = require('../../settings.js'),
superagent = require('superagent'),
userdb = require('../../userdb.js'),
_ = require('underscore');
var SERVER_URL = 'http://localhost:' + constants.PORT;
var SERVER_URL = 'http://localhost:' + config.get('port');
const ADMIN_DOMAIN = {
domain: 'admin.com',
@@ -42,6 +41,8 @@ var token = null;
var userId = '';
function setup(done) {
config._reset();
async.series([
server.start.bind(null),
database._clear.bind(null),
@@ -309,7 +310,7 @@ describe('Mail API', function () {
expect(res.body.dns.spf.domain).to.eql(spfDomain);
expect(res.body.dns.spf.type).to.eql('TXT');
expect(res.body.dns.spf.value).to.eql(null);
expect(res.body.dns.spf.expected).to.eql('v=spf1 a:' + settings.adminFqdn() + ' ~all');
expect(res.body.dns.spf.expected).to.eql('v=spf1 a:' + config.adminFqdn() + ' ~all');
expect(res.body.dns.spf.status).to.eql(false);
expect(res.body.dns.dmarc).to.be.an('object');
@@ -321,13 +322,13 @@ describe('Mail API', function () {
expect(res.body.dns.mx).to.be.an('object');
expect(res.body.dns.mx.type).to.eql('MX');
expect(res.body.dns.mx.value).to.eql(null);
expect(res.body.dns.mx.expected).to.eql('10 ' + settings.mailFqdn() + '.');
expect(res.body.dns.mx.expected).to.eql('10 ' + config.mailFqdn() + '.');
expect(res.body.dns.mx.status).to.eql(false);
expect(res.body.dns.ptr).to.be.an('object');
expect(res.body.dns.ptr.type).to.eql('PTR');
// expect(res.body.ptr.value).to.eql(null); this will be anything random
expect(res.body.dns.ptr.expected).to.eql(settings.mailFqdn());
expect(res.body.dns.ptr.expected).to.eql(config.mailFqdn());
expect(res.body.dns.ptr.status).to.eql(false);
done();
@@ -348,7 +349,7 @@ describe('Mail API', function () {
expect(res.statusCode).to.equal(200);
expect(res.body.dns.spf).to.be.an('object');
expect(res.body.dns.spf.expected).to.eql('v=spf1 a:' + settings.adminFqdn() + ' ~all');
expect(res.body.dns.spf.expected).to.eql('v=spf1 a:' + config.adminFqdn() + ' ~all');
expect(res.body.dns.spf.status).to.eql(false);
expect(res.body.dns.spf.value).to.eql(null);
@@ -364,11 +365,11 @@ describe('Mail API', function () {
expect(res.body.dns.mx).to.be.an('object');
expect(res.body.dns.mx.status).to.eql(false);
expect(res.body.dns.mx.expected).to.eql('10 ' + settings.mailFqdn() + '.');
expect(res.body.dns.mx.expected).to.eql('10 ' + config.mailFqdn() + '.');
expect(res.body.dns.mx.value).to.eql(null);
expect(res.body.dns.ptr).to.be.an('object');
expect(res.body.dns.ptr.expected).to.eql(settings.mailFqdn());
expect(res.body.dns.ptr.expected).to.eql(config.mailFqdn());
expect(res.body.dns.ptr.status).to.eql(false);
// expect(res.body.ptr.value).to.eql(null); this will be anything random
@@ -379,7 +380,7 @@ describe('Mail API', function () {
it('succeeds with all different spf, dkim, dmarc, mx, ptr records', function (done) {
clearDnsAnswerQueue();
dnsAnswerQueue[mxDomain].MX = [ { priority: '20', exchange: settings.mailFqdn() }, { priority: '30', exchange: settings.mailFqdn() } ];
dnsAnswerQueue[mxDomain].MX = [ { priority: '20', exchange: config.mailFqdn() }, { priority: '30', exchange: config.mailFqdn() } ];
dnsAnswerQueue[dmarcDomain].TXT = [['v=DMARC2; p=reject; pct=100']];
dnsAnswerQueue[dkimDomain].TXT = [['v=DKIM2; t=s; p=' + mail._readDkimPublicKeySync(DOMAIN_0.domain)]];
dnsAnswerQueue[spfDomain].TXT = [['v=spf1 a:random.com ~all']];
@@ -390,7 +391,7 @@ describe('Mail API', function () {
expect(res.statusCode).to.equal(200);
expect(res.body.dns.spf).to.be.an('object');
expect(res.body.dns.spf.expected).to.eql('v=spf1 a:' + settings.adminFqdn() + ' a:random.com ~all');
expect(res.body.dns.spf.expected).to.eql('v=spf1 a:' + config.adminFqdn() + ' a:random.com ~all');
expect(res.body.dns.spf.status).to.eql(false);
expect(res.body.dns.spf.value).to.eql('v=spf1 a:random.com ~all');
@@ -406,11 +407,11 @@ describe('Mail API', function () {
expect(res.body.dns.mx).to.be.an('object');
expect(res.body.dns.mx.status).to.eql(false);
expect(res.body.dns.mx.expected).to.eql('10 ' + settings.mailFqdn() + '.');
expect(res.body.dns.mx.value).to.eql('20 ' + settings.mailFqdn() + '. 30 ' + settings.mailFqdn() + '.');
expect(res.body.dns.mx.expected).to.eql('10 ' + config.mailFqdn() + '.');
expect(res.body.dns.mx.value).to.eql('20 ' + config.mailFqdn() + '. 30 ' + config.mailFqdn() + '.');
expect(res.body.dns.ptr).to.be.an('object');
expect(res.body.dns.ptr.expected).to.eql(settings.mailFqdn());
expect(res.body.dns.ptr.expected).to.eql(config.mailFqdn());
expect(res.body.dns.ptr.status).to.eql(false);
// expect(res.body.ptr.value).to.eql(null); this will be anything random
@@ -423,7 +424,7 @@ describe('Mail API', function () {
it('succeeds with existing embedded spf', function (done) {
clearDnsAnswerQueue();
dnsAnswerQueue[spfDomain].TXT = [['v=spf1 a:example.com a:' + settings.mailFqdn() + ' ~all']];
dnsAnswerQueue[spfDomain].TXT = [['v=spf1 a:example.com a:' + config.mailFqdn() + ' ~all']];
superagent.get(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/status')
.query({ access_token: token })
@@ -433,8 +434,8 @@ describe('Mail API', function () {
expect(res.body.dns.spf).to.be.an('object');
expect(res.body.dns.spf.domain).to.eql(spfDomain);
expect(res.body.dns.spf.type).to.eql('TXT');
expect(res.body.dns.spf.value).to.eql('v=spf1 a:example.com a:' + settings.mailFqdn() + ' ~all');
expect(res.body.dns.spf.expected).to.eql('v=spf1 a:example.com a:' + settings.mailFqdn() + ' ~all');
expect(res.body.dns.spf.value).to.eql('v=spf1 a:example.com a:' + config.mailFqdn() + ' ~all');
expect(res.body.dns.spf.expected).to.eql('v=spf1 a:example.com a:' + config.mailFqdn() + ' ~all');
expect(res.body.dns.spf.status).to.eql(true);
done();
@@ -463,10 +464,10 @@ describe('Mail API', function () {
it('succeeds with all correct records', function (done) {
clearDnsAnswerQueue();
dnsAnswerQueue[mxDomain].MX = [ { priority: '10', exchange: settings.mailFqdn() } ];
dnsAnswerQueue[mxDomain].MX = [ { priority: '10', exchange: config.mailFqdn() } ];
dnsAnswerQueue[dmarcDomain].TXT = [['v=DMARC1; p=reject; pct=100']];
dnsAnswerQueue[dkimDomain].TXT = [['v=DKIM1; t=s; p=', mail._readDkimPublicKeySync(DOMAIN_0.domain) ]];
dnsAnswerQueue[spfDomain].TXT = [['v=spf1 a:' + settings.adminFqdn() + ' ~all']];
dnsAnswerQueue[spfDomain].TXT = [['v=spf1 a:' + config.adminFqdn() + ' ~all']];
superagent.get(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/status')
.query({ access_token: token })
@@ -483,8 +484,8 @@ describe('Mail API', function () {
expect(res.body.dns.spf).to.be.an('object');
expect(res.body.dns.spf.domain).to.eql(spfDomain);
expect(res.body.dns.spf.type).to.eql('TXT');
expect(res.body.dns.spf.value).to.eql('v=spf1 a:' + settings.adminFqdn() + ' ~all');
expect(res.body.dns.spf.expected).to.eql('v=spf1 a:' + settings.adminFqdn() + ' ~all');
expect(res.body.dns.spf.value).to.eql('v=spf1 a:' + config.adminFqdn() + ' ~all');
expect(res.body.dns.spf.expected).to.eql('v=spf1 a:' + config.adminFqdn() + ' ~all');
expect(res.body.dns.spf.status).to.eql(true);
expect(res.body.dns.dmarc).to.be.an('object');
@@ -494,8 +495,8 @@ describe('Mail API', function () {
expect(res.body.dns.mx).to.be.an('object');
expect(res.body.dns.mx.status).to.eql(true);
expect(res.body.dns.mx.expected).to.eql('10 ' + settings.mailFqdn() + '.');
expect(res.body.dns.mx.value).to.eql('10 ' + settings.mailFqdn() + '.');
expect(res.body.dns.mx.expected).to.eql('10 ' + config.mailFqdn() + '.');
expect(res.body.dns.mx.value).to.eql('10 ' + config.mailFqdn() + '.');
done();
});
@@ -982,7 +983,7 @@ describe('Mail API', function () {
it('add succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/lists')
.send({ name: LIST_NAME, members: [ `admin2@${DOMAIN_0.domain}`, `${USERNAME}@${DOMAIN_0.domain}`] })
.send({ name: LIST_NAME, members: [ 'admin2', USERNAME ]})
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
@@ -992,7 +993,7 @@ describe('Mail API', function () {
it('add twice fails', function (done) {
superagent.post(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/lists')
.send({ name: LIST_NAME, members: [ `admin2@${DOMAIN_0.domain}`, `${USERNAME}@${DOMAIN_0.domain}`] })
.send({ name: LIST_NAME, members: [ 'admin2', USERNAME ] })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(409);
@@ -1019,7 +1020,7 @@ describe('Mail API', function () {
expect(res.body.list.ownerId).to.equal('admin');
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@${DOMAIN_0.domain}`, `superadmin@${DOMAIN_0.domain}` ]);
expect(res.body.list.members).to.eql([ 'admin2', 'superadmin' ]);
done();
});
});
@@ -1035,7 +1036,7 @@ describe('Mail API', function () {
expect(res.body.lists[0].ownerId).to.equal('admin');
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@${DOMAIN_0.domain}`, `superadmin@${DOMAIN_0.domain}` ]);
expect(res.body.lists[0].members).to.eql([ 'admin2', 'superadmin' ]);
done();
});
});
+63 -64
View File
@@ -11,7 +11,7 @@ var accesscontrol = require('../../accesscontrol.js'),
async = require('async'),
clientdb = require('../../clientdb.js'),
clients = require('../../clients.js'),
constants = require('../../constants.js'),
config = require('../../config.js'),
database = require('../../database.js'),
domains = require('../../domains.js'),
expect = require('expect.js'),
@@ -21,7 +21,6 @@ var accesscontrol = require('../../accesscontrol.js'),
querystring = require('querystring'),
request = require('request'),
server = require('../../server.js'),
settings = require('../../settings.js'),
speakeasy = require('speakeasy'),
superagent = require('superagent'),
urlParse = require('url').parse,
@@ -29,7 +28,7 @@ var accesscontrol = require('../../accesscontrol.js'),
users = require('../../users.js'),
uuid = require('uuid');
var SERVER_URL = 'http://localhost:' + constants.PORT;
var SERVER_URL = 'http://localhost:' + config.get('port');
let AUDIT_SOURCE = { ip: '1.2.3.4', userId: 'someuserid' };
@@ -54,8 +53,7 @@ describe('OAuth2', function () {
createdAt: (new Date()).toUTCString(),
modifiedAt: (new Date()).toUTCString(),
resetToken: hat(256),
displayName: '',
source: ''
displayName: ''
};
var APP_0 = {
@@ -67,8 +65,7 @@ describe('OAuth2', function () {
portBindings: {},
accessRestriction: null,
memoryLimit: 0,
installationState: 'pending_install',
runState: 'running'
ownerId: USER_0.id
};
var APP_1 = {
@@ -80,8 +77,7 @@ describe('OAuth2', function () {
portBindings: {},
accessRestriction: { users: [ 'foobar' ] },
memoryLimit: 0,
installationState: 'pending_install',
runState: 'running'
ownerId: USER_0.id
};
var APP_2 = {
@@ -93,8 +89,7 @@ describe('OAuth2', function () {
portBindings: {},
accessRestriction: { users: [ USER_0.id ] },
memoryLimit: 0,
installationState: 'pending_install',
runState: 'running'
ownerId: USER_0.id
};
var APP_3 = {
@@ -106,8 +101,7 @@ describe('OAuth2', function () {
portBindings: {},
accessRestriction: { groups: [ 'someothergroup', 'admin', 'anothergroup' ] },
memoryLimit: 0,
installationState: 'pending_install',
runState: 'running'
ownerId: USER_0.id
};
// unknown app
@@ -209,11 +203,14 @@ describe('OAuth2', function () {
};
function setup(done) {
config._reset();
config.setFqdn(APP_0.domain);
config.setAdminFqdn('my.' + APP_0.domain);
async.series([
server.start,
database._clear,
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0, AUDIT_SOURCE),
settings.setAdmin.bind(null, DOMAIN_0.domain, 'my.' + DOMAIN_0.domain),
clientdb.add.bind(null, CLIENT_0.id, CLIENT_0.appId, CLIENT_0.type, CLIENT_0.clientSecret, CLIENT_0.redirectURI, CLIENT_0.scope),
clientdb.add.bind(null, CLIENT_1.id, CLIENT_1.appId, CLIENT_1.type, CLIENT_1.clientSecret, CLIENT_1.redirectURI, CLIENT_1.scope),
clientdb.add.bind(null, CLIENT_2.id, CLIENT_2.appId, CLIENT_2.type, CLIENT_2.clientSecret, CLIENT_2.redirectURI, CLIENT_2.scope),
@@ -228,13 +225,14 @@ describe('OAuth2', function () {
expect(error).to.not.be.ok();
// update the global objects to reflect the new user id
USER_0.id = APP_0.ownerId = APP_1.ownerId = APP_2.ownerId = APP_3.ownerId = userObject.id;
APP_2.accessRestriction = { users: [ 'foobar', userObject.id ] };
async.series([
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, APP_0.portBindings, APP_0),
appdb.add.bind(null, APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, APP_1.domain, APP_1.portBindings, APP_1),
appdb.add.bind(null, APP_2.id, APP_2.appStoreId, APP_2.manifest, APP_2.location, APP_2.domain, APP_2.portBindings, APP_2),
appdb.add.bind(null, APP_3.id, APP_3.appStoreId, APP_3.manifest, APP_3.location, APP_3.domain, APP_3.portBindings, APP_3),
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, APP_0.ownerId, APP_0.portBindings, APP_0),
appdb.add.bind(null, APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, APP_1.domain, APP_1.ownerId, APP_1.portBindings, APP_1),
appdb.add.bind(null, APP_2.id, APP_2.appStoreId, APP_2.manifest, APP_2.location, APP_2.domain, APP_2.ownerId, APP_2.portBindings, APP_2),
appdb.add.bind(null, APP_3.id, APP_3.appStoreId, APP_3.manifest, APP_3.location, APP_3.domain, APP_3.ownerId, APP_3.portBindings, APP_3),
appdb.update.bind(null, APP_2.id, APP_2)
], callback);
@@ -375,7 +373,7 @@ describe('OAuth2', function () {
expect(response.statusCode).to.eql(200);
expect(body).to.eql('<script>window.location.href = "/api/v1/session/login?returnTo=' + CLIENT_0.redirectURI + '";</script>');
request.get(SERVER_URL + '/api/v1/session/login?returnTo=' + CLIENT_0.redirectURI, { jar: true, followRedirect: false }, function (error, response) {
request.get(SERVER_URL + '/api/v1/session/login?returnTo=' + CLIENT_0.redirectURI, { jar: true, followRedirect: false }, function (error, response, body) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(302);
expect(response.headers.location).to.eql(CLIENT_0.redirectURI);
@@ -392,7 +390,7 @@ describe('OAuth2', function () {
expect(response.statusCode).to.eql(200);
expect(body).to.eql('<script>window.location.href = "/api/v1/session/login?returnTo=' + CLIENT_1.redirectURI + '";</script>');
request.get(SERVER_URL + '/api/v1/session/login?returnTo=' + CLIENT_1.redirectURI, { jar: true, followRedirect: false }, function (error, response) {
request.get(SERVER_URL + '/api/v1/session/login?returnTo=' + CLIENT_1.redirectURI, { jar: true, followRedirect: false }, function (error, response, body) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(302);
expect(response.headers.location).to.eql(CLIENT_1.redirectURI);
@@ -443,7 +441,7 @@ describe('OAuth2', function () {
expect(response.statusCode).to.eql(200);
expect(body).to.eql('<script>window.location.href = "/api/v1/session/login?returnTo=' + CLIENT_4.redirectURI + '";</script>');
request.get(SERVER_URL + '/api/v1/session/login?returnTo=' + CLIENT_4.redirectURI, { jar: true, followRedirect: false }, function (error, response) {
request.get(SERVER_URL + '/api/v1/session/login?returnTo=' + CLIENT_4.redirectURI, { jar: true, followRedirect: false }, function (error, response, body) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(302);
expect(response.headers.location).to.eql(CLIENT_4.redirectURI);
@@ -499,7 +497,7 @@ describe('OAuth2', function () {
var url = SERVER_URL + '/api/v1/session/login?returnTo=' + CLIENT_2.redirectURI;
var data = {};
request.post({ url: url, jar: jar, form: data }, function (error, response) {
request.post({ url: url, jar: jar, form: data }, function (error, response, body) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(302);
@@ -520,7 +518,7 @@ describe('OAuth2', function () {
password: USER_0.password
};
request.post({ url: url, jar: jar, form: data }, function (error, response) {
request.post({ url: url, jar: jar, form: data }, function (error, response, body) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(302);
@@ -541,7 +539,7 @@ describe('OAuth2', function () {
password: 'password'
};
request.post({ url: url, jar: jar, form: data }, function (error, response) {
request.post({ url: url, jar: jar, form: data }, function (error, response, body) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(302);
@@ -562,7 +560,7 @@ describe('OAuth2', function () {
password: USER_0.password
};
request.post({ url: url, jar: jar, form: data }, function (error, response) {
request.post({ url: url, jar: jar, form: data }, function (error, response, body) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(302);
@@ -584,7 +582,7 @@ describe('OAuth2', function () {
password: USER_0.password
};
request.post({ url: url, jar: jar, form: data }, function (error, response) {
request.post({ url: url, jar: jar, form: data }, function (error, response, body) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(302);
@@ -623,7 +621,7 @@ describe('OAuth2', function () {
encoding: 'base32'
});
superagent.post(`${SERVER_URL}/api/v1/profile/twofactorauthentication/enable`).query({ access_token: accessToken }).send({ totpToken: totpToken }).end(function (error) {
superagent.post(`${SERVER_URL}/api/v1/profile/twofactorauthentication/enable`).query({ access_token: accessToken }).send({ totpToken: totpToken }).end(function (error, result) {
callback(error);
});
}
@@ -659,7 +657,7 @@ describe('OAuth2', function () {
password: USER_0.password
};
request.post({ url: url, jar: jar, form: data }, function (error, response) {
request.post({ url: url, jar: jar, form: data }, function (error, response, body) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(302);
@@ -681,7 +679,7 @@ describe('OAuth2', function () {
totpToken: 'wrongtoken'
};
request.post({ url: url, jar: jar, form: data }, function (error, response) {
request.post({ url: url, jar: jar, form: data }, function (error, response, body) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(302);
@@ -707,7 +705,7 @@ describe('OAuth2', function () {
totpToken: totpToken
};
request.post({ url: url, jar: jar, form: data }, function (error, response) {
request.post({ url: url, jar: jar, form: data }, function (error, response, body) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(302);
@@ -746,7 +744,7 @@ describe('OAuth2', function () {
password: USER_0.password
};
request.post({ url: url, jar: jar, form: data }, function (error, response) {
request.post({ url: url, jar: jar, form: data }, function (error, response, body) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(302);
@@ -765,7 +763,7 @@ describe('OAuth2', function () {
startAuthorizationFlow(CLIENT_2, 'code', function (jar) {
var url = SERVER_URL + '/api/v1/oauth/dialog/authorize?redirect_uri=' + CLIENT_2.redirectURI + '&client_id=' + CLIENT_2.id + '&response_type=code';
request.get(url, { jar: jar, followRedirect: false }, function (error, response) {
request.get(url, { jar: jar, followRedirect: false }, function (error, response, body) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(302);
@@ -782,7 +780,7 @@ describe('OAuth2', function () {
startAuthorizationFlow(CLIENT_2, 'token', function (jar) {
var url = SERVER_URL + '/api/v1/oauth/dialog/authorize?redirect_uri=' + CLIENT_2.redirectURI + '&client_id=' + CLIENT_2.id + '&response_type=token';
request.get(url, { jar: jar, followRedirect: false }, function (error, response) {
request.get(url, { jar: jar, followRedirect: false }, function (error, response, body) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(302);
@@ -817,7 +815,7 @@ describe('OAuth2', function () {
startAuthorizationFlow(CLIENT_7, 'code', function (jar) {
var url = SERVER_URL + '/api/v1/oauth/dialog/authorize?redirect_uri=' + CLIENT_7.redirectURI + '&client_id=' + CLIENT_7.id + '&response_type=code';
request.get(url, { jar: jar, followRedirect: false }, function (error, response) {
request.get(url, { jar: jar, followRedirect: false }, function (error, response, body) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(302);
@@ -864,7 +862,7 @@ describe('OAuth2', function () {
startAuthorizationFlow(CLIENT_7, 'token', function (jar) {
var url = SERVER_URL + '/api/v1/oauth/dialog/authorize?redirect_uri=' + CLIENT_7.redirectURI + '&client_id=' + CLIENT_7.id + '&response_type=token';
request.get(url, { jar: jar, followRedirect: false }, function (error, response) {
request.get(url, { jar: jar, followRedirect: false }, function (error, response, body) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(302);
@@ -890,7 +888,7 @@ describe('OAuth2', function () {
it('fails after logout', function (done) {
startAuthorizationFlow(CLIENT_2, 'token', function (jar) {
request.get(SERVER_URL + '/api/v1/session/logout', { jar: jar, followRedirect: false }, function (error, response) {
request.get(SERVER_URL + '/api/v1/session/logout', { jar: jar, followRedirect: false }, function (error, response, body) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(302);
expect(response.headers.location).to.eql('/');
@@ -910,7 +908,7 @@ describe('OAuth2', function () {
it('fails after logout width redirect', function (done) {
startAuthorizationFlow(CLIENT_2, 'token', function (jar) {
request.get(SERVER_URL + '/api/v1/session/logout', { jar: jar, followRedirect: false, qs: { redirect: 'http://foobar' } }, function (error, response) {
request.get(SERVER_URL + '/api/v1/session/logout', { jar: jar, followRedirect: false, qs: { redirect: 'http://foobar' } }, function (error, response, body) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(302);
expect(response.headers.location).to.eql('http://foobar');
@@ -952,7 +950,7 @@ describe('OAuth2', function () {
password: USER_0.password
};
request.post({ url: url, jar: jar, form: data }, function (error, response) {
request.post({ url: url, jar: jar, form: data }, function (error, response, body) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(302);
@@ -971,7 +969,7 @@ describe('OAuth2', function () {
startAuthorizationFlow('code', function (jar) {
var url = SERVER_URL + '/api/v1/oauth/dialog/authorize?redirect_uri=' + CLIENT_2.redirectURI + '&client_id=' + CLIENT_2.id + '&response_type=code';
request.get(url, { jar: jar, followRedirect: false }, function (error, response) {
request.get(url, { jar: jar, followRedirect: false }, function (error, response, body) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(302);
@@ -1024,7 +1022,7 @@ describe('OAuth2', function () {
password: USER_0.password
};
request.post({ url: url, jar: jar, form: data }, function (error, response) {
request.post({ url: url, jar: jar, form: data }, function (error, response, body) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(302);
@@ -1043,7 +1041,7 @@ describe('OAuth2', function () {
startAuthorizationFlow('code', function (jar) {
var url = SERVER_URL + '/api/v1/oauth/dialog/authorize?redirect_uri=' + CLIENT_2.redirectURI + '&client_id=' + CLIENT_2.id + '&response_type=code';
request.get(url, { jar: jar, followRedirect: false }, function (error, response) {
request.get(url, { jar: jar, followRedirect: false }, function (error, response, body) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(302);
@@ -1052,7 +1050,7 @@ describe('OAuth2', function () {
expect(tmp.query.redirectURI).to.eql(CLIENT_2.redirectURI + '/');
expect(tmp.query.code).to.be.a('string');
request.post(SERVER_URL + '/api/v1/oauth/token', { jar: jar }, function (error, response) {
request.post(SERVER_URL + '/api/v1/oauth/token', { jar: jar }, function (error, response, body) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(401);
@@ -1066,7 +1064,7 @@ describe('OAuth2', function () {
startAuthorizationFlow('code', function (jar) {
var url = SERVER_URL + '/api/v1/oauth/dialog/authorize?redirect_uri=' + CLIENT_2.redirectURI + '&client_id=' + CLIENT_2.id + '&response_type=code';
request.get(url, { jar: jar, followRedirect: false }, function (error, response) {
request.get(url, { jar: jar, followRedirect: false }, function (error, response, body) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(302);
@@ -1082,7 +1080,7 @@ describe('OAuth2', function () {
client_secret: CLIENT_2.clientSecret
};
request.post(SERVER_URL + '/api/v1/oauth/token', { jar: jar, json: data }, function (error, response) {
request.post(SERVER_URL + '/api/v1/oauth/token', { jar: jar, json: data }, function (error, response, body) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(401);
done();
@@ -1095,7 +1093,7 @@ describe('OAuth2', function () {
startAuthorizationFlow('code', function (jar) {
var url = SERVER_URL + '/api/v1/oauth/dialog/authorize?redirect_uri=' + CLIENT_2.redirectURI + '&client_id=' + CLIENT_2.id + '&response_type=code';
request.get(url, { jar: jar, followRedirect: false }, function (error, response) {
request.get(url, { jar: jar, followRedirect: false }, function (error, response, body) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(302);
@@ -1111,7 +1109,7 @@ describe('OAuth2', function () {
client_secret: CLIENT_2.clientSecret
};
request.post(SERVER_URL + '/api/v1/oauth/token', { jar: jar, json: data }, function (error, response) {
request.post(SERVER_URL + '/api/v1/oauth/token', { jar: jar, json: data }, function (error, response, body) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(501);
done();
@@ -1124,7 +1122,7 @@ describe('OAuth2', function () {
startAuthorizationFlow('code', function (jar) {
var url = SERVER_URL + '/api/v1/oauth/dialog/authorize?redirect_uri=' + CLIENT_2.redirectURI + '&client_id=' + CLIENT_2.id + '&response_type=code';
request.get(url, { jar: jar, followRedirect: false }, function (error, response) {
request.get(url, { jar: jar, followRedirect: false }, function (error, response, body) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(302);
@@ -1140,7 +1138,7 @@ describe('OAuth2', function () {
client_secret: CLIENT_2.clientSecret
};
request.post(SERVER_URL + '/api/v1/oauth/token', { jar: jar, json: data }, function (error, response) {
request.post(SERVER_URL + '/api/v1/oauth/token', { jar: jar, json: data }, function (error, response, body) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(400);
done();
@@ -1153,7 +1151,7 @@ describe('OAuth2', function () {
startAuthorizationFlow('code', function (jar) {
var url = SERVER_URL + '/api/v1/oauth/dialog/authorize?redirect_uri=' + CLIENT_2.redirectURI + '&client_id=' + CLIENT_2.id + '&response_type=code';
request.get(url, { jar: jar, followRedirect: false }, function (error, response) {
request.get(url, { jar: jar, followRedirect: false }, function (error, response, body) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(302);
@@ -1169,7 +1167,7 @@ describe('OAuth2', function () {
// client_secret: CLIENT_2.clientSecret
};
request.post(SERVER_URL + '/api/v1/oauth/token', { jar: jar, json: data }, function (error, response) {
request.post(SERVER_URL + '/api/v1/oauth/token', { jar: jar, json: data }, function (error, response, body) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(401);
done();
@@ -1182,7 +1180,7 @@ describe('OAuth2', function () {
startAuthorizationFlow('code', function (jar) {
var url = SERVER_URL + '/api/v1/oauth/dialog/authorize?redirect_uri=' + CLIENT_2.redirectURI + '&client_id=' + CLIENT_2.id + '&response_type=code';
request.get(url, { jar: jar, followRedirect: false }, function (error, response) {
request.get(url, { jar: jar, followRedirect: false }, function (error, response, body) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(302);
@@ -1198,7 +1196,7 @@ describe('OAuth2', function () {
client_secret: CLIENT_2.clientSecret+CLIENT_2.clientSecret
};
request.post(SERVER_URL + '/api/v1/oauth/token', { jar: jar, json: data }, function (error, response) {
request.post(SERVER_URL + '/api/v1/oauth/token', { jar: jar, json: data }, function (error, response, body) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(401);
done();
@@ -1211,7 +1209,7 @@ describe('OAuth2', function () {
startAuthorizationFlow('code', function (jar) {
var url = SERVER_URL + '/api/v1/oauth/dialog/authorize?redirect_uri=' + CLIENT_2.redirectURI + '&client_id=' + CLIENT_2.id + '&response_type=code';
request.get(url, { jar: jar, followRedirect: false }, function (error, response) {
request.get(url, { jar: jar, followRedirect: false }, function (error, response, body) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(302);
@@ -1227,7 +1225,7 @@ describe('OAuth2', function () {
client_secret: CLIENT_2.clientSecret
};
request.post(SERVER_URL + '/api/v1/oauth/token', { jar: jar, json: data }, function (error, response) {
request.post(SERVER_URL + '/api/v1/oauth/token', { jar: jar, json: data }, function (error, response, body) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(401);
done();
@@ -1240,7 +1238,7 @@ describe('OAuth2', function () {
startAuthorizationFlow('code', function (jar) {
var url = SERVER_URL + '/api/v1/oauth/dialog/authorize?redirect_uri=' + CLIENT_2.redirectURI + '&client_id=' + CLIENT_2.id + '&response_type=code';
request.get(url, { jar: jar, followRedirect: false }, function (error, response) {
request.get(url, { jar: jar, followRedirect: false }, function (error, response, body) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(302);
@@ -1290,8 +1288,7 @@ describe('Password', function () {
createdAt: (new Date()).toUTCString(),
modifiedAt: (new Date()).toUTCString(),
resetToken: hat(256),
displayName: '',
source: ''
displayName: ''
};
// make csrf always succeed for testing
@@ -1303,12 +1300,14 @@ describe('Password', function () {
};
function setup(done) {
async.series([
server.start,
database._clear,
settings.setAdmin.bind(null, 'example.com', 'my.example.com'),
userdb.add.bind(null, USER_0.userId, USER_0)
], done);
server.start(function (error) {
expect(error).to.not.be.ok();
database._clear(function (error) {
expect(error).to.not.be.ok();
userdb.add(USER_0.userId, USER_0, done);
});
});
}
function cleanup(done) {
@@ -1482,7 +1481,7 @@ describe('Password', function () {
});
it('succeeds', function (done) {
var scope = nock(settings.adminOrigin())
var scope = nock(config.adminOrigin())
.filteringPath(function (path) {
path = path.replace(/accessToken=[^&]*/, 'accessToken=token');
path = path.replace(/expiresAt=[^&]*/, 'expiresAt=1234');
+6 -3
View File
@@ -7,7 +7,7 @@
'use strict';
var accesscontrol = require('../../accesscontrol.js'),
constants = require('../../constants.js'),
config = require('../../config.js'),
database = require('../../database.js'),
expect = require('expect.js'),
hat = require('../../hat.js'),
@@ -16,7 +16,7 @@ var accesscontrol = require('../../accesscontrol.js'),
server = require('../../server.js'),
tokendb = require('../../tokendb.js');
const SERVER_URL = 'http://localhost:' + constants.PORT;
const SERVER_URL = 'http://localhost:' + config.get('port');
const USERNAME_0 = 'superaDmIn';
const PASSWORD = 'Foobar?1337';
@@ -30,6 +30,9 @@ describe('Profile API', function () {
var token_0;
function setup(done) {
config._reset();
config.setFqdn('example-profile-test.com');
server.start(function (error) {
expect(!error).to.be.ok();
@@ -245,7 +248,7 @@ describe('Profile API', function () {
.query({ access_token: token_0 })
.send({ password: 'some wrong password', newPassword: 'MOre#$%34' })
.end(function (err, res) {
expect(res.statusCode).to.equal(412);
expect(res.statusCode).to.equal(400);
done();
});
});
+4 -2
View File
@@ -7,18 +7,20 @@
'use strict';
var async = require('async'),
constants = require('../../constants.js'),
config = require('../../config.js'),
database = require('../../database.js'),
expect = require('expect.js'),
superagent = require('superagent'),
server = require('../../server.js');
var SERVER_URL = 'http://localhost:' + constants.PORT;
var SERVER_URL = 'http://localhost:' + config.get('port');
var DOMAIN = 'example-server-test.com';
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var token = null;
function setup(done) {
config._reset();
async.series([
server.start,
database._clear
+6 -1
View File
@@ -6,6 +6,7 @@
/* global after:false */
var async = require('async'),
config = require('../../config.js'),
constants = require('../../constants.js'),
database = require('../../database.js'),
expect = require('expect.js'),
@@ -14,12 +15,16 @@ var async = require('async'),
server = require('../../server.js'),
superagent = require('superagent');
var SERVER_URL = 'http://localhost:' + constants.PORT;
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-settings-test.com');
config.setAdminFqdn('my.example-settings-test.com');
async.series([
server.start.bind(null),
database._clear.bind(null),
+9 -6
View File
@@ -6,26 +6,27 @@
'use strict';
var async = require('async'),
constants = require('../../constants.js'),
config = require('../../config.js'),
database = require('../../database.js'),
expect = require('expect.js'),
nock = require('nock'),
path = require('path'),
paths = require('../../paths.js'),
safe = require('safetydance'),
settings = require('../../settings.js'),
settingsdb = require('../../settingsdb.js'),
superagent = require('superagent'),
server = require('../../server.js');
var SERVER_URL = 'http://localhost:' + constants.PORT;
var SERVER_URL = 'http://localhost:' + config.get('port');
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var AUTHORIZED_KEYS_FILE = path.join(paths.baseDir(), 'authorized_keys');
var AUTHORIZED_KEYS_FILE = path.join(config.baseDir(), 'authorized_keys');
var token = null;
function setup(done) {
nock.cleanAll();
config._reset();
config.setFqdn('example-ssh-test.com');
safe.fs.unlinkSync(AUTHORIZED_KEYS_FILE);
async.series([
@@ -56,6 +57,8 @@ function cleanup(done) {
database._clear(function (error) {
expect(error).to.not.be.ok();
config._reset();
server.stop(done);
});
}
@@ -226,7 +229,7 @@ describe('Support API', function () {
});
it('succeeds with ticket type', function (done) {
var scope2 = nock(settings.apiServerOrigin())
var scope2 = nock(config.apiServerOrigin())
.filteringRequestBody(function (/* unusedBody */) { return ''; }) // strip out body
.post('/api/v1/ticket?accessToken=CLOUDRON_TOKEN')
.reply(201, { });
@@ -242,7 +245,7 @@ describe('Support API', function () {
});
it('succeeds with app type', function (done) {
var scope2 = nock(settings.apiServerOrigin())
var scope2 = nock(config.apiServerOrigin())
.filteringRequestBody(function (/* unusedBody */) { return ''; }) // strip out body
.post('/api/v1/ticket?accessToken=CLOUDRON_TOKEN')
.reply(201, { });

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