diff --git a/CHANGES b/CHANGES index a32f84134..8cd495c80 100644 --- a/CHANGES +++ b/CHANGES @@ -2234,4 +2234,7 @@ [6.3.0] * mail: allow TLS from internal hosts +* tokens: add lastUsedTime +* update: set memory limit properly +* addons: better error handling diff --git a/migrations/20210315194623-tokens-add-lastUsedTime.js b/migrations/20210315194623-tokens-add-lastUsedTime.js new file mode 100644 index 000000000..1eb737aac --- /dev/null +++ b/migrations/20210315194623-tokens-add-lastUsedTime.js @@ -0,0 +1,15 @@ +'use strict'; + +exports.up = function(db, callback) { + db.runSql('ALTER TABLE tokens ADD COLUMN lastUsedTime TIMESTAMP NULL', function (error) { + if (error) console.error(error); + callback(error); + }); +}; + +exports.down = function(db, callback) { + db.runSql('ALTER TABLE tokens DROP COLUMN lastUsedTime', function (error) { + if (error) console.error(error); + callback(error); + }); +}; diff --git a/migrations/schema.sql b/migrations/schema.sql index b13aab5ff..e53e2da0f 100644 --- a/migrations/schema.sql +++ b/migrations/schema.sql @@ -55,6 +55,7 @@ CREATE TABLE IF NOT EXISTS tokens( clientId VARCHAR(128), scope VARCHAR(512) NOT NULL, expires BIGINT NOT NULL, // FIXME: make this a timestamp + lastUsedTime TIMESTAMP NULL, PRIMARY KEY(accessToken)); CREATE TABLE IF NOT EXISTS apps( diff --git a/src/accesscontrol.js b/src/accesscontrol.js index 6c11609fc..3703efc93 100644 --- a/src/accesscontrol.js +++ b/src/accesscontrol.js @@ -1,19 +1,22 @@ 'use strict'; exports = module.exports = { - verifyToken: verifyToken + verifyToken }; var assert = require('assert'), BoxError = require('./boxerror.js'), - tokendb = require('./tokendb.js'), + debug = require('debug')('box:accesscontrol'), + tokens = require('./tokens.js'), users = require('./users.js'); +const NOOP_CALLBACK = function (error) { if (error) debug(error); }; + function verifyToken(accessToken, callback) { assert.strictEqual(typeof accessToken, 'string'); assert.strictEqual(typeof callback, 'function'); - tokendb.getByAccessToken(accessToken, function (error, token) { + tokens.getByAccessToken(accessToken, function (error, token) { if (error && error.reason === BoxError.NOT_FOUND) return callback(new BoxError(BoxError.INVALID_CREDENTIALS)); if (error) return callback(error); @@ -23,6 +26,8 @@ function verifyToken(accessToken, callback) { if (!user.active) return callback(new BoxError(BoxError.INVALID_CREDENTIALS)); + tokens.update(token.id, { lastUsedTime: new Date() }, NOOP_CALLBACK); + callback(null, user); }); }); diff --git a/src/backups.js b/src/backups.js index b3bdf6722..1d920c561 100644 --- a/src/backups.js +++ b/src/backups.js @@ -1457,6 +1457,8 @@ function cleanupMissingBackups(backupConfig, progressCallback, callback) { let page = 1, perPage = 1000, more = false, missingBackupIds = []; + if (constants.TEST) return callback(null, missingBackupIds); + async.doWhilst(function (whilstCallback) { backupdb.list(page, perPage, function (error, result) { if (error) return whilstCallback(error); diff --git a/src/routes/test/mail-test.js b/src/routes/test/mail-test.js index 7de288ddc..2e6cef9e2 100644 --- a/src/routes/test/mail-test.js +++ b/src/routes/test/mail-test.js @@ -7,6 +7,7 @@ var async = require('async'), constants = require('../../constants.js'), + crypto = require('crypto'), database = require('../../database.js'), expect = require('expect.js'), mail = require('../../mail.js'), @@ -169,7 +170,8 @@ describe('Mail API', function () { callback(null, dnsAnswerQueue[hostname][type]); }; - dkimDomain = 'cloudron-admincom._domainkey.' + DOMAIN_0.domain; + const suffix = crypto.createHash('sha256').update(settings.adminDomain()).digest('hex').substr(0, 6); + dkimDomain = `cloudron-${suffix}._domainkey.${DOMAIN_0.domain}`; spfDomain = DOMAIN_0.domain; mxDomain = DOMAIN_0.domain; dmarcDomain = '_dmarc.' + DOMAIN_0.domain; diff --git a/src/test/apps-test.js b/src/test/apps-test.js index 7459f10f2..f524f648b 100644 --- a/src/test/apps-test.js +++ b/src/test/apps-test.js @@ -419,7 +419,7 @@ describe('Apps', function () { }); it('can mark apps for restore', function (done) { - apps.restoreInstalledApps(function (error) { + apps.restoreInstalledApps({}, function (error) { expect(error).to.be(null); apps.getAll(function (error, result) { diff --git a/src/test/apptask-test.js b/src/test/apptask-test.js index aaa17430d..a80314506 100644 --- a/src/test/apptask-test.js +++ b/src/test/apptask-test.js @@ -13,8 +13,6 @@ var appdb = require('../appdb.js'), domains = require('../domains.js'), expect = require('expect.js'), fs = require('fs'), - js2xml = require('js2xmlparser').parse, - nock = require('nock'), paths = require('../paths.js'), settings = require('../settings.js'), userdb = require('../userdb.js'), @@ -96,25 +94,8 @@ var APP = { aliasDomains: [] }; -var awsHostedZones; - describe('apptask', function () { before(function (done) { - awsHostedZones = { - HostedZones: [{ - Id: '/hostedzone/ZONEID', - Name: `${DOMAIN_0.domain}.`, - CallerReference: '305AFD59-9D73-4502-B020-F4E6F889CB30', - ResourceRecordSetCount: 2, - ChangeInfo: { - Id: '/change/CKRTFJA0ANHXB', - Status: 'INSYNC' - } - }], - IsTruncated: false, - MaxItems: '100' - }; - async.series([ database.initialize, database._clear, @@ -214,39 +195,4 @@ describe('apptask', function () { done(); }); }); - - it('registers subdomain', function (done) { - nock.cleanAll(); - - var awsScope = nock('http://localhost:5353') - .get('/2013-04-01/hostedzonesbyname?dnsname=example.com.&maxitems=1') - .times(2) - .reply(200, js2xml('ListHostedZonesResponse', awsHostedZones, { wrapHandlers: { HostedZones: () => 'HostedZone'} })) - .get('/2013-04-01/hostedzone/ZONEID/rrset?maxitems=1&name=applocation.' + DOMAIN_0.domain + '.&type=A') - .reply(200, js2xml('ListResourceRecordSetsResponse', { ResourceRecordSets: [ ] }, { 'Content-Type': 'application/xml' })) - .post('/2013-04-01/hostedzone/ZONEID/rrset/') - .reply(200, js2xml('ChangeResourceRecordSetsResponse', { ChangeInfo: { Id: 'RRID', Status: 'INSYNC' } })); - - apptask._registerLocations(APP, true /* overwrite */, function (error) { - expect(error).to.be(null); - expect(awsScope.isDone()).to.be.ok(); - done(); - }); - }); - - it('unregisters subdomain', function (done) { - nock.cleanAll(); - - var awsScope = nock('http://localhost:5353') - .get('/2013-04-01/hostedzonesbyname?dnsname=example.com.&maxitems=1') - .reply(200, js2xml('ListHostedZonesResponse', awsHostedZones, { wrapHandlers: { HostedZones: () => 'HostedZone'} })) - .post('/2013-04-01/hostedzone/ZONEID/rrset/') - .reply(200, js2xml('ChangeResourceRecordSetsResponse', { ChangeInfo: { Id: 'RRID', Status: 'INSYNC' } })); - - apptask._unregisterSubdomains(APP, [ { subdomain: APP.location, domain: APP.domain } ], function (error) { - expect(error).to.be(null); - expect(awsScope.isDone()).to.be.ok(); - done(); - }); - }); }); diff --git a/src/test/database-test.js b/src/test/database-test.js index 66bbb88d1..c74d51f77 100644 --- a/src/test/database-test.js +++ b/src/test/database-test.js @@ -736,6 +736,7 @@ describe('database', function () { identifier: '0', clientId: 'clientid-0', expires: Date.now() + 60 * 60000, + lastUsedTime: null, scope: '' }; var TOKEN_1 = { @@ -745,6 +746,7 @@ describe('database', function () { identifier: '1', clientId: 'clientid-1', expires: Number.MAX_SAFE_INTEGER, + lastUsedTime: null, scope: '' }; var TOKEN_2 = { @@ -754,6 +756,7 @@ describe('database', function () { identifier: '2', clientId: 'clientid-2', expires: Date.now(), + lastUsedTime: null, scope: '' }; diff --git a/src/test/dockerproxy-test.js b/src/test/dockerproxy-test.js index dd836b48d..75eae7d53 100644 --- a/src/test/dockerproxy-test.js +++ b/src/test/dockerproxy-test.js @@ -21,7 +21,7 @@ describe('Dockerproxy', function () { dockerProxy.start(function (error) { expect(error).to.not.be.ok(); - exec(`${DOCKER} run -d cloudron/base:2.0.0 "bin/bash" "-c" "while true; do echo 'perpetual walrus'; sleep 1; done"`, function (error, stdout, stderr) { + exec(`${DOCKER} run -d cloudron/base:3.0.0 "bin/bash" "-c" "while true; do echo 'perpetual walrus'; sleep 1; done"`, function (error, stdout, stderr) { expect(error).to.be(null); expect(stderr).to.be.empty(); @@ -54,7 +54,7 @@ describe('Dockerproxy', function () { }); it('can create container', function (done) { - var cmd = `${DOCKER} run cloudron/base:2.0.0 "/bin/bash" "-c" "echo 'hello'"`; + var cmd = `${DOCKER} run cloudron/base:3.0.0 "/bin/bash" "-c" "echo 'hello'"`; exec(cmd, function (error, stdout, stderr) { expect(error).to.be(null); expect(stdout).to.contain('hello'); @@ -64,7 +64,7 @@ describe('Dockerproxy', function () { }); it('proxy overwrites the container network option', function (done) { - var cmd = `${DOCKER} run --network ifnotrewritethiswouldfail cloudron/base:2.0.0 "/bin/bash" "-c" "echo 'hello'"`; + var cmd = `${DOCKER} run --network ifnotrewritethiswouldfail cloudron/base:3.0.0 "/bin/bash" "-c" "echo 'hello'"`; exec(cmd, function (error, stdout, stderr) { expect(error).to.be(null); expect(stdout).to.contain('hello'); @@ -97,7 +97,7 @@ describe('Dockerproxy', function () { exec(`${DOCKER} exec ${containerId} ls`, function (error, stdout, stderr) { expect(error).to.be(null); expect(stderr).to.be.empty(); - expect(stdout).to.equal('bin\nboot\ndev\netc\nhome\nlib\nlib64\nmedia\nmnt\nopt\nproc\nroot\nrun\nsbin\nsrv\nsys\ntmp\nusr\nvar\n'); + expect(stdout).to.equal('bin\nboot\ndev\netc\nhome\nlib\nlib32\nlib64\nlibx32\nmedia\nmnt\nopt\nproc\nroot\nrun\nsbin\nsrv\nsys\ntmp\nusr\nvar\n'); done(); }); diff --git a/src/test/domains-test.js b/src/test/domains-test.js index ee51bb866..c838b8be4 100644 --- a/src/test/domains-test.js +++ b/src/test/domains-test.js @@ -5,18 +5,86 @@ 'use strict'; -var async = require('async'), +var appdb = require('../appdb.js'), + apps = require('../apps.js'), + async = require('async'), database = require('../database.js'), domains = require('../domains.js'), expect = require('expect.js'), + js2xml = require('js2xmlparser').parse, + nock = require('nock'), settings = require('../settings.js'); +const DOMAIN_0 = { + domain: 'example.com', + zoneName: 'example.com', + provider: 'route53', + config: { + accessKeyId: 'accessKeyId', + secretAccessKey: 'secretAccessKey', + endpoint: 'http://localhost:5353' + }, + fallbackCertificate: null, + tlsConfig: { provider: 'letsencrypt-staging' }, + wellKnown: null +}; + +let AUDIT_SOURCE = { ip: '1.2.3.4' }; + +var MANIFEST = { + 'id': 'io.cloudron.test', + 'author': 'The Presidents Of the United States Of America', + 'title': 'test title', + 'description': 'test description', + 'tagline': 'test rocks', + 'website': 'http://test.cloudron.io', + 'contactEmail': 'test@cloudron.io', + 'version': '0.1.0', + 'manifestVersion': 1, + 'dockerImage': 'cloudron/test:25.2.0', + 'healthCheckPath': '/', + 'httpPort': 7777, + 'tcpPorts': { + 'ECHO_SERVER_PORT': { + 'title': 'Echo Server Port', + 'description': 'Echo server', + 'containerPort': 7778 + } + }, + 'addons': { + 'oauth': { }, + 'redis': { }, + 'mysql': { }, + 'postgresql': { } + } +}; + +var APP = { + id: 'appid', + appStoreId: 'appStoreId', + installationState: apps.ISTATE_PENDING_INSTALL, + runState: 'running', + location: 'applocation', + domain: DOMAIN_0.domain, + fqdn: DOMAIN_0.domain + '.' + 'applocation', + manifest: MANIFEST, + containerId: 'someid', + portBindings: null, + accessRestriction: null, + memoryLimit: 0, + mailboxDomain: DOMAIN_0.domain, + alternateDomains: [], + aliasDomains: [] +}; + describe('Domains', function () { before(function (done) { async.series([ database.initialize, database._clear, - settings.setAdminLocation.bind(null, 'example.com', 'my.example.com') + settings.setAdminLocation.bind(null, DOMAIN_0.domain, 'my.' + DOMAIN_0.domain), + domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0, AUDIT_SOURCE), + appdb.add.bind(null, APP.id, APP.appStoreId, APP.manifest, APP.location, APP.domain, APP.portBindings, APP) ], done); }); @@ -109,4 +177,56 @@ describe('Domains', function () { expect(domains.getName(domain, 'www.dev', 'TXT')).to.be('www.dev.dev'); }); }); + + var awsHostedZones; + + it('registers subdomain', function (done) { + awsHostedZones = { + HostedZones: [{ + Id: '/hostedzone/ZONEID', + Name: `${DOMAIN_0.domain}.`, + CallerReference: '305AFD59-9D73-4502-B020-F4E6F889CB30', + ResourceRecordSetCount: 2, + ChangeInfo: { + Id: '/change/CKRTFJA0ANHXB', + Status: 'INSYNC' + } + }], + IsTruncated: false, + MaxItems: '100' + }; + + nock.cleanAll(); + + var awsScope = nock('http://localhost:5353') + .get('/2013-04-01/hostedzonesbyname?dnsname=example.com.&maxitems=1') + .times(2) + .reply(200, js2xml('ListHostedZonesResponse', awsHostedZones, { wrapHandlers: { HostedZones: () => 'HostedZone'} })) + .get('/2013-04-01/hostedzone/ZONEID/rrset?maxitems=1&name=applocation.' + DOMAIN_0.domain + '.&type=A') + .reply(200, js2xml('ListResourceRecordSetsResponse', { ResourceRecordSets: [ ] }, { 'Content-Type': 'application/xml' })) + .post('/2013-04-01/hostedzone/ZONEID/rrset/') + .reply(200, js2xml('ChangeResourceRecordSetsResponse', { ChangeInfo: { Id: 'RRID', Status: 'INSYNC' } })); + + domains.registerLocations([ { subdomain: APP.location, domain: APP.domain } ], { overwriteDns: true }, (/*progress*/) => {}, function (error) { + expect(error).to.be(null); + expect(awsScope.isDone()).to.be.ok(); + done(); + }); + }); + + it('unregisters subdomain', function (done) { + nock.cleanAll(); + + var awsScope = nock('http://localhost:5353') + .get('/2013-04-01/hostedzonesbyname?dnsname=example.com.&maxitems=1') + .reply(200, js2xml('ListHostedZonesResponse', awsHostedZones, { wrapHandlers: { HostedZones: () => 'HostedZone'} })) + .post('/2013-04-01/hostedzone/ZONEID/rrset/') + .reply(200, js2xml('ChangeResourceRecordSetsResponse', { ChangeInfo: { Id: 'RRID', Status: 'INSYNC' } })); + + domains.unregisterLocations([ { subdomain: APP.location, domain: APP.domain } ], (/*progress*/) => {}, function (error) { + expect(error).to.be(null); + expect(awsScope.isDone()).to.be.ok(); + done(); + }); + }); }); diff --git a/src/test/janitor-test.js b/src/test/janitor-test.js index 3caf46996..a34dca980 100644 --- a/src/test/janitor-test.js +++ b/src/test/janitor-test.js @@ -22,7 +22,8 @@ describe('janitor', function () { clientId: 'clientid-0', expires: Date.now() + 60 * 60 * 1000, scope: 'settings', - name: 'clientid0' + name: 'clientid0', + lastUsedTime: null }; var TOKEN_1 = { id: 'tid-1', @@ -31,7 +32,8 @@ describe('janitor', function () { clientId: 'clientid-1', expires: Date.now() - 1000, scope: 'apps', - name: 'clientid1' + name: 'clientid1', + lastUsedTime: null }; before(function (done) { diff --git a/src/test/volumes-test.js b/src/test/volumes-test.js index 2b60de993..8f7f35fff 100644 --- a/src/test/volumes-test.js +++ b/src/test/volumes-test.js @@ -66,7 +66,6 @@ describe('Volumes', function () { it('cannot add duplicate name', function (done) { volumes.add('music', '/media/cloudron-test-music', AUDIT_SOURCE, function (error) { - console.dir(error); expect(error.reason).to.be(BoxError.ALREADY_EXISTS); done(); }); diff --git a/src/tokendb.js b/src/tokendb.js index 75fc7fe1a..314e0c4f2 100644 --- a/src/tokendb.js +++ b/src/tokendb.js @@ -3,13 +3,14 @@ 'use strict'; exports = module.exports = { - get: get, - getByAccessToken: getByAccessToken, - delByAccessToken: delByAccessToken, - add: add, - del: del, - getByIdentifier: getByIdentifier, - delExpired: delExpired, + get, + getByAccessToken, + delByAccessToken, + add, + del, + getByIdentifier, + delExpired, + update, _clear: clear }; @@ -18,7 +19,7 @@ var assert = require('assert'), BoxError = require('./boxerror.js'), database = require('./database.js'); -var TOKENS_FIELDS = [ 'id', 'accessToken', 'identifier', 'clientId', 'scope', 'expires', 'name' ].join(','); +var TOKENS_FIELDS = [ 'id', 'accessToken', 'identifier', 'clientId', 'scope', 'expires', 'name', 'lastUsedTime' ].join(','); function getByAccessToken(accessToken, callback) { assert.strictEqual(typeof accessToken, 'string'); @@ -79,6 +80,27 @@ function add(token, callback) { }); } +function update(id, values, callback) { + assert.strictEqual(typeof id, 'string'); + assert.strictEqual(typeof values, 'object'); + assert.strictEqual(typeof callback, 'function'); + + let args = [ ]; + let fields = [ ]; + for (let k in values) { + fields.push(k + ' = ?'); + args.push(values[k]); + } + args.push(id); + + database.query('UPDATE tokens SET ' + fields.join(', ') + ' WHERE id = ?', args, function (error, result) { + if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); + if (result.affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'Token not found')); + + return callback(null); + }); +} + function del(id, callback) { assert.strictEqual(typeof id, 'string'); assert.strictEqual(typeof callback, 'function'); diff --git a/src/tokens.js b/src/tokens.js index 6807bc8ef..7baffd784 100644 --- a/src/tokens.js +++ b/src/tokens.js @@ -1,12 +1,14 @@ 'use strict'; exports = module.exports = { - add: add, - get: get, - del: del, - getAllByUserId: getAllByUserId, + add, + get, + update, + del, + getAllByUserId, + getByAccessToken, - validateTokenType: validateTokenType, + validateTokenType, // token client ids. we categorize them so we can have different restrictions based on the client ID_WEBADMIN: 'cid-webadmin', // dashboard oauth @@ -103,3 +105,22 @@ function getAllByUserId(userId, callback) { callback(null, result); }); } + +function getByAccessToken(id, callback) { + assert.strictEqual(typeof id, 'string'); + assert.strictEqual(typeof callback, 'function'); + + tokendb.getByAccessToken(id, function (error, result) { + if (error) return callback(error); + + callback(null, result); + }); +} + +function update(id, values, callback) { + assert.strictEqual(typeof id, 'string'); + assert.strictEqual(typeof values, 'object'); + assert.strictEqual(typeof callback, 'function'); + + tokendb.update(id, values, callback); +} \ No newline at end of file