Compare commits

...

13 Commits

Author SHA1 Message Date
Girish Ramakrishnan
7913b8e862 6.3 changes 2021-08-11 12:43:29 -07:00
Girish Ramakrishnan
4101f6d712 password reset: require and verify totpToken
this is a port of 04d377d20d
2021-08-11 12:43:29 -07:00
Girish Ramakrishnan
0fa039db40 Use the addresses of all available interfaces
See https://forum.cloudron.io/topic/5481/special-treatment-of-port-53-does-not-work-in-all-cases

(cherry picked from commit 1e665b6323)
2021-08-11 12:38:18 -07:00
Girish Ramakrishnan
e4b95242a0 cloudron-setup: check if nginx/docker is already installed
(cherry picked from commit ef56bf9888)
2021-08-11 12:37:16 -07:00
Girish Ramakrishnan
4e356fccde 6.3.6 changes 2021-07-13 14:34:36 -07:00
Girish Ramakrishnan
2f98ac3e92 notification: app updated message shown despite failure
(cherry picked from commit db685d3a56)
2021-07-13 14:33:13 -07:00
Girish Ramakrishnan
0befba6648 Fix notifications.alert (async usage)
this broke the reboot button among other things

(cherry picked from commit 14000e56b7)
2021-07-12 16:12:50 -07:00
Girish Ramakrishnan
3c0064e0b4 more changes 2021-07-10 11:14:38 -07:00
Girish Ramakrishnan
c039fdecb9 eventlog: typo in cleanup
(cherry picked from commit eafd72b4e7)
2021-07-10 11:14:38 -07:00
Girish Ramakrishnan
dac86d6856 sshfs: only chown when auth as root user
(cherry picked from commit 5d836b3f7c)

also contains typo fix of username to user
2021-07-10 11:14:27 -07:00
Girish Ramakrishnan
86ca9ae9ce 6.3.5 changes 2021-07-09 14:47:40 -07:00
Girish Ramakrishnan
700a7637b6 6.3.4 changes 2021-06-27 09:00:09 -07:00
Girish Ramakrishnan
feb61c27d9 reverseproxy: remove any old dashboard domain configs
(cherry picked from commit c052882de9)
2021-06-27 08:59:43 -07:00
11 changed files with 105 additions and 49 deletions

27
CHANGES
View File

@@ -2295,3 +2295,30 @@
* mail: enable sieve extension editheader
* mail: update solr to 8.9.0
[6.3.4]
* Fix issue where old nginx configs where not removed before upgrade
[6.3.5]
* filemanager: reset selection if directory has changed
* branding: fix error highlight with empty cloudron name
* better text instead of "Cloudron in the wild"
* Make sso login hint translatable
* Give unread notifications a small left border
* Fix issue where clicking update indicator opened app in new tab
* Ensure notifications are only fetched and shown for at least admins
* setupaccount: Show input field errors below input field
* Set focus automatically for new alias or redirect
* eventlog: fix issue where old events are not periodically removed
* ssfs: fix chown
[6.3.6]
* Fix broken reboot button
* app updated notification shown despite failure
* Update translation for sso login information
* Hide groups/tags/state filter in app listing for normal users
* filemanager: Ensure breadcrumbs and hash are correctly updated on folder navigation
* cloudron-setup: check if nginx/docker is already installed
* Use the addresses of all available interfaces for port 53 binding
* refresh config on appstore login
* password reset: check 2fa when enabled

View File

@@ -99,6 +99,11 @@ if [[ "${ubuntu_version}" != "16.04" && "${ubuntu_version}" != "18.04" && "${ubu
exit 1
fi
if which nginx >/dev/null || which docker >/dev/null || which node > /dev/null; then
echo "Error: Some packages like nginx/docker/nodejs are already installed. Cloudron requires specific versions of these packages and will install them as part of it's installation. Please start with a fresh Ubuntu install and run this script again." > /dev/stderr
exit 1
fi
# Install MOTD file for stack script style installations. this is removed by the trap exit handler. Heredoc quotes prevents parameter expansion
cat > /etc/update-motd.d/91-cloudron-install-in-progress <<'EOF'
#!/bin/bash

View File

@@ -639,11 +639,11 @@ function scheduleTask(appId, installationState, taskId, callback) {
// see also apptask makeTaskError
boxError.details.taskId = taskId;
boxError.details.installationState = installationState;
appdb.update(appId, { installationState: exports.ISTATE_ERROR, error: boxError.toPlainObject(), taskId: null }, callback);
appdb.update(appId, { installationState: exports.ISTATE_ERROR, error: boxError.toPlainObject(), taskId: null }, callback.bind(null, error));
} else if (!(installationState === exports.ISTATE_PENDING_UNINSTALL && !error)) { // clear out taskId except for successful uninstall
appdb.update(appId, { taskId: null }, callback);
appdb.update(appId, { taskId: null }, callback.bind(null, error));
} else {
callback(null);
callback(error);
}
});
});
@@ -1356,7 +1356,7 @@ function update(app, data, auditSource, callback) {
args: { updateConfig },
values,
onFinished: (error) => {
eventlog.add(eventlog.ACTION_APP_UPDATE_FINISH, auditSource, { app, success: !error, errorMessage: error ? error.message : null }, () => {}); // ignore error
eventlog.add(eventlog.ACTION_APP_UPDATE_FINISH, auditSource, { app, success: !error, errorMessage: error ? error.message : null });
}
};
addTask(appId, exports.ISTATE_PENDING_UPDATE, task, function (error, result) {

View File

@@ -523,7 +523,7 @@ function backup(app, args, progressCallback, callback) {
if (error) {
debugApp(app, 'error backing up app:', error);
// return to installed state intentionally. the error is stashed only in the task and not the app (the UI shows error state otherwise)
return updateApp(app, { installationState: apps.ISTATE_INSTALLED, error: null }, callback.bind(null, makeTaskError(error, app)));
return updateApp(app, { installationState: apps.ISTATE_INSTALLED, error: null }, callback.bind(null, error));
}
callback(null);
});

View File

@@ -183,19 +183,16 @@ function getConfig(callback) {
});
}
function reboot(callback) {
notifications.alert(notifications.ALERT_REBOOT, 'Reboot Required', '', function (error) {
if (error) debug('reboot: failed to clear reboot notification.', error);
async function reboot() {
await notifications.alert(notifications.ALERT_REBOOT, 'Reboot Required', '');
shell.sudo('reboot', [ REBOOT_CMD ], {}, callback);
});
const [error] = await safe(shell.promises.sudo('reboot', [ REBOOT_CMD ], {}));
if (error) debug('reboot: could not reboot', error);
}
function isRebootRequired(callback) {
assert.strictEqual(typeof callback, 'function');
async function isRebootRequired() {
// https://serverfault.com/questions/92932/how-does-ubuntu-keep-track-of-the-system-restart-required-flag-in-motd
callback(null, fs.existsSync('/var/run/reboot-required'));
return fs.existsSync('/var/run/reboot-required');
}
// called from cron.js
@@ -214,30 +211,24 @@ function runSystemChecks(callback) {
function checkMailStatus(callback) {
assert.strictEqual(typeof callback, 'function');
mail.checkConfiguration(function (error, message) {
mail.checkConfiguration(async function (error, message) {
if (error) return callback(error);
notifications.alert(notifications.ALERT_MAIL_STATUS, 'Email is not configured properly', message, callback);
await safe(notifications.alert(notifications.ALERT_MAIL_STATUS, 'Email is not configured properly', message));
callback();
});
}
function checkRebootRequired(callback) {
assert.strictEqual(typeof callback, 'function');
isRebootRequired(function (error, rebootRequired) {
if (error) return callback(error);
notifications.alert(notifications.ALERT_REBOOT, 'Reboot Required', rebootRequired ? 'To finish ubuntu security updates, a reboot is necessary.' : '', callback);
});
async function checkRebootRequired() {
const rebootRequired = await isRebootRequired();
await notifications.alert(notifications.ALERT_REBOOT, 'Reboot Required', rebootRequired ? 'To finish ubuntu security updates, a reboot is necessary.' : '');
}
function checkUbuntuVersion(callback) {
assert.strictEqual(typeof callback, 'function');
async function checkUbuntuVersion() {
const isXenial = fs.readFileSync('/etc/lsb-release', 'utf-8').includes('16.04');
if (!isXenial) return callback();
if (!isXenial) return;
notifications.alert(notifications.ALERT_UPDATE_UBUNTU, 'Ubuntu upgrade required', 'Ubuntu 16.04 has reached end of life and will not receive security and maintenance updates. Please follow https://docs.cloudron.io/guides/upgrade-ubuntu-18/ to upgrade to Ubuntu 18 at the earliest.', callback);
await notifications.alert(notifications.ALERT_UPDATE_UBUNTU, 'Ubuntu upgrade required', 'Ubuntu 16.04 has reached end of life and will not receive security and maintenance updates. Please follow https://docs.cloudron.io/guides/upgrade-ubuntu-18/ to upgrade to Ubuntu 18 at the earliest.');
}
function getLogs(unit, options, callback) {

View File

@@ -101,7 +101,7 @@ function startJobs(callback) {
gJobs.cleanupEventlog = new CronJob({
cronTime: '00 */30 * * * *', // every 30 minutes
onTick: eventlog.cleanup.bind(null, new Date(Date.now() - 60 * 60 * 24 * 10 * 1000)), // 10 days ago
onTick: eventlog.cleanup.bind(null, { creationTime: new Date(Date.now() - 60 * 60 * 24 * 10 * 1000) }), // 10 days ago
start: true
});

View File

@@ -283,16 +283,21 @@ async function getMounts(app, callback) {
});
}
function getLowerUpIp() { // see getifaddrs and IFF_LOWER_UP and netdevice
const ni = os.networkInterfaces(); // { lo: [], eth0: [] }
for (const iname of Object.keys(ni)) {
if (iname === 'lo') continue;
for (const address of ni[iname]) {
if (!address.internal && address.family === 'IPv4') return address.address;
}
function getAddresses() {
const deviceLinks = safe.fs.readdirSync('/sys/class/net'); // https://man7.org/linux/man-pages/man5/sysfs.5.html
if (!deviceLinks) return [];
const devices = deviceLinks.map(d => { return { name: d, link: safe.fs.readlinkSync(`/sys/class/net/${d}`) }; });
const physicalDevices = devices.filter(d => d.link && !d.link.includes('virtual'));
const addresses = [];
for (const phy of physicalDevices) {
const result = safe.JSON.parse(safe.child_process.execSync(`ip -f inet -j addr show ${phy.name}`, { encoding: 'utf8' }));
const address = safe.query(result, '[0].addr_info[0].local');
if (address) addresses.push(address);
}
return null;
return addresses;
}
function createSubcontainer(app, name, cmd, options, callback) {
@@ -332,8 +337,8 @@ function createSubcontainer(app, name, cmd, options, callback) {
exposedPorts[`${containerPort}/${portType}`] = {};
portEnv.push(`${portName}=${hostPort}`);
const hostIp = hostPort === 53 ? getLowerUpIp() : '0.0.0.0'; // port 53 is special because it is possibly taken by systemd-resolved
dockerPortBindings[`${containerPort}/${portType}`] = [ { HostIp: hostIp, HostPort: hostPort + '' } ];
const hostIps = hostPort === 53 ? getAddresses() : [ '0.0.0.0' ]; // port 53 is special because it is possibly taken by systemd-resolved
dockerPortBindings[`${containerPort}/${portType}`] = hostIps.map(hip => { return { HostIp: hip, HostPort: hostPort + '' }; });
}
let appEnv = [];

View File

@@ -268,6 +268,8 @@ async function onEvent(id, action, source, data) {
return await oomEvent(id, data.app, data.addon, data.containerId, data.event);
case eventlog.ACTION_APP_UPDATE_FINISH:
if (source.username !== auditSource.CRON.username) return; // updated by user
if (data.errorMessage) return; // the update indicator will still appear, so no need to notify user
return await appUpdated(id, data.app);
case eventlog.ACTION_CERTIFICATE_RENEWAL:

View File

@@ -788,8 +788,11 @@ function checkCerts(options, auditSource, progressCallback, callback) {
}
function removeAppConfigs() {
const dashboardConfigFilename = `${settings.dashboardFqdn()}.conf`;
// remove all configs which are not the default or current dashboard
for (let appConfigFile of fs.readdirSync(paths.NGINX_APPCONFIG_DIR)) {
if (appConfigFile !== constants.NGINX_DEFAULT_CONFIG_FILE_NAME && !appConfigFile.startsWith(constants.DASHBOARD_LOCATION)) {
if (appConfigFile !== constants.NGINX_DEFAULT_CONFIG_FILE_NAME && appConfigFile !== dashboardConfigFilename) {
fs.unlinkSync(path.join(paths.NGINX_APPCONFIG_DIR, appConfigFile));
}
}

View File

@@ -35,6 +35,7 @@ const assert = require('assert'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
safe = require('safetydance'),
speakeasy = require('speakeasy'),
sysinfo = require('../sysinfo.js'),
system = require('../system.js'),
tokens = require('../tokens.js'),
@@ -95,6 +96,12 @@ function passwordReset(req, res, next) {
users.getByResetToken(req.body.resetToken, function (error, userObject) {
if (error) return next(new HttpError(401, 'Invalid resetToken'));
if (userObject.twoFactorAuthenticationEnabled) {
if (typeof req.body.totpToken !== 'string') return next(new HttpError(401, 'A totpToken must be provided'));
const verified = speakeasy.totp.verify({ secret: userObject.twoFactorAuthenticationSecret, encoding: 'base32', token: req.body.totpToken, window: 2 });
if (!verified) return next(new HttpError(401, 'Invalid totpToken'));
}
// if you fix the duration here, the emails and UI have to be fixed as well
if (Date.now() - userObject.resetTokenCreationTime > 7 * 24 * 60 * 60 * 1000) return next(new HttpError(401, 'Token expired'));
if (!userObject.username) return next(new HttpError(409, 'No username set'));
@@ -137,19 +144,18 @@ function setupAccount(req, res, next) {
});
}
function reboot(req, res, next) {
async function reboot(req, res, next) {
// Finish the request, to let the appstore know we triggered the reboot
next(new HttpSuccess(202, {}));
cloudron.reboot(function () {});
await safe(cloudron.reboot());
}
function isRebootRequired(req, res, next) {
cloudron.isRebootRequired(function (error, result) {
if (error) return next(BoxError.toHttpError(error));
async function isRebootRequired(req, res, next) {
const [error, rebootRequired] = await safe(cloudron.isRebootRequired());
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { rebootRequired: result }));
});
next(new HttpSuccess(200, { rebootRequired }));
}
function getConfig(req, res, next) {

View File

@@ -91,6 +91,23 @@ function checkPreconditions(apiConfig, dataLayout, callback) {
});
}
function hasChownSupportSync(apiConfig) {
switch (apiConfig.provider) {
case PROVIDER_NFS:
case PROVIDER_EXT4:
case PROVIDER_FILESYSTEM:
return true;
case PROVIDER_SSHFS:
// sshfs can be mounted as root or normal user. when mounted as root, we have to chown since we remove backups as the yellowtent user
// when mounted as non-root user, files are created as yellowtent user but they are still owned by the non-root user (thus del also works)
return apiConfig.mountOptions.user === 'root';
case PROVIDER_CIFS:
return true;
case PROVIDER_MOUNTPOINT:
return apiConfig.chown;
}
}
function upload(apiConfig, backupFilePath, sourceStream, callback) {
assert.strictEqual(typeof apiConfig, 'object');
assert.strictEqual(typeof backupFilePath, 'string');
@@ -117,7 +134,7 @@ function upload(apiConfig, backupFilePath, sourceStream, callback) {
fileStream.on('finish', function () {
const backupUid = parseInt(process.env.SUDO_UID, 10) || process.getuid(); // in test, upload() may or may not be called via sudo script
if ((apiConfig.provider !== PROVIDER_MOUNTPOINT && apiConfig.provider !== PROVIDER_CIFS) || apiConfig.chown) {
if (hasChownSupportSync(apiConfig)) {
if (!safe.fs.chownSync(backupFilePath, backupUid, backupUid)) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Unable to chown:' + safe.error.message));
if (!safe.fs.chownSync(path.dirname(backupFilePath), backupUid, backupUid)) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Unable to chown:' + safe.error.message));
}