Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7913b8e862 | ||
|
|
4101f6d712 | ||
|
|
0fa039db40 | ||
|
|
e4b95242a0 | ||
|
|
4e356fccde | ||
|
|
2f98ac3e92 | ||
|
|
0befba6648 | ||
|
|
3c0064e0b4 | ||
|
|
c039fdecb9 | ||
|
|
dac86d6856 | ||
|
|
86ca9ae9ce | ||
|
|
700a7637b6 | ||
|
|
feb61c27d9 |
27
CHANGES
27
CHANGES
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user