diff --git a/src/apps.js b/src/apps.js index 6e0f10cd0..0df5283ab 100644 --- a/src/apps.js +++ b/src/apps.js @@ -132,17 +132,6 @@ exports = module.exports = { HEALTH_ERROR: 'error', HEALTH_DEAD: 'dead', - // subdomain table types - LOCATION_TYPE_PRIMARY: 'primary', - LOCATION_TYPE_SECONDARY: 'secondary', - LOCATION_TYPE_REDIRECT: 'redirect', - LOCATION_TYPE_ALIAS: 'alias', - - // should probably be in table as well - LOCATION_TYPE_DASHBOARD: 'dashboard', - LOCATION_TYPE_MAIL: 'mail', - LOCATION_TYPE_DIRECTORY_SERVER: 'directoryserver', - // exported for testing _validatePortBindings: validatePortBindings, _validateAccessRestriction: validateAccessRestriction, @@ -168,6 +157,7 @@ const appstore = require('./appstore.js'), domains = require('./domains.js'), eventlog = require('./eventlog.js'), fs = require('fs'), + Location = require('./location.js'), logs = require('./logs.js'), mail = require('./mail.js'), manifestFormat = require('cloudron-manifestformat'), @@ -715,15 +705,15 @@ function postProcess(result) { for (let i = 0; i < subdomainTypes.length; i++) { const subdomain = subdomains[i], domain = domains[i], certificate = safe.JSON.parse(subdomainCertificateJsons[i]); - if (subdomainTypes[i] === exports.LOCATION_TYPE_PRIMARY) { + if (subdomainTypes[i] === Location.TYPE_PRIMARY) { result.subdomain = subdomain; result.domain = domain; result.certificate = certificate; - } else if (subdomainTypes[i] === exports.LOCATION_TYPE_SECONDARY) { + } else if (subdomainTypes[i] === Location.TYPE_SECONDARY) { result.secondaryDomains.push({ domain, subdomain, certificate, environmentVariable: subdomainEnvironmentVariables[i] }); - } else if (subdomainTypes[i] === exports.LOCATION_TYPE_REDIRECT) { + } else if (subdomainTypes[i] === Location.TYPE_REDIRECT) { result.redirectDomains.push({ domain, subdomain, certificate }); - } else if (subdomainTypes[i] === exports.LOCATION_TYPE_ALIAS) { + } else if (subdomainTypes[i] === Location.TYPE_ALIAS) { result.aliasDomains.push({ domain, subdomain, certificate }); } } @@ -852,7 +842,7 @@ async function add(id, appStoreId, manifest, subdomain, domain, portBindings, da queries.push({ query: 'INSERT INTO locations (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)', - args: [ id, domain, subdomain, exports.LOCATION_TYPE_PRIMARY ] + args: [ id, domain, subdomain, Location.TYPE_PRIMARY ] }); Object.keys(portBindings).forEach(function (env) { @@ -873,7 +863,7 @@ async function add(id, appStoreId, manifest, subdomain, domain, portBindings, da data.secondaryDomains.forEach(function (d) { queries.push({ query: 'INSERT INTO locations (appId, domain, subdomain, type, environmentVariable) VALUES (?, ?, ?, ?, ?)', - args: [ id, d.domain, d.subdomain, exports.LOCATION_TYPE_SECONDARY, d.environmentVariable ] + args: [ id, d.domain, d.subdomain, Location.TYPE_SECONDARY, d.environmentVariable ] }); }); } @@ -882,7 +872,7 @@ async function add(id, appStoreId, manifest, subdomain, domain, portBindings, da data.redirectDomains.forEach(function (d) { queries.push({ query: 'INSERT INTO locations (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)', - args: [ id, d.domain, d.subdomain, exports.LOCATION_TYPE_REDIRECT ] + args: [ id, d.domain, d.subdomain, Location.TYPE_REDIRECT ] }); }); } @@ -891,7 +881,7 @@ async function add(id, appStoreId, manifest, subdomain, domain, portBindings, da data.aliasDomains.forEach(function (d) { queries.push({ query: 'INSERT INTO locations (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)', - args: [ id, d.domain, d.subdomain, exports.LOCATION_TYPE_ALIAS ] + args: [ id, d.domain, d.subdomain, Location.TYPE_ALIAS ] }); }); } @@ -947,23 +937,23 @@ async function updateWithConstraints(id, app, constraints) { if ('subdomain' in app && 'domain' in app) { // must be updated together as they are unique together queries.push({ query: 'DELETE FROM locations WHERE appId = ?', args: [ id ]}); // all locations of an app must be updated together - queries.push({ query: 'INSERT INTO locations (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)', args: [ id, app.domain, app.subdomain, exports.LOCATION_TYPE_PRIMARY ]}); + queries.push({ query: 'INSERT INTO locations (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)', args: [ id, app.domain, app.subdomain, Location.TYPE_PRIMARY ]}); if ('secondaryDomains' in app) { app.secondaryDomains.forEach(function (d) { - queries.push({ query: 'INSERT INTO locations (appId, domain, subdomain, type, environmentVariable) VALUES (?, ?, ?, ?, ?)', args: [ id, d.domain, d.subdomain, exports.LOCATION_TYPE_SECONDARY, d.environmentVariable ]}); + queries.push({ query: 'INSERT INTO locations (appId, domain, subdomain, type, environmentVariable) VALUES (?, ?, ?, ?, ?)', args: [ id, d.domain, d.subdomain, Location.TYPE_SECONDARY, d.environmentVariable ]}); }); } if ('redirectDomains' in app) { app.redirectDomains.forEach(function (d) { - queries.push({ query: 'INSERT INTO locations (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)', args: [ id, d.domain, d.subdomain, exports.LOCATION_TYPE_REDIRECT ]}); + queries.push({ query: 'INSERT INTO locations (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)', args: [ id, d.domain, d.subdomain, Location.TYPE_REDIRECT ]}); }); } if ('aliasDomains' in app) { app.aliasDomains.forEach(function (d) { - queries.push({ query: 'INSERT INTO locations (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)', args: [ id, d.domain, d.subdomain, exports.LOCATION_TYPE_ALIAS ]}); + queries.push({ query: 'INSERT INTO locations (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)', args: [ id, d.domain, d.subdomain, Location.TYPE_ALIAS ]}); }); } } @@ -1261,19 +1251,19 @@ async function validateLocations(locations) { constants.IMAP_SUBDOMAIN ]; - const { fqdn:dashboardFqdn } = await dashboard.getLocation(); + const dashboardLocation = await dashboard.getLocation(); for (const location of locations) { if (!(location.domain in domainObjectMap)) return new BoxError(BoxError.BAD_FIELD, `No such domain in ${location.type} location`); let subdomain = location.subdomain; - if (location.type === exports.LOCATION_TYPE_ALIAS && subdomain.startsWith('*')) { + if (location.type === Location.TYPE_ALIAS && subdomain.startsWith('*')) { if (subdomain === '*') continue; subdomain = subdomain.replace(/^\*\./, ''); // remove *. } if (RESERVED_SUBDOMAINS.indexOf(subdomain) !== -1) return new BoxError(BoxError.BAD_FIELD, `subdomain '${subdomain}' is reserved`); - if (dns.fqdn(subdomain, location.domain) === dashboardFqdn) return new BoxError(BoxError.BAD_FIELD, `subdomain '${subdomain}' is reserved for dashboard`); + if (location.fqdn === dashboardLocation.fqdn) return new BoxError(BoxError.BAD_FIELD, `subdomain '${subdomain}' is reserved for dashboard`); const error = dns.validateHostname(subdomain, location.domain); if (error) return new BoxError(BoxError.BAD_FIELD, `Bad ${location.type} location: ${error.message}`); @@ -1366,10 +1356,10 @@ async function install(data, auditSource) { icon = Buffer.from(icon, 'base64'); } - const locations = [{ subdomain, domain, type: exports.LOCATION_TYPE_PRIMARY }] - .concat(secondaryDomains.map(ad => Object.assign(ad, { type: exports.LOCATION_TYPE_SECONDARY }))) - .concat(redirectDomains.map(ad => Object.assign(ad, { type: exports.LOCATION_TYPE_REDIRECT }))) - .concat(aliasDomains.map(ad => Object.assign(ad, { type: exports.LOCATION_TYPE_ALIAS }))); + const locations = [new Location(subdomain, domain, Location.TYPE_PRIMARY)] + .concat(secondaryDomains.map(sd => new Location(sd.subdomain, sd.domain, Location.TYPE_SECONDARY))) + .concat(redirectDomains.map(rd => new Location(rd.subdomain, rd.domain, Location.TYPE_REDIRECT))) + .concat(aliasDomains.map(ad => new Location(ad.subdomain, ad.domain, Location.TYPE_ALIAS))); error = await validateLocations(locations); if (error) throw error; @@ -1808,6 +1798,16 @@ async function setReverseProxyConfig(app, reverseProxyConfig, auditSource) { await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, reverseProxyConfig }); } +async function getLocation(subdomain, domain) { + assert.strictEqual(typeof subdomain, 'string'); + assert.strictEqual(typeof domain, 'string'); + + const result = await database.query(`SELECT ${LOCATION_FIELDS} FROM locations WHERE subdomain=? AND domain=?`, [ subdomain, domain ]); + if (result.length === 0) return null; + + return new Location(subdomain, domain, result[0].type, safe.JSON.parse(result[0].certificateJson)); +} + async function setCertificate(app, data, auditSource) { assert.strictEqual(typeof app, 'object'); assert(data && typeof data === 'object'); @@ -1827,23 +1827,11 @@ async function setCertificate(app, data, auditSource) { const result = await database.query('UPDATE locations SET certificateJson=? WHERE location=? AND domain=?', [ certificate ? JSON.stringify(certificate) : null, subdomain, domain ]); if (result.affectedRows === 0) throw new BoxError(BoxError.NOT_FOUND, 'Location not found'); - const location = await getLocation(subdomain, domain); // fresh location object + const location = await getLocation(subdomain, domain); // fresh location object with type await reverseProxy.setUserCertificate(app, location); await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: app.id, app, subdomain, domain, cert }); } -async function getLocation(subdomain, domain) { - assert.strictEqual(typeof subdomain, 'string'); - assert.strictEqual(typeof domain, 'string'); - - const result = await database.query(`SELECT ${LOCATION_FIELDS} FROM locations WHERE subdomain=? AND domain=?`, [ subdomain, domain ]); - if (result.length === 0) return null; - - result[0].certificate = safe.JSON.parse(result[0].certificateJson); - result[0].fqdn = dns.fqdn(subdomain, domain); - return result[0]; -} - async function setLocation(app, data, auditSource) { assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof data, 'object'); @@ -1888,10 +1876,10 @@ async function setLocation(app, data, auditSource) { values.aliasDomains = data.aliasDomains; } - const locations = [{ subdomain: values.subdomain, domain: values.domain, type: exports.LOCATION_TYPE_PRIMARY }] - .concat(values.secondaryDomains.map(ad => Object.assign(ad, { type: exports.LOCATION_TYPE_SECONDARY }))) - .concat(values.redirectDomains.map(ad => Object.assign(ad, { type: exports.LOCATION_TYPE_REDIRECT }))) - .concat(values.aliasDomains.map(ad => Object.assign(ad, { type: exports.LOCATION_TYPE_ALIAS }))); + const locations = [new Location(values.subdomain, values.domain, Location.TYPE_PRIMARY)] + .concat(values.secondaryDomains.map(sd => new Location(sd.subdomain, sd.domain, Location.TYPE_SECONDARY))) + .concat(values.redirectDomains.map(rd => new Location(rd.subdomain, rd.domain, Location.TYPE_REDIRECT))) + .concat(values.aliasDomains.map(ad => new Location(ad.subdomain, ad.domain, Location.TYPE_ALIAS))); error = await validateLocations(locations); if (error) throw error; @@ -2293,8 +2281,8 @@ async function clone(app, data, user, auditSource) { if (error) throw error; const secondaryDomains = translateSecondaryDomains(data.secondaryDomains || {}); - const locations = [{ subdomain, domain, type: exports.LOCATION_TYPE_PRIMARY }] - .concat(secondaryDomains.map(ad => Object.assign(ad, { type: exports.LOCATION_TYPE_SECONDARY }))); + const locations = [new Location(subdomain, domain, Location.TYPE_PRIMARY)] + .concat(secondaryDomains.map(sd => new Location(sd.subdomain, sd.domain, Location.TYPE_SECONDARY))); error = await validateLocations(locations); if (error) throw error; diff --git a/src/reverseproxy.js b/src/reverseproxy.js index 08a7fc8e4..40a46efe9 100644 --- a/src/reverseproxy.js +++ b/src/reverseproxy.js @@ -287,14 +287,13 @@ function needsRenewalSync(cert, options) { async function getCertificate(location) { assert.strictEqual(typeof location, 'object'); - const { domain, fqdn } = location; - const domainObject = await domains.get(domain); - if (!domainObject) throw new BoxError(BoxError.NOT_FOUND, `${domain} not found`); + const domainObject = await domains.get(location.domain); + if (!domainObject) throw new BoxError(BoxError.NOT_FOUND, `${location.domain} not found`); if (location.certificate) return location.certificate; if (domainObject.tlsConfig.provider === 'fallback') return domainObject.fallbackCertificate; - const certName = getAcmeCertificateNameSync(fqdn, domainObject); + const certName = getAcmeCertificateNameSync(location.fqdn, domainObject); const cert = await blobs.getString(`${blobs.CERT_PREFIX}-${certName}.cert`); const key = await blobs.getString(`${blobs.CERT_PREFIX}-${certName}.key`); if (!key || !cert) return domainObject.fallbackCertificate; @@ -303,13 +302,13 @@ async function getCertificate(location) { } async function getMailCertificate() { - const { domain, fqdn } = await mailServer.getLocation(); - return await getCertificate({ domain, fqdn, certificate: null, type: apps.LOCATION_TYPE_MAIL }); + const mailLocation = await mailServer.getLocation(); + return await getCertificate(mailLocation); } async function getDirectoryServerCertificate() { - const { domain, fqdn } = await dashboard.getLocation(); - return await getCertificate({ domain, fqdn, certificate: null, type: apps.LOCATION_TYPE_DIRECTORY_SERVER }); + const dashboardLocation = await dashboard.getLocation(); + return await getCertificate(dashboardLocation); } // write if contents mismatch (thus preserving mtime) @@ -329,13 +328,13 @@ async function setupTlsAddon(app) { const certificateDir = `${paths.PLATFORM_DATA_DIR}/tls/${app.id}`; const contents = []; for (const location of getAppLocationsSync(app)) { - if (location.type === apps.LOCATION_TYPE_REDIRECT) continue; + if (location.type === Location.TYPE_REDIRECT) continue; const certificate = await getCertificate(location); contents.push({ filename: `${location.fqdn.replace('*', '_')}.cert`, data: certificate.cert }); contents.push({ filename: `${location.fqdn.replace('*', '_')}.key`, data: certificate.key }); - if (location.type === apps.LOCATION_TYPE_PRIMARY) { // backward compat + if (location.type === Location.TYPE_PRIMARY) { // backward compat contents.push({ filename: 'tls_cert.pem', data: certificate.cert }); contents.push({ filename: 'tls_key.pem', data: certificate.key }); } @@ -487,11 +486,11 @@ async function writeAppLocationNginxConfig(app, location, certificatePath) { assert.strictEqual(typeof location, 'object'); assert.strictEqual(typeof certificatePath, 'object'); - const type = location.type, vhost = location.fqdn; + const { type, fqdn} = location; const data = { sourceDir: path.resolve(__dirname, '..'), - vhost, + vhost: fqdn, hasIPv6: network.hasIPv6(), ip: null, port: null, @@ -508,7 +507,7 @@ async function writeAppLocationNginxConfig(app, location, certificatePath) { hstsPreload: !!app.reverseProxyConfig?.hstsPreload }; - if (type === apps.LOCATION_TYPE_PRIMARY || type === apps.LOCATION_TYPE_ALIAS || type === apps.LOCATION_TYPE_SECONDARY) { + if (type === Location.TYPE_PRIMARY || type === Location.TYPE_ALIAS || type === Location.TYPE_SECONDARY) { data.endpoint = 'app'; if (app.manifest.id === constants.PROXY_APP_APPSTORE_ID) { @@ -524,7 +523,7 @@ async function writeAppLocationNginxConfig(app, location, certificatePath) { data.hideHeaders = [ 'Content-Security-Policy' ]; if (reverseProxyConfig.csp.includes('frame-ancestors ')) data.hideHeaders.push('X-Frame-Options'); } - if (type === apps.LOCATION_TYPE_PRIMARY || type == apps.LOCATION_TYPE_ALIAS) { + if (type === Location.TYPE_PRIMARY || type == Location.TYPE_ALIAS) { data.proxyAuth = { enabled: app.sso && app.manifest.addons && app.manifest.addons.proxyAuth, id: app.id, @@ -532,20 +531,20 @@ async function writeAppLocationNginxConfig(app, location, certificatePath) { }; data.ip = app.containerIp; data.port = app.manifest.httpPort; - } else if (type === apps.LOCATION_TYPE_SECONDARY) { + } else if (type === Location.TYPE_SECONDARY) { data.ip = app.containerIp; - const secondaryDomain = app.secondaryDomains.find(sd => sd.fqdn === vhost); + const secondaryDomain = app.secondaryDomains.find(sd => sd.fqdn === fqdn); data.port = app.manifest.httpPorts[secondaryDomain.environmentVariable].containerPort; } - } else if (type === apps.LOCATION_TYPE_REDIRECT) { + } else if (type === Location.TYPE_REDIRECT) { data.proxyAuth = { enabled: false, id: app.id, location: nginxLocation('/') }; data.endpoint = 'redirect'; data.redirectTo = app.fqdn; } const nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data); - const filename = path.join(paths.NGINX_APPCONFIG_DIR, app.id, `${vhost.replace('*', '_')}.conf`); - debug(`writeAppLocationNginxConfig: writing config for "${vhost}" to ${filename} with options ${JSON.stringify(data)}`); + const filename = path.join(paths.NGINX_APPCONFIG_DIR, app.id, `${fqdn.replace('*', '_')}.conf`); + debug(`writeAppLocationNginxConfig: writing config for "${fqdn}" to ${filename} with options ${JSON.stringify(data)}`); writeFileSync(filename, nginxConf); } diff --git a/src/test/apps-test.js b/src/test/apps-test.js index ed09f086d..8fa451f42 100644 --- a/src/test/apps-test.js +++ b/src/test/apps-test.js @@ -10,6 +10,7 @@ const apps = require('../apps.js'), BoxError = require('../boxerror.js'), common = require('./common.js'), expect = require('expect.js'), + Location = require('../location.js'), safe = require('safetydance'); describe('Apps', function () { @@ -20,17 +21,17 @@ describe('Apps', function () { describe('validateLocations', function () { it('does not allow reserved subdomain', async function () { - let location = { type: apps.LOCATION_TYPE_ALIAS, subdomain: 'my', domain: domain.domain }; + let location = new Location('my', domain.domain, Location.TYPE_ALIAS); expect(await apps._validateLocations([location])).to.be.an(Error); }); it('does not allow unknown domain', async function () { - let location = { type: apps.LOCATION_TYPE_PRIMARY, subdomain: 'my2', domain: domain.domain + 'x' }; + let location = new Location('my2', domain.domain + 'x', Location.TYPE_PRIMARY); expect(await apps._validateLocations([location])).to.be.an(Error); }); it('allows valid locations', async function () { - let location = { type: apps.LOCATION_TYPE_SECONDARY, subdomain: 'my2', domain: domain.domain }; + let location = new Location('my2', domain.domain, Location.TYPE_SECONDARY); expect(await apps._validateLocations([location])).to.be(null); }); });