diff --git a/src/routes/index.js b/src/routes/index.js index 2f9626868..58c1b64d0 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -13,5 +13,6 @@ exports = module.exports = { profile: require('./profile.js'), sysadmin: require('./sysadmin.js'), settings: require('./settings.js'), + ssh: require('./ssh.js'), user: require('./user.js') }; diff --git a/src/routes/ssh.js b/src/routes/ssh.js new file mode 100644 index 000000000..9300b33d3 --- /dev/null +++ b/src/routes/ssh.js @@ -0,0 +1,49 @@ +'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'); + +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) 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) 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) return next(new HttpError(500, error)); + next(new HttpSuccess(201, {})); + }); +} diff --git a/src/server.js b/src/server.js index 82e4a3144..c50f386ad 100644 --- a/src/server.js +++ b/src/server.js @@ -105,6 +105,9 @@ function initializeExpressSync() { router.post('/api/v1/cloudron/reboot', cloudronScope, routes.cloudron.reboot); router.post('/api/v1/cloudron/migrate', cloudronScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.cloudron.migrate); router.get ('/api/v1/cloudron/graphs', cloudronScope, routes.graphs.getGraphs); + router.get ('/api/v1/cloudron/ssh/authorized_keys', cloudronScope, routes.ssh.getAuthorizedKeys); + router.put ('/api/v1/cloudron/ssh/authorized_keys', cloudronScope, routes.ssh.addAuthorizedKey); + router.del ('/api/v1/cloudron/ssh/authorized_keys', cloudronScope, routes.ssh.delAuthorizedKey); // feedback router.post('/api/v1/cloudron/feedback', usersScope, routes.cloudron.feedback); diff --git a/src/ssh.js b/src/ssh.js new file mode 100644 index 000000000..7bf9c01bb --- /dev/null +++ b/src/ssh.js @@ -0,0 +1,111 @@ +'use strict'; + +exports = module.exports = { + SshError: SshError, + + getAuthorizedKeys: getAuthorizedKeys, + getAuthorizedKey: getAuthorizedKey, + addAuthorizedKey: addAuthorizedKey, + delAuthorizedKey: delAuthorizedKey +}; + +// var AUTHORIZED_KEYS_FILEPATH = '/root/.ssh/authorized_keys'; +var AUTHORIZED_KEYS_FILEPATH = '/home/nebulon/.ssh/authorized_keys'; + +var assert = require('assert'), + safe = require('safetydance'), + util = require('util'); + +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 getKeys() { + var content = safe.fs.readFileSync(AUTHORIZED_KEYS_FILEPATH, 'utf8'); + if (!content) return []; + + var keys = content.split('/n') + .filter(function (k) { return !!k.trim(); }) + .map(function (k) { return { identifier: k.split(' ')[2] || null, value: k }; }) + .filter(function (k) { return k.identifier && k.value; }); + + return keys; +} + +function getAuthorizedKeys(callback) { + assert.strictEqual(typeof callback, 'function'); + + return callback(null, getKeys()); +} + +function getAuthorizedKey(identifier, callback) { + assert.strictEqual(typeof identifier, 'string'); + assert.strictEqual(typeof callback, 'function'); + + var keys = getKeys(); + 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)); + var identifier = tmp[2]; + + var keys = getKeys(); + var index = keys.findIndex(function (k) { return k.identifier === identifier; }); + if (index !== -1) keys[index] = { identifier: identifier, value: key }; + else keys.push({ identifier: identifier, value: key }); + + if (!safe.fs.writeFileSync(AUTHORIZED_KEYS_FILEPATH, keys.map(function (k) { return k.value; }).join('\n'))) { + console.error(safe.error); + return callback(new SshError(SshError.INTERNAL_ERROR, safe.error)); + } + + callback(); +} + +function delAuthorizedKey(identifier, callback) { + assert.strictEqual(typeof identifier, 'string'); + assert.strictEqual(typeof callback, 'function'); + + var keys = getKeys(); + var 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); + + if (!safe.fs.writeFileSync(AUTHORIZED_KEYS_FILEPATH, keys.map(function (k) { return k.value; }).join('\n'))) { + console.error(safe.error); + return callback(new SshError(SshError.INTERNAL_ERROR, safe.error)); + } + + callback(); +}