2015-12-10 13:31:47 -08:00
'use strict' ;
2016-05-06 18:44:37 +02:00
exports = module . exports = {
2021-05-05 10:34:22 -07:00
setAppCertificate ,
2020-08-13 14:00:55 -07:00
setFallbackCertificate ,
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
2021-05-05 10:34:22 -07:00
getCertificatePath ,
2020-08-13 14:00:55 -07:00
ensureCertificate ,
2016-06-22 13:48:07 -05:00
2021-05-18 13:28:48 -07:00
checkCerts ,
2017-01-17 09:57:15 -08:00
2019-09-30 15:25:53 -07:00
// the 'configure' 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 ,
2020-08-13 14:00:55 -07:00
writeAppConfig ,
removeAppConfigs ,
2021-05-04 21:40:11 -07:00
restoreFallbackCertificates ,
2018-01-30 12:23:27 -08:00
2016-06-22 13:48:07 -05:00
// exported for testing
2020-08-07 22:59:57 -07:00
_getAcmeApi : getAcmeApi
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' ) ,
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' ) ,
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' ) ,
2015-12-11 13:52:21 -08:00
fs = require ( 'fs' ) ,
2019-03-04 15:20:58 -08:00
mail = require ( './mail.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' ) ,
2018-01-30 12:23:27 -08:00
shell = require ( './shell.js' ) ,
2019-07-25 11:26:53 -07:00
sysinfo = require ( './sysinfo.js' ) ,
2018-04-29 10:58:45 -07:00
users = require ( './users.js' ) ,
2021-05-02 23:28:41 -07:00
util = require ( 'util' ) ,
_ = require ( 'underscore' ) ;
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 ;
let re = s . replace ( /[\^$\\.*+?()[\]{}|]/g , '\\$&' ) ; // https://github.com/es-shims/regexp.escape/blob/master/implementation.js
return ` ~ ^(?!( ${ re . slice ( 1 ) } )) ` ; // negative regex assertion - https://stackoverflow.com/questions/16302897/nginx-location-not-equal-to-regex
}
2021-07-15 09:50:11 -07:00
async function getAcmeApi ( domainObject ) {
2018-11-14 19:36:12 -08:00
assert . strictEqual ( typeof domainObject , 'object' ) ;
2016-04-19 08:21:23 -07:00
2021-07-15 09:50:11 -07:00
const acmeApi = acme2 ;
2016-12-05 17:01:23 +01:00
2021-07-15 09:50:11 -07:00
let apiOptions = { prod : false , performHttpAuthorization : false , wildcard : false , email : '' } ;
apiOptions . prod = domainObject . tlsConfig . provider . match ( /.*-prod/ ) !== null ; // matches 'le-prod' or 'letsencrypt-prod'
apiOptions . performHttpAuthorization = domainObject . provider . match ( /noop|manual|wildcard/ ) !== null ;
apiOptions . wildcard = ! ! domainObject . tlsConfig . wildcard ;
2016-01-13 14:21:23 -08:00
2018-11-14 19:36:12 -08:00
// registering user with an email requires A or MX record (https://github.com/letsencrypt/boulder/issues/1197)
// we cannot use admin@fqdn because the user might not have set it up.
// we simply update the account with the latest email we have each time when getting letsencrypt certs
// https://github.com/ietf-wg-acme/acme/issues/30
2021-07-15 09:50:11 -07:00
const [ error , owner ] = await safe ( users . getOwner ( ) ) ;
2021-07-29 17:08:01 +02:00
apiOptions . email = ( error || ! owner ) ? 'webmaster@cloudron.io' : owner . email ; // can error if not activated yet
2015-12-17 13:17:46 -08:00
2021-07-15 09:50:11 -07:00
const accountKeyPem = await blobs . get ( blobs . ACME _ACCOUNT _KEY ) ;
if ( ! accountKeyPem ) throw new BoxError ( BoxError . NOT _FOUND , 'acme account key not found' ) ;
2021-05-02 23:28:41 -07:00
2021-07-15 09:50:11 -07:00
apiOptions . accountKeyPem = accountKeyPem ;
2021-05-02 23:28:41 -07:00
2021-07-15 09:50:11 -07:00
return { acmeApi , apiOptions } ;
2015-12-14 12:28:00 -08:00
}
2021-06-01 09:09:16 -07:00
function getExpiryDate ( certFilePath ) {
2016-03-23 08:49:08 -07:00
assert . strictEqual ( typeof certFilePath , 'string' ) ;
2016-03-19 12:50:31 -07:00
2021-06-01 09:09:16 -07:00
if ( ! fs . existsSync ( certFilePath ) ) return null ; // not found
2015-12-14 12:38:19 -08:00
2021-06-04 17:51:26 -07:00
const result = safe . child _process . spawnSync ( '/usr/bin/openssl' , [ 'x509' , '-enddate' , '-noout' , '-in' , certFilePath ] ) ;
2021-06-01 09:09:16 -07:00
if ( ! result ) return null ; // some error
2016-03-18 22:59:51 -07:00
2021-06-01 09:09:16 -07:00
const notAfter = result . stdout . toString ( 'utf8' ) . trim ( ) . 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 ) ;
debug ( ` expiryDate: ${ certFilePath } notAfter= ${ notAfter } daysLeft= ${ daysLeft } ` ) ;
return notAfterDate ;
2015-12-14 12:38:19 -08:00
}
2021-04-16 13:33:32 -07:00
// 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
function hasOCSPUriSync ( certFilePath ) {
const result = safe . child _process . execSync ( ` openssl x509 -in ${ certFilePath } -noout -ocsp_uri ` , { encoding : 'utf8' } ) ;
return result && 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)
function providerMatchesSync ( domainObject , certFilePath , apiOptions ) {
assert . strictEqual ( typeof domainObject , 'object' ) ;
2018-10-24 19:52:07 -07:00
assert . strictEqual ( typeof certFilePath , 'string' ) ;
assert . strictEqual ( typeof apiOptions , 'object' ) ;
if ( ! fs . existsSync ( certFilePath ) ) return false ; // not found
const subjectAndIssuer = safe . child _process . execSync ( ` /usr/bin/openssl x509 -noout -subject -issuer -in " ${ certFilePath } " ` , { encoding : 'utf8' } ) ;
2018-11-23 11:39:00 -08:00
if ( ! subjectAndIssuer ) 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
const issuerMismatch = ( apiOptions . prod && ! isLetsEncryptProd ) || ( ! apiOptions . prod && isLetsEncryptProd ) ;
// bare domain is not part of wildcard SAN
2018-11-15 10:45:27 -08:00
const wildcardMismatch = ( domain !== domainObject . domain ) && ( apiOptions . wildcard && ! isWildcardCert ) || ( ! apiOptions . 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
2020-12-04 11:47:19 -08:00
debug ( ` providerMatchesSync: ${ certFilePath } subject= ${ subject } domain= ${ domain } issuer= ${ issuer } `
+ ` wildcard= ${ isWildcardCert } / ${ apiOptions . wildcard } prod= ${ isLetsEncryptProd } / ${ apiOptions . prod } `
+ ` 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)
2018-11-05 22:36:16 -08:00
function validateCertificate ( location , domainObject , certificate ) {
2018-11-05 19:09:58 -08:00
assert . strictEqual ( typeof location , 'string' ) ;
2018-11-05 21:26:53 -08:00
assert . strictEqual ( typeof domainObject , 'object' ) ;
2018-11-05 22:36:16 -08:00
assert ( certificate && typeof certificate , 'object' ) ;
const cert = certificate . cert , key = certificate . key ;
2015-12-10 20:31:38 -08:00
2018-01-26 19:31:06 -08:00
// check for empty cert and key strings
2019-10-22 16:46:24 -07:00
if ( ! cert && key ) return new BoxError ( BoxError . BAD _FIELD , 'missing cert' , { field : 'cert' } ) ;
if ( cert && ! key ) return new BoxError ( BoxError . BAD _FIELD , 'missing key' , { field : '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.
2021-08-13 17:22:28 -07:00
const fqdn = dns . fqdn ( location , domainObject ) ;
2018-11-05 19:09:58 -08:00
2020-03-24 20:56:49 -07:00
let result = safe . child _process . execSync ( ` openssl x509 -noout -checkhost " ${ fqdn } " ` , { encoding : 'utf8' , input : cert } ) ;
2019-10-22 16:46:24 -07:00
if ( result === null ) return new BoxError ( BoxError . BAD _FIELD , 'Unable to get certificate subject:' + safe . error . message , { field : 'cert' } ) ;
2017-05-11 21:55:25 +02:00
2019-10-22 16:46:24 -07:00
if ( result . indexOf ( 'does match certificate' ) === - 1 ) return new BoxError ( BoxError . BAD _FIELD , ` Certificate is not valid for this domain. Expecting ${ fqdn } ` , { field : 'cert' } ) ;
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
const pubKeyFromCert = safe . child _process . execSync ( 'openssl x509 -noout -pubkey' , { encoding : 'utf8' , input : cert } ) ;
if ( pubKeyFromCert === null ) return new BoxError ( BoxError . BAD _FIELD , ` Unable to get public key from cert: ${ safe . error . message } ` , { field : 'cert' } ) ;
2018-11-23 11:39:00 -08:00
2020-03-24 20:56:49 -07:00
const pubKeyFromKey = safe . child _process . execSync ( 'openssl pkey -pubout' , { encoding : 'utf8' , input : key } ) ;
if ( pubKeyFromKey === null ) return new BoxError ( BoxError . BAD _FIELD , ` Unable to get public key from private key: ${ safe . error . message } ` , { field : 'cert' } ) ;
2018-11-23 11:39:00 -08:00
2020-03-24 20:56:49 -07:00
if ( pubKeyFromCert !== pubKeyFromKey ) return new BoxError ( BoxError . BAD _FIELD , 'Public key does not match the certificate.' , { field : 'cert' } ) ;
2015-12-10 20:31:38 -08:00
2017-11-27 10:39:42 -08:00
// check expiration
2017-02-24 19:21:53 -08:00
result = safe . child _process . execSync ( 'openssl x509 -checkend 0' , { encoding : 'utf8' , input : cert } ) ;
2019-10-22 16:46:24 -07:00
if ( ! result ) return new BoxError ( BoxError . BAD _FIELD , 'Certificate has expired.' , { field : 'cert' } ) ;
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
2021-08-17 14:04:29 -07:00
async function reload ( ) {
if ( constants . TEST ) return ;
2018-01-30 16:14:05 -08:00
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 } ` ) ;
2018-01-30 16:14:05 -08:00
}
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 ` ) ;
let 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 ;
2020-08-15 18:40:59 -07:00
let cn = domain ;
2018-11-05 20:36:58 -08:00
2021-05-04 21:40:11 -07:00
debug ( ` generateFallbackCertificateSync: domain= ${ domain } cn= ${ cn } ` ) ;
2018-11-05 20:36:58 -08:00
opensslConfWithSan = ` ${ opensslConf } \n [SAN] \n subjectAltName=DNS: ${ domain } ,DNS:*. ${ cn } \n ` ;
2018-11-05 19:09:58 -08:00
let 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)
let certCommand = util . format ( ` openssl req -x509 -newkey rsa:2048 -keyout ${ keyFilePath } -out ${ certFilePath } -days 800 -subj /CN=*. ${ cn } -extensions SAN -config ${ configFile } -nodes ` ) ;
2019-10-22 16:46:24 -07:00
if ( ! safe . child _process . execSync ( certCommand ) ) return { error : new BoxError ( BoxError . OPENSSL _ERROR , safe . error . message ) } ;
2018-11-05 19:09:58 -08:00
safe . fs . unlinkSync ( configFile ) ;
const cert = safe . fs . readFileSync ( certFilePath , 'utf8' ) ;
2019-10-23 10:02:04 -07:00
if ( ! cert ) return { error : 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' ) ;
2019-10-23 10:02:04 -07:00
if ( ! key ) return { error : new BoxError ( BoxError . FS _ERROR , safe . error . message ) } ;
2018-11-05 19:09:58 -08:00
safe . fs . unlinkSync ( keyFilePath ) ;
return { cert : cert , key : key , error : null } ;
}
2021-08-13 17:22:28 -07:00
async function setFallbackCertificate ( domain , fallback ) {
2018-01-24 14:28:35 -08:00
assert . strictEqual ( typeof domain , 'string' ) ;
2018-11-05 19:09:58 -08:00
assert ( fallback && typeof fallback === 'object' ) ;
2018-01-26 20:30:37 -08:00
assert . strictEqual ( typeof fallback , 'object' ) ;
2015-12-11 13:52:21 -08:00
2020-08-07 11:41:15 -07:00
debug ( ` setFallbackCertificate: setting certs for domain ${ domain } ` ) ;
2021-08-13 17:22:28 -07:00
if ( ! safe . fs . writeFileSync ( path . join ( paths . NGINX _CERT _DIR , ` ${ domain } .host.cert ` ) , fallback . cert ) ) throw new BoxError ( BoxError . FS _ERROR , safe . error . message ) ;
if ( ! safe . fs . writeFileSync ( path . join ( paths . NGINX _CERT _DIR , ` ${ domain } .host.key ` ) , fallback . key ) ) throw new BoxError ( BoxError . FS _ERROR , safe . error . message ) ;
2015-12-11 13:52:21 -08:00
2019-03-04 19:35:22 -08:00
// TODO: maybe the cert is being used by the mail container
2021-08-17 14:04:29 -07:00
await reload ( ) ;
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-08-17 14:04:29 -07:00
result . forEach ( function ( domain ) {
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-05-04 21:40:11 -07:00
} ) ;
}
function getFallbackCertificatePathSync ( domain ) {
assert . strictEqual ( typeof domain , 'string' ) ;
2021-05-07 20:19:18 -07:00
const certFilePath = path . join ( paths . NGINX _CERT _DIR , ` ${ domain } .host.cert ` ) ;
const keyFilePath = path . join ( paths . NGINX _CERT _DIR , ` ${ domain } .host.key ` ) ;
2018-01-31 18:20:29 -08:00
2021-05-04 21:40:11 -07:00
return { certFilePath , keyFilePath } ;
2016-05-04 17:37:21 -07:00
}
2021-05-07 21:56:26 -07:00
function getAppCertificatePathSync ( vhost ) {
assert . strictEqual ( typeof vhost , 'string' ) ;
2018-11-05 19:09:58 -08:00
2021-05-07 21:56:26 -07:00
const certFilePath = path . join ( paths . NGINX _CERT _DIR , ` ${ vhost } .user.cert ` ) ;
const keyFilePath = path . join ( paths . NGINX _CERT _DIR , ` ${ vhost } .user.key ` ) ;
2018-11-05 19:09:58 -08:00
2021-05-07 21:56:26 -07:00
return { certFilePath , keyFilePath } ;
2018-11-05 19:09:58 -08:00
}
2021-05-07 21:56:26 -07:00
function getAcmeCertificatePathSync ( vhost , domainObject ) {
2021-01-19 13:47:11 -08:00
assert . strictEqual ( typeof vhost , '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
2021-05-07 22:43:30 -07:00
let certName , certFilePath , keyFilePath , csrFilePath , acmeChallengesDir = paths . ACME _CHALLENGES _DIR ;
2017-01-17 09:58:55 -08:00
2021-01-19 13:47:11 -08:00
if ( vhost !== domainObject . domain && domainObject . tlsConfig . wildcard ) { // bare domain is not part of wildcard SAN
2021-08-13 17:22:28 -07:00
certName = dns . makeWildcard ( vhost ) . replace ( '*.' , '_.' ) ;
2021-05-07 20:19:18 -07:00
certFilePath = path . join ( paths . NGINX _CERT _DIR , ` ${ certName } .cert ` ) ;
keyFilePath = path . join ( paths . NGINX _CERT _DIR , ` ${ certName } .key ` ) ;
2021-05-07 21:56:26 -07:00
csrFilePath = path . join ( paths . NGINX _CERT _DIR , ` ${ certName } .csr ` ) ;
2018-11-14 19:36:12 -08:00
} else {
2021-05-07 21:56:26 -07:00
certName = vhost ;
2021-05-07 20:19:18 -07:00
certFilePath = path . join ( paths . NGINX _CERT _DIR , ` ${ vhost } .cert ` ) ;
keyFilePath = path . join ( paths . NGINX _CERT _DIR , ` ${ vhost } .key ` ) ;
2021-05-07 21:56:26 -07:00
csrFilePath = path . join ( paths . NGINX _CERT _DIR , ` ${ vhost } .csr ` ) ;
}
2018-09-11 22:46:17 -07:00
2021-05-07 22:43:30 -07:00
return { certName , certFilePath , keyFilePath , csrFilePath , acmeChallengesDir } ;
2021-05-07 21:56:26 -07:00
}
2021-08-17 14:04:29 -07:00
async function setAppCertificate ( location , domainObject , certificate ) {
2021-05-07 21:56:26 -07:00
assert . strictEqual ( typeof location , 'string' ) ;
assert . strictEqual ( typeof domainObject , 'object' ) ;
assert . strictEqual ( typeof certificate , 'object' ) ;
2021-08-13 17:22:28 -07:00
const fqdn = dns . fqdn ( location , domainObject ) ;
2021-05-07 21:56:26 -07:00
const { certFilePath , keyFilePath } = getAppCertificatePathSync ( fqdn ) ;
if ( certificate . cert && certificate . key ) {
2021-08-17 14:04:29 -07:00
if ( ! safe . fs . writeFileSync ( certFilePath , certificate . cert ) ) throw safe . error ;
if ( ! safe . fs . writeFileSync ( keyFilePath , certificate . key ) ) throw safe . error ;
2021-05-07 21:56:26 -07:00
} else { // remove existing cert/key
if ( ! safe . fs . unlinkSync ( certFilePath ) ) debug ( ` Error removing cert: ${ safe . error . message } ` ) ;
if ( ! safe . fs . unlinkSync ( keyFilePath ) ) debug ( ` Error removing key: ${ safe . error . message } ` ) ;
2018-11-14 19:36:12 -08:00
}
2018-09-11 23:47:23 -07:00
2021-08-17 14:04:29 -07:00
await reload ( ) ;
2018-09-11 23:47:23 -07:00
}
2021-08-17 14:04:29 -07:00
async function getCertificatePath ( fqdn , domain ) {
2018-12-19 14:20:48 -08:00
assert . strictEqual ( typeof fqdn , 'string' ) ;
assert . strictEqual ( typeof domain , 'string' ) ;
2018-09-11 23:47:23 -07:00
2020-08-07 22:59:57 -07:00
// 1. user cert always wins
// 2. if using fallback provider, return that cert
// 3. look for LE certs
2021-08-17 14:04:29 -07:00
const domainObject = await domains . get ( domain ) ;
2018-11-14 19:36:12 -08:00
2021-08-17 14:04:29 -07:00
const appCertPath = getAppCertificatePathSync ( fqdn ) ; // user cert always wins
if ( fs . existsSync ( appCertPath . certFilePath ) && fs . existsSync ( appCertPath . keyFilePath ) ) return appCertPath ;
2020-08-07 22:59:57 -07:00
2021-08-17 14:04:29 -07:00
if ( domainObject . tlsConfig . provider === 'fallback' ) return getFallbackCertificatePathSync ( domain ) ;
2020-08-07 22:59:57 -07:00
2021-08-17 14:04:29 -07:00
const acmeCertPath = getAcmeCertificatePathSync ( fqdn , domainObject ) ;
if ( fs . existsSync ( acmeCertPath . certFilePath ) && fs . existsSync ( acmeCertPath . keyFilePath ) ) return acmeCertPath ;
2018-09-11 22:46:17 -07:00
2021-08-17 14:04:29 -07:00
return getFallbackCertificatePathSync ( domain ) ;
2017-01-17 10:21:42 -08:00
}
2021-05-07 21:56:26 -07:00
async function checkAppCertificate ( vhost , domainObject ) {
assert . strictEqual ( typeof vhost , 'string' ) ; // this can contain wildcard domain (for alias domains)
assert . strictEqual ( typeof domainObject , 'object' ) ;
const subdomain = vhost . substr ( 0 , vhost . length - domainObject . domain . length - 1 ) ;
2021-05-07 22:43:30 -07:00
const certificate = await apps . getCertificate ( subdomain , domainObject . domain ) ;
2021-05-07 21:56:26 -07:00
if ( ! certificate ) return null ;
const { certFilePath , keyFilePath } = getAppCertificatePathSync ( vhost ) ;
if ( ! safe . fs . writeFileSync ( certFilePath , certificate . cert ) ) throw new BoxError ( BoxError . FS _ERROR , ` Failed to write certificate: ${ safe . error . message } ` ) ;
if ( ! safe . fs . writeFileSync ( keyFilePath , certificate . key ) ) throw new BoxError ( BoxError . FS _ERROR , ` Failed to write key: ${ safe . error . message } ` ) ;
return { certFilePath , keyFilePath } ;
}
async function checkAcmeCertificate ( vhost , domainObject ) {
assert . strictEqual ( typeof vhost , 'string' ) ; // this can contain wildcard domain (for alias domains)
assert . strictEqual ( typeof domainObject , 'object' ) ;
const { certName , certFilePath , keyFilePath , csrFilePath } = getAcmeCertificatePathSync ( vhost , domainObject ) ;
const privateKey = await blobs . get ( ` ${ blobs . CERT _PREFIX } - ${ certName } .key ` ) ;
const cert = await blobs . get ( ` ${ blobs . CERT _PREFIX } - ${ certName } .cert ` ) ;
const csr = await blobs . get ( ` ${ blobs . CERT _PREFIX } - ${ certName } .csr ` ) ;
if ( ! privateKey || ! cert ) return null ;
if ( ! safe . fs . writeFileSync ( keyFilePath , privateKey ) ) throw new BoxError ( BoxError . FS _ERROR , ` Failed to write private key: ${ safe . error . message } ` ) ;
if ( ! safe . fs . writeFileSync ( certFilePath , cert ) ) throw new BoxError ( BoxError . FS _ERROR , ` Failed to write certificate: ${ safe . error . message } ` ) ;
if ( csr ) safe . fs . writeFileSync ( csrFilePath , csr ) ;
return { certFilePath , keyFilePath } ;
}
2021-05-07 22:43:30 -07:00
async function updateCertBlobs ( vhost , domainObject ) {
assert . strictEqual ( typeof vhost , 'string' ) ; // this can contain wildcard domain (for alias domains)
assert . strictEqual ( typeof domainObject , 'object' ) ;
const { certName , certFilePath , keyFilePath , csrFilePath } = getAcmeCertificatePathSync ( vhost , domainObject ) ;
const privateKey = safe . fs . readFileSync ( keyFilePath ) ;
if ( ! privateKey ) throw new BoxError ( BoxError . FS _ERROR , ` Failed to read private key: ${ safe . error . message } ` ) ;
const cert = safe . fs . readFileSync ( certFilePath ) ;
if ( ! cert ) throw new BoxError ( BoxError . FS _ERROR , ` Failed to read cert: ${ safe . error . message } ` ) ;
const csr = safe . fs . readFileSync ( csrFilePath ) ;
if ( ! csr ) throw new BoxError ( BoxError . FS _ERROR , ` Failed to read csr: ${ safe . error . message } ` ) ;
await blobs . set ( ` ${ blobs . CERT _PREFIX } - ${ certName } .key ` , privateKey ) ;
await blobs . set ( ` ${ blobs . CERT _PREFIX } - ${ certName } .cert ` , cert ) ;
await blobs . set ( ` ${ blobs . CERT _PREFIX } - ${ certName } .csr ` , csr ) ;
}
2021-08-17 14:04:29 -07:00
async function ensureCertificate ( vhost , domain , auditSource ) {
2018-09-12 12:50:04 -07:00
assert . strictEqual ( typeof vhost , 'string' ) ;
assert . strictEqual ( typeof domain , 'string' ) ;
2018-01-30 15:16:34 -08:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2015-12-11 14:15:23 -08:00
2021-08-17 14:04:29 -07:00
const domainObject = await domains . get ( domain ) ;
2016-09-12 01:21:51 -07:00
2021-08-17 14:04:29 -07:00
let bundle = await checkAppCertificate ( vhost , domainObject ) ;
if ( bundle ) return { bundle , renewed : false } ;
2020-08-07 22:59:57 -07:00
2021-08-17 14:04:29 -07:00
if ( domainObject . tlsConfig . provider === 'fallback' ) {
debug ( ` ensureCertificate: ${ vhost } will use fallback certs ` ) ;
2020-08-10 14:54:37 -07:00
2021-08-17 14:04:29 -07:00
return { bundle : getFallbackCertificatePathSync ( domain ) , renewed : false } ;
}
2020-08-07 22:59:57 -07:00
2021-08-17 14:04:29 -07:00
const { acmeApi , apiOptions } = await getAcmeApi ( domainObject ) ;
let notAfter = null ;
2021-07-15 09:50:11 -07:00
2021-08-17 14:04:29 -07:00
const [ , currentBundle ] = await safe ( checkAcmeCertificate ( vhost , domainObject ) ) ;
if ( currentBundle ) {
debug ( ` ensureCertificate: ${ vhost } certificate already exists at ${ currentBundle . keyFilePath } ` ) ;
notAfter = getExpiryDate ( currentBundle . certFilePath ) ;
const isExpiring = ( notAfter - new Date ( ) ) <= ( 30 * 24 * 60 * 60 * 1000 ) ; // expiring in a month
if ( ! isExpiring && providerMatchesSync ( domainObject , currentBundle . certFilePath , apiOptions ) ) return { bundle : currentBundle , renewed : false } ;
debug ( ` ensureCertificate: ${ vhost } cert requires renewal ` ) ;
} else {
debug ( ` ensureCertificate: ${ vhost } cert does not exist ` ) ;
}
2015-12-11 14:15:23 -08:00
2021-08-17 14:04:29 -07:00
debug ( 'ensureCertificate: getting certificate for %s with options %j' , vhost , _ . omit ( apiOptions , 'accountKeyPem' ) ) ;
2018-02-02 21:21:51 -08:00
2021-08-17 14:04:29 -07:00
const acmePaths = getAcmeCertificatePathSync ( vhost , domainObject ) ;
2021-09-07 09:34:23 -07:00
let [ error ] = await safe ( acmeApi . getCertificate ( vhost , domain , acmePaths , apiOptions ) ) ;
2021-08-17 14:04:29 -07:00
debug ( ` ensureCertificate: error: ${ error ? error . message : 'null' } cert: ${ acmePaths . certFilePath || 'null' } ` ) ;
2019-10-03 10:36:57 -07:00
2021-08-17 14:04:29 -07:00
await safe ( eventlog . add ( currentBundle ? eventlog . ACTION _CERTIFICATE _RENEWAL : eventlog . ACTION _CERTIFICATE _NEW , auditSource , { domain : vhost , errorMessage : error ? error . message : '' , notAfter } ) ) ;
2015-12-14 17:09:40 -08:00
2021-08-17 14:04:29 -07:00
if ( error && currentBundle && ( notAfter - new Date ( ) > 0 ) ) { // still some life left in this certificate
debug ( 'ensureCertificate: continue using existing bundle since renewal failed' ) ;
return { bundle : currentBundle , renewed : false } ;
}
2019-10-03 10:46:03 -07:00
2021-08-17 14:04:29 -07:00
if ( ! error ) {
[ error ] = await safe ( updateCertBlobs ( vhost , domainObject ) ) ;
if ( ! error ) return { bundle : { certFilePath : acmePaths . certFilePath , keyFilePath : acmePaths . keyFilePath } , renewed : true } ;
}
2019-10-01 14:04:39 -07:00
2021-08-17 14:04:29 -07:00
debug ( ` ensureCertificate: renewal of ${ vhost } failed. using fallback certificates for ${ domain } ` ) ;
2019-10-03 10:36:57 -07:00
2021-08-17 14:04:29 -07:00
return { bundle : getFallbackCertificatePathSync ( domain ) , renewed : false } ;
2015-12-11 14:15:23 -08:00
}
2018-01-30 12:23:27 -08:00
2021-08-17 14:04:29 -07:00
async function writeDashboardNginxConfig ( bundle , configFileName , vhost ) {
2018-01-30 16:16:10 -08:00
assert . strictEqual ( typeof bundle , 'object' ) ;
2018-01-30 12:23:27 -08:00
assert . strictEqual ( typeof configFileName , 'string' ) ;
assert . strictEqual ( typeof vhost , 'string' ) ;
2021-04-16 11:17:13 -07:00
const data = {
2018-01-30 12:23:27 -08:00
sourceDir : path . resolve ( _ _dirname , '..' ) ,
2020-09-23 22:13:02 -07:00
vhost : vhost ,
2019-07-25 11:26:53 -07:00
hasIPv6 : sysinfo . hasIPv6 ( ) ,
2021-05-05 13:13:01 -07:00
endpoint : 'dashboard' ,
2018-01-30 16:16:10 -08:00
certFilePath : bundle . certFilePath ,
keyFilePath : bundle . 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 ( '/' ) } ,
2021-04-16 13:33:32 -07:00
ocsp : hasOCSPUriSync ( bundle . certFilePath )
2018-01-30 12:23:27 -08:00
} ;
2021-04-16 11:17:13 -07:00
const nginxConf = ejs . render ( NGINX _APPCONFIG _EJS , data ) ;
const nginxConfigFilename = path . join ( paths . NGINX _APPCONFIG _DIR , configFileName ) ;
2018-01-30 12:23:27 -08:00
2021-08-17 14:04:29 -07:00
if ( ! safe . fs . writeFileSync ( nginxConfigFilename , nginxConf ) ) throw new BoxError ( BoxError . FS _ERROR , safe . error ) ;
2018-01-30 12:23:27 -08:00
2021-08-17 14:04:29 -07:00
await reload ( ) ;
2018-01-30 12:23:27 -08:00
}
2021-08-17 14:04:29 -07:00
async function writeDashboardConfig ( domain ) {
2018-12-13 22:15:08 -08:00
assert . strictEqual ( typeof domain , 'string' ) ;
2020-09-23 15:45:04 -07:00
debug ( ` writeDashboardConfig: writing admin config for ${ domain } ` ) ;
2019-09-30 11:52:23 -07:00
2021-08-17 14:04:29 -07:00
const domainObject = await domains . get ( domain ) ;
2018-12-13 22:15:08 -08:00
2021-08-17 14:04:29 -07:00
const dashboardFqdn = dns . fqdn ( constants . DASHBOARD _LOCATION , domainObject ) ;
2018-12-13 22:15:08 -08:00
2021-08-17 14:04:29 -07:00
const bundle = await getCertificatePath ( dashboardFqdn , domainObject . domain ) ;
2018-12-13 22:15:08 -08:00
2021-08-17 14:04:29 -07:00
await writeDashboardNginxConfig ( bundle , ` ${ dashboardFqdn } .conf ` , dashboardFqdn ) ;
2018-01-30 13:54:13 -08:00
}
2018-01-30 12:23:27 -08:00
2021-08-17 14:04:29 -07:00
async function writeAppNginxConfig ( app , fqdn , bundle ) {
2018-01-30 16:16:10 -08:00
assert . strictEqual ( typeof app , 'object' ) ;
2021-01-18 17:26:26 -08:00
assert . strictEqual ( typeof fqdn , 'string' ) ;
2018-01-30 16:16:10 -08:00
assert . strictEqual ( typeof bundle , 'object' ) ;
2020-11-20 14:13:16 -08:00
var sourceDir = path . resolve ( _ _dirname , '..' ) ;
var endpoint = 'app' ;
2018-01-30 16:16:10 -08:00
2020-11-20 14:13:16 -08:00
let robotsTxtQuoted = null , hideHeaders = [ ] , cspQuoted = null ;
const reverseProxyConfig = app . reverseProxyConfig || { } ; // some of our code uses fake app objects
if ( reverseProxyConfig . robotsTxt ) robotsTxtQuoted = JSON . stringify ( app . reverseProxyConfig . robotsTxt ) ;
if ( reverseProxyConfig . csp ) {
cspQuoted = ` " ${ app . reverseProxyConfig . csp } " ` ;
hideHeaders = [ 'Content-Security-Policy' ] ;
if ( reverseProxyConfig . csp . includes ( 'frame-ancestors ' ) ) hideHeaders . push ( 'X-Frame-Options' ) ;
}
2019-10-13 18:22:03 -07:00
2021-04-16 11:17:13 -07:00
const data = {
2020-11-20 14:13:16 -08:00
sourceDir : sourceDir ,
2021-01-18 17:26:26 -08:00
vhost : fqdn ,
2020-11-20 14:13:16 -08:00
hasIPv6 : sysinfo . hasIPv6 ( ) ,
ip : app . containerIp ,
port : app . manifest . httpPort ,
endpoint : endpoint ,
certFilePath : bundle . certFilePath ,
keyFilePath : bundle . keyFilePath ,
robotsTxtQuoted ,
cspQuoted ,
hideHeaders ,
proxyAuth : {
enabled : app . sso && app . manifest . addons && app . manifest . addons . proxyAuth ,
2020-11-20 18:13:23 -08:00
id : app . id ,
2021-01-08 14:10:11 -08:00
location : nginxLocation ( safe . query ( app . manifest , 'addons.proxyAuth.path' ) || '/' )
2020-11-20 14:13:16 -08:00
} ,
2021-04-16 11:17:13 -07:00
httpPaths : app . manifest . httpPaths || { } ,
2021-04-16 13:33:32 -07:00
ocsp : hasOCSPUriSync ( bundle . certFilePath )
2020-11-20 14:13:16 -08:00
} ;
2021-04-16 11:17:13 -07:00
const nginxConf = ejs . render ( NGINX _APPCONFIG _EJS , data ) ;
2018-01-30 16:16:10 -08:00
2021-01-19 13:36:17 -08:00
const aliasSuffix = app . fqdn === fqdn ? '' : ` -alias- ${ fqdn . replace ( '*' , '_' ) } ` ;
2021-01-18 17:26:26 -08:00
var nginxConfigFilename = path . join ( paths . NGINX _APPCONFIG _DIR , ` ${ app . id } ${ aliasSuffix } .conf ` ) ;
debug ( 'writeAppNginxConfig: writing config for "%s" to %s with options %j' , fqdn , nginxConfigFilename , data ) ;
2018-01-30 16:16:10 -08:00
2020-11-20 14:13:16 -08:00
if ( ! safe . fs . writeFileSync ( nginxConfigFilename , nginxConf ) ) {
debug ( 'Error creating nginx config for "%s" : %s' , app . fqdn , safe . error . message ) ;
2021-08-17 14:04:29 -07:00
throw new BoxError ( BoxError . FS _ERROR , safe . error ) ;
2020-11-20 14:13:16 -08:00
}
2021-08-17 14:04:29 -07:00
await reload ( ) ;
2018-01-30 16:16:10 -08:00
}
2021-08-17 14:04:29 -07:00
async function writeAppRedirectNginxConfig ( app , fqdn , bundle ) {
2018-06-29 16:14:13 +02:00
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof fqdn , 'string' ) ;
assert . strictEqual ( typeof bundle , 'object' ) ;
2021-04-16 11:17:13 -07:00
const data = {
2018-06-29 16:14:13 +02:00
sourceDir : path . resolve ( _ _dirname , '..' ) ,
vhost : fqdn ,
redirectTo : app . fqdn ,
2019-07-25 11:26:53 -07:00
hasIPv6 : sysinfo . hasIPv6 ( ) ,
2018-06-29 16:14:13 +02:00
endpoint : 'redirect' ,
certFilePath : bundle . certFilePath ,
keyFilePath : bundle . 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 : [ ] ,
2021-04-16 11:17:13 -07:00
proxyAuth : { enabled : false , id : app . id , location : nginxLocation ( '/' ) } ,
2021-04-16 13:33:32 -07:00
ocsp : hasOCSPUriSync ( bundle . certFilePath )
2018-06-29 16:14:13 +02:00
} ;
2021-04-16 11:17:13 -07:00
const nginxConf = ejs . render ( NGINX _APPCONFIG _EJS , data ) ;
2018-06-29 16:14:13 +02:00
2018-06-29 19:04:48 +02:00
// if we change the filename, also change it in unconfigureApp()
2021-04-16 11:17:13 -07:00
const nginxConfigFilename = path . join ( paths . NGINX _APPCONFIG _DIR , ` ${ app . id } -redirect- ${ fqdn } .conf ` ) ;
2018-06-29 16:14:13 +02:00
debug ( 'writing config for "%s" redirecting to "%s" to %s with options %j' , app . fqdn , fqdn , nginxConfigFilename , data ) ;
if ( ! safe . fs . writeFileSync ( nginxConfigFilename , nginxConf ) ) {
debug ( 'Error creating nginx redirect config for "%s" : %s' , app . fqdn , safe . error . message ) ;
2021-08-17 14:04:29 -07:00
throw new BoxError ( BoxError . FS _ERROR , safe . error ) ;
2018-06-29 16:14:13 +02:00
}
2021-08-17 14:04:29 -07:00
await reload ( ) ;
2018-06-29 16:14:13 +02:00
}
2021-08-17 14:04:29 -07:00
async function writeAppConfig ( app ) {
2019-09-09 21:41:55 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
2021-01-18 17:26:26 -08:00
let appDomains = [ ] ;
2021-01-18 22:31:10 -08:00
appDomains . push ( { domain : app . domain , fqdn : app . fqdn , type : 'primary' } ) ;
2019-09-09 21:41:55 -07:00
2021-01-18 17:26:26 -08:00
app . alternateDomains . forEach ( function ( alternateDomain ) {
appDomains . push ( { domain : alternateDomain . domain , fqdn : alternateDomain . fqdn , type : 'alternate' } ) ;
} ) ;
2019-09-09 21:41:55 -07:00
2021-01-18 17:26:26 -08:00
app . aliasDomains . forEach ( function ( aliasDomain ) {
appDomains . push ( { domain : aliasDomain . domain , fqdn : aliasDomain . fqdn , type : 'alias' } ) ;
} ) ;
2019-09-09 21:41:55 -07:00
2021-08-17 14:04:29 -07:00
for ( const appDomain of appDomains ) {
const bundle = await getCertificatePath ( appDomain . fqdn , appDomain . domain ) ;
2021-01-18 17:26:26 -08:00
2021-08-17 14:04:29 -07:00
if ( appDomain . type === 'primary' ) {
await writeAppNginxConfig ( app , appDomain . fqdn , bundle ) ;
} else if ( appDomain . type === 'alternate' ) {
await writeAppRedirectNginxConfig ( app , appDomain . fqdn , bundle ) ;
} else if ( appDomain . type === 'alias' ) {
await writeAppNginxConfig ( app , appDomain . fqdn , bundle ) ;
}
}
2019-09-09 21:41:55 -07:00
}
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
2021-01-18 17:26:26 -08:00
let appDomains = [ ] ;
2021-01-18 22:31:10 -08:00
appDomains . push ( { domain : app . domain , fqdn : app . fqdn , type : 'primary' } ) ;
2018-01-30 12:23:27 -08:00
2021-01-18 17:26:26 -08:00
app . alternateDomains . forEach ( function ( alternateDomain ) {
appDomains . push ( { domain : alternateDomain . domain , fqdn : alternateDomain . fqdn , type : 'alternate' } ) ;
} ) ;
2018-06-29 16:02:33 +02:00
2021-01-18 17:26:26 -08:00
app . aliasDomains . forEach ( function ( aliasDomain ) {
appDomains . push ( { domain : aliasDomain . domain , fqdn : aliasDomain . fqdn , type : 'alias' } ) ;
} ) ;
2018-06-29 16:14:13 +02:00
2021-08-17 14:04:29 -07:00
for ( const appDomain of appDomains ) {
const { bundle } = await ensureCertificate ( appDomain . fqdn , appDomain . domain , auditSource ) ;
2021-01-18 17:26:26 -08:00
2021-08-17 14:04:29 -07:00
if ( appDomain . type === 'primary' ) {
await writeAppNginxConfig ( app , appDomain . fqdn , bundle ) ;
} else if ( appDomain . type === 'alternate' ) {
await writeAppRedirectNginxConfig ( app , appDomain . fqdn , bundle ) ;
} else if ( appDomain . type === 'alias' ) {
await writeAppNginxConfig ( app , appDomain . fqdn , bundle ) ;
}
}
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' ) ;
2021-08-17 14:04:29 -07:00
const configFilenames = safe . fs . readdirSync ( paths . NGINX _APPCONFIG _DIR ) ;
if ( ! configFilenames ) throw new BoxError ( BoxError . FS _ERROR , ` Error loading nginx config files: ${ safe . error . message } ` ) ;
2018-06-29 19:04:48 +02:00
2021-08-17 14:04:29 -07:00
for ( const filename of configFilenames ) {
if ( ! filename . startsWith ( app . id ) ) continue ;
safe . fs . unlinkSync ( path . join ( paths . NGINX _APPCONFIG _DIR , filename ) ) ;
}
await reload ( ) ;
2018-01-30 12:23:27 -08:00
}
2021-08-25 15:52:05 -07:00
async function renewCerts ( options , auditSource , progressCallback ) {
2018-10-24 13:01:45 -07:00
assert . strictEqual ( typeof options , 'object' ) ;
2018-01-30 16:16:10 -08:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2018-12-10 20:20:53 -08:00
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2018-01-30 16:16:10 -08:00
2021-08-25 15:52:05 -07:00
const allApps = await apps . list ( ) ;
2018-01-30 16:16:10 -08:00
2021-08-25 15:52:05 -07:00
let appDomains = [ ] ;
2018-08-25 11:04:49 +02:00
2021-08-25 15:52:05 -07:00
// add webadmin and mail domain
if ( settings . mailFqdn ( ) === settings . dashboardFqdn ( ) ) {
appDomains . push ( { domain : settings . dashboardDomain ( ) , fqdn : settings . dashboardFqdn ( ) , type : 'webadmin+mail' , nginxConfigFilename : path . join ( paths . NGINX _APPCONFIG _DIR , ` ${ settings . dashboardFqdn ( ) } .conf ` ) } ) ;
} else {
appDomains . push ( { domain : settings . dashboardDomain ( ) , fqdn : settings . dashboardFqdn ( ) , type : 'webadmin' , nginxConfigFilename : path . join ( paths . NGINX _APPCONFIG _DIR , ` ${ settings . dashboardFqdn ( ) } .conf ` ) } ) ;
appDomains . push ( { domain : settings . mailDomain ( ) , fqdn : settings . mailFqdn ( ) , type : 'mail' } ) ;
}
2020-01-26 16:05:23 -08:00
2021-08-25 15:52:05 -07:00
for ( const app of allApps ) {
if ( app . runState === apps . RSTATE _STOPPED ) continue ; // do not renew certs of stopped apps
2018-08-25 11:04:49 +02:00
2021-08-25 15:52:05 -07:00
appDomains . push ( { domain : app . domain , fqdn : app . fqdn , type : 'primary' , app : app , nginxConfigFilename : path . join ( paths . NGINX _APPCONFIG _DIR , app . id + '.conf' ) } ) ;
2021-01-18 17:26:26 -08:00
2021-08-25 15:52:05 -07:00
app . alternateDomains . forEach ( function ( alternateDomain ) {
const nginxConfigFilename = path . join ( paths . NGINX _APPCONFIG _DIR , ` ${ app . id } -redirect- ${ alternateDomain . fqdn } .conf ` ) ;
appDomains . push ( { domain : alternateDomain . domain , fqdn : alternateDomain . fqdn , type : 'alternate' , app : app , nginxConfigFilename } ) ;
2018-08-25 11:04:49 +02:00
} ) ;
2021-08-25 15:52:05 -07:00
app . aliasDomains . forEach ( function ( aliasDomain ) {
const nginxConfigFilename = path . join ( paths . NGINX _APPCONFIG _DIR , ` ${ app . id } -alias- ${ aliasDomain . fqdn . replace ( '*' , '_' ) } .conf ` ) ;
appDomains . push ( { domain : aliasDomain . domain , fqdn : aliasDomain . fqdn , type : 'alias' , app : app , nginxConfigFilename } ) ;
2019-10-01 11:25:17 -07:00
} ) ;
2021-08-25 15:52:05 -07:00
}
if ( options . domain ) appDomains = appDomains . filter ( function ( appDomain ) { return appDomain . domain === options . domain ; } ) ;
2021-08-30 15:21:30 -07:00
let progress = 1 , renewedCerts = [ ] ;
2021-08-25 15:52:05 -07:00
for ( const appDomain of appDomains ) {
progressCallback ( { percent : progress , message : ` Ensuring certs of ${ appDomain . fqdn } ` } ) ;
progress += Math . round ( 100 / appDomains . length ) ;
const { bundle , renewed } = await ensureCertificate ( appDomain . fqdn , appDomain . domain , auditSource ) ;
2021-08-30 15:21:30 -07:00
if ( renewed ) renewedCerts . push ( appDomain . fqdn ) ;
2021-08-25 15:52:05 -07:00
if ( appDomain . type === 'mail' ) continue ; // mail has no nginx config to check current cert
// hack to check if the app's cert changed or not. this doesn't handle prod/staging le change since they use same file name
let currentNginxConfig = safe . fs . readFileSync ( appDomain . nginxConfigFilename , 'utf8' ) || '' ;
if ( currentNginxConfig . includes ( bundle . certFilePath ) ) return ;
debug ( ` renewCerts: creating new nginx config since ${ appDomain . nginxConfigFilename } does not have ${ bundle . certFilePath } ` ) ;
// reconfigure since the cert changed
if ( appDomain . type === 'webadmin' || appDomain . type === 'webadmin+mail' ) {
await writeDashboardNginxConfig ( bundle , ` ${ settings . dashboardFqdn ( ) } .conf ` , settings . dashboardFqdn ( ) ) ;
} else if ( appDomain . type === 'primary' ) {
await writeAppNginxConfig ( appDomain . app , appDomain . fqdn , bundle ) ;
} else if ( appDomain . type === 'alternate' ) {
await writeAppRedirectNginxConfig ( appDomain . app , appDomain . fqdn , bundle ) ;
} else if ( appDomain . type === 'alias' ) {
await writeAppNginxConfig ( appDomain . app , appDomain . fqdn , bundle ) ;
} else {
throw new BoxError ( BoxError . INTERNAL _ERROR , ` Unknown domain type for ${ appDomain . fqdn } . This should never happen ` ) ;
}
}
2021-08-30 15:21:30 -07:00
debug ( ` renewCerts: Renewed certs of ${ JSON . stringify ( renewedCerts ) } ` ) ;
if ( renewedCerts . length === 0 ) return ;
2021-08-25 15:52:05 -07:00
2021-08-30 15:21:30 -07:00
if ( renewedCerts . includes ( settings . mailFqdn ( ) ) ) await mail . handleCertChanged ( ) ;
2021-08-25 15:52:05 -07:00
await reload ( ) ; // reload nginx if any certs were updated but the config was not rewritten
// restart tls apps on cert change
2021-08-30 15:21:30 -07:00
const tlsApps = allApps . filter ( app => app . manifest . addons && app . manifest . addons . tls && renewedCerts . includes ( app . fqdn ) ) ;
2021-08-25 15:52:05 -07:00
for ( const app of tlsApps ) {
await apps . restart ( app , auditSource ) ;
}
2018-01-30 16:16:10 -08:00
}
2021-05-18 13:28:48 -07:00
async function cleanupCerts ( ) {
const filenames = await fs . promises . readdir ( paths . NGINX _CERT _DIR ) ;
const certFilenames = filenames . filter ( f => f . endsWith ( '.cert' ) ) ;
2021-06-01 09:09:16 -07:00
const now = new Date ( ) ;
2021-05-18 13:28:48 -07:00
for ( const certFilename of certFilenames ) {
const certFilePath = path . join ( paths . NGINX _CERT _DIR , certFilename ) ;
2021-06-01 09:09:16 -07:00
const notAfter = getExpiryDate ( certFilePath ) ;
if ( ! notAfter ) continue ; // some error
if ( now - notAfter >= ( 60 * 60 * 24 * 30 * 6 * 1000 ) ) { // expired 6 months ago
2021-05-18 13:28:48 -07:00
const fqdn = certFilename . replace ( /\.cert$/ , '' ) ;
debug ( ` cleanupCerts: deleting certs of ${ fqdn } ` ) ;
safe . fs . unlinkSync ( certFilePath ) ;
safe . fs . unlinkSync ( path . join ( paths . NGINX _CERT _DIR , ` ${ fqdn } .key ` ) ) ;
safe . fs . unlinkSync ( path . join ( paths . NGINX _CERT _DIR , ` ${ fqdn } .csr ` ) ) ;
await blobs . del ( ` ${ blobs . CERT _PREFIX } - ${ fqdn } .key ` ) ;
await blobs . del ( ` ${ blobs . CERT _PREFIX } - ${ fqdn } .cert ` ) ;
await blobs . del ( ` ${ blobs . CERT _PREFIX } - ${ fqdn } .csr ` ) ;
}
}
}
2021-08-25 15:52:05 -07:00
async function checkCerts ( options , auditSource , progressCallback ) {
2021-05-18 13:28:48 -07:00
assert . strictEqual ( typeof options , 'object' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2021-08-25 15:52:05 -07:00
await renewCerts ( options , auditSource , progressCallback ) ;
await cleanupCerts ( ) ;
2021-05-18 13:28:48 -07:00
}
2018-01-30 12:23:27 -08:00
function removeAppConfigs ( ) {
2021-06-27 08:58:33 -07:00
const dashboardConfigFilename = ` ${ settings . dashboardFqdn ( ) } .conf ` ;
// remove all configs which are not the default or current dashboard
2018-11-10 22:02:42 -08:00
for ( let appConfigFile of fs . readdirSync ( paths . NGINX _APPCONFIG _DIR ) ) {
2021-06-27 08:58:33 -07:00
if ( appConfigFile !== constants . NGINX _DEFAULT _CONFIG _FILE _NAME && appConfigFile !== dashboardConfigFilename ) {
2018-11-10 22:02:42 -08:00
fs . unlinkSync ( path . join ( paths . NGINX _APPCONFIG _DIR , appConfigFile ) ) ;
}
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)
if ( ! safe . child _process . execSync ( ` openssl req -x509 -newkey rsa:2048 -keyout ${ keyFilePath } -out ${ certFilePath } -days 800 -subj /CN= ${ cn } -nodes ` ) ) {
2019-09-30 15:28:05 -07:00
debug ( ` writeDefaultConfig: could not generate certificate: ${ safe . error . message } ` ) ;
2021-08-17 14:04:29 -07:00
throw new BoxError ( BoxError . OPENSSL _ERROR , safe . error ) ;
2018-11-23 11:39:00 -08:00
}
2018-01-30 12:23:27 -08:00
}
2020-09-23 22:13:02 -07:00
const data = {
sourceDir : path . resolve ( _ _dirname , '..' ) ,
vhost : '' ,
hasIPv6 : sysinfo . 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 ( '/' ) } ,
ocsp : false // self-signed cert
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
}