2015-12-10 13:31:47 -08:00
'use strict' ;
2016-05-06 18:44:37 +02:00
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 ,
2018-11-05 19:09:58 -08:00
2020-08-13 14:00:55 -07:00
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 ,
2022-11-16 12:02:11 +01:00
2023-08-04 13:19:48 +05:30
startRenewCerts ,
2021-05-18 13:28:48 -07:00
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
2020-08-13 14:00:55 -07:00
configureApp ,
unconfigureApp ,
2018-01-30 13:54:13 -08:00
2019-09-30 15:25:53 -07:00
// these only generate nginx config
2020-08-13 14:00:55 -07:00
writeDefaultConfig ,
2020-09-23 15:45:04 -07:00
writeDashboardConfig ,
2022-07-13 09:26:27 +05:30
writeAppConfigs ,
2020-08-13 14:00:55 -07:00
2023-09-29 09:26:22 +05:30
removeDashboardConfig ,
2020-08-13 14:00:55 -07:00
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
2016-05-06 18:44:37 +02:00
} ;
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' ) ,
2018-02-09 13:44:29 -08:00
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' ) ,
2018-01-31 18:27:18 +01:00
domains = require ( './domains.js' ) ,
2018-01-30 12:23:27 -08:00
ejs = require ( 'ejs' ) ,
2016-04-30 22:27:33 -07:00
eventlog = require ( './eventlog.js' ) ,
2025-05-06 16:16:33 +02:00
ipaddr = require ( './ipaddr.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' ) ,
2018-02-09 13:44:29 -08:00
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' ) ,
2015-12-10 20:31:38 -08:00
safe = require ( 'safetydance' ) ,
2019-07-26 10:49:29 -07:00
settings = require ( './settings.js' ) ,
2024-10-14 19:10:31 +02:00
shell = require ( './shell.js' ) ( 'reverseproxy' ) ,
2025-03-07 11:56:47 +01:00
tasks = require ( './tasks.js' ) ;
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' ) ;
2018-01-30 12:23:27 -08:00
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-10-15 10:10:15 +02:00
const [ error , result ] = await safe ( shell . spawn ( 'openssl' , [ 'x509' , '-startdate' , '-enddate' , '-subject' , '-noout' ] , { encoding : 'utf8' , 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 } ;
2015-12-14 12:38:19 -08:00
}
2023-08-02 19:22:53 +05:30
async function getReverseProxyConfig ( ) {
2023-08-03 11:34:33 +05:30
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-10-15 10:10:15 +02:00
const [ error , result ] = await safe ( shell . spawn ( 'openssl' , [ 'x509' , '-in' , certFilePath , '-noout' , '-ocsp_uri' ] , { encoding : 'utf8' } ) ) ;
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
}
2018-11-14 19:46:38 -08: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 ) {
2018-11-14 19:46:38 -08:00
assert . strictEqual ( typeof domainObject , 'object' ) ;
2022-11-29 13:57:58 +01:00
assert . strictEqual ( typeof cert , 'string' ) ;
2018-10-24 19:52:07 -07:00
2024-10-15 10:10:15 +02:00
const [ error , subjectAndIssuer ] = await safe ( shell . spawn ( 'openssl' , [ 'x509' , '-noout' , '-subject' , '-issuer' ] , { encoding : 'utf8' , input : cert } ) ) ;
2024-02-21 12:33:04 +01:00
if ( error ) return false ; // something bad happenned
2018-10-24 19:52:07 -07:00
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
2018-11-14 19:46:38 -08:00
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' ) ;
2018-11-14 19:46:38 -08:00
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 ) ;
2018-11-14 19:46:38 -08:00
// 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 ) ;
2018-10-24 19:52:07 -07:00
2018-11-14 19:46:38 -08:00
const mismatch = issuerMismatch || wildcardMismatch ;
2018-10-24 19:52:07 -07:00
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 } ` ) ;
2018-10-24 19:52:07 -07:00
return ! mismatch ;
}
2015-12-10 20:31:38 -08:00
// 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 ;
2015-12-10 20:31:38 -08:00
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' ) ;
2015-12-10 20:31:38 -08:00
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 ) ;
2018-11-05 19:09:58 -08:00
2024-10-15 10:10:15 +02:00
const [ checkHostError , checkHostOutput ] = await safe ( shell . spawn ( 'openssl' , [ 'x509' , '-noout' , '-checkhost' , fqdn ] , { encoding : 'utf8' , 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 } ` ) ;
2015-12-10 20:31:38 -08:00
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-10-15 10:10:15 +02:00
const [ pubKeyError1 , pubKeyFromCert ] = await safe ( shell . spawn ( 'openssl' , [ 'x509' , '-noout' , '-pubkey' ] , { encoding : 'utf8' , 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-10-15 10:10:15 +02:00
const [ pubKeyError2 , pubKeyFromKey ] = await safe ( shell . spawn ( 'openssl' , [ 'pkey' , '-pubout' ] , { encoding : 'utf8' , 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' ) ;
2018-11-23 11:39:00 -08:00
2024-02-21 12:33:04 +01:00
if ( pubKeyFromCert !== pubKeyFromKey ) throw new BoxError ( BoxError . BAD _FIELD , 'Public key does not match the certificate.' ) ;
2015-12-10 20:31:38 -08:00
2017-11-27 10:39:42 -08:00
// check expiration
2024-10-15 10:10:15 +02:00
const [ error ] = await safe ( shell . spawn ( '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
2015-12-10 20:31:38 -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 ( ) ;
2025-07-16 21:53:22 +02:00
await shell . sudo ( [ RESTART _SERVICE _CMD , 'box' ] , { } ) ; // directory server
2022-11-29 18:27:08 +01:00
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 ;
2018-01-30 16:14:05 -08:00
2025-07-16 21:53:22 +02:00
const [ error ] = await safe ( shell . sudo ( [ RESTART _SERVICE _CMD , 'nginx' ] , { } ) ) ;
2021-08-17 14:04:29 -07:00
if ( error ) throw new BoxError ( BoxError . NGINX _ERROR , ` Error reloading nginx: ${ error . message } ` ) ;
2018-01-30 16:14:05 -08:00
}
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' ) ;
2018-11-05 19:09:58 -08:00
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' ) ;
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
2024-04-29 12:49:20 +02:00
// SAN must contain all the domains since CN check is based on implementation if SAN is found. -checkhost also checks only SAN if present!
const opensslConfWithSan = ` ${ opensslConf } \n [SAN] \n subjectAltName=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' ) ;
2018-11-05 19:09:58 -08:00
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-10-15 10:10:15 +02:00
await shell . spawn ( 'openssl' , [ 'req' , '-x509' , '-newkey' , 'rsa:2048' , '-keyout' , keyFilePath , '-out' , certFilePath , '-days' , '800' , '-subj' , ` /CN=*. ${ cn } ` , '-extensions' , 'SAN' , '-config' , configFile , '-nodes' ] , { encoding : 'utf8 ' } ) ;
2018-11-05 19:09:58 -08:00
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 ) ;
2018-11-05 19:09:58 -08:00
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 ) ;
2018-11-05 19:09:58 -08:00
safe . fs . unlinkSync ( keyFilePath ) ;
2021-10-06 13:16:36 -07:00
return { cert , key } ;
2018-11-05 19:09:58 -08:00
}
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 ( ) ;
2022-11-30 15:16:16 +01:00
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' ) ;
2018-11-05 19:09:58 -08:00
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 ) ) ) ;
2018-11-05 19:09:58 -08:00
}
2022-11-13 18:03:39 +01:00
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 ( '*.' , '_.' ) ;
2022-11-29 13:35:17 +01:00
} 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 ;
2021-05-07 21:56:26 -07:00
}
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 ) ;
2023-02-01 11:12:55 +01:00
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
2023-02-01 11:12:55 +01:00
} 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 ;
2021-05-07 21:56:26 -07:00
}
2022-11-28 22:32:34 +01:00
async function getCertificate ( location ) {
assert . strictEqual ( typeof location , 'object' ) ;
2018-09-11 23:47:23 -07:00
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
}
2021-05-07 21:56:26 -07: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
}
2021-05-07 21:56:26 -07:00
2022-11-30 15:16:16 +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' ) ;
2021-05-07 21:56:26 -07:00
2022-11-29 17:13:58 +01:00
const curData = safe . fs . readFileSync ( filePath , { encoding : 'utf8' } ) ;
2022-11-30 15:16:16 +01:00
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 ) ;
2022-11-30 15:16:16 +01:00
return true ;
2021-05-07 21:56:26 -07:00
}
2022-11-28 22:32:34 +01:00
async function setupTlsAddon ( app ) {
assert . strictEqual ( typeof app , 'object' ) ;
2021-05-07 21:56:26 -07:00
2022-11-28 22:32:34 +01:00
const certificateDir = ` ${ paths . PLATFORM _DATA _DIR } /tls/ ${ app . id } ` ;
2022-11-30 15:16:16 +01:00
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
2022-11-30 15:16:16 +01:00
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
2022-11-30 15:16:16 +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 ` ) ;
2021-05-07 21:56:26 -07:00
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 ` ) ;
2021-05-07 21:56:26 -07:00
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 } ;
}
2016-09-12 01:21:51 -07:00
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 ) ;
2015-12-11 14:15:23 -08:00
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
2023-01-31 23:45:17 +01:00
async function ensureCertificate ( location , options , auditSource ) {
2022-11-28 22:32:34 +01:00
assert . strictEqual ( typeof location , 'object' ) ;
2023-01-31 23:45:17 +01:00
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
}
2019-10-03 10:46:03 -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 || '' } ) ) ;
2015-12-11 14:15:23 -08:00
}
2018-01-30 12:23:27 -08:00
2022-11-17 08:27:06 +01:00
async function writeDashboardNginxConfig ( vhost , certificatePath ) {
assert . strictEqual ( typeof vhost , 'string' ) ;
2022-07-14 12:39:41 +05:30
assert . strictEqual ( typeof certificatePath , 'object' ) ;
2018-01-30 12:23:27 -08:00
2021-04-16 11:17:13 -07:00
const data = {
2018-01-30 12:23:27 -08:00
sourceDir : path . resolve ( _ _dirname , '..' ) ,
2022-11-17 08:27:06 +01:00
vhost ,
2023-08-03 13:38:42 +05:30
hasIPv6 : network . hasIPv6 ( ) ,
2021-05-05 13:13:01 -07:00
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
2018-01-30 12:23:27 -08:00
} ;
2021-04-16 11:17:13 -07:00
const nginxConf = ejs . render ( NGINX _APPCONFIG _EJS , data ) ;
2022-11-17 15:49:59 +01:00
const nginxConfigFilename = path . join ( paths . NGINX _APPCONFIG _DIR , ` dashboard/ ${ vhost } .conf ` ) ;
2018-01-30 12:23:27 -08:00
2022-11-28 22:32:34 +01:00
writeFileSync ( nginxConfigFilename , nginxConf ) ;
2018-01-30 12:23:27 -08:00
}
2022-11-28 22:32:34 +01:00
// also syncs the certs to disk
2024-04-27 11:10:24 +02:00
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
2023-09-29 09:26:22 +05:30
debug ( ` writeDashboardConfig: writing dashboard config for ${ domain } ` ) ;
2018-12-13 22:15:08 -08:00
2024-04-27 11:10:24 +02:00
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 ) ;
2022-11-16 07:55:26 +01:00
await reload ( ) ;
2018-01-30 13:54:13 -08:00
}
2018-01-30 12:23:27 -08:00
2024-04-27 11:10:24 +02:00
async function removeDashboardConfig ( subdomain , domain ) {
assert . strictEqual ( typeof subdomain , 'string' ) ;
2023-09-29 09:26:22 +05:30
assert . strictEqual ( typeof domain , 'string' ) ;
debug ( ` removeDashboardConfig: removing dashboard config of ${ domain } ` ) ;
2024-04-27 11:10:24 +02:00
const vhost = dns . fqdn ( subdomain , domain ) ;
2023-09-29 09:26:22 +05:30
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
2022-06-09 13:57:57 +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 ;
2024-04-15 12:35:03 +02:00
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
2022-11-17 12:02:39 +01: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
}
2022-11-16 07:55:26 +01: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 ) {
2018-01-30 13:54:13 -08:00
assert . strictEqual ( typeof app , 'object' ) ;
2018-01-30 15:16:34 -08:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2018-01-30 12:23:27 -08:00
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 ) {
2023-01-31 23:45:17 +01:00
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 ) ;
2018-01-30 12:23:27 -08:00
}
2021-08-17 14:04:29 -07:00
async function unconfigureApp ( app ) {
2018-01-30 12:23:27 -08:00
assert . strictEqual ( typeof app , 'object' ) ;
2022-11-17 12:02:39 +01:00
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 ( ) ;
2018-01-30 12:23:27 -08:00
}
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
}
2021-06-01 09:09:16 -07: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
2021-06-01 09:09:16 -07:00
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 } ` } ) ;
2021-05-18 13:28:48 -07:00
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 ` ) ) ;
2021-05-18 13:28:48 -07:00
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-05-18 13:28:48 -07:00
}
}
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' ) ;
2021-05-18 13:28:48 -07:00
}
2022-11-29 18:11:22 +01:00
async function checkCerts ( options , auditSource , progressCallback ) {
assert . strictEqual ( typeof options , 'object' ) ;
2022-11-16 12:02:11 +01:00
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 ) ) ;
}
2023-01-31 17:58:28 +01:00
let percent = 1 ;
for ( const location of locations ) {
percent += Math . round ( 100 / locations . length ) ;
progressCallback ( { percent , message : ` Ensuring certs of ${ location . fqdn } ` } ) ;
2023-01-31 23:45:17 +01:00
await ensureCertificate ( location , options , auditSource ) ;
2023-01-31 17:58:28 +01:00
}
2022-11-16 12:02:11 +01:00
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 ) ;
}
2024-04-27 11:10:24 +02:00
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 ) ;
2022-11-30 15:16:16 +01:00
} else {
2023-01-31 17:58:28 +01:00
// 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 ( ) ;
2022-11-30 15:16:16 +01:00
await notifyCertChange ( ) ; // propagate any cert changes to services
2022-11-16 12:02:11 +01:00
}
2022-11-29 18:11:22 +01:00
2022-11-28 22:32:34 +01:00
await cleanupCerts ( locations , auditSource , progressCallback ) ;
2021-05-18 13:28:48 -07:00
}
2023-08-04 13:19:48 +05:30
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 ] ) ;
2025-06-17 18:54:12 +02:00
safe ( tasks . startTask ( taskId , { } ) , { debug } ) ; // background
2023-08-04 13:19:48 +05:30
return taskId ;
}
2018-01-30 12:23:27 -08:00
function removeAppConfigs ( ) {
2022-11-17 12:02:39 +01:00
debug ( 'removeAppConfigs: removing app nginx configs' ) ;
2022-02-17 11:08:22 -08:00
2021-06-27 08:58:33 -07:00
// remove all configs which are not the default or current dashboard
2022-11-17 12:02:39 +01:00
for ( const entry of fs . readdirSync ( paths . NGINX _APPCONFIG _DIR , { withFileTypes : true } ) ) {
2022-11-17 15:49:59 +01:00
if ( entry . isDirectory ( ) && entry . name === 'dashboard' ) continue ;
if ( entry . isFile ( ) && entry . name === constants . NGINX _DEFAULT _CONFIG _FILE _NAME ) continue ;
2022-11-17 12:02:39 +01:00
const fullPath = path . join ( paths . NGINX _APPCONFIG _DIR , entry . name ) ;
if ( entry . isDirectory ( ) ) {
fs . rmSync ( fullPath , { recursive : true , force : true } ) ;
2022-11-17 15:49:59 +01:00
} else if ( entry . isFile ( ) ) {
2022-11-17 12:02:39 +01:00
fs . unlinkSync ( fullPath ) ;
2018-11-10 22:02:42 -08:00
}
2018-01-30 12:23:27 -08:00
}
}
2021-08-17 14:04:29 -07:00
async function writeDefaultConfig ( options ) {
2020-09-23 22:13:02 -07:00
assert . strictEqual ( typeof options , 'object' ) ;
2018-01-30 12:23:27 -08:00
2020-09-23 22:13:02 -07:00
const certFilePath = path . join ( paths . NGINX _CERT _DIR , 'default.cert' ) ;
const keyFilePath = path . join ( paths . NGINX _CERT _DIR , 'default.key' ) ;
2018-01-30 12:23:27 -08:00
if ( ! fs . existsSync ( certFilePath ) || ! fs . existsSync ( keyFilePath ) ) {
2019-09-30 15:28:05 -07:00
debug ( 'writeDefaultConfig: create new cert' ) ;
2018-01-30 12:23:27 -08:00
2020-09-23 22:13:02 -07:00
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-10-15 10:10:15 +02:00
await shell . spawn ( 'openssl' , [ 'req' , '-x509' , '-newkey' , 'rsa:2048' , '-keyout' , keyFilePath , '-out' , certFilePath , '-days' , '800' , '-subj' , ` /CN= ${ cn } ` , '-nodes' ] , { encoding : 'utf8' } ) ;
2018-01-30 12:23:27 -08:00
}
2020-09-23 22:13:02 -07:00
const data = {
sourceDir : path . resolve ( _ _dirname , '..' ) ,
vhost : '' ,
2023-08-03 13:38:42 +05:30
hasIPv6 : network . hasIPv6 ( ) ,
2020-09-23 22:13:02 -07:00
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
2020-09-23 22:13:02 -07:00
} ;
const nginxConf = ejs . render ( NGINX _APPCONFIG _EJS , data ) ;
const nginxConfigFilename = path . join ( paths . NGINX _APPCONFIG _DIR , constants . NGINX _DEFAULT _CONFIG _FILE _NAME ) ;
2020-08-13 14:00:55 -07:00
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 ) ;
2020-08-13 14:00:55 -07:00
2021-08-17 14:04:29 -07:00
await reload ( ) ;
2020-08-13 14:00:55 -07:00
}
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 ( ) {
2023-08-02 19:14:18 +05:30
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 ( ) ;
2025-03-07 11:56:47 +01:00
if ( ! ipaddr . isValid ( rangeOrIP ) && ! ipaddr . isValidCIDR ( rangeOrIP ) ) throw new BoxError ( BoxError . BAD _FIELD , ` ${ rangeOrIP } is not a valid IP or range ` ) ;
2023-05-13 14:59:57 +02:00
trustedIpsConfig += ` set_real_ip_from ${ rangeOrIP } ; \n ` ;
}
2023-08-02 19:14:18 +05:30
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 ( ) ;
}