Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| af512a669b | |||
| 5ed22ba6ff | |||
| 4eca370424 | |||
| 1f35b17812 |
@@ -565,16 +565,3 @@
|
||||
- Add plan migration interface
|
||||
- Initial EC2 support
|
||||
|
||||
[0.17.0]
|
||||
- Public beta release of Cloudron Mail Server
|
||||
- Add new DNS & Certs UI that enables easy migration to a custom domain
|
||||
- Allow sending and receiving email from alias subaddresses
|
||||
- Fix installation issue with some apps on the naked domain
|
||||
|
||||
[0.17.1]
|
||||
- Preliminary user impersonation support
|
||||
- Fix crash in mail container when generating bounces
|
||||
|
||||
[0.17.2]
|
||||
- Add config option to embed apps in other sites
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
dbm = dbm || require('db-migrate');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps ADD COLUMN xFrameOptions VARCHAR(512)', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps DROP COLUMN xFrameOptions', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -67,7 +67,6 @@ CREATE TABLE IF NOT EXISTS apps(
|
||||
createdAt TIMESTAMP(2) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
memoryLimit BIGINT DEFAULT 0,
|
||||
altDomain VARCHAR(256),
|
||||
xFrameOptions VARCHAR(512),
|
||||
|
||||
lastBackupId VARCHAR(128), // tracks last valid backup, can be removed
|
||||
|
||||
|
||||
+4
-9
@@ -10,8 +10,7 @@ arg_fqdn=""
|
||||
arg_is_custom_domain="false"
|
||||
arg_restore_key=""
|
||||
arg_restore_url=""
|
||||
arg_retire_reason=""
|
||||
arg_retire_info=""
|
||||
arg_retire=""
|
||||
arg_tls_config=""
|
||||
arg_tls_cert=""
|
||||
arg_tls_key=""
|
||||
@@ -24,17 +23,13 @@ arg_update_config=""
|
||||
arg_provider=""
|
||||
arg_app_bundle=""
|
||||
|
||||
args=$(getopt -o "" -l "data:,retire-reason:,retire-info:" -n "$0" -- "$@")
|
||||
args=$(getopt -o "" -l "data:,retire:" -n "$0" -- "$@")
|
||||
eval set -- "${args}"
|
||||
|
||||
while true; do
|
||||
case "$1" in
|
||||
--retire-reason)
|
||||
arg_retire_reason="$2"
|
||||
shift 2
|
||||
;;
|
||||
--retire-info)
|
||||
arg_retire_info="$2"
|
||||
--retire)
|
||||
arg_retire="$2"
|
||||
shift 2
|
||||
;;
|
||||
--data)
|
||||
|
||||
+6
-6
@@ -25,19 +25,19 @@ cp -r "${script_dir}/splash/website/"* "${SETUP_WEBSITE_DIR}"
|
||||
readonly current_infra=$(node -e "console.log(require('${script_dir}/../src/infra_version.js').version);")
|
||||
existing_infra="none"
|
||||
[[ -f "${DATA_DIR}/INFRA_VERSION" ]] && existing_infra=$(node -e "console.log(JSON.parse(require('fs').readFileSync('${DATA_DIR}/INFRA_VERSION', 'utf8')).version);")
|
||||
if [[ "${arg_retire_reason}" != "" || "${existing_infra}" != "${current_infra}" ]]; then
|
||||
echo "Showing progress bar on all subdomains in retired mode or infra update. retire: ${arg_retire_reason} existing: ${existing_infra} current: ${current_infra}"
|
||||
if [[ "${arg_retire}" != "" || "${existing_infra}" != "${current_infra}" ]]; then
|
||||
echo "Showing progress bar on all subdomains in retired mode or infra update. retire: ${arg_retire} existing: ${existing_infra} current: ${current_infra}"
|
||||
rm -f ${DATA_DIR}/nginx/applications/*
|
||||
${BOX_SRC_DIR}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/appconfig.ejs" \
|
||||
-O "{ \"vhost\": \"~^(.+)\$\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"splash\", \"sourceDir\": \"${SETUP_WEBSITE_DIR}\", \"certFilePath\": \"cert/host.cert\", \"keyFilePath\": \"cert/host.key\", \"xFrameOptions\": \"SAMEORIGIN\" }" > "${DATA_DIR}/nginx/applications/admin.conf"
|
||||
-O "{ \"vhost\": \"~^(.+)\$\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"splash\", \"sourceDir\": \"${SETUP_WEBSITE_DIR}\", \"certFilePath\": \"cert/host.cert\", \"keyFilePath\": \"cert/host.key\" }" > "${DATA_DIR}/nginx/applications/admin.conf"
|
||||
else
|
||||
echo "Show progress bar only on admin domain for normal update"
|
||||
${BOX_SRC_DIR}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/appconfig.ejs" \
|
||||
-O "{ \"vhost\": \"${admin_fqdn}\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"splash\", \"sourceDir\": \"${SETUP_WEBSITE_DIR}\", \"certFilePath\": \"cert/host.cert\", \"keyFilePath\": \"cert/host.key\", \"xFrameOptions\": \"SAMEORIGIN\" }" > "${DATA_DIR}/nginx/applications/admin.conf"
|
||||
-O "{ \"vhost\": \"${admin_fqdn}\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"splash\", \"sourceDir\": \"${SETUP_WEBSITE_DIR}\", \"certFilePath\": \"cert/host.cert\", \"keyFilePath\": \"cert/host.key\" }" > "${DATA_DIR}/nginx/applications/admin.conf"
|
||||
fi
|
||||
|
||||
if [[ "${arg_retire_reason}" == "migrate" ]]; then
|
||||
echo "{ \"migrate\": { \"percent\": \"10\", \"message\": \"Migrating cloudron. This could take up to 15 minutes.\", \"info\": ${arg_retire_info} }, \"backup\": null, \"apiServerOrigin\": \"${arg_api_server_origin}\" }" > "${SETUP_WEBSITE_DIR}/progress.json"
|
||||
if [[ "${arg_retire}" == "migrate" ]]; then
|
||||
echo '{ "migrate": { "percent": "10", "message": "Migrating cloudron. This could take up to 15 minutes." }, "backup": null }' > "${SETUP_WEBSITE_DIR}/progress.json"
|
||||
else
|
||||
echo '{ "update": { "percent": "10", "message": "Updating cloudron software" }, "backup": null }' > "${SETUP_WEBSITE_DIR}/progress.json"
|
||||
fi
|
||||
|
||||
+2
-7
@@ -34,14 +34,9 @@ set_progress() {
|
||||
set_progress "1" "Create container"
|
||||
$script_dir/container.sh
|
||||
|
||||
set_progress "5" "Adjust system settings"
|
||||
set_progress "5" "Set hostname"
|
||||
hostnamectl set-hostname "${arg_fqdn}"
|
||||
|
||||
# ec2 instances use lots of cpu for swapping, which can be significantly reduced adjusting the swappiness
|
||||
if [[ "${arg_provider}" == 'ec2' ]]; then
|
||||
sysctl vm.swappiness=0
|
||||
fi
|
||||
|
||||
set_progress "10" "Ensuring directories"
|
||||
# keep these in sync with paths.js
|
||||
[[ "${is_update}" == "false" ]] && btrfs subvolume create "${DATA_DIR}/box"
|
||||
@@ -115,7 +110,7 @@ if [[ -f "${DATA_DIR}/box/certs/${admin_fqdn}.cert" && -f "${DATA_DIR}/box/certs
|
||||
admin_key_file="${DATA_DIR}/box/certs/${admin_fqdn}.key"
|
||||
fi
|
||||
${BOX_SRC_DIR}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/appconfig.ejs" \
|
||||
-O "{ \"vhost\": \"${admin_fqdn}\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"admin\", \"sourceDir\": \"${BOX_SRC_DIR}\", \"certFilePath\": \"${admin_cert_file}\", \"keyFilePath\": \"${admin_key_file}\", \"xFrameOptions\": \"SAMEORIGIN\" }" > "${DATA_DIR}/nginx/applications/admin.conf"
|
||||
-O "{ \"vhost\": \"${admin_fqdn}\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"admin\", \"sourceDir\": \"${BOX_SRC_DIR}\", \"certFilePath\": \"${admin_cert_file}\", \"keyFilePath\": \"${admin_key_file}\" }" > "${DATA_DIR}/nginx/applications/admin.conf"
|
||||
|
||||
mkdir -p "${DATA_DIR}/nginx/cert"
|
||||
if [[ -f "${DATA_DIR}/box/certs/host.cert" && -f "${DATA_DIR}/box/certs/host.key" ]]; then
|
||||
|
||||
@@ -25,7 +25,7 @@ server {
|
||||
add_header Strict-Transport-Security "max-age=15768000; includeSubDomains";
|
||||
|
||||
# https://developer.mozilla.org/en-US/docs/Web/HTTP/X-Frame-Options
|
||||
add_header X-Frame-Options "<%= xFrameOptions %>";
|
||||
add_header X-Frame-Options SAMEORIGIN;
|
||||
|
||||
proxy_http_version 1.1;
|
||||
proxy_intercept_errors on;
|
||||
|
||||
+8
-6
@@ -399,7 +399,7 @@ function setupSendMail(app, options, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var from = (app.location ? app.location : app.manifest.title.replace(/[^a-zA-Z0-9]/g, '')) + '.app';
|
||||
var from = (app.location ? app.location : app.manifest.title.replace(/[^a-zA-Z0-9]/, '')) + '.app';
|
||||
|
||||
var cmd = [ '/addons/mail/service.sh', 'add-send', from ];
|
||||
|
||||
@@ -417,11 +417,13 @@ function teardownSendMail(app, options, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var from = (app.location ? app.location : app.manifest.title.replace(/[^a-zA-Z0-9]/g, '')) + '.app';
|
||||
debugApp(app, 'Tearing down sendmail');
|
||||
|
||||
var from = (app.location ? app.location : app.manifest.title.replace(/[^a-zA-Z0-9]/, '')) + '.app';
|
||||
|
||||
var cmd = [ '/addons/mail/service.sh', 'remove-send', from ];
|
||||
|
||||
debugApp(app, 'Tearing down sendmail : %j', cmd);
|
||||
debugApp(app, 'Tearing down sendmail');
|
||||
|
||||
docker.execContainer('mail', cmd, { }, function (error) {
|
||||
if (error) return callback(error);
|
||||
@@ -437,7 +439,7 @@ function setupRecvMail(app, options, callback) {
|
||||
|
||||
debugApp(app, 'Setting up recvmail');
|
||||
|
||||
var to = (app.location ? app.location : app.manifest.title.replace(/[^a-zA-Z0-9]/g, '')) + '.app';
|
||||
var to = (app.location ? app.location : app.manifest.title.replace(/[^a-zA-Z0-9]/, '')) + '.app';
|
||||
|
||||
var cmd = [ '/addons/mail/service.sh', 'add-recv', to ];
|
||||
|
||||
@@ -455,11 +457,11 @@ function teardownRecvMail(app, options, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var to = (app.location ? app.location : app.manifest.title.replace(/[^a-zA-Z0-9]/g, '')) + '.app';
|
||||
var to = (app.location ? app.location : app.manifest.title.replace(/[^a-zA-Z0-9]/, '')) + '.app';
|
||||
|
||||
var cmd = [ '/addons/mail/service.sh', 'remove-recv', to ];
|
||||
|
||||
debugApp(app, 'Tearing down recvmail: %j', cmd);
|
||||
debugApp(app, 'Tearing down recvmail');
|
||||
|
||||
docker.execContainer('mail', cmd, { }, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
+3
-7
@@ -59,7 +59,7 @@ var assert = require('assert'),
|
||||
|
||||
var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationState', 'apps.installationProgress', 'apps.runState',
|
||||
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.httpPort', 'apps.location', 'apps.dnsRecordId',
|
||||
'apps.accessRestrictionJson', 'apps.lastBackupId', 'apps.oldConfigJson', 'apps.memoryLimit', 'apps.altDomain', 'apps.xFrameOptions' ].join(',');
|
||||
'apps.accessRestrictionJson', 'apps.lastBackupId', 'apps.oldConfigJson', 'apps.memoryLimit', 'apps.altDomain' ].join(',');
|
||||
|
||||
var PORT_BINDINGS_FIELDS = [ 'hostPort', 'environmentVariable', 'appId' ].join(',');
|
||||
|
||||
@@ -92,9 +92,6 @@ function postProcess(result) {
|
||||
result.accessRestriction = safe.JSON.parse(result.accessRestrictionJson);
|
||||
if (result.accessRestriction && !result.accessRestriction.users) result.accessRestriction.users = [];
|
||||
delete result.accessRestrictionJson;
|
||||
|
||||
// TODO remove later once all apps have this attribute
|
||||
result.xFrameOptions = result.xFrameOptions || 'SAMEORIGIN';
|
||||
}
|
||||
|
||||
function get(id, callback) {
|
||||
@@ -178,14 +175,13 @@ function add(id, appStoreId, manifest, location, portBindings, data, callback) {
|
||||
var accessRestrictionJson = JSON.stringify(accessRestriction);
|
||||
var memoryLimit = data.memoryLimit || 0;
|
||||
var altDomain = data.altDomain || null;
|
||||
var xFrameOptions = data.xFrameOptions || '';
|
||||
var installationState = data.installationState || exports.ISTATE_PENDING_INSTALL;
|
||||
var lastBackupId = data.lastBackupId || null; // used when cloning
|
||||
|
||||
var queries = [ ];
|
||||
queries.push({
|
||||
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, location, accessRestrictionJson, memoryLimit, altDomain, xFrameOptions, lastBackupId) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
args: [ id, appStoreId, manifestJson, installationState, location, accessRestrictionJson, memoryLimit, altDomain, xFrameOptions, lastBackupId ]
|
||||
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, location, accessRestrictionJson, memoryLimit, altDomain, lastBackupId) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
args: [ id, appStoreId, manifestJson, installationState, location, accessRestrictionJson, memoryLimit, altDomain, lastBackupId ]
|
||||
});
|
||||
|
||||
Object.keys(portBindings).forEach(function (env) {
|
||||
|
||||
+3
-32
@@ -68,7 +68,6 @@ var addons = require('./addons.js'),
|
||||
split = require('split'),
|
||||
superagent = require('superagent'),
|
||||
taskmanager = require('./taskmanager.js'),
|
||||
url = require('url'),
|
||||
util = require('util'),
|
||||
uuid = require('node-uuid'),
|
||||
validator = require('validator');
|
||||
@@ -219,20 +218,6 @@ function validateMemoryLimit(manifest, memoryLimit) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// https://tools.ietf.org/html/rfc7034
|
||||
function validateXFrameOptions(xFrameOptions) {
|
||||
assert.strictEqual(typeof xFrameOptions, 'string');
|
||||
|
||||
if (xFrameOptions === 'DENY') return null;
|
||||
if (xFrameOptions === 'SAMEORIGIN') return null;
|
||||
|
||||
var parts = xFrameOptions.split(' ');
|
||||
if (parts.length !== 2 || parts[0] !== 'ALLOW-FROM') return new AppsError(AppsError.BAD_FIELD, 'xFrameOptions must be "DENY", "SAMEORIGIN" or "ALLOW-FROM uri"' );
|
||||
|
||||
var uri = url.parse(parts[1]);
|
||||
return (uri.protocol === 'http:' || uri.protocol === 'https:') ? null : new AppsError(AppsError.BAD_FIELD, 'xFrameOptions ALLOW-FROM uri must be a valid http[s] uri' );
|
||||
}
|
||||
|
||||
function getDuplicateErrorDetails(location, portBindings, error) {
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof portBindings, 'object');
|
||||
@@ -262,7 +247,6 @@ function getAppConfig(app) {
|
||||
accessRestriction: app.accessRestriction,
|
||||
portBindings: app.portBindings,
|
||||
memoryLimit: app.memoryLimit,
|
||||
xFrameOptions: app.xFrameOptions || 'SAMEORIGIN',
|
||||
altDomain: app.altDomain
|
||||
};
|
||||
}
|
||||
@@ -412,8 +396,7 @@ function install(data, auditSource, callback) {
|
||||
cert = data.cert || null,
|
||||
key = data.key || null,
|
||||
memoryLimit = data.memoryLimit || 0,
|
||||
altDomain = data.altDomain || null,
|
||||
xFrameOptions = data.xFrameOptions || 'SAMEORIGIN';
|
||||
altDomain = data.altDomain || null;
|
||||
|
||||
assert(data.appStoreId || data.manifest); // atleast one of them is required
|
||||
|
||||
@@ -438,9 +421,6 @@ function install(data, auditSource, callback) {
|
||||
error = validateMemoryLimit(manifest, memoryLimit);
|
||||
if (error) return callback(error);
|
||||
|
||||
error = validateXFrameOptions(xFrameOptions);
|
||||
if (error) return callback(error);
|
||||
|
||||
// memoryLimit might come in as 0 if not specified
|
||||
memoryLimit = memoryLimit || manifest.memoryLimit || constants.DEFAULT_MEMORY_LIMIT;
|
||||
|
||||
@@ -450,8 +430,6 @@ function install(data, auditSource, callback) {
|
||||
if (manifest.singleUser && accessRestriction === null) return callback(new AppsError(AppsError.USER_REQUIRED));
|
||||
if (manifest.singleUser && accessRestriction.users.length !== 1) return callback(new AppsError(AppsError.USER_REQUIRED));
|
||||
|
||||
var appId = uuid.v4();
|
||||
|
||||
if (icon) {
|
||||
if (!validator.isBase64(icon)) return callback(new AppsError(AppsError.BAD_FIELD, 'icon is not base64'));
|
||||
|
||||
@@ -463,6 +441,7 @@ function install(data, auditSource, callback) {
|
||||
error = certificates.validateCertificate(cert, key, config.appFqdn(location));
|
||||
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
|
||||
|
||||
var appId = uuid.v4();
|
||||
debug('Will install app with id : ' + appId);
|
||||
|
||||
purchase(appStoreId, function (error) {
|
||||
@@ -471,8 +450,7 @@ function install(data, auditSource, callback) {
|
||||
var data = {
|
||||
accessRestriction: accessRestriction,
|
||||
memoryLimit: memoryLimit,
|
||||
altDomain: altDomain,
|
||||
xFrameOptions: xFrameOptions
|
||||
altDomain: altDomain
|
||||
};
|
||||
|
||||
appdb.add(appId, appStoreId, manifest, location, portBindings, data, function (error) {
|
||||
@@ -542,12 +520,6 @@ function configure(appId, data, auditSource, callback) {
|
||||
values.memoryLimit = values.memoryLimit || app.memoryLimit || app.manifest.memoryLimit || constants.DEFAULT_MEMORY_LIMIT;
|
||||
}
|
||||
|
||||
if ('xFrameOptions' in data) {
|
||||
values.xFrameOptions = data.xFrameOptions;
|
||||
error = validateXFrameOptions(values.xFrameOptions);
|
||||
if (error) return callback(error);
|
||||
}
|
||||
|
||||
// save cert to data/box/certs. TODO: move this to apptask when we have a real task queue
|
||||
if ('cert' in data && 'key' in data) {
|
||||
if (data.cert && data.key) {
|
||||
@@ -789,7 +761,6 @@ function clone(appId, data, auditSource, callback) {
|
||||
installationState: appdb.ISTATE_PENDING_CLONE,
|
||||
memoryLimit: app.memoryLimit,
|
||||
accessRestriction: app.accessRestriction,
|
||||
xFrameOptions: app.xFrameOptions,
|
||||
lastBackupId: backupId
|
||||
};
|
||||
|
||||
|
||||
@@ -92,6 +92,8 @@ function getApi(app, callback) {
|
||||
}
|
||||
|
||||
function installAdminCertificate(callback) {
|
||||
if (!cloudron.isConfiguredSync()) return callback();
|
||||
|
||||
settings.getTlsConfig(function (error, tlsConfig) {
|
||||
if (error) return callback(error);
|
||||
|
||||
|
||||
+12
-30
@@ -46,7 +46,6 @@ var apps = require('./apps.js'),
|
||||
progress = require('./progress.js'),
|
||||
safe = require('safetydance'),
|
||||
settings = require('./settings.js'),
|
||||
SettingsError = settings.SettingsError,
|
||||
shell = require('./shell.js'),
|
||||
subdomains = require('./subdomains.js'),
|
||||
superagent = require('superagent'),
|
||||
@@ -292,15 +291,14 @@ function getBoxAndUserDetails(callback) {
|
||||
|
||||
if (gBoxAndUserDetails) return callback(null, gBoxAndUserDetails);
|
||||
|
||||
// only supported for caas
|
||||
if (config.provider() !== 'caas') return callback(null, {});
|
||||
if (!config.token()) return callback(new Error(CloudronError.EXTERNAL_ERROR, 'No appstore token'));
|
||||
|
||||
superagent
|
||||
.get(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn())
|
||||
.query({ token: config.token() })
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new CloudronError(CloudronError.EXTERNAL_ERROR, 'Cannot reach appstore'));
|
||||
if (result.statusCode !== 200) return callback(new CloudronError(CloudronError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
|
||||
if (error && !error.response) return callback(error);
|
||||
if (result.statusCode !== 200) return callback(new CloudronError(CloudronError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
|
||||
|
||||
gBoxAndUserDetails = result.body;
|
||||
|
||||
@@ -312,9 +310,11 @@ function getConfig(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getBoxAndUserDetails(function (error, result) {
|
||||
if (error) debug('Failed to fetch cloudron details.', error.reason, error.message);
|
||||
if (error) {
|
||||
debug('Failed to fetch cloudron details.', error);
|
||||
}
|
||||
|
||||
result = _.extend(BOX_AND_USER_TEMPLATE, result || {});
|
||||
result = _.extend(BOX_AND_USER_TEMPLATE, result || { });
|
||||
|
||||
settings.getCloudronName(function (error, cloudronName) {
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
@@ -456,7 +456,7 @@ function addDnsRecords() {
|
||||
records.push(dkimRecord);
|
||||
records.push(mxRecord);
|
||||
} else {
|
||||
// for non-custom domains, we show a nakeddomain.html page
|
||||
// for custom domains, we show a nakeddomain.html page
|
||||
var nakedDomainRecord = { subdomain: '', type: 'A', values: [ ip ] };
|
||||
|
||||
records.push(nakedDomainRecord);
|
||||
@@ -704,20 +704,18 @@ function checkDiskSpace(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function retire(reason, info, callback) {
|
||||
function retire(reason, callback) {
|
||||
assert(reason === 'migrate' || reason === 'upgrade');
|
||||
info = info || { };
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
|
||||
var data = {
|
||||
apiServerOrigin: config.apiServerOrigin(),
|
||||
isCustomDomain: config.isCustomDomain(),
|
||||
fqdn: config.fqdn()
|
||||
};
|
||||
shell.sudo('retire', [ RETIRE_CMD, reason, JSON.stringify(info), JSON.stringify(data) ], callback);
|
||||
shell.sudo('retire', [ RETIRE_CMD, reason, JSON.stringify(data) ], callback);
|
||||
}
|
||||
|
||||
function doMigrate(options, callback) {
|
||||
function migrate(options, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
@@ -752,25 +750,9 @@ function doMigrate(options, callback) {
|
||||
|
||||
progress.set(progress.MIGRATE, 10, 'Migrating');
|
||||
|
||||
retire('migrate', _.pick(options, 'domain', 'size', 'region'));
|
||||
retire('migrate');
|
||||
});
|
||||
});
|
||||
|
||||
callback(null);
|
||||
}
|
||||
|
||||
function migrate(options, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!options.domain) return doMigrate(options, callback);
|
||||
|
||||
var dnsConfig = _.pick(options, 'domain', 'provider', 'accessKeyId', 'secretAccessKey', 'region', 'endpoint');
|
||||
|
||||
settings.setDnsConfig(dnsConfig, function (error) {
|
||||
if (error && error.reason === SettingsError.BAD_FIELD) return callback(new CloudronError(CloudronError.BAD_FIELD, error.message));
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
|
||||
doMigrate(options, callback);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -14,8 +14,6 @@ exports = module.exports = {
|
||||
ADMIN_CLIENT_ID: 'webadmin', // oauth client id
|
||||
ADMIN_APPID: 'admin', // admin appid (settingsdb)
|
||||
|
||||
GHOST_USER_FILE: '/tmp/cloudron_ghost.json',
|
||||
|
||||
DEFAULT_MEMORY_LIMIT: (256 * 1024 * 1024) // see also client.js
|
||||
};
|
||||
|
||||
|
||||
+1
-1
@@ -66,7 +66,7 @@ function recreateJobs(unusedTimeZone, callback) {
|
||||
|
||||
if (gBackupJob) gBackupJob.stop();
|
||||
gBackupJob = new CronJob({
|
||||
cronTime: '00 00 */4 * * *', // every 4 hours. backups.ensureBackup() will only trigger a backup once per day
|
||||
cronTime: '00 00 00 * * *', // every day once at midnight
|
||||
onTick: backups.ensureBackup.bind(null, AUDIT_SOURCE, NOOP_CALLBACK),
|
||||
start: true,
|
||||
timeZone: allSettings[settings.TIME_ZONE_KEY]
|
||||
|
||||
+4
-24
@@ -5,14 +5,12 @@ exports = module.exports = {
|
||||
get: get,
|
||||
del: del,
|
||||
update: update,
|
||||
getChangeStatus: getChangeStatus,
|
||||
|
||||
// not part of "dns" interface
|
||||
getHostedZone: getHostedZone
|
||||
getChangeStatus: getChangeStatus
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
AWS = require('aws-sdk'),
|
||||
config = require('../config.js'),
|
||||
debug = require('debug')('box:dns/route53'),
|
||||
SubdomainError = require('../subdomains.js').SubdomainError,
|
||||
util = require('util'),
|
||||
@@ -52,24 +50,6 @@ function getZoneByName(dnsConfig, zoneName, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getHostedZone(dnsConfig, zoneName, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getZoneByName(dnsConfig, zoneName, function (error, zone) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
|
||||
route53.getHostedZone({ Id: zone.Id }, function (error, result) {
|
||||
if (error && error.code === 'AccessDenied') return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, error.message));
|
||||
if (error) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error.message));
|
||||
|
||||
callback(null, result);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function add(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
@@ -83,7 +63,7 @@ function add(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
getZoneByName(dnsConfig, zoneName, function (error, zone) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var fqdn = subdomain === '' ? zoneName : subdomain + '.' + zoneName;
|
||||
var fqdn = config.appFqdn(subdomain);
|
||||
var records = values.map(function (v) { return { Value: v }; });
|
||||
|
||||
var params = {
|
||||
@@ -171,7 +151,7 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||
getZoneByName(dnsConfig, zoneName, function (error, zone) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var fqdn = subdomain === '' ? zoneName : subdomain + '.' + zoneName;
|
||||
var fqdn = config.appFqdn(subdomain);
|
||||
var records = values.map(function (v) { return { Value: v }; });
|
||||
|
||||
var resourceRecordSet = {
|
||||
|
||||
@@ -43,6 +43,7 @@ var addons = require('./addons.js'),
|
||||
debug = require('debug')('box:src/docker.js'),
|
||||
once = require('once'),
|
||||
safe = require('safetydance'),
|
||||
semver = require('semver'),
|
||||
spawn = child_process.spawn,
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
// Do not require anything here!
|
||||
|
||||
exports = module.exports = {
|
||||
'version': 40,
|
||||
'version': 38,
|
||||
|
||||
'baseImage': 'cloudron/base:0.8.1',
|
||||
|
||||
@@ -14,7 +14,7 @@ exports = module.exports = {
|
||||
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:0.11.0' },
|
||||
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:0.10.0' },
|
||||
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:0.9.0' },
|
||||
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:0.17.0' },
|
||||
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:0.15.0' },
|
||||
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:0.9.0' }
|
||||
}
|
||||
};
|
||||
|
||||
@@ -20,7 +20,6 @@ module.exports = function cors(options) {
|
||||
if (!requestOrigin) return next();
|
||||
|
||||
requestOrigin = url.parse(requestOrigin);
|
||||
if (!requestOrigin.host) return res.status(405).send('CORS not allowed from this domain');
|
||||
|
||||
var hostname = requestOrigin.host.split(':')[0]; // remove any port
|
||||
var originAllowed = origins.some(function (o) { return o === '*' || o === hostname; });
|
||||
|
||||
+2
-4
@@ -45,8 +45,7 @@ function configureAdmin(certFilePath, keyFilePath, callback) {
|
||||
vhost: config.adminFqdn(),
|
||||
endpoint: 'admin',
|
||||
certFilePath: certFilePath,
|
||||
keyFilePath: keyFilePath,
|
||||
xFrameOptions: 'SAMEORIGIN'
|
||||
keyFilePath: keyFilePath
|
||||
};
|
||||
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
|
||||
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, 'admin.conf');
|
||||
@@ -74,8 +73,7 @@ function configureApp(app, certFilePath, keyFilePath, callback) {
|
||||
port: app.httpPort,
|
||||
endpoint: endpoint,
|
||||
certFilePath: certFilePath,
|
||||
keyFilePath: keyFilePath,
|
||||
xFrameOptions: app.xFrameOptions || 'SAMEORIGIN' // once all apps have been updated/
|
||||
keyFilePath: keyFilePath
|
||||
};
|
||||
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
|
||||
|
||||
|
||||
+1
-5
@@ -53,8 +53,7 @@ function removeInternalAppFields(app) {
|
||||
iconUrl: app.iconUrl,
|
||||
fqdn: app.fqdn,
|
||||
memoryLimit: app.memoryLimit,
|
||||
altDomain: app.altDomain,
|
||||
xFrameOptions: app.xFrameOptions
|
||||
altDomain: app.altDomain
|
||||
};
|
||||
}
|
||||
|
||||
@@ -121,8 +120,6 @@ function installApp(req, res, next) {
|
||||
// falsy value in altDomain unsets it
|
||||
if (data.altDomain && typeof data.altDomain !== 'string') return next(new HttpError(400, 'altDomain must be a string'));
|
||||
|
||||
if (data.xFrameOptions && typeof data.xFrameOptions !== 'string') return next(new HttpError(400, 'xFrameOptions must be a string'));
|
||||
|
||||
debug('Installing app id:%s data:%j', data);
|
||||
|
||||
apps.install(data, auditSource(req), function (error, app) {
|
||||
@@ -158,7 +155,6 @@ function configureApp(req, res, next) {
|
||||
|
||||
if ('memoryLimit' in data && typeof data.memoryLimit !== 'number') return next(new HttpError(400, 'memoryLimit is not a number'));
|
||||
if (data.altDomain && typeof data.altDomain !== 'string') return next(new HttpError(400, 'altDomain must be a string'));
|
||||
if (data.xFrameOptions && typeof data.xFrameOptions !== 'string') return next(new HttpError(400, 'xFrameOptions must be a string'));
|
||||
|
||||
debug('Configuring app id:%s data:%j', req.params.id, data);
|
||||
|
||||
|
||||
+4
-13
@@ -116,24 +116,15 @@ function reboot(req, res, next) {
|
||||
}
|
||||
|
||||
function migrate(req, res, next) {
|
||||
if (config.provider() !== 'caas') return next(new HttpError(422, 'Cannot use migrate API with this provider'));
|
||||
if (typeof req.body.size !== 'string') return next(new HttpError(400, 'size must be string'));
|
||||
if (typeof req.body.region !== 'string') return next(new HttpError(400, 'region must be string'));
|
||||
|
||||
if ('size' in req.body && typeof req.body.size !== 'string') return next(new HttpError(400, 'size must be string'));
|
||||
if ('region' in req.body && typeof req.body.region !== 'string') return next(new HttpError(400, 'region must be string'));
|
||||
|
||||
if ('domain' in req.body) {
|
||||
if (typeof req.body.domain !== 'string') return next(new HttpError(400, 'domain must be string'));
|
||||
if (typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider must be string'));
|
||||
}
|
||||
|
||||
debug('Migration requested domain:%s size:%s region:%s', req.body.domain, req.body.size, req.body.region);
|
||||
debug('Migration requested', req.body.size, req.body.region);
|
||||
|
||||
var options = _.pick(req.body, 'domain', 'size', 'region');
|
||||
if (Object.keys(options).length === 0) return next(new HttpError(400, 'no migrate option provided'));
|
||||
|
||||
cloudron.migrate(req.body, function (error) { // pass req.body because 'domain' can have arbitrary options
|
||||
cloudron.migrate(options, function (error) {
|
||||
if (error && error.reason === CloudronError.BAD_STATE) return next(new HttpError(409, error.message));
|
||||
if (error && error.reason === CloudronError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(202, {}));
|
||||
|
||||
+13
-8
@@ -8,6 +8,7 @@ exports = module.exports = {
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
groups = require('../groups.js'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
||||
user = require('../user.js'),
|
||||
@@ -22,14 +23,18 @@ function auditSource(req) {
|
||||
function get(req, res, next) {
|
||||
assert.strictEqual(typeof req.user, 'object');
|
||||
|
||||
next(new HttpSuccess(200, {
|
||||
id: req.user.id,
|
||||
username: req.user.username,
|
||||
email: req.user.email,
|
||||
admin: req.user.admin,
|
||||
displayName: req.user.displayName,
|
||||
showTutorial: req.user.showTutorial
|
||||
}));
|
||||
groups.isMember(groups.ADMIN_GROUP_ID, req.user.id, function (error, isAdmin) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, {
|
||||
id: req.user.id,
|
||||
username: req.user.username,
|
||||
email: req.user.email,
|
||||
admin: isAdmin,
|
||||
displayName: req.user.displayName,
|
||||
showTutorial: req.user.showTutorial
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
function update(req, res, next) {
|
||||
|
||||
@@ -46,7 +46,7 @@ function update(req, res, next) {
|
||||
function retire(req, res, next) {
|
||||
debug('triggering retire');
|
||||
|
||||
cloudron.retire('migrate', { }, function (error) {
|
||||
cloudron.retire(function (error) {
|
||||
if (error) console.error('Retire failed.', error);
|
||||
});
|
||||
|
||||
|
||||
@@ -150,7 +150,7 @@ function checkRedis(containerId, done) {
|
||||
}
|
||||
|
||||
describe('Apps', function () {
|
||||
this.timeout(100000);
|
||||
this.timeout(50000);
|
||||
|
||||
var dockerProxy;
|
||||
var imageDeleted = false;
|
||||
|
||||
@@ -325,12 +325,12 @@ describe('Cloudron', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds without size', function (done) {
|
||||
it('fails with missing size', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
|
||||
.send({ region: 'sfo', password: PASSWORD })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(202);
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -345,12 +345,12 @@ describe('Cloudron', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds without region', function (done) {
|
||||
it('fails with missing region', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
|
||||
.send({ size: 'small', password: PASSWORD })
|
||||
.query({ access_token: token })
|
||||
.end(function (error, result) {
|
||||
expect(result.statusCode).to.equal(202);
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,7 +20,7 @@ if [[ "${BOX_ENV}" != "cloudron" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
"${BOX_SRC_DIR}/setup/splashpage.sh" --retire-reason "$1" --retire-info "$2" --data "$3" # show splash
|
||||
"${BOX_SRC_DIR}/setup/splashpage.sh" --retire "$1" --data "$2" # show splash
|
||||
|
||||
echo "Stopping apps"
|
||||
systemctl stop docker # stop the apps
|
||||
|
||||
+7
-54
@@ -1,3 +1,5 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
@@ -49,15 +51,10 @@ var assert = require('assert'),
|
||||
config = require('./config.js'),
|
||||
CronJob = require('cron').CronJob,
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
debug = require('debug')('box:settings'),
|
||||
dns = require('native-dns'),
|
||||
moment = require('moment-timezone'),
|
||||
paths = require('./paths.js'),
|
||||
route53 = require('./dns/route53.js'),
|
||||
safe = require('safetydance'),
|
||||
settingsdb = require('./settingsdb.js'),
|
||||
SubdomainError = require('./subdomains.js').SubdomainError,
|
||||
sysinfo = require('./sysinfo.js'),
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
|
||||
@@ -247,48 +244,11 @@ function getDnsConfig(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function validateRoute53Config(domain, dnsConfig, callback) {
|
||||
const zoneName = domain;
|
||||
|
||||
if (process.env.BOX_ENV === 'test') return callback();
|
||||
|
||||
sysinfo.getIp(function (error, ip) {
|
||||
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, 'Error getting IP:' + error.message));
|
||||
|
||||
dns.resolveNs(zoneName, function (error, nameservers) {
|
||||
if (error || !nameservers) return callback(error || new Error('Unable to get nameservers'));
|
||||
|
||||
route53.getHostedZone(dnsConfig, zoneName, function (error, zone) {
|
||||
if (error && error.reason === SubdomainError.ACCESS_DENIED) return callback(new SettingsError(SettingsError.BAD_FIELD, 'Error getting zone information: Access denied'));
|
||||
if (error && error.reason === SubdomainError.NOT_FOUND) return callback(new SettingsError(SettingsError.BAD_FIELD, 'Zone not found'));
|
||||
if (error && error.reason === SubdomainError.EXTERNAL_ERROR) return callback(new SettingsError(SettingsError.BAD_FIELD, 'Error getting zone information:' + error.message));
|
||||
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
|
||||
|
||||
if (!_.isEqual(zone.DelegationSet.NameServers.sort(), nameservers.sort())) {
|
||||
debug('validateRoute53Config: %j and %j do not match', nameservers, zone.DelegationSet.NameServers);
|
||||
return callback(new Error('domain nameservers are not set to route53'));
|
||||
}
|
||||
|
||||
route53.add(dnsConfig, zoneName, 'my', 'A', [ ip ], function (error, changeId) {
|
||||
if (error && error.reason === SubdomainError.ACCESS_DENIED) return callback(new SettingsError(SettingsError.BAD_FIELD, 'Error adding A record. Access denied'));
|
||||
if (error && error.reason === SubdomainError.NOT_FOUND) return callback(new SettingsError(SettingsError.BAD_FIELD, 'Zone not found'));
|
||||
if (error && error.reason === SubdomainError.EXTERNAL_ERROR) return callback(new SettingsError(SettingsError.BAD_FIELD, 'Error adding A record:' + error.message));
|
||||
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
|
||||
|
||||
debug('validateRoute53Config: A record added with change id %s', changeId);
|
||||
|
||||
callback();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setDnsConfig(dnsConfig, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var credentials, validator;
|
||||
var credentials;
|
||||
|
||||
if (dnsConfig.provider === 'route53') {
|
||||
if (typeof dnsConfig.accessKeyId !== 'string') return callback(new SettingsError(SettingsError.BAD_FIELD, 'accessKeyId must be a string'));
|
||||
@@ -301,27 +261,20 @@ function setDnsConfig(dnsConfig, callback) {
|
||||
region: dnsConfig.region || 'us-east-1',
|
||||
endpoint: dnsConfig.endpoint || null
|
||||
};
|
||||
|
||||
validator = validateRoute53Config.bind(null, dnsConfig.domain || config.fqdn());
|
||||
} else if (dnsConfig.provider === 'caas') {
|
||||
credentials = {
|
||||
provider: dnsConfig.provider
|
||||
};
|
||||
validator = function (caasConfig, next) { return next(); };
|
||||
} else {
|
||||
return callback(new SettingsError(SettingsError.BAD_FIELD, 'provider must be route53 or caas'));
|
||||
}
|
||||
|
||||
validator(credentials, function (error) {
|
||||
if (error) return callback(error);
|
||||
settingsdb.set(exports.DNS_CONFIG_KEY, JSON.stringify(credentials), function (error) {
|
||||
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
|
||||
|
||||
settingsdb.set(exports.DNS_CONFIG_KEY, JSON.stringify(credentials), function (error) {
|
||||
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
|
||||
exports.events.emit(exports.DNS_CONFIG_KEY, dnsConfig);
|
||||
|
||||
exports.events.emit(exports.DNS_CONFIG_KEY, dnsConfig);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -131,7 +131,7 @@ function status(changeId, callback) {
|
||||
if (error) return callback(new SubdomainError(SubdomainError.INTERNAL_ERROR, error));
|
||||
|
||||
api(dnsConfig.provider).getChangeStatus(dnsConfig, changeId, function (error, status) {
|
||||
if (error) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error.message));
|
||||
if (error) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error));
|
||||
callback(null, status === 'INSYNC' ? 'done' : 'pending');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -34,7 +34,7 @@ describe('database', function () {
|
||||
database._clear(done);
|
||||
});
|
||||
|
||||
describe('user', function () {
|
||||
describe('userdb', function () {
|
||||
var USER_0 = {
|
||||
id: 'uuid0',
|
||||
username: 'uuid0',
|
||||
@@ -553,8 +553,7 @@ describe('database', function () {
|
||||
lastBackupId: null,
|
||||
oldConfig: null,
|
||||
memoryLimit: 4294967296,
|
||||
altDomain: null,
|
||||
xFrameOptions: 'DENY'
|
||||
altDomain: null
|
||||
};
|
||||
var APP_1 = {
|
||||
id: 'appid-1',
|
||||
@@ -573,8 +572,7 @@ describe('database', function () {
|
||||
lastBackupId: null,
|
||||
oldConfig: null,
|
||||
memoryLimit: 0,
|
||||
altDomain: null,
|
||||
xFrameOptions: ''
|
||||
altDomain: null
|
||||
};
|
||||
|
||||
it('add fails due to missing arguments', function () {
|
||||
|
||||
@@ -235,15 +235,6 @@ describe('Server', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('does not crash for malformed origin', function (done) {
|
||||
superagent('OPTIONS', SERVER_URL + '/api/v1/cloudron/status')
|
||||
.set('Origin', 'foobar')
|
||||
.end(function (error, res) {
|
||||
expect(res.statusCode).to.be(405);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
after(function (done) {
|
||||
server.stop(function () {
|
||||
done();
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ webadmin_scopes="cloudron,profile,users,apps,settings"
|
||||
webadmin_origin="https://${ADMIN_LOCATION}-localhost"
|
||||
|
||||
# create docker network (while the infra code does this, most tests skip infra setup)
|
||||
docker network create --subnet=172.18.0.0/16 cloudron || true
|
||||
docker network create cloudron --subnet=172.18.0.0/16 || true
|
||||
|
||||
# !!!!!! check clientdb.js clear() to not nuke those entries
|
||||
echo "Add webadmin api client"
|
||||
|
||||
@@ -7,9 +7,7 @@
|
||||
|
||||
var async = require('async'),
|
||||
database = require('../database.js'),
|
||||
constants = require('../constants.js'),
|
||||
expect = require('expect.js'),
|
||||
fs = require('fs'),
|
||||
groupdb = require('../groupdb.js'),
|
||||
groups = require('../groups.js'),
|
||||
mailer = require('../mailer.js'),
|
||||
@@ -295,62 +293,6 @@ describe('User', function () {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails for ghost if not enabled', function (done) {
|
||||
user.verify(userObject.id, 'foobar', function (error) {
|
||||
expect(error).to.be.a(UserError);
|
||||
expect(error.reason).to.equal(UserError.WRONG_PASSWORD);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails for ghost with wrong password', function (done) {
|
||||
var ghost = { };
|
||||
ghost[userObject.username] = 'testpassword';
|
||||
fs.writeFileSync(constants.GHOST_USER_FILE, JSON.stringify(ghost), 'utf8');
|
||||
|
||||
user.verify(userObject.id, 'foobar', function (error) {
|
||||
fs.unlinkSync(constants.GHOST_USER_FILE);
|
||||
|
||||
expect(error).to.be.a(UserError);
|
||||
expect(error.reason).to.equal(UserError.WRONG_PASSWORD);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds for ghost', function (done) {
|
||||
var ghost = { };
|
||||
ghost[userObject.username] = 'testpassword';
|
||||
fs.writeFileSync(constants.GHOST_USER_FILE, JSON.stringify(ghost), 'utf8');
|
||||
|
||||
user.verify(userObject.id, 'testpassword', function (error, result) {
|
||||
fs.unlinkSync(constants.GHOST_USER_FILE);
|
||||
|
||||
expect(error).to.equal(null);
|
||||
expect(result.id).to.equal(userObject.id);
|
||||
expect(result.username).to.equal(userObject.username);
|
||||
expect(result.email).to.equal(userObject.email);
|
||||
expect(result.displayName).to.equal(userObject.displayName);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds for normal user password when ghost file exists', function (done) {
|
||||
var ghost = { };
|
||||
ghost[userObject.username] = 'testpassword';
|
||||
fs.writeFileSync(constants.GHOST_USER_FILE, JSON.stringify(ghost), 'utf8');
|
||||
|
||||
user.verify(userObject.id, PASSWORD, function (error, result) {
|
||||
fs.unlinkSync(constants.GHOST_USER_FILE);
|
||||
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result).to.be.ok();
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
describe('verifyWithUsername', function () {
|
||||
@@ -404,40 +346,6 @@ describe('User', function () {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails for ghost with wrong password', function (done) {
|
||||
var ghost = { };
|
||||
ghost[userObject.username] = 'testpassword';
|
||||
|
||||
fs.writeFileSync(constants.GHOST_USER_FILE, JSON.stringify(ghost), 'utf8');
|
||||
|
||||
user.verifyWithUsername(USERNAME, 'foobar', function (error) {
|
||||
fs.unlinkSync(constants.GHOST_USER_FILE);
|
||||
|
||||
expect(error).to.be.a(UserError);
|
||||
expect(error.reason).to.equal(UserError.WRONG_PASSWORD);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds for ghost', function (done) {
|
||||
var ghost = { };
|
||||
ghost[userObject.username] = 'testpassword';
|
||||
|
||||
fs.writeFileSync(constants.GHOST_USER_FILE, JSON.stringify(ghost), 'utf8');
|
||||
|
||||
user.verifyWithUsername(USERNAME, 'testpassword', function (error, result) {
|
||||
fs.unlinkSync(constants.GHOST_USER_FILE);
|
||||
|
||||
expect(error).to.equal(null);
|
||||
expect(result.id).to.equal(userObject.id);
|
||||
expect(result.username).to.equal(userObject.username);
|
||||
expect(result.email).to.equal(userObject.email);
|
||||
expect(result.displayName).to.equal(userObject.displayName);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('verifyWithEmail', function () {
|
||||
@@ -491,40 +399,6 @@ describe('User', function () {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails for ghost with wrong password', function (done) {
|
||||
var ghost = { };
|
||||
ghost[userObject.username] = 'testpassword';
|
||||
|
||||
fs.writeFileSync(constants.GHOST_USER_FILE, JSON.stringify(ghost), 'utf8');
|
||||
|
||||
user.verifyWithEmail(EMAIL, 'foobar', function (error) {
|
||||
fs.unlinkSync(constants.GHOST_USER_FILE);
|
||||
|
||||
expect(error).to.be.a(UserError);
|
||||
expect(error.reason).to.equal(UserError.WRONG_PASSWORD);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds for ghost', function (done) {
|
||||
var ghost = { };
|
||||
ghost[userObject.username] = 'testpassword';
|
||||
|
||||
fs.writeFileSync(constants.GHOST_USER_FILE, JSON.stringify(ghost), 'utf8');
|
||||
|
||||
user.verifyWithEmail(EMAIL, 'testpassword', function (error, result) {
|
||||
fs.unlinkSync(constants.GHOST_USER_FILE);
|
||||
|
||||
expect(error).to.equal(null);
|
||||
expect(result.id).to.equal(userObject.id);
|
||||
expect(result.username).to.equal(userObject.username);
|
||||
expect(result.email).to.equal(userObject.email);
|
||||
expect(result.displayName).to.equal(userObject.displayName);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('retrieving', function () {
|
||||
|
||||
-20
@@ -26,7 +26,6 @@ exports = module.exports = {
|
||||
var assert = require('assert'),
|
||||
clients = require('./clients.js'),
|
||||
crypto = require('crypto'),
|
||||
constants = require('./constants.js'),
|
||||
debug = require('debug')('box:user'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
@@ -35,7 +34,6 @@ var assert = require('assert'),
|
||||
hat = require('hat'),
|
||||
mailer = require('./mailer.js'),
|
||||
mailboxes = require('./mailboxes.js'),
|
||||
safe = require('safetydance'),
|
||||
tokendb = require('./tokendb.js'),
|
||||
userdb = require('./userdb.js'),
|
||||
util = require('util'),
|
||||
@@ -191,22 +189,6 @@ function createUser(username, password, email, displayName, auditSource, options
|
||||
});
|
||||
}
|
||||
|
||||
// returns true if ghost user was matched
|
||||
function verifyGhost(username, password) {
|
||||
assert.strictEqual(typeof username, 'string');
|
||||
assert.strictEqual(typeof password, 'string');
|
||||
|
||||
var ghostData = safe.require(constants.GHOST_USER_FILE);
|
||||
if (!ghostData) return false;
|
||||
|
||||
if (username in ghostData && ghostData[username] === password) {
|
||||
debug('verifyGhost: matched ghost user');
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function verify(userId, password, callback) {
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert.strictEqual(typeof password, 'string');
|
||||
@@ -216,8 +198,6 @@ function verify(userId, password, callback) {
|
||||
if (error && error.reason == DatabaseError.NOT_FOUND) return callback(new UserError(UserError.NOT_FOUND));
|
||||
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
|
||||
|
||||
if (verifyGhost(user.username, password)) return callback(null, user);
|
||||
|
||||
var saltBinary = new Buffer(user.salt, 'hex');
|
||||
crypto.pbkdf2(password, saltBinary, CRYPTO_ITERATIONS, CRYPTO_KEY_LENGTH, function (error, derivedKey) {
|
||||
if (error) return callback(new UserError(UserError.INTERNAL_ERROR, error));
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -40,9 +40,6 @@
|
||||
<script src="3rdparty/js/angular-ui-notification.min.js"></script>
|
||||
<script src="3rdparty/js/autofill-event.js"></script>
|
||||
|
||||
<!-- Angular directives for bootstrap https://angular-ui.github.io/bootstrap/ -->
|
||||
<script src="3rdparty/js/ui-bootstrap-tpls-1.3.3.min.js"></script>
|
||||
|
||||
<script src="3rdparty/js/Chart.js"></script>
|
||||
<script src="3rdparty/js/ansi_up.js"></script>
|
||||
|
||||
@@ -188,7 +185,7 @@
|
||||
<li><a href="#/account"><i class="fa fa-user fa-fw"></i> Account</a></li>
|
||||
<li ng-show="user.admin"><a href="#/graphs"><i class="fa fa-bar-chart fa-fw"></i> Graphs</a></li>
|
||||
<li ng-show="user.admin"><a href="#/activity"><i class="fa fa-list-alt fa-fw"></i> Activity</a></li>
|
||||
<li ng-show="user.admin"><a href="#/certs"><i class="fa fa-certificate fa-fw"></i> Domain & Certs</a></li>
|
||||
<li ng-show="user.admin && config.isCustomDomain"><a href="#/certs"><i class="fa fa-certificate fa-fw"></i> DNS & Certs</a></li>
|
||||
<li ng-show="user.admin"><a href="#/tokens"><i class="fa fa-key fa-fw"></i> API Access</a></li>
|
||||
<li ng-show="user.admin"><a href="#/settings"><i class="fa fa-wrench fa-fw"></i> Settings</a></li>
|
||||
<li class="divider"></li>
|
||||
|
||||
@@ -295,8 +295,7 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
|
||||
cert: config.cert,
|
||||
key: config.key,
|
||||
memoryLimit: config.memoryLimit,
|
||||
altDomain: config.altDomain || null,
|
||||
xFrameOptions: config.xFrameOptions
|
||||
altDomain: config.altDomain || null
|
||||
};
|
||||
|
||||
$http.post(client.apiOrigin + '/api/v1/apps/' + id + '/configure', data).success(function (data, status) {
|
||||
|
||||
@@ -9,7 +9,7 @@ if (search.accessToken) localStorage.token = search.accessToken;
|
||||
|
||||
|
||||
// create main application module
|
||||
var app = angular.module('Application', ['ngRoute', 'ngAnimate', 'ngSanitize', 'angular-md5', 'slick', 'ui-notification', 'ui.bootstrap', 'ui.bootstrap-slider']);
|
||||
var app = angular.module('Application', ['ngRoute', 'ngAnimate', 'ngSanitize', 'angular-md5', 'slick', 'ui-notification', 'ui.bootstrap-slider']);
|
||||
|
||||
app.config(['NotificationProvider', function (NotificationProvider) {
|
||||
NotificationProvider.setOptions({
|
||||
@@ -194,7 +194,7 @@ app.filter('prettyDate', function () {
|
||||
diff = (((new Date()).getTime() - date.getTime()) / 1000) + 30, // add 30seconds for clock skew
|
||||
day_diff = Math.floor(diff / 86400);
|
||||
|
||||
if (isNaN(day_diff) || day_diff < 0)
|
||||
if (isNaN(day_diff) || day_diff < 0 || day_diff >= 31)
|
||||
return;
|
||||
|
||||
return day_diff === 0 && (
|
||||
@@ -205,9 +205,7 @@ app.filter('prettyDate', function () {
|
||||
diff < 86400 && Math.floor( diff / 3600 ) + ' hours ago') ||
|
||||
day_diff === 1 && 'Yesterday' ||
|
||||
day_diff < 7 && day_diff + ' days ago' ||
|
||||
day_diff < 31 && Math.ceil( day_diff / 7 ) + ' weeks ago' ||
|
||||
day_diff < 365 && Math.round( day_diff / 30 ) + ' months ago' ||
|
||||
Math.round( day_diff / 365 ) + ' years ago';
|
||||
day_diff < 31 && Math.ceil( day_diff / 7 ) + ' weeks ago';
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ app.controller('Controller', ['$scope', '$http', '$interval', function ($scope,
|
||||
function fetchProgress() {
|
||||
$http.get('/api/v1/cloudron/progress').success(function(data, status) {
|
||||
if (status === 404) return; // just wait until we create the progress.json on the server side
|
||||
if (status !== 200 || typeof data !== 'object') return console.error('Invalid response for progress', status, data);
|
||||
if (status !== 200 || typeof data !== 'object') return console.error(status, data);
|
||||
if (!data.update && !data.migrate) return $scope.loadWebadmin();
|
||||
|
||||
if (data.update) {
|
||||
@@ -38,18 +38,10 @@ app.controller('Controller', ['$scope', '$http', '$interval', function ($scope,
|
||||
$scope.title = 'Migration in progress...';
|
||||
$scope.percent = data.migrate.percent;
|
||||
$scope.message = data.migrate.message;
|
||||
|
||||
if (!data.migrate.info) return;
|
||||
|
||||
// check if the new domain is available via the appstore (cannot use cloudron
|
||||
// directly as we might hit NXDOMAIN)
|
||||
$http.get(data.apiServerOrigin + '/api/v1/boxes/' + data.migrate.info.domain + '/status').success(function(data2, status) {
|
||||
if (status === 200 && data2.status === 'ready') return window.location = 'https://my.' + data.migrate.info.domain;
|
||||
});
|
||||
}
|
||||
}
|
||||
}).error(function (data, status) {
|
||||
console.error('Error getting progress', status, data);
|
||||
console.error(status, data);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ $navbar-default-link-active-color: #62bdfc !default;
|
||||
$navbar-default-brand-color: #777 !default;
|
||||
|
||||
$btn-default-bg: transparent !default;
|
||||
$btn-default-color: #444 !default;
|
||||
$btn-default-color: #aaa !default;
|
||||
$btn-default-border: #aaa !default;
|
||||
|
||||
//$btn-primary-bg: $btn-default-bg;
|
||||
@@ -141,10 +141,6 @@ html {
|
||||
padding: 15px 0;
|
||||
}
|
||||
|
||||
.radio {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// Apps view
|
||||
// ----------------------------
|
||||
@@ -620,10 +616,6 @@ footer a {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
|
||||
// ----------------------------
|
||||
// Upgrade
|
||||
|
||||
@@ -30,13 +30,13 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col-md-2">Time</th>
|
||||
<th class="col-md-3">Source</th>
|
||||
<th class="col-md-7">Action</th>
|
||||
<th class="col-md-2">Source</th>
|
||||
<th class="col-md-6">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="eventLog in eventLogs">
|
||||
<td><span uib-tooltip="{{eventLog.creationTime}}" class="arrow">{{ eventLog.creationTime | prettyDate }}</span></td>
|
||||
<th scope="row">{{ eventLog.creationTime | prettyDate }}</td>
|
||||
<td>{{ eventLog.source.username || eventLog.source.userId || eventLog.source.authType }} <span ng-show="eventLog.source.ip || eventLog.source.appId"> ({{ eventLog.source.ip || eventLog.source.appId }}) </span> </td>
|
||||
<td>{{ eventLog | eventLogDetails }}</td>
|
||||
</tr>
|
||||
|
||||
@@ -63,9 +63,6 @@
|
||||
</div>
|
||||
</ng-form>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="form-group" ng-show="appConfigure.app.manifest.singleUser">
|
||||
<label class="control-label">User</label>
|
||||
<p>
|
||||
@@ -96,49 +93,39 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-hide="true">
|
||||
<label class="control-label" for="memoryUsage">Maximum Memory Usage: <b>{{ appConfigure.memoryUsage / 1024 / 1024 }} MB</b></label>
|
||||
<br/>
|
||||
<div style="padding: 0 10px;">
|
||||
<slider id="memoryUsage" ng-model="appConfigure.memoryUsage" step="33554432" tooltip="hide" ticks="memoryTicks" ticks-snap-bounds="67108864"></slider>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hide">
|
||||
<label class="control-label" for="appConfigureCertificateInput" ng-show="config.isCustomDomain">Certificate (optional)</label>
|
||||
<div class="has-error text-center" ng-show="appConfigure.error.cert && config.isCustomDomain">{{ appConfigure.error.cert }}</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': !appConfigureForm.certificate.$dirty && appConfigure.error.cert }" ng-show="config.isCustomDomain">
|
||||
<div class="input-group">
|
||||
<input type="file" id="appConfigureCertificateFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Certificate" ng-model="appConfigure.certificateFileName" id="appConfigureCertificateInput" name="certificate" onclick="getElementById('appConfigureCertificateFileInput').click();" style="cursor: pointer;" ng-required="appConfigure.keyFileName">
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('appConfigureCertificateFileInput').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': !appConfigureForm.key.$dirty && appConfigure.error.cert }" ng-show="config.isCustomDomain">
|
||||
<div class="input-group">
|
||||
<input type="file" id="appConfigureKeyFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Key" ng-model="appConfigure.keyFileName" id="appConfigureKeyInput" name="key" onclick="getElementById('appConfigureKeyFileInput').click();" style="cursor: pointer;" ng-required="appConfigure.certificateFileName">
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('appConfigureKeyFileInput').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a ng-show="!!appConfigure.app.manifest.configurePath" ng-href="https://{{ appConfigure.app.fqdn }}/{{ appConfigure.app.manifest.configurePath }}" target="_blank">Application Specific Settings</a>
|
||||
<br/>
|
||||
|
||||
<!-- <label class="control-label hand" data-toggle="collapse" data-target="#appConfigureCollapseAdvanced">Show advanced</label> -->
|
||||
<!-- <div class="collapse" id="appConfigureCollapseAdvanced"> -->
|
||||
<div class="form-group" ng-hide="true">
|
||||
<label class="control-label" for="memoryUsage">Maximum Memory Usage: <b>{{ appConfigure.memoryUsage / 1024 / 1024 }} MB</b></label>
|
||||
<br/>
|
||||
<div style="padding: 0 10px;">
|
||||
<slider id="memoryUsage" ng-model="appConfigure.memoryUsage" step="33554432" tooltip="hide" ticks="memoryTicks" ticks-snap-bounds="67108864"></slider>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': !appConfigureForm.xFrameOptions.$dirty && appConfigure.error.xFrameOptions }">
|
||||
<label class="control-label">Allow site embedding from origin</label>
|
||||
<div class="control-label" ng-show="appConfigure.error.xFrameOptions"><small>Must be empty of a valid URL</small></div>
|
||||
<input type="text" class="form-control" id="appConfigureXFrameOptionsInput" name="xFrameOptions" placeholder="https://example.com" ng-model="appConfigure.xFrameOptions" uib-tooltip="Leave blank to not allow embedding">
|
||||
</div>
|
||||
|
||||
<div class="hide">
|
||||
<label class="control-label" for="appConfigureCertificateInput" ng-show="config.isCustomDomain">Certificate (optional)</label>
|
||||
<div class="has-error text-center" ng-show="appConfigure.error.cert && config.isCustomDomain">{{ appConfigure.error.cert }}</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': !appConfigureForm.certificate.$dirty && appConfigure.error.cert }" ng-show="config.isCustomDomain">
|
||||
<div class="input-group">
|
||||
<input type="file" id="appConfigureCertificateFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Certificate" ng-model="appConfigure.certificateFileName" id="appConfigureCertificateInput" name="certificate" onclick="getElementById('appConfigureCertificateFileInput').click();" style="cursor: pointer;" ng-required="appConfigure.keyFileName">
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('appConfigureCertificateFileInput').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': !appConfigureForm.key.$dirty && appConfigure.error.cert }" ng-show="config.isCustomDomain">
|
||||
<div class="input-group">
|
||||
<input type="file" id="appConfigureKeyFileInput" style="display:none"/>
|
||||
<input type="text" class="form-control" placeholder="Key" ng-model="appConfigure.keyFileName" id="appConfigureKeyInput" name="key" onclick="getElementById('appConfigureKeyFileInput').click();" style="cursor: pointer;" ng-required="appConfigure.certificateFileName">
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('appConfigureKeyFileInput').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- </div> -->
|
||||
|
||||
<br/>
|
||||
<div class="form-group" ng-class="{ 'has-error': (appConfigureForm.password.$dirty && appConfigureForm.password.$invalid) || (!appConfigureForm.password.$dirty && appConfigure.error.password) }">
|
||||
<label class="control-label" for="appConfigurePasswordInput">Provide your password to confirm this action</label>
|
||||
|
||||
@@ -35,7 +35,6 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
memoryLimit: $scope.memoryTicks[0],
|
||||
accessRestrictionOption: '',
|
||||
accessRestriction: { users: [], groups: [] },
|
||||
xFrameOptions: '',
|
||||
|
||||
isAccessRestrictionValid: function () {
|
||||
var tmp = $scope.appConfigure.accessRestriction;
|
||||
@@ -91,7 +90,6 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
$scope.appConfigure.memoryLimit = $scope.memoryTicks[0];
|
||||
$scope.appConfigure.accessRestrictionOption = '';
|
||||
$scope.appConfigure.accessRestriction = { users: [], groups: [] };
|
||||
$scope.appConfigure.xFrameOptions = '';
|
||||
|
||||
$scope.appConfigureForm.$setPristine();
|
||||
$scope.appConfigureForm.$setUntouched();
|
||||
@@ -180,7 +178,6 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
$scope.appConfigure.accessRestrictionOption = app.accessRestriction ? 'restricted' : '';
|
||||
$scope.appConfigure.accessRestriction = app.accessRestriction || { users: [], groups: [] };
|
||||
$scope.appConfigure.memoryUsage = app.memoryUsage || 256;
|
||||
$scope.appConfigure.xFrameOptions = app.xFrameOptions.indexOf('ALLOW-FROM') === 0 ? app.xFrameOptions.split(' ')[1] : '';
|
||||
|
||||
// fill the portBinding structures. There might be holes in the app.portBindings, which signalizes a disabled port
|
||||
for (var env in $scope.appConfigure.portBindingsInfo) {
|
||||
@@ -201,7 +198,6 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
$scope.appConfigure.error.other = null;
|
||||
$scope.appConfigure.error.location = null;
|
||||
$scope.appConfigure.error.password = null;
|
||||
$scope.appConfigure.error.xFrameOptions = null;
|
||||
|
||||
// only use enabled ports from portBindings
|
||||
var finalPortBindings = {};
|
||||
@@ -217,8 +213,7 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
portBindings: finalPortBindings,
|
||||
accessRestriction: !$scope.appConfigure.accessRestrictionOption ? null : $scope.appConfigure.accessRestriction,
|
||||
cert: $scope.appConfigure.certificateFile,
|
||||
key: $scope.appConfigure.keyFile,
|
||||
xFrameOptions: $scope.appConfigure.xFrameOptions ? ('ALLOW-FROM ' + $scope.appConfigure.xFrameOptions) : 'SAMEORIGIN'
|
||||
key: $scope.appConfigure.keyFile
|
||||
// memoryLimit: $scope.appConfigure.memoryLimit
|
||||
};
|
||||
|
||||
@@ -241,10 +236,6 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
$scope.appConfigure.certificateFile = null;
|
||||
$scope.appConfigure.keyFileName = '';
|
||||
$scope.appConfigure.keyFile = null;
|
||||
} else if (error.statusCode === 400 && error.message.indexOf('xFrameOptions') !== -1 ) {
|
||||
$scope.appConfigure.error.xFrameOptions = error.message;
|
||||
$scope.appConfigureForm.xFrameOptions.$setPristine();
|
||||
$('#appConfigureXFrameOptionsInput').focus();
|
||||
} else {
|
||||
$scope.appConfigure.error.other = error.message;
|
||||
}
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
<img ng-src="{{appInstall.app.iconUrl}}" onerror="this.onerror=null;this.src='img/appicon_fallback.png'" class="app-icon"/>
|
||||
<h3 class="appstore-install-title" title="Version {{ appInstall.app.manifest.version }}">{{ appInstall.app.manifest.title }} <span class="badge badge-danger" ng-show="appInstall.app.publishState === 'testing'">Testing</span></h3>
|
||||
<br/>
|
||||
<span class="appstore-install-meta"><a href="{{ appInstall.app.manifest.website }}" target="_blank">{{ appInstall.app.manifest.author }}</a></span>
|
||||
<span class="appstore-install-meta">{{ appInstall.app.manifest.author }}</span>
|
||||
<br/>
|
||||
<span class="appstore-install-meta">Last updated {{ appInstall.app.creationDate | prettyDate }}</span>
|
||||
<span class="appstore-install-meta"><a href="{{ appInstall.app.manifest.website }}" target="_blank">Website</a></span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="collapse" id="collapseInstallForm" data-toggle="false">
|
||||
|
||||
@@ -1,18 +1,40 @@
|
||||
<div class="modal fade" id="dnsCredentialsModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Configure DNS</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="dnsCredentialsForm" role="form" novalidate ng-submit="setDnsCredentials()" autocomplete="off">
|
||||
<fieldset>
|
||||
<p class="has-error text-center" ng-show="dnsCredentials.error">{{ dnsCredentials.error }}</p>
|
||||
<div class="section-header">
|
||||
<div class="text-left">
|
||||
<h1>DNS & Certs</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': false }">
|
||||
<label class="control-label" for="customDomainId">Domain Name</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.customDomain" id="customDomainId" name="customDomainId" ng-disabled="dnsCredentials.busy" ng-minlength="4" ng-maxlength="128" placeholder="example.com" required autofocus>
|
||||
</div>
|
||||
<div class="section-header">
|
||||
<div class="text-left">
|
||||
<h3>DNS Credentials</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p>Currently only Amazon <a href="https://aws.amazon.com/route53/">Route53</a> is supported. Let us know if you require a different DNS provider <a href="#/support">here</a>.</p>
|
||||
|
||||
<table width="100%">
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">Access Key Id</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ dnsConfig.accessKeyId }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">Secret Access Key</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;"><i>hidden</i></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;"></td>
|
||||
<td class="text-right" style="vertical-align: top;"><span class="text-success" ng-show="dnsCredentials.success"><b>Done</b></span> <button class="btn btn-outline btn-xs btn-primary" ng-show="!dnsCredentials.formVisible" ng-click="showDnsCredentialsForm()">Change</button></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="collapse" id="collapseDnsCredentialsForm" data-toggle="false">
|
||||
<p>The security credentials have to be valid for full Route53 access.</p>
|
||||
<form name="dnsCredentialsForm" ng-submit="setDnsCredentials()">
|
||||
<fieldset>
|
||||
<div class="has-error text-center" ng-show="dnsCredentials.error">{{ dnsCredentials.error }}</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': false }">
|
||||
<label class="control-label" for="dnsCredentialsAccessKeyId">Access Key Id</label>
|
||||
@@ -22,68 +44,13 @@
|
||||
<label class="control-label" for="dnsCredentialsSecretAccessKey">Secret Access Key</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.secretAccessKey" id="dnsCredentialsSecretAccessKey" name="secretAccessKey" ng-disabled="dnsCredentials.busy" required>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-if="config.fqdn !== dnsCredentials.customDomain">
|
||||
<label class="control-label" for="dnsCredentialsPassword">Provide your password to confirm this action</label>
|
||||
<input type="password" class="form-control" ng-model="dnsCredentials.password" id="dnsCredentialsPassword" name="password" ng-disabled="dnsCredentials.busy" required>
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="dnsCredentialsForm.$invalid"/>
|
||||
<a href="" class="pull-left" ng-click="hideDnsCredentialsForm()">Collapse</a>
|
||||
|
||||
<button type="submit" class="btn btn-outline btn-success pull-right" ng-disabled="dnsCredentialsForm.$invalid || busy"><i class="fa fa-spinner fa-pulse" ng-show="dnsCredentials.busy"></i> Save</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer ">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="submit" class="btn btn-outline btn-success pull-right" ng-click="setDnsCredentials()"
|
||||
ng-disabled="dnsCredentialsForm.$invalid || busy"><i class="fa fa-spinner fa-pulse" ng-show="dnsCredentials.busy"></i>
|
||||
<span ng-show="dnsCredentials.customDomain === config.fqdn">Save</span>
|
||||
<span ng-show="dnsCredentials.customDomain !== config.fqdn">Change Domain</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-header">
|
||||
<div class="text-left">
|
||||
<h1>Domain & Certificates</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-header">
|
||||
<div class="text-left">
|
||||
<h3>Domain</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p ng-show="!config.isCustomDomain">To use a custom domain, configure your domain to use <a target="_blank" href="https://aws.amazon.com/route53/">Route53.</a> Moving to a custom domain will retain all your apps and data and will take around 15 minutes.</p>
|
||||
|
||||
<table width="100%">
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">Domain Name</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ config.fqdn }}</td>
|
||||
</tr>
|
||||
|
||||
<tr ng-show="config.isCustomDomain">
|
||||
<td class="text-muted" style="vertical-align: top;">Access Key Id</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ dnsConfig.accessKeyId || 'unset' }}</td>
|
||||
</tr>
|
||||
<tr ng-show="config.isCustomDomain">
|
||||
<td class="text-muted" style="vertical-align: top;">Secret Access Key</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;"><i>hidden</i></td>
|
||||
</tr>
|
||||
<!-- add some space -->
|
||||
<tr>
|
||||
<td><br/></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;"></td>
|
||||
<td class="text-right" style="vertical-align: top;"><button class="btn btn-outline btn-xs btn-primary" ng-click="showChangeDnsCredentials()">Change</button></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -95,20 +62,14 @@
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;">
|
||||
<div class="row" ng-show="!config.isCustomDomain">
|
||||
<div class="col-md-12">
|
||||
Certificates can only by set for custom domains.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="config.isCustomDomain">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<form name="defaultCertForm" ng-submit="setDefaultCert()">
|
||||
<fieldset>
|
||||
<p>By default, certificates are obtained via <a href="https://letsencrypt.org/" target="_blank">Let’s Encrypt</a>.</p>
|
||||
<p>By default certificates will be obtained via <a href="https://letsencrypt.org/" target="_blank">Let’s Encrypt</a>.</p>
|
||||
<br/>
|
||||
<label class="control-label" for="defaultCertInput">Fallback Certificate</label>
|
||||
<p>A wildcard certificate that will be used for apps, if getting a Let’s Encrypt certificate failed. This might be due to rate limits on Let’s Encrypt side.</p>
|
||||
<p>A wildcard certificate that will be used for apps where getting a Let’s Encrypt certificate failed. This might be due to rate limits on Let’s Encrypt side.</p>
|
||||
<div class="has-error text-center" ng-show="defaultCert.error">{{ defaultCert.error }}</div>
|
||||
<div class="text-success text-center" ng-show="defaultCert.success"><b>Upload successful</b></div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (!defaultCert.cert.$dirty && defaultCert.error) }">
|
||||
|
||||
+18
-44
@@ -1,10 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('Application').controller('CertsController', ['$scope', '$location', 'Client', function ($scope, $location, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().admin) $location.path('/'); });
|
||||
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.dnsConfig = null;
|
||||
Client.onReady(function () { if (!Client.getUserInfo().admin || !Client.getConfig().isCustomDomain) $location.path('/'); });
|
||||
|
||||
$scope.defaultCert = {
|
||||
error: null,
|
||||
@@ -30,11 +27,10 @@ angular.module('Application').controller('CertsController', ['$scope', '$locatio
|
||||
error: null,
|
||||
success: false,
|
||||
busy: false,
|
||||
customDomain: '',
|
||||
formVisible: false,
|
||||
accessKeyId: '',
|
||||
secretAccessKey: '',
|
||||
provider: 'route53',
|
||||
password: ''
|
||||
provider: 'route53'
|
||||
};
|
||||
|
||||
function readFileLocally(obj, file, fileName) {
|
||||
@@ -97,28 +93,23 @@ angular.module('Application').controller('CertsController', ['$scope', '$locatio
|
||||
});
|
||||
};
|
||||
|
||||
$scope.hideDnsCredentialsForm = function () {
|
||||
$('#collapseDnsCredentialsForm').collapse('hide');
|
||||
$scope.dnsCredentials.formVisible = false;
|
||||
};
|
||||
|
||||
$scope.setDnsCredentials = function () {
|
||||
$scope.dnsCredentials.busy = true;
|
||||
$scope.dnsCredentials.error = null;
|
||||
$scope.dnsCredentials.success = false;
|
||||
|
||||
var migrateDomain = $scope.dnsCredentials.customDomain !== $scope.config.fqdn;
|
||||
|
||||
var data = {
|
||||
provider: $scope.dnsCredentials.provider,
|
||||
accessKeyId: $scope.dnsCredentials.accessKeyId,
|
||||
secretAccessKey: $scope.dnsCredentials.secretAccessKey
|
||||
};
|
||||
|
||||
var func;
|
||||
if (migrateDomain) {
|
||||
data.domain = $scope.dnsCredentials.customDomain;
|
||||
func = Client.migrate.bind(Client, data, $scope.dnsCredentials.password);
|
||||
} else {
|
||||
func = Client.setDnsConfig.bind(Client, data);
|
||||
}
|
||||
|
||||
func(function (error) {
|
||||
Client.setDnsConfig(data, function (error) {
|
||||
if (error) {
|
||||
$scope.dnsCredentials.error = error.message;
|
||||
} else {
|
||||
@@ -127,41 +118,31 @@ angular.module('Application').controller('CertsController', ['$scope', '$locatio
|
||||
$scope.dnsConfig.accessKeyId = $scope.dnsCredentials.accessKeyId;
|
||||
$scope.dnsConfig.secretAccessKey = $scope.dnsCredentials.secretAccessKey;
|
||||
|
||||
$('#dnsCredentialsModal').modal('hide');
|
||||
$scope.dnsCredentials.accessKeyId = '';
|
||||
$scope.dnsCredentials.secretAccessKey = '';
|
||||
|
||||
dnsCredentialsReset();
|
||||
$scope.hideDnsCredentialsForm();
|
||||
|
||||
if (migrateDomain) window.location.href = '/update.html';
|
||||
// attempt to reload to make the browser get the new certs
|
||||
window.location.reload(true);
|
||||
}
|
||||
|
||||
$scope.dnsCredentials.busy = false;
|
||||
});
|
||||
};
|
||||
|
||||
function dnsCredentialsReset() {
|
||||
$scope.showDnsCredentialsForm = function () {
|
||||
$scope.dnsCredentials.busy = false;
|
||||
$scope.dnsCredentials.success = false;
|
||||
$scope.dnsCredentials.error = null;
|
||||
|
||||
$scope.dnsCredentials.customDomain = '';
|
||||
$scope.dnsCredentials.accessKeyId = '';
|
||||
$scope.dnsCredentials.secretAccessKey = '';
|
||||
$scope.dnsCredentials.password = '';
|
||||
|
||||
$scope.dnsCredentialsForm.$setPristine();
|
||||
$scope.dnsCredentialsForm.$setUntouched();
|
||||
|
||||
$('#customDomainId').focus();
|
||||
}
|
||||
|
||||
$scope.showChangeDnsCredentials = function () {
|
||||
dnsCredentialsReset();
|
||||
|
||||
$scope.dnsCredentials.customDomain = $scope.dnsConfig.accessKeyId ? $scope.config.fqdn : '';
|
||||
$scope.dnsCredentials.accessKeyId = $scope.dnsConfig.accessKeyId;
|
||||
$scope.dnsCredentials.secretAccessKey = $scope.dnsConfig.secretAccessKey;
|
||||
|
||||
$('#dnsCredentialsModal').modal('show');
|
||||
$scope.dnsCredentials.formVisible = true;
|
||||
$('#collapseDnsCredentialsForm').collapse('show');
|
||||
$('#dnsCredentialsAccessKeyId').focus();
|
||||
};
|
||||
|
||||
Client.onReady(function () {
|
||||
@@ -171,11 +152,4 @@ angular.module('Application').controller('CertsController', ['$scope', '$locatio
|
||||
$scope.dnsConfig = result;
|
||||
});
|
||||
});
|
||||
|
||||
// setup all the dialog focus handling
|
||||
['dnsCredentialsModal'].forEach(function (id) {
|
||||
$('#' + id).on('shown.bs.modal', function () {
|
||||
$(this).find("[autofocus]:first").focus();
|
||||
});
|
||||
});
|
||||
}]);
|
||||
|
||||
@@ -261,7 +261,7 @@
|
||||
<tbody>
|
||||
<tr ng-repeat="user in users">
|
||||
<td>
|
||||
<i class="fa fa-briefcase arrow" ng-show="user.admin" uib-tooltip="This user can manage apps, groups and other users"></i>
|
||||
<i class="fa fa-briefcase" ng-show="user.admin" data-toggle="tooltip" title="This user can manage apps, groups and other users" ng-init="initTooltip()"></i>
|
||||
</td>
|
||||
<td class="hand elide-table-cell" ng-click="showUserEdit(user)">
|
||||
{{ user.username }}
|
||||
|
||||
@@ -391,6 +391,10 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
|
||||
|
||||
refresh();
|
||||
|
||||
$scope.initTooltip = function () {
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
};
|
||||
|
||||
// setup all the dialog focus handling
|
||||
['userAddModal', 'userRemoveModal', 'userEditModal', 'groupAddModal', 'groupRemoveModal'].forEach(function (id) {
|
||||
$('#' + id).on('shown.bs.modal', function () {
|
||||
|
||||
Reference in New Issue
Block a user