2020-02-18 23:55:06 -05:00
const fs = require ( 'fs' ) ;
const _ = require ( 'lodash' ) ;
const logger = require ( '../logger' ) . ssl ;
const error = require ( '../lib/error' ) ;
const certificateModel = require ( '../models/certificate' ) ;
const internalAuditLog = require ( './audit-log' ) ;
const tempWrite = require ( 'temp-write' ) ;
const utils = require ( '../lib/utils' ) ;
const moment = require ( 'moment' ) ;
const debug _mode = process . env . NODE _ENV !== 'production' || ! ! process . env . DEBUG ;
const le _staging = process . env . NODE _ENV !== 'production' ;
const internalNginx = require ( './nginx' ) ;
const internalHost = require ( './host' ) ;
const certbot _command = '/usr/bin/certbot' ;
const le _config = '/etc/letsencrypt.ini' ;
2020-10-08 07:21:17 -04:00
const dns _plugins = require ( '../global/certbot-dns-plugins' ) ;
2020-02-18 23:55:06 -05:00
function omissions ( ) {
return [ 'is_deleted' ] ;
}
const internalCertificate = {
allowed _ssl _files : [ 'certificate' , 'certificate_key' , 'intermediate_certificate' ] ,
interval _timeout : 1000 * 60 * 60 , // 1 hour
interval : null ,
interval _processing : false ,
initTimer : ( ) => {
logger . info ( 'Let\'s Encrypt Renewal Timer initialized' ) ;
internalCertificate . interval = setInterval ( internalCertificate . processExpiringHosts , internalCertificate . interval _timeout ) ;
// And do this now as well
internalCertificate . processExpiringHosts ( ) ;
} ,
/ * *
* Triggered by a timer , this will check for expiring hosts and renew their ssl certs if required
* /
processExpiringHosts : ( ) => {
if ( ! internalCertificate . interval _processing ) {
internalCertificate . interval _processing = true ;
logger . info ( 'Renewing SSL certs close to expiry...' ) ;
let cmd = certbot _command + ' renew --non-interactive --quiet ' +
'--config "' + le _config + '" ' +
'--preferred-challenges "dns,http" ' +
'--disable-hook-validation ' +
( le _staging ? '--staging' : '' ) ;
return utils . exec ( cmd )
. then ( ( result ) => {
if ( result ) {
logger . info ( 'Renew Result: ' + result ) ;
}
return internalNginx . reload ( )
. then ( ( ) => {
logger . info ( 'Renew Complete' ) ;
return result ;
} ) ;
} )
. then ( ( ) => {
// Now go and fetch all the letsencrypt certs from the db and query the files and update expiry times
return certificateModel
. query ( )
. where ( 'is_deleted' , 0 )
. andWhere ( 'provider' , 'letsencrypt' )
. then ( ( certificates ) => {
if ( certificates && certificates . length ) {
let promises = [ ] ;
certificates . map ( function ( certificate ) {
promises . push (
internalCertificate . getCertificateInfoFromFile ( '/etc/letsencrypt/live/npm-' + certificate . id + '/fullchain.pem' )
. then ( ( cert _info ) => {
return certificateModel
. query ( )
. where ( 'id' , certificate . id )
. andWhere ( 'provider' , 'letsencrypt' )
. patch ( {
2020-08-14 19:23:19 -04:00
expires _on : moment ( cert _info . dates . to , 'X' ) . format ( 'YYYY-MM-DD HH:mm:ss' )
2020-02-18 23:55:06 -05:00
} ) ;
} )
. catch ( ( err ) => {
// Don't want to stop the train here, just log the error
logger . error ( err . message ) ;
} )
) ;
} ) ;
return Promise . all ( promises ) ;
}
} ) ;
} )
. then ( ( ) => {
internalCertificate . interval _processing = false ;
} )
. catch ( ( err ) => {
logger . error ( err ) ;
internalCertificate . interval _processing = false ;
} ) ;
}
} ,
/ * *
* @ param { Access } access
* @ param { Object } data
* @ returns { Promise }
* /
create : ( access , data ) => {
return access . can ( 'certificates:create' , data )
. then ( ( ) => {
data . owner _user _id = access . token . getUserId ( 1 ) ;
if ( data . provider === 'letsencrypt' ) {
data . nice _name = data . domain _names . sort ( ) . join ( ', ' ) ;
}
return certificateModel
. query ( )
. omit ( omissions ( ) )
. insertAndFetch ( data ) ;
} )
. then ( ( certificate ) => {
if ( certificate . provider === 'letsencrypt' ) {
// Request a new Cert from LE. Let the fun begin.
// 1. Find out any hosts that are using any of the hostnames in this cert
// 2. Disable them in nginx temporarily
// 3. Generate the LE config
// 4. Request cert
// 5. Remove LE config
// 6. Re-instate previously disabled hosts
// 1. Find out any hosts that are using any of the hostnames in this cert
return internalHost . getHostsWithDomains ( certificate . domain _names )
. then ( ( in _use _result ) => {
// 2. Disable them in nginx temporarily
return internalCertificate . disableInUseHosts ( in _use _result )
. then ( ( ) => {
return in _use _result ;
} ) ;
} )
. then ( ( in _use _result ) => {
2020-10-06 08:52:06 -04:00
// With DNS challenge no config is needed, so skip 3 and 5.
if ( certificate . meta . dns _challenge ) {
2020-08-23 09:24:20 -04:00
return internalNginx . reload ( ) . then ( ( ) => {
2020-02-18 23:55:06 -05:00
// 4. Request cert
2020-10-06 08:52:06 -04:00
return internalCertificate . requestLetsEncryptSslWithDnsChallenge ( certificate ) ;
2020-02-18 23:55:06 -05:00
} )
2020-08-23 14:56:25 -04:00
. then ( internalNginx . reload )
. then ( ( ) => {
// 6. Re-instate previously disabled hosts
return internalCertificate . enableInUseHosts ( in _use _result ) ;
} )
. then ( ( ) => {
return certificate ;
} )
. catch ( ( err ) => {
// In the event of failure, revert things and throw err back
return internalCertificate . enableInUseHosts ( in _use _result )
. then ( internalNginx . reload )
. then ( ( ) => {
throw err ;
} ) ;
} ) ;
2020-08-23 09:24:20 -04:00
} else {
// 3. Generate the LE config
return internalNginx . generateLetsEncryptRequestConfig ( certificate )
. then ( internalNginx . reload )
. then ( ( ) => {
// 4. Request cert
return internalCertificate . requestLetsEncryptSsl ( certificate ) ;
} )
. then ( ( ) => {
// 5. Remove LE config
return internalNginx . deleteLetsEncryptRequestConfig ( certificate ) ;
} )
. then ( internalNginx . reload )
. then ( ( ) => {
// 6. Re-instate previously disabled hosts
return internalCertificate . enableInUseHosts ( in _use _result ) ;
} )
. then ( ( ) => {
return certificate ;
} )
. catch ( ( err ) => {
// In the event of failure, revert things and throw err back
return internalNginx . deleteLetsEncryptRequestConfig ( certificate )
. then ( ( ) => {
return internalCertificate . enableInUseHosts ( in _use _result ) ;
} )
. then ( internalNginx . reload )
. then ( ( ) => {
throw err ;
} ) ;
} ) ;
}
2020-02-18 23:55:06 -05:00
} )
. then ( ( ) => {
// At this point, the letsencrypt cert should exist on disk.
// Lets get the expiry date from the file and update the row silently
return internalCertificate . getCertificateInfoFromFile ( '/etc/letsencrypt/live/npm-' + certificate . id + '/fullchain.pem' )
. then ( ( cert _info ) => {
return certificateModel
. query ( )
. patchAndFetchById ( certificate . id , {
2020-08-14 19:23:19 -04:00
expires _on : moment ( cert _info . dates . to , 'X' ) . format ( 'YYYY-MM-DD HH:mm:ss' )
2020-02-18 23:55:06 -05:00
} )
. then ( ( saved _row ) => {
// Add cert data for audit log
saved _row . meta = _ . assign ( { } , saved _row . meta , {
letsencrypt _certificate : cert _info
} ) ;
return saved _row ;
} ) ;
} ) ;
2020-11-06 06:29:38 -05:00
} ) . catch ( async ( error ) => {
// Delete the certificate from the database if it was not created successfully
await certificateModel
. query ( )
. deleteById ( certificate . id ) ;
throw error ;
2020-02-18 23:55:06 -05:00
} ) ;
} else {
return certificate ;
}
} ) . then ( ( certificate ) => {
data . meta = _ . assign ( { } , data . meta || { } , certificate . meta ) ;
// Add to audit log
return internalAuditLog . add ( access , {
action : 'created' ,
object _type : 'certificate' ,
object _id : certificate . id ,
meta : data
} )
. then ( ( ) => {
return certificate ;
} ) ;
} ) ;
} ,
/ * *
* @ param { Access } access
* @ param { Object } data
* @ param { Number } data . id
* @ param { String } [ data . email ]
* @ param { String } [ data . name ]
* @ return { Promise }
* /
update : ( access , data ) => {
return access . can ( 'certificates:update' , data . id )
. then ( ( /*access_data*/ ) => {
return internalCertificate . get ( access , { id : data . id } ) ;
} )
. then ( ( row ) => {
if ( row . id !== data . id ) {
// Sanity check that something crazy hasn't happened
throw new error . InternalValidationError ( 'Certificate could not be updated, IDs do not match: ' + row . id + ' !== ' + data . id ) ;
}
return certificateModel
. query ( )
. omit ( omissions ( ) )
. patchAndFetchById ( row . id , data )
. then ( ( saved _row ) => {
saved _row . meta = internalCertificate . cleanMeta ( saved _row . meta ) ;
data . meta = internalCertificate . cleanMeta ( data . meta ) ;
// Add row.nice_name for custom certs
if ( saved _row . provider === 'other' ) {
data . nice _name = saved _row . nice _name ;
}
// Add to audit log
return internalAuditLog . add ( access , {
action : 'updated' ,
object _type : 'certificate' ,
object _id : row . id ,
meta : _ . omit ( data , [ 'expires_on' ] ) // this prevents json circular reference because expires_on might be raw
} )
. then ( ( ) => {
return _ . omit ( saved _row , omissions ( ) ) ;
} ) ;
} ) ;
} ) ;
} ,
/ * *
* @ param { Access } access
* @ param { Object } data
* @ param { Number } data . id
* @ param { Array } [ data . expand ]
* @ param { Array } [ data . omit ]
* @ return { Promise }
* /
get : ( access , data ) => {
if ( typeof data === 'undefined' ) {
data = { } ;
}
return access . can ( 'certificates:get' , data . id )
. then ( ( access _data ) => {
let query = certificateModel
. query ( )
. where ( 'is_deleted' , 0 )
. andWhere ( 'id' , data . id )
. allowEager ( '[owner]' )
. first ( ) ;
if ( access _data . permission _visibility !== 'all' ) {
query . andWhere ( 'owner_user_id' , access . token . getUserId ( 1 ) ) ;
}
// Custom omissions
if ( typeof data . omit !== 'undefined' && data . omit !== null ) {
query . omit ( data . omit ) ;
}
if ( typeof data . expand !== 'undefined' && data . expand !== null ) {
query . eager ( '[' + data . expand . join ( ', ' ) + ']' ) ;
}
return query ;
} )
. then ( ( row ) => {
if ( row ) {
return _ . omit ( row , omissions ( ) ) ;
} else {
throw new error . ItemNotFoundError ( data . id ) ;
}
} ) ;
} ,
/ * *
* @ param { Access } access
* @ param { Object } data
* @ param { Number } data . id
* @ param { String } [ data . reason ]
* @ returns { Promise }
* /
delete : ( access , data ) => {
return access . can ( 'certificates:delete' , data . id )
. then ( ( ) => {
return internalCertificate . get ( access , { id : data . id } ) ;
} )
. then ( ( row ) => {
if ( ! row ) {
throw new error . ItemNotFoundError ( data . id ) ;
}
return certificateModel
. query ( )
. where ( 'id' , row . id )
. patch ( {
is _deleted : 1
} )
. then ( ( ) => {
// Add to audit log
row . meta = internalCertificate . cleanMeta ( row . meta ) ;
return internalAuditLog . add ( access , {
action : 'deleted' ,
object _type : 'certificate' ,
object _id : row . id ,
meta : _ . omit ( row , omissions ( ) )
} ) ;
} )
. then ( ( ) => {
if ( row . provider === 'letsencrypt' ) {
// Revoke the cert
return internalCertificate . revokeLetsEncryptSsl ( row ) ;
}
} ) ;
} )
. then ( ( ) => {
return true ;
} ) ;
} ,
/ * *
* All Certs
*
* @ param { Access } access
* @ param { Array } [ expand ]
* @ param { String } [ search _query ]
* @ returns { Promise }
* /
getAll : ( access , expand , search _query ) => {
return access . can ( 'certificates:list' )
. then ( ( access _data ) => {
let query = certificateModel
. query ( )
. where ( 'is_deleted' , 0 )
. groupBy ( 'id' )
. omit ( [ 'is_deleted' ] )
. allowEager ( '[owner]' )
. orderBy ( 'nice_name' , 'ASC' ) ;
if ( access _data . permission _visibility !== 'all' ) {
query . andWhere ( 'owner_user_id' , access . token . getUserId ( 1 ) ) ;
}
// Query is used for searching
if ( typeof search _query === 'string' ) {
query . where ( function ( ) {
this . where ( 'name' , 'like' , '%' + search _query + '%' ) ;
} ) ;
}
if ( typeof expand !== 'undefined' && expand !== null ) {
query . eager ( '[' + expand . join ( ', ' ) + ']' ) ;
}
return query ;
} ) ;
} ,
/ * *
* Report use
*
* @ param { Number } user _id
* @ param { String } visibility
* @ returns { Promise }
* /
getCount : ( user _id , visibility ) => {
let query = certificateModel
. query ( )
. count ( 'id as count' )
. where ( 'is_deleted' , 0 ) ;
if ( visibility !== 'all' ) {
query . andWhere ( 'owner_user_id' , user _id ) ;
}
return query . first ( )
. then ( ( row ) => {
return parseInt ( row . count , 10 ) ;
} ) ;
} ,
/ * *
* @ param { Object } certificate
* @ returns { Promise }
* /
writeCustomCert : ( certificate ) => {
if ( debug _mode ) {
logger . info ( 'Writing Custom Certificate:' , certificate ) ;
}
let dir = '/data/custom_ssl/npm-' + certificate . id ;
return new Promise ( ( resolve , reject ) => {
if ( certificate . provider === 'letsencrypt' ) {
reject ( new Error ( 'Refusing to write letsencrypt certs here' ) ) ;
return ;
}
let cert _data = certificate . meta . certificate ;
if ( typeof certificate . meta . intermediate _certificate !== 'undefined' ) {
cert _data = cert _data + '\n' + certificate . meta . intermediate _certificate ;
}
try {
if ( ! fs . existsSync ( dir ) ) {
fs . mkdirSync ( dir ) ;
}
} catch ( err ) {
reject ( err ) ;
return ;
}
fs . writeFile ( dir + '/fullchain.pem' , cert _data , function ( err ) {
if ( err ) {
reject ( err ) ;
} else {
resolve ( ) ;
}
} ) ;
} )
. then ( ( ) => {
return new Promise ( ( resolve , reject ) => {
fs . writeFile ( dir + '/privkey.pem' , certificate . meta . certificate _key , function ( err ) {
if ( err ) {
reject ( err ) ;
} else {
resolve ( ) ;
}
} ) ;
} ) ;
} ) ;
} ,
/ * *
* @ param { Access } access
* @ param { Object } data
* @ param { Array } data . domain _names
* @ param { String } data . meta . letsencrypt _email
* @ param { Boolean } data . meta . letsencrypt _agree
* @ returns { Promise }
* /
createQuickCertificate : ( access , data ) => {
return internalCertificate . create ( access , {
provider : 'letsencrypt' ,
domain _names : data . domain _names ,
meta : data . meta
} ) ;
} ,
/ * *
* Validates that the certs provided are good .
* No access required here , nothing is changed or stored .
*
* @ param { Object } data
* @ param { Object } data . files
* @ returns { Promise }
* /
validate : ( data ) => {
return new Promise ( ( resolve ) => {
// Put file contents into an object
let files = { } ;
_ . map ( data . files , ( file , name ) => {
if ( internalCertificate . allowed _ssl _files . indexOf ( name ) !== - 1 ) {
files [ name ] = file . data . toString ( ) ;
}
} ) ;
resolve ( files ) ;
} )
. then ( ( files ) => {
// For each file, create a temp file and write the contents to it
// Then test it depending on the file type
let promises = [ ] ;
_ . map ( files , ( content , type ) => {
promises . push ( new Promise ( ( resolve ) => {
if ( type === 'certificate_key' ) {
resolve ( internalCertificate . checkPrivateKey ( content ) ) ;
} else {
// this should handle `certificate` and intermediate certificate
resolve ( internalCertificate . getCertificateInfo ( content , true ) ) ;
}
} ) . then ( ( res ) => {
return { [ type ] : res } ;
} ) ) ;
} ) ;
return Promise . all ( promises )
. then ( ( files ) => {
let data = { } ;
_ . each ( files , ( file ) => {
data = _ . assign ( { } , data , file ) ;
} ) ;
return data ;
} ) ;
} ) ;
} ,
/ * *
* @ param { Access } access
* @ param { Object } data
* @ param { Number } data . id
* @ param { Object } data . files
* @ returns { Promise }
* /
upload : ( access , data ) => {
return internalCertificate . get ( access , { id : data . id } )
. then ( ( row ) => {
if ( row . provider !== 'other' ) {
throw new error . ValidationError ( 'Cannot upload certificates for this type of provider' ) ;
}
return internalCertificate . validate ( data )
. then ( ( validations ) => {
if ( typeof validations . certificate === 'undefined' ) {
throw new error . ValidationError ( 'Certificate file was not provided' ) ;
}
_ . map ( data . files , ( file , name ) => {
if ( internalCertificate . allowed _ssl _files . indexOf ( name ) !== - 1 ) {
row . meta [ name ] = file . data . toString ( ) ;
}
} ) ;
// TODO: This uses a mysql only raw function that won't translate to postgres
return internalCertificate . update ( access , {
id : data . id ,
2020-08-14 19:23:19 -04:00
expires _on : moment ( validations . certificate . dates . to , 'X' ) . format ( 'YYYY-MM-DD HH:mm:ss' ) ,
2020-02-18 23:55:06 -05:00
domain _names : [ validations . certificate . cn ] ,
meta : _ . clone ( row . meta ) // Prevent the update method from changing this value that we'll use later
} )
. then ( ( certificate ) => {
console . log ( 'ROWMETA:' , row . meta ) ;
certificate . meta = row . meta ;
return internalCertificate . writeCustomCert ( certificate ) ;
} ) ;
} )
. then ( ( ) => {
return _ . pick ( row . meta , internalCertificate . allowed _ssl _files ) ;
} ) ;
} ) ;
} ,
/ * *
* Uses the openssl command to validate the private key .
* It will save the file to disk first , then run commands on it , then delete the file .
*
* @ param { String } private _key This is the entire key contents as a string
* /
checkPrivateKey : ( private _key ) => {
return tempWrite ( private _key , '/tmp' )
. then ( ( filepath ) => {
2020-12-14 06:08:39 -05:00
return new Promise ( ( resolve , reject ) => {
const failTimeout = setTimeout ( ( ) => {
reject ( new error . ValidationError ( 'Result Validation Error: Validation timed out. This could be due to the key being passphrase-protected.' ) ) ;
} , 10000 ) ;
utils
. exec ( 'openssl pkey -in ' + filepath + ' -check -noout 2>&1 ' )
. then ( ( result ) => {
clearTimeout ( failTimeout ) ;
if ( ! result . toLowerCase ( ) . includes ( 'key is valid' ) ) {
reject ( new error . ValidationError ( 'Result Validation Error: ' + result ) ) ;
}
fs . unlinkSync ( filepath ) ;
resolve ( true ) ;
} )
. catch ( ( err ) => {
clearTimeout ( failTimeout ) ;
fs . unlinkSync ( filepath ) ;
reject ( new error . ValidationError ( 'Certificate Key is not valid (' + err . message + ')' , err ) ) ;
} ) ;
} ) ;
2020-02-18 23:55:06 -05:00
} ) ;
} ,
/ * *
* Uses the openssl command to both validate and get info out of the certificate .
* It will save the file to disk first , then run commands on it , then delete the file .
*
* @ param { String } certificate This is the entire cert contents as a string
* @ param { Boolean } [ throw _expired ] Throw when the certificate is out of date
* /
getCertificateInfo : ( certificate , throw _expired ) => {
return tempWrite ( certificate , '/tmp' )
. then ( ( filepath ) => {
return internalCertificate . getCertificateInfoFromFile ( filepath , throw _expired )
. then ( ( cert _data ) => {
fs . unlinkSync ( filepath ) ;
return cert _data ;
} ) . catch ( ( err ) => {
fs . unlinkSync ( filepath ) ;
throw err ;
} ) ;
} ) ;
} ,
/ * *
* Uses the openssl command to both validate and get info out of the certificate .
* It will save the file to disk first , then run commands on it , then delete the file .
*
* @ param { String } certificate _file The file location on disk
* @ param { Boolean } [ throw _expired ] Throw when the certificate is out of date
* /
getCertificateInfoFromFile : ( certificate _file , throw _expired ) => {
let cert _data = { } ;
return utils . exec ( 'openssl x509 -in ' + certificate _file + ' -subject -noout' )
. then ( ( result ) => {
// subject=CN = something.example.com
let regex = /(?:subject=)?[^=]+=\s+(\S+)/gim ;
let match = regex . exec ( result ) ;
if ( typeof match [ 1 ] === 'undefined' ) {
throw new error . ValidationError ( 'Could not determine subject from certificate: ' + result ) ;
}
cert _data [ 'cn' ] = match [ 1 ] ;
} )
. then ( ( ) => {
return utils . exec ( 'openssl x509 -in ' + certificate _file + ' -issuer -noout' ) ;
} )
. then ( ( result ) => {
// issuer=C = US, O = Let's Encrypt, CN = Let's Encrypt Authority X3
let regex = /^(?:issuer=)?(.*)$/gim ;
let match = regex . exec ( result ) ;
if ( typeof match [ 1 ] === 'undefined' ) {
throw new error . ValidationError ( 'Could not determine issuer from certificate: ' + result ) ;
}
cert _data [ 'issuer' ] = match [ 1 ] ;
} )
. then ( ( ) => {
return utils . exec ( 'openssl x509 -in ' + certificate _file + ' -dates -noout' ) ;
} )
. then ( ( result ) => {
// notBefore=Jul 14 04:04:29 2018 GMT
// notAfter=Oct 12 04:04:29 2018 GMT
let valid _from = null ;
let valid _to = null ;
let lines = result . split ( '\n' ) ;
lines . map ( function ( str ) {
let regex = /^(\S+)=(.*)$/gim ;
let match = regex . exec ( str . trim ( ) ) ;
if ( match && typeof match [ 2 ] !== 'undefined' ) {
let date = parseInt ( moment ( match [ 2 ] , 'MMM DD HH:mm:ss YYYY z' ) . format ( 'X' ) , 10 ) ;
if ( match [ 1 ] . toLowerCase ( ) === 'notbefore' ) {
valid _from = date ;
} else if ( match [ 1 ] . toLowerCase ( ) === 'notafter' ) {
valid _to = date ;
}
}
} ) ;
if ( ! valid _from || ! valid _to ) {
throw new error . ValidationError ( 'Could not determine dates from certificate: ' + result ) ;
}
if ( throw _expired && valid _to < parseInt ( moment ( ) . format ( 'X' ) , 10 ) ) {
throw new error . ValidationError ( 'Certificate has expired' ) ;
}
cert _data [ 'dates' ] = {
from : valid _from ,
to : valid _to
} ;
return cert _data ;
} ) . catch ( ( err ) => {
throw new error . ValidationError ( 'Certificate is not valid (' + err . message + ')' , err ) ;
} ) ;
} ,
/ * *
* Cleans the ssl keys from the meta object and sets them to "true"
*
* @ param { Object } meta
* @ param { Boolean } [ remove ]
* @ returns { Object }
* /
cleanMeta : function ( meta , remove ) {
internalCertificate . allowed _ssl _files . map ( ( key ) => {
if ( typeof meta [ key ] !== 'undefined' && meta [ key ] ) {
if ( remove ) {
delete meta [ key ] ;
} else {
meta [ key ] = true ;
}
}
} ) ;
return meta ;
} ,
/ * *
* @ param { Object } certificate the certificate row
* @ returns { Promise }
* /
requestLetsEncryptSsl : ( certificate ) => {
logger . info ( 'Requesting Let\'sEncrypt certificates for Cert #' + certificate . id + ': ' + certificate . domain _names . join ( ', ' ) ) ;
let cmd = certbot _command + ' certonly --non-interactive ' +
'--config "' + le _config + '" ' +
'--cert-name "npm-' + certificate . id + '" ' +
'--agree-tos ' +
'--email "' + certificate . meta . letsencrypt _email + '" ' +
'--preferred-challenges "dns,http" ' +
'--domains "' + certificate . domain _names . join ( ',' ) + '" ' +
( le _staging ? '--staging' : '' ) ;
if ( debug _mode ) {
logger . info ( 'Command:' , cmd ) ;
}
return utils . exec ( cmd )
. then ( ( result ) => {
logger . success ( result ) ;
return result ;
} ) ;
} ,
2020-08-23 08:50:41 -04:00
/ * *
2020-10-06 08:52:06 -04:00
* @ param { Object } certificate the certificate row
* @ param { String } dns _provider the dns provider name ( key used in ` certbot-dns-plugins.js ` )
* @ param { String | null } credentials the content of this providers credentials file
* @ param { String } propagation _seconds the cloudflare api token
2020-08-23 08:50:41 -04:00
* @ returns { Promise }
* /
2020-10-06 08:52:06 -04:00
requestLetsEncryptSslWithDnsChallenge : ( certificate ) => {
const dns _plugin = dns _plugins [ certificate . meta . dns _provider ] ;
2020-10-08 08:23:21 -04:00
if ( ! dns _plugin ) {
throw Error ( ` Unknown DNS provider ' ${ certificate . meta . dns _provider } ' ` ) ;
2020-10-06 08:52:06 -04:00
}
logger . info ( ` Requesting Let'sEncrypt certificates via ${ dns _plugin . display _name } for Cert # ${ certificate . id } : ${ certificate . domain _names . join ( ', ' ) } ` ) ;
2020-08-23 08:50:41 -04:00
2020-10-17 06:13:08 -04:00
const credentials _loc = '/etc/letsencrypt/credentials/credentials-' + certificate . id ;
2020-11-04 13:31:40 -05:00
const credentials _cmd = 'mkdir -p /etc/letsencrypt/credentials 2> /dev/null; echo \'' + certificate . meta . dns _provider _credentials . replace ( '\'' , '\\\'' ) + '\' > \'' + credentials _loc + '\' && chmod 600 \'' + credentials _loc + '\'' ;
2020-11-03 15:59:18 -05:00
const prepare _cmd = 'pip3 install ' + dns _plugin . package _name + '==' + dns _plugin . package _version + ' ' + dns _plugin . dependencies ;
2020-08-23 08:50:41 -04:00
2020-10-14 03:20:52 -04:00
// Whether the plugin has a --<name>-credentials argument
2021-04-26 14:25:24 -04:00
const has _config _arg = certificate . meta . dns _provider !== 'route53' && certificate . meta . dns _provider !== 'duckdns' ;
2020-10-14 03:20:52 -04:00
let main _cmd =
2020-08-23 09:24:20 -04:00
certbot _command + ' certonly --non-interactive ' +
2020-08-23 08:50:41 -04:00
'--cert-name "npm-' + certificate . id + '" ' +
'--agree-tos ' +
'--email "' + certificate . meta . letsencrypt _email + '" ' +
'--domains "' + certificate . domain _names . join ( ',' ) + '" ' +
2020-10-06 08:52:06 -04:00
'--authenticator ' + dns _plugin . full _plugin _name + ' ' +
2020-10-14 03:20:52 -04:00
(
has _config _arg
? '--' + dns _plugin . full _plugin _name + '-credentials "' + credentials _loc + '"'
: ''
) +
2020-10-06 08:52:06 -04:00
(
certificate . meta . propagation _seconds !== undefined
2020-10-08 08:23:21 -04:00
? ' --' + dns _plugin . full _plugin _name + '-propagation-seconds ' + certificate . meta . propagation _seconds
: ''
2020-10-06 08:52:06 -04:00
) +
( le _staging ? ' --staging' : '' ) ;
2020-10-14 03:20:52 -04:00
2020-10-14 03:55:45 -04:00
// Prepend the path to the credentials file as an environment variable
if ( certificate . meta . dns _provider === 'route53' ) {
main _cmd = 'AWS_CONFIG_FILE=\'' + credentials _loc + '\' ' + main _cmd ;
}
2020-08-23 08:50:41 -04:00
2021-04-26 14:25:24 -04:00
if ( certificate . meta . dns _provider === 'duckdns' ) {
main _cmd = main _cmd + ' --' + dns _plugin . full _plugin _name + '-token ' + certificate . meta . dns _provider _credentials ;
}
2020-08-23 08:50:41 -04:00
if ( debug _mode ) {
2020-10-17 06:13:08 -04:00
logger . info ( 'Command:' , ` ${ credentials _cmd } && ${ prepare _cmd } && ${ main _cmd } ` ) ;
2020-08-23 08:50:41 -04:00
}
2020-10-06 08:52:06 -04:00
return utils . exec ( credentials _cmd )
. then ( ( ) => {
return utils . exec ( prepare _cmd )
. then ( ( ) => {
return utils . exec ( main _cmd )
. then ( async ( result ) => {
logger . info ( result ) ;
return result ;
} ) ;
} ) ;
2020-10-17 06:13:08 -04:00
} ) . catch ( async ( err ) => {
// Don't fail if file does not exist
const delete _credentials _cmd = ` rm -f ' ${ credentials _loc } ' || true ` ;
await utils . exec ( delete _credentials _cmd ) ;
throw err ;
2020-10-06 08:52:06 -04:00
} ) ;
2020-08-23 08:50:41 -04:00
} ,
2020-02-18 23:55:06 -05:00
/ * *
* @ param { Access } access
* @ param { Object } data
* @ param { Number } data . id
* @ returns { Promise }
* /
renew : ( access , data ) => {
return access . can ( 'certificates:update' , data )
. then ( ( ) => {
return internalCertificate . get ( access , data ) ;
} )
. then ( ( certificate ) => {
if ( certificate . provider === 'letsencrypt' ) {
2020-10-06 08:52:06 -04:00
let renewMethod = certificate . meta . dns _challenge ? internalCertificate . renewLetsEncryptSslWithDnsChallenge : internalCertificate . renewLetsEncryptSsl ;
2020-08-23 14:29:16 -04:00
return renewMethod ( certificate )
2020-02-18 23:55:06 -05:00
. then ( ( ) => {
return internalCertificate . getCertificateInfoFromFile ( '/etc/letsencrypt/live/npm-' + certificate . id + '/fullchain.pem' ) ;
} )
. then ( ( cert _info ) => {
return certificateModel
. query ( )
. patchAndFetchById ( certificate . id , {
2020-08-14 19:23:19 -04:00
expires _on : moment ( cert _info . dates . to , 'X' ) . format ( 'YYYY-MM-DD HH:mm:ss' )
2020-02-18 23:55:06 -05:00
} ) ;
} )
. then ( ( updated _certificate ) => {
// Add to audit log
return internalAuditLog . add ( access , {
action : 'renewed' ,
object _type : 'certificate' ,
object _id : updated _certificate . id ,
meta : updated _certificate
} )
. then ( ( ) => {
return updated _certificate ;
} ) ;
} ) ;
} else {
throw new error . ValidationError ( 'Only Let\'sEncrypt certificates can be renewed' ) ;
}
} ) ;
} ,
/ * *
* @ param { Object } certificate the certificate row
* @ returns { Promise }
* /
renewLetsEncryptSsl : ( certificate ) => {
logger . info ( 'Renewing Let\'sEncrypt certificates for Cert #' + certificate . id + ': ' + certificate . domain _names . join ( ', ' ) ) ;
let cmd = certbot _command + ' renew --non-interactive ' +
'--config "' + le _config + '" ' +
'--cert-name "npm-' + certificate . id + '" ' +
'--preferred-challenges "dns,http" ' +
'--disable-hook-validation ' +
( le _staging ? '--staging' : '' ) ;
if ( debug _mode ) {
logger . info ( 'Command:' , cmd ) ;
}
return utils . exec ( cmd )
. then ( ( result ) => {
logger . info ( result ) ;
return result ;
} ) ;
} ,
2020-08-23 14:29:16 -04:00
/ * *
* @ param { Object } certificate the certificate row
* @ returns { Promise }
* /
2020-10-06 08:52:06 -04:00
renewLetsEncryptSslWithDnsChallenge : ( certificate ) => {
const dns _plugin = dns _plugins [ certificate . meta . dns _provider ] ;
2020-08-23 14:29:16 -04:00
2020-10-08 08:23:21 -04:00
if ( ! dns _plugin ) {
throw Error ( ` Unknown DNS provider ' ${ certificate . meta . dns _provider } ' ` ) ;
2020-10-06 08:52:06 -04:00
}
logger . info ( ` Renewing Let'sEncrypt certificates via ${ dns _plugin . display _name } for Cert # ${ certificate . id } : ${ certificate . domain _names . join ( ', ' ) } ` ) ;
2020-10-14 03:20:52 -04:00
let main _cmd =
2020-10-06 08:52:06 -04:00
certbot _command + ' renew --non-interactive ' +
2020-08-23 14:29:16 -04:00
'--cert-name "npm-' + certificate . id + '" ' +
2020-10-06 08:52:06 -04:00
'--disable-hook-validation' +
( le _staging ? ' --staging' : '' ) ;
2020-10-14 03:20:52 -04:00
// Prepend the path to the credentials file as an environment variable
if ( certificate . meta . dns _provider === 'route53' ) {
2020-10-17 06:25:36 -04:00
const credentials _loc = '/etc/letsencrypt/credentials/credentials-' + certificate . id ;
main _cmd = 'AWS_CONFIG_FILE=\'' + credentials _loc + '\' ' + main _cmd ;
2020-10-14 03:20:52 -04:00
}
2020-08-23 14:29:16 -04:00
if ( debug _mode ) {
2020-10-17 06:13:08 -04:00
logger . info ( 'Command:' , main _cmd ) ;
2020-08-23 14:29:16 -04:00
}
2020-10-17 06:13:08 -04:00
return utils . exec ( main _cmd )
. then ( async ( result ) => {
logger . info ( result ) ;
return result ;
2020-08-23 14:29:16 -04:00
} ) ;
} ,
2020-02-18 23:55:06 -05:00
/ * *
* @ param { Object } certificate the certificate row
* @ param { Boolean } [ throw _errors ]
* @ returns { Promise }
* /
revokeLetsEncryptSsl : ( certificate , throw _errors ) => {
logger . info ( 'Revoking Let\'sEncrypt certificates for Cert #' + certificate . id + ': ' + certificate . domain _names . join ( ', ' ) ) ;
2020-10-17 06:13:08 -04:00
const main _cmd = certbot _command + ' revoke --non-interactive ' +
2020-02-18 23:55:06 -05:00
'--cert-path "/etc/letsencrypt/live/npm-' + certificate . id + '/fullchain.pem" ' +
'--delete-after-revoke ' +
( le _staging ? '--staging' : '' ) ;
2020-10-17 06:13:08 -04:00
// Don't fail command if file does not exist
const delete _credentials _cmd = ` rm -f '/etc/letsencrypt/credentials/credentials- ${ certificate . id } ' || true ` ;
2020-02-18 23:55:06 -05:00
if ( debug _mode ) {
2020-10-17 06:13:08 -04:00
logger . info ( 'Command:' , main _cmd + '; ' + delete _credentials _cmd ) ;
2020-02-18 23:55:06 -05:00
}
2020-10-17 06:13:08 -04:00
return utils . exec ( main _cmd )
. then ( async ( result ) => {
await utils . exec ( delete _credentials _cmd ) ;
2020-02-18 23:55:06 -05:00
logger . info ( result ) ;
return result ;
} )
. catch ( ( err ) => {
if ( debug _mode ) {
logger . error ( err . message ) ;
}
if ( throw _errors ) {
throw err ;
}
} ) ;
} ,
/ * *
* @ param { Object } certificate
* @ returns { Boolean }
* /
hasLetsEncryptSslCerts : ( certificate ) => {
let le _path = '/etc/letsencrypt/live/npm-' + certificate . id ;
return fs . existsSync ( le _path + '/fullchain.pem' ) && fs . existsSync ( le _path + '/privkey.pem' ) ;
} ,
/ * *
* @ param { Object } in _use _result
* @ param { Number } in _use _result . total _count
* @ param { Array } in _use _result . proxy _hosts
* @ param { Array } in _use _result . redirection _hosts
* @ param { Array } in _use _result . dead _hosts
* /
disableInUseHosts : ( in _use _result ) => {
if ( in _use _result . total _count ) {
let promises = [ ] ;
if ( in _use _result . proxy _hosts . length ) {
promises . push ( internalNginx . bulkDeleteConfigs ( 'proxy_host' , in _use _result . proxy _hosts ) ) ;
}
if ( in _use _result . redirection _hosts . length ) {
promises . push ( internalNginx . bulkDeleteConfigs ( 'redirection_host' , in _use _result . redirection _hosts ) ) ;
}
if ( in _use _result . dead _hosts . length ) {
promises . push ( internalNginx . bulkDeleteConfigs ( 'dead_host' , in _use _result . dead _hosts ) ) ;
}
return Promise . all ( promises ) ;
} else {
return Promise . resolve ( ) ;
}
} ,
/ * *
* @ param { Object } in _use _result
* @ param { Number } in _use _result . total _count
* @ param { Array } in _use _result . proxy _hosts
* @ param { Array } in _use _result . redirection _hosts
* @ param { Array } in _use _result . dead _hosts
* /
enableInUseHosts : ( in _use _result ) => {
if ( in _use _result . total _count ) {
let promises = [ ] ;
if ( in _use _result . proxy _hosts . length ) {
promises . push ( internalNginx . bulkGenerateConfigs ( 'proxy_host' , in _use _result . proxy _hosts ) ) ;
}
if ( in _use _result . redirection _hosts . length ) {
promises . push ( internalNginx . bulkGenerateConfigs ( 'redirection_host' , in _use _result . redirection _hosts ) ) ;
}
if ( in _use _result . dead _hosts . length ) {
promises . push ( internalNginx . bulkGenerateConfigs ( 'dead_host' , in _use _result . dead _hosts ) ) ;
}
return Promise . all ( promises ) ;
} else {
return Promise . resolve ( ) ;
}
}
} ;
module . exports = internalCertificate ;