diff --git a/CHANGES b/CHANGES index 887e7338e..8a96e3790 100644 --- a/CHANGES +++ b/CHANGES @@ -1507,4 +1507,4 @@ [3.5.0] * Add UI to switch dashboard domain - +* Fix remote support button to not remove misparsed ssh keys diff --git a/setup/start/sudoers b/setup/start/sudoers index 30a14f29f..e08a09915 100644 --- a/setup/start/sudoers +++ b/setup/start/sudoers @@ -25,8 +25,8 @@ yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/retire.sh Defaults!/home/yellowtent/box/src/scripts/update.sh env_keep="HOME BOX_ENV" yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/update.sh -Defaults!/home/yellowtent/box/src/scripts/authorized_keys.sh env_keep="HOME BOX_ENV" -yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/authorized_keys.sh +Defaults!/home/yellowtent/box/src/scripts/remotesupport.sh env_keep="HOME BOX_ENV" +yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/remotesupport.sh Defaults!/home/yellowtent/box/src/scripts/configurelogrotate.sh env_keep="HOME BOX_ENV" yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/configurelogrotate.sh diff --git a/src/routes/index.js b/src/routes/index.js index ce2fe0fe6..d70ea1e8b 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -18,7 +18,6 @@ exports = module.exports = { provision: require('./provision.js'), services: require('./services.js'), settings: require('./settings.js'), - ssh: require('./ssh.js'), support: require('./support.js'), sysadmin: require('./sysadmin.js'), tasks: require('./tasks.js'), diff --git a/src/routes/ssh.js b/src/routes/ssh.js deleted file mode 100644 index f106d6186..000000000 --- a/src/routes/ssh.js +++ /dev/null @@ -1,54 +0,0 @@ -'use strict'; - -exports = module.exports = { - getAuthorizedKeys: getAuthorizedKeys, - getAuthorizedKey: getAuthorizedKey, - addAuthorizedKey: addAuthorizedKey, - delAuthorizedKey: delAuthorizedKey -}; - -var assert = require('assert'), - HttpError = require('connect-lastmile').HttpError, - HttpSuccess = require('connect-lastmile').HttpSuccess, - ssh = require('../ssh.js'), - SshError = ssh.SshError; - -function getAuthorizedKeys(req, res, next) { - ssh.getAuthorizedKeys(function (error, result) { - if (error) return next(new HttpError(500, error)); - next(new HttpSuccess(200, { keys: result })); - }); -} - -function getAuthorizedKey(req, res, next) { - assert.strictEqual(typeof req.params.identifier, 'string'); - - ssh.getAuthorizedKey(req.params.identifier, function (error, result) { - if (error && error.reason === SshError.NOT_FOUND) return next(new HttpError(404, error.message)); - if (error) return next(new HttpError(500, error)); - next(new HttpSuccess(200, { identifier: result.identifier, key: result.key })); - }); -} - -function addAuthorizedKey(req, res, next) { - assert.strictEqual(typeof req.body, 'object'); - - if (typeof req.body.key !== 'string' || !req.body.key) return next(new HttpError(400, 'key must be a non empty')); - - ssh.addAuthorizedKey(req.body.key, function (error) { - if (error && error.reason === SshError.INVALID_KEY) return next(new HttpError(400, error.message)); - if (error) return next(new HttpError(500, error)); - - next(new HttpSuccess(201, {})); - }); -} - -function delAuthorizedKey(req, res, next) { - assert.strictEqual(typeof req.params.identifier, 'string'); - - ssh.delAuthorizedKey(req.params.identifier, function (error) { - if (error && error.reason === SshError.NOT_FOUND) return next(new HttpError(404, error.message)); - if (error) return next(new HttpError(500, error)); - next(new HttpSuccess(202, {})); - }); -} diff --git a/src/routes/support.js b/src/routes/support.js index 1ac05372e..e44efc5c8 100644 --- a/src/routes/support.js +++ b/src/routes/support.js @@ -1,7 +1,10 @@ 'use strict'; exports = module.exports = { - feedback: feedback + feedback: feedback, + + getRemoteSupport: getRemoteSupport, + enableRemoteSupport: enableRemoteSupport }; var appstore = require('../appstore.js'), @@ -9,6 +12,7 @@ var appstore = require('../appstore.js'), assert = require('assert'), HttpError = require('connect-lastmile').HttpError, HttpSuccess = require('connect-lastmile').HttpSuccess, + support = require('../support.js'), _ = require('underscore'); function feedback(req, res, next) { @@ -29,3 +33,23 @@ function feedback(req, res, next) { next(new HttpSuccess(201, {})); }); } + +function enableRemoteSupport(req, res, next) { + assert.strictEqual(typeof req.body, 'object'); + + if (typeof req.body.enable !== 'boolean') return next(new HttpError(400, 'enabled is required')); + + support.enableRemoteSupport(req.body.enable, function (error) { + if (error) return next(new HttpError(500, error)); + + next(new HttpSuccess(202, {})); + }); +} + +function getRemoteSupport(req, res, next) { + support.getRemoteSupport(function (error, status) { + if (error) return next(new HttpError(500, error)); + + next(new HttpSuccess(200, status)); + }); +} diff --git a/src/routes/test/ssh-test.js b/src/routes/test/ssh-test.js deleted file mode 100644 index f6802ee47..000000000 --- a/src/routes/test/ssh-test.js +++ /dev/null @@ -1,232 +0,0 @@ -/* global it:false */ -/* global describe:false */ -/* global before:false */ -/* global after:false */ - -'use strict'; - -var ssh = require('../../ssh.js'), - async = require('async'), - config = require('../../config.js'), - database = require('../../database.js'), - expect = require('expect.js'), - superagent = require('superagent'), - server = require('../../server.js'); - -var SERVER_URL = 'http://localhost:' + config.get('port'); - -var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com'; - -var INVALID_KEY_TYPE = 'ssh-foobar AAAAB3NzaC1yc2EAAAADAQABAAABAQCibC8G04mZy3o3AVMxjUMQoEQj0HSsl6AMVZDQK9A0e8qVRWft4HaRZdw0dW3iFDsEdny7s1zSAc5Kp5y38kJdSyEHGKxvcR8TaghUa8jpmu0sEVOTn+X4UtoonkNuJ0Jnl2tjPYsq5BtJmAeYUa1bKH5CjomCYi5OSfXRtnuZV6SiYX0A1OnZPXFWa/iFwwOUJQvDLGbFkgtJxqhxpc7yvzwFK5B9MNs7LJxA8+kRibJ9LTN1OKWNxb0oSk/PE6PFo9M2Q/SL9uj2IRXRipGj2XcOtZlqcAK5i+aq3UjjAGekztK2srQPcBkWbnI3Oim2N8l2purCfe0AoCCQHK7N nebulon@nebulon'; -var INVALID_KEY_VALUE = 'ssh-rsa foobar nebulon@nebulon'; -var INVALID_KEY_IDENTIFIER = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCibC8G04mZy3o3AVMxjUMQoEQj0HSsl6AMVZDQK9A0e8qVRWft4HaRZdw0dW3iFDsEdny7s1zSAc5Kp5y38kJdSyEHGKxvcR8TaghUa8jpmu0sEVOTn+X4UtoonkNuJ0Jnl2tjPYsq5BtJmAeYUa1bKH5CjomCYi5OSfXRtnuZV6SiYX0A1OnZPXFWa/iFwwOUJQvDLGbFkgtJxqhxpc7yvzwFK5B9MNs7LJxA8+kRibJ9LTN1OKWNxb0oSk/PE6PFo9M2Q/SL9uj2IRXRipGj2XcOtZlqcAK5i+aq3UjjAGekztK2srQPcBkWbnI3Oim2N8l2purCfe0AoCCQHK7N'; -var VALID_KEY = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCibC8G04mZy3o3AVMxjUMQoEQj0HSsl6AMVZDQK9A0e8qVRWft4HaRZdw0dW3iFDsEdny7s1zSAc5Kp5y38kJdSyEHGKxvcR8TaghUa8jpmu0sEVOTn+X4UtoonkNuJ0Jnl2tjPYsq5BtJmAeYUa1bKH5CjomCYi5OSfXRtnuZV6SiYX0A1OnZPXFWa/iFwwOUJQvDLGbFkgtJxqhxpc7yvzwFK5B9MNs7LJxA8+kRibJ9LTN1OKWNxb0oSk/PE6PFo9M2Q/SL9uj2IRXRipGj2XcOtZlqcAK5i+aq3UjjAGekztK2srQPcBkWbnI3Oim2N8l2purCfe0AoCCQHK7N nebulon@nebulon'; -var VALID_KEY_1 = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCibC8G04mZy3o3AVMxjUMQoEQj0HSsl6AMVZDQK9A0e8qVRWft4HaRZdw0dW3iFDsEdny7s1zSAc5Kp5y38kJdSyEHGKxvcR8TaghUa8jpmu0sEVOTn+X4UtoonkNuJ0Jnl2tjPYsq5BtJmAeYUa1bKH5CjomCYi5OSfXRtnuZV6SiYX0A1OnZPXFWa/iFwwOUJQvDLGbFkgtJxqhxpc7yvzwFK5B9MNs7LJxA8+kRibJ9LTN1OKWNxb0oSk/PE6PFo9M2Q/SL9uj2IRXRipGj2XcOtZlqcAK5i+aq3UjjAGekztK2srQPcBkWbnI3Oim2N8l2purCfe0AoCCQHK7N muchmore'; - -var token = null; - -function setup(done) { - config._reset(); - config.setFqdn('example-ssh-test.com'); - - async.series([ - server.start.bind(server), - - ssh._clear, - database._clear, - - function createAdmin(callback) { - superagent.post(SERVER_URL + '/api/v1/cloudron/activate') - .query({ setupToken: 'somesetuptoken' }) - .send({ username: USERNAME, password: PASSWORD, email: EMAIL }) - .end(function (error, result) { - expect(result).to.be.ok(); - expect(result.statusCode).to.eql(201); - - // stash token for further use - token = result.body.token; - - callback(); - }); - } - ], done); -} - -function cleanup(done) { - database._clear(function (error) { - expect(!error).to.be.ok(); - - server.stop(done); - }); -} - -describe('SSH API', function () { - before(setup); - after(cleanup); - - describe('add authorized_keys', function () { - it('fails due to missing key', function (done) { - superagent.put(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys') - .query({ access_token: token }) - .end(function (err, res) { - expect(res.statusCode).to.equal(400); - done(); - }); - }); - - it('fails due to empty key', function (done) { - superagent.put(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys') - .query({ access_token: token }) - .send({ key: '' }) - .end(function (err, res) { - expect(res.statusCode).to.equal(400); - done(); - }); - }); - - it('fails due to invalid key', function (done) { - superagent.put(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys') - .query({ access_token: token }) - .send({ key: 'foobar' }) - .end(function (err, res) { - expect(res.statusCode).to.equal(400); - done(); - }); - }); - - it('fails due to invalid key type', function (done) { - superagent.put(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys') - .query({ access_token: token }) - .send({ key: INVALID_KEY_TYPE }) - .end(function (err, res) { - expect(res.statusCode).to.equal(400); - done(); - }); - }); - - it('fails due to invalid key value', function (done) { - superagent.put(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys') - .query({ access_token: token }) - .send({ key: INVALID_KEY_VALUE }) - .end(function (err, res) { - expect(res.statusCode).to.equal(400); - done(); - }); - }); - - it('fails due to invalid key identifier', function (done) { - superagent.put(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys') - .query({ access_token: token }) - .send({ key: INVALID_KEY_IDENTIFIER }) - .end(function (err, res) { - expect(res.statusCode).to.equal(400); - done(); - }); - }); - - it('succeeds', function (done) { - superagent.put(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys') - .query({ access_token: token }) - .send({ key: VALID_KEY }) - .end(function (err, res) { - expect(res.statusCode).to.equal(201); - done(); - }); - }); - }); - - describe('get authorized_keys', function () { - it('fails for non existing key', function (done) { - superagent.get(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys/foobar') - .query({ access_token: token }) - .end(function (err, res) { - expect(res.statusCode).to.equal(404); - done(); - }); - }); - - it('succeeds', function (done) { - superagent.get(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys/' + VALID_KEY.split(' ')[2]) - .query({ access_token: token }) - .end(function (err, res) { - expect(res.statusCode).to.equal(200); - expect(res.body).to.be.an('object'); - expect(res.body.identifier).to.be.a('string'); - expect(res.body.identifier).to.equal(VALID_KEY.split(' ')[2]); - expect(res.body.key).to.equal(VALID_KEY); - done(); - }); - }); - }); - - describe('list authorized_keys', function () { - it('succeeds', function (done) { - superagent.get(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys') - .query({ access_token: token }) - .end(function (err, res) { - expect(res.statusCode).to.equal(200); - expect(res.body).to.be.an('object'); - expect(res.body.keys).to.be.an('array'); - expect(res.body.keys.length).to.equal(1); - expect(res.body.keys[0]).to.be.an('object'); - expect(res.body.keys[0].identifier).to.be.a('string'); - expect(res.body.keys[0].identifier).to.equal(VALID_KEY.split(' ')[2]); - expect(res.body.keys[0].key).to.equal(VALID_KEY); - done(); - }); - }); - - it('succeeds with two keys', function (done) { - superagent.put(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys') - .query({ access_token: token }) - .send({ key: VALID_KEY_1 }) - .end(function (err, res) { - expect(res.statusCode).to.equal(201); - - superagent.get(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys') - .query({ access_token: token }) - .end(function (err, res) { - expect(res.statusCode).to.equal(200); - expect(res.body).to.be.an('object'); - expect(res.body.keys).to.be.an('array'); - expect(res.body.keys.length).to.equal(2); - expect(res.body.keys[0]).to.be.an('object'); - expect(res.body.keys[0].identifier).to.be.a('string'); - expect(res.body.keys[0].identifier).to.equal(VALID_KEY_1.split(' ')[2]); - expect(res.body.keys[0].key).to.equal(VALID_KEY_1); - expect(res.body.keys[1]).to.be.an('object'); - expect(res.body.keys[1].identifier).to.be.a('string'); - expect(res.body.keys[1].identifier).to.equal(VALID_KEY.split(' ')[2]); - expect(res.body.keys[1].key).to.equal(VALID_KEY); - done(); - }); - }); - }); - }); - - describe('delete authorized_keys', function () { - it('fails for non existing key', function (done) { - superagent.del(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys/foobar') - .query({ access_token: token }) - .end(function (err, res) { - expect(res.statusCode).to.equal(404); - done(); - }); - }); - - it('succeeds', function (done) { - superagent.del(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys/' + VALID_KEY.split(' ')[2]) - .query({ access_token: token }) - .end(function (err, res) { - expect(res.statusCode).to.equal(202); - - superagent.get(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys/' + VALID_KEY.split(' ')[2]) - .query({ access_token: token }) - .end(function (err, res) { - expect(res.statusCode).to.equal(404); - done(); - }); - }); - }); - }); -}); diff --git a/src/routes/test/support-test.js b/src/routes/test/support-test.js new file mode 100644 index 000000000..88a850c74 --- /dev/null +++ b/src/routes/test/support-test.js @@ -0,0 +1,139 @@ +/* global it:false */ +/* global describe:false */ +/* global before:false */ +/* global after:false */ + +'use strict'; + +var async = require('async'), + config = require('../../config.js'), + database = require('../../database.js'), + expect = require('expect.js'), + path = require('path'), + safe = require('safetydance'), + superagent = require('superagent'), + server = require('../../server.js'); + +var SERVER_URL = 'http://localhost:' + config.get('port'); + +var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com'; +var AUTHORIZED_KEYS_FILE = path.join(config.baseDir(), 'authorized_keys'); +var token = null; + +function setup(done) { + config._reset(); + config.setFqdn('example-ssh-test.com'); + safe.fs.unlinkSync(AUTHORIZED_KEYS_FILE); + + async.series([ + server.start.bind(server), + + database._clear, + + function createAdmin(callback) { + superagent.post(SERVER_URL + '/api/v1/cloudron/activate') + .query({ setupToken: 'somesetuptoken' }) + .send({ username: USERNAME, password: PASSWORD, email: EMAIL }) + .end(function (error, result) { + expect(result).to.be.ok(); + expect(result.statusCode).to.eql(201); + + // stash token for further use + token = result.body.token; + + callback(); + }); + } + ], done); +} + +function cleanup(done) { + database._clear(function (error) { + expect(!error).to.be.ok(); + + server.stop(done); + }); +} + +describe('Support API', function () { + before(setup); + after(cleanup); + + describe('remote support', function () { + it('get remote support', function (done) { + superagent.get(SERVER_URL + '/api/v1/support/remote_support') + .query({ access_token: token }) + .end(function (err, res) { + expect(res.statusCode).to.equal(200); + expect(res.body.enabled).to.be(false); + done(); + }); + }); + + it('enable remote support', function (done) { + superagent.post(SERVER_URL + '/api/v1/support/remote_support') + .query({ access_token: token }) + .send({ enable: true }) + .end(function (err, res) { + expect(res.statusCode).to.equal(202); + + let data = safe.fs.readFileSync(AUTHORIZED_KEYS_FILE, 'utf8'); + let count = (data.match(/support@cloudron.io/g) || []).length; + expect(count).to.be(1); + done(); + }); + }); + + it('returns true when remote support enabled', function (done) { + superagent.get(SERVER_URL + '/api/v1/support/remote_support') + .query({ access_token: token }) + .end(function (err, res) { + expect(res.statusCode).to.equal(200); + expect(res.body.enabled).to.be(true); + done(); + }); + }); + + it('enable remote support (again)', function (done) { + superagent.post(SERVER_URL + '/api/v1/support/remote_support') + .query({ access_token: token }) + .send({ enable: true }) + .end(function (err, res) { + expect(res.statusCode).to.equal(202); + + let data = safe.fs.readFileSync(AUTHORIZED_KEYS_FILE, 'utf8'); + let count = (data.match(/support@cloudron.io/g) || []).length; + expect(count).to.be(1); + done(); + }); + }); + + it('disable remote support', function (done) { + superagent.post(SERVER_URL + '/api/v1/support/remote_support') + .query({ access_token: token }) + .send({ enable: false }) + .end(function (err, res) { + expect(res.statusCode).to.equal(202); + + let data = safe.fs.readFileSync(AUTHORIZED_KEYS_FILE, 'utf8'); + let count = (data.match(/support@cloudron.io/g) || []).length; + expect(count).to.be(0); + done(); + }); + }); + + it('disable remote support (again)', function (done) { + superagent.post(SERVER_URL + '/api/v1/support/remote_support') + .query({ access_token: token }) + .send({ enable: false }) + .end(function (err, res) { + expect(res.statusCode).to.equal(202); + + let data = safe.fs.readFileSync(AUTHORIZED_KEYS_FILE, 'utf8'); + let count = (data.match(/support@cloudron.io/g) || []).length; + expect(count).to.be(0); + done(); + }); + }); + }); +}); diff --git a/src/scripts/authorized_keys.sh b/src/scripts/authorized_keys.sh deleted file mode 100755 index 0d0a39457..000000000 --- a/src/scripts/authorized_keys.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/bin/bash - -set -eu -o pipefail - -if [[ ${EUID} -ne 0 ]]; then - echo "This script should be run as root." > /dev/stderr - exit 1 -fi - -if [[ $# -eq 0 ]]; then - echo "No arguments supplied" - exit 1 -fi - -if [[ "$1" == "--check" ]]; then - echo "OK" - exit 0 -fi - -# verify argument count -if [[ $# -lt 3 ]]; then - echo "Usage: authorized_keys.sh " - exit 1 -fi - -if [[ -f "$2" ]]; then - # on some vanilla ubuntu installs, the .ssh directory does not exist - mkdir -p "$(dirname $3)" - - cp "$2" "$3" - chown "$1":"$1" "$3" -fi diff --git a/src/scripts/remotesupport.sh b/src/scripts/remotesupport.sh new file mode 100755 index 000000000..d8d237d17 --- /dev/null +++ b/src/scripts/remotesupport.sh @@ -0,0 +1,45 @@ +#!/bin/bash + +set -eu -o pipefail + +if [[ ${EUID} -ne 0 ]]; then + echo "This script should be run as root." > /dev/stderr + exit 1 +fi + +if [[ $# -eq 0 ]]; then + echo "No arguments supplied" + exit 1 +fi + +if [[ "$1" == "--check" ]]; then + echo "OK" + exit 0 +fi + +CLOUDRON_SUPPORT_PUBLIC_KEY='ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDQVilclYAIu+ioDp/sgzzFz6YU0hPcRYY7ze/LiF/lC7uQqK062O54BFXTvQ3ehtFZCx3bNckjlT2e6gB8Qq07OM66De4/S/g+HJW4TReY2ppSPMVNag0TNGxDzVH8pPHOysAm33LqT2b6L/wEXwC6zWFXhOhHjcMqXvi8Ejaj20H1HVVcf/j8qs5Thkp9nAaFTgQTPu8pgwD8wDeYX1hc9d0PYGesTADvo6HF4hLEoEnefLw7PaStEbzk2fD3j7/g5r5HcgQQXBe74xYZ/1gWOX2pFNuRYOBSEIrNfJEjFJsqk3NR1+ZoMGK7j+AZBR4k0xbrmncQLcQzl6MMDzkp support@cloudron.io' + +cmd="$1" +keys_file="$2" +user="${3:-1000}" + +if [[ "$1" == "is-enabled" ]]; then + if grep -q "${CLOUDRON_SUPPORT_PUBLIC_KEY}" "${keys_file}"; then + echo "true" + else + echo "false" + fi +elif [[ "$1" == "enable" ]]; then + mkdir -p $(dirname "${keys_file}") # .ssh does not exist sometimes + touch "${keys_file}" # required for concat to work + if ! grep -q "${CLOUDRON_SUPPORT_PUBLIC_KEY}" "${keys_file}"; then + echo -e "\n${CLOUDRON_SUPPORT_PUBLIC_KEY}" >> "${keys_file}" + chmod 600 "${keys_file}" + chown "${user}" "${keys_file}" + fi +elif [[ "$1" == "disable" ]]; then + if [[ -f "${keys_file}" ]]; then + sed -e "/ support@cloudron.io$/d" -i "${keys_file}" + fi +fi + diff --git a/src/server.js b/src/server.js index e056615a8..c4b812717 100644 --- a/src/server.js +++ b/src/server.js @@ -132,10 +132,6 @@ function initializeExpressSync() { router.get ('/api/v1/cloudron/disks', cloudronScope, routes.cloudron.getDisks); router.get ('/api/v1/cloudron/logs/:unit', cloudronScope, routes.cloudron.getLogs); router.get ('/api/v1/cloudron/logstream/:unit', cloudronScope, routes.cloudron.getLogStream); - router.get ('/api/v1/cloudron/ssh/authorized_keys', cloudronScope, isUnmanaged, routes.ssh.getAuthorizedKeys); - router.put ('/api/v1/cloudron/ssh/authorized_keys', cloudronScope, isUnmanaged, routes.ssh.addAuthorizedKey); - router.get ('/api/v1/cloudron/ssh/authorized_keys/:identifier', cloudronScope, isUnmanaged, routes.ssh.getAuthorizedKey); - router.del ('/api/v1/cloudron/ssh/authorized_keys/:identifier', cloudronScope, isUnmanaged, routes.ssh.delAuthorizedKey); router.get ('/api/v1/cloudron/eventlog', cloudronScope, routes.eventlog.get); // tasks @@ -288,6 +284,8 @@ function initializeExpressSync() { // feedback router.post('/api/v1/support/feedback', cloudronScope, isUnmanaged, routes.support.feedback); + router.get ('/api/v1/support/remote_support', cloudronScope, isUnmanaged, routes.support.getRemoteSupport); + router.post('/api/v1/support/remote_support', cloudronScope, isUnmanaged, routes.support.enableRemoteSupport); // domain routes router.post('/api/v1/domains', domainsManageScope, routes.domains.add); diff --git a/src/ssh.js b/src/ssh.js deleted file mode 100644 index 164b0b594..000000000 --- a/src/ssh.js +++ /dev/null @@ -1,171 +0,0 @@ -'use strict'; - -exports = module.exports = { - SshError: SshError, - - getAuthorizedKeys: getAuthorizedKeys, - getAuthorizedKey: getAuthorizedKey, - addAuthorizedKey: addAuthorizedKey, - delAuthorizedKey: delAuthorizedKey, - - _clear: clear -}; - -var assert = require('assert'), - config = require('./config.js'), - debug = require('debug')('box:ssh'), - path = require('path'), - safe = require('safetydance'), - shell = require('./shell.js'), - util = require('util'); - -var AUTHORIZED_KEYS_FILEPATH = config.TEST ? path.join(config.baseDir(), 'authorized_keys') : ((config.provider() === 'ec2' || config.provider() === 'lightsail' || config.provider() === 'ami') ? '/home/ubuntu/.ssh/authorized_keys' : '/root/.ssh/authorized_keys'); -var AUTHORIZED_KEYS_TMP_FILEPATH = '/tmp/.authorized_keys'; -var AUTHORIZED_KEYS_CMD = path.join(__dirname, 'scripts/authorized_keys.sh'); -var VALID_KEY_TYPES = ['ssh-rsa']; // TODO add all supported ones -var VALID_MIN_KEY_LENGTH = 370; // TODO verify this length requirement - -function SshError(reason, errorOrMessage) { - assert.strictEqual(typeof reason, 'string'); - assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined'); - - Error.call(this); - Error.captureStackTrace(this, this.constructor); - - this.name = this.constructor.name; - this.reason = reason; - if (typeof errorOrMessage === 'undefined') { - this.message = reason; - } else if (typeof errorOrMessage === 'string') { - this.message = errorOrMessage; - } else { - this.message = 'Internal error'; - this.nestedError = errorOrMessage; - } -} -util.inherits(SshError, Error); -SshError.NOT_FOUND = 'Not found'; -SshError.INVALID_KEY = 'Invalid key'; -SshError.INTERNAL_ERROR = 'Internal Error'; - -function clear(callback) { - assert.strictEqual(typeof callback, 'function'); - - safe.fs.unlinkSync(AUTHORIZED_KEYS_FILEPATH); - callback(); -} - -function saveKeys(keys, callback) { - assert(Array.isArray(keys)); - assert.strictEqual(typeof callback, 'function'); - - if (!safe.fs.writeFileSync(AUTHORIZED_KEYS_TMP_FILEPATH, keys.map(function (k) { return k.key; }).join('\n'))) { - debug('Error writing to temporary file', safe.error); - return callback(safe.error); - } - - if (!safe.fs.chmodSync(AUTHORIZED_KEYS_TMP_FILEPATH, '600')) { // 600 = rw------- - debug('Failed to adjust permissions of %s %s', AUTHORIZED_KEYS_TMP_FILEPATH, safe.error); - return callback(safe.error); - } - - var user = config.TEST ? process.env.USER : ((config.provider() === 'ec2' || config.provider() === 'lightsail' || config.provider() === 'ami') ? 'ubuntu' : 'root'); - shell.sudo('authorized_keys', [ AUTHORIZED_KEYS_CMD, user, AUTHORIZED_KEYS_TMP_FILEPATH, AUTHORIZED_KEYS_FILEPATH ], {}, function (error) { - if (error) return callback(error); - - callback(null); - }); -} - -function getKeys(callback) { - assert.strictEqual(typeof callback, 'function'); - - shell.sudo('authorized_keys', [ AUTHORIZED_KEYS_CMD, process.env.USER, AUTHORIZED_KEYS_FILEPATH, AUTHORIZED_KEYS_TMP_FILEPATH ], {}, function (error) { - if (error) return callback(error); - - var content = safe.fs.readFileSync(AUTHORIZED_KEYS_TMP_FILEPATH, 'utf8'); - if (!content) return callback(null, []); - - var keys = content.split('\n') - .filter(function (k) { return !!k.trim(); }) - .map(function (k) { return { identifier: k.split(' ')[2], key: k }; }) - .filter(function (k) { return k.identifier && k.key; }); - - safe.fs.unlinkSync(AUTHORIZED_KEYS_TMP_FILEPATH); - - return callback(null, keys); - }); -} - -function getAuthorizedKeys(callback) { - assert.strictEqual(typeof callback, 'function'); - - getKeys(function (error, keys) { - if (error) return callback(new SshError(SshError.INTERNAL_ERROR, error)); - - return callback(null, keys.sort(function (a, b) { return a.identifier.localeCompare(b.identifier); })); - }); -} - -function getAuthorizedKey(identifier, callback) { - assert.strictEqual(typeof identifier, 'string'); - assert.strictEqual(typeof callback, 'function'); - - getKeys(function (error, keys) { - if (error) return callback(new SshError(SshError.INTERNAL_ERROR, error)); - - if (keys.length === 0) return callback(new SshError(SshError.NOT_FOUND)); - - var key = keys.find(function (k) { return k.identifier === identifier; }); - if (!key) return callback(new SshError(SshError.NOT_FOUND)); - - callback(null, key); - }); -} - -function addAuthorizedKey(key, callback) { - assert.strictEqual(typeof key, 'string'); - assert.strictEqual(typeof callback, 'function'); - - var tmp = key.split(' '); - if (tmp.length !== 3) return callback(new SshError(SshError.INVALID_KEY)); - if (!VALID_KEY_TYPES.some(function (t) { return tmp[0] === t; })) return callback(new SshError(SshError.INVALID_KEY, 'Invalid key type')); - if (tmp[1].length < VALID_MIN_KEY_LENGTH) return callback(new SshError(SshError.INVALID_KEY)); - - var identifier = tmp[2]; - - getKeys(function (error, keys) { - if (error) return callback(new SshError(SshError.INTERNAL_ERROR, error)); - - var index = keys.findIndex(function (k) { return k.identifier === identifier; }); - if (index !== -1) keys[index] = { identifier: identifier, key: key }; - else keys.push({ identifier: identifier, key: key }); - - saveKeys(keys, function (error) { - if (error) return callback(new SshError(SshError.INTERNAL_ERROR, error)); - - callback(null); - }); - }); -} - -function delAuthorizedKey(identifier, callback) { - assert.strictEqual(typeof identifier, 'string'); - assert.strictEqual(typeof callback, 'function'); - - getKeys(function (error, keys) { - if (error) return callback(new SshError(SshError.INTERNAL_ERROR, error)); - - let index = keys.findIndex(function (k) { return k.identifier === identifier; }); - if (index === -1) return callback(new SshError(SshError.NOT_FOUND)); - - // now remove the key - keys.splice(index, 1); - - saveKeys(keys, function (error) { - if (error) return callback(new SshError(SshError.INTERNAL_ERROR, error)); - - callback(null); - }); - }); -} diff --git a/src/support.js b/src/support.js new file mode 100644 index 000000000..a04376646 --- /dev/null +++ b/src/support.js @@ -0,0 +1,66 @@ +'use strict'; + +exports = module.exports = { + getRemoteSupport: getRemoteSupport, + enableRemoteSupport: enableRemoteSupport, + + SupportError: SupportError +}; + +let assert = require('assert'), + config = require('./config.js'), + shell = require('./shell.js'), + once = require('once'), + path = require('path'), + util = require('util'); + +var AUTHORIZED_KEYS_FILEPATH = config.TEST ? path.join(config.baseDir(), 'authorized_keys') : ((config.provider() === 'ec2' || config.provider() === 'lightsail' || config.provider() === 'ami') ? '/home/ubuntu/.ssh/authorized_keys' : '/root/.ssh/authorized_keys'), + AUTHORIZED_KEYS_USER = config.TEST ? process.getuid() : ((config.provider() === 'ec2' || config.provider() === 'lightsail' || config.provider() === 'ami') ? 'ubuntu' : 'root'), + AUTHORIZED_KEYS_CMD = path.join(__dirname, 'scripts/remotesupport.sh'); + +function SupportError(reason, errorOrMessage) { + assert.strictEqual(typeof reason, 'string'); + assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined'); + + Error.call(this); + Error.captureStackTrace(this, this.constructor); + + this.name = this.constructor.name; + this.reason = reason; + if (typeof errorOrMessage === 'undefined') { + this.message = reason; + } else if (typeof errorOrMessage === 'string') { + this.message = errorOrMessage; + } else { + this.message = 'Internal error'; + this.nestedError = errorOrMessage; + } +} +util.inherits(SupportError, Error); +SupportError.NOT_FOUND = 'Not found'; +SupportError.INVALID_KEY = 'Invalid key'; +SupportError.INTERNAL_ERROR = 'Internal Error'; + +function getRemoteSupport(callback) { + assert.strictEqual(typeof callback, 'function'); + + callback = once(callback); // exit may or may not be called after an 'error' + + let result = ''; + let cp = shell.sudo('support', [ AUTHORIZED_KEYS_CMD, 'is-enabled', AUTHORIZED_KEYS_FILEPATH ], {}, function (error) { + if (error) callback(new SupportError(SupportError.INTERNAL_ERROR, error)); + + callback(null, { enabled: result.trim() === 'true' }); + }); + cp.stdout.on('data', (data) => result = result + data.toString('utf8')); +} + +function enableRemoteSupport(enable, callback) { + assert.strictEqual(typeof callback, 'function'); + + shell.sudo('support', [ AUTHORIZED_KEYS_CMD, enable ? 'enable' : 'disable', AUTHORIZED_KEYS_FILEPATH, AUTHORIZED_KEYS_USER ], {}, function (error) { + if (error) callback(new SupportError(SupportError.INTERNAL_ERROR, error)); + + callback(); + }); +} diff --git a/src/test/checkInstall b/src/test/checkInstall index 819382666..13fb0c3fc 100755 --- a/src/test/checkInstall +++ b/src/test/checkInstall @@ -19,7 +19,7 @@ scripts=("${SOURCE_DIR}/src/scripts/rmvolume.sh" \ "${SOURCE_DIR}/src/scripts/update.sh" \ "${SOURCE_DIR}/src/scripts/collectlogs.sh" \ "${SOURCE_DIR}/src/scripts/configurecollectd.sh" \ - "${SOURCE_DIR}/src/scripts/authorized_keys.sh" \ + "${SOURCE_DIR}/src/scripts/remotesupport.sh" \ "${SOURCE_DIR}/src/scripts/backupupload.js" \ "${SOURCE_DIR}/src/scripts/configurelogrotate.sh")