Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 22d731f06d | |||
| e3d288ef7d | |||
| 455f597543 | |||
| 8c9e626920 | |||
| 5a000c1ff4 | |||
| ddf634bfb2 | |||
| 89d3b8cc6a | |||
| 49af6d09a2 | |||
| e5b0cac284 | |||
| 6f33900f85 | |||
| 514823af7d | |||
| 65b058f563 | |||
| 7c8560deff | |||
| 6bbe2613b4 | |||
| 5771478e4b | |||
| e13030bc89 | |||
| 0a0ac93a55 | |||
| 214fb50e74 | |||
| 959f8ee31e | |||
| cb0d75be37 |
@@ -1631,3 +1631,18 @@
|
||||
[4.1.4]
|
||||
* Add CLOUDRON_ prefix to MySQL addon variables
|
||||
|
||||
[4.1.5]
|
||||
* Make the terminal addon button inject variables based on manifest version
|
||||
* Preserve addon passwords correctly when using v2 manifest
|
||||
* Show error message instead of logging out user when invalid 2FA token is provided
|
||||
* Ensure redis vars are renamed with manifest v2
|
||||
* Add missing Scaleway Object Storage to restore UI
|
||||
* Fix Exoscale endpoints in restore UI
|
||||
* Reset the app icon when showing the configure UI
|
||||
|
||||
[4.1.6]
|
||||
* Fix issue where CLOUDRON_APP_HOSTNAME was incorrectly set
|
||||
* Remove chat link from the footer of login screen
|
||||
* Add support for oplog tailing in mongodb
|
||||
* Fix LDAP not accessible via scheduler containers
|
||||
|
||||
|
||||
+70
-64
@@ -906,7 +906,7 @@ function setupSendMail(app, options, callback) {
|
||||
|
||||
debugApp(app, 'Setting up SendMail');
|
||||
|
||||
appdb.getAddonConfigByName(app.id, 'sendmail', 'MAIL_SMTP_PASSWORD', function (error, existingPassword) {
|
||||
appdb.getAddonConfigByName(app.id, 'sendmail', '%MAIL_SMTP_PASSWORD', function (error, existingPassword) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
|
||||
|
||||
var password = error ? hat(4 * 48) : existingPassword; // see box#565 for password length
|
||||
@@ -944,7 +944,7 @@ function setupRecvMail(app, options, callback) {
|
||||
|
||||
debugApp(app, 'Setting up recvmail');
|
||||
|
||||
appdb.getAddonConfigByName(app.id, 'recvmail', 'MAIL_IMAP_PASSWORD', function (error, existingPassword) {
|
||||
appdb.getAddonConfigByName(app.id, 'recvmail', '%MAIL_IMAP_PASSWORD', function (error, existingPassword) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
|
||||
|
||||
var password = error ? hat(4 * 48) : existingPassword; // see box#565 for password length
|
||||
@@ -1040,7 +1040,7 @@ function setupMySql(app, options, callback) {
|
||||
|
||||
debugApp(app, 'Setting up mysql');
|
||||
|
||||
appdb.getAddonConfigByName(app.id, 'mysql', 'MYSQL_PASSWORD', function (error, existingPassword) {
|
||||
appdb.getAddonConfigByName(app.id, 'mysql', '%MYSQL_PASSWORD', function (error, existingPassword) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
|
||||
|
||||
const tmp = mysqlDatabaseName(app.id);
|
||||
@@ -1256,7 +1256,7 @@ function setupPostgreSql(app, options, callback) {
|
||||
|
||||
const { database, username } = postgreSqlNames(app.id);
|
||||
|
||||
appdb.getAddonConfigByName(app.id, 'postgresql', 'POSTGRESQL_PASSWORD', function (error, existingPassword) {
|
||||
appdb.getAddonConfigByName(app.id, 'postgresql', '%POSTGRESQL_PASSWORD', function (error, existingPassword) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
|
||||
|
||||
const data = {
|
||||
@@ -1431,13 +1431,14 @@ function setupMongoDb(app, options, callback) {
|
||||
|
||||
debugApp(app, 'Setting up mongodb');
|
||||
|
||||
appdb.getAddonConfigByName(app.id, 'mongodb', 'MONGODB_PASSWORD', function (error, existingPassword) {
|
||||
appdb.getAddonConfigByName(app.id, 'mongodb', '%MONGODB_PASSWORD', function (error, existingPassword) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
|
||||
|
||||
const data = {
|
||||
database: app.id,
|
||||
username: app.id,
|
||||
password: error ? hat(4 * 128) : existingPassword
|
||||
password: error ? hat(4 * 128) : existingPassword,
|
||||
oplog: !!options.oplog
|
||||
};
|
||||
|
||||
getServiceDetails('mongodb', 'CLOUDRON_MONGODB_TOKEN', function (error, result) {
|
||||
@@ -1450,7 +1451,7 @@ function setupMongoDb(app, options, callback) {
|
||||
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
|
||||
|
||||
var env = [
|
||||
{ name: `${envPrefix}MONGODB_URL`, value : `mongodb://${data.username}:${data.password}@mongodb/${data.database}` },
|
||||
{ name: `${envPrefix}MONGODB_URL`, value : `mongodb://${data.username}:${data.password}@mongodb:27017/${data.database}` },
|
||||
{ name: `${envPrefix}MONGODB_USERNAME`, value : data.username },
|
||||
{ name: `${envPrefix}MONGODB_PASSWORD`, value: data.password },
|
||||
{ name: `${envPrefix}MONGODB_HOST`, value : 'mongodb' },
|
||||
@@ -1458,6 +1459,10 @@ function setupMongoDb(app, options, callback) {
|
||||
{ name: `${envPrefix}MONGODB_DATABASE`, value : data.database }
|
||||
];
|
||||
|
||||
if (options.oplog) {
|
||||
env.push({ name: `${envPrefix}MONGODB_OPLOG_URL`, value : `mongodb://${data.username}:${data.password}@mongodb:27017/local?authSource=${data.database}` });
|
||||
}
|
||||
|
||||
debugApp(app, 'Setting mongodb addon config to %j', env);
|
||||
appdb.setAddonConfig(app.id, 'mongodb', env, callback);
|
||||
});
|
||||
@@ -1564,68 +1569,69 @@ function setupRedis(app, options, callback) {
|
||||
|
||||
const redisName = 'redis-' + app.id;
|
||||
|
||||
docker.inspect(redisName, function (error, result) {
|
||||
if (!error) {
|
||||
debug(`Re-using existing redis container with state: ${result.State}`);
|
||||
return callback();
|
||||
appdb.getAddonConfigByName(app.id, 'redis', '%REDIS_PASSWORD', function (error, existingPassword) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
|
||||
|
||||
const redisPassword = error ? hat(4 * 48) : existingPassword; // see box#362 for password length
|
||||
const redisServiceToken = hat(4 * 48);
|
||||
|
||||
// Compute redis memory limit based on app's memory limit (this is arbitrary)
|
||||
var memoryLimit = app.memoryLimit || app.manifest.memoryLimit || 0;
|
||||
|
||||
if (memoryLimit === -1) { // unrestricted (debug mode)
|
||||
memoryLimit = 0;
|
||||
} else if (memoryLimit === 0 || memoryLimit <= (2 * 1024 * 1024 * 1024)) { // less than 2G (ram+swap)
|
||||
memoryLimit = 150 * 1024 * 1024; // 150m
|
||||
} else {
|
||||
memoryLimit = 600 * 1024 * 1024; // 600m
|
||||
}
|
||||
|
||||
appdb.getAddonConfigByName(app.id, 'redis', 'REDIS_PASSWORD', function (error, existingPassword) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
|
||||
const tag = infra.images.redis.tag;
|
||||
const label = app.fqdn;
|
||||
// note that we do not add appId label because this interferes with the stop/start app logic
|
||||
const cmd = `docker run --restart=always -d --name=${redisName} \
|
||||
--hostname ${redisName} \
|
||||
--label=location=${label} \
|
||||
--net cloudron \
|
||||
--net-alias ${redisName} \
|
||||
--log-driver syslog \
|
||||
--log-opt syslog-address=udp://127.0.0.1:2514 \
|
||||
--log-opt syslog-format=rfc5424 \
|
||||
--log-opt tag="${redisName}" \
|
||||
-m ${memoryLimit/2} \
|
||||
--memory-swap ${memoryLimit} \
|
||||
--dns 172.18.0.1 \
|
||||
--dns-search=. \
|
||||
-e CLOUDRON_REDIS_PASSWORD="${redisPassword}" \
|
||||
-e CLOUDRON_REDIS_TOKEN="${redisServiceToken}" \
|
||||
-v "${paths.PLATFORM_DATA_DIR}/redis/${app.id}:/var/lib/redis" \
|
||||
--label isCloudronManaged=true \
|
||||
--read-only -v /tmp -v /run ${tag}`;
|
||||
|
||||
const redisPassword = error ? hat(4 * 48) : existingPassword; // see box#362 for password length
|
||||
const redisServiceToken = hat(4 * 48);
|
||||
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
|
||||
|
||||
// Compute redis memory limit based on app's memory limit (this is arbitrary)
|
||||
var memoryLimit = app.memoryLimit || app.manifest.memoryLimit || 0;
|
||||
var env = [
|
||||
{ name: `${envPrefix}REDIS_URL`, value: 'redis://redisuser:' + redisPassword + '@redis-' + app.id },
|
||||
{ name: `${envPrefix}REDIS_PASSWORD`, value: redisPassword },
|
||||
{ name: `${envPrefix}REDIS_HOST`, value: redisName },
|
||||
{ name: `${envPrefix}REDIS_PORT`, value: '6379' }
|
||||
];
|
||||
|
||||
if (memoryLimit === -1) { // unrestricted (debug mode)
|
||||
memoryLimit = 0;
|
||||
} else if (memoryLimit === 0 || memoryLimit <= (2 * 1024 * 1024 * 1024)) { // less than 2G (ram+swap)
|
||||
memoryLimit = 150 * 1024 * 1024; // 150m
|
||||
} else {
|
||||
memoryLimit = 600 * 1024 * 1024; // 600m
|
||||
}
|
||||
|
||||
const tag = infra.images.redis.tag;
|
||||
const label = app.fqdn;
|
||||
// note that we do not add appId label because this interferes with the stop/start app logic
|
||||
const cmd = `docker run --restart=always -d --name=${redisName} \
|
||||
--hostname ${redisName} \
|
||||
--label=location=${label} \
|
||||
--net cloudron \
|
||||
--net-alias ${redisName} \
|
||||
--log-driver syslog \
|
||||
--log-opt syslog-address=udp://127.0.0.1:2514 \
|
||||
--log-opt syslog-format=rfc5424 \
|
||||
--log-opt tag="${redisName}" \
|
||||
-m ${memoryLimit/2} \
|
||||
--memory-swap ${memoryLimit} \
|
||||
--dns 172.18.0.1 \
|
||||
--dns-search=. \
|
||||
-e CLOUDRON_REDIS_PASSWORD="${redisPassword}" \
|
||||
-e CLOUDRON_REDIS_TOKEN="${redisServiceToken}" \
|
||||
-v "${paths.PLATFORM_DATA_DIR}/redis/${app.id}:/var/lib/redis" \
|
||||
--label isCloudronManaged=true \
|
||||
--read-only -v /tmp -v /run ${tag}`;
|
||||
|
||||
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
|
||||
|
||||
var env = [
|
||||
{ name: `${envPrefix}REDIS_URL`, value: 'redis://redisuser:' + redisPassword + '@redis-' + app.id },
|
||||
{ name: `${envPrefix}REDIS_PASSWORD`, value: redisPassword },
|
||||
{ name: `${envPrefix}REDIS_HOST`, value: redisName },
|
||||
{ name: `${envPrefix}REDIS_PORT`, value: '6379' }
|
||||
];
|
||||
|
||||
async.series([
|
||||
shell.exec.bind(null, 'startRedis', cmd),
|
||||
appdb.setAddonConfig.bind(null, app.id, 'redis', env),
|
||||
waitForService.bind(null, 'redis-' + app.id, 'CLOUDRON_REDIS_TOKEN')
|
||||
], function (error) {
|
||||
if (error) debug('Error setting up redis: ', error);
|
||||
callback(error);
|
||||
});
|
||||
async.series([
|
||||
(next) => {
|
||||
docker.inspect(redisName, function (inspectError, result) {
|
||||
if (!inspectError) {
|
||||
debug(`Re-using existing redis container with state: ${JSON.stringify(result.State)}`);
|
||||
return next();
|
||||
}
|
||||
shell.exec('startRedis', cmd, next);
|
||||
});
|
||||
},
|
||||
appdb.setAddonConfig.bind(null, app.id, 'redis', env),
|
||||
waitForService.bind(null, 'redis-' + app.id, 'CLOUDRON_REDIS_TOKEN')
|
||||
], function (error) {
|
||||
if (error) debug('Error setting up redis: ', error);
|
||||
callback(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+3
-3
@@ -635,13 +635,13 @@ function getAppIdByAddonConfigValue(addonId, namePattern, value, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getAddonConfigByName(appId, addonId, name, callback) {
|
||||
function getAddonConfigByName(appId, addonId, namePattern, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof addonId, 'string');
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof namePattern, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT value FROM appAddonConfigs WHERE appId = ? AND addonId = ? AND name = ?', [ appId, addonId, name ], function (error, results) {
|
||||
database.query('SELECT value FROM appAddonConfigs WHERE appId = ? AND addonId = ? AND name LIKE ?', [ appId, addonId, namePattern ], function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
|
||||
+9
-1
@@ -486,6 +486,7 @@ function getByContainerId(containerId, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
// returns the app associated with this IP (app or scheduler)
|
||||
function getByIpAddress(ip, callback) {
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -493,7 +494,14 @@ function getByIpAddress(ip, callback) {
|
||||
docker.getContainerIdByIp(ip, function (error, containerId) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
getByContainerId(containerId, callback);
|
||||
docker.inspect(containerId, function (error, result) {
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
const appId = safe.query(result, 'Config.Labels.appId', null);
|
||||
if (!appId) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
|
||||
|
||||
get(appId, callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+4
-3
@@ -176,18 +176,19 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var docker = exports.connection,
|
||||
isAppContainer = !cmd; // non app-containers are like scheduler containers
|
||||
isAppContainer = !cmd; // non app-containers are like scheduler and exec (terminal) containers
|
||||
|
||||
var manifest = app.manifest;
|
||||
var exposedPorts = {}, dockerPortBindings = { };
|
||||
var domain = app.fqdn;
|
||||
const hostname = isAppContainer ? app.id : name;
|
||||
|
||||
const envPrefix = manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
|
||||
|
||||
let stdEnv = [
|
||||
'CLOUDRON=1',
|
||||
'CLOUDRON_PROXY_IP=172.18.0.1',
|
||||
`CLOUDRON_APP_HOSTNAME=${name}`,
|
||||
`CLOUDRON_APP_HOSTNAME=${app.id}`,
|
||||
`${envPrefix}WEBADMIN_ORIGIN=${config.adminOrigin()}`,
|
||||
`${envPrefix}API_ORIGIN=${config.adminOrigin()}`,
|
||||
`${envPrefix}APP_ORIGIN=https://${domain}`,
|
||||
@@ -240,7 +241,7 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
||||
var containerOptions = {
|
||||
name: name, // for referencing containers
|
||||
Tty: isAppContainer,
|
||||
Hostname: name,
|
||||
Hostname: hostname,
|
||||
Image: app.manifest.dockerImage,
|
||||
Cmd: (isAppContainer && app.debugMode && app.debugMode.cmd) ? app.debugMode.cmd : cmd,
|
||||
Env: stdEnv.concat(addonEnv).concat(portEnv).concat(appEnv),
|
||||
|
||||
@@ -17,7 +17,7 @@ exports = module.exports = {
|
||||
'images': {
|
||||
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:2.0.2@sha256:a28320f313785816be60e3f865e09065504170a3d20ed37de675c719b32b01eb' },
|
||||
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:2.0.2@sha256:6dcee0731dfb9b013ed94d56205eee219040ee806c7e251db3b3886eaa4947ff' },
|
||||
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:2.0.2@sha256:95e006390ddce7db637e1672eb6f3c257d3c2652747424f529b1dee3cbe6728c' },
|
||||
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:2.1.0@sha256:6d1bf221cfe6124957e2c58b57c0a47214353496009296acb16adf56df1da9d5' },
|
||||
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:2.0.0@sha256:8a88dd334b62b578530a014ca1a2425a54cb9df1e475f5d3a36806e5cfa22121' },
|
||||
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:2.3.1@sha256:9693e3ae42a12a7ac8cf5df94d828d46f5b22b4e2e1c7d1bc614d6ee2a22c365' },
|
||||
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:2.0.2@sha256:454f035d60b768153d4f31210380271b5ba1c09367c9d95c7fa37f9e39d2f59c' },
|
||||
|
||||
@@ -126,7 +126,6 @@ function checkOutboundPort25(callback) {
|
||||
'smtp.gmail.com',
|
||||
'smtp.live.com',
|
||||
'smtp.mail.yahoo.com',
|
||||
'smtp.o2.ie',
|
||||
'smtp.comcast.net',
|
||||
'smtp.1und1.de',
|
||||
]);
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
|
||||
<footer class="text-center">
|
||||
<span class="text-muted">© 2016-19 <a href="https://cloudron.io" target="_blank">Cloudron</a></span>
|
||||
<span class="text-muted"><a href="https://twitter.com/cloudron_io" target="_blank">Twitter <i class="fa fa-twitter"></i></a></span>
|
||||
<span class="text-muted"><a href="https://chat.cloudron.io" target="_blank">Chat <i class="fa fa-comments"></i></a></span>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<link href="<%= adminOrigin %>/theme.css" rel="stylesheet">
|
||||
|
||||
<!-- Custom Fonts -->
|
||||
<link href="<%= adminOrigin %>/3rdparty/css/font-awesome.min.css" rel="stylesheet" rel="stylesheet" type="text/css">
|
||||
<link href="<%= adminOrigin %>/3rdparty/fontawesome/css/all.min.css" rel="stylesheet" rel="stylesheet" type="text/css">
|
||||
|
||||
<!-- jQuery-->
|
||||
<script src="<%= adminOrigin %>/3rdparty/js/jquery.min.js"></script>
|
||||
|
||||
@@ -85,7 +85,7 @@ function enableTwoFactorAuthentication(req, res, next) {
|
||||
|
||||
users.enableTwoFactorAuthentication(req.user.id, req.body.totpToken, function (error) {
|
||||
if (error && error.reason === UsersError.NOT_FOUND) return next(new HttpError(404, 'User not found'));
|
||||
if (error && error.reason === UsersError.BAD_TOKEN) return next(new HttpError(403, 'Invalid token'));
|
||||
if (error && error.reason === UsersError.BAD_TOKEN) return next(new HttpError(412, 'Invalid token'));
|
||||
if (error && error.reason === UsersError.ALREADY_EXISTS) return next(new HttpError(409, 'TwoFactor Authentication is already enabled'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
|
||||
@@ -248,7 +248,7 @@ describe('Profile API', function () {
|
||||
.query({ access_token: token_0 })
|
||||
.send({ password: 'some wrong password', newPassword: 'MOre#$%34' })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
expect(res.statusCode).to.equal(412);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
+1
-1
@@ -132,7 +132,7 @@ function verifyPassword(req, res, next) {
|
||||
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'API call requires user password'));
|
||||
|
||||
users.verifyWithUsername(req.user.username, req.body.password, function (error) {
|
||||
if (error && error.reason === UsersError.WRONG_PASSWORD) return next(new HttpError(400, 'Password incorrect'));
|
||||
if (error && error.reason === UsersError.WRONG_PASSWORD) return next(new HttpError(412, 'Password incorrect'));
|
||||
if (error && error.reason === UsersError.NOT_FOUND) return next(new HttpError(404, 'No such user'));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user