diff --git a/src/apptask.js b/src/apptask.js index 49a417a4a..ebbb6aa70 100644 --- a/src/apptask.js +++ b/src/apptask.js @@ -243,13 +243,12 @@ async function waitForDnsPropagation(app) { } const ipv4 = await sysinfo.getServerIPv4(); - const ipv6Enabled = await settings.getIPv6Config(); - const ipv6 = ipv6Enabled ? await sysinfo.getServerIPv6() : null; + const ipv6 = await sysinfo.getServerIPv6(); let error; [error] = await safe(dns.waitForDnsRecord(app.subdomain, app.domain, 'A', ipv4, { times: 240 })); if (error) throw new BoxError(BoxError.DNS_ERROR, `DNS A Record is not synced yet: ${error.message}`, { ipv4, subdomain: app.subdomain, domain: app.domain }); - if (ipv6Enabled) { + if (ipv6) { [error] = await safe(dns.waitForDnsRecord(app.subdomain, app.domain, 'AAAA', ipv6, { times: 240 })); if (error) throw new BoxError(BoxError.DNS_ERROR, `DNS AAAA Record is not synced yet: ${error.message}`, { ipv6, subdomain: app.subdomain, domain: app.domain }); } @@ -259,11 +258,10 @@ async function waitForDnsPropagation(app) { for (const domain of allDomains) { [error] = await safe(dns.waitForDnsRecord(domain.subdomain, domain.domain, 'A', ipv4, { times: 240 })); if (error) throw new BoxError(BoxError.DNS_ERROR, `DNS A Record is not synced yet: ${error.message}`, { ipv4, subdomain: domain.subdomain, domain: domain.domain }); - if (ipv6Enabled) { + if (ipv6) { [error] = await safe(dns.waitForDnsRecord(domain.subdomain, domain.domain, 'AAAA', ipv6, { times: 240 })); if (error) throw new BoxError(BoxError.DNS_ERROR, `DNS AAAA Record is not synced yet: ${error.message}`, { ipv6, subdomain: domain.subdomain, domain: domain.domain }); } - } } diff --git a/src/cloudron.js b/src/cloudron.js index ae5babed2..771f1a602 100644 --- a/src/cloudron.js +++ b/src/cloudron.js @@ -328,15 +328,14 @@ async function setupDnsAndCert(subdomain, domain, auditSource, progressCallback) const dashboardFqdn = dns.fqdn(subdomain, domainObject); const ipv4 = await sysinfo.getServerIPv4(); - const ipv6Enabled = await settings.getIPv6Config(); - const ipv6 = ipv6Enabled ? await sysinfo.getServerIPv6() : null; + const ipv6 = await sysinfo.getServerIPv6(); progressCallback({ percent: 20, message: `Updating DNS of ${dashboardFqdn}` }); await dns.upsertDnsRecords(subdomain, domain, 'A', [ ipv4 ]); - if (ipv6Enabled) await dns.upsertDnsRecords(subdomain, domain, 'AAAA', [ ipv6 ]); + if (ipv6) await dns.upsertDnsRecords(subdomain, domain, 'AAAA', [ ipv6 ]); progressCallback({ percent: 40, message: `Waiting for DNS of ${dashboardFqdn}` }); await dns.waitForDnsRecord(subdomain, domain, 'A', ipv4, { interval: 30000, times: 50000 }); - if (ipv6Enabled) await dns.waitForDnsRecord(subdomain, domain, 'AAAA', ipv6, { interval: 30000, times: 50000 }); + if (ipv6) await dns.waitForDnsRecord(subdomain, domain, 'AAAA', ipv6, { interval: 30000, times: 50000 }); progressCallback({ percent: 60, message: `Getting certificate of ${dashboardFqdn}` }); await reverseProxy.ensureCertificate(dns.fqdn(subdomain, domainObject), domain, auditSource); } diff --git a/src/dns.js b/src/dns.js index 4dacc8bf1..0ad86eae2 100644 --- a/src/dns.js +++ b/src/dns.js @@ -128,10 +128,9 @@ async function checkDnsRecords(subdomain, domain) { // if empty OR exactly one record with the ip, we don't need to overwrite if (ipv4Records.length !== 0 && (ipv4Records.length !== 1 || ipv4Records[0] !== ipv4)) return { needsOverwrite: true }; - const ipv6Enabled = await settings.getIPv6Config(); - if (ipv6Enabled) { + const ipv6 = await sysinfo.getServerIPv6(); + if (ipv6) { const ipv6Records = await getDnsRecords(subdomain, domain, 'AAAA'); - const ipv6 = await sysinfo.getServerIPv6(); // if empty OR exactly one record with the ip, we don't need to overwrite if (ipv6Records.length !== 0 && (ipv6Records.length !== 1 || ipaddr.parse(ipv6Records[0]).toRFC5952String() !== ipv6)) return { needsOverwrite: true }; @@ -222,15 +221,14 @@ async function registerLocations(locations, options, progressCallback) { debug(`registerLocations: Will register ${JSON.stringify(locations)} with options ${JSON.stringify(options)}`); const ipv4 = await sysinfo.getServerIPv4(); - const ipv6Enabled = await settings.getIPv6Config(); - const ipv6 = ipv6Enabled ? await sysinfo.getServerIPv6() : null; + const ipv6 = await sysinfo.getServerIPv6(); for (const location of locations) { progressCallback({ message: `Registering location: ${location.subdomain ? (location.subdomain + '.') : ''}${location.domain}` }); await promiseRetry({ times: 200, interval: 5000, debug, retry: (error) => error.retryable }, async function () { await registerLocation(location, options, 'A', ipv4); - if (ipv6Enabled) await registerLocation(location, options, 'AAAA', ipv6); + if (ipv6) await registerLocation(location, options, 'AAAA', ipv6); }); } } @@ -250,15 +248,14 @@ async function unregisterLocations(locations, progressCallback) { assert.strictEqual(typeof progressCallback, 'function'); const ipv4 = await sysinfo.getServerIPv4(); - const ipv6Enabled = await settings.getIPv6Config(); - const ipv6 = ipv6Enabled ? await sysinfo.getServerIPv6() : null; + const ipv6 = await sysinfo.getServerIPv6(); for (const location of locations) { progressCallback({ message: `Unregistering location: ${location.subdomain ? (location.subdomain + '.') : ''}${location.domain}` }); await promiseRetry({ times: 30, interval: 5000, debug, retry: (error) => error.retryable }, async function () { await unregisterLocation(location, 'A', ipv4); - if (ipv6Enabled) await unregisterLocation(location, 'AAAA', ipv6); + if (ipv6) await unregisterLocation(location, 'AAAA', ipv6); }); } } diff --git a/src/dns/wildcard.js b/src/dns/wildcard.js index bc370d6f6..598778906 100644 --- a/src/dns/wildcard.js +++ b/src/dns/wildcard.js @@ -16,7 +16,6 @@ const assert = require('assert'), dig = require('../dig.js'), dns = require('../dns.js'), safe = require('safetydance'), - settings = require('../settings.js'), sysinfo = require('../sysinfo.js'), waitForDns = require('./waitfordns.js'); @@ -87,13 +86,12 @@ async function verifyDomainConfig(domainObject) { const ipv4 = await sysinfo.getServerIPv4(); if (ipv4Result.length !== 1 || ipv4 !== ipv4Result[0]) throw new BoxError(BoxError.EXTERNAL_ERROR, `Domain resolves to ${JSON.stringify(ipv4Result)} instead of IPv4 ${ipv4}`); - const ipv6Enabled = await settings.getIPv6Config(); - if (ipv6Enabled) { + const ipv6 = await sysinfo.getServerIPv6(); // both should be RFC 5952 format + if (ipv6) { const [ipv6Error, ipv6Result] = await safe(dig.resolve(fqdn, 'AAAA', { server: '127.0.0.1', timeout: 5000 })); if (ipv6Error && ipv6Error.code === 'ENOTFOUND') throw new BoxError(BoxError.BAD_FIELD, `Unable to resolve IPv6 of ${fqdn}`); if (ipv6Error || !ipv6Result) throw new BoxError(BoxError.BAD_FIELD, ipv6Error ? ipv6Error.message : `Unable to resolve IPv6 of ${fqdn}`); - const ipv6 = await sysinfo.getServerIPv6(); // both should be RFC 5952 format if (ipv6Result.length !== 1 || ipv6 !== ipv6Result[0]) throw new BoxError(BoxError.EXTERNAL_ERROR, `Domain resolves to ${JSON.stringify(ipv6Result)} instead of IPv6 ${ipv6}`); } diff --git a/src/dyndns.js b/src/dyndns.js index fcec307ff..65ad76586 100644 --- a/src/dyndns.js +++ b/src/dyndns.js @@ -20,8 +20,7 @@ async function sync(auditSource) { assert.strictEqual(typeof auditSource, 'object'); const ipv4 = await sysinfo.getServerIPv4(); - const ipv6Enabled = await settings.getIPv6Config(); - const ipv6 = ipv6Enabled ? await sysinfo.getServerIPv6() : null; + const ipv6 = await sysinfo.getServerIPv6(); const info = safe.JSON.parse(safe.fs.readFileSync(paths.DYNDNS_INFO_FILE, 'utf8')) || { ipv4: null, ipv6: null }; if (info.ip) { // legacy cache file @@ -29,7 +28,7 @@ async function sync(auditSource) { delete info.ip; } const ipv4Changed = info.ip !== ipv4; - const ipv6Changed = ipv6Enabled && info.ipv6 !== ipv6; // both should be RFC 5952 format + const ipv6Changed = ipv6 && info.ipv6 !== ipv6; // both should be RFC 5952 format if (!ipv4Changed && !ipv6Changed) { debug(`refreshDNS: no change in IP ipv4: ${ipv4} ipv6: ${ipv6}`); diff --git a/src/provision.js b/src/provision.js index 8effd8d4b..807def63b 100644 --- a/src/provision.js +++ b/src/provision.js @@ -230,7 +230,7 @@ async function restore(backupConfig, backupId, version, sysinfoConfig, options, backupConfig.encryption = null; } - error = await sysinfo.testConfig(sysinfoConfig); + error = await sysinfo.testIPv4Config(sysinfoConfig); if (error) throw error; safe(restoreTask(backupConfig, backupId, sysinfoConfig, options, auditSource), { debug }); // now that args are validated run the task in the background diff --git a/src/routes/settings.js b/src/routes/settings.js index 718db5679..7aae87c68 100644 --- a/src/routes/settings.js +++ b/src/routes/settings.js @@ -176,18 +176,18 @@ async function setDynamicDnsConfig(req, res, next) { } async function getIPv6Config(req, res, next) { - const [error, enabled] = await safe(settings.getIPv6Config()); + const [error, ipv6Config] = await safe(settings.getSysinfoConfig()); if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(200, { enabled })); + next(new HttpSuccess(200, ipv6Config)); } async function setIPv6Config(req, res, next) { assert.strictEqual(typeof req.body, 'object'); - if (typeof req.body.enabled !== 'boolean') return next(new HttpError(400, 'enabled boolean is required')); + if (!req.body.provider || typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider is required')); - const [error] = await safe(settings.setIPv6Config(req.body.enabled)); + const [error] = await safe(settings.setIPv6Config(req.body)); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(200, {})); @@ -295,7 +295,7 @@ function get(req, res, next) { switch (req.params.setting) { case settings.DYNAMIC_DNS_KEY: return getDynamicDnsConfig(req, res, next); - case settings.IPV6_KEY: return getIPv6Config(req, res, next); + case settings.IPV6_CONFIG_KEY: return getIPv6Config(req, res, next); case settings.BACKUP_CONFIG_KEY: return getBackupConfig(req, res, next); case settings.EXTERNAL_LDAP_KEY: return getExternalLdapConfig(req, res, next); case settings.USER_DIRECTORY_KEY: return getUserDirectoryConfig(req, res, next); @@ -319,7 +319,7 @@ function set(req, res, next) { switch (req.params.setting) { case settings.DYNAMIC_DNS_KEY: return setDynamicDnsConfig(req, res, next); - case settings.IPV6_KEY: return setIPv6Config(req, res, next); + case settings.IPV6_CONFIG_KEY: return setIPv6Config(req, res, next); case settings.EXTERNAL_LDAP_KEY: return setExternalLdapConfig(req, res, next); case settings.USER_DIRECTORY_KEY: return setUserDirectoryConfig(req, res, next); case settings.UNSTABLE_APPS_KEY: return setUnstableAppsConfig(req, res, next); diff --git a/src/settings.js b/src/settings.js index 47de8d446..6dcd45557 100644 --- a/src/settings.js +++ b/src/settings.js @@ -93,7 +93,6 @@ exports = module.exports = { // booleans. if you add an entry here, be sure to fix list() DYNAMIC_DNS_KEY: 'dynamic_dns', UNSTABLE_APPS_KEY: 'unstable_apps', - IPV6_KEY: 'ipv6', DEMO_KEY: 'demo', // json. if you add an entry here, be sure to fix list() @@ -102,12 +101,13 @@ exports = module.exports = { EXTERNAL_LDAP_KEY: 'external_ldap_config', USER_DIRECTORY_KEY: 'user_directory_config', REGISTRY_CONFIG_KEY: 'registry_config', - SYSINFO_CONFIG_KEY: 'sysinfo_config', + SYSINFO_CONFIG_KEY: 'sysinfo_config', // misnomer: ipv4 config APPSTORE_LISTING_CONFIG_KEY: 'appstore_listing_config', SUPPORT_CONFIG_KEY: 'support_config', PROFILE_CONFIG_KEY: 'profile_config', GHOSTS_CONFIG_KEY: 'ghosts_config', REVERSE_PROXY_CONFIG_KEY: 'reverseproxy_config', + IPV6_CONFIG_KEY: 'ipv6', // strings AUTOUPDATE_PATTERN_KEY: 'autoupdate_pattern', @@ -171,7 +171,9 @@ const gDefaults = (function () { result[exports.TIME_ZONE_KEY] = 'America/Los_Angeles'; result[exports.CLOUDRON_NAME_KEY] = 'Cloudron'; result[exports.DYNAMIC_DNS_KEY] = false; - result[exports.IPV6_KEY] = false; + result[exports.IPV6_CONFIG_KEY] = { + provider: 'noop' + }; result[exports.UNSTABLE_APPS_KEY] = true; result[exports.LICENSE_KEY] = ''; result[exports.LANGUAGE_KEY] = 'en'; @@ -370,18 +372,21 @@ async function setDynamicDnsConfig(enabled) { } async function getIPv6Config() { - const enabled = await get(exports.IPV6_KEY); - if (enabled === null) return gDefaults[exports.IPV6_KEY]; - return !!enabled; // db holds string values only + const value = await get(exports.IPV6_CONFIG_KEY); + if (value === null) return gDefaults[exports.IPV6_CONFIG_KEY]; + return JSON.parse(value); } -async function setIPv6Config(enabled) { - assert.strictEqual(typeof enabled, 'boolean'); +async function setIPv6Config(ipv6Config) { + assert.strictEqual(typeof ipv6Config, 'object'); - // we don't validate if server has IPv6 intentionally. our api server could be down, maybe user assigns - // ipv6 later, fixed/static address is not defined yet etc - await set(exports.IPV6_KEY, enabled ? 'enabled' : ''); // db holds string values only - notifyChange(exports.IPV6_KEY, enabled); + if (isDemo()) throw new BoxError(BoxError.BAD_FIELD, 'Not allowed in demo mode'); + + const error = await sysinfo.testIPv6Config(ipv6Config); + if (error) throw error; + + await set(exports.IPV6_CONFIG_KEY, JSON.stringify(ipv6Config)); + notifyChange(exports.IPV6_CONFIG_KEY, ipv6Config); } async function getUnstableAppsConfig() { @@ -603,7 +608,7 @@ async function setSysinfoConfig(sysinfoConfig) { if (isDemo()) throw new BoxError(BoxError.BAD_FIELD, 'Not allowed in demo mode'); - const error = await sysinfo.testConfig(sysinfoConfig); + const error = await sysinfo.testIPv4Config(sysinfoConfig); if (error) throw error; await set(exports.SYSINFO_CONFIG_KEY, JSON.stringify(sysinfoConfig)); @@ -756,12 +761,11 @@ async function list() { // convert booleans result[exports.DYNAMIC_DNS_KEY] = !!result[exports.DYNAMIC_DNS_KEY]; - result[exports.IPV6_KEY] = !!result[exports.IPV6_KEY]; result[exports.UNSTABLE_APPS_KEY] = !!result[exports.UNSTABLE_APPS_KEY]; result[exports.DEMO_KEY] = !!result[exports.DEMO_KEY]; // convert JSON objects - [exports.BACKUP_CONFIG_KEY, exports.PROFILE_CONFIG_KEY, exports.SERVICES_CONFIG_KEY, exports.EXTERNAL_LDAP_KEY, exports.REGISTRY_CONFIG_KEY, exports.SYSINFO_CONFIG_KEY, exports.REVERSE_PROXY_CONFIG_KEY ].forEach(function (key) { + [exports.BACKUP_CONFIG_KEY, exports.IPV6_CONFIG_KEY, exports.PROFILE_CONFIG_KEY, exports.SERVICES_CONFIG_KEY, exports.EXTERNAL_LDAP_KEY, exports.REGISTRY_CONFIG_KEY, exports.SYSINFO_CONFIG_KEY, exports.REVERSE_PROXY_CONFIG_KEY ].forEach(function (key) { result[key] = typeof result[key] === 'object' ? result[key] : safe.JSON.parse(result[key]); }); diff --git a/src/sysinfo.js b/src/sysinfo.js index fba1e1e5c..a017b8f29 100644 --- a/src/sysinfo.js +++ b/src/sysinfo.js @@ -3,7 +3,8 @@ exports = module.exports = { getServerIPv4, getServerIPv6, - testConfig, + testIPv4Config, + testIPv6Config, hasIPv6 }; @@ -33,6 +34,8 @@ async function getServerIPv4() { async function getServerIPv6() { const config = await settings.getSysinfoConfig(); + if (config.provider === 'noop') return null; + const result = await api(config.provider).getServerIPv6(config); return ipaddr.parse(result).toRFC5952String(); } @@ -43,8 +46,14 @@ function hasIPv6() { return fs.existsSync(IPV6_PROC_FILE) && fs.readFileSync(IPV6_PROC_FILE, 'utf8').trim().length !== 0; } -async function testConfig(config) { +async function testIPv4Config(config) { assert.strictEqual(typeof config, 'object'); - return await api(config.provider).testConfig(config); + return await api(config.provider).testIPv4Config(config); +} + +async function testIPv6Config(config) { + assert.strictEqual(typeof config, 'object'); + + return await api(config.provider).testIPv6Config(config); } diff --git a/src/sysinfo/fixed.js b/src/sysinfo/fixed.js index 5d94b72a4..70c39fdab 100644 --- a/src/sysinfo/fixed.js +++ b/src/sysinfo/fixed.js @@ -3,7 +3,8 @@ exports = module.exports = { getServerIPv4, getServerIPv6, - testConfig + testIPv4Config, + testIPv6Config }; const assert = require('assert'), @@ -24,16 +25,20 @@ async function getServerIPv6(config) { throw new BoxError(BoxError.NETWORK_ERROR, 'No IPv6 configured'); } -async function testConfig(config) { +async function testIPv4Config(config) { assert.strictEqual(typeof config, 'object'); if (typeof config.ipv4 !== 'string') return new BoxError(BoxError.BAD_FIELD, 'ipv4 must be a string'); if (!net.isIPv4(config.ipv4)) return new BoxError(BoxError.BAD_FIELD, 'invalid IPv4'); - if ('ipv6' in config) { - if (typeof config.ipv6 !== 'string') return new BoxError(BoxError.BAD_FIELD, 'ipv6 must be a string'); - if (!net.isIPv6(config.ipv6)) return new BoxError(BoxError.BAD_FIELD, 'invalid IPv6'); - } + return null; +} + +async function testIPv6Config(config) { + assert.strictEqual(typeof config, 'object'); + + if (typeof config.ipv6 !== 'string') return new BoxError(BoxError.BAD_FIELD, 'ipv6 must be a string'); + if (!net.isIPv6(config.ipv6)) return new BoxError(BoxError.BAD_FIELD, 'invalid IPv6'); return null; } diff --git a/src/sysinfo/generic.js b/src/sysinfo/generic.js index 9d0f9b785..7105bf8ec 100644 --- a/src/sysinfo/generic.js +++ b/src/sysinfo/generic.js @@ -3,7 +3,8 @@ exports = module.exports = { getServerIPv4, getServerIPv6, - testConfig + testIPv4Config, + testIPv6Config }; const assert = require('assert'), @@ -62,7 +63,13 @@ async function getServerIPv6(config) { return response.body.ip; } -async function testConfig(config) { +async function testIPv4Config(config) { + assert.strictEqual(typeof config, 'object'); + + return null; +} + +async function testIPv6Config(config) { assert.strictEqual(typeof config, 'object'); return null; diff --git a/src/sysinfo/interface.js b/src/sysinfo/interface.js index 1d02c52d9..40b967056 100644 --- a/src/sysinfo/interface.js +++ b/src/sysinfo/interface.js @@ -9,7 +9,8 @@ exports = module.exports = { getServerIPv4, getServerIPv6, - testConfig + testIPv4Config, + testIPv6Config }; const assert = require('assert'), @@ -27,7 +28,13 @@ async function getServerIPv6(config) { throw new BoxError(BoxError.NOT_IMPLEMENTED, 'getServerIPv6 is not implemented'); } -async function testConfig(config) { +async function testIPv4Config(config) { + assert.strictEqual(typeof config, 'object'); + + return null; +} + +async function testIPv6Config(config) { assert.strictEqual(typeof config, 'object'); return null; diff --git a/src/sysinfo/network-interface.js b/src/sysinfo/network-interface.js index 9fc822eb3..2ffa580f7 100644 --- a/src/sysinfo/network-interface.js +++ b/src/sysinfo/network-interface.js @@ -3,7 +3,8 @@ exports = module.exports = { getServerIPv4, getServerIPv6, - testConfig + testIPv4Config, + testIPv6Config }; const assert = require('assert'), @@ -40,7 +41,7 @@ async function getServerIPv6(config) { return addresses[0]; } -async function testConfig(config) { +async function testIPv4Config(config) { assert.strictEqual(typeof config, 'object'); assert.strictEqual(typeof callback, 'function'); @@ -49,3 +50,13 @@ async function testConfig(config) { const [error] = await safe(getServerIPv4(config)); return error || null; } + +async function testIPv6Config(config) { + assert.strictEqual(typeof config, 'object'); + assert.strictEqual(typeof callback, 'function'); + + if (typeof config.ifname !== 'string') return new BoxError(BoxError.BAD_FIELD, 'ifname is not a string'); + + const [error] = await safe(getServerIPv6(config)); + return error || null; +} diff --git a/src/test/settings-test.js b/src/test/settings-test.js index b4b234a70..4b7be4583 100644 --- a/src/test/settings-test.js +++ b/src/test/settings-test.js @@ -55,15 +55,15 @@ describe('Settings', function () { }); it('can get default IPv6 setting', async function () { - const enabled = await settings.getIPv6Config(); - expect(enabled).to.be(false); + const config = await settings.getIPv6Config(); + expect(config.provider).to.be('noop'); }); it('can set IPv6 setting', async function () { - await settings.setIPv6Config(true); + await settings.setIPv6Config({ provider: 'generic' }); - const enabled = await settings.getIPv6Config(); - expect(enabled).to.be(true); + const config = await settings.getIPv6Config(); + expect(config.provider).to.be('generic'); }); it('can get default profile config', async function () { @@ -88,6 +88,6 @@ describe('Settings', function () { expect(allSettings[settings.AUTOUPDATE_PATTERN_KEY]).to.be.a('string'); expect(allSettings[settings.CLOUDRON_NAME_KEY]).to.be.a('string'); expect(allSettings[settings.UNSTABLE_APPS_KEY]).to.be.a('boolean'); - expect(allSettings[settings.IPV6_KEY]).to.be.a('boolean'); + expect(allSettings[settings.IPV6_CONFIG_KEY]).to.be.an('object'); }); });