Compare commits

..

9 Commits

Author SHA1 Message Date
Girish Ramakrishnan 837ce0a879 suppress reset-failed warning message
(cherry picked from commit f9f44b18ad)
2020-10-12 10:09:24 -07:00
Girish Ramakrishnan cdae1f0d06 restore: disable IP based api calls after all activation tasks
the restore code relies on the status call to get the domain to
redirect. if the IP/v1/cloudron/status does not respond, it will
fail the redirection.

(cherry picked from commit fa8a6bfc8c814b20c8bc04b629732a51e4edcf1f)
2020-10-07 10:57:28 -07:00
Johannes Zellner 96468dd931 Limit log files to last 1000 lines
(cherry picked from commit 645c1b9151)
2020-10-07 17:49:56 +02:00
Johannes Zellner a8949649a8 For app tickets, send the log files along
(cherry picked from commit 678fca6704)
2020-10-06 13:04:27 -07:00
Johannes Zellner a3fc7e9990 Support SSH remote enabling on ticket submission
(cherry picked from commit b74fae3762)
2020-10-06 13:04:21 -07:00
Johannes Zellner c749842eab Add enableSshSupport option to support tickets
(cherry picked from commit 2817ea833a)
2020-10-06 13:04:14 -07:00
Girish Ramakrishnan 503497dcc7 add changes
(cherry picked from commit b7ed6d8463)
2020-10-05 21:32:44 -07:00
Girish Ramakrishnan 516a822cd8 locations (primary, secondary) of an app must be updated together
do the delete first to clear out all the domains. this way, you can
move primary to redirect in a single shot.

(cherry picked from commit 005c33dbb5)
2020-10-05 16:17:09 -07:00
Girish Ramakrishnan 75eb8992a9 Disable focal for now 2020-10-05 12:46:26 -07:00
99 changed files with 956 additions and 2751 deletions
-39
View File
@@ -2114,44 +2114,5 @@
* Pre-select app domain by default in the redirection drop down
* robots: preseve leading and trailing whitespaces/newlines
[5.6.3]
* Fix postgres locale issue
[6.0.0]
* Focal support
* Reduce duration of self-signed certs to 800 days
* Better backup config filename when downloading
* branding: footer can have template variables like %YEAR% and %VERSION%
* sftp: secure the API with a token
* filemanager: Add extract context menu item
* Do not download docker images if present locally
* sftp: disable access to non-admins by default
* postgresql: whitelist pgcrypto extension for loomio
* filemanager: Add new file creation action and collapse new and upload actions
* rsync: add warning to remove lifecycle rules
* Add volume management
* backups: adjust node's heap size based on memory limit
* s3: diasble per-chunk timeout
* logs: more descriptive log file names on download
* collectd: remove collectd config when app stopped (and add it back when started)
* Apps can optionally request an authwall to be installed in front of them
* mailbox can now owned by a group
* linode: enable dns provider in setup view
* dns: apps can now use the dns port
* httpPaths: allow apps to specify forwarding from custom paths to container ports (for OLS)
* add elasticemail smtp relay option
* mail: add option to fts using solr
* mail: change the namespace separator of new installations to /
* mail: enable acl
* Disable THP
* filemanager: allow download dirs as zip files
* aws: add china region
* security: fix issue where apps could send with any username (but valid password)
* i18n support
[6.0.1]
* app: add export route
* mail: on location change, fix lock up when one or more domains have invalid credentials
* mail: fix crash because of write after timeout closure
* scaleway: fix installation issue where THP is not enabled in kernel
+4 -4
View File
@@ -29,7 +29,6 @@ debconf-set-selections <<< 'mysql-server mysql-server/root_password_again passwo
# this enables automatic security upgrades (https://help.ubuntu.com/community/AutomaticSecurityUpdates)
# resolvconf is needed for unbound to work property after disabling systemd-resolved in 18.04
gpg_package=$([[ "${ubuntu_version}" == "16.04" ]] && echo "gnupg" || echo "gpg")
mysql_package=$([[ "${ubuntu_version}" == "20.04" ]] && echo "mysql-server-8.0" || echo "mysql-server-5.7")
apt-get -y install \
@@ -57,7 +56,7 @@ apt-get -y install \
xfsprogs
echo "==> installing nginx for xenial for TLSv3 support"
curl -sL http://nginx.org/packages/ubuntu/pool/nginx/n/nginx/nginx_1.18.0-2~${ubuntu_codename}_amd64.deb -o /tmp/nginx.deb
curl -sL http://nginx.org/packages/ubuntu/pool/nginx/n/nginx/nginx_1.18.0-1~${ubuntu_codename}_amd64.deb -o /tmp/nginx.deb
# apt install with install deps (as opposed to dpkg -i)
apt install -y /tmp/nginx.deb
rm /tmp/nginx.deb
@@ -142,7 +141,7 @@ if [ -f "/etc/default/motd-news" ]; then
sed -i 's/^ENABLED=.*/ENABLED=0/' /etc/default/motd-news
fi
# Disable bind for good measure (on online.net, kimsufi servers these are pre-installed)
# Disable bind for good measure (on online.net, kimsufi servers these are pre-installed and conflicts with unbound)
systemctl stop bind9 || true
systemctl disable bind9 || true
@@ -154,7 +153,7 @@ systemctl disable dnsmasq || true
systemctl stop postfix || true
systemctl disable postfix || true
# on ubuntu 18.04 and 20.04, this is the default. this requires resolvconf for DNS to work further after the disable
# on ubuntu 18.04, this is the default. this requires resolvconf for DNS to work further after the disable
systemctl stop systemd-resolved || true
systemctl disable systemd-resolved || true
@@ -163,3 +162,4 @@ systemctl disable systemd-resolved || true
ip6=$([[ -s /proc/net/if_inet6 ]] && echo "yes" || echo "no")
echo -e "server:\n\tinterface: 127.0.0.1\n\tdo-ip6: ${ip6}" > /etc/unbound/unbound.conf.d/cloudron-network.conf
systemctl restart unbound
+1 -5
View File
@@ -7,7 +7,6 @@ let async = require('async'),
fs = require('fs'),
ldap = require('./src/ldap.js'),
paths = require('./src/paths.js'),
proxyAuth = require('./src/proxyauth.js'),
server = require('./src/server.js');
const NOOP_CALLBACK = function () { };
@@ -23,8 +22,7 @@ function setupLogging(callback) {
async.series([
setupLogging,
server.start, // do this first since it also inits the database
proxyAuth.start,
server.start,
ldap.start,
dockerProxy.start
], function (error) {
@@ -40,7 +38,6 @@ async.series([
process.on('SIGINT', function () {
debug('Received SIGINT. Shutting down.');
proxyAuth.stop(NOOP_CALLBACK);
server.stop(NOOP_CALLBACK);
ldap.stop(NOOP_CALLBACK);
dockerProxy.stop(NOOP_CALLBACK);
@@ -50,7 +47,6 @@ async.series([
process.on('SIGTERM', function () {
debug('Received SIGTERM. Shutting down.');
proxyAuth.stop(NOOP_CALLBACK);
server.stop(NOOP_CALLBACK);
ldap.stop(NOOP_CALLBACK);
dockerProxy.stop(NOOP_CALLBACK);
@@ -1,40 +0,0 @@
'use strict';
exports.up = function(db, callback) {
var cmd1 = 'CREATE TABLE volumes(' +
'id VARCHAR(128) NOT NULL UNIQUE,' +
'name VARCHAR(256) NOT NULL UNIQUE,' +
'hostPath VARCHAR(1024) NOT NULL UNIQUE,' +
'creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,' +
'PRIMARY KEY (id)) CHARACTER SET utf8 COLLATE utf8_bin';
var cmd2 = 'CREATE TABLE appMounts(' +
'appId VARCHAR(128) NOT NULL,' +
'volumeId VARCHAR(128) NOT NULL,' +
'readOnly BOOLEAN DEFAULT 1,' +
'UNIQUE KEY appMounts_appId_volumeId (appId, volumeId),' +
'FOREIGN KEY(appId) REFERENCES apps(id),' +
'FOREIGN KEY(volumeId) REFERENCES volumes(id)) CHARACTER SET utf8 COLLATE utf8_bin;';
db.runSql(cmd1, function (error) {
if (error) console.error(error);
db.runSql(cmd2, function (error) {
if (error) console.error(error);
db.runSql('ALTER TABLE apps DROP COLUMN bindsJson', callback);
});
});
};
exports.down = function(db, callback) {
db.runSql('DROP TABLE appMounts', function (error) {
if (error) console.error(error);
db.runSql('DROP TABLE volumes', function (error) {
if (error) console.error(error);
callback(error);
});
});
};
@@ -1,16 +0,0 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN proxyAuth BOOLEAN DEFAULT 0', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps DROP COLUMN proxyAuth', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,18 +0,0 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'ALTER TABLE mailboxes ADD COLUMN ownerType VARCHAR(16)'),
db.runSql.bind(db, 'UPDATE mailboxes SET ownerType=?', [ 'user' ]),
db.runSql.bind(db, 'ALTER TABLE mailboxes MODIFY ownerType VARCHAR(16) NOT NULL'),
], callback);
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE mailboxes DROP COLUMN ownerType', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,13 +0,0 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'ALTER TABLE apps DROP COLUMN httpPort')
], callback);
};
exports.down = function(db, callback) {
callback();
};
@@ -1,29 +0,0 @@
'use strict';
const async = require('async'),
iputils = require('../src/iputils.js');
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN containerIp VARCHAR(16) UNIQUE', function (error) {
if (error) console.error(error);
let baseIp = iputils.intFromIp('172.18.16.0');
db.all('SELECT * FROM apps', function (error, apps) {
if (error) return callback(error);
async.eachSeries(apps, function (app, iteratorDone) {
const nextIp = iputils.ipFromInt(++baseIp);
db.runSql('UPDATE apps SET containerIp=? WHERE id=?', [ nextIp, app.id ], iteratorDone);
}, callback);
});
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps DROP COLUMN containerIp', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,21 +0,0 @@
'use strict';
exports.up = function(db, callback) {
db.all('SELECT * FROM settings WHERE name=?', ['platform_config'], function (error, results) {
let value;
if (error || results.length === 0) {
value = { sftp: { requireAdmin: true } };
} else {
value = JSON.parse(results[0].value);
if (!value.sftp) value.sftp = {};
value.sftp.requireAdmin = true;
}
// existing installations may not even have the key. so use REPLACE instead of UPDATE
db.runSql('REPLACE INTO settings (name, value) VALUES (?, ?)', [ 'platform_config', JSON.stringify(value) ], callback);
});
};
exports.down = function(db, callback) {
callback();
};
+2 -18
View File
@@ -65,6 +65,7 @@ CREATE TABLE IF NOT EXISTS apps(
healthTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, // when the app last responded
containerId VARCHAR(128),
manifestJson TEXT,
httpPort INTEGER, // this is the nginx proxy port and not manifest.httpPort
accessRestrictionJson TEXT, // { users: [ ], groups: [ ] }
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, // when the app was installed
updateTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, // when the last app update was done
@@ -84,8 +85,8 @@ CREATE TABLE IF NOT EXISTS apps(
dataDir VARCHAR(256) UNIQUE,
taskId INTEGER, // current task
errorJson TEXT,
bindsJson TEXT, // bind mounts
servicesConfigJson TEXT, // app services configuration
containerIp VARCHAR(16) UNIQUE, // this is not-null because of ip allocation fails, user can 'repair'
FOREIGN KEY(mailboxDomain) REFERENCES domains(domain),
FOREIGN KEY(taskId) REFERENCES tasks(id),
@@ -180,7 +181,6 @@ CREATE TABLE IF NOT EXISTS mailboxes(
name VARCHAR(128) NOT NULL,
type VARCHAR(16) NOT NULL, /* 'mailbox', 'alias', 'list' */
ownerId VARCHAR(128) NOT NULL, /* user id */
ownerType VARCHAR(16) NOT NULL,
aliasName VARCHAR(128), /* the target name type is an alias */
aliasDomain VARCHAR(128), /* the target domain */
membersJson TEXT, /* members of a group. fully qualified */
@@ -237,20 +237,4 @@ CREATE TABLE IF NOT EXISTS appPasswords(
PRIMARY KEY (id)
);
CREATE TABLE IF NOT EXISTS volumes(
id VARCHAR(128) NOT NULL UNIQUE,
name VARCHAR(256) NOT NULL UNIQUE,
hostPath VARCHAR(1024) NOT NULL UNIQUE,
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
);
CREATE TABLE IF NOT EXISTS appMounts(
appId VARCHAR(128) NOT NULL,
volumeId VARCHAR(128) NOT NULL,
readOnly BOOLEAN DEFAULT 1,
UNIQUE KEY appMounts_appId_volumeId (appId, volumeId),
FOREIGN KEY(appId) REFERENCES apps(id),
FOREIGN KEY(volumeId) REFERENCES volumes(id));
CHARACTER SET utf8 COLLATE utf8_bin;
+6 -120
View File
@@ -322,9 +322,9 @@
}
},
"abstract-logging": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz",
"integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA=="
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.0.tgz",
"integrity": "sha512-/oA9z7JszpIioo6J6dB79LVUgJ3eD3cxkAmdCkvWWS+Y9tPtALs1rLqOekLUXUbYqM2fB9TTK0ibAyZJJOP/CA=="
},
"accepts": {
"version": "1.3.7",
@@ -743,9 +743,9 @@
}
},
"cloudron-manifestformat": {
"version": "5.9.0",
"resolved": "https://registry.npmjs.org/cloudron-manifestformat/-/cloudron-manifestformat-5.9.0.tgz",
"integrity": "sha512-bgHadG6s4PRCCPbWGeZ3lC1TTt9rIb/F1eXTQDym6AboXfBMDUO3fZeADISBNCP305pwyUgVqDFR5yfjm/wOKA==",
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/cloudron-manifestformat/-/cloudron-manifestformat-5.6.0.tgz",
"integrity": "sha512-BqM2vw/OWUHmPmrQo3xwAME0ncX3JPmPtxrhOYy0ZRpNcRDLrwXz02WVM9hAvIoawJNJjVb+x22RQoa1y5DdMw==",
"requires": {
"cron": "^1.8.2",
"java-packagename-regex": "^1.0.0",
@@ -950,20 +950,6 @@
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
"integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
},
"cookie": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
"integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg=="
},
"cookie-parser": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.5.tgz",
"integrity": "sha512-f13bPUj/gG/5mDr+xLmSxxDsB9DQiTIfhJS/sqjrmfAWiAN+x2O4i/XguTL9yDZ+/IFDanJ+5x7hC4CXT9Tdzw==",
"requires": {
"cookie": "0.4.0",
"cookie-signature": "1.0.6"
}
},
"cookie-session": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/cookie-session/-/cookie-session-1.4.0.tgz",
@@ -2643,49 +2629,6 @@
"resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz",
"integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA="
},
"jsonwebtoken": {
"version": "8.5.1",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz",
"integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==",
"requires": {
"jws": "^3.2.2",
"lodash.includes": "^4.3.0",
"lodash.isboolean": "^3.0.3",
"lodash.isinteger": "^4.0.4",
"lodash.isnumber": "^3.0.3",
"lodash.isplainobject": "^4.0.6",
"lodash.isstring": "^4.0.1",
"lodash.once": "^4.0.0",
"ms": "^2.1.1",
"semver": "^5.6.0"
},
"dependencies": {
"jwa": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
"integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
"requires": {
"buffer-equal-constant-time": "1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"jws": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
"requires": {
"jwa": "^1.4.1",
"safe-buffer": "^5.0.1"
}
},
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
}
}
},
"jsprim": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
@@ -2804,41 +2747,6 @@
"resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz",
"integrity": "sha1-Cwih3PaDl8OXhVwyOXg4Mt90A9E="
},
"lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
"integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8="
},
"lodash.isboolean": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
"integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY="
},
"lodash.isinteger": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
"integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M="
},
"lodash.isnumber": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
"integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w="
},
"lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs="
},
"lodash.isstring": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
"integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE="
},
"lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
"integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w="
},
"log-symbols": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz",
@@ -3207,28 +3115,6 @@
}
}
},
"mustache": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/mustache/-/mustache-3.2.1.tgz",
"integrity": "sha512-RERvMFdLpaFfSRIEe632yDm5nsd0SDKn8hGmcUwswnyiE5mtdZLDybtHAz6hjJhawokF0hXvGLtx9mrQfm6FkA=="
},
"mustache-express": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/mustache-express/-/mustache-express-1.3.0.tgz",
"integrity": "sha512-JWG8Rzxh9tpoLEH0NZ2u/caDiwhIkW+50IOBrcO+lHya3tCYj41bYPDEHCxPbKXvPrSyMNpI6ly4xdU2zpNQtg==",
"requires": {
"async": "~3.1.0",
"lru-cache": "~5.1.1",
"mustache": "^3.1.0"
},
"dependencies": {
"async": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/async/-/async-3.1.1.tgz",
"integrity": "sha512-X5Dj8hK1pJNC2Wzo2Rcp9FBVdJMGRR/S7V+lH46s8GVFhtbo5O4Le5GECCF/8PISVdkUA6mMPvgz7qTTD1rf1g=="
}
}
},
"mute-stream": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
+1 -5
View File
@@ -19,13 +19,11 @@
"@sindresorhus/df": "git+https://github.com/cloudron-io/df.git#type",
"async": "^2.6.3",
"aws-sdk": "^2.759.0",
"basic-auth": "^2.0.1",
"body-parser": "^1.19.0",
"cloudron-manifestformat": "^5.9.0",
"cloudron-manifestformat": "^5.6.0",
"connect": "^3.7.0",
"connect-lastmile": "^2.0.0",
"connect-timeout": "^1.9.0",
"cookie-parser": "^1.4.5",
"cookie-session": "^1.4.0",
"cron": "^1.8.2",
"db-migrate": "^0.11.11",
@@ -38,7 +36,6 @@
"ipaddr.js": "^2.0.0",
"js-yaml": "^3.14.0",
"json": "^9.0.6",
"jsonwebtoken": "^8.5.1",
"ldapjs": "^2.2.0",
"lodash": "^4.17.20",
"lodash.chunk": "^4.2.0",
@@ -47,7 +44,6 @@
"moment-timezone": "^0.5.31",
"morgan": "^1.10.0",
"multiparty": "^4.2.2",
"mustache-express": "^1.3.0",
"mysql": "^2.18.1",
"nodemailer": "^6.4.11",
"nodemailer-smtp-transport": "^2.7.4",
+6 -13
View File
@@ -2,11 +2,11 @@
set -eu
readonly source_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SOURCE_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly DATA_DIR="${HOME}/.cloudron_test"
readonly DEFAULT_TESTS="./src/test/*-test.js ./src/routes/test/*-test.js"
! "${source_dir}/src/test/checkInstall" && exit 1
! "${SOURCE_dir}/src/test/checkInstall" && exit 1
# cleanup old data dirs some of those docker container data requires sudo to be removed
echo "=> Provide root password to purge any leftover data in ${DATA_DIR} and load apparmor profile:"
@@ -22,26 +22,19 @@ fi
mkdir -p ${DATA_DIR}
cd ${DATA_DIR}
mkdir -p appsdata
mkdir -p boxdata/profileicons boxdata/appicons boxdata/mail boxdata/certs boxdata/mail/dkim/localhost boxdata/mail/dkim/foobar.com boxdata/sftp/ssh
mkdir -p platformdata/addons/mail/banner platformdata/nginx/cert platformdata/nginx/applications platformdata/collectd/collectd.conf.d platformdata/addons platformdata/logrotate.d platformdata/backup platformdata/logs/tasks
# translations
mkdir -p box/dashboard/dist/translation
cp -r ${source_dir}/../dashboard/dist/translation/* box/dashboard/dist/translation
mkdir -p boxdata/profileicons boxdata/appicons boxdata/mail boxdata/certs boxdata/mail/dkim/localhost boxdata/mail/dkim/foobar.com
mkdir -p platformdata/addons/mail platformdata/nginx/cert platformdata/nginx/applications platformdata/collectd/collectd.conf.d platformdata/addons platformdata/logrotate.d platformdata/backup platformdata/logs/tasks
# put cert
echo "=> Generating a localhost selfsigned cert"
openssl req -x509 -newkey rsa:2048 -keyout platformdata/nginx/cert/host.key -out platformdata/nginx/cert/host.cert -days 3650 -subj '/CN=localhost' -nodes -config <(cat /etc/ssl/openssl.cnf <(printf "\n[SAN]\nsubjectAltName=DNS:*.localhost"))
# generate legacy key format for sftp
ssh-keygen -m PEM -t rsa -f boxdata/sftp/ssh/ssh_host_rsa_key -q -N ""
# clear out any containers
echo "=> Delete all docker containers first"
docker ps -qa | xargs --no-run-if-empty docker rm -f
# create docker network (while the infra code does this, most tests skip infra setup)
docker network create --subnet=172.18.0.0/16 --ip-range=172.18.0.0/20 cloudron || true
docker network create --subnet=172.18.0.0/16 cloudron || true
# create the same mysql server version to test with
OUT=`docker inspect mysql-server` || true
@@ -66,7 +59,7 @@ echo "=> Ensure database"
mysql -h"${MYSQL_IP}" -uroot -ppassword -e 'CREATE DATABASE IF NOT EXISTS box'
echo "=> Run database migrations"
cd "${source_dir}"
cd "${SOURCE_dir}"
BOX_ENV=test DATABASE_URL=mysql://root:password@${MYSQL_IP}/box node_modules/.bin/db-migrate up
echo "=> Run tests with mocha"
+3 -3
View File
@@ -80,8 +80,8 @@ fi
# Only --help works with mismatched ubuntu
ubuntu_version=$(lsb_release -rs)
if [[ "${ubuntu_version}" != "16.04" && "${ubuntu_version}" != "18.04" && "${ubuntu_version}" != "20.04" ]]; then
echo "Cloudron requires Ubuntu 16.04, 18.04 or 20.04" > /dev/stderr
if [[ "${ubuntu_version}" != "16.04" && "${ubuntu_version}" != "18.04" ]]; then
echo "Cloudron requires Ubuntu 16.04 or 18.04" > /dev/stderr
exit 1
fi
@@ -178,7 +178,7 @@ done
if ! ip=$(curl -s --fail --connect-timeout 2 --max-time 2 https://api.cloudron.io/api/v1/helper/public_ip | sed -n -e 's/.*"ip": "\(.*\)"/\1/p'); then
ip='<IP>'
fi
echo -e "\n\n${GREEN}After reboot, visit https://${ip} and accept the self-signed certificate to finish setup.${DONE}\n"
echo -e "\n\n${GREEN}Visit https://${ip} and accept the self-signed certificate to finish setup.${DONE}\n"
if [[ "${rebootServer}" == "true" ]]; then
systemctl stop box mysql # sometimes mysql ends up having corrupt privilege tables
+1 -1
View File
@@ -42,7 +42,7 @@ while true; do
ghost_file=/home/yellowtent/platformdata/cloudron_ghost.json
printf '{"%s":"%s"}\n' "${admin_username}" "${admin_password}" > "${ghost_file}"
chown yellowtent:yellowtent "${ghost_file}" && chmod o-r,g-r "${ghost_file}"
echo "Login as ${admin_username} / ${admin_password} . This password may only be used once. ${ghost_file} will be automatically removed after use."
echo "Login as ${admin_username} / ${admin_password} . Remove ${ghost_file} when done."
exit 0
;;
--) break;;
-31
View File
@@ -1,31 +0,0 @@
#!/bin/bash
set -eu -o pipefail
# This script downloads new translation data from weblate at https://translate.cloudron.io
OUT="/home/yellowtent/box/dashboard/dist/translation"
# We require root
if [[ ${EUID} -ne 0 ]]; then
echo "This script should be run as root. Run with sudo"
exit 1
fi
echo "=> Downloading new translation files..."
curl https://translate.cloudron.io/download/cloudron/dashboard/?format=zip -o /tmp/lang.zip
echo "=> Unpacking..."
unzip -jo /tmp/lang.zip -d $OUT
chown -R yellowtent:yellowtent $OUT
# unzip put very restrictive permissions
chmod ua+r $OUT/*
echo "=> Cleanup..."
rm /tmp/lang.zip
echo "=> Done"
echo ""
echo "Reload the dashboard to see the new translations"
echo ""
+1 -7
View File
@@ -60,7 +60,7 @@ fi
readonly nginx_version=$(nginx -v 2>&1)
if [[ "${nginx_version}" != *"1.18."* ]]; then
echo "==> installer: installing nginx 1.18"
curl -sL http://nginx.org/packages/ubuntu/pool/nginx/n/nginx/nginx_1.18.0-2~${ubuntu_codename}_amd64.deb -o /tmp/nginx.deb
curl -sL http://nginx.org/packages/ubuntu/pool/nginx/n/nginx/nginx_1.18.0-1~${ubuntu_codename}_amd64.deb -o /tmp/nginx.deb
# apt install with install deps (as opposed to dpkg -i)
apt install -y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" --force-yes /tmp/nginx.deb
rm /tmp/nginx.deb
@@ -71,12 +71,6 @@ if ! which ipset; then
apt install -y ipset
fi
# Only used for the cloudron-translation-update script
if ! which unzip; then
echo "==> installer: installing unzip"
apt install -y unzip
fi
echo "==> installer: updating node"
if [[ "$(node --version)" != "v10.18.1" ]]; then
mkdir -p /usr/local/node-10.18.1
+7 -16
View File
@@ -19,7 +19,6 @@ readonly json="$(realpath ${script_dir}/../node_modules/.bin/json)"
readonly ubuntu_version=$(lsb_release -rs)
cp -f "${script_dir}/../scripts/cloudron-support" /usr/bin/cloudron-support
cp -f "${script_dir}/../scripts/cloudron-translation-update" /usr/bin/cloudron-translation-update
# this needs to match the cloudron/base:2.0.0 gid
if ! getent group media; then
@@ -32,8 +31,7 @@ systemctl enable apparmor
systemctl restart apparmor
usermod ${USER} -a -G docker
# unbound (which starts after box code) relies on this interface to exist. dockerproxy also relies on this.
docker network create --subnet=172.18.0.0/16 --ip-range=172.18.0.0/20 cloudron || true
docker network create --subnet=172.18.0.0/16 cloudron || true
mkdir -p "${BOX_DATA_DIR}"
mkdir -p "${APPS_DATA_DIR}"
@@ -65,7 +63,6 @@ mkdir -p "${BOX_DATA_DIR}/certs"
mkdir -p "${BOX_DATA_DIR}/acme" # acme keys
mkdir -p "${BOX_DATA_DIR}/mail/dkim"
mkdir -p "${BOX_DATA_DIR}/well-known" # .well-known documents
mkdir -p "${BOX_DATA_DIR}/sftp/ssh" # sftp keys
# ensure backups folder exists and is writeable
mkdir -p /var/backups
@@ -104,13 +101,14 @@ unbound-anchor -a /var/lib/unbound/root.key
echo "==> Adding systemd services"
cp -r "${script_dir}/start/systemd/." /etc/systemd/system/
systemctl disable cloudron.target || true
rm -f /etc/systemd/system/cloudron.target
[[ "${ubuntu_version}" == "16.04" ]] && sed -e 's/MemoryMax/MemoryLimit/g' -i /etc/systemd/system/box.service
systemctl daemon-reload
systemctl enable --now cloudron-syslog
systemctl enable unbound
systemctl enable cloudron-syslog
systemctl enable box
systemctl enable cloudron-firewall
systemctl enable --now cloudron-disable-thp
# update firewall rules
systemctl restart cloudron-firewall
@@ -220,20 +218,13 @@ else
cp "${BOX_DATA_DIR}/dhparams.pem" "${PLATFORM_DATA_DIR}/addons/mail/dhparams.pem"
fi
if [[ ! -f "${BOX_DATA_DIR}/sftp/ssh/ssh_host_rsa_key" ]]; then
# the key format in Ubuntu 20 changed, so we create keys in legacy format. for older ubuntu, just re-use the host keys
# see https://github.com/proftpd/proftpd/issues/793
if [[ "${ubuntu_version}" == "20.04" ]]; then
ssh-keygen -m PEM -t rsa -f "${BOX_DATA_DIR}/sftp/ssh/ssh_host_rsa_key" -q -N ""
else
cp /etc/ssh/ssh_host_rsa_key* ${BOX_DATA_DIR}/sftp/ssh
fi
fi
# old installations used to create appdata/<app>/redis which is now part of old backups and prevents restore
echo "==> Cleaning up stale redis directories"
find "${APPS_DATA_DIR}" -maxdepth 2 -type d -name redis -exec rm -rf {} +
echo "==> Cleaning up old logs"
rm -f /home/yellowtent/platformdata/logs/*/*.log.* || true
echo "==> Changing ownership"
# be careful of what is chown'ed here. subdirs like mysql,redis etc are owned by the containers and will stop working if perms change
chown -R "${USER}" /etc/cloudron
-14
View File
@@ -1,14 +0,0 @@
#!/bin/bash
set -eu
echo "==> Disabling THP"
# https://docs.couchbase.com/server/current/install/thp-disable.html
if [[ -d /sys/kernel/mm/transparent_hugepage ]]; then
echo "never" > /sys/kernel/mm/transparent_hugepage/enabled
echo "never" > /sys/kernel/mm/transparent_hugepage/defrag
else
echo "==> kernel does not have THP"
fi
-4
View File
@@ -26,10 +26,6 @@ if allowed_tcp_ports=$(node -e "console.log(JSON.parse(fs.readFileSync('${ports_
[[ -n "${allowed_tcp_ports}" ]] && iptables -A CLOUDRON -p tcp -m tcp -m multiport --dports "${allowed_tcp_ports}" -j ACCEPT
fi
if allowed_udp_ports=$(node -e "console.log(JSON.parse(fs.readFileSync('${ports_json}', 'utf8')).allowed_udp_ports.join(','))" 2>/dev/null); then
[[ -n "${allowed_tcp_ports}" ]] && iptables -A CLOUDRON -p udp -m udp -m multiport --dports "${allowed_tcp_ports}" -j ACCEPT
fi
# turn and stun service
iptables -t filter -A CLOUDRON -p tcp -m multiport --dports 3478,5349 -j ACCEPT
iptables -t filter -A CLOUDRON -p udp -m multiport --dports 3478,5349 -j ACCEPT
+1 -1
View File
@@ -6,7 +6,7 @@ disks = []
def init():
global disks
lines = [s.split() for s in subprocess.check_output(["df", "--type=ext4", "--output=source,target,size,used,avail"]).decode('utf-8').splitlines()]
lines = [s.split() for s in subprocess.check_output(["df", "--type=ext4", "--output=source,target,size,used,avail"]).splitlines()]
disks = lines[1:] # strip header
collectd.info('custom df plugin initialized with %s' % disks)
+1 -1
View File
@@ -6,7 +6,7 @@
performance_schema=OFF
max_connections=50
# on ec2, without this we get a sporadic connection drop when doing the initial migration
max_allowed_packet=64M
max_allowed_packet=32M
# https://mathiasbynens.be/notes/mysql-utf8mb4
character-set-server = utf8mb4
@@ -1,15 +0,0 @@
# https://docs.mongodb.com/manual/tutorial/transparent-huge-pages/
[Unit]
Description=Disable Transparent Huge Pages (THP)
DefaultDependencies=no
After=sysinit.target local-fs.target
Before=docker.service
[Service]
Type=oneshot
ExecStart="/home/yellowtent/box/setup/start/cloudron-disable-thp.sh"
RemainAfterExit=yes
[Install]
WantedBy=basic.target
+1 -1
View File
@@ -2,7 +2,7 @@
[Unit]
Description=Unbound DNS Resolver
After=network.target docker.service
After=network.target
[Service]
PIDFile=/run/unbound.pid
+1 -3
View File
@@ -1,7 +1,5 @@
server:
port: 53
interface: 127.0.0.1
interface: 172.18.0.1
interface: 0.0.0.0
do-ip6: no
access-control: 127.0.0.1 allow
access-control: 172.18.0.1/16 allow
+14 -48
View File
@@ -3,7 +3,6 @@
exports = module.exports = {
getServices,
getService,
getServicesConfig,
configureService,
getServiceLogs,
restartService,
@@ -117,13 +116,6 @@ var ADDONS = {
restore: restorePostgreSql,
clear: clearPostgreSql,
},
proxyAuth: {
setup: setupProxyAuth,
teardown: teardownProxyAuth,
backup: NOOP,
restore: NOOP,
clear: NOOP
},
recvmail: {
setup: setupRecvMail,
teardown: teardownRecvMail,
@@ -183,7 +175,7 @@ const SERVICES = {
mongodb: {
status: containerStatus.bind(null, 'mongodb', 'CLOUDRON_MONGODB_TOKEN'),
restart: restartContainer.bind(null, 'mongodb'),
defaultMemoryLimit: (1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 256 * 1024 * 1024
defaultMemoryLimit: (1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 200 * 1024 * 1024
},
mysql: {
status: containerStatus.bind(null, 'mysql', 'CLOUDRON_MYSQL_TOKEN'),
@@ -326,7 +318,7 @@ function containerStatus(containerName, tokenEnvName, callback) {
if (error && (error.reason === BoxError.NOT_FOUND || error.reason === BoxError.INACTIVE)) return callback(null, { status: exports.SERVICE_STATUS_STOPPED });
if (error) return callback(error);
request.get(`https://${addonDetails.ip}:3000/healthcheck?access_token=${addonDetails.token}`, { json: true, rejectUnauthorized: false, timeout: 3000 }, function (error, response) {
request.get(`https://${addonDetails.ip}:3000/healthcheck?access_token=${addonDetails.token}`, { json: true, rejectUnauthorized: false }, function (error, response) {
if (error) return callback(null, { status: exports.SERVICE_STATUS_STARTING, error: `Error waiting for ${containerName}: ${error.message}` });
if (response.statusCode !== 200 || !response.body.status) return callback(null, { status: exports.SERVICE_STATUS_STARTING, error: `Error waiting for ${containerName}. Status code: ${response.statusCode} message: ${response.body.message}` });
@@ -336,8 +328,7 @@ function containerStatus(containerName, tokenEnvName, callback) {
var tmp = {
status: addonDetails.state.Running ? exports.SERVICE_STATUS_ACTIVE : exports.SERVICE_STATUS_STOPPED,
memoryUsed: result.memory_stats.usage,
memoryPercent: parseInt(100 * result.memory_stats.usage / result.memory_stats.limit),
healthcheck: response.body
memoryPercent: parseInt(100 * result.memory_stats.usage / result.memory_stats.limit)
};
callback(null, tmp);
@@ -406,7 +397,6 @@ function getService(id, callback) {
memoryUsed: 0,
memoryPercent: 0,
error: null,
healthcheck: null,
config: {
// If a property is not set then we cannot change it through the api, see below
// memory: 0,
@@ -421,15 +411,16 @@ function getService(id, callback) {
tmp.memoryUsed = result.memoryUsed;
tmp.memoryPercent = result.memoryPercent;
tmp.error = result.error || null;
tmp.healthcheck = result.healthcheck || null;
getServicesConfig(id, function (error, service, servicesConfig) {
if (error) return callback(error);
const serviceConfig = servicesConfig[name];
tmp.config = Object.assign({}, serviceConfig);
if ((!tmp.config.memory || !tmp.config.memorySwap) && service.defaultMemoryLimit) {
if (serviceConfig && serviceConfig.memory && serviceConfig.memorySwap) {
tmp.config.memory = serviceConfig.memory;
tmp.config.memorySwap = serviceConfig.memorySwap;
} else if (service.defaultMemoryLimit) {
tmp.config.memory = service.defaultMemoryLimit;
tmp.config.memorySwap = tmp.config.memory * 2;
}
@@ -459,10 +450,10 @@ function configureService(id, data, callback) {
// if not specified we clear the entry and use defaults
if (!data.memory || !data.memorySwap) {
delete servicesConfig[name].memory;
delete servicesConfig[name].memorySwap;
delete servicesConfig[name];
} else {
servicesConfig[name] = data;
servicesConfig[name].memory = data.memory;
servicesConfig[name].memorySwap = data.memorySwap;
}
if (instance) {
@@ -623,7 +614,7 @@ function waitForContainer(containerName, tokenEnvName, callback) {
if (error) return callback(error);
async.retry({ times: 10, interval: 15000 }, function (retryCallback) {
request.get(`https://${result.ip}:3000/healthcheck?access_token=${result.token}`, { json: true, rejectUnauthorized: false, timeout: 3000 }, function (error, response) {
request.get(`https://${result.ip}:3000/healthcheck?access_token=${result.token}`, { json: true, rejectUnauthorized: false }, function (error, response) {
if (error) return retryCallback(new BoxError(BoxError.ADDONS_ERROR, `Network error waiting for ${containerName}: ${error.message}`));
if (response.statusCode !== 200 || !response.body.status) return retryCallback(new BoxError(BoxError.ADDONS_ERROR, `Error waiting for ${containerName}. Status code: ${response.statusCode} message: ${response.body.message}`));
@@ -1462,8 +1453,7 @@ function setupPostgreSql(app, options, callback) {
const data = {
database: database,
username: username,
password: error ? hat(4 * 128) : existingPassword,
locale: options.locale || 'C'
password: error ? hat(4 * 128) : existingPassword
};
getContainerDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
@@ -1497,14 +1487,13 @@ function clearPostgreSql(app, options, callback) {
assert.strictEqual(typeof callback, 'function');
const { database, username } = postgreSqlNames(app.id);
const locale = options.locale || 'C';
debugApp(app, 'Clearing postgresql');
getContainerDetails('postgresql', 'CLOUDRON_POSTGRESQL_TOKEN', function (error, result) {
if (error) return callback(error);
request.post(`https://${result.ip}:3000/databases/${database}/clear?access_token=${result.token}&username=${username}&locale=${locale}`, { json: true, rejectUnauthorized: false }, function (error, response) {
request.post(`https://${result.ip}:3000/databases/${database}/clear?access_token=${result.token}&username=${username}`, { json: true, rejectUnauthorized: false }, function (error, response) {
if (error) return callback(new BoxError(BoxError.ADDONS_ERROR, `Network error clearing postgresql: ${error.message}`));
if (response.statusCode !== 200) return callback(new BoxError(BoxError.ADDONS_ERROR, `Error clearing postgresql. Status code: ${response.statusCode} message: ${response.body.message}`));
@@ -1592,7 +1581,7 @@ function startTurn(existingInfra, callback) {
const memoryLimit = 256;
const realm = settings.adminFqdn();
// this exports 3478/tcp, 5349/tls and 50000-51000/udp. note that this runs on the host network!
// this exports 3478/tcp, 5349/tls and 50000-51000/udp
const cmd = `docker run --restart=always -d --name="turn" \
--hostname turn \
--net host \
@@ -1809,29 +1798,6 @@ function restoreMongoDb(app, options, callback) {
});
}
function setupProxyAuth(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'Setting up proxyAuth');
const enabled = app.sso && app.manifest.addons && app.manifest.addons.proxyAuth;
if (!enabled) return callback();
const env = [ { name: 'CLOUDRON_PROXY_AUTH', value: '1' } ];
appdb.setAddonConfig(app.id, 'proxyauth', env, callback);
}
function teardownProxyAuth(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
appdb.unsetAddonConfig(app.id, 'proxyauth', callback);
}
function startRedis(existingInfra, callback) {
assert.strictEqual(typeof existingInfra, 'object');
assert.strictEqual(typeof callback, 'function');
+82 -57
View File
@@ -1,27 +1,28 @@
'use strict';
exports = module.exports = {
get,
add,
exists,
del,
update,
getAll,
getPortBindings,
delPortBinding,
get: get,
getByHttpPort: getByHttpPort,
getByContainerId: getByContainerId,
add: add,
exists: exists,
del: del,
update: update,
getAll: getAll,
getPortBindings: getPortBindings,
delPortBinding: delPortBinding,
setAddonConfig,
getAddonConfig,
getAddonConfigByAppId,
getAddonConfigByName,
unsetAddonConfig,
unsetAddonConfigByAppId,
getAppIdByAddonConfigValue,
getByIpAddress,
setAddonConfig: setAddonConfig,
getAddonConfig: getAddonConfig,
getAddonConfigByAppId: getAddonConfigByAppId,
getAddonConfigByName: getAddonConfigByName,
unsetAddonConfig: unsetAddonConfig,
unsetAddonConfigByAppId: unsetAddonConfigByAppId,
getAppIdByAddonConfigValue: getAppIdByAddonConfigValue,
setHealth,
setTask,
getAppStoreIds,
setHealth: setHealth,
setTask: setTask,
getAppStoreIds: getAppStoreIds,
// subdomain table types
SUBDOMAIN_TYPE_PRIMARY: 'primary',
@@ -38,9 +39,10 @@ var assert = require('assert'),
util = require('util');
var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationState', 'apps.errorJson', 'apps.runState',
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.accessRestrictionJson', 'apps.memoryLimit', 'apps.cpuShares',
'apps.label', 'apps.tagsJson', 'apps.taskId', 'apps.reverseProxyConfigJson', 'apps.servicesConfigJson',
'apps.sso', 'apps.debugModeJson', 'apps.enableBackup', 'apps.proxyAuth', 'apps.containerIp',
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.httpPort', 'subdomains.subdomain AS location', 'subdomains.domain',
'apps.accessRestrictionJson', 'apps.memoryLimit', 'apps.cpuShares',
'apps.label', 'apps.tagsJson', 'apps.taskId', 'apps.reverseProxyConfigJson', 'apps.servicesConfigJson', 'apps.bindsJson',
'apps.sso', 'apps.debugModeJson', 'apps.enableBackup',
'apps.creationTime', 'apps.updateTime', 'apps.mailboxName', 'apps.mailboxDomain', 'apps.enableAutomaticUpdate',
'apps.dataDir', 'apps.ts', 'apps.healthTime' ].join(',');
@@ -87,7 +89,6 @@ function postProcess(result) {
result.sso = !!result.sso; // make it bool
result.enableBackup = !!result.enableBackup; // make it bool
result.enableAutomaticUpdate = !!result.enableAutomaticUpdate; // make it bool
result.proxyAuth = !!result.proxyAuth;
assert(result.debugModeJson === null || typeof result.debugModeJson === 'string');
result.debugMode = safe.JSON.parse(result.debugModeJson);
@@ -97,6 +98,10 @@ function postProcess(result) {
result.servicesConfig = safe.JSON.parse(result.servicesConfigJson) || {};
delete result.servicesConfigJson;
assert(result.bindsJson === null || typeof result.bindsJson === 'string');
result.binds = safe.JSON.parse(result.bindsJson) || {};
delete result.bindsJson;
result.alternateDomains = result.alternateDomains || [];
result.alternateDomains.forEach(function (d) {
delete d.appId;
@@ -111,35 +116,24 @@ function postProcess(result) {
if (envNames[i]) result.env[envNames[i]] = envValues[i];
}
let volumeIds = JSON.parse(result.volumeIds);
delete result.volumeIds;
let volumeReadOnlys = JSON.parse(result.volumeReadOnlys);
delete result.volumeReadOnlys;
result.mounts = volumeIds[0] === null ? [] : volumeIds.map((v, idx) => { return { volumeId: v, readOnly: !!volumeReadOnlys[idx] }; }); // NOTE: volumeIds is [null] when volumes of an app is empty
result.error = safe.JSON.parse(result.errorJson);
delete result.errorJson;
result.taskId = result.taskId ? String(result.taskId) : null;
}
// each query simply join apps table with another table by id. we then join the full result together
const PB_QUERY = 'SELECT id, GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes FROM apps LEFT JOIN appPortBindings ON apps.id = appPortBindings.appId GROUP BY apps.id';
const ENV_QUERY = 'SELECT id, JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues FROM apps LEFT JOIN appEnvVars ON apps.id = appEnvVars.appId GROUP BY apps.id';
const SUBDOMAIN_QUERY = `SELECT id, subdomains.subdomain AS location, subdomains.domain AS domain FROM apps LEFT JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = '${exports.SUBDOMAIN_TYPE_PRIMARY}' GROUP BY apps.id`;
const MOUNTS_QUERY = 'SELECT id, JSON_ARRAYAGG(appMounts.volumeId) AS volumeIds, JSON_ARRAYAGG(appMounts.readOnly) AS volumeReadOnlys FROM apps LEFT JOIN appMounts ON apps.id = appMounts.appId GROUP BY apps.id';
const APPS_QUERY = `SELECT ${APPS_FIELDS_PREFIXED}, hostPorts, environmentVariables, portTypes, envNames, envValues, location, domain, volumeIds, volumeReadOnlys FROM apps`
+ ` LEFT JOIN (${PB_QUERY}) AS q1 on q1.id = apps.id`
+ ` LEFT JOIN (${ENV_QUERY}) AS q2 on q2.id = apps.id`
+ ` LEFT JOIN (${SUBDOMAIN_QUERY}) AS q3 on q3.id = apps.id`
+ ` LEFT JOIN (${MOUNTS_QUERY}) AS q4 on q4.id = apps.id`;
function get(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
database.query(`${APPS_QUERY} WHERE apps.id = ?`, [ id ], function (error, result) {
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes, '
+ 'JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues'
+ ' FROM apps'
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
+ ' LEFT OUTER JOIN appEnvVars ON apps.id = appEnvVars.appId'
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
+ ' WHERE apps.id = ? GROUP BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY, id ], function (error, result) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
@@ -155,11 +149,18 @@ function get(id, callback) {
});
}
function getByIpAddress(ip, callback) {
assert.strictEqual(typeof ip, 'string');
function getByHttpPort(httpPort, callback) {
assert.strictEqual(typeof httpPort, 'number');
assert.strictEqual(typeof callback, 'function');
database.query(`${APPS_QUERY} WHERE apps.containerIp = ?`, [ ip ], function (error, result) {
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes,'
+ 'JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues'
+ ' FROM apps'
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
+ ' LEFT OUTER JOIN appEnvVars ON apps.id = appEnvVars.appId'
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
+ ' WHERE httpPort = ? GROUP BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY, httpPort ], function (error, result) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
@@ -167,7 +168,32 @@ function getByIpAddress(ip, callback) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
result[0].alternateDomains = alternateDomains;
postProcess(result[0]);
callback(null, result[0]);
});
});
}
function getByContainerId(containerId, callback) {
assert.strictEqual(typeof containerId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes,'
+ 'JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues'
+ ' FROM apps'
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
+ ' LEFT OUTER JOIN appEnvVars ON apps.id = appEnvVars.appId'
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
+ ' WHERE containerId = ? GROUP BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY, containerId ], function (error, result) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
database.query('SELECT ' + SUBDOMAIN_FIELDS + ' FROM subdomains WHERE appId = ? AND type = ?', [ result[0].id, exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
result[0].alternateDomains = alternateDomains;
postProcess(result[0]);
callback(null, result[0]);
@@ -178,7 +204,14 @@ function getByIpAddress(ip, callback) {
function getAll(callback) {
assert.strictEqual(typeof callback, 'function');
database.query(`${APPS_QUERY} ORDER BY apps.id`, [ ], function (error, results) {
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes,'
+ 'JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues'
+ ' FROM apps'
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
+ ' LEFT OUTER JOIN appEnvVars ON apps.id = appEnvVars.appId'
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
+ ' GROUP BY apps.id ORDER BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
database.query('SELECT ' + SUBDOMAIN_FIELDS + ' FROM subdomains WHERE type = ?', [ exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
@@ -325,13 +358,12 @@ function del(id, callback) {
{ query: 'DELETE FROM appPortBindings WHERE appId = ?', args: [ id ] },
{ query: 'DELETE FROM appEnvVars WHERE appId = ?', args: [ id ] },
{ query: 'DELETE FROM appPasswords WHERE identifier = ?', args: [ id ] },
{ query: 'DELETE FROM appMounts WHERE appId = ?', args: [ id ] },
{ query: 'DELETE FROM apps WHERE id = ?', args: [ id ] }
];
database.transaction(queries, function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (results[5].affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
if (results[4].affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
callback(null);
});
@@ -401,19 +433,12 @@ function updateWithConstraints(id, app, constraints, callback) {
}
}
if ('mounts' in app) {
queries.push({ query: 'DELETE FROM appMounts WHERE appId = ?', args: [ id ]});
app.mounts.forEach(function (m) {
queries.push({ query: 'INSERT INTO appMounts (appId, volumeId, readOnly) VALUES (?, ?, ?)', args: [ id, m.volumeId, m.readOnly ]});
});
}
var fields = [ ], values = [ ];
for (var p in app) {
if (p === 'manifest' || p === 'tags' || p === 'accessRestriction' || p === 'debugMode' || p === 'error' || p === 'reverseProxyConfig' || p === 'servicesConfig') {
if (p === 'manifest' || p === 'tags' || p === 'accessRestriction' || p === 'debugMode' || p === 'error' || p === 'reverseProxyConfig' || p === 'servicesConfig' || p === 'binds') {
fields.push(`${p}Json = ?`);
values.push(JSON.stringify(app[p]));
} else if (p !== 'portBindings' && p !== 'location' && p !== 'domain' && p !== 'alternateDomains' && p !== 'env' && p !== 'mounts') {
} else if (p !== 'portBindings' && p !== 'location' && p !== 'domain' && p !== 'alternateDomains' && p !== 'env') {
fields.push(p + ' = ?');
values.push(app[p]);
}
+4 -3
View File
@@ -14,7 +14,7 @@ var appdb = require('./appdb.js'),
util = require('util');
exports = module.exports = {
run
run: run
};
const HEALTHCHECK_INTERVAL = 10 * 1000; // every 10 seconds. this needs to be small since the UI makes only healthy apps clickable
@@ -85,7 +85,8 @@ function checkAppHealth(app, callback) {
// non-appstore apps may not have healthCheckPath
if (!manifest.healthCheckPath) return setHealth(app, apps.HEALTH_HEALTHY, callback);
const healthCheckUrl = `http://${app.containerIp}:${manifest.httpPort}${manifest.healthCheckPath}`;
// 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
@@ -95,7 +96,7 @@ function checkAppHealth(app, callback) {
.end(function (error, res) {
if (error && !error.response) {
setHealth(app, apps.HEALTH_UNHEALTHY, callback);
} else if (res.statusCode >= 403) { // 2xx and 3xx are ok. even 401 and 403 are ok for now (for WP sites)
} else if (res.statusCode >= 400) { // 2xx and 3xx are ok
setHealth(app, apps.HEALTH_UNHEALTHY, callback);
} else {
setHealth(app, apps.HEALTH_HEALTHY, callback);
+103 -86
View File
@@ -1,71 +1,71 @@
'use strict';
exports = module.exports = {
hasAccessTo,
removeInternalFields,
removeRestrictedFields,
hasAccessTo: hasAccessTo,
removeInternalFields: removeInternalFields,
removeRestrictedFields: removeRestrictedFields,
get,
getByIpAddress,
getByFqdn,
getAll,
getAllByUser,
install,
uninstall,
get: get,
getByContainerId: getByContainerId,
getByIpAddress: getByIpAddress,
getByFqdn: getByFqdn,
getAll: getAll,
getAllByUser: getAllByUser,
install: install,
uninstall: uninstall,
setAccessRestriction,
setLabel,
setIcon,
setTags,
setMemoryLimit,
setCpuShares,
setMounts,
setAutomaticBackup,
setAutomaticUpdate,
setReverseProxyConfig,
setCertificate,
setDebugMode,
setEnvironment,
setMailbox,
setLocation,
setDataDir,
repair,
setAccessRestriction: setAccessRestriction,
setLabel: setLabel,
setIcon: setIcon,
setTags: setTags,
setMemoryLimit: setMemoryLimit,
setCpuShares: setCpuShares,
setBinds: setBinds,
setAutomaticBackup: setAutomaticBackup,
setAutomaticUpdate: setAutomaticUpdate,
setReverseProxyConfig: setReverseProxyConfig,
setCertificate: setCertificate,
setDebugMode: setDebugMode,
setEnvironment: setEnvironment,
setMailbox: setMailbox,
setLocation: setLocation,
setDataDir: setDataDir,
repair: repair,
restore,
importApp,
exportApp,
clone,
restore: restore,
importApp: importApp,
clone: clone,
update,
update: update,
backup,
listBackups,
backup: backup,
listBackups: listBackups,
getLocalLogfilePaths,
getLogs,
getLocalLogfilePaths: getLocalLogfilePaths,
getLogs: getLogs,
start,
stop,
restart,
start: start,
stop: stop,
restart: restart,
exec,
exec: exec,
checkManifestConstraints,
downloadManifest,
checkManifestConstraints: checkManifestConstraints,
downloadManifest: downloadManifest,
canAutoupdateApp,
autoupdateApps,
canAutoupdateApp: canAutoupdateApp,
autoupdateApps: autoupdateApps,
restoreInstalledApps,
configureInstalledApps,
schedulePendingTasks,
restartAppsUsingAddons,
restoreInstalledApps: restoreInstalledApps,
configureInstalledApps: configureInstalledApps,
schedulePendingTasks: schedulePendingTasks,
restartAppsUsingAddons: restartAppsUsingAddons,
getDataDir,
getIconPath,
getDataDir: getDataDir,
getIconPath: getIconPath,
downloadFile,
uploadFile,
downloadFile: downloadFile,
uploadFile: uploadFile,
PORT_TYPE_TCP: 'tcp',
PORT_TYPE_UDP: 'udp',
@@ -155,6 +155,7 @@ function validatePortBindings(portBindings, manifest) {
const RESERVED_PORTS = [
22, /* ssh */
25, /* smtp */
53, /* dns */
80, /* http */
143, /* imap */
202, /* alternate ssh */
@@ -167,7 +168,7 @@ function validatePortBindings(portBindings, manifest) {
2004, /* graphite (lo) */
2514, /* cloudron-syslog (lo) */
constants.PORT, /* app server (lo) */
constants.AUTHWALL_PORT, /* protected sites */
constants.SYSADMIN_PORT, /* sysadmin app server (lo) */
constants.INTERNAL_SMTP_PORT, /* internal smtp port (lo) */
constants.LDAP_PORT,
3306, /* mysql (lo) */
@@ -191,7 +192,7 @@ function validatePortBindings(portBindings, manifest) {
if (!Number.isInteger(hostPort)) return new BoxError(BoxError.BAD_FIELD, `${hostPort} is not an integer`, { field: 'portBindings', portName: portName });
if (RESERVED_PORTS.indexOf(hostPort) !== -1) return new BoxError(BoxError.BAD_FIELD, `Port ${hostPort} is reserved.`, { field: 'portBindings', portName: portName });
if (RESERVED_PORT_RANGES.find(range => (hostPort >= range[0] && hostPort <= range[1]))) return new BoxError(BoxError.BAD_FIELD, `Port ${hostPort} is reserved.`, { field: 'portBindings', portName: portName });
if (hostPort !== 53 && (hostPort <= 1023 || hostPort > 65535)) return new BoxError(BoxError.BAD_FIELD, `${hostPort} is not in permitted range`, { field: 'portBindings', portName: portName }); // dns 53 is special and adblocker apps can use them
if (hostPort <= 1023 || hostPort > 65535) return new BoxError(BoxError.BAD_FIELD, `${hostPort} is not in permitted range`, { field: 'portBindings', portName: portName });
}
// it is OK if there is no 1-1 mapping between values in manifest.tcpPorts and portBindings. missing values implies
@@ -334,6 +335,20 @@ function validateEnv(env) {
return null;
}
function validateBinds(binds) {
for (let name of Object.keys(binds)) {
// just have friendly characters under /media
if (!/^[-0-9a-zA-Z_@$=#.%+]+$/.test(name)) return new BoxError(BoxError.BAD_FIELD, `Invalid bind name: ${name}`);
const bind = binds[name];
if (!bind.hostPath.startsWith('/mnt') && !bind.hostPath.startsWith('/media')) return new BoxError(BoxError.BAD_FIELD, 'hostPath must be in /mnt or /media');
if (path.normalize(bind.hostPath) !== bind.hostPath) return new BoxError(BoxError.BAD_FIELD, 'hostPath is not normalized');
}
return null;
}
function validateDataDir(dataDir) {
if (dataDir === null) return null;
@@ -384,7 +399,7 @@ function getDuplicateErrorDetails(errorMessage, locations, domainObjectMap, port
// check if any of the port bindings conflict
for (let portName in portBindings) {
if (portBindings[portName] === parseInt(match[1])) return new BoxError(BoxError.ALREADY_EXISTS, `Port ${match[1]} is in use`, { portName });
if (portBindings[portName] === parseInt(match[1])) return new BoxError(BoxError.ALREADY_EXISTS, `Port ${match[1]} is reserved`, { portName });
}
if (match[2] === 'dataDir') {
@@ -406,7 +421,7 @@ function removeInternalFields(app) {
'location', 'domain', 'fqdn', 'mailboxName', 'mailboxDomain',
'accessRestriction', 'manifest', 'portBindings', 'iconUrl', 'memoryLimit', 'cpuShares',
'sso', 'debugMode', 'reverseProxyConfig', 'enableBackup', 'creationTime', 'updateTime', 'ts', 'tags',
'label', 'alternateDomains', 'env', 'enableAutomaticUpdate', 'dataDir', 'mounts');
'label', 'alternateDomains', 'env', 'enableAutomaticUpdate', 'dataDir', 'binds');
}
// non-admins can only see these
@@ -498,6 +513,25 @@ function get(appId, callback) {
if (error) return callback(error);
appdb.get(appId, function (error, app) {
if (error && error.reason === BoxError.NOT_FOUND) return callback(new BoxError(BoxError.NOT_FOUND, 'No such app'));
if (error) return callback(error);
postProcess(app, domainObjectMap);
callback(null, app);
});
});
}
function getByContainerId(containerId, callback) {
assert.strictEqual(typeof containerId, 'string');
assert.strictEqual(typeof callback, 'function');
getDomainObjectMap(function (error, domainObjectMap) {
if (error) return callback(error);
appdb.getByContainerId(containerId, function (error, app) {
if (error && error.reason === BoxError.NOT_FOUND) return callback(new BoxError(BoxError.NOT_FOUND, 'No such app'));
if (error) return callback(error);
postProcess(app, domainObjectMap);
@@ -515,15 +549,16 @@ function getByIpAddress(ip, callback) {
// this is only used by the ldap test. the apps tests still uses proper docker
if (constants.TEST && exports._MOCK_GET_BY_IP_APP_ID) return get(exports._MOCK_GET_BY_IP_APP_ID, callback);
appdb.getByIpAddress(ip, function (error, app) {
docker.getContainerIdByIp(ip, function (error, containerId) {
if (error) return callback(error);
getDomainObjectMap(function (error, domainObjectMap) {
docker.inspect(containerId, function (error, result) {
if (error) return callback(error);
postProcess(app, domainObjectMap);
const appId = safe.query(result, 'Config.Labels.appId', null);
if (!appId) return callback(new BoxError(BoxError.NOT_FOUND, 'No such app'));
callback(null, app);
get(appId, callback);
});
});
}
@@ -745,7 +780,7 @@ function install(data, auditSource, callback) {
if ('sso' in data && !('optionalSso' in manifest)) return callback(new BoxError(BoxError.BAD_FIELD, 'sso can only be specified for apps with optionalSso'));
// if sso was unspecified, enable it by default if possible
if (sso === null) sso = !!manifest.addons['ldap'] || !!manifest.addons['proxyAuth'];
if (sso === null) sso = !!manifest.addons['ldap'] || !!manifest.addons['oauth'];
error = validateEnv(env);
if (error) return callback(error);
@@ -959,9 +994,9 @@ function setCpuShares(app, cpuShares, auditSource, callback) {
});
}
function setMounts(app, mounts, auditSource, callback) {
function setBinds(app, binds, auditSource, callback) {
assert.strictEqual(typeof app, 'object');
assert(Array.isArray(mounts));
assert(binds && typeof binds === 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -969,15 +1004,17 @@ function setMounts(app, mounts, auditSource, callback) {
let error = checkAppState(app, exports.ISTATE_PENDING_RECREATE_CONTAINER);
if (error) return callback(error);
error = validateBinds(binds);
if (error) return callback(error);
const task = {
args: {},
values: { mounts }
values: { binds }
};
addTask(appId, exports.ISTATE_PENDING_RECREATE_CONTAINER, task, function (error, result) {
if (error && error.reason === BoxError.ALREADY_EXISTS) return callback(new BoxError(BoxError.CONFLICT, 'Duplicate mount points'));
if (error) return callback(error);
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, mounts, taskId: result.taskId });
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, binds, taskId: result.taskId });
callback(null, { taskId: result.taskId });
});
@@ -1550,26 +1587,6 @@ function importApp(app, data, auditSource, callback) {
});
}
function exportApp(app, data, auditSource, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
const appId = app.id;
let error = checkAppState(app, exports.ISTATE_PENDING_BACKUP);
if (error) return callback(error);
const task = {
args: { snapshotOnly: true },
values: {}
};
addTask(appId, exports.ISTATE_PENDING_BACKUP, task, (error, result) => {
if (error) return callback(error);
callback(null, { taskId: result.taskId });
});
}
function purchaseApp(data, callback) {
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof callback, 'function');
+7 -7
View File
@@ -243,7 +243,7 @@ function getBoxUpdate(options, callback) {
automatic: options.automatic
};
superagent.get(url).query(query).timeout(30 * 1000).end(function (error, result) {
superagent.get(url).query(query).timeout(10 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
@@ -286,7 +286,7 @@ function getAppUpdate(app, options, callback) {
automatic: options.automatic
};
superagent.get(url).query(query).timeout(30 * 1000).end(function (error, result) {
superagent.get(url).query(query).timeout(10 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error));
if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
@@ -413,7 +413,7 @@ function createTicket(info, auditSource, callback) {
support.enableRemoteSupport(true, auditSource, function (error) {
// ensure we can at least get the ticket through
if (error) debug('Unable to enable SSH support.', error);
if (error) console.error('Unable to enable SSH support.', error);
callback();
});
@@ -433,7 +433,7 @@ function createTicket(info, auditSource, callback) {
var req = superagent.post(`${settings.apiServerOrigin()}/api/v1/ticket`)
.query({ accessToken: token })
.timeout(30 * 1000);
.timeout(20 * 1000);
// either send as JSON through body or as multipart, depending on attachments
if (info.app) {
@@ -455,7 +455,7 @@ function createTicket(info, auditSource, callback) {
eventlog.add(eventlog.ACTION_SUPPORT_TICKET, auditSource, info);
callback(null, { message: `An email was sent to ${constants.SUPPORT_EMAIL}. We will get back shortly!` });
callback(null, { message: `An email for sent to ${constants.SUPPORT_EMAIL}. We will get back shortly!` });
});
});
});
@@ -472,7 +472,7 @@ function getApps(callback) {
if (error) return callback(error);
const url = `${settings.apiServerOrigin()}/api/v1/apps`;
superagent.get(url).query({ accessToken: token, boxVersion: constants.VERSION, unstable: unstable }).timeout(30 * 1000).end(function (error, result) {
superagent.get(url).query({ accessToken: token, boxVersion: constants.VERSION, unstable: unstable }).timeout(10 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 403 || result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
@@ -507,7 +507,7 @@ function getAppVersion(appId, version, callback) {
let url = `${settings.apiServerOrigin()}/api/v1/apps/${appId}`;
if (version !== 'latest') url += `/versions/${version}`;
superagent.get(url).query({ accessToken: token }).timeout(30 * 1000).end(function (error, result) {
superagent.get(url).query({ accessToken: token }).timeout(10 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 403 || result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
if (result.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND));
+26 -35
View File
@@ -6,6 +6,7 @@ exports = module.exports = {
run: run,
// exported for testing
_reserveHttpPort: reserveHttpPort,
_configureReverseProxy: configureReverseProxy,
_unconfigureReverseProxy: unconfigureReverseProxy,
_createAppDir: createAppDir,
@@ -33,8 +34,8 @@ var addons = require('./addons.js'),
ejs = require('ejs'),
eventlog = require('./eventlog.js'),
fs = require('fs'),
iputils = require('./iputils.js'),
manifestFormat = require('cloudron-manifestformat'),
net = require('net'),
os = require('os'),
path = require('path'),
paths = require('./paths.js'),
@@ -88,16 +89,22 @@ function updateApp(app, values, callback) {
});
}
function allocateContainerIp(app, callback) {
function reserveHttpPort(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
async.retry({ times: 10 }, function (retryCallback) {
const iprange = iputils.intFromIp('172.18.20.255') - iputils.intFromIp('172.18.16.1');
let rnd = Math.floor(Math.random() * iprange);
const containerIp = iputils.ipFromInt(iputils.intFromIp('172.18.16.1') + rnd);
updateApp(app, { containerIp }, retryCallback);
}, callback);
let server = net.createServer();
server.listen(0, function () {
let port = server.address().port;
updateApp(app, { httpPort: port }, function (error) {
server.close(function (/* closeError */) {
if (error) return callback(new BoxError(BoxError.NETWORK_ERROR, `Failed to allocate http port ${port}: ${error.message}`));
callback(null);
});
});
});
}
function configureReverseProxy(app, callback) {
@@ -343,7 +350,7 @@ function registerSubdomains(app, overwrite, callback) {
const allDomains = [ { subdomain: app.location, domain: app.domain }].concat(app.alternateDomains);
debugApp(app, `registerSubdomain: Will register ${JSON.stringify(allDomains)}`);
debug(`registerSubdomain: Will register ${JSON.stringify(allDomains)}`);
async.eachSeries(allDomains, function (domain, iteratorDone) {
async.retry({ times: 200, interval: 5000 }, function (retryCallback) {
@@ -363,7 +370,7 @@ function registerSubdomains(app, overwrite, callback) {
domains.upsertDnsRecords(domain.subdomain, domain.domain, 'A', [ ip ], function (error) {
if (error && (error.reason === BoxError.BUSY || error.reason === BoxError.EXTERNAL_ERROR)) {
debugApp(app, 'registerSubdomains: Upsert error. Will retry.', error.message);
debug('registerSubdomains: Upsert error. Will retry.', error.message);
return retryCallback(new BoxError(BoxError.EXTERNAL_ERROR, error.message, { domain })); // try again
}
@@ -394,7 +401,7 @@ function unregisterSubdomains(app, allDomains, callback) {
domains.removeDnsRecords(domain.subdomain, domain.domain, 'A', [ ip ], function (error) {
if (error && error.reason === BoxError.NOT_FOUND) return retryCallback(null, null);
if (error && (error.reason === BoxError.SBUSY || error.reason === BoxError.EXTERNAL_ERROR)) {
debugApp(app, 'registerSubdomains: Remove error. Will retry.', error.message);
debug('registerSubdomains: Remove error. Will retry.', error.message);
return retryCallback(new BoxError(BoxError.EXTERNAL_ERROR, error.message, { domain })); // try again
}
@@ -444,7 +451,7 @@ function moveDataDir(app, targetDir, callback) {
let resolvedSourceDir = apps.getDataDir(app, app.dataDir);
let resolvedTargetDir = apps.getDataDir(app, targetDir);
debugApp(app, `moveDataDir: migrating data from ${resolvedSourceDir} to ${resolvedTargetDir}`);
debug(`moveDataDir: migrating data from ${resolvedSourceDir} to ${resolvedTargetDir}`);
if (resolvedSourceDir === resolvedTargetDir) return callback();
@@ -477,8 +484,6 @@ function downloadImage(manifest, callback) {
}
function startApp(app, callback){
debugApp(app, 'startApp: starting container');
if (app.runState === apps.RSTATE_STOPPED) return callback();
docker.startContainer(app.id, callback);
@@ -527,8 +532,7 @@ function install(app, args, progressCallback, callback) {
docker.deleteImage(oldManifest, done);
},
// allocating container ip here, lets the users "repair" an app if allocation fails at appdb.add time
allocateContainerIp.bind(null, app),
reserveHttpPort.bind(null, app),
progressCallback.bind(null, { percent: 20, message: 'Downloading icon' }),
downloadIcon.bind(null, app),
@@ -598,7 +602,7 @@ function backup(app, args, progressCallback, callback) {
async.series([
progressCallback.bind(null, { percent: 10, message: 'Backing up' }),
backups.backupApp.bind(null, app, { snapshotOnly: !!args.snapshotOnly }, (progress) => {
backups.backupApp.bind(null, app, { /* options */ }, (progress) => {
progressCallback({ percent: 30, message: progress.message });
}),
@@ -750,6 +754,7 @@ function configure(app, args, progressCallback, callback) {
progressCallback.bind(null, { percent: 10, message: 'Cleaning up old install' }),
unconfigureReverseProxy.bind(null, app),
deleteContainers.bind(null, app, { managedOnly: true }),
reserveHttpPort.bind(null, app),
progressCallback.bind(null, { percent: 20, message: 'Downloading icon' }),
downloadIcon.bind(null, app),
@@ -784,6 +789,7 @@ function configure(app, args, progressCallback, callback) {
});
}
// nginx configuration is skipped because app.httpPort is expected to be available
function update(app, args, progressCallback, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof args, 'object');
@@ -795,8 +801,7 @@ function update(app, args, progressCallback, callback) {
// app does not want these addons anymore
// FIXME: this does not handle option changes (like multipleDatabases)
const unusedAddons = _.omit(app.manifest.addons, Object.keys(updateConfig.manifest.addons));
const httpPathsChanged = app.manifest.httpPaths !== updateConfig.manifest.httpPaths;
var unusedAddons = _.omit(app.manifest.addons, Object.keys(updateConfig.manifest.addons));
async.series([
// this protects against the theoretical possibility of an app being marked for update from
@@ -847,7 +852,7 @@ function update(app, args, progressCallback, callback) {
if (newTcpPorts[portName] || newUdpPorts[portName]) return callback(null); // port still in use
appdb.delPortBinding(currentPorts[portName], apps.PORT_TYPE_TCP, function (error) {
if (error && error.reason === BoxError.NOT_FOUND) debugApp(app, 'update: portbinding does not exist in database', error);
if (error && error.reason === BoxError.NOT_FOUND) debug('update: portbinding does not exist in database', error);
else if (error) return next(error);
// also delete from app object for further processing (the db is updated in the next step)
@@ -871,14 +876,6 @@ function update(app, args, progressCallback, callback) {
startApp.bind(null, app),
// needed for httpPaths changes
progressCallback.bind(null, { percent: 90, message: 'Configuring reverse proxy' }),
function (next) {
if (!httpPathsChanged) return next();
configureReverseProxy(app, next);
},
progressCallback.bind(null, { percent: 100, message: 'Done' }),
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null, updateTime: new Date() })
], function seriesDone(error) {
@@ -907,11 +904,8 @@ function start(app, args, progressCallback, callback) {
progressCallback.bind(null, { percent: 35, message: 'Starting container' }),
docker.startContainer.bind(null, app.id),
progressCallback.bind(null, { percent: 60, message: 'Adding collectd profile' }),
addCollectdProfile.bind(null, app),
// stopped apps do not renew certs. currently, we don't do DNS to not overwrite existing user settings
progressCallback.bind(null, { percent: 80, message: 'Configuring reverse proxy' }),
progressCallback.bind(null, { percent: 60, message: 'Configuring reverse proxy' }),
configureReverseProxy.bind(null, app),
progressCallback.bind(null, { percent: 100, message: 'Done' }),
@@ -938,9 +932,6 @@ function stop(app, args, progressCallback, callback) {
progressCallback.bind(null, { percent: 50, message: 'Stopping app services' }),
addons.stopAppServices.bind(null, app),
progressCallback.bind(null, { percent: 80, message: 'Removing collectd profile' }),
removeCollectdProfile.bind(null, app),
progressCallback.bind(null, { percent: 100, message: 'Done' }),
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null })
], function seriesDone(error) {
+4 -6
View File
@@ -1,7 +1,7 @@
'use strict';
exports = module.exports = {
scheduleTask
scheduleTask: scheduleTask
};
let assert = require('assert'),
@@ -12,7 +12,6 @@ let assert = require('assert'),
safe = require('safetydance'),
path = require('path'),
paths = require('./paths.js'),
scheduler = require('./scheduler.js'),
sftp = require('./sftp.js'),
tasks = require('./tasks.js');
@@ -70,8 +69,6 @@ function scheduleTask(appId, taskId, callback) {
if (!fs.existsSync(path.dirname(logFile))) safe.fs.mkdirSync(path.dirname(logFile)); // ensure directory
scheduler.suspendJobs(appId);
// TODO: set memory limit for app backup task
tasks.startTask(taskId, { logFile, timeout: 20 * 60 * 60 * 1000 /* 20 hours */, nice: 15 }, function (error, result) {
callback(error, result);
@@ -80,8 +77,9 @@ function scheduleTask(appId, taskId, callback) {
locker.unlock(locker.OP_APPTASK); // unlock event will trigger next task
// post app task hooks
sftp.rebuild(error => { if (error) debug('Unable to rebuild sftp:', error); });
scheduler.resumeJobs(appId);
sftp.rebuild(function (error) {
if (error) console.error('Unable to rebuild sftp:', error);
});
});
}
+5 -15
View File
@@ -851,23 +851,15 @@ function runBackupUpload(uploadConfig, progressCallback, callback) {
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
const { backupId, backupConfig, dataLayout, progressTag } = uploadConfig;
const { backupId, format, dataLayout, progressTag } = uploadConfig;
assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof format, 'string');
assert.strictEqual(typeof progressTag, 'string');
assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout');
let result = ''; // the script communicates error result as a string
// https://stackoverflow.com/questions/48387040/node-js-recommended-max-old-space-size
const envCopy = Object.assign({}, process.env);
if (backupConfig.memoryLimit && backupConfig.memoryLimit >= 2*1024*1024*1024) {
const heapSize = Math.min((backupConfig.memoryLimit/1024/1024) - 256, 8192);
debug(`runBackupUpload: adjusting heap size to ${heapSize}M`);
envCopy.NODE_OPTIONS = `--max-old-space-size=${heapSize}`;
}
shell.sudo(`backup-${backupId}`, [ BACKUP_UPLOAD_CMD, backupId, backupConfig.format, dataLayout.toString() ], { env: envCopy, preserveEnv: true, ipc: true }, function (error) {
shell.sudo(`backup-${backupId}`, [ BACKUP_UPLOAD_CMD, backupId, format, dataLayout.toString() ], { preserveEnv: true, ipc: true }, function (error) {
if (error && (error.code === null /* signal */ || (error.code !== 0 && error.code !== 50))) { // backuptask crashed
return callback(new BoxError(BoxError.INTERNAL_ERROR, 'Backuptask crashed'));
} else if (error && error.code === 50) { // exited with error
@@ -936,7 +928,7 @@ function uploadBoxSnapshot(backupConfig, progressCallback, callback) {
const uploadConfig = {
backupId: 'snapshot/box',
backupConfig,
format: backupConfig.format,
dataLayout: new DataLayout(boxDataDir, []),
progressTag: 'box'
};
@@ -1123,7 +1115,7 @@ function uploadAppSnapshot(backupConfig, app, progressCallback, callback) {
const uploadConfig = {
backupId,
backupConfig,
format: backupConfig.format,
dataLayout,
progressTag: app.fqdn
};
@@ -1175,8 +1167,6 @@ function backupApp(app, options, progressCallback, callback) {
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
if (options.snapshotOnly) return snapshotApp(app, progressCallback, callback);
const tag = (new Date()).toISOString().replace(/[T.]/g, '-').replace(/[:Z]/g,'');
debug(`backupApp - Backing up ${app.fqdn} with tag ${tag}`);
-18
View File
@@ -1,18 +0,0 @@
'use strict';
exports = module.exports = {
renderFooter
};
const assert = require('assert'),
constants = require('./constants.js');
function renderFooter(footer) {
assert.strictEqual(typeof footer, 'string');
const year = new Date().getFullYear();
return footer.replace(/%YEAR%/g, year)
.replace(/%VERSION%/g, constants.VERSION);
}
+2 -2
View File
@@ -185,8 +185,8 @@ Acme2.prototype.newOrder = function (domain, callback) {
this.sendSignedRequest(this.directory.newOrder, JSON.stringify(payload), function (error, result) {
if (error) return callback(error);
if (result.statusCode === 403) return callback(new BoxError(BoxError.ACCESS_DENIED, `Forbidden sending new order: ${result.body.detail}`));
if (result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Failed to send new order. Expecting 201, got %s %s', result.statusCode, result.text)));
if (result.statusCode === 403) return callback(new BoxError(BoxError.ACCESS_DENIED, `Forbidden sending signed request: ${result.body.detail}`));
if (result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Failed to register user. Expecting 201, got %s %s', result.statusCode, result.text)));
debug('newOrder: created order %s %j', domain, result.body);
+1 -2
View File
@@ -29,7 +29,6 @@ var addons = require('./addons.js'),
auditSource = require('./auditsource.js'),
backups = require('./backups.js'),
BoxError = require('./boxerror.js'),
branding = require('./branding.js'),
constants = require('./constants.js'),
cron = require('./cron.js'),
debug = require('debug')('box:cloudron'),
@@ -176,7 +175,7 @@ function getConfig(callback) {
version: constants.VERSION,
isDemo: settings.isDemo(),
cloudronName: allSettings[settings.CLOUDRON_NAME_KEY],
footer: branding.renderFooter(allSettings[settings.FOOTER_KEY] || constants.FOOTER),
footer: allSettings[settings.FOOTER_KEY] || constants.FOOTER,
features: appstore.getFeatures(),
profileLocked: allSettings[settings.DIRECTORY_CONFIG_KEY].lockUserProfiles,
mandatory2FA: allSettings[settings.DIRECTORY_CONFIG_KEY].mandatory2FA
+4 -4
View File
@@ -26,7 +26,7 @@ exports = module.exports = {
PORT: CLOUDRON ? 3000 : 5454,
INTERNAL_SMTP_PORT: 2525, // this value comes from the mail container
AUTHWALL_PORT: 3001,
SYSADMIN_PORT: 3001, // unused
LDAP_PORT: 3002,
DOCKER_PROXY_PORT: 3003,
@@ -37,7 +37,7 @@ exports = module.exports = {
DEFAULT_MEMORY_LIMIT: (256 * 1024 * 1024), // see also client.js
DEMO_USERNAME: 'cloudron',
DEMO_BLACKLISTED_APPS: [ 'com.github.cloudtorrent', 'net.alltubedownload.cloudronapp', 'com.adguard.home.cloudronapp' ],
DEMO_BLACKLISTED_APPS: [ 'com.github.cloudtorrent' ],
AUTOUPDATE_PATTERN_NEVER: 'never',
@@ -48,8 +48,8 @@ exports = module.exports = {
SUPPORT_EMAIL: 'support@cloudron.io',
FOOTER: '&copy; %YEAR% &nbsp; [Cloudron](https://cloudron.io) &nbsp; &nbsp; &nbsp; [Forum <i class="fa fa-comments"></i>](https://forum.cloudron.io)',
FOOTER: '&copy; 2020 &nbsp; [Cloudron](https://cloudron.io) &nbsp; &nbsp; &nbsp; [Forum <i class="fa fa-comments"></i>](https://forum.cloudron.io)',
VERSION: process.env.BOX_ENV === 'cloudron' ? fs.readFileSync(path.join(__dirname, '../VERSION'), 'utf8').trim() : '6.0.1-test'
VERSION: process.env.BOX_ENV === 'cloudron' ? fs.readFileSync(path.join(__dirname, '../VERSION'), 'utf8').trim() : '5.1.1-test'
};
-2
View File
@@ -64,8 +64,6 @@ var NOOP_CALLBACK = function (error) { if (error) debug(error); };
function startJobs(callback) {
assert.strictEqual(typeof callback, 'function');
debug('startJobs: starting cron jobs');
const randomTick = Math.floor(60*Math.random());
gJobs.systemChecks = new CronJob({
cronTime: '00 30 2 * * *', // once a day. if you change this interval, change the notification messages with correct duration
+4 -7
View File
@@ -119,10 +119,9 @@ function get(domainObject, location, type, callback) {
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '';
getZoneRecords(dnsConfig, zoneName, name, type, function (error, result) {
getZoneRecords(dnsConfig, zoneName, name, type, function (error, { records }) {
if (error) return callback(error);
const { records } = result;
var tmp = records.map(function (record) { return record.target; });
debug('get: %j', tmp);
@@ -144,10 +143,9 @@ function upsert(domainObject, location, type, values, callback) {
debug('upsert: %s for zone %s of type %s with values %j', name, zoneName, type, values);
getZoneRecords(dnsConfig, zoneName, name, type, function (error, result) {
getZoneRecords(dnsConfig, zoneName, name, type, function (error, { zoneId, records }) {
if (error) return callback(error);
const { zoneId, records } = result;
let i = 0, recordIds = []; // used to track available records to update instead of create
async.eachSeries(values, function (value, iteratorCallback) {
@@ -224,10 +222,9 @@ function del(domainObject, location, type, values, callback) {
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '';
getZoneRecords(dnsConfig, zoneName, name, type, function (error, result) {
getZoneRecords(dnsConfig, zoneName, name, type, function (error, { zoneId, records }) {
if (error) return callback(error);
const { zoneId, records } = result;
if (records.length === 0) return callback(null);
var tmp = records.filter(function (record) { return values.some(function (value) { return value === record.target; }); });
@@ -290,7 +287,7 @@ function verifyDnsConfig(domainObject, callback) {
if (error || !nameservers) return callback(new BoxError(BoxError.BAD_FIELD, error ? error.message : 'Unable to get nameservers', { field: 'nameservers' }));
if (nameservers.map(function (n) { return n.toLowerCase(); }).indexOf('ns1.linode.com') === -1) {
debug('verifyDnsConfig: %j does not contains linode NS', nameservers);
debug('verifyDnsConfig: %j does not contains DO NS', nameservers);
return callback(new BoxError(BoxError.BAD_FIELD, 'Domain nameservers are not set to Linode', { field: 'nameservers' }));
}
+119 -169
View File
@@ -1,35 +1,34 @@
'use strict';
exports = module.exports = {
testRegistryConfig,
setRegistryConfig,
injectPrivateFields,
removePrivateFields,
testRegistryConfig: testRegistryConfig,
setRegistryConfig: setRegistryConfig,
injectPrivateFields: injectPrivateFields,
removePrivateFields: removePrivateFields,
ping,
ping: ping,
info,
downloadImage,
createContainer,
startContainer,
restartContainer,
stopContainer,
info: info,
downloadImage: downloadImage,
createContainer: createContainer,
startContainer: startContainer,
restartContainer: restartContainer,
stopContainer: stopContainer,
stopContainerByName: stopContainer,
stopContainers,
deleteContainer,
deleteImage,
deleteContainers,
createSubcontainer,
getContainerIdByIp,
inspect,
getContainerIp,
stopContainers: stopContainers,
deleteContainer: deleteContainer,
deleteImage: deleteImage,
deleteContainers: deleteContainers,
createSubcontainer: createSubcontainer,
getContainerIdByIp: getContainerIdByIp,
inspect: inspect,
inspectByName: inspect,
execContainer,
getEvents,
memoryUsage,
createVolume,
removeVolume,
clearVolume
execContainer: execContainer,
getEvents: getEvents,
memoryUsage: memoryUsage,
createVolume: createVolume,
removeVolume: removeVolume,
clearVolume: clearVolume
};
var addons = require('./addons.js'),
@@ -40,13 +39,11 @@ var addons = require('./addons.js'),
constants = require('./constants.js'),
debug = require('debug')('box:docker'),
Docker = require('dockerode'),
os = require('os'),
path = require('path'),
settings = require('./settings.js'),
shell = require('./shell.js'),
safe = require('safetydance'),
util = require('util'),
volumes = require('./volumes.js'),
_ = require('underscore');
const CLEARVOLUME_CMD = path.join(__dirname, 'scripts/clearvolume.sh'),
@@ -173,53 +170,26 @@ function downloadImage(manifest, callback) {
debug('downloadImage %s', manifest.dockerImage);
const image = gConnection.getImage(manifest.dockerImage);
var attempt = 1;
image.inspect(function (error, result) {
if (!error && result) return callback(null); // image is already present locally
async.retry({ times: 10, interval: 5000, errorFilter: e => e.reason !== BoxError.NOT_FOUND }, function (retryCallback) {
debug('Downloading image %s. attempt: %s', manifest.dockerImage, attempt++);
let attempt = 1;
async.retry({ times: 10, interval: 5000, errorFilter: e => e.reason !== BoxError.NOT_FOUND }, function (retryCallback) {
debug('Downloading image %s. attempt: %s', manifest.dockerImage, attempt++);
pullImage(manifest, retryCallback);
}, callback);
});
pullImage(manifest, retryCallback);
}, callback);
}
function getBinds(app, callback) {
function getBindsSync(app) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
if (app.mounts.length === 0) return callback(null);
let binds = [];
volumes.list(function (error, result) {
if (error) return callback(error);
let volumesById = {};
result.forEach(r => volumesById[r.id] = r);
for (const mount of app.mounts) {
const volume = volumesById[mount.volumeId];
binds.push(`${volume.hostPath}:/media/${volume.name}:${mount.readOnly ? 'ro' : 'rw'}`);
}
callback(null, binds);
});
}
function getLowerUpIp() { // see getifaddrs and IFF_LOWER_UP and netdevice
const ni = os.networkInterfaces(); // { lo: [], eth0: [] }
for (const iname of Object.keys(ni)) {
if (iname === 'lo') continue;
for (const address of ni[iname]) {
if (!address.internal && address.family === 'IPv4') return address.address;
}
for (let name of Object.keys(app.binds)) {
const bind = app.binds[name];
binds.push(`${bind.hostPath}:/media/${name}:${bind.readOnly ? 'ro' : 'rw'}`);
}
return null;
return binds;
}
function createSubcontainer(app, name, cmd, options, callback) {
@@ -247,6 +217,11 @@ function createSubcontainer(app, name, cmd, options, callback) {
`${envPrefix}APP_DOMAIN=${domain}`
];
// docker portBindings requires ports to be exposed
exposedPorts[manifest.httpPort + '/tcp'] = {};
dockerPortBindings[manifest.httpPort + '/tcp'] = [ { HostIp: '127.0.0.1', HostPort: app.httpPort + '' } ];
var portEnv = [];
for (let portName in app.portBindings) {
const hostPort = app.portBindings[portName];
@@ -255,12 +230,10 @@ function createSubcontainer(app, name, cmd, options, callback) {
var containerPort = ports[portName].containerPort || hostPort;
// docker portBindings requires ports to be exposed
exposedPorts[`${containerPort}/${portType}`] = {};
portEnv.push(`${portName}=${hostPort}`);
const hostIp = hostPort === 53 ? getLowerUpIp() : '0.0.0.0'; // port 53 is special because it is possibly taken by systemd-resolved
dockerPortBindings[`${containerPort}/${portType}`] = [ { HostIp: hostIp, HostPort: hostPort + '' } ];
dockerPortBindings[`${containerPort}/${portType}`] = [ { HostIp: '0.0.0.0', HostPort: hostPort + '' } ];
}
let appEnv = [];
@@ -282,101 +255,94 @@ function createSubcontainer(app, name, cmd, options, callback) {
addons.getEnvironment(app, function (error, addonEnv) {
if (error) return callback(error);
getBinds(app, function (error, binds) {
if (error) return callback(error);
let containerOptions = {
name: name, // for referencing containers
Tty: isAppContainer,
Image: app.manifest.dockerImage,
Cmd: (isAppContainer && app.debugMode && app.debugMode.cmd) ? app.debugMode.cmd : cmd,
Env: stdEnv.concat(addonEnv).concat(portEnv).concat(appEnv),
ExposedPorts: isAppContainer ? exposedPorts : { },
Volumes: { // see also ReadonlyRootfs
'/tmp': {},
'/run': {}
},
Labels: {
'fqdn': app.fqdn,
'appId': app.id,
'isSubcontainer': String(!isAppContainer),
'isCloudronManaged': String(true)
},
HostConfig: {
Mounts: addons.getMountsSync(app, app.manifest.addons),
Binds: getBindsSync(app), // ideally, we have to use 'Mounts' but we have to create volumes then
LogConfig: {
Type: 'syslog',
Config: {
'tag': app.id,
'syslog-address': 'udp://127.0.0.1:2514', // see apps.js:validatePortBindings()
'syslog-format': 'rfc5424'
}
},
Memory: memoryLimit / 2,
MemorySwap: memoryLimit, // Memory + Swap
PortBindings: isAppContainer ? dockerPortBindings : { },
PublishAllPorts: false,
ReadonlyRootfs: app.debugMode ? !!app.debugMode.readonlyRootfs : true,
RestartPolicy: {
'Name': isAppContainer ? 'unless-stopped' : 'no',
'MaximumRetryCount': 0
},
CpuShares: app.cpuShares,
VolumesFrom: isAppContainer ? null : [ app.containerId + ':rw' ],
SecurityOpt: [ 'apparmor=docker-cloudron-app' ],
CapAdd: [],
CapDrop: []
}
};
let containerOptions = {
name: name, // for referencing containers
Tty: isAppContainer,
Image: app.manifest.dockerImage,
Cmd: (isAppContainer && app.debugMode && app.debugMode.cmd) ? app.debugMode.cmd : cmd,
Env: stdEnv.concat(addonEnv).concat(portEnv).concat(appEnv),
ExposedPorts: isAppContainer ? exposedPorts : { },
Volumes: { // see also ReadonlyRootfs
'/tmp': {},
'/run': {}
},
Labels: {
'fqdn': app.fqdn,
'appId': app.id,
'isSubcontainer': String(!isAppContainer),
'isCloudronManaged': String(true)
},
HostConfig: {
Mounts: addons.getMountsSync(app, app.manifest.addons),
Binds: binds, // ideally, we have to use 'Mounts' but we have to create volumes then
LogConfig: {
Type: 'syslog',
Config: {
'tag': app.id,
'syslog-address': 'udp://127.0.0.1:2514', // see apps.js:validatePortBindings()
'syslog-format': 'rfc5424'
}
},
Memory: memoryLimit / 2,
MemorySwap: memoryLimit, // Memory + Swap
PortBindings: isAppContainer ? dockerPortBindings : { },
PublishAllPorts: false,
ReadonlyRootfs: app.debugMode ? !!app.debugMode.readonlyRootfs : true,
RestartPolicy: {
'Name': isAppContainer ? 'unless-stopped' : 'no',
'MaximumRetryCount': 0
},
CpuShares: app.cpuShares,
VolumesFrom: isAppContainer ? null : [ app.containerId + ':rw' ],
SecurityOpt: [ 'apparmor=docker-cloudron-app' ],
CapAdd: [],
CapDrop: []
// do no set hostname of containers to location as it might conflict with addons names. for example, an app installed in mail
// location may not reach mail container anymore by DNS. We cannot set hostname to fqdn either as that sets up the dns
// name to look up the internal docker ip. this makes curl from within container fail
// Note that Hostname has no effect on DNS. We have to use the --net-alias for dns.
// Hostname cannot be set with container NetworkMode. Subcontainers run is the network space of the app container
// This is done to prevent lots of up/down events and iptables locking
if (isAppContainer) {
containerOptions.Hostname = app.id;
containerOptions.HostConfig.NetworkMode = 'cloudron'; // user defined bridge network
containerOptions.HostConfig.Dns = ['172.18.0.1']; // use internal dns
containerOptions.HostConfig.DnsSearch = ['.']; // use internal dns
containerOptions.NetworkingConfig = {
EndpointsConfig: {
cloudron: {
Aliases: [ name ] // adds hostname entry with container name
}
}
};
} else {
containerOptions.HostConfig.NetworkMode = `container:${app.containerId}`;
}
// do no set hostname of containers to location as it might conflict with addons names. for example, an app installed in mail
// location may not reach mail container anymore by DNS. We cannot set hostname to fqdn either as that sets up the dns
// name to look up the internal docker ip. this makes curl from within container fail
// Note that Hostname has no effect on DNS. We have to use the --net-alias for dns.
// Hostname cannot be set with container NetworkMode. Subcontainers run is the network space of the app container
// This is done to prevent lots of up/down events and iptables locking
if (isAppContainer) {
containerOptions.Hostname = app.id;
containerOptions.HostConfig.NetworkMode = 'cloudron'; // user defined bridge network
containerOptions.HostConfig.Dns = ['172.18.0.1']; // use internal dns
containerOptions.HostConfig.DnsSearch = ['.']; // use internal dns
var capabilities = manifest.capabilities || [];
containerOptions.NetworkingConfig = {
EndpointsConfig: {
cloudron: {
IPAMConfig: {
IPv4Address: app.containerIp
},
Aliases: [ name ] // adds hostname entry with container name
}
}
};
} else {
containerOptions.HostConfig.NetworkMode = `container:${app.containerId}`; // scheduler containers must have same IP as app for various addon auth
}
// https://docs-stage.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities
if (capabilities.includes('net_admin')) containerOptions.HostConfig.CapAdd.push('NET_ADMIN', 'NET_RAW');
if (capabilities.includes('mlock')) containerOptions.HostConfig.CapAdd.push('IPC_LOCK'); // mlock prevents swapping
if (!capabilities.includes('ping')) containerOptions.HostConfig.CapDrop.push('NET_RAW'); // NET_RAW is included by default by Docker
var capabilities = manifest.capabilities || [];
if (capabilities.includes('vaapi') && safe.fs.existsSync('/dev/dri')) {
containerOptions.HostConfig.Devices = [
{ PathOnHost: '/dev/dri', PathInContainer: '/dev/dri', CgroupPermissions: 'rwm' }
];
}
// https://docs-stage.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities
if (capabilities.includes('net_admin')) containerOptions.HostConfig.CapAdd.push('NET_ADMIN', 'NET_RAW');
if (capabilities.includes('mlock')) containerOptions.HostConfig.CapAdd.push('IPC_LOCK'); // mlock prevents swapping
if (!capabilities.includes('ping')) containerOptions.HostConfig.CapDrop.push('NET_RAW'); // NET_RAW is included by default by Docker
containerOptions = _.extend(containerOptions, options);
if (capabilities.includes('vaapi') && safe.fs.existsSync('/dev/dri')) {
containerOptions.HostConfig.Devices = [
{ PathOnHost: '/dev/dri', PathInContainer: '/dev/dri', CgroupPermissions: 'rwm' }
];
}
gConnection.createContainer(containerOptions, function (error, container) {
if (error && error.statusCode === 409) return callback(new BoxError(BoxError.ALREADY_EXISTS, error));
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
containerOptions = _.extend(containerOptions, options);
gConnection.createContainer(containerOptions, function (error, container) {
if (error && error.statusCode === 409) return callback(new BoxError(BoxError.ALREADY_EXISTS, error));
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
callback(null, container);
});
callback(null, container);
});
});
}
@@ -560,22 +526,6 @@ function inspect(containerId, callback) {
});
}
function getContainerIp(containerId, callback) {
assert.strictEqual(typeof containerId, 'string');
assert.strictEqual(typeof callback, 'function');
if (constants.TEST) return callback(null, '127.0.5.5');
inspect(containerId, function (error, result) {
if (error) return callback(error);
const ip = safe.query(result, 'NetworkSettings.Networks.cloudron.IPAddress', null);
if (!ip) return callback(new BoxError(BoxError.DOCKER_ERROR, 'Error getting container IP'));
callback(null, ip);
});
}
function execContainer(containerId, options, callback) {
assert.strictEqual(typeof containerId, 'string');
assert.strictEqual(typeof options, 'object');
-4
View File
@@ -61,10 +61,6 @@ exports = module.exports = {
ACTION_USER_UPDATE: 'user.update',
ACTION_USER_TRANSFER: 'user.transfer',
ACTION_VOLUME_ADD: 'volume.add',
ACTION_VOLUME_UPDATE: 'volume.update',
ACTION_VOLUME_REMOVE: 'volume.remove',
ACTION_DYNDNS_UPDATE: 'dyndns.update',
ACTION_SUPPORT_TICKET: 'support.ticket',
+2 -3
View File
@@ -16,7 +16,6 @@ function startGraphite(existingInfra, callback) {
const tag = infra.images.graphite.tag;
const dataDir = paths.PLATFORM_DATA_DIR;
const memoryLimit = 256;
if (existingInfra.version === infra.version && infra.images.graphite.tag === existingInfra.images.graphite.tag) return callback();
@@ -28,8 +27,8 @@ function startGraphite(existingInfra, callback) {
--log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \
--log-opt tag=graphite \
-m ${memoryLimit}m \
--memory-swap ${memoryLimit * 2}m \
-m 150m \
--memory-swap 150m \
--dns 172.18.0.1 \
--dns-search=. \
-p 127.0.0.1:2003:2003 \
+4 -4
View File
@@ -6,7 +6,7 @@
exports = module.exports = {
// a version change recreates all containers with latest docker config
'version': '48.18.0',
'version': '48.17.1',
'baseImages': [
{ repo: 'cloudron/base', tag: 'cloudron/base:2.0.0@sha256:f9fea80513aa7c92fe2e7bf3978b54c8ac5222f47a9a32a7f8833edf0eb5a4f4' }
@@ -17,11 +17,11 @@ exports = module.exports = {
'images': {
'turn': { repo: 'cloudron/turn', tag: 'cloudron/turn:1.1.0@sha256:e1dd22aa6eef5beb7339834b200a8bb787ffc2264ce11139857a054108fefb4f' },
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:2.3.2@sha256:dd624870c7f8ba9b2759f93ce740d1e092a1ac4b2d6af5007a01b30ad6b316d0' },
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:3.3.0@sha256:0daf1be5320c095077392bf21d247b93ceaddca46c866c17259a335c80d2f357' },
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:3.2.1@sha256:ca45ba2c8356fd1ec5ec996a4e8ce1e9df6711b36c358ca19f6ab4bdc476695e' },
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:3.0.0@sha256:59e50b1f55e433ffdf6d678f8c658812b4119f631db8325572a52ee40d3bc562' },
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:2.3.0@sha256:0e31ec817e235b1814c04af97b1e7cf0053384aca2569570ce92bef0d95e94d2' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:3.0.1@sha256:ff24c70966937e8c3477d534bbb192e0364d3e9d6924ee0911278009d802b2b0' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:2.10.0@sha256:3aff92bfc85d6ca3cc6fc381c8a89625d2af95cc55ed2db692ef4e483e600372' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:2.3.0@sha256:b7bc1ca4f4d0603a01369a689129aa273a938ce195fe43d00d42f4f2d5212f50' },
'sftp': { repo: 'cloudron/sftp', tag: 'cloudron/sftp:3.0.0@sha256:7e0165f17789192fd4f92efb34aa373450fa859e3b502684b2b121a5582965bf' }
'sftp': { repo: 'cloudron/sftp', tag: 'cloudron/sftp:2.0.2@sha256:cbd604eaa970c99ba5c4c2e7984929668e05de824172f880e8c576b2fb7c976d' }
}
};
-35
View File
@@ -1,35 +0,0 @@
'use strict';
exports = module.exports = {
ipFromInt,
intFromIp
};
const assert = require('assert');
function intFromIp(address) {
assert.strictEqual(typeof address, 'string');
const parts = address.split('.');
if (parts.length !== 4) return null;
return (parseInt(parts[0], 10) << (8*3)) & 0xFF000000 |
(parseInt(parts[1], 10) << (8*2)) & 0x00FF0000 |
(parseInt(parts[2], 10) << (8*1)) & 0x0000FF00 |
(parseInt(parts[3], 10) << (8*0)) & 0x000000FF;
}
function ipFromInt(input) {
assert.strictEqual(typeof input, 'number');
let output = [];
for (let i = 3; i >= 0; --i) {
const octet = (input >> (i*8)) & 0x000000FF;
output.push(octet);
}
return output.join('.');
}
+23 -80
View File
@@ -5,8 +5,7 @@ exports = module.exports = {
stop: stop
};
var addons = require('./addons.js'),
assert = require('assert'),
var assert = require('assert'),
appdb = require('./appdb.js'),
apps = require('./apps.js'),
async = require('async'),
@@ -14,7 +13,6 @@ var addons = require('./addons.js'),
constants = require('./constants.js'),
debug = require('debug')('box:ldap'),
eventlog = require('./eventlog.js'),
groups = require('./groups.js'),
ldap = require('ldapjs'),
mail = require('./mail.js'),
mailboxdb = require('./mailboxdb.js'),
@@ -134,8 +132,8 @@ function userSearch(req, res, next) {
var dn = ldap.parseDN('cn=' + user.id + ',ou=users,dc=cloudron');
var memberof = [ GROUP_USERS_DN ];
if (users.compareRoles(user.role, users.ROLE_ADMIN) >= 0) memberof.push(GROUP_ADMINS_DN);
var groups = [ GROUP_USERS_DN ];
if (users.compareRoles(user.role, users.ROLE_ADMIN) >= 0) groups.push(GROUP_ADMINS_DN);
var displayName = user.displayName || user.username || ''; // displayName can be empty and username can be null
var nameParts = displayName.split(' ');
@@ -156,7 +154,7 @@ function userSearch(req, res, next) {
givenName: firstName,
username: user.username,
samaccountname: user.username, // to support ActiveDirectory clients
memberof: memberof
memberof: groups
}
};
@@ -329,9 +327,7 @@ function mailboxSearch(req, res, next) {
async.eachSeries(mailboxes, function (mailbox, callback) {
var dn = ldap.parseDN(`cn=${mailbox.name}@${mailbox.domain},ou=mailboxes,dc=cloudron`);
let getFunc = mailbox.ownerType === mail.OWNERTYPE_USER ? users.get : groups.get;
getFunc(mailbox.ownerId, function (error, ownerObject) {
users.get(mailbox.ownerId, function (error, userObject) {
if (error) return callback(); // skip mailboxes with unknown owner
var obj = {
@@ -339,7 +335,7 @@ function mailboxSearch(req, res, next) {
attributes: {
objectclass: ['mailbox'],
objectcategory: 'mailbox',
displayname: mailbox.ownerType === mail.OWNERTYPE_USER ? ownerObject.displayName : ownerObject.name,
displayname: userObject.displayName,
cn: `${mailbox.name}@${mailbox.domain}`,
uid: `${mailbox.name}@${mailbox.domain}`,
mail: `${mailbox.name}@${mailbox.domain}`
@@ -486,11 +482,11 @@ function authorizeUserForApp(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
assert.strictEqual(typeof req.app, 'object');
apps.hasAccessTo(req.app, req.user, function (error, hasAccess) {
apps.hasAccessTo(req.app, req.user, function (error, result) {
if (error) return next(new ldap.OperationsError(error.toString()));
// we return no such object, to avoid leakage of a users existence
if (!hasAccess) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (!result) return next(new ldap.NoSuchObjectError(req.dn.toString()));
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', appId: req.app.id }, { userId: req.user.id, user: users.removePrivateFields(req.user) });
@@ -498,30 +494,6 @@ function authorizeUserForApp(req, res, next) {
});
}
function verifyMailboxPassword(mailbox, password, callback) {
assert.strictEqual(typeof mailbox, 'object');
assert.strictEqual(typeof password, 'string');
assert.strictEqual(typeof callback, 'function');
if (mailbox.ownerType === mail.OWNERTYPE_USER) return users.verify(mailbox.ownerId, password, users.AP_MAIL /* identifier */, callback);
groups.getMembers(mailbox.ownerId, function (error, userIds) {
if (error) return callback(error);
let verifiedUser = null;
async.someSeries(userIds, function iterator(userId, iteratorDone) {
users.verify(userId, password, users.AP_MAIL /* identifier */, function (error, result) {
if (error) return iteratorDone(null, false);
verifiedUser = result;
iteratorDone(null, true);
});
}, function (error, result) {
if (!result) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
callback(null, verifiedUser);
});
});
}
function authenticateUserMailbox(req, res, next) {
debug('user mailbox auth: %s (from %s)', req.dn.toString(), req.connection.ldap.id);
@@ -541,7 +513,7 @@ function authenticateUserMailbox(req, res, next) {
if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.message));
verifyMailboxPassword(mailbox, req.credentials || '', function (error, result) {
users.verify(mailbox.ownerId, req.credentials || '', users.AP_MAIL, function (error, result) {
if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error && error.reason === BoxError.INVALID_CREDENTIALS) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.message));
@@ -575,16 +547,6 @@ function authenticateSftp(req, res, next) {
});
}
function loadSftpConfig(req, res, next) {
addons.getServicesConfig('sftp', function (error, service, servicesConfig) {
if (error) return next(new ldap.OperationsError(error.toString()));
req.requireAdmin = servicesConfig['sftp'].requireAdmin;
next();
});
}
function userSearchSftp(req, res, next) {
debug('sftp user search: dn %s, scope %s, filter %s (from %s)', req.dn.toString(), req.scope, req.filter.toString(), req.connection.ldap.id);
@@ -608,8 +570,6 @@ function userSearchSftp(req, res, next) {
users.getByUsername(username, function (error, user) {
if (error) return next(new ldap.OperationsError(error.toString()));
if (req.requireAdmin && users.compareRoles(user.role, users.ROLE_ADMIN) < 0) return next(new ldap.InsufficientAccessRightsError('Insufficient previleges'));
apps.hasAccessTo(app, user, function (error, hasAccess) {
if (error) return next(new ldap.OperationsError(error.toString()));
if (!hasAccess) return next(new ldap.InsufficientAccessRightsError('Not authorized'));
@@ -633,37 +593,16 @@ function userSearchSftp(req, res, next) {
});
}
function verifyAppMailboxPassword(addonId, username, password, callback) {
assert.strictEqual(typeof addonId, 'string');
assert.strictEqual(typeof username, 'string');
assert.strictEqual(typeof password, 'string');
assert.strictEqual(typeof callback, 'function');
const pattern = addonId === 'sendmail' ? 'MAIL_SMTP' : 'MAIL_IMAP';
appdb.getAppIdByAddonConfigValue(addonId, `%${pattern}_PASSWORD`, password, function (error, appId) { // search by password because this is unique for each app
if (error) return callback(error);
appdb.getAddonConfig(appId, addonId, function (error, result) {
if (error) return callback(error);
if (!result.some(r => r.name.endsWith(`${pattern}_USERNAME`) && r.value === username)) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
callback(null);
});
});
}
function authenticateMailAddon(req, res, next) {
debug('mail addon auth: %s (from %s)', req.dn.toString(), req.connection.ldap.id);
if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString()));
const email = req.dn.rdns[0].attrs.cn.value.toLowerCase();
const parts = email.split('@');
var email = req.dn.rdns[0].attrs.cn.value.toLowerCase();
var parts = email.split('@');
if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString()));
const addonId = req.dn.rdns[1].attrs.ou.value.toLowerCase(); // 'sendmail' or 'recvmail'
if (addonId !== 'sendmail' && addonId !== 'recvmail') return next(new ldap.OperationsError('Invalid DN'));
mail.getDomain(parts[1], function (error, domain) {
if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
@@ -671,17 +610,21 @@ function authenticateMailAddon(req, res, next) {
if (addonId === 'recvmail' && !domain.enabled) return next(new ldap.NoSuchObjectError(req.dn.toString()));
verifyAppMailboxPassword(addonId, email, req.credentials || '', function (error) {
if (!error) return res.end(); // validated as app
let namePattern; // manifest v2 has a CLOUDRON_ prefix for names
if (addonId === 'sendmail') namePattern = '%MAIL_SMTP_PASSWORD';
else if (addonId === 'recvmail') namePattern = '%MAIL_IMAP_PASSWORD';
else return next(new ldap.OperationsError('Invalid DN'));
if (error && error.reason === BoxError.INVALID_CREDENTIALS) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
// note: with sendmail addon, apps can send mail without a mailbox (unlike users)
appdb.getAppIdByAddonConfigValue(addonId, namePattern, req.credentials || '', function (error, appId) {
if (error && error.reason !== BoxError.NOT_FOUND) return next(new ldap.OperationsError(error.message));
if (appId) return res.end();
mailboxdb.getMailbox(parts[0], parts[1], function (error, mailbox) {
if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.message));
verifyMailboxPassword(mailbox, req.credentials || '', function (error, result) {
users.verify(mailbox.ownerId, req.credentials || '', users.AP_MAIL, function (error, result) {
if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error && error.reason === BoxError.INVALID_CREDENTIALS) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.message));
@@ -717,16 +660,16 @@ function start(callback) {
gServer.bind('ou=users,dc=cloudron', authenticateApp, authenticateUser, authorizeUserForApp);
// http://www.ietf.org/proceedings/43/I-D/draft-srivastava-ldap-mail-00.txt
gServer.search('ou=mailboxes,dc=cloudron', mailboxSearch); // haraka (address translation), dovecot (LMTP), sogo (mailbox search)
gServer.search('ou=mailboxes,dc=cloudron', mailboxSearch); // haraka, dovecot
gServer.bind('ou=mailboxes,dc=cloudron', authenticateUserMailbox); // apps like sogo can use domain=${domain} to authenticate a mailbox
gServer.search('ou=mailaliases,dc=cloudron', mailAliasSearch); // haraka
gServer.search('ou=mailinglists,dc=cloudron', mailingListSearch); // haraka
gServer.bind('ou=recvmail,dc=cloudron', authenticateMailAddon); // dovecot (IMAP auth)
gServer.bind('ou=sendmail,dc=cloudron', authenticateMailAddon); // haraka (MSA auth)
gServer.bind('ou=recvmail,dc=cloudron', authenticateMailAddon); // dovecot
gServer.bind('ou=sendmail,dc=cloudron', authenticateMailAddon); // haraka
gServer.bind('ou=sftp,dc=cloudron', authenticateSftp); // sftp
gServer.search('ou=sftp,dc=cloudron', loadSftpConfig, userSearchSftp);
gServer.search('ou=sftp,dc=cloudron', userSearchSftp);
gServer.compare('cn=users,ou=groups,dc=cloudron', authenticateApp, groupUsersCompare);
gServer.compare('cn=admins,ou=groups,dc=cloudron', authenticateApp, groupAdminsCompare);
+12 -45
View File
@@ -52,15 +52,11 @@ exports = module.exports = {
removeList,
resolveList,
OWNERTYPE_USER: 'user',
OWNERTYPE_GROUP: 'group',
_removeMailboxes: removeMailboxes,
_readDkimPublicKeySync: readDkimPublicKeySync
};
var addons = require('./addons.js'),
assert = require('assert'),
var assert = require('assert'),
async = require('async'),
BoxError = require('./boxerror.js'),
cloudron = require('./cloudron.js'),
@@ -79,7 +75,6 @@ var addons = require('./addons.js'),
nodemailer = require('nodemailer'),
path = require('path'),
paths = require('./paths.js'),
request = require('request'),
reverseProxy = require('./reverseproxy.js'),
safe = require('safetydance'),
settings = require('./settings.js'),
@@ -768,7 +763,7 @@ function txtRecordsWithSpf(domain, mailFqdn, callback) {
assert.strictEqual(typeof callback, 'function');
domains.getDnsRecords('', domain, 'TXT', function (error, txtRecords) {
if (error) return callback(error);
if (error) return error;
debug('txtRecordsWithSpf: current txt records - %j', txtRecords);
@@ -939,10 +934,7 @@ function changeLocation(auditSource, progressCallback, callback) {
progressCallback({ percent: progress, message: `Updating DNS of ${domainObject.domain}` });
progress += Math.round(70/allDomains.length);
upsertDnsRecords(domainObject.domain, fqdn, function (error) { // ignore any errors. we anyway report dns errors in status tab
progressCallback({ percent: progress, message: `Updated DNS of ${domainObject.domain}: ${error ? error.message : 'success'}` });
iteratorDone();
});
upsertDnsRecords(domainObject.domain, fqdn, iteratorDone);
}, function (error) {
if (error) return callback(error);
@@ -1170,11 +1162,10 @@ function getMailbox(name, domain, callback) {
});
}
function addMailbox(name, domain, ownerId, ownerType, auditSource, callback) {
function addMailbox(name, domain, userId, auditSource, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof ownerId, 'string');
assert.strictEqual(typeof ownerType, 'string');
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -1183,53 +1174,31 @@ function addMailbox(name, domain, ownerId, ownerType, auditSource, callback) {
var error = validateName(name);
if (error) return callback(error);
if (ownerType !== exports.OWNERTYPE_USER && ownerType !== exports.OWNERTYPE_GROUP) return callback(new BoxError(BoxError.BAD_FIELD, 'bad owner type'));
mailboxdb.addMailbox(name, domain, ownerId, ownerType, function (error) {
mailboxdb.addMailbox(name, domain, userId, function (error) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_ADD, auditSource, { name, domain, ownerId, ownerType });
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_ADD, auditSource, { name, domain, userId });
callback(null);
});
}
function updateMailboxOwner(name, domain, ownerId, ownerType, auditSource, callback) {
function updateMailboxOwner(name, domain, userId, auditSource, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof ownerId, 'string');
assert.strictEqual(typeof ownerType, 'string');
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
name = name.toLowerCase();
if (ownerType !== exports.OWNERTYPE_USER && ownerType !== exports.OWNERTYPE_GROUP) return callback(new BoxError(BoxError.BAD_FIELD, 'bad owner type'));
getMailbox(name, domain, function (error, result) {
if (error) return callback(error);
mailboxdb.updateMailboxOwner(name, domain, ownerId, ownerType, function (error) {
mailboxdb.updateMailboxOwner(name, domain, userId, function (error) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_UPDATE, auditSource, { name, domain, oldUserId: result.userId, ownerId, ownerType });
callback(null);
});
});
}
function removeSolrIndex(mailbox, callback) {
assert.strictEqual(typeof mailbox, 'string');
assert.strictEqual(typeof callback, 'function');
addons.getContainerDetails('mail', 'CLOUDRON_MAIL_TOKEN', function (error, addonDetails) {
if (error) return callback(error);
request.post(`https://${addonDetails.ip}:3000/solr_delete_index?access_token=${addonDetails.token}`, { timeout: 2000, rejectUnauthorized: false, json: { mailbox } }, function (error, response) {
if (error) return callback(error);
if (response.statusCode !== 200) return callback(new Error(`Error removing solr index - ${response.statusCode} ${JSON.stringify(response.body)}`));
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_UPDATE, auditSource, { name, domain, oldUserId: result.userId, userId });
callback(null);
});
@@ -1243,8 +1212,7 @@ function removeMailbox(name, domain, options, auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
const mailbox =`${name}@${domain}`;
const deleteMailFunc = options.deleteMails ? shell.sudo.bind(null, 'removeMailbox', [ REMOVE_MAILBOX, mailbox ], {}) : (next) => next();
const deleteMailFunc = options.deleteMails ? shell.sudo.bind(null, 'removeMailbox', [ REMOVE_MAILBOX, `${name}@${domain}` ], {}) : (next) => next();
deleteMailFunc(function (error) {
if (error) return callback(new BoxError(BoxError.FS_ERROR, `Error removing mailbox: ${error.message}`));
@@ -1252,7 +1220,6 @@ function removeMailbox(name, domain, options, auditSource, callback) {
mailboxdb.del(name, domain, function (error) {
if (error) return callback(error);
removeSolrIndex(mailbox, NOOP_CALLBACK);
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_REMOVE, auditSource, { name, domain });
callback();
@@ -1,24 +0,0 @@
<center>
<img src="<%= cloudronAvatarUrl %>" width="128px" height="128px"/>
<h3>{{ passwordResetEmail.salutation }}</h3>
<p>{{ passwordResetEmail.description }}</p>
<p>
<a href="<%= resetLink %>">{{ passwordResetEmail.resetAction }}</a>
</p>
<br/>
{{ passwordResetEmail.expireNote }}
<br/>
<br/>
<div style="font-size: 10px; color: #333333; background: #ffffff;">
Powered by <a href="https://cloudron.io">Cloudron</a>
</div>
</center>
@@ -1,9 +0,0 @@
{{ passwordResetEmail.salutation }}
{{ passwordResetEmail.description }}
{{ passwordResetEmail.resetActionText }}
{{ passwordResetEmail.expireNote }}
Powered by https://cloudron.io
+45
View File
@@ -0,0 +1,45 @@
<%if (format === 'text') { %>
Hi <%= user.displayName || user.username || user.email %>,
Someone, hopefully you, has requested your account's password
be reset. If you did not request this reset, please ignore this message.
To reset your password, please visit the following page:
<%- resetLink %>
Please note that the password reset link will expire in 24 hours.
Powered by https://cloudron.io
<% } else { %>
<center>
<img src="<%= cloudronAvatarUrl %>" width="128px" height="128px"/>
<h3>Hi <%= user.displayName || user.username || user.email %>,</h3>
<p>
Someone, hopefully you, has requested your account's password be reset.<br/>
If you did not request this reset, please ignore this message.
</p>
<p>
<a href="<%= resetLink %>">Click to reset your password</a>
</p>
<br/>
Please note that the password reset link will expire in 24 hours.
<br/>
<br/>
<div style="font-size: 10px; color: #333333; background: #ffffff;">
Powered by <a href="https://cloudron.io">Cloudron</a>
</div>
</center>
<% } %>
-28
View File
@@ -1,28 +0,0 @@
<center>
<img src="<%= cloudronAvatarUrl %>" width="128px" height="128px"/>
<h3>{{ welcomeEmail.salutation }}</h3>
<h2>{{ welcomeEmail.welcomeTo }}</h2>
<p>
<a href="<%= inviteLink %>">{{ welcomeEmail.inviteLinkAction }}</a>
</p>
<br/>
<br/>
<div style="font-size: 10px; color: #333333; background: #ffffff;">
<% if (invitor) { -%>
{{ welcomeEmail.invitor }}
<% } -%>
<br/>
{{ welcomeEmail.expireNote }}
<br/>
Powered by <a href="https://cloudron.io">Cloudron</a>
</div>
</center>
-13
View File
@@ -1,13 +0,0 @@
{{ welcomeEmail.salutation }}
{{ welcomeEmail.welcomeTo }}
{{ welcomeEmail.inviteLinkActionText }}
<% if (invitor) { %>
{{ welcomeEmail.invitor }}
<% } %>
{{ welcomeEmail.expireNote }}
Powered by https://cloudron.io
+50
View File
@@ -0,0 +1,50 @@
<%if (format === 'text') { %>
Dear <%= user.displayName || user.username || user.email %>,
Welcome to <%= cloudronName %>!
Follow the link to get started.
<%- inviteLink %>
<% if (invitor && invitor.email) { %>
You are receiving this email because you were invited by <%= invitor.email %>.
<% } %>
Please note that the invite link will expire in 7 days.
Powered by https://cloudron.io
<% } else { %>
<center>
<img src="<%= cloudronAvatarUrl %>" width="128px" height="128px"/>
<h3>Hi <%= user.displayName || user.username || user.email %>,</h3>
<h2>Welcome to <%= cloudronName %>!</h2>
<p>
<a href="<%= inviteLink %>">Get started</a>.
</p>
<br/>
<br/>
<div style="font-size: 10px; color: #333333; background: #ffffff;">
<% if (invitor && invitor.email) { %>
You are receiving this email because you were invited by <%= invitor.email %>.
<% } %>
<br/>
Please note that the invite link will expire in 7 days.
<br/>
Powered by <a href="https://cloudron.io">Cloudron</a>
</div>
</center>
<% } %>
+9 -11
View File
@@ -42,7 +42,7 @@ var assert = require('assert'),
safe = require('safetydance'),
util = require('util');
var MAILBOX_FIELDS = [ 'name', 'type', 'ownerId', 'ownerType', 'aliasName', 'aliasDomain', 'creationTime', 'membersJson', 'membersOnly', 'domain' ].join(',');
var MAILBOX_FIELDS = [ 'name', 'type', 'ownerId', 'aliasName', 'aliasDomain', 'creationTime', 'membersJson', 'membersOnly', 'domain' ].join(',');
function postProcess(data) {
data.members = safe.JSON.parse(data.membersJson) || [ ];
@@ -53,14 +53,13 @@ function postProcess(data) {
return data;
}
function addMailbox(name, domain, ownerId, ownerType, callback) {
function addMailbox(name, domain, ownerId, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof ownerId, 'string');
assert.strictEqual(typeof ownerType, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('INSERT INTO mailboxes (name, type, domain, ownerId, ownerType) VALUES (?, ?, ?, ?, ?)', [ name, exports.TYPE_MAILBOX, domain, ownerId, ownerType ], function (error) {
database.query('INSERT INTO mailboxes (name, type, domain, ownerId) VALUES (?, ?, ?, ?)', [ name, exports.TYPE_MAILBOX, domain, ownerId ], function (error) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS, 'mailbox already exists'));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
@@ -68,14 +67,13 @@ function addMailbox(name, domain, ownerId, ownerType, callback) {
});
}
function updateMailboxOwner(name, domain, ownerId, ownerType, callback) {
function updateMailboxOwner(name, domain, ownerId, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof ownerId, 'string');
assert.strictEqual(typeof ownerType, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('UPDATE mailboxes SET ownerId = ?, ownerType = ? WHERE name = ? AND domain = ?', [ ownerId, ownerType, name, domain ], function (error, result) {
database.query('UPDATE mailboxes SET ownerId = ? WHERE name = ? AND domain = ?', [ ownerId, name, domain ], function (error, result) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.affectedRows === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Mailbox not found'));
@@ -90,8 +88,8 @@ function addList(name, domain, members, membersOnly, callback) {
assert.strictEqual(typeof membersOnly, 'boolean');
assert.strictEqual(typeof callback, 'function');
database.query('INSERT INTO mailboxes (name, type, domain, ownerId, ownerType, membersJson, membersOnly) VALUES (?, ?, ?, ?, ?, ?, ?)',
[ name, exports.TYPE_LIST, domain, 'admin', 'user', JSON.stringify(members), membersOnly ], function (error) {
database.query('INSERT INTO mailboxes (name, type, domain, ownerId, membersJson, membersOnly) VALUES (?, ?, ?, ?, ?, ?)',
[ name, exports.TYPE_LIST, domain, 'admin', JSON.stringify(members), membersOnly ], function (error) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS, 'mailbox already exists'));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
@@ -316,8 +314,8 @@ function setAliasesForName(name, domain, aliases, callback) {
// clear existing aliases
queries.push({ query: 'DELETE FROM mailboxes WHERE aliasName = ? AND aliasDomain = ? AND type = ?', args: [ name, domain, exports.TYPE_ALIAS ] });
aliases.forEach(function (alias) {
queries.push({ query: 'INSERT INTO mailboxes (name, domain, type, aliasName, aliasDomain, ownerId, ownerType) VALUES (?, ?, ?, ?, ?, ?, ?)',
args: [ alias.name, alias.domain, exports.TYPE_ALIAS, name, domain, results[0].ownerId, results[0].ownerType ] });
queries.push({ query: 'INSERT INTO mailboxes (name, domain, type, aliasName, aliasDomain, ownerId) VALUES (?, ?, ?, ?, ?, ?)',
args: [ alias.name, alias.domain, exports.TYPE_ALIAS, name, domain, results[0].ownerId ] });
});
database.transaction(queries, function (error) {
+42 -46
View File
@@ -34,7 +34,6 @@ var assert = require('assert'),
safe = require('safetydance'),
settings = require('./settings.js'),
showdown = require('showdown'),
translation = require('./translation.js'),
smtpTransport = require('nodemailer-smtp-transport'),
util = require('util');
@@ -92,21 +91,14 @@ function sendMail(mailOptions, callback) {
});
}
function render(templateFile, params, translationAssets) {
function render(templateFile, params) {
assert.strictEqual(typeof templateFile, 'string');
assert.strictEqual(typeof params, 'object');
var content = null;
var raw = safe.fs.readFileSync(path.join(MAIL_TEMPLATES_DIR, templateFile), 'utf8');
if (raw === null) {
debug(`Error loading ${templateFile}`);
return '';
}
if (typeof translationAssets === 'object') raw = translation.translate(raw, translationAssets.translations || {}, translationAssets.fallback || {});
try {
content = ejs.render(raw, params);
content = ejs.render(safe.fs.readFileSync(path.join(MAIL_TEMPLATES_DIR, templateFile), 'utf8'), params);
} catch (e) {
debug(`Error rendering ${templateFile}`, e);
}
@@ -143,28 +135,30 @@ function sendInvite(user, invitor, inviteLink) {
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
translation.getTranslations(function (error, translationAssets) {
if (error) return debug('Error getting translations:', error);
var templateData = {
user: user,
webadminUrl: settings.adminOrigin(),
inviteLink: inviteLink,
invitor: invitor,
cloudronName: mailConfig.cloudronName,
cloudronAvatarUrl: settings.adminOrigin() + '/api/v1/cloudron/avatar'
};
var templateData = {
user: user.displayName || user.username || user.email,
webadminUrl: settings.adminOrigin(),
inviteLink: inviteLink,
invitor: invitor ? invitor.email : null,
cloudronName: mailConfig.cloudronName,
cloudronAvatarUrl: settings.adminOrigin() + '/api/v1/cloudron/avatar'
};
var templateDataText = JSON.parse(JSON.stringify(templateData));
templateDataText.format = 'text';
var mailOptions = {
from: mailConfig.notificationFrom,
to: user.fallbackEmail,
subject: ejs.render(translation.translate('{{ welcomeEmail.subject }}', translationAssets.translations || {}, translationAssets.fallback || {}), { cloudron: mailConfig.cloudronName }),
text: render('welcome_user-text.ejs', templateData, translationAssets),
html: render('welcome_user-html.ejs', templateData, translationAssets)
};
var templateDataHTML = JSON.parse(JSON.stringify(templateData));
templateDataHTML.format = 'html';
sendMail(mailOptions);
});
var mailOptions = {
from: mailConfig.notificationFrom,
to: user.fallbackEmail,
subject: util.format('Welcome to %s', mailConfig.cloudronName),
text: render('welcome_user.ejs', templateDataText),
html: render('welcome_user.ejs', templateDataHTML)
};
sendMail(mailOptions);
});
}
@@ -227,26 +221,28 @@ function passwordReset(user) {
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
translation.getTranslations(function (error, translationAssets) {
if (error) return debug('Error getting translations:', error);
var templateData = {
user: user,
resetLink: `${settings.adminOrigin()}/login.html?resetToken=${user.resetToken}`,
cloudronName: mailConfig.cloudronName,
cloudronAvatarUrl: settings.adminOrigin() + '/api/v1/cloudron/avatar'
};
var templateData = {
user: user.displayName || user.username || user.email,
resetLink: `${settings.adminOrigin()}/login.html?resetToken=${user.resetToken}`,
cloudronName: mailConfig.cloudronName,
cloudronAvatarUrl: settings.adminOrigin() + '/api/v1/cloudron/avatar'
};
var templateDataText = JSON.parse(JSON.stringify(templateData));
templateDataText.format = 'text';
var mailOptions = {
from: mailConfig.notificationFrom,
to: user.fallbackEmail,
subject: ejs.render(translation.translate('{{ passwordResetEmail.subject }}', translationAssets.translations || {}, translationAssets.fallback || {}), { cloudron: mailConfig.cloudronName }),
text: render('password_reset-text.ejs', templateData, translationAssets),
html: render('password_reset-html.ejs', templateData, translationAssets)
};
var templateDataHTML = JSON.parse(JSON.stringify(templateData));
templateDataHTML.format = 'html';
sendMail(mailOptions);
});
var mailOptions = {
from: mailConfig.notificationFrom,
to: user.fallbackEmail,
subject: util.format('[%s] Password Reset', mailConfig.cloudronName),
text: render('password_reset.ejs', templateDataText),
html: render('password_reset.ejs', templateDataHTML)
};
sendMail(mailOptions);
});
}
+1 -2
View File
@@ -1,11 +1,10 @@
'use strict';
exports = module.exports = {
cookieParser: require('cookie-parser'),
cors: require('./cors'),
json: require('body-parser').json,
morgan: require('morgan'),
proxy: require('./proxy-middleware.js'),
proxy: require('proxy-middleware'),
lastMile: require('connect-lastmile'),
multipart: require('./multipart.js'),
timeout: require('connect-timeout'),
-149
View File
@@ -1,149 +0,0 @@
// https://github.com/cloudron-io/node-proxy-middleware
// MIT license
// contains https://github.com/gonzalocasas/node-proxy-middleware/pull/59
var os = require('os');
var http = require('http');
var https = require('https');
var owns = {}.hasOwnProperty;
module.exports = function proxyMiddleware(options) {
//enable ability to quickly pass a url for shorthand setup
if(typeof options === 'string'){
options = require('url').parse(options);
}
var httpLib = options.protocol === 'https:' ? https : http;
var request = httpLib.request;
options = options || {};
options.hostname = options.hostname;
options.port = options.port;
options.pathname = options.pathname || '/';
return function (req, resp, next) {
var url = req.url;
// You can pass the route within the options, as well
if (typeof options.route === 'string') {
if (url === options.route) {
url = '';
} else if (url.slice(0, options.route.length) === options.route) {
url = url.slice(options.route.length);
} else {
return next();
}
}
//options for this request
var opts = extend({}, options);
if (url && url.charAt(0) === '?') { // prevent /api/resource/?offset=0
if (options.pathname.length > 1 && options.pathname.charAt(options.pathname.length - 1) === '/') {
opts.path = options.pathname.substring(0, options.pathname.length - 1) + url;
} else {
opts.path = options.pathname + url;
}
} else if (url) {
opts.path = slashJoin(options.pathname, url);
} else {
opts.path = options.pathname;
}
opts.method = req.method;
opts.headers = options.headers ? merge(req.headers, options.headers) : req.headers;
applyViaHeader(req.headers, opts, opts.headers);
if (!options.preserveHost) {
// Forwarding the host breaks dotcloud
delete opts.headers.host;
}
var myReq = request(opts, function (myRes) {
var statusCode = myRes.statusCode
, headers = myRes.headers
, location = headers.location;
// Fix the location
if (((statusCode > 300 && statusCode < 304) || statusCode === 201) && location && location.indexOf(options.href) > -1) {
// absoulte path
headers.location = location.replace(options.href, slashJoin('/', slashJoin((options.route || ''), '')));
}
applyViaHeader(myRes.headers, opts, myRes.headers);
rewriteCookieHosts(myRes.headers, opts, myRes.headers, req);
resp.writeHead(myRes.statusCode, myRes.headers);
myRes.on('error', function (err) {
next(err);
});
myRes.on('end', function (err) {
next();
});
myRes.pipe(resp);
});
myReq.on('error', function (err) {
next(err);
});
if (!req.readable) {
myReq.end();
} else {
req.pipe(myReq);
}
};
};
function applyViaHeader(existingHeaders, opts, applyTo) {
if (!opts.via) return;
var viaName = (true === opts.via) ? os.hostname() : opts.via;
var viaHeader = '1.1 ' + viaName;
if(existingHeaders.via) {
viaHeader = existingHeaders.via + ', ' + viaHeader;
}
applyTo.via = viaHeader;
}
function rewriteCookieHosts(existingHeaders, opts, applyTo, req) {
if (!opts.cookieRewrite || !owns.call(existingHeaders, 'set-cookie')) {
return;
}
var existingCookies = existingHeaders['set-cookie'],
rewrittenCookies = [],
rewriteHostname = (true === opts.cookieRewrite) ? os.hostname() : opts.cookieRewrite;
if (!Array.isArray(existingCookies)) {
existingCookies = [ existingCookies ];
}
for (var i = 0; i < existingCookies.length; i++) {
var rewrittenCookie = existingCookies[i].replace(/(Domain)=[a-z\.-_]*?(;|$)/gi, '$1=' + rewriteHostname + '$2');
if (!req.connection.encrypted) {
rewrittenCookie = rewrittenCookie.replace(/;\s*?(Secure)/i, '');
}
rewrittenCookies.push(rewrittenCookie);
}
applyTo['set-cookie'] = rewrittenCookies;
}
function slashJoin(p1, p2) {
var trailing_slash = false;
if (p1.length && p1[p1.length - 1] === '/') { trailing_slash = true; }
if (trailing_slash && p2.length && p2[0] === '/') {p2 = p2.substring(1); }
return p1 + p2;
}
function extend(obj, src) {
for (var key in src) if (owns.call(src, key)) obj[key] = src[key];
return obj;
}
//merges data without changing state in either argument
function merge(src1, src2) {
var merged = {};
extend(merged, src1);
extend(merged, src2);
return merged;
}
+2 -49
View File
@@ -147,7 +147,7 @@ server {
<% if ( endpoint === 'admin' ) { %>
proxy_pass http://127.0.0.1:3000;
<% } else if ( endpoint === 'app' ) { %>
proxy_pass http://<%= ip %>:<%= port %>;
proxy_pass http://127.0.0.1:<%= port %>;
<% } else if ( endpoint === 'redirect' ) { %>
return 302 https://<%= redirectTo %>$request_uri;
<% } %>
@@ -159,27 +159,6 @@ server {
try_files /$1 @wellknown-upstream;
}
<% if (proxyAuth.enabled) { %>
proxy_set_header X-App-ID "<%= proxyAuth.id %>";
location = /proxy-auth {
internal;
proxy_pass http://127.0.0.1:3001/auth;
proxy_pass_request_body off;
# repeat proxy headers since we addded proxy_set_header at this location level
proxy_set_header X-App-ID "<%= proxyAuth.id %>";
proxy_set_header Content-Length "";
}
location ~ ^/(login|logout)$ {
proxy_pass http://127.0.0.1:3001;
}
location @proxy-auth-login {
return 302 /login?redirect=$request_uri;
}
<% } %>
location / {
# increase the proxy buffer sizes to not run into buffer issues (http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_buffers)
proxy_buffer_size 128k;
@@ -226,11 +205,6 @@ server {
client_max_body_size 0;
}
location ~ ^/api/v1/volumes/.*/files/ {
proxy_pass http://127.0.0.1:3000;
client_max_body_size 0;
}
# graphite paths (uncomment block below and visit /graphite-web/dashboard)
# remember to comment out the CSP policy as well to access the graphite dashboard
# location ~ ^/graphite-web/ {
@@ -243,28 +217,7 @@ server {
index index.html index.htm;
}
<% } else if ( endpoint === 'app' ) { %>
location = /appstatus.html {
}
<% if (proxyAuth.enabled) { %>
location "<%= proxyAuth.path %>" {
auth_request /proxy-auth;
auth_request_set $auth_cookie $upstream_http_set_cookie;
add_header Set-Cookie $auth_cookie;
error_page 401 = @proxy-auth-login;
proxy_pass http://<%= ip %>:<%= port %>;
}
<% } %>
<% Object.keys(httpPaths).forEach(function (path) { -%>
location "<%= path %>" {
# the trailing / will replace part of the original URI matched by the location.
proxy_pass http://<%= ip %>:<%= httpPaths[path] %>/;
}
<% }); %>
proxy_pass http://<%= ip %>:<%= port %>;
proxy_pass http://127.0.0.1:<%= port %>;
<% } else if ( endpoint === 'redirect' ) { %>
# redirect everything to the app. this is temporary because there is no way
# to clear a permanent redirect on the browser
+1 -4
View File
@@ -16,7 +16,6 @@ exports = module.exports = {
CLOUDRON_DEFAULT_AVATAR_FILE: path.join(__dirname + '/../assets/avatar.png'),
INFRA_VERSION_FILE: path.join(baseDir(), 'platformdata/INFRA_VERSION'),
DASHBOARD_DIR: constants.TEST ? path.join(__dirname, '../../dashboard/src') : path.join(baseDir(), 'box/dashboard/dist'),
PROVIDER_FILE: '/etc/cloudron/PROVIDER',
@@ -36,14 +35,12 @@ exports = module.exports = {
SNAPSHOT_INFO_FILE: path.join(baseDir(), 'platformdata/backup/snapshot-info.json'),
DYNDNS_INFO_FILE: path.join(baseDir(), 'platformdata/dyndns-info.json'),
FEATURES_INFO_FILE: path.join(baseDir(), 'platformdata/features-info.json'),
PROXY_AUTH_TOKEN_SECRET_FILE: path.join(baseDir(), 'platformdata/proxy-auth-token-secret'),
VERSION_FILE: path.join(baseDir(), 'platformdata/VERSION'),
// this is not part of appdata because an icon may be set before install
APP_ICONS_DIR: path.join(baseDir(), 'boxdata/appicons'),
PROFILE_ICONS_DIR: path.join(baseDir(), 'boxdata/profileicons'),
MAIL_DATA_DIR: path.join(baseDir(), 'boxdata/mail'),
SFTP_KEYS_DIR: path.join(baseDir(), 'boxdata/sftp/ssh'),
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'),
@@ -61,5 +58,5 @@ exports = module.exports = {
// 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'),
UPDATER_LOG_FILE: path.join(baseDir(), 'platformdata/logs/updater/app.log')
};
+3 -3
View File
@@ -54,7 +54,7 @@ function start(callback) {
if (error) return callback(error);
async.series([
(next) => { if (existingInfra.version !== infra.version) removeAllContainers(next); else next(); },
(next) => { if (existingInfra.version !== infra.version) removeAllContainers(existingInfra, next); else next(); },
markApps.bind(null, existingInfra), // mark app state before we start addons. this gives the db import logic a chance to mark an app as errored
graphs.startGraphite.bind(null, existingInfra),
sftp.startSftp.bind(null, existingInfra),
@@ -115,14 +115,14 @@ function pruneInfraImages(callback) {
debug(`pruneInfraImages: removing unused image of ${image.repo}: tag: ${parts[1]} id: ${parts[0]}`);
let result = safe.child_process.execSync(`docker rmi ${parts[0]}`, { encoding: 'utf8' });
if (result === null) debug(`Error removing image ${parts[0]}: ${safe.error.mesage}`);
if (result === null) debug(`Erroring removing image ${parts[0]}: ${safe.error.mesage}`);
}
iteratorCallback();
}, callback);
}
function removeAllContainers(callback) {
function removeAllContainers(existingInfra, callback) {
debug('removeAllContainers: removing all containers for infra upgrade');
async.series([
+1 -3
View File
@@ -11,7 +11,6 @@ var assert = require('assert'),
async = require('async'),
backups = require('./backups.js'),
BoxError = require('./boxerror.js'),
branding = require('./branding.js'),
constants = require('./constants.js'),
cloudron = require('./cloudron.js'),
debug = require('debug')('box:provision'),
@@ -234,9 +233,8 @@ function getStatus(callback) {
apiServerOrigin: settings.apiServerOrigin(), // used by CaaS tool
webServerOrigin: settings.webServerOrigin(), // used by CaaS tool
cloudronName: allSettings[settings.CLOUDRON_NAME_KEY],
footer: branding.renderFooter(allSettings[settings.FOOTER_KEY] || constants.FOOTER),
footer: allSettings[settings.FOOTER_KEY] || constants.FOOTER,
adminFqdn: settings.adminDomain() ? settings.adminFqdn() : null,
language: allSettings[settings.LANGUAGE_KEY],
activated: activated,
provider: settings.provider() // used by setup wizard of marketplace images
}, gProvisionStatus));
-226
View File
@@ -1,226 +0,0 @@
'use strict';
// heavily inspired from https://gock.net/blog/2020/nginx-subrequest-authentication-server/ and https://github.com/andygock/auth-server
exports = module.exports = {
start,
stop
};
const apps = require('./apps.js'),
assert = require('assert'),
basicAuth = require('basic-auth'),
constants = require('./constants.js'),
debug = require('debug')('box:proxyAuth'),
ejs = require('ejs'),
express = require('express'),
fs = require('fs'),
hat = require('./hat.js'),
http = require('http'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
jwt = require('jsonwebtoken'),
middleware = require('./middleware'),
path = require('path'),
paths = require('./paths.js'),
safe = require('safetydance'),
translation = require('./translation.js'),
users = require('./users.js');
let gHttpServer = null;
let TOKEN_SECRET = null;
const EXPIRY_DAYS = 7;
function jwtVerify(req, res, next) {
const token = req.cookies.authToken;
if (!token) return next();
jwt.verify(token, TOKEN_SECRET, function (error, decoded) {
if (error) {
debug('clearing token', error);
res.clearCookie('authToken');
return next(new HttpError(403, 'Malformed token or bad signature'));
}
req.user = decoded.user || null;
next();
});
}
function basicAuthVerify(req, res, next) {
const appId = req.headers['x-app-id'] || '';
const credentials = basicAuth(req);
if (!appId || !credentials) return next();
const api = credentials.name.indexOf('@') !== -1 ? users.verifyWithEmail : users.verifyWithUsername;
api(credentials.name, credentials.pass, appId, function (error, user) {
if (error) return next(new HttpError(403, 'Invalid username or password' ));
req.user = user;
next();
});
}
function loginPage(req, res, next) {
const appId = req.headers['x-app-id'] || '';
if (!appId) return next(new HttpError(503, 'Nginx misconfiguration'));
translation.getTranslations(function (error, translationAssets) {
if (error) return next(new HttpError(500, 'No translation found'));
const raw = safe.fs.readFileSync(path.join(paths.DASHBOARD_DIR, 'templates/proxyauth-login.ejs'), 'utf8');
if (raw === null) return next(new HttpError(500, 'Login template not found'));
const translatedContent = translation.translate(raw, translationAssets.translations || {}, translationAssets.fallback || {});
var finalContent = '';
apps.get(appId, function (error, app) {
if (error) return next(new HttpError(503, error.message));
const title = app.label || app.manifest.title;
apps.getIconPath(app, {}, function (error, iconPath) {
const icon = 'data:image/png;base64,' + safe.fs.readFileSync(iconPath || '', 'base64');
try {
finalContent = ejs.render(translatedContent, { title, icon });
} catch (e) {
debug('Error rendering proxyauth-login.ejs', e);
return next(new HttpError(500, 'Login template error'));
}
res.set('Content-Type', 'text/html');
return res.send(finalContent);
});
});
});
}
// called by nginx to authorize any protected route
function auth(req, res, next) {
if (!req.user) return next(new HttpError(401, 'Unauthorized'));
// user is already authenticated, refresh cookie
const token = jwt.sign({ user: req.user }, TOKEN_SECRET, { expiresIn: `${EXPIRY_DAYS}d` });
res.cookie('authToken', token, {
httpOnly: true,
maxAge: EXPIRY_DAYS * 86400 * 1000, // milliseconds
secure: true
});
return next(new HttpSuccess(200, {}));
}
// endpoint called by login page, username and password posted as JSON body
function authenticate(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
const appId = req.headers['x-app-id'] || '';
if (!appId) return next(new HttpError(503, 'Nginx misconfiguration'));
if (typeof req.body.username !== 'string') return next(new HttpError(400, 'username must be non empty string' ));
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'password must be non empty string' ));
const { username, password } = req.body;
const api = username.indexOf('@') !== -1 ? users.verifyWithEmail : users.verifyWithUsername;
api(username, password, appId, function (error, user) {
if (error) return next(new HttpError(403, 'Invalid username or password' ));
req.user = user;
next();
});
}
function authorize(req, res, next) {
const appId = req.headers['x-app-id'] || '';
if (!appId) return next(new HttpError(503, 'Nginx misconfiguration'));
apps.get(appId, function (error, app) {
if (error) return next(new HttpError(403, 'No such app' ));
apps.hasAccessTo(app, req.user, function (error, hasAccess) {
if (error) return next(new HttpError(403, 'Forbidden' ));
if (!hasAccess) return next(new HttpError(403, 'Forbidden' ));
const token = jwt.sign({ user: users.removePrivateFields(req.user) }, TOKEN_SECRET, { expiresIn: `${EXPIRY_DAYS}d` });
res.cookie('authToken', token, {
httpOnly: true,
maxAge: EXPIRY_DAYS * 86400 * 1000, // milliseconds
secure: true
});
res.redirect('/');
});
});
}
function logoutPage(req, res) {
res.clearCookie('authToken');
res.redirect('/'); // do not redirect to '/login' as it may not be protected
}
function logout(req, res, next) {
res.clearCookie('authToken');
next(new HttpSuccess(200, {}));
}
// provides webhooks for the auth wall
function initializeAuthwallExpressSync() {
let app = express();
let httpServer = http.createServer(app);
let QUERY_LIMIT = '1mb'; // max size for json and urlencoded queries
let REQUEST_TIMEOUT = 10000; // timeout for all requests
let json = middleware.json({ strict: true, limit: QUERY_LIMIT }); // application/json
if (process.env.BOX_ENV !== 'test') app.use(middleware.morgan('proxyauth :method :url :status :response-time ms - :res[content-length]', { immediate: false }));
var router = new express.Router();
router.del = router.delete; // amend router.del for readability further on
app
.use(middleware.timeout(REQUEST_TIMEOUT))
.use(middleware.cookieParser())
.use(router)
.use(middleware.lastMile());
router.get ('/login', loginPage);
router.get ('/auth', jwtVerify, basicAuthVerify, auth);
router.post('/login', json, authenticate, authorize);
router.get ('/logout', logoutPage);
router.post('/logout', json, logout);
return httpServer;
}
function start(callback) {
assert.strictEqual(typeof callback, 'function');
assert.strictEqual(gHttpServer, null, 'Authwall is already up and running.');
if (!fs.existsSync(paths.PROXY_AUTH_TOKEN_SECRET_FILE)) {
TOKEN_SECRET = hat(64);
fs.writeFileSync(paths.PROXY_AUTH_TOKEN_SECRET_FILE, TOKEN_SECRET, 'utf8');
} else {
TOKEN_SECRET = fs.readFileSync(paths.PROXY_AUTH_TOKEN_SECRET_FILE, 'utf8').trim();
}
gHttpServer = initializeAuthwallExpressSync();
gHttpServer.listen(constants.AUTHWALL_PORT, '127.0.0.1', callback);
}
function stop(callback) {
assert.strictEqual(typeof callback, 'function');
if (!gHttpServer) return callback(null);
gHttpServer.close(callback);
gHttpServer = null;
}
+11 -27
View File
@@ -109,7 +109,7 @@ function providerMatchesSync(domainObject, certFilePath, apiOptions) {
const domain = subject.substr(subject.indexOf('=') + 1).trim(); // subject can be /CN=, CN=, CN = and other forms
const issuer = subjectAndIssuer.match(/^issuer=(.*)$/m)[1];
const isWildcardCert = domain.includes('*');
const isLetsEncryptProd = issuer.includes('Let\'s Encrypt');
const isLetsEncryptProd = issuer.includes('Let\'s Encrypt Authority');
const issuerMismatch = (apiOptions.prod && !isLetsEncryptProd) || (!apiOptions.prod && isLetsEncryptProd);
// bare domain is not part of wildcard SAN
@@ -117,9 +117,7 @@ function providerMatchesSync(domainObject, certFilePath, apiOptions) {
const mismatch = issuerMismatch || wildcardMismatch;
debug(`providerMatchesSync: ${certFilePath} subject=${subject} domain=${domain} issuer=${issuer} `
+ `wildcard=${isWildcardCert}/${apiOptions.wildcard} prod=${isLetsEncryptProd}/${apiOptions.prod} `
+ `issuerMismatch=${issuerMismatch} wildcardMismatch=${wildcardMismatch} match=${!mismatch}`);
debug(`providerMatchesSync: ${certFilePath} subject=${subject} domain=${domain} issuer=${issuer} wildcard=${isWildcardCert}/${apiOptions.wildcard} prod=${isLetsEncryptProd}/${apiOptions.prod} match=${!mismatch}`);
return !mismatch;
}
@@ -162,7 +160,7 @@ function validateCertificate(location, domainObject, certificate) {
}
function reload(callback) {
if (constants.TEST) return callback();
if (process.env.BOX_ENV === 'test') return callback();
shell.sudo('reload', [ RELOAD_NGINX_CMD ], {}, function (error) {
if (error) return callback(new BoxError(BoxError.NGINX_ERROR, `Error reloading nginx: ${error.message}`));
@@ -188,8 +186,7 @@ function generateFallbackCertificateSync(domainObject) {
opensslConfWithSan = `${opensslConf}\n[SAN]\nsubjectAltName=DNS:${domain},DNS:*.${cn}\n`;
let configFile = path.join(os.tmpdir(), 'openssl-' + crypto.randomBytes(4).readUInt32LE(0) + '.conf');
safe.fs.writeFileSync(configFile, opensslConfWithSan, 'utf8');
// the days field is chosen to be less than 825 days per apple requirement (https://support.apple.com/en-us/HT210176)
let certCommand = util.format(`openssl req -x509 -newkey rsa:2048 -keyout ${keyFilePath} -out ${certFilePath} -days 800 -subj /CN=*.${cn} -extensions SAN -config ${configFile} -nodes`);
let certCommand = util.format(`openssl req -x509 -newkey rsa:2048 -keyout ${keyFilePath} -out ${certFilePath} -days 3650 -subj /CN=*.${cn} -extensions SAN -config ${configFile} -nodes`);
if (!safe.child_process.execSync(certCommand)) return { error: new BoxError(BoxError.OPENSSL_ERROR, safe.error.message) };
safe.fs.unlinkSync(configFile);
@@ -336,7 +333,7 @@ function ensureCertificate(vhost, domain, auditSource, callback) {
debug(`ensureCertificate: ${vhost} certificate already exists at ${currentBundle.keyFilePath}`);
if (!isExpiringSync(currentBundle.certFilePath, 24 * 30) && providerMatchesSync(domainObject, currentBundle.certFilePath, apiOptions)) return callback(null, currentBundle, { renewed: false });
debug(`ensureCertificate: ${vhost} cert requires renewal`);
debug(`ensureCertificate: ${vhost} cert require renewal`);
} else {
debug(`ensureCertificate: ${vhost} cert does not exist`);
}
@@ -382,8 +379,7 @@ function writeDashboardNginxConfig(bundle, configFileName, vhost, callback) {
endpoint: 'admin',
certFilePath: bundle.certFilePath,
keyFilePath: bundle.keyFilePath,
robotsTxtQuoted: JSON.stringify('User-agent: *\nDisallow: /\n'),
proxyAuth: { enabled: false, id: null, path: '/' }
robotsTxtQuoted: JSON.stringify('User-agent: *\nDisallow: /\n')
};
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, configFileName);
@@ -452,20 +448,13 @@ function writeAppNginxConfig(app, bundle, callback) {
adminOrigin: settings.adminOrigin(),
vhost: app.fqdn,
hasIPv6: sysinfo.hasIPv6(),
ip: app.containerIp,
port: app.manifest.httpPort,
port: app.httpPort,
endpoint: endpoint,
certFilePath: bundle.certFilePath,
keyFilePath: bundle.keyFilePath,
robotsTxtQuoted,
cspQuoted,
hideHeaders,
proxyAuth: {
enabled: app.sso && app.manifest.addons && app.manifest.addons.proxyAuth,
id: app.id,
path: safe.query(app.manifest, 'addons.proxyAuth.path') || '/'
},
httpPaths: app.manifest.httpPaths || {}
hideHeaders
};
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
@@ -496,8 +485,7 @@ function writeAppRedirectNginxConfig(app, fqdn, bundle, callback) {
keyFilePath: bundle.keyFilePath,
robotsTxtQuoted: null,
cspQuoted: null,
hideHeaders: [],
proxyAuth: { enabled: false, id: app.id, path: '/' }
hideHeaders: []
};
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
@@ -669,8 +657,7 @@ function writeDefaultConfig(options, callback) {
debug('writeDefaultConfig: create new cert');
const cn = 'cloudron-' + (new Date()).toISOString(); // randomize date a bit to keep firefox happy
// the days field is chosen to be less than 825 days per apple requirement (https://support.apple.com/en-us/HT210176)
if (!safe.child_process.execSync(`openssl req -x509 -newkey rsa:2048 -keyout ${keyFilePath} -out ${certFilePath} -days 800 -subj /CN=${cn} -nodes`)) {
if (!safe.child_process.execSync(`openssl req -x509 -newkey rsa:2048 -keyout ${keyFilePath} -out ${certFilePath} -days 3650 -subj /CN=${cn} -nodes`)) {
debug(`writeDefaultConfig: could not generate certificate: ${safe.error.message}`);
return callback(new BoxError(BoxError.OPENSSL_ERROR, safe.error));
}
@@ -684,14 +671,11 @@ function writeDefaultConfig(options, callback) {
endpoint: options.activated ? 'ip' : 'setup',
certFilePath,
keyFilePath,
robotsTxtQuoted: JSON.stringify('User-agent: *\nDisallow: /\n'),
proxyAuth: { enabled: false, id: null, path: '/' }
robotsTxtQuoted: JSON.stringify('User-agent: *\nDisallow: /\n')
};
const nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
const nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, constants.NGINX_DEFAULT_CONFIG_FILE_NAME);
debug(`writeDefaultConfig: writing configs for endpoint "${data.endpoint}"`);
if (!safe.fs.writeFileSync(nginxConfigFilename, nginxConf)) return callback(new BoxError(BoxError.FS_ERROR, safe.error));
reload(callback);
+48 -58
View File
@@ -1,50 +1,49 @@
'use strict';
exports = module.exports = {
getApp,
getApps,
getAppIcon,
install,
uninstall,
restore,
importApp,
exportApp,
backup,
update,
getLogs,
getLogStream,
listBackups,
repair,
getApp: getApp,
getApps: getApps,
getAppIcon: getAppIcon,
install: install,
uninstall: uninstall,
restore: restore,
importApp: importApp,
backup: backup,
update: update,
getLogs: getLogs,
getLogStream: getLogStream,
listBackups: listBackups,
repair: repair,
setAccessRestriction,
setLabel,
setTags,
setIcon,
setMemoryLimit,
setCpuShares,
setAutomaticBackup,
setAutomaticUpdate,
setReverseProxyConfig,
setCertificate,
setDebugMode,
setEnvironment,
setMailbox,
setLocation,
setDataDir,
setMounts,
setAccessRestriction: setAccessRestriction,
setLabel: setLabel,
setTags: setTags,
setIcon: setIcon,
setMemoryLimit: setMemoryLimit,
setCpuShares: setCpuShares,
setAutomaticBackup: setAutomaticBackup,
setAutomaticUpdate: setAutomaticUpdate,
setReverseProxyConfig: setReverseProxyConfig,
setCertificate: setCertificate,
setDebugMode: setDebugMode,
setEnvironment: setEnvironment,
setMailbox: setMailbox,
setLocation: setLocation,
setDataDir: setDataDir,
setBinds: setBinds,
stop,
start,
restart,
exec,
execWebSocket,
stop: stop,
start: start,
restart: restart,
exec: exec,
execWebSocket: execWebSocket,
clone,
clone: clone,
uploadFile,
downloadFile,
uploadFile: uploadFile,
downloadFile: downloadFile,
load
load: load
};
var apps = require('../apps.js'),
@@ -444,17 +443,6 @@ function importApp(req, res, next) {
});
}
function exportApp(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.resource, 'object');
apps.exportApp(req.resource, {}, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
function clone(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.resource, 'object');
@@ -609,7 +597,7 @@ function getLogs(req, res, next) {
res.writeHead(200, {
'Content-Type': 'application/x-logs',
'Content-Disposition': `attachment; filename="${req.resource.id}.log"`,
'Content-Disposition': 'attachment; filename="log.txt"',
'Cache-Control': 'no-cache',
'X-Accel-Buffering': 'no' // disable nginx buffering
});
@@ -778,20 +766,22 @@ function downloadFile(req, res, next) {
});
}
function setMounts(req, res, next) {
function setBinds(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.resource, 'object');
if (!Array.isArray(req.body.mounts)) return next(new HttpError(400, 'mounts should be an array'));
for (let m of req.body.mounts) {
if (!m || typeof m !== 'object') return next(new HttpError(400, 'mounts must be an object'));
if (typeof m.volumeId !== 'string') return next(new HttpError(400, 'volumeId must be a string'));
if (typeof m.readOnly !== 'boolean') return next(new HttpError(400, 'readOnly must be a boolean'));
if (!req.body.binds || typeof req.body.binds !== 'object') return next(new HttpError(400, 'binds should be an object'));
for (let name of Object.keys(req.body.binds)) {
if (!req.body.binds[name] || typeof req.body.binds[name] !== 'object') return next(new HttpError(400, 'each bind should be an object'));
if (typeof req.body.binds[name].hostPath !== 'string') return next(new HttpError(400, 'hostPath must be a string'));
if (typeof req.body.binds[name].readOnly !== 'boolean') return next(new HttpError(400, 'readOnly must be a boolean'));
}
apps.setMounts(req.resource, req.body.mounts, auditSource.fromRequest(req), function (error, result) {
apps.setBinds(req.resource, req.body.binds, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
+1 -11
View File
@@ -20,7 +20,6 @@ exports = module.exports = {
prepareDashboardDomain,
renewCerts,
getServerIp,
getLanguages,
syncExternalLdap
};
@@ -37,7 +36,6 @@ let assert = require('assert'),
system = require('../system.js'),
tokendb = require('../tokendb.js'),
tokens = require('../tokens.js'),
translation = require('../translation.js'),
updater = require('../updater.js'),
users = require('../users.js'),
updateChecker = require('../updatechecker.js');
@@ -227,7 +225,7 @@ function getLogs(req, res, next) {
res.writeHead(200, {
'Content-Type': 'application/x-logs',
'Content-Disposition': `attachment; filename="${req.params.unit}.log"`,
'Content-Disposition': 'attachment; filename="log.txt"',
'Cache-Control': 'no-cache',
'X-Accel-Buffering': 'no' // disable nginx buffering
});
@@ -315,11 +313,3 @@ function getServerIp(req, res, next) {
next(new HttpSuccess(200, { ip }));
});
}
function getLanguages(req, res, next) {
translation.getLanguages(function (error, languages) {
if (error) return next(new BoxError.toHttpError(error));
next(new HttpSuccess(200, { languages }));
});
}
+10 -9
View File
@@ -4,29 +4,30 @@ exports = module.exports = {
proxy
};
var addons = require('../addons.js'),
assert = require('assert'),
var assert = require('assert'),
BoxError = require('../boxerror.js'),
docker = require('../docker.js'),
middleware = require('../middleware/index.js'),
HttpError = require('connect-lastmile').HttpError,
safe = require('safetydance'),
url = require('url');
function proxy(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
const id = req.params.id; // app id or volume id
const appId = req.params.id;
req.clearTimeout();
addons.getContainerDetails('sftp', 'CLOUDRON_SFTP_TOKEN', function (error, result) {
if (error) return next(BoxError.toHttpError(error));
docker.inspect('sftp', function (error, result) {
if (error)return next(BoxError.toHttpError(error));
let parsedUrl = url.parse(req.url, true /* parseQueryString */);
parsedUrl.query['access_token'] = result.token;
const ip = safe.query(result, 'NetworkSettings.Networks.cloudron.IPAddress', null);
if (!ip) return next(new BoxError(BoxError.INACTIVE, 'Error getting IP of sftp service'));
req.url = url.format({ pathname: `/files/${id}/${encodeURIComponent(req.params[0])}`, query: parsedUrl.query }); // params[0] already contains leading '/'
req.url = req.originalUrl.replace(`/api/v1/apps/${appId}/files`, `/files/${appId}`);
const proxyOptions = url.parse(`https://${result.ip}:3000`);
const proxyOptions = url.parse(`https://${ip}:3000`);
proxyOptions.rejectUnauthorized = false;
const fileManagerProxy = middleware.proxy(proxyOptions);
-1
View File
@@ -62,7 +62,6 @@ function updateMembers(req, res, next) {
if (!req.body.userIds) return next(new HttpError(404, 'missing or invalid userIds fields'));
if (!Array.isArray(req.body.userIds)) return next(new HttpError(404, 'userIds must be an array'));
if (req.body.userIds.some((u) => typeof u !== 'string')) return next(new HttpError(400, 'userIds array must contain strings'));
groups.setMembers(req.params.groupId, req.body.userIds, function (error) {
if (error) return next(BoxError.toHttpError(error));
+1 -2
View File
@@ -24,6 +24,5 @@ exports = module.exports = {
support: require('./support.js'),
tasks: require('./tasks.js'),
tokens: require('./tokens.js'),
users: require('./users.js'),
volumes: require('./volumes.js')
users: require('./users.js')
};
+4 -6
View File
@@ -196,10 +196,9 @@ function addMailbox(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
if (typeof req.body.name !== 'string') return next(new HttpError(400, 'name must be a string'));
if (typeof req.body.ownerId !== 'string') return next(new HttpError(400, 'ownerId must be a string'));
if (typeof req.body.ownerType !== 'string') return next(new HttpError(400, 'ownerType must be a string'));
if (typeof req.body.userId !== 'string') return next(new HttpError(400, 'userId must be a string'));
mail.addMailbox(req.body.name, req.params.domain, req.body.ownerId, req.body.ownerType, auditSource.fromRequest(req), function (error) {
mail.addMailbox(req.body.name, req.params.domain, req.body.userId, auditSource.fromRequest(req), function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(201, {}));
@@ -210,10 +209,9 @@ function updateMailbox(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.params.name, 'string');
if (typeof req.body.ownerId !== 'string') return next(new HttpError(400, 'ownerId must be a string'));
if (typeof req.body.ownerType !== 'string') return next(new HttpError(400, 'ownerType must be a string'));
if (typeof req.body.userId !== 'string') return next(new HttpError(400, 'userId must be a string'));
mail.updateMailboxOwner(req.params.name, req.params.domain, req.body.ownerId, req.body.ownerType, auditSource.fromRequest(req), function (error) {
mail.updateMailboxOwner(req.params.name, req.params.domain, req.body.userId, auditSource.fromRequest(req), function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(204));
-8
View File
@@ -2,7 +2,6 @@
exports = module.exports = {
proxy,
restart,
getLocation,
setLocation
@@ -12,18 +11,12 @@ var addons = require('../addons.js'),
assert = require('assert'),
auditSource = require('../auditsource.js'),
BoxError = require('../boxerror.js'),
debug = require('debug')('box:routes/mailserver'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
mail = require('../mail.js'),
middleware = require('../middleware/index.js'),
url = require('url');
function restart(req, res, next) {
mail.restartMail((error) => debug('Error restarting mail container', error));
next();
}
function proxy(req, res, next) {
let parsedUrl = url.parse(req.url, true /* parseQueryString */);
const pathname = req.path.split('/').pop();
@@ -43,7 +36,6 @@ function proxy(req, res, next) {
proxyOptions.rejectUnauthorized = false;
const mailserverProxy = middleware.proxy(proxyOptions);
req.clearTimeout(); // TODO: add timeout to mail server proxy logic instead of this
mailserverProxy(req, res, function (error) {
if (!error) return next();
+1 -6
View File
@@ -46,11 +46,6 @@ function configure(req, res, next) {
memorySwap: req.body.memorySwap
};
if (req.params.service === 'sftp') {
if (typeof req.body.requireAdmin !== 'boolean') return next(new HttpError(400, 'requireAdmin must be a boolean'));
data.requireAdmin = req.body.requireAdmin;
}
addons.configureService(req.params.service, data, function (error) {
if (error) return next(BoxError.toHttpError(error));
@@ -75,7 +70,7 @@ function getLogs(req, res, next) {
res.writeHead(200, {
'Content-Type': 'application/x-logs',
'Content-Disposition': `attachment; filename="${req.params.service}.log"`,
'Content-Disposition': 'attachment; filename="log.txt"',
'Cache-Control': 'no-cache',
'X-Accel-Buffering': 'no' // disable nginx buffering
});
-22
View File
@@ -273,26 +273,6 @@ function setSysinfoConfig(req, res, next) {
});
}
function getLanguage(req, res, next) {
settings.getLanguage(function (error, language) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { language }));
});
}
function setLanguage(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (!req.body.language || typeof req.body.language !== 'string') return next(new HttpError(400, 'language is required'));
settings.setLanguage(req.body.language, function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
});
}
function get(req, res, next) {
assert.strictEqual(typeof req.params.setting, 'string');
@@ -304,7 +284,6 @@ function get(req, res, next) {
case settings.UNSTABLE_APPS_KEY: return getUnstableAppsConfig(req, res, next);
case settings.REGISTRY_CONFIG_KEY: return getRegistryConfig(req, res, next);
case settings.SYSINFO_CONFIG_KEY: return getSysinfoConfig(req, res, next);
case settings.LANGUAGE_KEY: return getLanguage(req, res, next);
case settings.AUTOUPDATE_PATTERN_KEY: return getAutoupdatePattern(req, res, next);
case settings.TIME_ZONE_KEY: return getTimeZone(req, res, next);
@@ -326,7 +305,6 @@ function set(req, res, next) {
case settings.UNSTABLE_APPS_KEY: return setUnstableAppsConfig(req, res, next);
case settings.REGISTRY_CONFIG_KEY: return setRegistryConfig(req, res, next);
case settings.SYSINFO_CONFIG_KEY: return setSysinfoConfig(req, res, next);
case settings.LANGUAGE_KEY: return setLanguage(req, res, next);
case settings.AUTOUPDATE_PATTERN_KEY: return setAutoupdatePattern(req, res, next);
case settings.TIME_ZONE_KEY: return setTimeZone(req, res, next);
+1 -1
View File
@@ -70,7 +70,7 @@ function getLogs(req, res, next) {
res.writeHead(200, {
'Content-Type': 'application/x-logs',
'Content-Disposition': `attachment; filename="task-${req.params.taskId}.log"`,
'Content-Disposition': 'attachment; filename="log.txt"',
'Cache-Control': 'no-cache',
'X-Accel-Buffering': 'no' // disable nginx buffering
});
+155 -13
View File
@@ -37,7 +37,7 @@ const docker = new Docker({ socketPath: '/var/run/docker.sock' });
// Test image information
var TEST_IMAGE_REPO = 'docker.io/cloudron/io.cloudron.testapp';
var TEST_IMAGE_TAG = '20201121-223249-985e86ebb';
var TEST_IMAGE_TAG = '20200207-233155-725d9e015';
var TEST_IMAGE = TEST_IMAGE_REPO + ':' + TEST_IMAGE_TAG;
const DOMAIN_0 = {
@@ -74,6 +74,39 @@ var token_1 = null;
let KEY, CERT;
let appstoreIconServer = hock.createHock({ throwOnUnmatched: false });
function checkAddons(appEntry, done) {
async.retry({ times: 15, interval: 3000 }, function (callback) {
// this was previously written with superagent but it was getting sporadic EPIPE
var req = http.get({ hostname: 'localhost', port: appEntry.httpPort, path: '/check_addons?username=' + USERNAME + '&password=' + PASSWORD });
req.on('error', callback);
req.on('response', function (res) {
if (res.statusCode !== 200) return callback('app returned non-200 status : ' + res.statusCode);
var d = '';
res.on('data', function (chunk) { d += chunk.toString('utf8'); });
res.on('end', function () {
var body = JSON.parse(d);
delete body.recvmail; // unclear why dovecot mail delivery won't work
delete body.stdenv; // cannot access APP_ORIGIN
delete body.email; // sieve will fail not sure why yet
delete body.docker; // TODO fix this for some reason we cannot connect to the docker proxy on port 3003
for (var key in body) {
if (body[key] !== 'OK') {
console.log('Not done yet: ' + JSON.stringify(body));
return callback('Not done yet: ' + JSON.stringify(body));
}
}
callback();
});
});
req.end();
}, done);
}
function checkRedis(containerId, done) {
var redisIp, exportedRedisPort;
@@ -550,7 +583,31 @@ describe('App API', function () {
});
});
xit('tcp port mapping works', function (done) {
it('http is up and running', function (done) {
var tryCount = 20;
// TODO what does that check for?
expect(appResult.httpPort).to.be(undefined);
(function healthCheck() {
superagent.get('http://localhost:' + appEntry.httpPort + appResult.manifest.healthCheckPath)
.end(function (err, res) {
if (err || res.statusCode !== 200) {
if (--tryCount === 0) {
console.log('Unable to curl http://localhost:' + appEntry.httpPort + appResult.manifest.healthCheckPath);
return done(new Error('Timedout'));
}
return setTimeout(healthCheck, 2000);
}
expect(!err).to.be.ok();
expect(res.statusCode).to.equal(200);
done();
});
})();
});
it('tcp port mapping works', function (done) {
var client = net.connect(7171);
client.on('data', function (data) {
expect(data.toString()).to.eql('ECHO_SERVER_PORT=7171');
@@ -568,15 +625,30 @@ describe('App API', function () {
});
});
xit('app responds to http request', function (done) {
console.log(`talking to http://${appEntry.containerIp}:7777`);
superagent.get(`http://${appEntry.containerIp}:7777`).end(function (error, result) {
console.dir(error);
expect(result.statusCode).to.equal(200);
it('app responds to http request', function (done) {
superagent.get('http://localhost:' + appEntry.httpPort).end(function (err, res) {
expect(!err).to.be.ok();
expect(res.statusCode).to.equal(200);
done();
});
});
it('installation - app can populate addons', function (done) {
superagent.get(`http://localhost:${appEntry.httpPort}/populate_addons`).end(function (error, res) {
expect(!error).to.be.ok();
expect(res.statusCode).to.equal(200);
for (var key in res.body) {
expect(res.body[key]).to.be('OK');
}
done();
});
});
it('installation - app can check addons', function (done) {
console.log('This test can take a while as it waits for scheduler addon to tick 3');
checkAddons(appEntry, done);
});
it('installation - redis addon created', function (done) {
checkRedis('redis-' + APP_ID, done);
});
@@ -1082,7 +1154,7 @@ describe('App API', function () {
});
});
xit('port mapping works after reconfiguration', function (done) {
it('port mapping works after reconfiguration', function (done) {
setTimeout(function () {
var client = net.connect(7172);
client.on('data', function (data) {
@@ -1092,6 +1164,16 @@ describe('App API', function () {
client.on('error', done);
}, 4000);
});
it('app can check addons', function (done) {
console.log('This test can take a while as it waits for scheduler addon to tick 4');
apps.get(APP_ID, function (error, app) {
if (error) return done(error);
checkAddons(app, done);
});
});
});
describe('configure debug mode', function () {
@@ -1361,6 +1443,16 @@ describe('App API', function () {
waitForTask(taskId, done);
});
it('app can check addons', function (done) {
console.log('This test can take a while as it waits for scheduler addon to tick 4');
apps.get(APP_ID, function (error, app) {
if (error) return done(error);
checkAddons(app, done);
});
});
it('can reset data dir', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure/data_dir')
.query({ access_token: token })
@@ -1376,6 +1468,15 @@ describe('App API', function () {
waitForTask(taskId, done);
});
it('app can check addons', function (done) {
console.log('This test can take a while as it waits for scheduler addon to tick 4');
apps.get(APP_ID, function (error, app) {
if (error) return done(error);
checkAddons(app, done);
});
});
});
describe('start/stop', function () {
@@ -1402,11 +1503,11 @@ describe('App API', function () {
waitForTask(taskId, done);
});
xit('did stop the app', function (done) {
it('did stop the app', function (done) {
apps.get(APP_ID, function (error, app) {
if (error) return done(error);
superagent.get(`http://${app.containerIp}:7777` + APP_MANIFEST.healthCheckPath).end(function (err) {
superagent.get('http://localhost:' + app.httpPort + APP_MANIFEST.healthCheckPath).end(function (err) {
if (!err || err.code !== 'ECONNREFUSED') return done(new Error('App has not died'));
// wait for app status to be updated
@@ -1428,7 +1529,7 @@ describe('App API', function () {
});
});
xit('can start app', function (done) {
it('can start app', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/start')
.query({ access_token: token })
.end(function (err, res) {
@@ -1442,7 +1543,7 @@ describe('App API', function () {
waitForTask(taskId, function () { setTimeout(done, 5000); }); // give app 5 seconds to start
});
xit('did start the app', function (done) {
it('did start the app', function (done) {
apps.get(APP_ID, function (error, app) {
if (error) return done(error);
@@ -1468,7 +1569,7 @@ describe('App API', function () {
waitForTask(taskId, function () { setTimeout(done, 12000); }); // give app 12 seconds (to die and start)
});
xit('did restart the app', function (done) {
it('did restart the app', function (done) {
apps.get(APP_ID, function (error, app) {
if (error) return done(error);
@@ -1569,6 +1670,47 @@ describe('App API', function () {
});
});
describe('not sure what this is', function () {
it('app install succeeds again', function (done) {
var fake1 = nock(settings.apiServerOrigin()).get('/api/v1/apps/' + APP_STORE_ID).reply(200, { manifest: APP_MANIFEST });
var fake2 = nock(settings.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/cloudronapps') >= 0; }, (body) => body.appstoreId === APP_STORE_ID && body.manifestId === APP_MANIFEST.id && body.appId).reply(201, { });
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, location: APP_LOCATION_2, domain: DOMAIN_0.domain, portBindings: null, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
expect(res.body.id).to.be.a('string');
APP_ID = res.body.id;
expect(fake1.isDone()).to.be.ok();
expect(fake2.isDone()).to.be.ok();
done();
});
});
it('app install fails with developer token', function (done) {
superagent.post(SERVER_URL + '/api/v1/developer/login')
.send({ username: USERNAME, password: PASSWORD })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(200);
expect(new Date(result.body.expires).toString()).to.not.be('Invalid Date');
expect(result.body.accessToken).to.be.a('string');
// overwrite non dev token
token = result.body.accessToken;
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ manifest: APP_MANIFEST, location: APP_LOCATION+APP_LOCATION, domain: DOMAIN_0.domain, portBindings: null, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(424); // appstore purchase external error
done();
});
});
});
});
describe('the end', function () {
// this is here so we can debug things if tests fail
it('can stop box', stopBox);
-12
View File
@@ -558,17 +558,5 @@ describe('Cloudron API', function () {
});
});
});
describe('languages', function () {
it('succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/cloudron/languages')
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.languages).to.be.an('array');
expect(result.body.languages.indexOf('en')).to.not.equal(-1);
done();
});
});
});
});
});
+3 -5
View File
@@ -565,7 +565,7 @@ describe('Mail API', function () {
describe('mailboxes', function () {
it('add succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/mailboxes')
.send({ name: MAILBOX_NAME, ownerId: userId, ownerType: 'user' })
.send({ name: MAILBOX_NAME, userId: userId })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
@@ -575,7 +575,7 @@ describe('Mail API', function () {
it('cannot add again', function (done) {
superagent.post(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/mailboxes')
.send({ name: MAILBOX_NAME, ownerId: userId, ownerType: 'user' })
.send({ name: MAILBOX_NAME, userId: userId })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(409);
@@ -600,7 +600,6 @@ describe('Mail API', function () {
expect(res.body.mailbox).to.be.an('object');
expect(res.body.mailbox.name).to.equal(MAILBOX_NAME);
expect(res.body.mailbox.ownerId).to.equal(userId);
expect(res.body.mailbox.ownerType).to.equal('user');
expect(res.body.mailbox.aliasName).to.equal(null);
expect(res.body.mailbox.aliasDomain).to.equal(null);
expect(res.body.mailbox.domain).to.equal(DOMAIN_0.domain);
@@ -617,7 +616,6 @@ describe('Mail API', function () {
expect(res.body.mailboxes[0]).to.be.an('object');
expect(res.body.mailboxes[0].name).to.equal(MAILBOX_NAME);
expect(res.body.mailboxes[0].ownerId).to.equal(userId);
expect(res.body.mailboxes[0].ownerType).to.equal('user');
expect(res.body.mailboxes[0].aliasName).to.equal(null);
expect(res.body.mailboxes[0].aliasDomain).to.equal(null);
expect(res.body.mailboxes[0].domain).to.equal(DOMAIN_0.domain);
@@ -661,7 +659,7 @@ describe('Mail API', function () {
it('add the mailbox', function (done) {
superagent.post(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/mailboxes')
.send({ name: MAILBOX_NAME, ownerId: userId, ownerType: 'user' })
.send({ name: MAILBOX_NAME, userId: userId })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
-52
View File
@@ -382,56 +382,4 @@ describe('Settings API', function () {
});
});
});
describe('language', function () {
it('can get default language', function (done) {
superagent.get(SERVER_URL + '/api/v1/settings/language')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.language).to.equal('en');
done();
});
});
it('cannot set language with missing language', function (done) {
superagent.post(SERVER_URL + '/api/v1/settings/language')
.query({ access_token: token })
.send({ foo: 'bar' })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('cannot set language with invalid language', function (done) {
superagent.post(SERVER_URL + '/api/v1/settings/language')
.query({ access_token: token })
.send({ language: 'doesnotexist' })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
done();
});
});
it('can set language', function (done) {
superagent.post(SERVER_URL + '/api/v1/settings/language')
.query({ access_token: token })
.send({ language: 'de' })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
done();
});
});
it('can get language', function (done) {
superagent.get(SERVER_URL + '/api/v1/settings/language')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.language).to.equal('de');
done();
});
});
});
});
-127
View File
@@ -1,127 +0,0 @@
'use strict';
/* global it:false */
/* global describe:false */
/* global before:false */
/* global after:false */
var async = require('async'),
constants = require('../../constants.js'),
database = require('../../database.js'),
expect = require('expect.js'),
server = require('../../server.js'),
superagent = require('superagent');
var SERVER_URL = 'http://localhost:' + constants.PORT;
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var token = null;
function setup(done) {
async.series([
server.start.bind(null),
database._clear.bind(null),
function createAdmin(callback) {
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(201);
// stash token for further use
token = result.body.token;
callback();
});
}
], done);
}
function cleanup(done) {
database._clear(function (error) {
expect(!error).to.be.ok();
server.stop(done);
});
}
describe('Volumes API', function () {
before(setup);
after(cleanup);
let volumeId;
it('cannot create volume with bad name', function (done) {
superagent.post(SERVER_URL + '/api/v1/volumes')
.query({ access_token: token })
.send({ name: 'music#/ ', hostPath: '/media/music' })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('cannot create volume with bad path', function (done) {
superagent.post(SERVER_URL + '/api/v1/volumes')
.query({ access_token: token })
.send({ name: 'music', hostPath: '/tmp/music' })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('can create volume', function (done) {
superagent.post(SERVER_URL + '/api/v1/volumes')
.query({ access_token: token })
.send({ name: 'music', hostPath: '/media/music' })
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
expect(res.body.id).to.be.a('string');
volumeId = res.body.id;
done();
});
});
it('can list volumes', function (done) {
superagent.get(SERVER_URL + '/api/v1/volumes')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.volumes.length).to.be(1);
expect(res.body.volumes[0].id).to.be(volumeId);
expect(res.body.volumes[0].hostPath).to.be('/media/music');
done();
});
});
it('cannot get non-existent volume', function (done) {
superagent.get(SERVER_URL + '/api/v1/volumes/foobar')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
done();
});
});
it('can get volume', function (done) {
superagent.get(SERVER_URL + `/api/v1/volumes/${volumeId}`)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.id).to.be(volumeId);
expect(res.body.hostPath).to.be('/media/music');
done();
});
});
it('can delete volume', function (done) {
superagent.del(SERVER_URL + `/api/v1/volumes/${volumeId}`)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
done();
});
});
});
-66
View File
@@ -1,66 +0,0 @@
'use strict';
exports = module.exports = {
add,
get,
del,
list,
load
};
const assert = require('assert'),
auditSource = require('../auditsource.js'),
BoxError = require('../boxerror.js'),
volumes = require('../volumes.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess;
function load(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
volumes.get(req.params.id, function (error, result) {
if (error) return next(BoxError.toHttpError(error));
req.resource = result;
next();
});
}
function add(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.name !== 'string') return next(new HttpError(400, 'name must be a string'));
if (typeof req.body.hostPath !== 'string') return next(new HttpError(400, 'hostPath must be a string'));
volumes.add(req.body.name, req.body.hostPath, auditSource.fromRequest(req), function (error, id) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(201, { id }));
});
}
function get(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
next(new HttpSuccess(200, req.resource));
}
function del(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
volumes.del(req.resource, auditSource.fromRequest(req), function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(204));
});
}
function list(req, res, next) {
volumes.list(function (error, result) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { volumes: result }));
});
}
+2 -17
View File
@@ -1,9 +1,7 @@
'use strict';
exports = module.exports = {
sync,
suspendJobs,
resumeJobs
sync: sync
};
let apps = require('./apps.js'),
@@ -17,19 +15,8 @@ let apps = require('./apps.js'),
_ = require('underscore');
// appId -> { containerId, schedulerConfig (manifest), cronjobs }
let gState = { };
let gSuspendedAppIds = new Set(); // suspended because some apptask is running
var gState = { };
// TODO: this should probably also stop existing jobs to completely prevent race but the code is not re-entrant
function suspendJobs(appId) {
debug(`suspendJobs: ${appId}`);
gSuspendedAppIds.add(appId);
}
function resumeJobs(appId) {
debug(`resumeJobs: ${appId}`);
gSuspendedAppIds.delete(appId);
}
function runTask(appId, taskName, callback) {
assert.strictEqual(typeof appId, 'string');
@@ -39,8 +26,6 @@ function runTask(appId, taskName, callback) {
const JOB_MAX_TIME = 30 * 60 * 1000; // 30 minutes
const containerName = `${appId}-${taskName}`;
if (gSuspendedAppIds.has(appId)) return callback();
apps.get(appId, function (error, app) {
if (error) return callback(error);
+5 -17
View File
@@ -1,4 +1,7 @@
#!/usr/bin/env node
#!/bin/bash
':' //# comment; exec /usr/bin/env node --expose-gc "$0" "$@"
// to understand the above hack read http://sambal.org/2014/02/passing-options-node-shebang-line/
'use strict';
@@ -9,8 +12,7 @@ var assert = require('assert'),
backups = require('../backups.js'),
database = require('../database.js'),
debug = require('debug')('box:backupupload'),
settings = require('../settings.js'),
v8 = require('v8');
settings = require('../settings.js');
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -50,27 +52,13 @@ function throttledProgressCallback(msecs) {
};
}
// https://github.com/josefzamrzla/gc-heap-stats#readme
// https://stackoverflow.com/questions/41541843/nodejs-v8-getheapstatistics-method
function dumpMemoryInfo() {
const mu = process.memoryUsage();
const hs = v8.getHeapStatistics();
debug(`process: rss: ${mu.rss} heapTotal: ${mu.heapTotal} heapUsed: ${mu.heapUsed} external: ${mu.external}`);
debug(`v8 heap : used ${hs.used_heap_size} total: ${hs.total_heap_size} max: ${hs.heap_size_limit}`);
}
initialize(function (error) {
if (error) throw error;
dumpMemoryInfo();
const timerId = setInterval(dumpMemoryInfo, 30000);
backups.upload(backupId, format, dataLayoutString, throttledProgressCallback(5000), function resultHandler(error) {
debug('upload completed. error: ', error);
process.send({ result: error ? error.message : '' });
clearInterval(timerId);
// https://nodejs.org/api/process.html are exit codes used by node. apps.js uses the value below
// to check apptask crashes
+3 -14
View File
@@ -20,7 +20,7 @@ let assert = require('assert'),
users = require('./users.js'),
ws = require('ws');
let gHttpServer = null;
var gHttpServer = null;
function initializeExpressSync() {
var app = express();
@@ -90,7 +90,6 @@ function initializeExpressSync() {
router.post('/api/v1/cloudron/restore', json, routes.provision.restore); // only available until activated
router.post('/api/v1/cloudron/activate', json, routes.provision.activate);
router.get ('/api/v1/cloudron/status', routes.provision.getStatus);
router.get ('/api/v1/cloudron/languages', routes.cloudron.getLanguages);
router.get ('/api/v1/cloudron/avatar', routes.branding.getCloudronAvatar); // this is a public alias for /api/v1/branding/cloudron_avatar
// login/logout routes
@@ -216,12 +215,11 @@ function initializeExpressSync() {
router.post('/api/v1/apps/:id/configure/env', json, token, authorizeAdmin, routes.apps.load, routes.apps.setEnvironment);
router.post('/api/v1/apps/:id/configure/data_dir', json, token, authorizeAdmin, routes.apps.load, routes.apps.setDataDir);
router.post('/api/v1/apps/:id/configure/location', json, token, authorizeAdmin, routes.apps.load, routes.apps.setLocation);
router.post('/api/v1/apps/:id/configure/mounts', json, token, authorizeAdmin, routes.apps.load, routes.apps.setMounts);
router.post('/api/v1/apps/:id/configure/binds', json, token, authorizeAdmin, routes.apps.load, routes.apps.setBinds);
router.post('/api/v1/apps/:id/repair', json, token, authorizeAdmin, routes.apps.load, routes.apps.repair);
router.post('/api/v1/apps/:id/update', json, token, authorizeAdmin, routes.apps.load, routes.apps.update);
router.post('/api/v1/apps/:id/restore', json, token, authorizeAdmin, routes.apps.load, routes.apps.restore);
router.post('/api/v1/apps/:id/import', json, token, authorizeAdmin, routes.apps.load, routes.apps.importApp);
router.post('/api/v1/apps/:id/export', json, token, authorizeAdmin, routes.apps.load, routes.apps.exportApp);
router.post('/api/v1/apps/:id/backup', json, token, authorizeAdmin, routes.apps.load, routes.apps.backup);
router.get ('/api/v1/apps/:id/backups', token, authorizeAdmin, routes.apps.load, routes.apps.listBackups);
router.post('/api/v1/apps/:id/start', json, token, authorizeAdmin, routes.apps.load, routes.apps.start);
@@ -264,8 +262,6 @@ function initializeExpressSync() {
router.post('/api/v1/mailserver/spam_acl', token, authorizeAdmin, routes.mailserver.proxy);
router.get ('/api/v1/mailserver/spam_custom_config', token, authorizeAdmin, routes.mailserver.proxy);
router.post('/api/v1/mailserver/spam_custom_config', token, authorizeAdmin, routes.mailserver.proxy);
router.get ('/api/v1/mailserver/solr_config', token, authorizeAdmin, routes.mailserver.proxy);
router.post('/api/v1/mailserver/solr_config', token, authorizeAdmin, routes.mailserver.proxy, routes.mailserver.restart);
router.get ('/api/v1/mail/:domain', token, authorizeAdmin, routes.mail.getDomain);
router.get ('/api/v1/mail/:domain/status', token, authorizeAdmin, routes.mail.getStatus);
@@ -303,13 +299,6 @@ function initializeExpressSync() {
router.del ('/api/v1/domains/:domain', token, authorizeAdmin, routes.domains.del);
router.get ('/api/v1/domains/:domain/dns_check', token, authorizeAdmin, routes.domains.checkDnsRecords);
// volume routes
router.post('/api/v1/volumes', json, token, authorizeAdmin, routes.volumes.add);
router.get ('/api/v1/volumes', token, authorizeAdmin, routes.volumes.list);
router.get ('/api/v1/volumes/:id', token, authorizeAdmin, routes.volumes.load, routes.volumes.get);
router.del ('/api/v1/volumes/:id', token, authorizeAdmin, routes.volumes.load, routes.volumes.del);
router.use ('/api/v1/volumes/:id/files/*', token, authorizeAdmin, routes.filemanager.proxy);
// addon routes
router.get ('/api/v1/services', token, authorizeAdmin, routes.services.getAll);
router.get ('/api/v1/services/:service', token, authorizeAdmin, routes.services.get);
@@ -376,7 +365,7 @@ function stop(callback) {
async.series([
cloudron.uninitialize,
database.uninitialize,
gHttpServer.close.bind(gHttpServer)
gHttpServer.close.bind(gHttpServer),
], function (error) {
if (error) return callback(error);
-36
View File
@@ -35,9 +35,6 @@ exports = module.exports = {
getLicenseKey,
setLicenseKey,
getLanguage,
setLanguage,
getCloudronId,
setCloudronId,
@@ -96,7 +93,6 @@ exports = module.exports = {
TIME_ZONE_KEY: 'time_zone',
CLOUDRON_NAME_KEY: 'cloudron_name',
LICENSE_KEY: 'license_key',
LANGUAGE_KEY: 'language',
CLOUDRON_ID_KEY: 'cloudron_id',
CLOUDRON_TOKEN_KEY: 'cloudron_token',
@@ -134,7 +130,6 @@ var addons = require('./addons.js'),
safe = require('safetydance'),
settingsdb = require('./settingsdb.js'),
sysinfo = require('./sysinfo.js'),
translation = require('./translation.js'),
util = require('util'),
_ = require('underscore');
@@ -146,7 +141,6 @@ let gDefaults = (function () {
result[exports.DYNAMIC_DNS_KEY] = false;
result[exports.UNSTABLE_APPS_KEY] = true;
result[exports.LICENSE_KEY] = '';
result[exports.LANGUAGE_KEY] = 'en';
result[exports.CLOUDRON_ID_KEY] = '';
result[exports.CLOUDRON_TOKEN_KEY] = '';
result[exports.BACKUP_CONFIG_KEY] = {
@@ -655,36 +649,6 @@ function setLicenseKey(licenseKey, callback) {
});
}
function getLanguage(callback) {
assert.strictEqual(typeof callback, 'function');
settingsdb.get(exports.LANGUAGE_KEY, function (error, value) {
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, gDefaults[exports.LANGUAGE_KEY]);
if (error) return callback(error);
callback(null, value);
});
}
function setLanguage(language, callback) {
assert.strictEqual(typeof language, 'string');
assert.strictEqual(typeof callback, 'function');
translation.getLanguages(function (error, languages) {
if (error) return callback(error);
if (languages.indexOf(language) === -1) return callback(new BoxError(BoxError.NOT_FOUND));
settingsdb.set(exports.LANGUAGE_KEY, language, function (error) {
if (error) return callback(error);
notifyChange(exports.LANGUAGE_KEY, language);
callback(null);
});
});
}
function getCloudronId(callback) {
assert.strictEqual(typeof callback, 'function');
+38 -56
View File
@@ -1,20 +1,17 @@
'use strict';
exports = module.exports = {
startSftp,
rebuild
startSftp: startSftp,
rebuild: rebuild
};
var apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
debug = require('debug')('box:sftp'),
hat = require('./hat.js'),
infra = require('./infra_version.js'),
paths = require('./paths.js'),
safe = require('safetydance'),
shell = require('./shell.js'),
volumes = require('./volumes.js'),
_ = require('underscore');
function startSftp(existingInfra, callback) {
@@ -46,7 +43,6 @@ function rebuild(callback) {
const tag = infra.images.sftp.tag;
const memoryLimit = 256;
const cloudronToken = hat(8 * 128);
apps.getAll(function (error, result) {
if (error) return done(error);
@@ -66,62 +62,48 @@ function rebuild(callback) {
});
volumes.list(function (error, allVolumes) {
if (error) return callback(error);
shell.exec('inspectSftp', 'docker inspect --format="{{json .Mounts }}" sftp', function (error, result) {
if (!error && result) {
let currentDataDirs = safe.JSON.parse(result);
if (currentDataDirs) {
currentDataDirs = currentDataDirs.filter(function (d) { return d.Destination.indexOf('/app/data/') === 0; }).map(function (d) { return { hostDir: d.Source, mountDir: d.Destination }; });
allVolumes.forEach(function (volume) {
if (!safe.fs.existsSync(volume.hostPath)) {
debug(`Ignoring volume host path ${volume.hostPath} since it does not exist`);
return;
}
// sort for comparison
currentDataDirs.sort(function (a, b) { return a.hostDir < b.hostDir ? -1 : 1; });
dataDirs.sort(function (a, b) { return a.hostDir < b.hostDir ? -1 : 1; });
dataDirs.push({ hostDir: volume.hostPath, mountDir: `/app/data/${volume.id}` });
});
shell.exec('inspectSftp', 'docker inspect --format="{{json .Mounts }}" sftp', function (error, result) {
if (!error && result) {
let currentDataDirs = safe.JSON.parse(result);
if (currentDataDirs) {
currentDataDirs = currentDataDirs.filter(function (d) { return d.Destination.indexOf('/app/data/') === 0; }).map(function (d) { return { hostDir: d.Source, mountDir: d.Destination }; });
// sort for comparison
currentDataDirs.sort(function (a, b) { return a.hostDir < b.hostDir ? -1 : 1; });
dataDirs.sort(function (a, b) { return a.hostDir < b.hostDir ? -1 : 1; });
if (_.isEqual(currentDataDirs, dataDirs)) {
debug('Skipping rebuild, no changes');
return done();
}
if (_.isEqual(currentDataDirs, dataDirs)) {
debug('Skipping rebuild, no changes');
return done();
}
}
}
const mounts = dataDirs.map(function (v) { return `-v "${v.hostDir}:${v.mountDir}"`; }).join(' ');
const cmd = `docker run --restart=always -d --name="sftp" \
--hostname sftp \
--net cloudron \
--net-alias sftp \
--log-driver syslog \
--log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \
--log-opt tag=sftp \
-m ${memoryLimit}m \
--memory-swap ${memoryLimit * 2}m \
--dns 172.18.0.1 \
--dns-search=. \
-p 222:22 \
${mounts} \
-e CLOUDRON_SFTP_TOKEN="${cloudronToken}" \
-v "${paths.SFTP_KEYS_DIR}:/etc/ssh:ro" \
--label isCloudronManaged=true \
--read-only -v /tmp -v /run "${tag}"`;
const appDataVolumes = dataDirs.map(function (v) { return `-v "${v.hostDir}:${v.mountDir}"`; }).join(' ');
const cmd = `docker run --restart=always -d --name="sftp" \
--hostname sftp \
--net cloudron \
--net-alias sftp \
--log-driver syslog \
--log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \
--log-opt tag=sftp \
-m ${memoryLimit}m \
--memory-swap ${memoryLimit * 2}m \
--dns 172.18.0.1 \
--dns-search=. \
-p 222:22 \
${appDataVolumes} \
-v "/etc/ssh:/etc/ssh:ro" \
--label isCloudronManaged=true \
--read-only -v /tmp -v /run "${tag}"`;
// ignore error if container not found (and fail later) so that this code works across restarts
async.series([
shell.exec.bind(null, 'stopSftp', 'docker stop sftp || true'),
shell.exec.bind(null, 'removeSftp', 'docker rm -f sftp || true'),
shell.exec.bind(null, 'startSftp', cmd)
], done);
});
// ignore error if container not found (and fail later) so that this code works across restarts
async.series([
shell.exec.bind(null, 'stopSftp', 'docker stop sftp || true'),
shell.exec.bind(null, 'removeSftp', 'docker rm -f sftp || true'),
shell.exec.bind(null, 'startSftp', cmd)
], done);
});
});
}
+1 -26
View File
@@ -32,7 +32,6 @@ var assert = require('assert'),
EventEmitter = require('events'),
fs = require('fs'),
path = require('path'),
paths = require('../paths.js'),
prettyBytes = require('pretty-bytes'),
readdirp = require('readdirp'),
safe = require('safetydance'),
@@ -74,7 +73,7 @@ function checkPreconditions(apiConfig, dataLayout, callback) {
if (result.mountpoint === '/') return callback(new BoxError(BoxError.FS_ERROR, `${apiConfig.backupFolder} is not mounted`));
}
const needed = 0.6 * used + (1024 * 1024 * 1024); // check if there is atleast 1GB left afterwards. aim for 60% because rsync/tgz won't need full 100%
const needed = used + (1024 * 1024 * 1024); // check if there is atleast 1GB left afterwards
if (result.available <= needed) return callback(new BoxError(BoxError.FS_ERROR, `Not enough disk space for backup. Needed: ${prettyBytes(needed)} Available: ${prettyBytes(result.available)}`));
callback(null);
@@ -226,42 +225,18 @@ function removeDir(apiConfig, pathPrefix) {
return events;
}
function validateBackupTarget(folder) {
assert.strictEqual(typeof folder, 'string');
if (path.normalize(folder) !== folder) return new BoxError(BoxError.BAD_FIELD, 'backupFolder must contain a normalized path', { field: 'backupFolder' });
if (!path.isAbsolute(folder)) return new BoxError(BoxError.BAD_FIELD, 'backupFolder must be an absolute path', { field: 'backupFolder' });
if (folder === '/') return new BoxError(BoxError.BAD_FIELD, 'backupFolder cannot be /', { field: 'backupFolder' });
if (!folder.endsWith('/')) folder = folder + '/'; // ensure trailing slash for the prefix matching to work
const PROTECTED_PREFIXES = [ '/boot/', '/usr/', '/bin/', '/lib/', '/root/', '/var/lib/', paths.baseDir() ];
if (PROTECTED_PREFIXES.some(p => folder.startsWith(p))) return new BoxError(BoxError.BAD_FIELD, 'backupFolder path is protected', { field: 'backupFolder' });
return null;
}
function testConfig(apiConfig, callback) {
assert.strictEqual(typeof apiConfig, 'object');
assert.strictEqual(typeof callback, 'function');
if (apiConfig.provider === PROVIDER_FILESYSTEM) {
if (!apiConfig.backupFolder || typeof apiConfig.backupFolder !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'backupFolder must be non-empty string', { field: 'backupFolder' }));
let error = validateBackupTarget(apiConfig.backupFolder);
if (error) return callback(error);
if ('externalDisk' in apiConfig && typeof apiConfig.externalDisk !== 'boolean') return callback(new BoxError(BoxError.BAD_FIELD, 'externalDisk must be boolean', { field: 'externalDisk' }));
}
if (apiConfig.provider === PROVIDER_SSHFS || apiConfig.provider === PROVIDER_CIFS || apiConfig.provider === PROVIDER_NFS) {
if (!apiConfig.mountPoint || typeof apiConfig.mountPoint !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'mountPoint must be non-empty string', { field: 'mountPoint' }));
let error = validateBackupTarget(apiConfig.mountPoint);
if (error) return callback(error);
if (typeof apiConfig.prefix !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'prefix must be a string', { field: 'prefix' }));
if (path.isAbsolute(apiConfig.prefix)) return new BoxError(BoxError.BAD_FIELD, 'prefix must be a relative path', { field: 'backupFolder' });
if (path.normalize(apiConfig.prefix) !== apiConfig.prefix) return callback(new BoxError(BoxError.BAD_FIELD, 'prefix must contain a normalized relative path', { field: 'prefix' }));
const mounts = safe.fs.readFileSync('/proc/mounts', 'utf8');
const mountInfo = mounts.split('\n').filter(function (l) { return l.indexOf(apiConfig.mountPoint) !== -1; })[0];
+5 -5
View File
@@ -62,13 +62,13 @@ function getS3Config(apiConfig, callback) {
accessKeyId: apiConfig.accessKeyId,
secretAccessKey: apiConfig.secretAccessKey,
region: apiConfig.region || 'us-east-1',
maxRetries: 10,
maxRetries: 5,
retryDelayOptions: {
customBackoff: () => 20000 // constant backoff - https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Config.html#retryDelayOptions-property
},
httpOptions: {
connectTimeout: 20000, // https://github.com/aws/aws-sdk-js/pull/1446
timeout: 0 // https://github.com/aws/aws-sdk-js/issues/1704 (allow unlimited time for chunk upload)
connectTimeout: 10000, // https://github.com/aws/aws-sdk-js/pull/1446
timeout: 600 * 1000 // https://github.com/aws/aws-sdk-js/issues/1704 (allow chunk upload to take upto 5 minutes)
}
};
@@ -255,8 +255,8 @@ function copy(apiConfig, oldFilePath, newFilePath) {
};
// S3 copyObject has a file size limit of 5GB so if we have larger files, we do a multipart copy
// Exoscale and B2 take too long to copy 5GB
const largeFileLimit = (apiConfig.provider === 'exoscale-sos' || apiConfig.provider === 'backblaze-b2') ? 1024 * 1024 * 1024 : 5 * 1024 * 1024 * 1024;
// Exoscale takes too long to copy 5GB
const largeFileLimit = apiConfig.provider === 'exoscale-sos' ? 1024 * 1024 * 1024 : 5 * 1024 * 1024 * 1024;
if (entry.size < largeFileLimit) {
events.emit('progress', `Copying ${relativePath || oldFilePath}`);
-3
View File
@@ -67,9 +67,6 @@ async.series([
process.on('SIGTERM', () => process.exit(0)); // sent as timeout notification
// ensure we log task crashes with the task logs
process.on('uncaughtException', function (e) { debug(e); process.exit(1); });
debug(`Starting task ${taskId}. Logs are at ${logFile}`);
tasks.get(taskId, function (error, task) {
+18 -6
View File
@@ -14,6 +14,7 @@ var appdb = require('../appdb.js'),
expect = require('expect.js'),
fs = require('fs'),
js2xml = require('js2xmlparser').parse,
net = require('net'),
nock = require('nock'),
paths = require('../paths.js'),
settings = require('../settings.js'),
@@ -86,12 +87,13 @@ var APP = {
domain: DOMAIN_0.domain,
fqdn: DOMAIN_0.domain + '.' + 'applocation',
manifest: MANIFEST,
containerId: 'someid',
containerId: null,
httpPort: 4567,
portBindings: null,
accessRestriction: null,
memoryLimit: 0,
mailboxDomain: DOMAIN_0.domain,
alternateDomains: [],
alternateDomains: []
};
var awsHostedZones;
@@ -130,18 +132,28 @@ describe('apptask', function () {
], done);
});
it('reserve port', function (done) {
apptask._reserveHttpPort(APP, function (error) {
expect(error).to.not.be.ok();
expect(APP.httpPort).to.be.a('number');
var client = net.connect(APP.httpPort);
client.on('connect', function () { done(new Error('Port is not free:' + APP.httpPort)); });
client.on('error', function () { done(); });
});
});
it('configure nginx correctly', function (done) {
apptask._configureReverseProxy(APP, function (error) {
apptask._configureReverseProxy(APP, function () {
expect(fs.existsSync(paths.NGINX_APPCONFIG_DIR + '/' + APP.id + '.conf'));
expect(error).to.be(null);
// expect(error).to.be(null); // this fails because nginx cannot be restarted
done();
});
});
it('unconfigure nginx', function (done) {
apptask._unconfigureReverseProxy(APP, function (error) {
apptask._unconfigureReverseProxy(APP, function () {
expect(!fs.existsSync(paths.NGINX_APPCONFIG_DIR + '/' + APP.id + '.conf'));
expect(error).to.be(null);
// expect(error).to.be(null); // this fails because nginx cannot be restarted
done();
});
});
-1
View File
@@ -112,7 +112,6 @@ describe('retention policy', function () {
expect(b[3].keepReason).to.be(undefined);
});
// if you are debugging this test, it's because of some timezone issue with all the hour substraction!
it('2 daily, 1 weekly', function () {
let b = [
{ id: '0', state: backups.BACKUP_STATE_NORMAL, creationTime: moment().toDate() },
+20 -12
View File
@@ -397,8 +397,8 @@ describe('database', function () {
location: 'some-location-0',
domain: DOMAIN_0.domain,
manifest: { version: '0.1', dockerImage: 'docker/app0', healthCheckPath: '/', httpPort: 80, title: 'app0' },
httpPort: null,
containerId: null,
containerIp: null,
portBindings: { port: { hostPort: 5678, type: 'tcp' } },
health: null,
accessRestriction: null,
@@ -417,8 +417,7 @@ describe('database', function () {
tags: [],
label: null,
taskId: null,
mounts: [],
proxyAuth: false,
binds: {},
servicesConfig: {}
};
@@ -869,8 +868,8 @@ describe('database', function () {
location: 'some-location-0',
domain: DOMAIN_0.domain,
manifest: { version: '0.1', dockerImage: 'docker/app0', healthCheckPath: '/', httpPort: 80, title: 'app0' },
httpPort: null,
containerId: null,
containerIp: null,
portBindings: { port: { hostPort: 5678, type: 'tcp' } },
health: null,
accessRestriction: null,
@@ -891,8 +890,7 @@ describe('database', function () {
tags: [],
label: null,
taskId: null,
mounts: [],
proxyAuth: false,
binds: {},
servicesConfig: {}
};
@@ -905,8 +903,8 @@ describe('database', function () {
location: 'some-location-1',
domain: DOMAIN_0.domain,
manifest: { version: '0.2', dockerImage: 'docker/app1', healthCheckPath: '/', httpPort: 80, title: 'app1' },
httpPort: null,
containerId: null,
containerIp: null,
portBindings: { },
health: null,
accessRestriction: { users: [ 'foobar' ] },
@@ -925,8 +923,7 @@ describe('database', function () {
tags: [],
label: null,
taskId: null,
mounts: [],
proxyAuth: false,
binds: {},
servicesConfig: {}
};
@@ -1009,6 +1006,7 @@ describe('database', function () {
APP_0.location = 'some-other-location';
APP_0.manifest.version = '0.2';
APP_0.accessRestriction = '';
APP_0.httpPort = 1337;
APP_0.memoryLimit = 1337;
APP_0.cpuShares = 1024;
@@ -1018,6 +1016,7 @@ describe('database', function () {
domain: APP_0.domain,
manifest: APP_0.manifest,
accessRestriction: APP_0.accessRestriction,
httpPort: APP_0.httpPort,
memoryLimit: APP_0.memoryLimit,
cpuShares: APP_0.cpuShares
};
@@ -1034,6 +1033,15 @@ describe('database', function () {
});
});
it('getByHttpPort succeeds', function (done) {
appdb.getByHttpPort(APP_0.httpPort, function (error, result) {
expect(error).to.be(null);
expect(result).to.be.an('object');
expect(_.omit(result, ['creationTime', 'updateTime', 'ts', 'healthTime','resetTokenCreationTime'])).to.be.eql(APP_0);
done();
});
});
it('update of nonexisting app fails', function (done) {
appdb.update(APP_1.id, { installationState: APP_1.installationState, location: APP_1.location }, function (error) {
expect(error).to.be.a(BoxError);
@@ -1809,21 +1817,21 @@ describe('database', function () {
});
it('add user mailbox succeeds', function (done) {
mailboxdb.addMailbox('girish', DOMAIN_0.domain, 'uid-0', 'user', function (error) {
mailboxdb.addMailbox('girish', DOMAIN_0.domain, 'uid-0', function (error) {
expect(error).to.be(null);
done();
});
});
it('cannot add dup entry', function (done) {
mailboxdb.addMailbox('girish', DOMAIN_0.domain, 'uid-1', 'group', function (error) {
mailboxdb.addMailbox('girish', DOMAIN_0.domain, 'uid-1', function (error) {
expect(error.reason).to.be(BoxError.ALREADY_EXISTS);
done();
});
});
it('add app mailbox succeeds', function (done) {
mailboxdb.addMailbox('support', DOMAIN_0.domain, 'osticket', 'user', function (error) {
mailboxdb.addMailbox('support', DOMAIN_0.domain, 'osticket', function (error) {
expect(error).to.be(null);
done();
});
+5 -5
View File
@@ -19,7 +19,6 @@ var appdb = require('../appdb.js'),
maildb = require('../maildb.js'),
mailboxdb = require('../mailboxdb.js'),
ldap = require('ldapjs'),
mail = require('../mail.js'),
settings = require('../settings.js'),
users = require('../users.js');
@@ -74,6 +73,7 @@ var APP_0 = {
location: 'some-location-0',
domain: DOMAIN_0.domain,
manifest: { version: '0.1', dockerImage: 'docker/app0', healthCheckPath: '/', httpPort: 80, title: 'app0' },
httpPort: null,
containerId: 'someContainerId',
portBindings: { port: 5678 },
health: null,
@@ -107,12 +107,12 @@ function setup(done) {
callback();
});
},
(done) => mailboxdb.addMailbox(USER_0.username.toLowerCase(), DOMAIN_0.domain, USER_0.id, mail.OWNERTYPE_USER, done),
(done) => mailboxdb.addMailbox(USER_0.username.toLowerCase(), DOMAIN_0.domain, USER_0.id, done),
(done) => mailboxdb.setAliasesForName(USER_0.username.toLowerCase(), DOMAIN_0.domain, [ { name: USER_0_ALIAS.toLocaleLowerCase(), domain: DOMAIN_0.domain} ], done),
appdb.update.bind(null, APP_0.id, { containerId: APP_0.containerId }),
appdb.setAddonConfig.bind(null, APP_0.id, 'sendmail', [{ name: 'MAIL_SMTP_USERNAME', value : `${APP_0.location}.app@${DOMAIN_0.domain}` }, { name: 'MAIL_SMTP_PASSWORD', value : 'sendmailpassword' }]),
appdb.setAddonConfig.bind(null, APP_0.id, 'recvmail', [{ name: 'MAIL_IMAP_USERNAME', value : `${APP_0.location}.app@${DOMAIN_0.domain}` }, { name: 'MAIL_IMAP_PASSWORD', value : 'recvmailpassword' }]),
mailboxdb.addMailbox.bind(null, APP_0.location + '.app', APP_0.domain, APP_0.id, mail.OWNERTYPE_USER),
appdb.setAddonConfig.bind(null, APP_0.id, 'sendmail', [{ name: 'MAIL_SMTP_PASSWORD', value : 'sendmailpassword' }]),
appdb.setAddonConfig.bind(null, APP_0.id, 'recvmail', [{ name: 'MAIL_IMAP_PASSWORD', value : 'recvmailpassword' }]),
mailboxdb.addMailbox.bind(null, APP_0.location + '.app', APP_0.domain, APP_0.id),
function (callback) {
users.create(USER_1.username, USER_1.password, USER_1.email, USER_0.displayName, { }, AUDIT_SOURCE, function (error, result) {
+14 -17
View File
@@ -9,8 +9,7 @@ var async = require('async'),
database = require('../database.js'),
domains = require('../domains.js'),
expect = require('expect.js'),
reverseProxy = require('../reverseproxy.js'),
settings = require('../settings.js');
reverseProxy = require('../reverseproxy.js');
const DOMAIN_0 = {
domain: 'example-reverseproxy-test.com',
@@ -27,9 +26,7 @@ function setup(done) {
async.series([
database.initialize,
database._clear,
settings.setAdminLocation.bind(null, DOMAIN_0.domain, 'my.' + DOMAIN_0.domain),
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0, AUDIT_SOURCE),
settings.initCache
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0, AUDIT_SOURCE)
], done);
}
@@ -55,31 +52,31 @@ describe('Certificates', function () {
Generate these with:
openssl genrsa -out server.key 512
openssl req -new -key server.key -out server.csr -subj "/C=DE/ST=Berlin/L=Berlin/O=Nebulon/OU=CTO/CN=baz.foobar.com"
openssl x509 -req -days 3650 -in server.csr -signkey server.key -out server.crt
openssl x509 -req -days 1460 -in server.csr -signkey server.key -out server.crt
*/
// foobar.com
var validCert0 = '-----BEGIN CERTIFICATE-----\nMIIBxTCCAW8CFBVWRFizZeUIdp94/l9Qx/+7UM4GMA0GCSqGSIb3DQEBCwUAMGQx\nCzAJBgNVBAYTAkRFMQ8wDQYDVQQIDAZCZXJsaW4xDzANBgNVBAcMBkJlcmxpbjEQ\nMA4GA1UECgwHTmVidWxvbjEMMAoGA1UECwwDQ1RPMRMwEQYDVQQDDApmb29iYXIu\nY29tMB4XDTIwMTEyMjAxNTI0M1oXDTMwMTEyMDAxNTI0M1owZDELMAkGA1UEBhMC\nREUxDzANBgNVBAgMBkJlcmxpbjEPMA0GA1UEBwwGQmVybGluMRAwDgYDVQQKDAdO\nZWJ1bG9uMQwwCgYDVQQLDANDVE8xEzARBgNVBAMMCmZvb2Jhci5jb20wXDANBgkq\nhkiG9w0BAQEFAANLADBIAkEA++BvW/oDsaM57d4Q4GQjkUzjB0/glKLj4P0Y8InS\nhLHOud9Uxz7dIcqHm9x9MOtqTRhtiHNoFLZLsU3a3upr2QIDAQABMA0GCSqGSIb3\nDQEBCwUAA0EAy9Acsgr/lH1rrE8DZov7dvvNjExkC+VO0kujO25aQIGBAtzLp9MG\nEblQ3ZXMBSX4b/nLMjOH8Xr4ZA0GUDgdew==\n-----END CERTIFICATE-----';
var validKey0 = '-----BEGIN RSA PRIVATE KEY-----\nMIIBPAIBAAJBAPvgb1v6A7GjOe3eEOBkI5FM4wdP4JSi4+D9GPCJ0oSxzrnfVMc+\n3SHKh5vcfTDrak0YbYhzaBS2S7FN2t7qa9kCAwEAAQJBALsBjWyKmcd/2vjCkWEo\nuEefAEhjg+iXb/2RrLyad1TQfgs35UfigcjpWbzT2ScpFZT61ng6hKmclt2OCT9F\nBKECIQD/bjRbGiPq762ikWkfvalgkAAhSoXo2AcD/MsrhWyyPQIhAPxwM7jZRNvO\nng3TJaAgISwwUC9vuaNJQ06Yt02pvoXNAiEAuQipTrGCAWe8vb5ei8rFzxihr3wf\nw0vy0RWoTA+sbPUCIHDFOwXf4bgEJG1unwdacxdHefrHAXold3D8Hh8OrnMdAiEA\nov6sW0C1+maNpoWC+moDGFdImZnej2SDIB5976akWVo=\n-----END RSA PRIVATE KEY-----';
var validCert0 = '-----BEGIN CERTIFICATE-----\nMIIBujCCAWQCCQDuY8krIDA+KzANBgkqhkiG9w0BAQsFADBkMQswCQYDVQQGEwJE\nRTEPMA0GA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4xEDAOBgNVBAoMB05l\nYnVsb24xDDAKBgNVBAsMA0NUTzETMBEGA1UEAwwKZm9vYmFyLmNvbTAeFw0xNjEx\nMDgwODI2MTRaFw0yMDExMDcwODI2MTRaMGQxCzAJBgNVBAYTAkRFMQ8wDQYDVQQI\nDAZCZXJsaW4xDzANBgNVBAcMBkJlcmxpbjEQMA4GA1UECgwHTmVidWxvbjEMMAoG\nA1UECwwDQ1RPMRMwEQYDVQQDDApmb29iYXIuY29tMFwwDQYJKoZIhvcNAQEBBQAD\nSwAwSAJBALmlwGXb1B9OzZIE9E6eKG1pZJ3P6Sy2tNAWiQ0658uyZhD1udGMNGM1\nRs9IRX+J5p+rAlPglNiG/ArOZtIES8MCAwEAATANBgkqhkiG9w0BAQsFAANBAER1\nxTRc7NQxYYhwld2/gIW5nBJMel7LxYzNlDCbRo1T8a7K6Y4kugORKFidyTjIbsAP\n84gnjmQl9NvBmv33yFk=\n-----END CERTIFICATE-----';
var validKey0 = '-----BEGIN RSA PRIVATE KEY-----\nMIIBOgIBAAJBALmlwGXb1B9OzZIE9E6eKG1pZJ3P6Sy2tNAWiQ0658uyZhD1udGM\nNGM1Rs9IRX+J5p+rAlPglNiG/ArOZtIES8MCAwEAAQJAZhXVVK2rWYP12uPKjCjA\nRln8MCOSLzpQ91RNDO9lY0bIpU+9YfKyyeEPWvFKsvBPTFaS0nyGIiZYIoYoZpCJ\nsQIhAODmkO+UsKTmGKMHqvvmN1Am9zisbiwLqw1F/5g/q6PfAiEA01GhntKZ6vqp\nhihca3tEZKDA3URI/axHTxLKCnp4tJ0CIDFu3Gqcrxr/rGihNdb6aiwG9I4TcH/j\n7KwVN7H6RLrXAiEAyDhtKP2kJncPznRJdPEbkTia5EtB2VC1U9+anSkDWyUCICLn\ngje2pXjZfRtcp49uM/WrQhBifrpuqFSIrLNU3Eb5\n-----END RSA PRIVATE KEY-----';
// *.foobar.com
var validCert1 = '-----BEGIN CERTIFICATE-----\nMIIByTCCAXMCFEXpWxabfp9Nybi7akGuxKlXdQVsMA0GCSqGSIb3DQEBCwUAMGYx\nCzAJBgNVBAYTAkRFMQ8wDQYDVQQIDAZCZXJsaW4xDzANBgNVBAcMBkJlcmxpbjEQ\nMA4GA1UECgwHTmVidWxvbjEMMAoGA1UECwwDQ1RPMRUwEwYDVQQDDAwqLmZvb2Jh\nci5jb20wHhcNMjAxMTIyMDIxNjQzWhcNMzAxMTIwMDIxNjQzWjBmMQswCQYDVQQG\nEwJERTEPMA0GA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4xEDAOBgNVBAoM\nB05lYnVsb24xDDAKBgNVBAsMA0NUTzEVMBMGA1UEAwwMKi5mb29iYXIuY29tMFww\nDQYJKoZIhvcNAQEBBQADSwAwSAJBAKMUYf86EG+J6ughAvhKGbIIyOpB3XqnK6KV\nM+r2/DvFx2KGIew7KopkzM2+UThDWE2YTcgL5846QRbx+K5NAXECAwEAATANBgkq\nhkiG9w0BAQsFAANBAJrX5wdszGt0lhDx0w2saJtTM3A6AfYdI7F37rgnvQKwRA0u\nTlN9Ekp4HbZsRi36g3W9zl6nWa3/HWbnBiRNuXk=\n-----END CERTIFICATE-----\n';
var validKey1 = '-----BEGIN RSA PRIVATE KEY-----\nMIIBOQIBAAJBAKMUYf86EG+J6ughAvhKGbIIyOpB3XqnK6KVM+r2/DvFx2KGIew7\nKopkzM2+UThDWE2YTcgL5846QRbx+K5NAXECAwEAAQJAL/m/GqaqTyXzxXZwuTqT\ndJzA/qmBzqN/YsUiEO24Jp0AVuERlgiKBbxpu0xp8EpDsLTEt6TWWy1p0HIH6e0j\nAQIhANIZkHD6gVxvAMz0tquSprBnylqHngdT/PymDEHHNPv1AiEAxrUTvxV+vmii\n5CCLFTnYTQliKr+PC5qxn2WxV1rPng0CIGTiS55EW0t0LbE8rF40XAAGxn6z8ijY\npnj2jpojOojlAiBoaA6XEXFGFO651QufPISVfb+x3HMJ0t9PdHxo/NMoJQIgbVUh\naQKzUcrgIM2nbg4fLp3+VAh0ZkxNwaeKcsZz0cQ=\n-----END RSA PRIVATE KEY-----\n';
var validCert1 = '-----BEGIN CERTIFICATE-----\nMIIBvjCCAWgCCQDLKYLGisj0djANBgkqhkiG9w0BAQsFADBmMQswCQYDVQQGEwJE\nRTEPMA0GA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4xEDAOBgNVBAoMB05l\nYnVsb24xDDAKBgNVBAsMA0NUTzEVMBMGA1UEAwwMKi5mb29iYXIuY29tMB4XDTE2\nMTEwODA4MjcxNloXDTIwMTEwNzA4MjcxNlowZjELMAkGA1UEBhMCREUxDzANBgNV\nBAgMBkJlcmxpbjEPMA0GA1UEBwwGQmVybGluMRAwDgYDVQQKDAdOZWJ1bG9uMQww\nCgYDVQQLDANDVE8xFTATBgNVBAMMDCouZm9vYmFyLmNvbTBcMA0GCSqGSIb3DQEB\nAQUAA0sAMEgCQQDXApN6RG4Q6VqJbPsfZNin29V57giGmA81icZFiU0ARv8V0SRF\nShRqPo7iem+0mfH3PgGmauOP+xEu6rFJbZQPAgMBAAEwDQYJKoZIhvcNAQELBQAD\nQQAZxeVrCNoXIs4jtCxgyTDoyFM5IGkq1dlM2CvZW+z3JV8ReCJOw1OEVgd0jIQs\nqZtqd7CQqyWiPMk/QhNInlEd\n-----END CERTIFICATE-----';
var validKey1 = '-----BEGIN RSA PRIVATE KEY-----\nMIIBPAIBAAJBANcCk3pEbhDpWols+x9k2Kfb1XnuCIaYDzWJxkWJTQBG/xXRJEVK\nFGo+juJ6b7SZ8fc+AaZq44/7ES7qsUltlA8CAwEAAQJBAMyD1MgeQxuu+8FwekXY\nZQT15E9AjbeI+B6S2JfYC/hP0AcGldmQ03KD8N497OOwuagEOZcGdS1eU45E224l\n6DECIQD+yLV6K7BUISdnIXvjkmjkwm1pQNWh4T5o3dArW4Hi+wIhANgJRaF5tbBF\ntYbFzdaDwkPlQurtUM5il/Trci9Q7Sb9AiEA+s2Wn2HcXKSaRhIXA2j/apjd3Ste\nYND6f35CSjv0+vsCIBrIg35ydWkGK2wrB8rpiOMcAEDZ7SO5K3es3PoqwUwNAiEA\n1CAqYa+GI9vDIwDJuInK3k/u4VlsiQiPdjoBySI+bDY=\n-----END RSA PRIVATE KEY-----';
// baz.foobar.com
var validCert2 = '-----BEGIN CERTIFICATE-----\nMIIBzTCCAXcCFG3UtlC/mgM6sp2591h+oywv83xhMA0GCSqGSIb3DQEBCwUAMGgx\nCzAJBgNVBAYTAkRFMQ8wDQYDVQQIDAZCZXJsaW4xDzANBgNVBAcMBkJlcmxpbjEQ\nMA4GA1UECgwHTmVidWxvbjEMMAoGA1UECwwDQ1RPMRcwFQYDVQQDDA5iYXouZm9v\nYmFyLmNvbTAeFw0yMDExMjIwMTU0MjFaFw0yNDExMjEwMTU0MjFaMGgxCzAJBgNV\nBAYTAkRFMQ8wDQYDVQQIDAZCZXJsaW4xDzANBgNVBAcMBkJlcmxpbjEQMA4GA1UE\nCgwHTmVidWxvbjEMMAoGA1UECwwDQ1RPMRcwFQYDVQQDDA5iYXouZm9vYmFyLmNv\nbTBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQD74G9b+gOxoznt3hDgZCORTOMHT+CU\nouPg/RjwidKEsc6531THPt0hyoeb3H0w62pNGG2Ic2gUtkuxTdre6mvZAgMBAAEw\nDQYJKoZIhvcNAQELBQADQQCarEdycosj9EMNB7HYrqMsSpwdhpORbFozsGYRbTaA\ntDE8tCCOleSsVMDtW2jwL5e+we1QQO+dM88K0pqTKHEm\n-----END CERTIFICATE-----\n';
var validKey2 = '-----BEGIN RSA PRIVATE KEY-----\nMIIBPAIBAAJBAPvgb1v6A7GjOe3eEOBkI5FM4wdP4JSi4+D9GPCJ0oSxzrnfVMc+\n3SHKh5vcfTDrak0YbYhzaBS2S7FN2t7qa9kCAwEAAQJBALsBjWyKmcd/2vjCkWEo\nuEefAEhjg+iXb/2RrLyad1TQfgs35UfigcjpWbzT2ScpFZT61ng6hKmclt2OCT9F\nBKECIQD/bjRbGiPq762ikWkfvalgkAAhSoXo2AcD/MsrhWyyPQIhAPxwM7jZRNvO\nng3TJaAgISwwUC9vuaNJQ06Yt02pvoXNAiEAuQipTrGCAWe8vb5ei8rFzxihr3wf\nw0vy0RWoTA+sbPUCIHDFOwXf4bgEJG1unwdacxdHefrHAXold3D8Hh8OrnMdAiEA\nov6sW0C1+maNpoWC+moDGFdImZnej2SDIB5976akWVo=\n-----END RSA PRIVATE KEY-----\n';
var validCert2 = '-----BEGIN CERTIFICATE-----\nMIIBwjCCAWwCCQCZjm6jL50XfTANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJE\nRTEPMA0GA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4xEDAOBgNVBAoMB05l\nYnVsb24xDDAKBgNVBAsMA0NUTzEXMBUGA1UEAwwOYmF6LmZvb2Jhci5jb20wHhcN\nMTYxMTA4MDgyMDE1WhcNMjAxMTA3MDgyMDE1WjBoMQswCQYDVQQGEwJERTEPMA0G\nA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4xEDAOBgNVBAoMB05lYnVsb24x\nDDAKBgNVBAsMA0NUTzEXMBUGA1UEAwwOYmF6LmZvb2Jhci5jb20wXDANBgkqhkiG\n9w0BAQEFAANLADBIAkEAtKoyTPrf2DjKbnW7Xr1HbRvV+quHTcGmUq5anDI7G4w/\nabqDXGYyakHHlPyZxYp7FWQxCm83rHUuDT1LiLIBZQIDAQABMA0GCSqGSIb3DQEB\nCwUAA0EAVaD2Q6bF9hcUUBev5NyjaMdDYURuWfjuwWUkb8W50O2ed3O+MATKrDdS\nyVaBy8W02KJ4Y1ym4je/MF8nilPurA==\n-----END CERTIFICATE-----';
var validKey2 = '-----BEGIN RSA PRIVATE KEY-----\nMIIBPQIBAAJBALSqMkz639g4ym51u169R20b1fqrh03BplKuWpwyOxuMP2m6g1xm\nMmpBx5T8mcWKexVkMQpvN6x1Lg09S4iyAWUCAwEAAQJBAJXu7YHPbjfuoalcUZzF\nbuKRCFtZQRf5z0Os6QvZ8A3iR0SzYJzx+c2ibp7WdifMXp3XaKm4tHSOfumrjUIq\nt10CIQDrs9Xo7bq0zuNjUV5IshNfaiYKZRfQciRVW2O8xBP9VwIhAMQ5CCEDZy+u\nsaF9RtmB0bjbe6XonBlAzoflfH/MAwWjAiEA50hL+ohr0MfCMM7DKaozgEj0kvan\n645VQLywnaX5x3kCIQDCwjinS9FnKmV0e/uOd6PJb0/S5IXLKt/TUpu33K5DMQIh\nAM9peu3B5t9pO59MmeUGZwI+bEJfEb+h03WTptBxS3pO\n-----END RSA PRIVATE KEY-----';
/*
Generate these with:
openssl ecparam -genkey -name prime256v1 -out server.key
openssl req -new -key server.key -out server.csr -subj "/C=DE/ST=Berlin/L=Berlin/O=Nebulon/OU=CTO/CN=*.foobar.com"
openssl req -x509 -sha256 -days 3650 -key server.key -in server.csr -out server.crt
openssl req -new -sha256 -key server.key -out server.csr
openssl req -x509 -sha256 -days 1460 -key server.key -in server.csr -out server.crt
*/
// *.foobar.com
var validCert4 = '-----BEGIN CERTIFICATE-----\nMIICITCCAcegAwIBAgIUThSKBnGJ3TzM3ACzYQinCB5KS0QwCgYIKoZIzj0EAwIw\nZjELMAkGA1UEBhMCREUxDzANBgNVBAgMBkJlcmxpbjEPMA0GA1UEBwwGQmVybGlu\nMRAwDgYDVQQKDAdOZWJ1bG9uMQwwCgYDVQQLDANDVE8xFTATBgNVBAMMDCouZm9v\nYmFyLmNvbTAeFw0yMDExMjIwMTU5MjhaFw0zMDExMjAwMTU5MjhaMGYxCzAJBgNV\nBAYTAkRFMQ8wDQYDVQQIDAZCZXJsaW4xDzANBgNVBAcMBkJlcmxpbjEQMA4GA1UE\nCgwHTmVidWxvbjEMMAoGA1UECwwDQ1RPMRUwEwYDVQQDDAwqLmZvb2Jhci5jb20w\nWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARXV7XqwL8dTTdKJ1sngAAgFXBmppsy\n5GLjm49GrDTB2ho6sjjwMUzKKP9jVCRrSlcKwmXNAy75/pPtLkL4A+s/o1MwUTAd\nBgNVHQ4EFgQUWajw1bCj16I+F8ZpjQEMnJb56XkwHwYDVR0jBBgwFoAUWajw1bCj\n16I+F8ZpjQEMnJb56XkwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAgNIADBF\nAiEA8eeVP+FvAfg4RVjH17DL/zPUBUIsmyTnPm9D7zIAdc0CICZYPU5qrAKA1h5U\n6+8vX4w+EuVQ8vjc8ATl7L/IKdmL\n-----END CERTIFICATE-----\n';
var validKey4 = '-----BEGIN EC PARAMETERS-----\nBggqhkjOPQMBBw==\n-----END EC PARAMETERS-----\n-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIAczzARUd1L4KN/2Fl4s5kc1to6QzP9XGPCVLfQdtwbSoAoGCCqGSM49\nAwEHoUQDQgAEV1e16sC/HU03SidbJ4AAIBVwZqabMuRi45uPRqw0wdoaOrI48DFM\nyij/Y1Qka0pXCsJlzQMu+f6T7S5C+APrPw==\n-----END EC PRIVATE KEY-----\n';
var validCert4 = '-----BEGIN CERTIFICATE-----\nMIICDDCCAbOgAwIBAgIUduLaSQC6kh9LxVdua1EUBCgQOHYwCgYIKoZIzj0EAwIw\nXDELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGElu\ndGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEVMBMGA1UEAwwMKi5mb29iYXIuY29tMB4X\nDTIwMDMyNTA0MTYxMloXDTI0MDMyNDA0MTYxMlowXDELMAkGA1UEBhMCQVUxEzAR\nBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5\nIEx0ZDEVMBMGA1UEAwwMKi5mb29iYXIuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0D\nAQcDQgAEmBum8MbyGXKuLP+NEOmR15XlemPEHR4b68A+B0Zjh/cuLQncAIwfmLT7\nutUOh3CivEKvZYkQIdd71xhCbVtbkqNTMFEwHQYDVR0OBBYEFCxEvAFsSFyAITNw\niBttbdsyEwO4MB8GA1UdIwQYMBaAFCxEvAFsSFyAITNwiBttbdsyEwO4MA8GA1Ud\nEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDRwAwRAIgd+rxp8xTXy7wsV45hiu1HQ2p\nwrEEPFmfPinVHwhDCiECIAEnIr5bEYUzSjujiHg7C2q3zh41XJhZWQie3VHLY/Kt\n-----END CERTIFICATE-----\n';
var validKey4 = '-----BEGIN EC PARAMETERS-----\nBggqhkjOPQMBBw==\n-----END EC PARAMETERS-----\n-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIAXuQG4YDaQuwOCvWOZjkOvw/Y5V8Oum+rWnliMTsA5woAoGCCqGSM49\nAwEHoUQDQgAEmBum8MbyGXKuLP+NEOmR15XlemPEHR4b68A+B0Zjh/cuLQncAIwf\nmLT7utUOh3CivEKvZYkQIdd71xhCbVtbkg==\n-----END EC PRIVATE KEY-----\n';
// cp /etc/ssl/openssl.cnf /tmp/openssl.cnf
// echo -e "[SAN]\nsubjectAltName=DNS:amazing.com,DNS:*.amazing.com\n" >> /tmp/openssl.cnf
@@ -130,7 +127,7 @@ describe('Certificates', function () {
});
it('does not allow invalid cert/key tuple', function () {
//expect(reverseProxy.validateCertificate('', foobarDomain, { cert: validCert0, key: validKey1 })).to.be.an(Error);
expect(reverseProxy.validateCertificate('', foobarDomain, { cert: validCert0, key: validKey1 })).to.be.an(Error);
});
it('picks certificate in SAN', function () {
-65
View File
@@ -1,65 +0,0 @@
/* jslint node:true */
/* global it:false */
/* global describe:false */
/* global before:false */
'use strict';
var expect = require('expect.js'),
translation = require('../translation.js');
describe('translation', function () {
describe('translate', function () {
before(function (done) {
done();
});
it('nonexisting token', function () {
var out = translation.translate('Foo {{ bar }}', {}, {});
expect(out).to.contain('{{ bar }}');
});
it('existing token', function () {
var out = translation.translate('Foo {{ bar }}', { bar: 'here' }, {});
expect(out).to.contain('here');
});
it('existing token as fallback', function () {
var out = translation.translate('Foo {{ bar }}', {}, { bar: 'here' });
expect(out).to.contain('here');
});
it('existing token deep', function () {
var out = translation.translate('Foo {{ bar.baz.foo }}', { bar: { baz: { foo: 'here' }}}, {});
expect(out).to.contain('here');
});
it('existing token deep as fallback', function () {
var out = translation.translate('Foo {{ bar.baz.foo }}', { bar: '' }, { bar: { baz: { foo: 'here' }}});
expect(out).to.contain('here');
});
it('with whitespace tokens', function () {
var obj = {
something: {
missing: {
there: '1'
}
},
here: '2',
there: '3',
foo: '4',
bar: '5'
};
var input = 'Hello {{ something.missing.there}} and some more {{here}} and {{ there }} with odd spacing {{foo }} lots of{{ bar }}';
var out = translation.translate(input, obj, {});
expect(out).to.contain('1');
expect(out).to.contain('2');
expect(out).to.contain('3');
expect(out).to.contain('4');
expect(out).to.contain('5');
});
});
});
+3
View File
@@ -233,6 +233,7 @@ describe('updatechecker - app - manual (email)', function () {
}
}
},
httpPort: null,
containerId: null,
portBindings: { PORT: 5678 },
healthy: null,
@@ -341,6 +342,7 @@ describe('updatechecker - app - automatic (no email)', function () {
}
}
},
httpPort: null,
containerId: null,
portBindings: { PORT: 5678 },
healthy: null,
@@ -405,6 +407,7 @@ describe('updatechecker - app - automatic free (email)', function () {
}
}
},
httpPort: null,
containerId: null,
portBindings: { PORT: 5678 },
healthy: null,
+1
View File
@@ -12,6 +12,7 @@ var async = require('async'),
fs = require('fs'),
groupdb = require('../groupdb.js'),
groups = require('../groups.js'),
domains = require('../domains.js'),
mailboxdb = require('../mailboxdb.js'),
maildb = require('../maildb.js'),
mailer = require('../mailer.js'),
-113
View File
@@ -1,113 +0,0 @@
/* jslint node:true */
/* global it:false */
/* global describe:false */
/* global before:false */
/* global after:false */
'use strict';
var async = require('async'),
BoxError = require('../boxerror.js'),
database = require('../database.js'),
expect = require('expect.js'),
volumes = require('../volumes.js');
const AUDIT_SOURCE = { ip: '1.2.3.4', userId: 'someuserid' };
function setup(done) {
// ensure data/config/mount paths
async.series([
database.initialize,
database._clear
], done);
}
function cleanup(done) {
async.series([
database._clear,
database.uninitialize
], done);
}
describe('Volumes', function () {
before(setup);
after(cleanup);
let volume;
it('cannot add bad name', function (done) {
volumes.add('music/is', '/tmp/music', AUDIT_SOURCE, function (error) {
expect(error.reason).to.be(BoxError.BAD_FIELD);
done();
});
});
it('cannot add bad path', function (done) {
volumes.add('music', '/tmp/music', AUDIT_SOURCE, function (error) {
expect(error.reason).to.be(BoxError.BAD_FIELD);
done();
});
});
it('can add volume', function (done) {
volumes.add('music', '/mnt/music', AUDIT_SOURCE, function (error, id) {
expect(error).to.be(null);
expect(id).to.be.a('string');
volume = { id, name: 'music', hostPath: '/mnt/music' };
done();
});
});
it('cannot add duplicate path', function (done) {
volumes.add('music-dup', '/mnt/music', AUDIT_SOURCE, function (error) {
expect(error.reason).to.be(BoxError.ALREADY_EXISTS);
done();
});
});
it('cannot add duplicate name', function (done) {
volumes.add('music', '/media/music', AUDIT_SOURCE, function (error) {
expect(error.reason).to.be(BoxError.ALREADY_EXISTS);
done();
});
});
it('can get volume', function (done) {
volumes.get(volume.id, function (error, result) {
expect(error).to.be(null);
expect(result.hostPath).to.be('/mnt/music');
done();
});
});
it('cannot get random volume', function (done) {
volumes.get('randomvolume', function (error) {
expect(error.reason).to.be(BoxError.NOT_FOUND);
done();
});
});
it('can list volumes', function (done) {
volumes.list(function (error, result) {
expect(error).to.be(null);
expect(result).to.be.an(Array);
expect(result.length).to.be(1);
expect(result[0].id).to.be(volume.id);
expect(result[0].hostPath).to.be('/mnt/music');
done();
});
});
it('cannot del random volume', function (done) {
volumes.get('randomvolume', function (error) {
expect(error.reason).to.be(BoxError.NOT_FOUND);
done();
});
});
it('can del volume', function (done) {
volumes.del(volume, AUDIT_SOURCE, function (error) {
expect(error).to.be(null);
done();
});
});
});
-92
View File
@@ -1,92 +0,0 @@
'use strict';
exports = module.exports = {
translate,
getTranslations,
getLanguages
};
var assert = require('assert'),
debug = require('debug')('box:translation'),
fs = require('fs'),
path = require('path'),
paths = require('./paths.js'),
settings = require('./settings.js');
const TRANSLATION_FOLDER = path.join(paths.DASHBOARD_DIR, 'translation');
// to be used together with getTranslations() => { translations, fallback }
function translate(input, translations, fallbackTranslations) {
assert.strictEqual(typeof input, 'string');
assert.strictEqual(typeof translations, 'object');
assert.strictEqual(typeof fallbackTranslations, 'object');
var tokens = input.match(/{{(.*?)}}/gm);
if (!tokens) return input;
var output = input;
tokens.forEach(function (token) {
var key = token.slice(2).slice(0, -2).trim();
var value = key.split('.').reduce(function (acc, cur) {
if (acc === null) return null;
return typeof acc[cur] !== 'undefined' ? acc[cur] : null;
}, translations);
// try fallback
if (value === null) value = key.split('.').reduce(function (acc, cur) {
if (acc === null) return null;
return typeof acc[cur] !== 'undefined' ? acc[cur] : null;
}, fallbackTranslations);
if (value === null) value = token;
output = output.replace(token, value);
});
return output;
}
function getTranslations(callback) {
assert.strictEqual(typeof callback, 'function');
var fallback = {};
try {
fallback = JSON.parse(fs.readFileSync(path.join(TRANSLATION_FOLDER, 'en.json'), 'utf8'));
} catch (e) {
debug('getTranslations: Fallback language en not found', e);
}
settings.getLanguage(function (error, lang) {
if (error) return callback(error);
var translations = {};
try {
translations = JSON.parse(fs.readFileSync(path.join(TRANSLATION_FOLDER, lang + '.json'), 'utf8'));
} catch (e) {
debug(`getTranslations: Requested language ${lang} not found`, e);
}
return callback(null, { translations, fallback });
});
}
function getLanguages(callback) {
assert.strictEqual(typeof callback, 'function');
// we always return english to avoid dashboard breakage
var languages = ['en'];
fs.readdir(TRANSLATION_FOLDER, function (error, result) {
if (error) {
debug('getLanguages: Failed to list translations', error);
return callback(null, languages);
}
var jsonFiles = result.filter(function (file) { return path.extname(file) === '.json'; });
languages = jsonFiles.map(function (file) { return path.basename(file, '.json'); });
debug('Languages found:', jsonFiles);
callback(null, languages);
});
}
-2
View File
@@ -423,8 +423,6 @@ function update(user, data, auditSource, callback) {
assert(auditSource && typeof auditSource === 'object');
assert.strictEqual(typeof callback, 'function');
if (settings.isDemo() && user.username === constants.DEMO_USERNAME) return callback(new BoxError(BoxError.BAD_FIELD, 'Not allowed in demo mode'));
var error;
data = _.pick(data, 'email', 'fallbackEmail', 'displayName', 'username', 'active', 'role');
-95
View File
@@ -1,95 +0,0 @@
/* jslint node:true */
'use strict';
exports = module.exports = {
add,
get,
list,
update,
del,
clear
};
const assert = require('assert'),
BoxError = require('./boxerror.js'),
database = require('./database.js');
const VOLUMES_FIELDS = [ 'id', 'name', 'hostPath', 'creationTime' ].join(',');
function get(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
database.query(`SELECT ${VOLUMES_FIELDS} FROM volumes WHERE id=?`, [ id ], function (error, result) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Volume not found'));
callback(null, result[0]);
});
}
function list(callback) {
database.query(`SELECT ${VOLUMES_FIELDS} FROM volumes ORDER BY name`, function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null, results);
});
}
function add(id, name, hostPath, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof hostPath, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('INSERT INTO volumes (id, name, hostPath) VALUES (?, ?, ?)', [ id, name, hostPath ], function (error) {
if (error && error.code === 'ER_DUP_ENTRY' && error.sqlMessage.indexOf('name') !== -1) return callback(new BoxError(BoxError.ALREADY_EXISTS, 'name already exists'));
if (error && error.code === 'ER_DUP_ENTRY' && error.sqlMessage.indexOf('hostPath') !== -1) return callback(new BoxError(BoxError.ALREADY_EXISTS, 'hostPath already exists'));
if (error && error.code === 'ER_DUP_ENTRY' && error.sqlMessage.indexOf('PRIMARY') !== -1) return callback(new BoxError(BoxError.ALREADY_EXISTS, 'id already exists'));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null);
});
}
function update(id, data, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof callback, 'function');
var args = [ ], fields = [ ];
for (var k in data) {
fields.push(k + ' = ?');
args.push(data[k]);
}
args.push(id);
database.query('UPDATE volumes SET ' + fields.join(', ') + ' WHERE id=?', args, function (error) {
if (error && error.reason === BoxError.NOT_FOUND) return callback(new BoxError(BoxError.NOT_FOUND, 'Volume not found'));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null);
});
}
function del(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('DELETE FROM volumes WHERE id=?', [ id ], function (error, result) {
if (error && error.code === 'ER_ROW_IS_REFERENCED_2') return callback(new BoxError(BoxError.CONFLICT, 'Volume is in use'));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'Volume not found'));
callback(null);
});
}
function clear(callback) {
database.query('DELETE FROM volumes', function (error) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(error);
});
}
-106
View File
@@ -1,106 +0,0 @@
'use strict';
exports = module.exports = {
add,
get,
del,
list
};
const assert = require('assert'),
BoxError = require('./boxerror.js'),
debug = require('debug')('box:volumes'),
eventlog = require('./eventlog.js'),
path = require('path'),
safe = require('safetydance'),
sftp = require('./sftp.js'),
uuid = require('uuid'),
volumedb = require('./volumedb.js');
function validateName(name) {
assert.strictEqual(typeof name, 'string');
if (!/^[-\w^&'@{}[\],$=!#().%+~ ]+$/.test(name)) return new BoxError(BoxError.BAD_FIELD, 'Invalid name');
return null;
}
function validateHostPath(hostPath) {
assert.strictEqual(typeof hostPath, 'string');
if (path.normalize(hostPath) !== hostPath) return new BoxError(BoxError.BAD_FIELD, 'hostPath must contain a normalized path', { field: 'hostPath' });
if (!path.isAbsolute(hostPath)) return new BoxError(BoxError.BAD_FIELD, 'backupFolder must be an absolute path', { field: 'hostPath' });
if (hostPath === '/') return new BoxError(BoxError.BAD_FIELD, 'hostPath cannot be /', { field: 'hostPath' });
if (!hostPath.endsWith('/')) hostPath = hostPath + '/'; // ensure trailing slash for the prefix matching to work
const allowedPaths = [ '/mnt/', '/media/', '/srv/', '/opt/' ];
if (!allowedPaths.some(p => hostPath.startsWith(p))) return new BoxError(BoxError.BAD_FIELD, 'hostPath must be under /mnt, /media, /opt or /srv', { field: 'hostPath' });
const stat = safe.fs.lstatSync(hostPath);
if (!stat) return new BoxError(BoxError.BAD_FIELD, 'hostPath does not exist. Please create it on the server first', { field: 'hostPath' });
if (!stat.isDirectory()) return new BoxError(BoxError.BAD_FIELD, 'hostPath is not a directory', { field: 'hostPath' });
return null;
}
function add(name, hostPath, auditSource, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof hostPath, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
let error = validateName(name);
if (error) return callback(error);
error = validateHostPath(hostPath);
if (error) return callback(error);
const id = uuid();
volumedb.add(id, name, hostPath, function (error) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_VOLUME_ADD, auditSource, { id, name, hostPath });
sftp.rebuild((error) => { if (error) debug('Unable to rebuild sftp:', error); });
callback(null, id);
});
}
function get(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
volumedb.get(id, function (error, result) {
if (error) return callback(error);
callback(null, result);
});
}
function list(callback) {
assert.strictEqual(typeof callback, 'function');
volumedb.list(function (error, result) {
if (error) return callback(error);
return callback(null, result);
});
}
function del(volume, auditSource, callback) {
assert.strictEqual(typeof volume, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
volumedb.del(volume.id, function (error) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_VOLUME_REMOVE, auditSource, { volume });
sftp.rebuild((error) => { if (error) debug('Unable to rebuild sftp:', error); });
return callback(null);
});
}