diff --git a/CHANGES b/CHANGES index ee51dfbed..67e4f2524 100644 --- a/CHANGES +++ b/CHANGES @@ -2168,4 +2168,5 @@ * nfs: chown the backups for hardlinks to work * remove user add/remove/role change email notifications * persist update indicator across restarts +* cloudron-setup: add --generate-setup-token diff --git a/scripts/cloudron-setup b/scripts/cloudron-setup index 3a9f15729..0e2a239b5 100755 --- a/scripts/cloudron-setup +++ b/scripts/cloudron-setup @@ -47,8 +47,9 @@ apiServerOrigin="https://api.cloudron.io" webServerOrigin="https://cloudron.io" sourceTarballUrl="" rebootServer="true" +setupToken="" -args=$(getopt -o "" -l "help,skip-baseimage-init,provider:,version:,env:,skip-reboot" -n "$0" -- "$@") +args=$(getopt -o "" -l "help,skip-baseimage-init,provider:,version:,env:,skip-reboot,generate-setup-token" -n "$0" -- "$@") eval set -- "${args}" while true; do @@ -67,6 +68,7 @@ while true; do shift 2;; --skip-baseimage-init) initBaseImage="false"; shift;; --skip-reboot) rebootServer="false"; shift;; + --generate-setup-token) setupToken="$(openssl rand -hex 10)"; shift;; --) break;; *) echo "Unknown option $1"; exit 1;; esac @@ -157,6 +159,7 @@ fi echo "=> Installing version ${version} (this takes some time) ..." mkdir -p /etc/cloudron echo "${provider}" > /etc/cloudron/PROVIDER +[[ ! -z "${setupToken}" ]] && echo "${setupToken}" > /etc/cloudron/SETUP_TOKEN if ! /bin/bash "${box_src_tmp_dir}/scripts/installer.sh" &>> "${LOG_FILE}"; then echo "Failed to install cloudron. See ${LOG_FILE} for details" @@ -178,7 +181,12 @@ done if ! ip=$(curl -s --fail --connect-timeout 2 --max-time 2 https://api.cloudron.io/api/v1/helper/public_ip | sed -n -e 's/.*"ip": "\(.*\)"/\1/p'); then ip='' fi -echo -e "\n\n${GREEN}After reboot, visit https://${ip} and accept the self-signed certificate to finish setup.${DONE}\n" +if [[ -z "${setupToken}" ]]; then + url="https://${ip}" +else + url="https://${ip}/?setupToken=${setupToken}" +fi +echo -e "\n\n${GREEN}After reboot, visit ${url} and accept the self-signed certificate to finish setup.${DONE}\n" if [[ "${rebootServer}" == "true" ]]; then systemctl stop box mysql # sometimes mysql ends up having corrupt privilege tables diff --git a/setup/start/cloudron-motd b/setup/start/cloudron-motd index bb09d2a29..9d82b8229 100755 --- a/setup/start/cloudron-motd +++ b/setup/start/cloudron-motd @@ -10,10 +10,17 @@ if [[ -z "$(ls -A /home/yellowtent/boxdata/mail/dkim)" ]]; then fi echo "${ip}" > /tmp/.cloudron-motd-cache + if [[ ! -f /etc/cloudron/SETUP_TOKEN ]]; then + url="https://${ip}" + else + setupToken="$(cat /etc/cloudron/SETUP_TOKEN)" + url="https://${ip}/?setupToken=${setupToken}" + fi + printf "\t\t\tWELCOME TO CLOUDRON\n" printf "\t\t\t-------------------\n" - printf '\n\e[1;32m%-6s\e[m\n\n' "Visit https://${ip} on your browser and accept the self-signed certificate to finish setup." + printf '\n\e[1;32m%-6s\e[m\n\n' "Visit ${url} on your browser and accept the self-signed certificate to finish setup." printf "Cloudron overview - https://docs.cloudron.io/ \n" printf "Cloudron setup - https://docs.cloudron.io/installation/#setup \n" else diff --git a/src/paths.js b/src/paths.js index e11cfc8aa..5d3c03bf7 100644 --- a/src/paths.js +++ b/src/paths.js @@ -19,6 +19,7 @@ exports = module.exports = { DASHBOARD_DIR: constants.TEST ? path.join(__dirname, '../../dashboard/src') : path.join(baseDir(), 'box/dashboard/dist'), PROVIDER_FILE: '/etc/cloudron/PROVIDER', + SETUP_TOKEN_FILE: '/etc/cloudron/SETUP_TOKEN', PLATFORM_DATA_DIR: path.join(baseDir(), 'platformdata'), APPS_DATA_DIR: path.join(baseDir(), 'appsdata'), diff --git a/src/routes/provision.js b/src/routes/provision.js index 7464c46b7..41c6721bb 100644 --- a/src/routes/provision.js +++ b/src/routes/provision.js @@ -1,11 +1,12 @@ 'use strict'; exports = module.exports = { - providerTokenAuth: providerTokenAuth, - setup: setup, - activate: activate, - restore: restore, - getStatus: getStatus + providerTokenAuth, + setup, + activate, + restore, + getStatus, + setupTokenAuth }; var assert = require('assert'), @@ -15,10 +16,24 @@ var assert = require('assert'), debug = require('debug')('box:routes/setup'), HttpError = require('connect-lastmile').HttpError, HttpSuccess = require('connect-lastmile').HttpSuccess, + paths = require('../paths.js'), provision = require('../provision.js'), request = require('request'), + safe = require('safetydance'), settings = require('../settings.js'); +function setupTokenAuth(req, res, next) { + assert.strictEqual(typeof req.body, 'object'); + + const setupToken = safe.fs.readFileSync(paths.SETUP_TOKEN_FILE, 'utf8'); + if (!setupToken) return next(); + + if (!req.body.setupToken) return next(new HttpError(400, 'setup token required')); + if (setupToken.trim() !== req.body.setupToken) return next(new HttpError(422, 'setup token does not match')); + + return next(); +} + function providerTokenAuth(req, res, next) { assert.strictEqual(typeof req.body, 'object'); diff --git a/src/server.js b/src/server.js index ba5b07cfa..6b537741b 100644 --- a/src/server.js +++ b/src/server.js @@ -86,9 +86,9 @@ function initializeExpressSync() { const authorizeUserManager = routes.accesscontrol.authorize(users.ROLE_USER_MANAGER); // public routes - router.post('/api/v1/cloudron/setup', json, routes.provision.providerTokenAuth, routes.provision.setup); // only available until no-domain - router.post('/api/v1/cloudron/restore', json, routes.provision.restore); // only available until activated - router.post('/api/v1/cloudron/activate', json, routes.provision.activate); + router.post('/api/v1/cloudron/setup', json, routes.provision.setupTokenAuth, routes.provision.providerTokenAuth, routes.provision.setup); // only available until no-domain + router.post('/api/v1/cloudron/restore', json, routes.provision.setupTokenAuth, routes.provision.restore); // only available until activated + router.post('/api/v1/cloudron/activate', json, routes.provision.setupTokenAuth, routes.provision.activate); router.get ('/api/v1/cloudron/status', routes.provision.getStatus); router.get ('/api/v1/cloudron/languages', routes.cloudron.getLanguages); router.get ('/api/v1/cloudron/avatar', routes.branding.getCloudronAvatar); // this is a public alias for /api/v1/branding/cloudron_avatar