diff --git a/src/network.js b/src/network.js index 5b23e1d83..00903b7d8 100644 --- a/src/network.js +++ b/src/network.js @@ -17,48 +17,36 @@ const assert = require('assert'), const SET_BLOCKLIST_CMD = path.join(__dirname, 'scripts/setblocklist.sh'); -function getBlocklist(callback) { - assert.strictEqual(typeof callback, 'function'); - - settings.getFirewallBlocklist(function (error, blocklist) { - if (error) return callback(error); - - callback(null, blocklist); - }); +async function getBlocklist() { + return await settings.getFirewallBlocklist(); } -function setBlocklist(blocklist, auditSource, callback) { +async function setBlocklist(blocklist, auditSource) { assert.strictEqual(typeof blocklist, 'string'); assert.strictEqual(typeof auditSource, 'object'); - assert.strictEqual(typeof callback, 'function'); const parsedIp = ipaddr.process(auditSource.ip); for (const line of blocklist.split('\n')) { if (!line || line.startsWith('#')) continue; const rangeOrIP = line.trim(); - if (!validator.isIP(rangeOrIP) && !validator.isIPRange(rangeOrIP)) return callback(new BoxError(BoxError.BAD_FIELD, `${rangeOrIP} is not a valid IP or range`)); + if (!validator.isIP(rangeOrIP) && !validator.isIPRange(rangeOrIP)) throw new BoxError(BoxError.BAD_FIELD, `${rangeOrIP} is not a valid IP or range`); if (rangeOrIP.indexOf('/') === -1) { - if (auditSource.ip === rangeOrIP) return callback(new BoxError(BoxError.BAD_FIELD, `${rangeOrIP} includes client IP. Cannot block yourself`)); + if (auditSource.ip === rangeOrIP) throw new BoxError(BoxError.BAD_FIELD, `${rangeOrIP} includes client IP. Cannot block yourself`); } else { const parsedRange = ipaddr.parseCIDR(rangeOrIP); // returns [addr, range] - if (parsedRange[0].kind() === parsedIp.kind() && parsedIp.match(parsedRange)) return callback(new BoxError(BoxError.BAD_FIELD, `${rangeOrIP} includes client IP. Cannot block yourself`)); + if (parsedRange[0].kind() === parsedIp.kind() && parsedIp.match(parsedRange)) throw new BoxError(BoxError.BAD_FIELD, `${rangeOrIP} includes client IP. Cannot block yourself`); } } - if (settings.isDemo()) return callback(new BoxError(BoxError.CONFLICT, 'Not allowed in demo mode')); + if (settings.isDemo()) throw new BoxError(BoxError.CONFLICT, 'Not allowed in demo mode'); - settings.setFirewallBlocklist(blocklist, function (error) { - if (error) return callback(error); + await settings.setFirewallBlocklist(blocklist); - // this is done only because it's easier for the shell script and the firewall service to get the value - if (!safe.fs.writeFileSync(paths.FIREWALL_BLOCKLIST_FILE, blocklist + '\n', 'utf8')) return callback(new BoxError(BoxError.FS_ERROR, safe.error.message)); + // this is done only because it's easier for the shell script and the firewall service to get the value + if (!safe.fs.writeFileSync(paths.FIREWALL_BLOCKLIST_FILE, blocklist + '\n', 'utf8')) throw new BoxError(BoxError.FS_ERROR, safe.error.message); - shell.sudo('setBlocklist', [ SET_BLOCKLIST_CMD ], {}, function (error) { - if (error) return callback(new BoxError(BoxError.IPTABLES_ERROR, `Error setting blocklist: ${error.message}`)); - - callback(null); - }); - }); + const [error] = await safe(shell.promises.sudo('setBlocklist', [ SET_BLOCKLIST_CMD ], {})); + if (error) throw new BoxError(BoxError.IPTABLES_ERROR, `Error setting blocklist: ${error.message}`); } diff --git a/src/routes/branding.js b/src/routes/branding.js index ee44ec96b..3493c9018 100644 --- a/src/routes/branding.js +++ b/src/routes/branding.js @@ -88,30 +88,28 @@ function getAppstoreListingConfig(req, res, next) { }); } -function setCloudronAvatar(req, res, next) { +async function setCloudronAvatar(req, res, next) { assert.strictEqual(typeof req.files, 'object'); if (!req.files.avatar) return next(new HttpError(400, 'avatar must be provided')); const avatar = safe.fs.readFileSync(req.files.avatar.path); if (!avatar) return next(500, safe.error.message); - settings.setCloudronAvatar(avatar, function (error) { - if (error) return next(BoxError.toHttpError(error)); + const [error] = await safe(settings.setCloudronAvatar(avatar)); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(202, {})); - }); + next(new HttpSuccess(202, {})); } -function getCloudronAvatar(req, res, next) { - settings.getCloudronAvatar(function (error, avatar) { - if (error) return next(BoxError.toHttpError(error)); +async function getCloudronAvatar(req, res, next) { + const [error, avatar] = await safe(settings.getCloudronAvatar()); + if (error) return next(BoxError.toHttpError(error)); - // avoid caching the avatar on the client to see avatar changes immediately - res.set('Cache-Control', 'no-cache'); + // avoid caching the avatar on the client to see avatar changes immediately + res.set('Cache-Control', 'no-cache'); - res.set('Content-Type', 'image/png'); - res.status(200).send(avatar); - }); + res.set('Content-Type', 'image/png'); + res.status(200).send(avatar); } function get(req, res, next) { diff --git a/src/routes/network.js b/src/routes/network.js index 5201dbbec..5a84faf0a 100644 --- a/src/routes/network.js +++ b/src/routes/network.js @@ -5,31 +5,30 @@ exports = module.exports = { setBlocklist }; -var assert = require('assert'), +const assert = require('assert'), auditSource = require('../auditsource.js'), BoxError = require('../boxerror.js'), HttpError = require('connect-lastmile').HttpError, HttpSuccess = require('connect-lastmile').HttpSuccess, - network = require('../network.js'); + network = require('../network.js'), + safe = require('safetydance'); -function getBlocklist(req, res, next) { - network.getBlocklist(function (error, blocklist) { - if (error) return next(BoxError.toHttpError(error)); +async function getBlocklist(req, res, next) { + const [error, blocklist] = await safe(network.getBlocklist()); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(200, { blocklist })); - }); + next(new HttpSuccess(200, { blocklist })); } -function setBlocklist(req, res, next) { +async function setBlocklist(req, res, next) { assert.strictEqual(typeof req.body, 'object'); if (typeof req.body.blocklist !== 'string') return next(new HttpError(400, 'blocklist must be a string')); req.clearTimeout(); // can take a while if there is a lot of network ranges - network.setBlocklist(req.body.blocklist, auditSource.fromRequest(req), function (error) { - if (error) return next(BoxError.toHttpError(error)); + const [error] = await safe(network.setBlocklist(req.body.blocklist, auditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(200, {})); - }); + next(new HttpSuccess(200, {})); } diff --git a/src/settings.js b/src/settings.js index b241cd01d..e0d081038 100644 --- a/src/settings.js +++ b/src/settings.js @@ -141,6 +141,7 @@ const assert = require('assert'), _ = require('underscore'); const SETTINGS_FIELDS = [ 'name', 'value' ].join(','); +const SETTINGS_BLOB_FIELDS = [ 'name', 'valueBlob' ].join(','); let gDefaults = (function () { var result = { }; @@ -217,6 +218,42 @@ function notifyChange(key, value) { cron.handleSettingsChanged(key, value); } +async function get(key) { + assert.strictEqual(typeof key, 'string'); + + const result = await database.query(`SELECT ${SETTINGS_FIELDS} FROM settings WHERE name = ?`, [ key ]); + if (result.length === 0) return gDefaults[key]; + + return result[0].value; +} + +async function set(key, value) { + assert.strictEqual(typeof key, 'string'); + assert(value === null || typeof value === 'string'); + + await database.query('INSERT INTO settings (name, value) VALUES (?, ?) ON DUPLICATE KEY UPDATE value=VALUES(value)', [ key, value ]); // don't rely on affectedRows here since it gives 2 +} + +async function getBlob(key) { + assert.strictEqual(typeof key, 'string'); + + const result = await database.query(`SELECT ${SETTINGS_BLOB_FIELDS} FROM settings WHERE name = ?`, [ key ]); + if (result.length === 0) return null; + + return result[0].valueBlob; +} + +async function setBlob(key, value) { + assert.strictEqual(typeof key, 'string'); + assert(value === null || Buffer.isBuffer(value)); + + await database.query('INSERT INTO settings (name, valueBlob) VALUES (?, ?) ON DUPLICATE KEY UPDATE valueBlob=VALUES(valueBlob)', [ key, value ]); // don't rely on affectedRows here since it gives 2 +} + +async function clear() { + await database.query('DELETE FROM settings'); +} + function setAutoupdatePattern(pattern, callback) { assert.strictEqual(typeof pattern, 'string'); assert.strictEqual(typeof callback, 'function'); @@ -302,31 +339,21 @@ function setCloudronName(name, callback) { }); } -function getCloudronAvatar(callback) { - assert.strictEqual(typeof callback, 'function'); +async function getCloudronAvatar() { + let avatar = await getBlob(exports.CLOUDRON_AVATAR_KEY); + if (avatar) return avatar; - settingsdb.getBlob(exports.CLOUDRON_AVATAR_KEY, function (error, avatar) { - if (error && error.reason !== BoxError.NOT_FOUND) return callback(error); + // try default fallback + avatar = safe.fs.readFileSync(paths.CLOUDRON_DEFAULT_AVATAR_FILE); + if (avatar) return avatar; - if (avatar) return callback(null, avatar); - - // try default fallback - avatar = safe.fs.readFileSync(paths.CLOUDRON_DEFAULT_AVATAR_FILE); - if (avatar) return callback(null, avatar); - - callback(new BoxError(BoxError.FS_ERROR, safe.error)); - }); + throw new BoxError(BoxError.FS_ERROR, `Could not read avatar: ${safe.error.message}`); } -function setCloudronAvatar(avatar, callback) { +async function setCloudronAvatar(avatar) { assert(Buffer.isBuffer(avatar)); - assert.strictEqual(typeof callback, 'function'); - settingsdb.setBlob(exports.CLOUDRON_AVATAR_KEY, avatar, function (error) { - if (error) return callback(error); - - return callback(null); - }); + await setBlob(exports.CLOUDRON_AVATAR_KEY, avatar); } function getDynamicDnsConfig(callback) { @@ -669,27 +696,18 @@ function setAppstoreListingConfig(listingConfig, callback) { }); } -function getFirewallBlocklist(callback) { - assert.strictEqual(typeof callback, 'function'); +async function getFirewallBlocklist() { + const value = await getBlob(exports.FIREWALL_BLOCKLIST_KEY); + if (value === null) return gDefaults[exports.FIREWALL_BLOCKLIST_KEY]; - settingsdb.getBlob(exports.FIREWALL_BLOCKLIST_KEY, function (error, value) { - if (error && error.reason === BoxError.NOT_FOUND) return callback(null, gDefaults[exports.FIREWALL_BLOCKLIST_KEY]); - if (error) return callback(error); - - callback(null, value.toString('utf8')); - }); + return value.toString('utf8'); } -function setFirewallBlocklist(blocklist, callback) { +async function setFirewallBlocklist(blocklist) { assert.strictEqual(typeof blocklist, 'string'); - assert.strictEqual(typeof callback, 'function'); // store in blob since the value field is TEXT and has 16kb size limit - settingsdb.setBlob(exports.FIREWALL_BLOCKLIST_KEY, Buffer.from(blocklist), function (error) { - if (error) return callback(error); - - callback(null); - }); + await setBlob(exports.FIREWALL_BLOCKLIST_KEY, Buffer.from(blocklist)); } function getSupportConfig(callback) { diff --git a/src/settingsdb.js b/src/settingsdb.js index 20ba848a9..31e043931 100644 --- a/src/settingsdb.js +++ b/src/settingsdb.js @@ -16,7 +16,6 @@ const assert = require('assert'), database = require('./database.js'); const SETTINGS_FIELDS = [ 'name', 'value' ].join(','); -const SETTINGS_BLOB_FIELDS = [ 'name', 'valueBlob' ].join(','); function get(key, callback) { assert.strictEqual(typeof key, 'string'); @@ -51,31 +50,6 @@ function set(key, value, callback) { }); } -function getBlob(key, callback) { - assert.strictEqual(typeof key, 'string'); - assert.strictEqual(typeof callback, 'function'); - - database.query(`SELECT ${SETTINGS_BLOB_FIELDS} FROM settings WHERE name = ?`, [ key ], function (error, result) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Setting not found')); - - callback(null, result[0].valueBlob); - }); -} - -function setBlob(key, value, callback) { - assert.strictEqual(typeof key, 'string'); - assert(value === null || Buffer.isBuffer(value)); - assert.strictEqual(typeof callback, 'function'); - - database.query('INSERT INTO settings (name, valueBlob) VALUES (?, ?) ON DUPLICATE KEY UPDATE valueBlob=VALUES(valueBlob)', [ key, value ], function (error) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); // don't rely on affectedRows here since it gives 2 - - callback(null); - }); -} - - function clear(callback) { database.query('DELETE FROM settings', function (error) { if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); diff --git a/src/test/network-test.js b/src/test/network-test.js index e7da7e41b..cdf4a027e 100644 --- a/src/test/network-test.js +++ b/src/test/network-test.js @@ -10,193 +10,103 @@ const BoxError = require('../boxerror.js'), expect = require('expect.js'), fs = require('fs'), network = require('../network.js'), - paths = require('../paths.js'); + paths = require('../paths.js'), + safe = require('safetydance'); describe('Network', function () { - const { setup, cleanup } = common; + const { setup, cleanup, auditSource } = common; before(setup); after(cleanup); describe('Blocklist', function () { - before(function () { fs.writeFileSync(paths.FIREWALL_BLOCKLIST_FILE, '', 'utf8'); }); - it('can get empty blocklist', function (done) { - network.getBlocklist(function (error, result) { - expect(error).to.equal(null); - expect(result).to.equal(''); - - done(); - }); + it('can get empty blocklist', async function () { + const result = await network.getBlocklist(); + expect(result).to.equal(''); }); - it('can set empty blocklist', function (done) { - network.setBlocklist('', { ip: '127.0.0.1' }, function (error) { - expect(error).to.equal(null); - - network.getBlocklist(function (error, result) { - expect(error).to.equal(null); - expect(result).to.equal(''); - - done(); - }); - }); + it('can set empty blocklist', async function () { + await network.setBlocklist('', auditSource); + const result = await network.getBlocklist(); + expect(result).to.equal(''); }); - it('can set single IPv4 in blocklist', function (done) { - network.setBlocklist('192.168.178.1', { ip: '127.0.0.1' }, function (error) { - expect(error).to.equal(null); - - network.getBlocklist(function (error, result) { - expect(error).to.equal(null); - expect(result).to.equal('192.168.178.1'); - - done(); - }); - }); + it('can set single IPv4 in blocklist', async function () { + await network.setBlocklist('192.168.178.1', auditSource); + const result = await network.getBlocklist(); + expect(result).to.equal('192.168.178.1'); }); - it('can set single IPv6 in blocklist', function (done) { - network.setBlocklist('2a02:8106:2f:bb00:7afc:5703:ee71:3ef8', { ip: '127.0.0.1' }, function (error) { - expect(error).to.equal(null); - - network.getBlocklist(function (error, result) { - expect(error).to.equal(null); - expect(result).to.equal('2a02:8106:2f:bb00:7afc:5703:ee71:3ef8'); - - done(); - }); - }); + it('can set single IPv6 in blocklist', async function () { + await network.setBlocklist('2a02:8106:2f:bb00:7afc:5703:ee71:3ef8', auditSource); + const result = await network.getBlocklist(); + expect(result).to.equal('2a02:8106:2f:bb00:7afc:5703:ee71:3ef8'); }); - it('can set mixed IPs with comment in blocklist', function (done) { - network.setBlocklist('2a02:8106:2f:bb00:7afc:5703:ee71:3ef8\n# some comment\n192.168.178.1', { ip: '127.0.0.1' }, function (error) { - expect(error).to.equal(null); - - network.getBlocklist(function (error, result) { - expect(error).to.equal(null); - expect(result).to.equal('2a02:8106:2f:bb00:7afc:5703:ee71:3ef8\n# some comment\n192.168.178.1'); - - done(); - }); - }); + it('can set mixed IPs with comment in blocklist', async function () { + await network.setBlocklist('2a02:8106:2f:bb00:7afc:5703:ee71:3ef8\n# some comment\n192.168.178.1', auditSource); + const result = await network.getBlocklist(); + expect(result).to.equal('2a02:8106:2f:bb00:7afc:5703:ee71:3ef8\n# some comment\n192.168.178.1'); }); - it('can set single IPv4 range in blocklist', function (done) { - network.setBlocklist('192.168.178.1/24', { ip: '127.0.0.1' }, function (error) { - expect(error).to.equal(null); - - network.getBlocklist(function (error, result) { - expect(error).to.equal(null); - expect(result).to.equal('192.168.178.1/24'); - - done(); - }); - }); + it('can set single IPv4 range in blocklist', async function () { + await network.setBlocklist('192.168.178.1/24', auditSource); + const result = await network.getBlocklist(); + expect(result).to.equal('192.168.178.1/24'); }); - it('can set single IPv6 range in blocklist', function (done) { - network.setBlocklist('2001:db8::', { ip: '127.0.0.1' }, function (error) { - expect(error).to.equal(null); - - network.getBlocklist(function (error, result) { - expect(error).to.equal(null); - expect(result).to.equal('2001:db8::'); - - done(); - }); - }); + it('can set single IPv6 range in blocklist', async function () { + await network.setBlocklist('2001:db8::', auditSource); + const result = await network.getBlocklist(); + expect(result).to.equal('2001:db8::'); }); - it('cannot set IPv4 in blocklist if source is same', function (done) { - network.setBlocklist('127.0.0.1', { ip: '127.0.0.1' }, function (error) { - expect(error).to.be.a(BoxError); - expect(error.reason).to.equal(BoxError.BAD_FIELD); - - done(); - }); + it('cannot set IPv4 in blocklist if source is same', async function () { + const [error] = await safe(network.setBlocklist('127.0.0.1', { ip: '127.0.0.1' })); + expect(error.reason).to.equal(BoxError.BAD_FIELD); }); - it('cannot set IPv6 in blocklist if source is same', function (done) { - network.setBlocklist('2001:db8:1234::1', { ip: '2001:db8:1234::1' }, function (error) { - expect(error).to.be.a(BoxError); - expect(error.reason).to.equal(BoxError.BAD_FIELD); - - done(); - }); + it('cannot set IPv6 in blocklist if source is same', async function () { + const [error] = await safe(network.setBlocklist('2001:db8:1234::1', { ip: '2001:db8:1234::1' })); + expect(error.reason).to.equal(BoxError.BAD_FIELD); }); - it('cannot set IPv4 range in blocklist if source is same', function (done) { - network.setBlocklist('127.0.0.1/32', { ip: '127.0.0.1' }, function (error) { - expect(error).to.be.a(BoxError); - expect(error.reason).to.equal(BoxError.BAD_FIELD); - - done(); - }); + it('cannot set IPv4 range in blocklist if source is same', async function () { + const [error] = await safe(network.setBlocklist('127.0.0.1/32', { ip: '127.0.0.1' })); + expect(error.reason).to.equal(BoxError.BAD_FIELD); }); - it('cannot set IPv6 range in blocklist if source is same', function (done) { - network.setBlocklist('2001:db8:1234:::', { ip: '2001:db8:1234::1' }, function (error) { - expect(error).to.be.a(BoxError); - expect(error.reason).to.equal(BoxError.BAD_FIELD); - - done(); - }); + it('cannot set IPv6 range in blocklist if source is same', async function () { + const [error] = await safe(network.setBlocklist('2001:db8:1234:::', { ip: '2001:db8:1234::1' })); + expect(error.reason).to.equal(BoxError.BAD_FIELD); }); - it('can set IPv4 in blocklist if source is IPv6', function (done) { - network.setBlocklist('192.168.178.1', { ip: '2001:db8:1234::1' }, function (error) { - expect(error).to.equal(null); - - network.getBlocklist(function (error, result) { - expect(error).to.equal(null); - expect(result).to.equal('192.168.178.1'); - - done(); - }); - }); + it('can set IPv4 in blocklist if source is IPv6', async function () { + await network.setBlocklist('192.168.178.1', { ip: '2001:db8:1234::1' }); + const result = await network.getBlocklist(); + expect(result).to.equal('192.168.178.1'); }); - it('can set IPv6 in blocklist if source is IPv4', function (done) { - network.setBlocklist('2001:db8:1234::1', { ip: '127.0.0.1' }, function (error) { - expect(error).to.equal(null); - - network.getBlocklist(function (error, result) { - expect(error).to.equal(null); - expect(result).to.equal('2001:db8:1234::1'); - - done(); - }); - }); + it('can set IPv6 in blocklist if source is IPv4', async function () { + await network.setBlocklist('2001:db8:1234::1', auditSource); + const result = await network.getBlocklist(); + expect(result).to.equal('2001:db8:1234::1'); }); - it('can set IPv4 range in blocklist if source is IPv6', function (done) { - network.setBlocklist('192.168.178.1/32', { ip: '2001:db8:1234::1' }, function (error) { - expect(error).to.equal(null); - - network.getBlocklist(function (error, result) { - expect(error).to.equal(null); - expect(result).to.equal('192.168.178.1/32'); - - done(); - }); - }); + it('can set IPv4 range in blocklist if source is IPv6', async function () { + await network.setBlocklist('192.168.178.1/32', { ip: '2001:db8:1234::1' }); + const result = await network.getBlocklist(); + expect(result).to.equal('192.168.178.1/32'); }); - it('can set IPv6 range in blocklist if source is IPv4', function (done) { - network.setBlocklist('2001:db8:1234::', { ip: '127.0.0.1' }, function (error) { - expect(error).to.equal(null); - - network.getBlocklist(function (error, result) { - expect(error).to.equal(null); - expect(result).to.equal('2001:db8:1234::'); - - done(); - }); - }); + it('can set IPv6 range in blocklist if source is IPv4', async function () { + await network.setBlocklist('2001:db8:1234::', auditSource); + const result = await network.getBlocklist(); + expect(result).to.equal('2001:db8:1234::'); }); }); }); diff --git a/src/test/settings-test.js b/src/test/settings-test.js index a08e53976..94f2897e6 100644 --- a/src/test/settings-test.js +++ b/src/test/settings-test.js @@ -70,12 +70,9 @@ describe('Settings', function () { }); }); - it('can get default cloudron avatar', function (done) { - settings.getCloudronAvatar(function (error, gravatar) { - expect(error).to.be(null); - expect(gravatar).to.be.a(Buffer); - done(); - }); + it('can get default cloudron avatar', async function () { + const avatar = await settings.getCloudronAvatar(); + expect(avatar).to.be.a(Buffer); }); it('can get backup config', function (done) {