diff --git a/setup/start/sudoers b/setup/start/sudoers index cc1fc8c89..2cb2eba3d 100644 --- a/setup/start/sudoers +++ b/setup/start/sudoers @@ -28,8 +28,9 @@ 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/node.sh env_keep="HOME BOX_ENV NODE_ENV" -yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/node.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 + +Defaults!/home/yellowtent/box/src/scripts/tar.js env_keep="HOME BOX_ENV" +yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/tar.js + diff --git a/src/backups.js b/src/backups.js index 9673af38a..bce72244e 100644 --- a/src/backups.js +++ b/src/backups.js @@ -46,9 +46,6 @@ var addons = require('./addons.js'), SettingsError = require('./settings.js').SettingsError, util = require('util'); -var NODE_CMD = path.join(__dirname, './scripts/node.sh'); -var BACKUPTASK_CMD = path.join(__dirname, 'backuptask.js'); - var NOOP_CALLBACK = function (error) { if (error) debug(error); }; function debugApp(app, args) { @@ -145,11 +142,12 @@ function getRestoreConfig(backupId, callback) { }); } -function copyLastBackup(app, manifest, prefix, callback) { +function copyLastBackup(app, manifest, prefix, backupConfig, callback) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof app.lastBackupId, 'string'); assert(manifest && typeof manifest === 'object'); assert.strictEqual(typeof prefix, 'string'); + assert.strictEqual(typeof backupConfig, 'object'); assert.strictEqual(typeof callback, 'function'); var timestamp = (new Date()).toISOString().replace(/[T.]/g, '-').replace(/[:Z]/g,''); @@ -158,58 +156,26 @@ function copyLastBackup(app, manifest, prefix, callback) { var restoreConfig = apps.getAppConfig(app); restoreConfig.manifest = manifest; - settings.getBackupConfig(function (error, backupConfig) { + debug('copyLastBackup: copying backup %s to %s', app.lastBackupId, newBackupId); + + backupdb.add({ id: newBackupId, version: manifest.version, type: backupdb.BACKUP_TYPE_APP, dependsOn: [ ], restoreConfig: restoreConfig }, function (error) { if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error)); - debug('copyLastBackup: copying backup %s to %s', app.lastBackupId, newBackupId); + api(backupConfig.provider).copyBackup(backupConfig, app.lastBackupId, newBackupId, function (copyBackupError) { + const state = copyBackupError ? backupdb.BACKUP_STATE_ERROR : backupdb.BACKUP_STATE_NORMAL; - backupdb.add({ id: newBackupId, version: manifest.version, type: backupdb.BACKUP_TYPE_APP, dependsOn: [ ], restoreConfig: restoreConfig }, function (error) { - if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error)); + debugApp(app, 'copyLastBackup: %s done with state %s', newBackupId, state); - api(backupConfig.provider).copyBackup(backupConfig, app.lastBackupId, newBackupId, function (copyBackupError) { - const state = copyBackupError ? backupdb.BACKUP_STATE_ERROR : backupdb.BACKUP_STATE_NORMAL; + backupdb.update(newBackupId, { state: state }, function (error) { + if (copyBackupError) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, copyBackupError.message)); + if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error)); - debugApp(app, 'copyLastBackup: %s done with state %s', newBackupId, state); - - backupdb.update(newBackupId, { state: state }, function (error) { - if (copyBackupError) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, copyBackupError.message)); - if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error)); - - callback(null, newBackupId); - }); + callback(null, newBackupId); }); }); }); } -function runBackupTask(backupId, appId, callback) { - assert.strictEqual(typeof backupId, 'string'); - assert(appId === null || typeof appId === 'string'); - assert.strictEqual(typeof callback, 'function'); - - var killTimerId = null; - - var cp = shell.sudo('backup' + (appId ? 'App' : 'Box'), [ NODE_CMD, BACKUPTASK_CMD, backupId ].concat(appId ? [ appId ] : [ ]), function (error) { - - clearTimeout(killTimerId); - cp = null; - - if (error && (error.code === null /* signal */ || (error.code !== 0 && error.code !== 50))) { // backuptask crashed - return callback(new BackupsError(BackupsError.INTERNAL_ERROR, 'backuptask crashed')); - } else if (error && error.code === 50) { // exited with error - var result = safe.fs.readFileSync(paths.BACKUP_RESULT_FILE, 'utf8') || safe.error.message; - return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, result)); - } - - callback(); - }); - - killTimerId = setTimeout(function () { - debug('runBackupTask: backup task taking too long. killing'); - cp.kill(); - }, 4 * 60 * 60 * 1000); // 4 hours -} - function backupBoxWithAppBackupIds(appBackupIds, prefix, callback) { assert(Array.isArray(appBackupIds)); assert.strictEqual(typeof prefix, 'string'); @@ -233,7 +199,7 @@ function backupBoxWithAppBackupIds(appBackupIds, prefix, callback) { backupdb.add({ id: backupId, version: config.version(), type: backupdb.BACKUP_TYPE_BOX, dependsOn: appBackupIds, restoreConfig: null }, function (error) { if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error)); - runBackupTask(backupId, null /* appId */, function (backupTaskError) { + api(backupConfig.provider).backup(backupConfig, backupId, paths.BOX_DATA_DIR, function (backupTaskError) { const state = backupTaskError ? backupdb.BACKUP_STATE_ERROR : backupdb.BACKUP_STATE_NORMAL; debug('backupBoxWithAppBackupIds: %s time: %s secs', state, (new Date() - startTime)/1000); @@ -262,10 +228,11 @@ function canBackupApp(app) { app.installationState === appdb.ISTATE_PENDING_UPDATE; // called from apptask } -function createNewAppBackup(app, manifest, prefix, callback) { +function createNewAppBackup(app, manifest, prefix, backupConfig, callback) { assert.strictEqual(typeof app, 'object'); assert(manifest && typeof manifest === 'object'); assert.strictEqual(typeof prefix, 'string'); + assert.strictEqual(typeof backupConfig, 'object'); assert.strictEqual(typeof callback, 'function'); var timestamp = (new Date()).toISOString().replace(/[T.]/g, '-').replace(/[:Z]/g,''); @@ -284,7 +251,8 @@ function createNewAppBackup(app, manifest, prefix, callback) { backupdb.add({ id: backupId, version: manifest.version, type: backupdb.BACKUP_TYPE_APP, dependsOn: [ ], restoreConfig: restoreConfig }, function (error) { if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error)); - runBackupTask(backupId, app.id, function (backupTaskError) { + var appDataDir = safe.fs.realpathSync(path.join(paths.APPS_DATA_DIR, app.id)); + api(backupConfig.provider).backup(backupConfig, backupId, appDataDir, function (backupTaskError) { const state = backupTaskError ? backupdb.BACKUP_STATE_ERROR : backupdb.BACKUP_STATE_NORMAL; debugApp(app, 'createNewAppBackup: %s done with state %s', backupId, state); @@ -321,28 +289,32 @@ function backupApp(app, manifest, prefix, callback) { var backupFunction, startTime = new Date(); - if (!canBackupApp(app)) { - if (!app.lastBackupId) { - debugApp(app, 'backupApp: cannot backup app'); - return callback(new BackupsError(BackupsError.BAD_STATE, 'App not healthy and never backed up previously')); + settings.getBackupConfig(function (error, backupConfig) { + if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error)); + + if (!canBackupApp(app)) { + if (!app.lastBackupId) { + debugApp(app, 'backupApp: cannot backup app'); + return callback(new BackupsError(BackupsError.BAD_STATE, 'App not healthy and never backed up previously')); + } + + // set the 'creation' date of lastBackup so that the backup persists across time based archival rules + // s3 does not allow changing creation time, so copying the last backup is easy way out for now + backupFunction = copyLastBackup.bind(null, app, manifest, prefix, backupConfig); + } else { + backupFunction = createNewAppBackup.bind(null, app, manifest, prefix, backupConfig); } - // set the 'creation' date of lastBackup so that the backup persists across time based archival rules - // s3 does not allow changing creation time, so copying the last backup is easy way out for now - backupFunction = copyLastBackup.bind(null, app, manifest, prefix); - } else { - backupFunction = createNewAppBackup.bind(null, app, manifest, prefix); - } - - backupFunction(function (error, backupId) { - if (error) return callback(error); - - debugApp(app, 'backupApp: successful id:%s time:%s secs', backupId, (new Date() - startTime)/1000); - - setRestorePoint(app.id, backupId, function (error) { + backupFunction(function (error, backupId) { if (error) return callback(error); - return callback(null, backupId); + debugApp(app, 'backupApp: successful id:%s time:%s secs', backupId, (new Date() - startTime)/1000); + + setRestorePoint(app.id, backupId, function (error) { + if (error) return callback(error); + + return callback(null, backupId); + }); }); }); } diff --git a/src/backuptask.js b/src/backuptask.js deleted file mode 100755 index 762e87b4a..000000000 --- a/src/backuptask.js +++ /dev/null @@ -1,100 +0,0 @@ -#!/usr/bin/env node - -'use strict'; - -require('supererror')({ splatchError: true }); - -// remove timestamp from debug() based output -require('debug').formatArgs = function formatArgs(args) { - args[0] = this.namespace + ' ' + args[0]; -}; - -var assert = require('assert'), - BackupsError = require('./backups.js').BackupsError, - caas = require('./storage/caas.js'), - database = require('./database.js'), - debug = require('debug')('box:backuptask'), - filesystem = require('./storage/filesystem.js'), - noop = require('./storage/noop.js'), - path = require('path'), - paths = require('./paths.js'), - s3 = require('./storage/s3.js'), - safe = require('safetydance'), - settings = require('./settings.js'); - -function api(provider) { - switch (provider) { - case 'caas': return caas; - case 's3': return s3; - case 'filesystem': return filesystem; - case 'minio': return s3; - case 'exoscale-sos': return s3; - case 'noop': return noop; - default: return null; - } -} - -function initialize(callback) { - assert.strictEqual(typeof callback, 'function'); - - database.initialize(callback); -} - -function backupApp(backupId, appId, callback) { - assert.strictEqual(typeof backupId, 'string'); - assert.strictEqual(typeof appId, 'string'); - assert.strictEqual(typeof callback, 'function'); - - debug('Start app backup with id %s for %s', backupId, appId); - - settings.getBackupConfig(function (error, backupConfig) { - if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error)); - - var appDataDir = safe.fs.realpathSync(path.join(paths.APPS_DATA_DIR, appId)); - - api(backupConfig.provider).backup(backupConfig, backupId, appDataDir, callback); - }); -} - -function backupBox(backupId, callback) { - assert.strictEqual(typeof backupId, 'string'); - assert.strictEqual(typeof callback, 'function'); - - debug('Start box backup with id %s', backupId); - - settings.getBackupConfig(function (error, backupConfig) { - if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error)); - - api(backupConfig.provider).backup(backupConfig, backupId, paths.BOX_DATA_DIR, callback); - }); -} - -// Main process starts here -var backupId = process.argv[2]; -var appId = process.argv[3]; - -if (appId) debug('Backuptask for the app %s with id %s', appId, backupId); -else debug('Backuptask for the whole Cloudron with id %s', backupId); - -process.on('SIGTERM', function () { - process.exit(0); -}); - -initialize(function (error) { - if (error) throw error; - - function resultHandler(error) { - if (error) debug('completed with error', error); - - debug('completed'); - - safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, error ? error.message : ''); - - // https://nodejs.org/api/process.html are exit codes used by node. apps.js uses the value below - // to check apptask crashes - process.exit(error ? 50 : 0); - } - - if (appId) backupApp(backupId, appId, resultHandler); - else backupBox(backupId, resultHandler); -}); diff --git a/src/routes/test/backups-test.js b/src/routes/test/backups-test.js index c4497c724..e3e06eb31 100644 --- a/src/routes/test/backups-test.js +++ b/src/routes/test/backups-test.js @@ -10,7 +10,6 @@ var appdb = require('../../appdb.js'), config = require('../../config.js'), database = require('../../database.js'), expect = require('expect.js'), - hock = require('hock'), http = require('http'), nock = require('nock'), superagent = require('superagent'), @@ -23,7 +22,6 @@ var SERVER_URL = 'http://localhost:' + config.get('port'); var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com'; var token = null; -var server; function setup(done) { nock.cleanAll(); config._reset(); @@ -40,19 +38,19 @@ function setup(done) { var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {}); 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); - expect(scope1.isDone()).to.be.ok(); - expect(scope2.isDone()).to.be.ok(); + .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); + expect(scope1.isDone()).to.be.ok(); + expect(scope2.isDone()).to.be.ok(); - // stash token for further use - token = result.body.token; + // stash token for further use + token = result.body.token; - callback(); - }); + callback(); + }); }, function addApp(callback) { @@ -75,19 +73,12 @@ function cleanup(done) { } describe('Backups API', function () { - var apiHockInstance = hock.createHock({ throwOnUnmatched: false }), apiHockServer; + var scope1 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=BACKUP_TOKEN') + .reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey' } }, { 'Content-Type': 'application/json' }); before(setup); - before(function (done) { - apiHockInstance - .post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=BACKUP_TOKEN') - .reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey' } }, { 'Content-Type': 'application/json' }); - var port = parseInt(url.parse(config.apiServerOrigin()).port, 10); - apiHockServer = http.createServer(apiHockInstance.handler).listen(port, done); - }); after(function (done) { - apiHockServer.close(); done(); }); after(cleanup); @@ -95,37 +86,35 @@ describe('Backups API', function () { describe('create', function () { it('fails due to mising token', function (done) { superagent.post(SERVER_URL + '/api/v1/backups') - .end(function (error, result) { - expect(result.statusCode).to.equal(401); - done(); - }); + .end(function (error, result) { + expect(result.statusCode).to.equal(401); + done(); + }); }); it('fails due to wrong token', function (done) { superagent.post(SERVER_URL + '/api/v1/backups') - .query({ access_token: token.toUpperCase() }) - .end(function (error, result) { - expect(result.statusCode).to.equal(401); - done(); - }); + .query({ access_token: token.toUpperCase() }) + .end(function (error, result) { + expect(result.statusCode).to.equal(401); + done(); + }); }); it('succeeds', function (done) { superagent.post(SERVER_URL + '/api/v1/backups') - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(202); + .query({ access_token: token }) + .end(function (error, result) { + expect(result.statusCode).to.equal(202); - function checkAppstoreServerCalled() { - apiHockInstance.done(function (error) { - if (!error) return done(); + function checkAppstoreServerCalled() { + if (scope1.isDone()) return done(); setTimeout(checkAppstoreServerCalled, 100); - }); - } + } - checkAppstoreServerCalled(); - }); + checkAppstoreServerCalled(); + }); }); }); }); diff --git a/src/routes/test/cloudron-test.js b/src/routes/test/cloudron-test.js index b1198e539..7c5db6ef0 100644 --- a/src/routes/test/cloudron-test.js +++ b/src/routes/test/cloudron-test.js @@ -25,7 +25,6 @@ var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com'; var token = null; // authentication token var USERNAME_1 = 'userTheFirst', EMAIL_1 = 'taO@zen.mac', userId_1, token_1; -var server; function setup(done) { nock.cleanAll(); config._reset(); @@ -67,89 +66,89 @@ describe('Cloudron', function () { it('fails due to missing setupToken', function (done) { superagent.post(SERVER_URL + '/api/v1/cloudron/activate') - .send({ username: '', password: 'somepassword', email: 'admin@foo.bar' }) - .end(function (error, result) { - expect(result.statusCode).to.equal(400); - done(); - }); + .send({ username: '', password: 'somepassword', email: 'admin@foo.bar' }) + .end(function (error, result) { + expect(result.statusCode).to.equal(400); + done(); + }); }); it('fails due to internal server error on appstore side', function (done) { var scope = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(500, { message: 'this is wrong' }); superagent.post(SERVER_URL + '/api/v1/cloudron/activate') - .query({ setupToken: 'somesetuptoken' }) - .send({ username: 'someuser', password: 'strong#A3asdf', email: 'admin@foo.bar' }) - .end(function (error, result) { - expect(result.statusCode).to.equal(500); - expect(scope.isDone()).to.be.ok(); - done(); - }); + .query({ setupToken: 'somesetuptoken' }) + .send({ username: 'someuser', password: 'strong#A3asdf', email: 'admin@foo.bar' }) + .end(function (error, result) { + expect(result.statusCode).to.equal(500); + expect(scope.isDone()).to.be.ok(); + done(); + }); }); it('fails due to empty username', function (done) { var scope = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {}); superagent.post(SERVER_URL + '/api/v1/cloudron/activate') - .query({ setupToken: 'somesetuptoken' }) - .send({ username: '', password: 'ADSFsdf$%436', email: 'admin@foo.bar' }) - .end(function (error, result) { - expect(result.statusCode).to.equal(400); - expect(scope.isDone()).to.be.ok(); - done(); - }); + .query({ setupToken: 'somesetuptoken' }) + .send({ username: '', password: 'ADSFsdf$%436', email: 'admin@foo.bar' }) + .end(function (error, result) { + expect(result.statusCode).to.equal(400); + expect(scope.isDone()).to.be.ok(); + done(); + }); }); it('fails due to empty password', function (done) { var scope = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {}); superagent.post(SERVER_URL + '/api/v1/cloudron/activate') - .query({ setupToken: 'somesetuptoken' }) - .send({ username: 'someuser', password: '', email: 'admin@foo.bar' }) - .end(function (error, result) { - expect(result.statusCode).to.equal(400); - expect(scope.isDone()).to.be.ok(); - done(); - }); + .query({ setupToken: 'somesetuptoken' }) + .send({ username: 'someuser', password: '', email: 'admin@foo.bar' }) + .end(function (error, result) { + expect(result.statusCode).to.equal(400); + expect(scope.isDone()).to.be.ok(); + done(); + }); }); it('fails due to empty email', function (done) { var scope = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {}); superagent.post(SERVER_URL + '/api/v1/cloudron/activate') - .query({ setupToken: 'somesetuptoken' }) - .send({ username: 'someuser', password: 'ADSF#asd546', email: '' }) - .end(function (error, result) { - expect(result.statusCode).to.equal(400); - expect(scope.isDone()).to.be.ok(); - done(); - }); + .query({ setupToken: 'somesetuptoken' }) + .send({ username: 'someuser', password: 'ADSF#asd546', email: '' }) + .end(function (error, result) { + expect(result.statusCode).to.equal(400); + expect(scope.isDone()).to.be.ok(); + done(); + }); }); it('fails due to wrong displayName type', function (done) { var scope = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {}); superagent.post(SERVER_URL + '/api/v1/cloudron/activate') - .query({ setupToken: 'somesetuptoken' }) - .send({ username: 'someuser', password: 'ADSF?#asd546', email: 'admin@foo.bar', displayName: 1234 }) - .end(function (error, result) { - expect(result.statusCode).to.equal(400); - expect(scope.isDone()).to.be.ok(); - done(); - }); + .query({ setupToken: 'somesetuptoken' }) + .send({ username: 'someuser', password: 'ADSF?#asd546', email: 'admin@foo.bar', displayName: 1234 }) + .end(function (error, result) { + expect(result.statusCode).to.equal(400); + expect(scope.isDone()).to.be.ok(); + done(); + }); }); it('fails due to invalid email', function (done) { var scope = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {}); superagent.post(SERVER_URL + '/api/v1/cloudron/activate') - .query({ setupToken: 'somesetuptoken' }) - .send({ username: 'someuser', password: 'ADSF#asd546', email: 'invalidemail' }) - .end(function (error, result) { - expect(result.statusCode).to.equal(400); - expect(scope.isDone()).to.be.ok(); - done(); - }); + .query({ setupToken: 'somesetuptoken' }) + .send({ username: 'someuser', password: 'ADSF#asd546', email: 'invalidemail' }) + .end(function (error, result) { + expect(result.statusCode).to.equal(400); + expect(scope.isDone()).to.be.ok(); + done(); + }); }); it('succeeds', function (done) { @@ -157,27 +156,27 @@ describe('Cloudron', function () { var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {}); superagent.post(SERVER_URL + '/api/v1/cloudron/activate') - .query({ setupToken: 'somesetuptoken' }) - .send({ username: 'someuser', password: 'ADSF#asd546', email: 'admin@foo.bar', displayName: 'tester' }) - .end(function (error, result) { - expect(result.statusCode).to.equal(201); - expect(scope1.isDone()).to.be.ok(); - expect(scope2.isDone()).to.be.ok(); - done(); - }); + .query({ setupToken: 'somesetuptoken' }) + .send({ username: 'someuser', password: 'ADSF#asd546', email: 'admin@foo.bar', displayName: 'tester' }) + .end(function (error, result) { + expect(result.statusCode).to.equal(201); + expect(scope1.isDone()).to.be.ok(); + expect(scope2.isDone()).to.be.ok(); + done(); + }); }); it('fails the second time', function (done) { var scope = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {}); superagent.post(SERVER_URL + '/api/v1/cloudron/activate') - .query({ setupToken: 'somesetuptoken' }) - .send({ username: 'someuser', password: 'ADSF#asd546', email: 'admin@foo.bar' }) - .end(function (error, result) { - expect(result.statusCode).to.equal(409); - expect(scope.isDone()).to.be.ok(); - done(); - }); + .query({ setupToken: 'somesetuptoken' }) + .send({ username: 'someuser', password: 'ADSF#asd546', email: 'admin@foo.bar' }) + .end(function (error, result) { + expect(result.statusCode).to.equal(409); + expect(scope.isDone()).to.be.ok(); + done(); + }); }); }); @@ -193,34 +192,34 @@ describe('Cloudron', function () { config._reset(); 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(scope1.isDone()).to.be.ok(); - expect(scope2.isDone()).to.be.ok(); + .query({ setupToken: 'somesetuptoken' }) + .send({ username: USERNAME, password: PASSWORD, email: EMAIL }) + .end(function (error, result) { + expect(result).to.be.ok(); + expect(scope1.isDone()).to.be.ok(); + expect(scope2.isDone()).to.be.ok(); - // stash token for further use - token = result.body.token; + // stash token for further use + token = result.body.token; - callback(); - }); + callback(); + }); }, function (callback) { superagent.post(SERVER_URL + '/api/v1/users') - .query({ access_token: token }) - .send({ username: USERNAME_1, email: EMAIL_1, invite: false }) - .end(function (error, result) { - expect(result).to.be.ok(); - expect(result.statusCode).to.eql(201); + .query({ access_token: token }) + .send({ username: USERNAME_1, email: EMAIL_1, invite: false }) + .end(function (error, result) { + expect(result).to.be.ok(); + expect(result.statusCode).to.eql(201); - token_1 = tokendb.generateToken(); - userId_1 = result.body.id; + token_1 = tokendb.generateToken(); + userId_1 = result.body.id; - // HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...) - tokendb.add(token_1, userId_1, 'test-client-id', Date.now() + 100000, '*', callback); - }); + // HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...) + tokendb.add(token_1, userId_1, 'test-client-id', Date.now() + 100000, '*', callback); + }); } ], done); }); @@ -229,86 +228,86 @@ describe('Cloudron', function () { it('cannot get without token', function (done) { superagent.get(SERVER_URL + '/api/v1/cloudron/config') - .end(function (error, result) { - expect(result.statusCode).to.equal(401); - done(); - }); + .end(function (error, result) { + expect(result.statusCode).to.equal(401); + done(); + }); }); it('succeeds without appstore', function (done) { superagent.get(SERVER_URL + '/api/v1/cloudron/config') - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(200); - expect(result.body.apiServerOrigin).to.eql('http://localhost:6060'); - expect(result.body.webServerOrigin).to.eql(null); - expect(result.body.fqdn).to.eql(config.fqdn()); - expect(result.body.isCustomDomain).to.eql(true); - expect(result.body.progress).to.be.an('object'); - expect(result.body.update).to.be.an('object'); - expect(result.body.version).to.eql(config.version()); - expect(result.body.developerMode).to.be.a('boolean'); - expect(result.body.size).to.eql(null); - expect(result.body.region).to.eql(null); - expect(result.body.memory).to.eql(os.totalmem()); - expect(result.body.cloudronName).to.be.a('string'); + .query({ access_token: token }) + .end(function (error, result) { + expect(result.statusCode).to.equal(200); + expect(result.body.apiServerOrigin).to.eql('http://localhost:6060'); + expect(result.body.webServerOrigin).to.eql(null); + expect(result.body.fqdn).to.eql(config.fqdn()); + expect(result.body.isCustomDomain).to.eql(true); + expect(result.body.progress).to.be.an('object'); + expect(result.body.update).to.be.an('object'); + expect(result.body.version).to.eql(config.version()); + expect(result.body.developerMode).to.be.a('boolean'); + expect(result.body.size).to.eql(null); + expect(result.body.region).to.eql(null); + expect(result.body.memory).to.eql(os.totalmem()); + expect(result.body.cloudronName).to.be.a('string'); - done(); - }); + done(); + }); }); it('succeeds (admin)', function (done) { var scope = nock(config.apiServerOrigin()) - .get('/api/v1/boxes/localhost?token=' + config.token()) - .reply(200, { box: { region: 'sfo', size: '1gb' }, user: { }}); + .get('/api/v1/boxes/localhost?token=' + config.token()) + .reply(200, { box: { region: 'sfo', size: '1gb' }, user: { }}); superagent.get(SERVER_URL + '/api/v1/cloudron/config') - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(200); - expect(result.body.apiServerOrigin).to.eql('http://localhost:6060'); - expect(result.body.webServerOrigin).to.eql(null); - expect(result.body.fqdn).to.eql(config.fqdn()); - expect(result.body.isCustomDomain).to.eql(true); - expect(result.body.progress).to.be.an('object'); - expect(result.body.update).to.be.an('object'); - expect(result.body.version).to.eql(config.version()); - expect(result.body.developerMode).to.be.a('boolean'); - expect(result.body.size).to.eql('1gb'); - expect(result.body.region).to.eql('sfo'); - expect(result.body.memory).to.eql(os.totalmem()); - expect(result.body.cloudronName).to.be.a('string'); - expect(result.body.provider).to.be.a('string'); + .query({ access_token: token }) + .end(function (error, result) { + expect(result.statusCode).to.equal(200); + expect(result.body.apiServerOrigin).to.eql('http://localhost:6060'); + expect(result.body.webServerOrigin).to.eql(null); + expect(result.body.fqdn).to.eql(config.fqdn()); + expect(result.body.isCustomDomain).to.eql(true); + expect(result.body.progress).to.be.an('object'); + expect(result.body.update).to.be.an('object'); + expect(result.body.version).to.eql(config.version()); + expect(result.body.developerMode).to.be.a('boolean'); + expect(result.body.size).to.eql('1gb'); + expect(result.body.region).to.eql('sfo'); + expect(result.body.memory).to.eql(os.totalmem()); + expect(result.body.cloudronName).to.be.a('string'); + expect(result.body.provider).to.be.a('string'); - expect(scope.isDone()).to.be.ok(); + expect(scope.isDone()).to.be.ok(); - done(); - }); + done(); + }); }); it('succeeds (non-admin)', function (done) { superagent.get(SERVER_URL + '/api/v1/cloudron/config') - .query({ access_token: token_1 }) - .end(function (error, result) { - expect(result.statusCode).to.equal(200); - console.dir(result.body); + .query({ access_token: token_1 }) + .end(function (error, result) { + expect(result.statusCode).to.equal(200); + console.dir(result.body); - expect(result.body.apiServerOrigin).to.eql('http://localhost:6060'); - expect(result.body.webServerOrigin).to.eql(null); - expect(result.body.fqdn).to.eql(config.fqdn()); - expect(result.body.isCustomDomain).to.eql(true); - expect(result.body.progress).to.be.an('object'); - expect(result.body.version).to.eql(config.version()); - expect(result.body.cloudronName).to.be.a('string'); - expect(result.body.provider).to.be.a('string'); + expect(result.body.apiServerOrigin).to.eql('http://localhost:6060'); + expect(result.body.webServerOrigin).to.eql(null); + expect(result.body.fqdn).to.eql(config.fqdn()); + expect(result.body.isCustomDomain).to.eql(true); + expect(result.body.progress).to.be.an('object'); + expect(result.body.version).to.eql(config.version()); + expect(result.body.cloudronName).to.be.a('string'); + expect(result.body.provider).to.be.a('string'); - expect(result.body.update).to.be(undefined); - expect(result.body.size).to.be(undefined); - expect(result.body.region).to.be(undefined); - expect(result.body.memory).to.be(undefined); + expect(result.body.update).to.be(undefined); + expect(result.body.size).to.be(undefined); + expect(result.body.region).to.be(undefined); + expect(result.body.memory).to.be(undefined); - done(); - }); + done(); + }); }); }); @@ -323,18 +322,18 @@ describe('Cloudron', function () { var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {}); 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(scope1.isDone()).to.be.ok(); - expect(scope2.isDone()).to.be.ok(); + .query({ setupToken: 'somesetuptoken' }) + .send({ username: USERNAME, password: PASSWORD, email: EMAIL }) + .end(function (error, result) { + expect(result).to.be.ok(); + expect(scope1.isDone()).to.be.ok(); + expect(scope2.isDone()).to.be.ok(); - // stash token for further use - token = result.body.token; + // stash token for further use + token = result.body.token; - callback(); - }); + callback(); + }); } ], done); }); @@ -346,73 +345,73 @@ describe('Cloudron', function () { it('fails without token', function (done) { superagent.post(SERVER_URL + '/api/v1/cloudron/migrate') - .send({ size: 'small', region: 'sfo'}) - .end(function (error, result) { - expect(result.statusCode).to.equal(401); - done(); - }); + .send({ size: 'small', region: 'sfo'}) + .end(function (error, result) { + expect(result.statusCode).to.equal(401); + done(); + }); }); it('fails without password', function (done) { superagent.post(SERVER_URL + '/api/v1/cloudron/migrate') - .send({ size: 'small', region: 'sfo'}) - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(400); - done(); - }); + .send({ size: 'small', region: 'sfo'}) + .query({ access_token: token }) + .end(function (error, result) { + expect(result.statusCode).to.equal(400); + done(); + }); }); it('succeeds without size', function (done) { superagent.post(SERVER_URL + '/api/v1/cloudron/migrate') - .send({ region: 'sfo', password: PASSWORD }) - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(202); - done(); - }); + .send({ region: 'sfo', password: PASSWORD }) + .query({ access_token: token }) + .end(function (error, result) { + expect(result.statusCode).to.equal(202); + done(); + }); }); it('fails with wrong size type', function (done) { superagent.post(SERVER_URL + '/api/v1/cloudron/migrate') - .send({ size: 4, region: 'sfo', password: PASSWORD }) - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(400); - done(); - }); + .send({ size: 4, region: 'sfo', password: PASSWORD }) + .query({ access_token: token }) + .end(function (error, result) { + expect(result.statusCode).to.equal(400); + done(); + }); }); it('succeeds without region', function (done) { superagent.post(SERVER_URL + '/api/v1/cloudron/migrate') - .send({ size: 'small', password: PASSWORD }) - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(202); - done(); - }); + .send({ size: 'small', password: PASSWORD }) + .query({ access_token: token }) + .end(function (error, result) { + expect(result.statusCode).to.equal(202); + done(); + }); }); it('fails with wrong region type', function (done) { superagent.post(SERVER_URL + '/api/v1/cloudron/migrate') - .send({ size: 'small', region: 4, password: PASSWORD }) - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(400); - done(); - }); + .send({ size: 'small', region: 4, password: PASSWORD }) + .query({ access_token: token }) + .end(function (error, result) { + expect(result.statusCode).to.equal(400); + done(); + }); }); it('fails when in wrong state', function (done) { var scope2 = nock(config.apiServerOrigin()) - .post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=BACKUP_TOKEN') - .reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } }); + .post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=BACKUP_TOKEN') + .reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } }); var scope3 = nock(config.apiServerOrigin()) - .post('/api/v1/boxes/' + config.fqdn() + '/backupDone?token=APPSTORE_TOKEN', function (body) { - return body.boxVersion && body.restoreKey && !body.appId && !body.appVersion && body.appBackupIds.length === 0; - }) - .reply(200, { id: 'someid' }); + .post('/api/v1/boxes/' + config.fqdn() + '/backupDone?token=APPSTORE_TOKEN', function (body) { + return body.boxVersion && body.restoreKey && !body.appId && !body.appVersion && body.appBackupIds.length === 0; + }) + .reply(200, { id: 'someid' }); var scope1 = nock(config.apiServerOrigin()) .post('/api/v1/boxes/' + config.fqdn() + '/migrate?token=APPSTORE_TOKEN', function (body) { @@ -422,22 +421,22 @@ describe('Cloudron', function () { injectShellMock(); superagent.post(SERVER_URL + '/api/v1/cloudron/migrate') - .send({ size: 'small', region: 'sfo', password: PASSWORD }) - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(202); + .send({ size: 'small', region: 'sfo', password: PASSWORD }) + .query({ access_token: token }) + .end(function (error, result) { + expect(result.statusCode).to.equal(202); - function checkAppstoreServerCalled() { - if (scope1.isDone() && scope2.isDone() && scope3.isDone()) { - restoreShellMock(); - return done(); + function checkAppstoreServerCalled() { + if (scope1.isDone() && scope2.isDone() && scope3.isDone()) { + restoreShellMock(); + return done(); + } + + setTimeout(checkAppstoreServerCalled, 100); } - setTimeout(checkAppstoreServerCalled, 100); - } - - checkAppstoreServerCalled(); - }); + checkAppstoreServerCalled(); + }); }); it('succeeds', function (done) { @@ -446,34 +445,34 @@ describe('Cloudron', function () { }).reply(202, {}); var scope2 = nock(config.apiServerOrigin()) - .post('/api/v1/boxes/' + config.fqdn() + '/backupDone?token=APPSTORE_TOKEN', function (body) { - return body.boxVersion && body.restoreKey && !body.appId && !body.appVersion && body.appBackupIds.length === 0; - }) - .reply(200, { id: 'someid' }); + .post('/api/v1/boxes/' + config.fqdn() + '/backupDone?token=APPSTORE_TOKEN', function (body) { + return body.boxVersion && body.restoreKey && !body.appId && !body.appVersion && body.appBackupIds.length === 0; + }) + .reply(200, { id: 'someid' }); var scope3 = nock(config.apiServerOrigin()) - .post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=BACKUP_TOKEN') - .reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } }); + .post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=BACKUP_TOKEN') + .reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } }); injectShellMock(); superagent.post(SERVER_URL + '/api/v1/cloudron/migrate') - .send({ size: 'small', region: 'sfo', password: PASSWORD }) - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(202); + .send({ size: 'small', region: 'sfo', password: PASSWORD }) + .query({ access_token: token }) + .end(function (error, result) { + expect(result.statusCode).to.equal(202); - function checkAppstoreServerCalled() { - if (scope1.isDone() && scope2.isDone() && scope3.isDone()) { - restoreShellMock(); - return done(); + function checkAppstoreServerCalled() { + if (scope1.isDone() && scope2.isDone() && scope3.isDone()) { + restoreShellMock(); + return done(); + } + + setTimeout(checkAppstoreServerCalled, 100); } - setTimeout(checkAppstoreServerCalled, 100); - } - - checkAppstoreServerCalled(); - }); + checkAppstoreServerCalled(); + }); }); }); @@ -489,18 +488,18 @@ describe('Cloudron', function () { config._reset(); 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(scope1.isDone()).to.be.ok(); - expect(scope2.isDone()).to.be.ok(); + .query({ setupToken: 'somesetuptoken' }) + .send({ username: USERNAME, password: PASSWORD, email: EMAIL }) + .end(function (error, result) { + expect(result).to.be.ok(); + expect(scope1.isDone()).to.be.ok(); + expect(scope2.isDone()).to.be.ok(); - // stash token for further use - token = result.body.token; + // stash token for further use + token = result.body.token; - callback(); - }); + callback(); + }); }, ], done); }); @@ -509,111 +508,111 @@ describe('Cloudron', function () { it('fails without token', function (done) { superagent.post(SERVER_URL + '/api/v1/feedback') - .send({ type: 'ticket', subject: 'some subject', description: 'some description' }) - .end(function (error, result) { - expect(result.statusCode).to.equal(401); - done(); - }); + .send({ type: 'ticket', subject: 'some subject', description: 'some description' }) + .end(function (error, result) { + expect(result.statusCode).to.equal(401); + done(); + }); }); it('fails without type', function (done) { superagent.post(SERVER_URL + '/api/v1/feedback') - .send({ subject: 'some subject', description: 'some description' }) - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(400); - done(); - }); + .send({ subject: 'some subject', description: 'some description' }) + .query({ access_token: token }) + .end(function (error, result) { + expect(result.statusCode).to.equal(400); + done(); + }); }); it('fails with empty type', function (done) { superagent.post(SERVER_URL + '/api/v1/feedback') - .send({ type: '', subject: 'some subject', description: 'some description' }) - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(400); - done(); - }); + .send({ type: '', subject: 'some subject', description: 'some description' }) + .query({ access_token: token }) + .end(function (error, result) { + expect(result.statusCode).to.equal(400); + done(); + }); }); it('fails with unknown type', function (done) { superagent.post(SERVER_URL + '/api/v1/feedback') - .send({ type: 'foobar', subject: 'some subject', description: 'some description' }) - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(400); - done(); - }); + .send({ type: 'foobar', subject: 'some subject', description: 'some description' }) + .query({ access_token: token }) + .end(function (error, result) { + expect(result.statusCode).to.equal(400); + done(); + }); }); it('succeeds with ticket type', function (done) { superagent.post(SERVER_URL + '/api/v1/feedback') - .send({ type: 'ticket', subject: 'some subject', description: 'some description' }) - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(201); - done(); - }); + .send({ type: 'ticket', subject: 'some subject', description: 'some description' }) + .query({ access_token: token }) + .end(function (error, result) { + expect(result.statusCode).to.equal(201); + done(); + }); }); it('succeeds with app type', function (done) { superagent.post(SERVER_URL + '/api/v1/feedback') - .send({ type: 'app_missing', subject: 'some subject', description: 'some description' }) - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(201); - done(); - }); + .send({ type: 'app_missing', subject: 'some subject', description: 'some description' }) + .query({ access_token: token }) + .end(function (error, result) { + expect(result.statusCode).to.equal(201); + done(); + }); }); it('fails without description', function (done) { superagent.post(SERVER_URL + '/api/v1/feedback') - .send({ type: 'ticket', subject: 'some subject' }) - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(400); - done(); - }); + .send({ type: 'ticket', subject: 'some subject' }) + .query({ access_token: token }) + .end(function (error, result) { + expect(result.statusCode).to.equal(400); + done(); + }); }); it('fails with empty subject', function (done) { superagent.post(SERVER_URL + '/api/v1/feedback') - .send({ type: 'ticket', subject: '', description: 'some description' }) - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(400); - done(); - }); + .send({ type: 'ticket', subject: '', description: 'some description' }) + .query({ access_token: token }) + .end(function (error, result) { + expect(result.statusCode).to.equal(400); + done(); + }); }); it('fails with empty description', function (done) { superagent.post(SERVER_URL + '/api/v1/feedback') - .send({ type: 'ticket', subject: 'some subject', description: '' }) - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(400); - done(); - }); + .send({ type: 'ticket', subject: 'some subject', description: '' }) + .query({ access_token: token }) + .end(function (error, result) { + expect(result.statusCode).to.equal(400); + done(); + }); }); it('succeeds with feedback type', function (done) { superagent.post(SERVER_URL + '/api/v1/feedback') - .send({ type: 'feedback', subject: 'some subject', description: 'some description' }) - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(201); - done(); - }); + .send({ type: 'feedback', subject: 'some subject', description: 'some description' }) + .query({ access_token: token }) + .end(function (error, result) { + expect(result.statusCode).to.equal(201); + done(); + }); }); it('fails without subject', function (done) { superagent.post(SERVER_URL + '/api/v1/feedback') - .send({ type: 'ticket', description: 'some description' }) - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(400); - done(); - }); + .send({ type: 'ticket', description: 'some description' }) + .query({ access_token: token }) + .end(function (error, result) { + expect(result.statusCode).to.equal(400); + done(); + }); }); }); @@ -629,18 +628,18 @@ describe('Cloudron', function () { config._reset(); 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(scope1.isDone()).to.be.ok(); - expect(scope2.isDone()).to.be.ok(); + .query({ setupToken: 'somesetuptoken' }) + .send({ username: USERNAME, password: PASSWORD, email: EMAIL }) + .end(function (error, result) { + expect(result).to.be.ok(); + expect(scope1.isDone()).to.be.ok(); + expect(scope2.isDone()).to.be.ok(); - // stash token for further use - token = result.body.token; + // stash token for further use + token = result.body.token; - callback(); - }); + callback(); + }); }, ], done); }); @@ -651,9 +650,9 @@ describe('Cloudron', function () { superagent.get(SERVER_URL + '/api/v1/cloudron/logstream') .query({ access_token: token, fromLine: 0 }) .end(function (err, res) { - expect(res.statusCode).to.be(400); - done(); - }); + expect(res.statusCode).to.be(400); + done(); + }); }); it('logStream - stream logs', function (done) { diff --git a/src/routes/test/sysadmin-test.js b/src/routes/test/sysadmin-test.js index 5c08abea9..da351045c 100644 --- a/src/routes/test/sysadmin-test.js +++ b/src/routes/test/sysadmin-test.js @@ -10,20 +10,15 @@ var appdb = require('../../appdb.js'), config = require('../../config.js'), database = require('../../database.js'), expect = require('expect.js'), - hock = require('hock'), - http = require('http'), nock = require('nock'), superagent = require('superagent'), server = require('../../server.js'), - settings = require('../../settings.js'), - url = require('url'); + settings = require('../../settings.js'); var SERVER_URL = 'http://localhost:' + config.get('port'); var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com'; -var token = null; -var server; function setup(done) { config.setVersion('1.2.3'); @@ -37,19 +32,16 @@ function setup(done) { var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {}); 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); - expect(scope1.isDone()).to.be.ok(); - expect(scope2.isDone()).to.be.ok(); + .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); + expect(scope1.isDone()).to.be.ok(); + expect(scope2.isDone()).to.be.ok(); - // stash token for further use - token = result.body.token; - - callback(); - }); + callback(); + }); }, function addApp(callback) { @@ -75,37 +67,23 @@ describe('Internal API', function () { before(setup); after(cleanup); - var apiHockInstance = hock.createHock({ throwOnUnmatched: false }), apiHockServer; - - before(function (done) { - apiHockInstance - .post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=BACKUP_TOKEN') - .reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey' } }); - var port = parseInt(url.parse(config.apiServerOrigin()).port, 10); - apiHockServer = http.createServer(apiHockInstance.handler).listen(port, done); - }); - - after(function (done) { - apiHockServer.close(); - done(); - }); - describe('backup', function () { it('succeeds', function (done) { + var scope1 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=BACKUP_TOKEN') + .reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey' } }, { 'Content-Type': 'application/json' }); + superagent.post(config.sysadminOrigin() + '/api/v1/backup') - .end(function (error, result) { - expect(result.statusCode).to.equal(202); + .end(function (error, result) { + expect(result.statusCode).to.equal(202); - function checkAppstoreServerCalled() { - apiHockInstance.done(function (error) { - if (!error) return done(); + function checkAppstoreServerCalled() { + if (scope1.isDone()) return done(); setTimeout(checkAppstoreServerCalled, 100); - }); - } + } - checkAppstoreServerCalled(); - }); + checkAppstoreServerCalled(); + }); }); }); }); diff --git a/src/scripts/node.sh b/src/scripts/node.sh deleted file mode 100755 index dbdde0618..000000000 --- a/src/scripts/node.sh +++ /dev/null @@ -1,23 +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 - -echo "Running node with memory constraints" - -# note BOX_ENV and NODE_ENV are derived from parent process -exec env "DEBUG=box*,connect-lastmile" /usr/bin/node --max_old_space_size=300 "$@" diff --git a/src/scripts/tar.js b/src/scripts/tar.js new file mode 100755 index 000000000..56380b5b0 --- /dev/null +++ b/src/scripts/tar.js @@ -0,0 +1,21 @@ +#!/usr/bin/env node + +'use strict'; + +require('supererror')({ splatchError: true }); + +var tar = require('tar-fs'); + +var sourceDir = process.argv[2]; + +process.stderr.write('Packing ' + sourceDir + '\n'); + +tar.pack('/', { + dereference: false, // pack the symlink and not what it points to + entries: [ sourceDir ], + map: function(header) { + header.name = header.name.replace(new RegExp('^' + sourceDir + '(/?)'), '.$1'); // make paths relative + return header; + }, + strict: false // do not error for unknown types (skip fifo, char/block devices) +}).pipe(process.stdout); diff --git a/src/shell.js b/src/shell.js index 3c9a1cfcc..b722b9e22 100644 --- a/src/shell.js +++ b/src/shell.js @@ -41,9 +41,11 @@ function exec(tag, file, args, options, callback) { debug(tag + ' execFile: %s', file); // do not dump args as it might have sensitive info var cp = child_process.spawn(file, args, options); - cp.stdout.on('data', function (data) { - debug(tag + ' (stdout): %s', data.toString('utf8')); - }); + if (!options.noDebugStdout) { + cp.stdout.on('data', function (data) { + debug(tag + ' (stdout): %s', data.toString('utf8')); + }); + } cp.stderr.on('data', function (data) { debug(tag + ' (stderr): %s', data.toString('utf8')); diff --git a/src/storage/targz.js b/src/storage/targz.js index 8a03c2eef..6a4987bb6 100644 --- a/src/storage/targz.js +++ b/src/storage/targz.js @@ -10,33 +10,27 @@ var assert = require('assert'), crypto = require('crypto'), debug = require('debug')('box:storage/targz'), mkdirp = require('mkdirp'), + path = require('path'), progress = require('progress-stream'), + shell = require('../shell.js'), tar = require('tar-fs'), zlib = require('zlib'); +var TARJS_CMD = path.join(__dirname, '../scripts/tar.js'); + +// curiously, this function never calls back on success :-) function create(sourceDir, key, outStream, callback) { assert.strictEqual(typeof sourceDir, 'string'); assert(key === null || typeof key === 'string'); assert.strictEqual(typeof callback, 'function'); - var pack = tar.pack('/', { - dereference: false, // pack the symlink and not what it points to - entries: [ sourceDir ], - map: function(header) { - header.name = header.name.replace(new RegExp('^' + sourceDir + '(/?)'), '.$1'); // make paths relative - return header; - }, - strict: false // do not error for unknown types (skip fifo, char/block devices) + var pack = shell.sudo('tar', [ TARJS_CMD, sourceDir ], { noDebugStdout: true, timeout: 4 * 60 * 60 * 1000 }, function (error) { + if (error) callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message)); }); var gzip = zlib.createGzip({}); var progressStream = progress({ time: 10000 }); // display a progress every 10 seconds - pack.on('error', function (error) { - debug('backup: tar stream error.', error); - callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message)); - }); - gzip.on('error', function (error) { debug('backup: gzip stream error.', error); callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message)); @@ -52,9 +46,9 @@ function create(sourceDir, key, outStream, callback) { debug('backup: encrypt stream error.', error); callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message)); }); - pack.pipe(gzip).pipe(encrypt).pipe(progressStream).pipe(outStream); + pack.stdout.pipe(gzip).pipe(encrypt).pipe(progressStream).pipe(outStream); } else { - pack.pipe(gzip).pipe(progressStream).pipe(outStream); + pack.stdout.pipe(gzip).pipe(progressStream).pipe(outStream); } } diff --git a/src/test/checkInstall b/src/test/checkInstall index 71026e11e..90aee2db1 100755 --- a/src/test/checkInstall +++ b/src/test/checkInstall @@ -17,7 +17,6 @@ scripts=("${SOURCE_DIR}/src/scripts/rmappdir.sh" \ "${SOURCE_DIR}/src/scripts/collectlogs.sh" \ "${SOURCE_DIR}/src/scripts/configurecollectd.sh" \ "${SOURCE_DIR}/src/scripts/authorized_keys.sh" \ - "${SOURCE_DIR}/src/scripts/node.sh" \ "${SOURCE_DIR}/src/scripts/configurelogrotate.sh") for script in "${scripts[@]}"; do @@ -33,6 +32,21 @@ for script in "${scripts[@]}"; do fi done +scripts=("${SOURCE_DIR}/src/scripts/tar.js") + +for script in "${scripts[@]}"; do + if [[ $(sudo -n "${script}" 2>/dev/null) ]]; then + echo "" + echo "${script} does not have sudo access." + echo "You have to add the lines below to /etc/sudoers.d/yellowtent." + echo "" + echo "Defaults!${script} env_keep=\"HOME BOX_ENV\"" + echo "${USER} ALL=(ALL) NOPASSWD:SETENV: ${script}" + echo "" + exit 1 + fi +done + image_missing="" images=$(node -e "var i = require('${SOURCE_DIR}/src/infra_version.js'); console.log(Object.keys(i.images).map(function (x) { return i.images[x].tag; }).join('\n'));"; echo $TEST_IMAGE)