Files
cloudron-box/src/reverseproxy.js
T

797 lines
33 KiB
JavaScript
Raw Normal View History

2015-12-10 13:31:47 -08:00
'use strict';
exports = module.exports = {
2022-07-14 13:25:41 +05:30
setUserCertificate, // per location certificate
setFallbackCertificate, // per domain certificate
2017-01-17 09:57:15 -08:00
2021-08-17 14:04:29 -07:00
generateFallbackCertificate,
validateCertificate,
2017-01-17 09:57:15 -08:00
2022-11-28 22:32:34 +01:00
getMailCertificate,
getDirectoryServerCertificate,
2016-06-22 13:48:07 -05:00
2022-11-28 22:32:34 +01:00
ensureCertificate,
startRenewCerts,
checkCerts,
2017-01-17 09:57:15 -08:00
2022-07-13 09:26:27 +05:30
// the 'configure' functions ensure a certificate and generate nginx config
configureApp,
unconfigureApp,
2019-09-30 15:25:53 -07:00
// these only generate nginx config
writeDefaultConfig,
2020-09-23 15:45:04 -07:00
writeDashboardConfig,
2022-07-13 09:26:27 +05:30
writeAppConfigs,
removeDashboardConfig,
removeAppConfigs,
2021-05-04 21:40:11 -07:00
restoreFallbackCertificates,
2022-11-29 18:11:22 +01:00
2023-05-13 14:59:57 +02:00
handleCertificateProviderChanged,
getTrustedIps,
setTrustedIps
};
2021-05-07 22:44:13 -07:00
const acme2 = require('./acme2.js'),
2016-03-17 12:20:02 -07:00
apps = require('./apps.js'),
2015-12-10 13:31:47 -08:00
assert = require('assert'),
2021-05-02 23:28:41 -07:00
blobs = require('./blobs.js'),
2019-10-22 16:46:24 -07:00
BoxError = require('./boxerror.js'),
2015-12-11 13:52:21 -08:00
constants = require('./constants.js'),
crypto = require('crypto'),
2023-08-11 19:41:05 +05:30
dashboard = require('./dashboard.js'),
2018-10-31 15:41:02 -07:00
debug = require('debug')('box:reverseproxy'),
2021-08-13 17:22:28 -07:00
dns = require('./dns.js'),
2022-11-28 22:32:34 +01:00
docker = require('./docker.js'),
domains = require('./domains.js'),
ejs = require('ejs'),
2016-04-30 22:27:33 -07:00
eventlog = require('./eventlog.js'),
2015-12-11 13:52:21 -08:00
fs = require('fs'),
2023-08-17 10:44:07 +05:30
Location = require('./location.js'),
2023-08-04 20:54:16 +05:30
mailServer = require('./mailserver.js'),
2023-08-03 13:38:42 +05:30
network = require('./network.js'),
os = require('os'),
2015-12-11 13:52:21 -08:00
path = require('path'),
2015-12-10 13:31:47 -08:00
paths = require('./paths.js'),
safe = require('safetydance'),
2019-07-26 10:49:29 -07:00
settings = require('./settings.js'),
shell = require('./shell.js'),
tasks = require('./tasks.js'),
2023-05-13 14:59:57 +02:00
validator = require('validator');
2015-12-10 13:31:47 -08:00
2021-03-23 11:01:14 -07:00
const NGINX_APPCONFIG_EJS = fs.readFileSync(__dirname + '/nginxconfig.ejs', { encoding: 'utf8' });
const RESTART_SERVICE_CMD = path.join(__dirname, 'scripts/restartservice.sh');
2021-01-08 14:10:11 -08:00
function nginxLocation(s) {
if (!s.startsWith('!')) return s;
2022-07-13 09:26:27 +05:30
const re = s.replace(/[\^$\\.*+?()[\]{}|]/g, '\\$&'); // https://github.com/es-shims/regexp.escape/blob/master/implementation.js
2021-01-08 14:10:11 -08:00
return `~ ^(?!(${re.slice(1)}))`; // negative regex assertion - https://stackoverflow.com/questions/16302897/nginx-location-not-equal-to-regex
}
2024-02-21 12:33:04 +01:00
async function getCertificateDates(cert) {
2022-11-29 13:57:58 +01:00
assert.strictEqual(typeof cert, 'string');
2016-03-19 12:50:31 -07:00
2024-02-21 19:40:27 +01:00
const [error, result] = await safe(shell.exec('getCertificateDates', 'openssl x509 -startdate -enddate -subject -noout', { input: cert }));
2024-02-21 12:33:04 +01:00
if (error) return { startDate: null, endDate: null } ; // some error
2016-03-18 22:59:51 -07:00
2024-02-21 12:33:04 +01:00
const lines = result.trim().split('\n');
2023-02-01 12:38:09 +01:00
const notBefore = lines[0].split('=')[1];
2023-02-01 11:05:50 +01:00
const notBeforeDate = new Date(notBefore);
const notAfter = lines[1].split('=')[1];
2021-06-24 00:48:54 -07:00
const notAfterDate = new Date(notAfter);
2018-11-23 11:26:18 -08:00
2021-06-24 00:48:54 -07:00
const daysLeft = (notAfterDate - new Date())/(24 * 60 * 60 * 1000);
2023-02-01 12:38:09 +01:00
debug(`expiryDate: ${lines[2]} notBefore=${notBefore} notAfter=${notAfter} daysLeft=${daysLeft}`);
2021-06-24 00:48:54 -07:00
2023-02-01 11:05:50 +01:00
return { startDate: notBeforeDate, endDate: notAfterDate };
}
2023-08-02 19:22:53 +05:30
async function getReverseProxyConfig() {
const value = await settings.getJson(settings.REVERSE_PROXY_CONFIG_KEY);
return value || { ocsp: true };
2023-08-02 19:22:53 +05:30
}
2021-09-22 09:13:16 -07:00
async function isOcspEnabled(certFilePath) {
// on some servers, OCSP does not work. see #796
2023-08-02 19:22:53 +05:30
const config = await getReverseProxyConfig();
2021-09-22 09:13:16 -07:00
if (!config.ocsp) return false;
// We used to check for the must-staple in the cert using openssl x509 -text -noout -in ${certFilePath} | grep -q status_request
// however, we cannot set the must-staple because first request to nginx fails because of it's OCSP caching behavior
2024-02-21 19:40:27 +01:00
const [error, result] = await safe(shell.exec('isOscpEnabled', `openssl x509 -in ${certFilePath} -noout -ocsp_uri`, {}));
2024-02-21 12:33:04 +01:00
return !error && result.length > 0; // no error and has uri
2021-04-16 11:17:13 -07:00
}
// checks if the certificate matches the options provided by user (like wildcard, le-staging etc)
2024-02-21 12:33:04 +01:00
async function providerMatches(domainObject, cert) {
assert.strictEqual(typeof domainObject, 'object');
2022-11-29 13:57:58 +01:00
assert.strictEqual(typeof cert, 'string');
2024-02-21 19:40:27 +01:00
const [error, subjectAndIssuer] = await safe(shell.exec('providerMatches', 'openssl x509 -noout -subject -issuer', { input: cert }));
2024-02-21 12:33:04 +01:00
if (error) return false; // something bad happenned
2018-11-15 14:18:34 +01:00
const subject = subjectAndIssuer.match(/^subject=(.*)$/m)[1];
2018-11-15 10:45:27 -08:00
const domain = subject.substr(subject.indexOf('=') + 1).trim(); // subject can be /CN=, CN=, CN = and other forms
const issuer = subjectAndIssuer.match(/^issuer=(.*)$/m)[1];
2018-11-15 10:45:27 -08:00
const isWildcardCert = domain.includes('*');
2021-04-27 12:55:11 -07:00
const isLetsEncryptProd = issuer.includes('Let\'s Encrypt') && !issuer.includes('STAGING');
2022-11-17 12:39:23 +01:00
const prod = domainObject.tlsConfig.provider.match(/.*-prod/) !== null; // matches 'le-prod' or 'letsencrypt-prod'
const wildcard = !!domainObject.tlsConfig.wildcard;
const issuerMismatch = (prod && !isLetsEncryptProd) || (!prod && isLetsEncryptProd);
// bare domain is not part of wildcard SAN
2022-11-17 12:39:23 +01:00
const wildcardMismatch = (domain !== domainObject.domain) && (wildcard && !isWildcardCert) || (!wildcard && isWildcardCert);
const mismatch = issuerMismatch || wildcardMismatch;
2024-02-21 12:33:04 +01:00
debug(`providerMatches: subject=${subject} domain=${domain} issuer=${issuer} `
2022-11-17 12:39:23 +01:00
+ `wildcard=${isWildcardCert}/${wildcard} prod=${isLetsEncryptProd}/${prod} `
2020-12-04 11:47:19 -08:00
+ `issuerMismatch=${issuerMismatch} wildcardMismatch=${wildcardMismatch} match=${!mismatch}`);
return !mismatch;
}
// note: https://tools.ietf.org/html/rfc4346#section-7.4.2 (certificate_list) requires that the
// servers certificate appears first (and not the intermediate cert)
2024-02-21 12:33:04 +01:00
async function validateCertificate(subdomain, domain, certificate) {
2022-07-13 09:26:27 +05:30
assert.strictEqual(typeof subdomain, 'string');
2022-11-28 21:23:06 +01:00
assert.strictEqual(typeof domain, 'string');
2022-07-14 12:39:41 +05:30
assert(certificate && typeof certificate, 'object');
2018-11-05 22:36:16 -08:00
2022-07-14 12:39:41 +05:30
const { cert, key } = certificate;
2018-01-26 19:31:06 -08:00
// check for empty cert and key strings
2024-02-21 12:33:04 +01:00
if (!cert && key) throw new BoxError(BoxError.BAD_FIELD, 'missing cert');
if (cert && !key) throw new BoxError(BoxError.BAD_FIELD, 'missing key');
2018-02-09 14:05:01 -08:00
// -checkhost checks for SAN or CN exclusively. SAN takes precedence and if present, ignores the CN.
2022-11-28 21:23:06 +01:00
const fqdn = dns.fqdn(subdomain, domain);
2024-02-21 19:40:27 +01:00
const [checkHostError, checkHostOutput] = await safe(shell.exec('validateCertificate', `openssl x509 -noout -checkhost ${fqdn}`, { input: cert }));
2024-02-21 12:33:04 +01:00
if (checkHostError) throw new BoxError(BoxError.BAD_FIELD, 'Could not validate certificate');
if (checkHostOutput.indexOf('does match certificate') === -1) throw new BoxError(BoxError.BAD_FIELD, `Certificate is not valid for this domain. Expecting ${fqdn}`);
2020-03-24 20:56:49 -07:00
// check if public key in the cert and private key matches. pkey below works for RSA and ECDSA keys
2024-02-21 19:40:27 +01:00
const [pubKeyError1, pubKeyFromCert] = await safe(shell.exec('validateCertificate', 'openssl x509 -noout -pubkey', { input: cert }));
2024-02-21 12:33:04 +01:00
if (pubKeyError1) throw new BoxError(BoxError.BAD_FIELD, 'Could not get public key from cert');
2024-02-21 19:40:27 +01:00
const [pubKeyError2, pubKeyFromKey] = await safe(shell.exec('validateCertificate', 'openssl pkey -pubout', { input: key }));
2024-02-21 12:33:04 +01:00
if (pubKeyError2) throw new BoxError(BoxError.BAD_FIELD, 'Could not get public key from private key');
2024-02-21 12:33:04 +01:00
if (pubKeyFromCert !== pubKeyFromKey) throw new BoxError(BoxError.BAD_FIELD, 'Public key does not match the certificate.');
2017-11-27 10:39:42 -08:00
// check expiration
2024-02-21 19:40:27 +01:00
const [error] = await safe(shell.exec('validateCertificate', 'openssl x509 -checkend 0', { input: cert }));
2024-02-21 12:33:04 +01:00
if (error) throw new BoxError(BoxError.BAD_FIELD, 'Certificate has expired');
2017-02-24 19:21:53 -08:00
return null;
}
2015-12-11 13:52:21 -08:00
2022-11-29 18:27:08 +01:00
async function notifyCertChange() {
2023-08-04 20:54:16 +05:30
await mailServer.checkCertificate();
2022-11-29 18:27:08 +01:00
await shell.promises.sudo('notifyCertChange', [ RESTART_SERVICE_CMD, 'box' ], {}); // directory server
const allApps = (await apps.list()).filter(app => app.runState !== apps.RSTATE_STOPPED);
for (const app of allApps) {
if (app.manifest.addons?.tls) await setupTlsAddon(app);
}
}
2021-08-17 14:04:29 -07:00
async function reload() {
if (constants.TEST) return;
2021-08-17 14:04:29 -07:00
const [error] = await safe(shell.promises.sudo('reload', [ RESTART_SERVICE_CMD, 'nginx' ], {}));
if (error) throw new BoxError(BoxError.NGINX_ERROR, `Error reloading nginx: ${error.message}`);
}
2021-10-06 13:16:36 -07:00
// this is used in migration - 20211006200150-domains-ensure-fallbackCertificate.js
2021-08-17 14:04:29 -07:00
async function generateFallbackCertificate(domain) {
2021-05-04 21:40:11 -07:00
assert.strictEqual(typeof domain, 'string');
const certFilePath = path.join(os.tmpdir(), `${domain}-${crypto.randomBytes(4).readUInt32LE(0)}.cert`);
const keyFilePath = path.join(os.tmpdir(), `${domain}-${crypto.randomBytes(4).readUInt32LE(0)}.key`);
2021-10-06 13:16:36 -07:00
const opensslConf = safe.fs.readFileSync('/etc/ssl/openssl.cnf', 'utf8');
// SAN must contain all the domains since CN check is based on implementation if SAN is found. -checkhost also checks only SAN if present!
let opensslConfWithSan;
2021-10-06 13:16:36 -07:00
const cn = domain;
2018-11-05 20:36:58 -08:00
2022-07-13 09:26:27 +05:30
debug(`generateFallbackCertificate: domain=${domain} cn=${cn}`);
2018-11-05 20:36:58 -08:00
opensslConfWithSan = `${opensslConf}\n[SAN]\nsubjectAltName=DNS:${domain},DNS:*.${cn}\n`;
2021-10-06 13:16:36 -07:00
const configFile = path.join(os.tmpdir(), 'openssl-' + crypto.randomBytes(4).readUInt32LE(0) + '.conf');
safe.fs.writeFileSync(configFile, opensslConfWithSan, 'utf8');
2020-10-08 14:38:52 -07:00
// the days field is chosen to be less than 825 days per apple requirement (https://support.apple.com/en-us/HT210176)
2024-02-21 12:33:04 +01:00
const certCommand = `openssl req -x509 -newkey rsa:2048 -keyout ${keyFilePath} -out ${certFilePath} -days 800 -subj /CN=*.${cn} -extensions SAN -config ${configFile} -nodes`;
2024-02-21 19:40:27 +01:00
await shell.exec('generateFallbackCertificate', certCommand, {});
safe.fs.unlinkSync(configFile);
const cert = safe.fs.readFileSync(certFilePath, 'utf8');
2021-10-06 13:16:36 -07:00
if (!cert) throw new BoxError(BoxError.FS_ERROR, safe.error.message);
safe.fs.unlinkSync(certFilePath);
const key = safe.fs.readFileSync(keyFilePath, 'utf8');
2021-10-06 13:16:36 -07:00
if (!key) throw new BoxError(BoxError.FS_ERROR, safe.error.message);
safe.fs.unlinkSync(keyFilePath);
2021-10-06 13:16:36 -07:00
return { cert, key };
}
2022-07-14 12:39:41 +05:30
async function setFallbackCertificate(domain, certificate) {
2018-01-24 14:28:35 -08:00
assert.strictEqual(typeof domain, 'string');
2022-07-14 12:39:41 +05:30
assert(certificate && typeof certificate === 'object');
2015-12-11 13:52:21 -08:00
2020-08-07 11:41:15 -07:00
debug(`setFallbackCertificate: setting certs for domain ${domain}`);
2022-07-14 12:39:41 +05:30
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, `${domain}.host.cert`), certificate.cert)) throw new BoxError(BoxError.FS_ERROR, safe.error.message);
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, `${domain}.host.key`), certificate.key)) throw new BoxError(BoxError.FS_ERROR, safe.error.message);
2015-12-11 13:52:21 -08:00
2021-08-17 14:04:29 -07:00
await reload();
await notifyCertChange(); // if domain uses fallback certs, propagate immediately
2015-12-11 13:52:21 -08:00
}
2021-08-17 14:04:29 -07:00
async function restoreFallbackCertificates() {
const result = await domains.list();
2021-05-04 21:40:11 -07:00
2021-10-06 13:01:12 -07:00
for (const domain of result) {
2021-08-17 14:04:29 -07:00
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, `${domain.domain}.host.cert`), domain.fallbackCertificate.cert)) throw new BoxError(BoxError.FS_ERROR, safe.error.message);
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, `${domain.domain}.host.key`), domain.fallbackCertificate.key)) throw new BoxError(BoxError.FS_ERROR, safe.error.message);
2021-10-06 13:01:12 -07:00
}
2021-05-04 21:40:11 -07:00
}
2022-11-28 22:32:34 +01:00
function getAppLocationsSync(app) {
assert.strictEqual(typeof app, 'object');
2023-08-17 10:44:07 +05:30
return [new Location(app.subdomain, app.domain, Location.TYPE_PRIMARY, app.certificate)]
2023-08-17 13:02:36 +05:30
.concat(app.secondaryDomains.map(sd => new Location(sd.subdomain, sd.domain, Location.TYPE_SECONDARY, sd.certificate)))
.concat(app.redirectDomains.map(rd => new Location(rd.subdomain, rd.domain, Location.TYPE_REDIRECT, rd.certificate)))
.concat(app.aliasDomains.map(ad => new Location(ad.subdomain, ad.domain, Location.TYPE_ALIAS, ad.certificate)));
}
function getAcmeCertificateNameSync(fqdn, domainObject) {
2022-07-13 09:26:27 +05:30
assert.strictEqual(typeof fqdn, 'string'); // this can contain wildcard domain (for alias domains)
2018-11-14 19:36:12 -08:00
assert.strictEqual(typeof domainObject, 'object');
2016-05-04 17:37:21 -07:00
2022-07-13 09:26:27 +05:30
if (fqdn !== domainObject.domain && domainObject.tlsConfig.wildcard) { // bare domain is not part of wildcard SAN
2022-11-11 18:09:10 +01:00
return dns.makeWildcard(fqdn).replace('*.', '_.');
} else if (fqdn.includes('*')) { // alias domain with non-wildcard cert
return fqdn.replace('*.', '_.');
2018-11-14 19:36:12 -08:00
} else {
2022-11-11 18:09:10 +01:00
return fqdn;
}
2022-11-11 18:09:10 +01:00
}
2024-02-21 12:33:04 +01:00
async function needsRenewal(cert, options) {
2022-11-29 13:57:58 +01:00
assert.strictEqual(typeof cert, 'string');
2023-02-01 12:28:46 +01:00
assert.strictEqual(typeof options, 'object');
2018-09-11 22:46:17 -07:00
2024-02-21 12:33:04 +01:00
const { startDate, endDate } = await getCertificateDates(cert);
const now = new Date();
let isExpiring;
if (options.forceRenewal) {
2023-02-01 12:52:37 +01:00
isExpiring = (now - startDate) > (65 * 60 * 1000); // was renewed 5 minutes ago. LE backdates issue date by 1 hour for clock skew
} else {
isExpiring = (endDate - now) <= (30 * 24 * 60 * 60 * 1000); // expiring in a month
}
debug(`needsRenewal: ${isExpiring}. force: ${!!options.forceRenewal}`);
2022-11-28 22:32:34 +01:00
return isExpiring;
}
2022-11-28 22:32:34 +01:00
async function getCertificate(location) {
assert.strictEqual(typeof location, 'object');
2023-08-17 16:05:19 +05:30
const domainObject = await domains.get(location.domain);
if (!domainObject) throw new BoxError(BoxError.NOT_FOUND, `${location.domain} not found`);
2018-11-14 19:36:12 -08:00
2022-11-28 22:32:34 +01:00
if (location.certificate) return location.certificate;
if (domainObject.tlsConfig.provider === 'fallback') return domainObject.fallbackCertificate;
2020-08-07 22:59:57 -07:00
2023-08-17 16:05:19 +05:30
const certName = getAcmeCertificateNameSync(location.fqdn, domainObject);
2022-11-29 13:57:58 +01:00
const cert = await blobs.getString(`${blobs.CERT_PREFIX}-${certName}.cert`);
const key = await blobs.getString(`${blobs.CERT_PREFIX}-${certName}.key`);
2022-11-28 22:32:34 +01:00
if (!key || !cert) return domainObject.fallbackCertificate;
2018-09-11 22:46:17 -07:00
2022-11-28 22:32:34 +01:00
return { key, cert };
2017-01-17 10:21:42 -08:00
}
2022-11-28 22:32:34 +01:00
async function getMailCertificate() {
2023-08-17 16:05:19 +05:30
const mailLocation = await mailServer.getLocation();
return await getCertificate(mailLocation);
2022-11-28 22:32:34 +01:00
}
2022-11-28 22:32:34 +01:00
async function getDirectoryServerCertificate() {
2023-08-17 16:05:19 +05:30
const dashboardLocation = await dashboard.getLocation();
return await getCertificate(dashboardLocation);
2022-11-28 22:32:34 +01:00
}
// write if contents mismatch (thus preserving mtime)
2022-11-28 22:32:34 +01:00
function writeFileSync(filePath, data) {
assert.strictEqual(typeof filePath, 'string');
2022-11-29 17:13:58 +01:00
assert.strictEqual(typeof data, 'string');
2022-11-29 17:13:58 +01:00
const curData = safe.fs.readFileSync(filePath, { encoding: 'utf8' });
if (curData === data) return false;
2022-11-28 22:32:34 +01:00
if (!safe.fs.writeFileSync(filePath, data)) throw new BoxError(BoxError.FS_ERROR, safe.error.message);
return true;
}
2022-11-28 22:32:34 +01:00
async function setupTlsAddon(app) {
assert.strictEqual(typeof app, 'object');
2022-11-28 22:32:34 +01:00
const certificateDir = `${paths.PLATFORM_DATA_DIR}/tls/${app.id}`;
const contents = [];
2022-11-28 22:32:34 +01:00
for (const location of getAppLocationsSync(app)) {
2023-08-17 16:05:19 +05:30
if (location.type === Location.TYPE_REDIRECT) continue;
2023-02-25 14:49:41 +01:00
2022-11-28 22:32:34 +01:00
const certificate = await getCertificate(location);
2023-02-25 14:49:41 +01:00
contents.push({ filename: `${location.fqdn.replace('*', '_')}.cert`, data: certificate.cert });
contents.push({ filename: `${location.fqdn.replace('*', '_')}.key`, data: certificate.key });
2022-11-11 18:09:10 +01:00
2023-08-17 16:05:19 +05:30
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 });
2022-11-28 22:32:34 +01:00
}
}
2022-11-11 18:09:10 +01:00
let changed = 0;
for (const content of contents) {
if (writeFileSync(`${certificateDir}/${content.filename}`, content.data)) ++changed;
}
debug(`setupTlsAddon: ${changed} files changed`);
// clean up any certs of old locations
const filenamesInUse = new Set(contents.map(c => c.filename));
const filenames = safe.fs.readdirSync(certificateDir) || [];
let removed = 0;
for (const filename of filenames) {
if (filenamesInUse.has(filename)) continue;
safe.fs.unlinkSync(path.join(certificateDir, filename));
++removed;
}
debug(`setupTlsAddon: ${removed} files removed`);
if (changed || removed) await docker.restartContainer(app.id);
2022-11-11 18:09:10 +01:00
}
2022-11-28 22:32:34 +01:00
// writes latest certificate to disk and returns the path
async function writeCertificate(location) {
assert.strictEqual(typeof location, 'object');
2022-11-11 18:09:10 +01:00
2022-11-28 22:32:34 +01:00
const { domain, fqdn } = location;
const domainObject = await domains.get(domain);
if (!domainObject) throw new BoxError(BoxError.NOT_FOUND, `${domain} not found`);
2022-11-28 22:32:34 +01:00
if (location.certificate) {
const certFilePath = path.join(paths.NGINX_CERT_DIR, `${fqdn}.user.cert`);
const keyFilePath = path.join(paths.NGINX_CERT_DIR, `${fqdn}.user.key`);
2022-11-28 22:32:34 +01:00
writeFileSync(certFilePath, location.certificate.cert);
writeFileSync(keyFilePath, location.certificate.key);
2022-11-13 17:27:05 +01:00
2022-11-28 22:32:34 +01:00
return { certFilePath, keyFilePath };
}
2022-11-28 22:32:34 +01:00
if (domainObject.tlsConfig.provider === 'fallback') {
const certFilePath = path.join(paths.NGINX_CERT_DIR, `${domain}.host.cert`);
const keyFilePath = path.join(paths.NGINX_CERT_DIR, `${domain}.host.key`);
2020-08-07 22:59:57 -07:00
2022-11-28 22:32:34 +01:00
debug(`writeCertificate: ${fqdn} will use fallback certs`);
writeFileSync(certFilePath, domainObject.fallbackCertificate.cert);
writeFileSync(keyFilePath, domainObject.fallbackCertificate.key);
2022-11-13 17:27:05 +01:00
2022-11-28 22:32:34 +01:00
return { certFilePath, keyFilePath };
}
2020-08-10 14:54:37 -07:00
2022-11-28 22:32:34 +01:00
const certName = getAcmeCertificateNameSync(fqdn, domainObject);
2022-11-29 13:57:58 +01:00
let cert = await blobs.getString(`${blobs.CERT_PREFIX}-${certName}.cert`);
let key = await blobs.getString(`${blobs.CERT_PREFIX}-${certName}.key`);
2022-11-17 08:58:20 +01:00
2022-11-28 22:32:34 +01:00
if (!key || !cert) { // use fallback certs if we didn't manage to get acme certs
debug(`writeCertificate: ${fqdn} will use fallback certs because acme is missing`);
cert = domainObject.fallbackCertificate.cert;
key = domainObject.fallbackCertificate.key;
2021-08-17 14:04:29 -07:00
}
2022-11-28 22:32:34 +01:00
const certFilePath = path.join(paths.NGINX_CERT_DIR, `${certName}.cert`);
const keyFilePath = path.join(paths.NGINX_CERT_DIR, `${certName}.key`);
2020-08-07 22:59:57 -07:00
2022-11-28 22:32:34 +01:00
writeFileSync(certFilePath, cert);
writeFileSync(keyFilePath, key);
2022-11-28 22:32:34 +01:00
return { certFilePath, keyFilePath };
2022-11-11 18:09:10 +01:00
}
2018-02-02 21:21:51 -08:00
async function ensureCertificate(location, options, auditSource) {
2022-11-28 22:32:34 +01:00
assert.strictEqual(typeof location, 'object');
assert.strictEqual(typeof options, 'object');
2022-11-11 18:09:10 +01:00
assert.strictEqual(typeof auditSource, 'object');
2019-10-03 10:36:57 -07:00
2022-11-28 22:32:34 +01:00
const domainObject = await domains.get(location.domain);
if (!domainObject) throw new BoxError(BoxError.NOT_FOUND, `${location.domain} not found`);
2023-08-14 09:40:31 +05:30
const fqdn = dns.fqdn(location.subdomain, location.domain);
2022-11-28 22:32:34 +01:00
if (location.certificate) { // user certificate
2022-11-11 18:09:10 +01:00
debug(`ensureCertificate: ${fqdn} will use user certs`);
return;
}
2015-12-14 17:09:40 -08:00
2022-11-11 18:09:10 +01:00
if (domainObject.tlsConfig.provider === 'fallback') {
debug(`ensureCertificate: ${fqdn} will use fallback certs`);
return;
2021-08-17 14:04:29 -07:00
}
2022-11-28 22:32:34 +01:00
const certName = getAcmeCertificateNameSync(fqdn, domainObject);
2022-11-29 13:57:58 +01:00
const key = await blobs.getString(`${blobs.CERT_PREFIX}-${certName}.key`);
const cert = await blobs.getString(`${blobs.CERT_PREFIX}-${certName}.cert`);
2022-11-11 18:09:10 +01:00
2022-11-28 22:32:34 +01:00
if (key && cert) {
2024-02-21 12:33:04 +01:00
const sameProvider = await providerMatches(domainObject, cert);
const outdated = await needsRenewal(cert, options);
if (sameProvider && !outdated) {
2022-11-28 22:32:34 +01:00
debug(`ensureCertificate: ${fqdn} acme cert exists and is up to date`);
return;
}
debug(`ensureCertificate: ${fqdn} acme cert exists but provider mismatch or needs renewal`);
2021-08-17 14:04:29 -07:00
}
2019-10-01 14:04:39 -07:00
2022-11-28 22:32:34 +01:00
debug(`ensureCertificate: ${fqdn} needs acme cert`);
const [error] = await safe(acme2.getCertificate(fqdn, domainObject));
debug(`ensureCertificate: error: ${error ? error.message : 'null'}`);
2022-11-11 18:09:10 +01:00
2022-11-28 22:32:34 +01:00
await safe(eventlog.add(eventlog.ACTION_CERTIFICATE_NEW, auditSource, { domain: fqdn, errorMessage: error?.message || '' }));
}
async function writeDashboardNginxConfig(vhost, certificatePath) {
assert.strictEqual(typeof vhost, 'string');
2022-07-14 12:39:41 +05:30
assert.strictEqual(typeof certificatePath, 'object');
2021-04-16 11:17:13 -07:00
const data = {
sourceDir: path.resolve(__dirname, '..'),
vhost,
2023-08-03 13:38:42 +05:30
hasIPv6: network.hasIPv6(),
endpoint: 'dashboard',
2022-07-14 12:39:41 +05:30
certFilePath: certificatePath.certFilePath,
keyFilePath: certificatePath.keyFilePath,
2020-11-09 20:34:48 -08:00
robotsTxtQuoted: JSON.stringify('User-agent: *\nDisallow: /\n'),
2021-04-16 11:17:13 -07:00
proxyAuth: { enabled: false, id: null, location: nginxLocation('/') },
2023-03-06 11:15:55 +01:00
ocsp: await isOcspEnabled(certificatePath.certFilePath),
hstsPreload: false
};
2021-04-16 11:17:13 -07:00
const nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
const nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, `dashboard/${vhost}.conf`);
2022-11-28 22:32:34 +01:00
writeFileSync(nginxConfigFilename, nginxConf);
}
2022-11-28 22:32:34 +01:00
// also syncs the certs to disk
async function writeDashboardConfig(subdomain, domain) {
assert.strictEqual(typeof subdomain, 'string');
2022-11-28 22:32:34 +01:00
assert.strictEqual(typeof domain, 'string');
2019-09-30 11:52:23 -07:00
debug(`writeDashboardConfig: writing dashboard config for ${domain}`);
const dashboardFqdn = dns.fqdn(subdomain, domain);
2022-11-28 22:32:34 +01:00
const location = { domain, fqdn: dashboardFqdn, certificate: null };
const certificatePath = await writeCertificate(location);
2022-07-14 12:39:41 +05:30
await writeDashboardNginxConfig(dashboardFqdn, certificatePath);
await reload();
}
async function removeDashboardConfig(subdomain, domain) {
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof domain, 'string');
debug(`removeDashboardConfig: removing dashboard config of ${domain}`);
const vhost = dns.fqdn(subdomain, domain);
const nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, `dashboard/${vhost}.conf`);
if (!safe.fs.unlinkSync(nginxConfigFilename)) throw new BoxError(BoxError.FS_ERROR, safe.error.message);
await reload();
}
2022-11-28 22:32:34 +01:00
async function writeAppLocationNginxConfig(app, location, certificatePath) {
2018-06-29 16:14:13 +02:00
assert.strictEqual(typeof app, 'object');
2022-11-28 22:32:34 +01:00
assert.strictEqual(typeof location, 'object');
2022-07-14 12:39:41 +05:30
assert.strictEqual(typeof certificatePath, 'object');
2018-06-29 16:14:13 +02:00
2023-08-17 16:05:19 +05:30
const { type, fqdn} = location;
2022-11-28 22:32:34 +01:00
2021-04-16 11:17:13 -07:00
const data = {
2018-06-29 16:14:13 +02:00
sourceDir: path.resolve(__dirname, '..'),
2023-08-17 16:05:19 +05:30
vhost: fqdn,
2023-08-03 13:38:42 +05:30
hasIPv6: network.hasIPv6(),
2022-01-16 10:40:16 -08:00
ip: null,
port: null,
endpoint: null,
redirectTo: null,
2022-07-14 12:39:41 +05:30
certFilePath: certificatePath.certFilePath,
keyFilePath: certificatePath.keyFilePath,
2019-10-13 18:22:03 -07:00
robotsTxtQuoted: null,
2019-10-14 16:59:22 -07:00
cspQuoted: null,
2020-11-09 20:34:48 -08:00
hideHeaders: [],
2022-01-20 16:57:30 -08:00
proxyAuth: { enabled: false },
2022-06-06 20:04:22 +02:00
upstreamUri: '', // only for endpoint === external
2023-03-06 11:15:55 +01:00
ocsp: await isOcspEnabled(certificatePath.certFilePath),
hstsPreload: !!app.reverseProxyConfig?.hstsPreload
2018-06-29 16:14:13 +02:00
};
2022-01-16 10:40:16 -08:00
2023-08-17 16:05:19 +05:30
if (type === Location.TYPE_PRIMARY || type === Location.TYPE_ALIAS || type === Location.TYPE_SECONDARY) {
2022-01-14 22:40:51 -08:00
data.endpoint = 'app';
2022-06-06 20:04:22 +02:00
if (app.manifest.id === constants.PROXY_APP_APPSTORE_ID) {
2022-06-06 20:04:22 +02:00
data.endpoint = 'external';
2022-06-08 11:30:04 +02:00
data.upstreamUri = app.upstreamUri;
2022-06-06 20:04:22 +02:00
}
2022-01-14 22:40:51 -08:00
// maybe these should become per domain at some point
2022-01-16 10:40:16 -08:00
const reverseProxyConfig = app.reverseProxyConfig || {}; // some of our code uses fake app objects
if (reverseProxyConfig.robotsTxt) data.robotsTxtQuoted = JSON.stringify(app.reverseProxyConfig.robotsTxt);
if (reverseProxyConfig.csp) {
data.cspQuoted = `"${app.reverseProxyConfig.csp}"`;
data.hideHeaders = [ 'Content-Security-Policy' ];
if (reverseProxyConfig.csp.includes('frame-ancestors ')) data.hideHeaders.push('X-Frame-Options');
}
2023-08-17 16:05:19 +05:30
if (type === Location.TYPE_PRIMARY || type == Location.TYPE_ALIAS) {
2022-01-14 22:40:51 -08:00
data.proxyAuth = {
enabled: app.sso && app.manifest.addons && app.manifest.addons.proxyAuth,
id: app.id,
location: nginxLocation(safe.query(app.manifest, 'addons.proxyAuth.path') || '/')
};
data.ip = app.containerIp;
data.port = app.manifest.httpPort;
if (data.proxyAuth.enabled) {
data.proxyAuth.oidcClientId = app.id;
data.proxyAuth.oidcEndpoint = (await dashboard.getLocation()).fqdn;
}
2023-08-17 16:05:19 +05:30
} else if (type === Location.TYPE_SECONDARY) {
2022-01-14 22:40:51 -08:00
data.ip = app.containerIp;
2023-08-17 16:05:19 +05:30
const secondaryDomain = app.secondaryDomains.find(sd => sd.fqdn === fqdn);
2022-01-14 22:40:51 -08:00
data.port = app.manifest.httpPorts[secondaryDomain.environmentVariable].containerPort;
}
2023-08-17 16:05:19 +05:30
} else if (type === Location.TYPE_REDIRECT) {
2022-01-16 10:40:16 -08:00
data.proxyAuth = { enabled: false, id: app.id, location: nginxLocation('/') };
data.endpoint = 'redirect';
data.redirectTo = app.fqdn;
}
2021-04-16 11:17:13 -07:00
const nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
2023-08-17 16:05:19 +05:30
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)}`);
2022-11-28 22:32:34 +01:00
writeFileSync(filename, nginxConf);
2018-06-29 16:14:13 +02:00
}
2022-07-13 09:26:27 +05:30
async function writeAppConfigs(app) {
2019-09-09 21:41:55 -07:00
assert.strictEqual(typeof app, 'object');
2022-11-28 22:32:34 +01:00
const locations = getAppLocationsSync(app);
2019-09-09 21:41:55 -07:00
if (!safe.fs.mkdirSync(path.join(paths.NGINX_APPCONFIG_DIR, app.id), { recursive: true })) throw new BoxError(BoxError.FS_ERROR, `Could not create nginx config directory: ${safe.error.message}`);
2022-11-11 18:09:10 +01:00
for (const location of locations) {
2022-11-28 22:32:34 +01:00
const certificatePath = await writeCertificate(location);
await writeAppLocationNginxConfig(app, location, certificatePath);
2021-08-17 14:04:29 -07:00
}
await reload();
2019-09-09 21:41:55 -07:00
}
2022-11-28 22:32:34 +01:00
async function setUserCertificate(app, location) {
2022-11-13 17:27:05 +01:00
assert.strictEqual(typeof app, 'object');
2022-11-28 22:32:34 +01:00
assert.strictEqual(typeof location, 'object');
2022-11-13 17:27:05 +01:00
2022-11-28 22:32:34 +01:00
const certificatePath = await writeCertificate(location);
await writeAppLocationNginxConfig(app, location, certificatePath);
await reload();
2022-07-14 13:25:41 +05:30
}
2021-08-17 14:04:29 -07:00
async function configureApp(app, auditSource) {
assert.strictEqual(typeof app, 'object');
2018-01-30 15:16:34 -08:00
assert.strictEqual(typeof auditSource, 'object');
2022-11-28 22:32:34 +01:00
const locations = getAppLocationsSync(app);
2018-06-29 16:14:13 +02:00
2022-11-11 18:09:10 +01:00
for (const location of locations) {
await ensureCertificate(location, {}, auditSource);
2021-08-17 14:04:29 -07:00
}
2022-01-16 10:28:49 -08:00
2022-07-13 09:26:27 +05:30
await writeAppConfigs(app);
2022-11-28 22:32:34 +01:00
if (app.manifest.addons?.tls) await setupTlsAddon(app);
}
2021-08-17 14:04:29 -07:00
async function unconfigureApp(app) {
assert.strictEqual(typeof app, 'object');
if (!safe.fs.rmSync(path.join(paths.NGINX_APPCONFIG_DIR, app.id), { recursive: true, force: true })) throw new BoxError(BoxError.FS_ERROR, `Could not remove nginx config directory: ${safe.error.message}`);
2021-08-17 14:04:29 -07:00
await reload();
}
2022-11-28 22:32:34 +01:00
async function cleanupCerts(locations, auditSource, progressCallback) {
assert(Array.isArray(locations));
2022-02-24 19:52:51 -08:00
assert.strictEqual(typeof auditSource, 'object');
2022-07-13 10:49:08 +05:30
assert.strictEqual(typeof progressCallback, 'function');
2022-02-24 19:52:51 -08:00
2022-11-11 18:09:10 +01:00
progressCallback({ message: 'Checking expired certs for removal' });
2022-11-28 22:32:34 +01:00
const domainObjectMap = await domains.getDomainObjectMap();
const certNamesInUse = new Set();
2022-11-11 18:09:10 +01:00
for (const location of locations) {
2022-11-28 22:32:34 +01:00
certNamesInUse.add(await getAcmeCertificateNameSync(location.fqdn, domainObjectMap[location.domain]));
2022-11-11 18:09:10 +01:00
}
const now = new Date();
2022-11-28 22:32:34 +01:00
const certIds = await blobs.listCertIds();
const removedCertNames = [];
for (const certId of certIds) {
2022-12-08 10:04:50 +01:00
const certName = certId.match(new RegExp(`${blobs.CERT_PREFIX}-(.*).cert`))[1];
2022-11-28 22:32:34 +01:00
if (certNamesInUse.has(certName)) continue;
2022-11-29 13:57:58 +01:00
const cert = await blobs.getString(certId);
2024-02-21 12:33:04 +01:00
const { endDate } = await getCertificateDates(cert);
2023-02-01 11:05:50 +01:00
if (!endDate) continue; // some error
2023-02-01 11:05:50 +01:00
if (now - endDate >= (60 * 60 * 24 * 30 * 6 * 1000)) { // expired 6 months ago and not in use
2022-11-28 22:32:34 +01:00
progressCallback({ message: `deleting certs of ${certName}` });
2022-01-28 09:52:03 -08:00
// it is safe to delete the certs of stopped apps because their nginx configs are removed
2022-11-28 22:32:34 +01:00
safe.fs.unlinkSync(path.join(paths.NGINX_CERT_DIR, `${certName}.cert`));
safe.fs.unlinkSync(path.join(paths.NGINX_CERT_DIR, `${certName}.key`));
safe.fs.unlinkSync(path.join(paths.NGINX_CERT_DIR, `${certName}.csr`));
2022-11-28 22:32:34 +01:00
await blobs.del(`${blobs.CERT_PREFIX}-${certName}.key`);
await blobs.del(`${blobs.CERT_PREFIX}-${certName}.cert`);
await blobs.del(`${blobs.CERT_PREFIX}-${certName}.csr`);
2022-02-24 19:52:51 -08:00
2022-11-28 22:32:34 +01:00
removedCertNames.push(certName);
}
}
2021-09-23 17:39:59 -07:00
2022-11-28 22:32:34 +01:00
if (removedCertNames.length) await safe(eventlog.add(eventlog.ACTION_CERTIFICATE_CLEANUP, auditSource, { domains: removedCertNames }));
2022-02-24 19:52:51 -08:00
2021-09-23 17:39:59 -07:00
debug('cleanupCerts: done');
}
2022-11-29 18:11:22 +01:00
async function checkCerts(options, auditSource, progressCallback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof progressCallback, 'function');
2022-11-28 22:32:34 +01:00
let locations = [];
2023-08-17 10:44:07 +05:30
const dashboardLocation = await dashboard.getLocation();
locations.push(dashboardLocation);
2023-08-11 19:41:05 +05:30
2023-08-17 10:44:07 +05:30
const mailLocation = await mailServer.getLocation();
if (dashboardLocation.fqdn !== mailLocation.fqdn) locations.push(mailLocation);
2022-11-28 22:32:34 +01:00
const allApps = (await apps.list()).filter(app => app.runState !== apps.RSTATE_STOPPED);
for (const app of allApps) {
locations = locations.concat(getAppLocationsSync(app));
}
let percent = 1;
for (const location of locations) {
percent += Math.round(100/locations.length);
progressCallback({ percent, message: `Ensuring certs of ${location.fqdn}` });
await ensureCertificate(location, options, auditSource);
}
2022-11-29 18:11:22 +01:00
if (options.rebuild || fs.existsSync(paths.REVERSE_PROXY_REBUILD_FILE)) {
progressCallback( { message: 'Rebuilding app configs' });
for (const app of allApps) {
await writeAppConfigs(app);
}
await writeDashboardConfig(dashboardLocation.subdomain, dashboardLocation.domain);
2022-11-29 18:27:08 +01:00
await notifyCertChange(); // this allows user to "rebuild" using UI just in case we crashed and went out of sync
2022-11-29 18:11:22 +01:00
safe.fs.unlinkSync(paths.REVERSE_PROXY_REBUILD_FILE);
} else {
// sync all locations and not just the ones that changed. this helps with 0 length certs when disk is full and also
// if renewal task crashed midway.
for (const location of locations) {
await writeCertificate(location);
}
await reload();
await notifyCertChange(); // propagate any cert changes to services
}
2022-11-29 18:11:22 +01:00
2022-11-28 22:32:34 +01:00
await cleanupCerts(locations, auditSource, progressCallback);
}
async function startRenewCerts(options, auditSource) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof auditSource, 'object');
const taskId = await tasks.add(tasks.TASK_CHECK_CERTS, [ options, auditSource ]);
tasks.startTask(taskId, {});
return taskId;
}
function removeAppConfigs() {
debug('removeAppConfigs: removing app nginx configs');
2022-02-17 11:08:22 -08:00
// remove all configs which are not the default or current dashboard
for (const entry of fs.readdirSync(paths.NGINX_APPCONFIG_DIR, { withFileTypes: true })) {
if (entry.isDirectory() && entry.name === 'dashboard') continue;
if (entry.isFile() && entry.name === constants.NGINX_DEFAULT_CONFIG_FILE_NAME) continue;
const fullPath = path.join(paths.NGINX_APPCONFIG_DIR, entry.name);
if (entry.isDirectory()) {
fs.rmSync(fullPath, { recursive: true, force: true });
} else if (entry.isFile()) {
fs.unlinkSync(fullPath);
}
}
}
2021-08-17 14:04:29 -07:00
async function writeDefaultConfig(options) {
assert.strictEqual(typeof options, 'object');
const certFilePath = path.join(paths.NGINX_CERT_DIR, 'default.cert');
const keyFilePath = path.join(paths.NGINX_CERT_DIR, 'default.key');
if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) {
2019-09-30 15:28:05 -07:00
debug('writeDefaultConfig: create new cert');
const cn = 'cloudron-' + (new Date()).toISOString(); // randomize date a bit to keep firefox happy
2020-10-08 14:38:52 -07:00
// the days field is chosen to be less than 825 days per apple requirement (https://support.apple.com/en-us/HT210176)
2024-02-21 19:40:27 +01:00
await shell.exec('writeDefaultConfig', `openssl req -x509 -newkey rsa:2048 -keyout ${keyFilePath} -out ${certFilePath} -days 800 -subj /CN=${cn} -nodes`, {});
}
const data = {
sourceDir: path.resolve(__dirname, '..'),
vhost: '',
2023-08-03 13:38:42 +05:30
hasIPv6: network.hasIPv6(),
endpoint: options.activated ? 'ip' : 'setup',
certFilePath,
keyFilePath,
2020-11-09 20:34:48 -08:00
robotsTxtQuoted: JSON.stringify('User-agent: *\nDisallow: /\n'),
2021-04-16 11:17:13 -07:00
proxyAuth: { enabled: false, id: null, location: nginxLocation('/') },
2023-03-06 11:15:55 +01:00
ocsp: false, // self-signed cert
hstsPreload: false
};
const nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
const nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, constants.NGINX_DEFAULT_CONFIG_FILE_NAME);
2020-10-07 14:47:51 -07:00
debug(`writeDefaultConfig: writing configs for endpoint "${data.endpoint}"`);
2021-08-17 14:04:29 -07:00
if (!safe.fs.writeFileSync(nginxConfigFilename, nginxConf)) throw new BoxError(BoxError.FS_ERROR, safe.error);
2021-08-17 14:04:29 -07:00
await reload();
}
2022-11-29 18:11:22 +01:00
async function handleCertificateProviderChanged(domain) {
assert.strictEqual(typeof domain, 'string');
safe.fs.appendFileSync(paths.REVERSE_PROXY_REBUILD_FILE, `${domain}\n`, 'utf8');
}
2023-05-13 14:59:57 +02:00
async function getTrustedIps() {
const value = await settings.get(settings.TRUSTED_IPS_KEY);
return value || '';
2023-05-13 14:59:57 +02:00
}
async function setTrustedIps(trustedIps) {
assert.strictEqual(typeof trustedIps, 'string');
let trustedIpsConfig = 'real_ip_header X-Forwarded-For;\nreal_ip_recursive on;\n';
for (const line of trustedIps.split('\n')) {
if (!line || line.startsWith('#')) continue;
const rangeOrIP = line.trim();
// this checks for IPv4 and IPv6
if (!validator.isIP(rangeOrIP) && !validator.isIPRange(rangeOrIP)) throw new BoxError(BoxError.BAD_FIELD, `${rangeOrIP} is not a valid IP or range`);
trustedIpsConfig += `set_real_ip_from ${rangeOrIP};\n`;
}
await settings.set(settings.TRUSTED_IPS_KEY, trustedIps);
2023-05-13 14:59:57 +02:00
if (!safe.fs.writeFileSync(paths.NGINX_TRUSTED_IPS_FILE, trustedIpsConfig, 'utf8')) throw new BoxError(BoxError.FS_ERROR, safe.error.message);
await reload();
}