2018-01-12 14:41:26 -05:00
/ * *
2018-01-15 00:01:06 -05:00
* @ description MeshCentral letsEncrypt module , uses GreenLock to do all the work .
2018-01-12 14:41:26 -05:00
* @ author Ylian Saint - Hilaire
2020-01-02 21:30:12 -05:00
* @ copyright Intel Corporation 2018 - 2020
2018-01-12 14:41:26 -05:00
* @ license Apache - 2.0
2018-01-15 00:01:06 -05:00
* @ version v0 . 0.2
2018-01-12 14:41:26 -05:00
* /
2018-08-29 20:40:30 -04:00
/*xjslint node: true */
/*xjslint plusplus: true */
/*xjslint maxlen: 256 */
/*jshint node: true */
/*jshint strict: false */
/*jshint esversion: 6 */
2019-11-14 01:47:17 -05:00
'use strict' ;
2018-08-27 15:24:15 -04:00
2020-03-05 04:39:40 -05:00
// GreenLock Implementation
2020-01-25 15:14:14 -05:00
var globalLetsEncrypt = null ;
2019-11-15 20:55:05 -05:00
module . exports . CreateLetsEncrypt = function ( parent ) {
2018-01-15 00:01:06 -05:00
try {
2019-11-18 14:42:09 -05:00
// Get the GreenLock version
var greenLockVersion = null ;
try { greenLockVersion = require ( 'greenlock/package.json' ) . version ; } catch ( ex ) { }
if ( greenLockVersion == null ) {
parent . debug ( 'cert' , "Initializing Let's Encrypt support" ) ;
} else {
parent . debug ( 'cert' , "Initializing Let's Encrypt support, using GreenLock v" + greenLockVersion ) ;
}
2019-11-14 01:47:17 -05:00
2019-11-19 14:18:08 -05:00
// Check the current node version and support for generateKeyPair
if ( require ( 'crypto' ) . generateKeyPair == null ) { return null ; }
if ( Number ( process . version . match ( /^v(\d+\.\d+)/ ) [ 1 ] ) < 10 ) { return null ; }
2019-11-14 01:47:17 -05:00
2019-01-11 17:01:36 -05:00
// Try to delete the "./ursa-optional" or "./node_modules/ursa-optional" folder if present.
// This is an optional module that GreenLock uses that causes issues.
try {
const fs = require ( 'fs' ) ;
2019-11-14 01:47:17 -05:00
if ( fs . existsSync ( parent . path . join ( _ _dirname , 'ursa-optional' ) ) ) { fs . unlinkSync ( obj . path . join ( _ _dirname , 'ursa-optional' ) ) ; }
if ( fs . existsSync ( parent . path . join ( _ _dirname , 'node_modules' , 'ursa-optional' ) ) ) { fs . unlinkSync ( obj . path . join ( _ _dirname , 'node_modules' , 'ursa-optional' ) ) ; }
2019-01-11 17:01:36 -05:00
} catch ( ex ) { }
2018-01-15 00:01:06 -05:00
2019-01-11 17:01:36 -05:00
// Get GreenLock setup and running.
const greenlock = require ( 'greenlock' ) ;
2018-01-15 00:01:06 -05:00
var obj = { } ;
2020-01-25 15:14:14 -05:00
globalLetsEncrypt = obj ;
2018-01-15 00:01:06 -05:00
obj . parent = parent ;
2020-03-05 04:39:40 -05:00
obj . lib = 'greenlock' ;
2019-11-14 01:47:17 -05:00
obj . path = require ( 'path' ) ;
2018-01-15 00:01:06 -05:00
obj . redirWebServerHooked = false ;
obj . leDomains = null ;
obj . leResults = null ;
2019-11-16 14:21:32 -05:00
obj . leResultsStaging = null ;
obj . performRestart = false ; // Indicates we need to restart the server
obj . performMoveToProduction = false ; // Indicates we just got a staging certificate and need to move to production
obj . runAsProduction = false ; // This starts at false and moves to true if staging cert is ok.
2018-01-15 00:01:06 -05:00
// Setup the certificate storage paths
2019-11-14 01:59:33 -05:00
obj . configPath = obj . path . join ( obj . parent . datapath , 'letsencrypt3' ) ;
2018-01-15 00:01:06 -05:00
try { obj . parent . fs . mkdirSync ( obj . configPath ) ; } catch ( e ) { }
2019-11-16 14:21:32 -05:00
obj . configPathStaging = obj . path . join ( obj . parent . datapath , 'letsencrypt3-staging' ) ;
try { obj . parent . fs . mkdirSync ( obj . configPathStaging ) ; } catch ( e ) { }
2018-01-15 00:01:06 -05:00
2019-11-14 01:47:17 -05:00
// Setup Let's Encrypt default configuration
2019-11-16 14:21:32 -05:00
obj . leDefaults = { agreeToTerms : true , store : { module : 'greenlock-store-fs' , basePath : obj . configPath } } ;
obj . leDefaultsStaging = { agreeToTerms : true , store : { module : 'greenlock-store-fs' , basePath : obj . configPathStaging } } ;
2018-01-15 00:01:06 -05:00
2019-11-14 01:47:17 -05:00
// Get package and maintainer email
const pkg = require ( './package.json' ) ;
var maintainerEmail = null ;
if ( typeof pkg . author == 'string' ) {
// Older NodeJS
maintainerEmail = pkg . author ;
var i = maintainerEmail . indexOf ( '<' ) ;
if ( i >= 0 ) { maintainerEmail = maintainerEmail . substring ( i + 1 ) ; }
var i = maintainerEmail . indexOf ( '>' ) ;
if ( i >= 0 ) { maintainerEmail = maintainerEmail . substring ( 0 , i ) ; }
} else if ( typeof pkg . author == 'object' ) {
// Latest NodeJS
maintainerEmail = pkg . author . email ;
}
2019-11-15 20:55:05 -05:00
2019-11-18 17:17:27 -05:00
// Check if we need to be in debug mode
var ledebug = false ;
try { ledebug = ( ( obj . parent . args . debug != null ) || ( obj . parent . args . debug . indexOf ( 'cert' ) ) ) ; } catch ( ex ) { }
2019-11-16 14:21:32 -05:00
// Create the main GreenLock code module for production.
2018-01-15 00:01:06 -05:00
var greenlockargs = {
2019-11-14 01:47:17 -05:00
parent : obj ,
packageRoot : _ _dirname ,
packageAgent : pkg . name + '/' + pkg . version ,
manager : obj . path . join ( _ _dirname , 'letsencrypt.js' ) ,
maintainerEmail : maintainerEmail ,
notify : function ( ev , args ) { if ( typeof args == 'string' ) { parent . debug ( 'cert' , ev + ': ' + args ) ; } else { parent . debug ( 'cert' , ev + ': ' + JSON . stringify ( args ) ) ; } } ,
2019-11-16 14:21:32 -05:00
staging : false ,
2019-11-18 17:17:27 -05:00
debug : ledebug
2018-08-29 20:40:30 -04:00
} ;
if ( obj . parent . args . debug == null ) { greenlockargs . log = function ( debug ) { } ; } // If not in debug mode, ignore all console output from greenlock (makes things clean).
2018-01-15 00:01:06 -05:00
obj . le = greenlock . create ( greenlockargs ) ;
2018-01-12 14:41:26 -05:00
2019-11-16 14:21:32 -05:00
// Create the main GreenLock code module for staging.
var greenlockargsstaging = {
parent : obj ,
packageRoot : _ _dirname ,
packageAgent : pkg . name + '/' + pkg . version ,
manager : obj . path . join ( _ _dirname , 'letsencrypt.js' ) ,
maintainerEmail : maintainerEmail ,
2019-11-18 17:17:27 -05:00
notify : function ( ev , args ) { if ( typeof args == 'string' ) { parent . debug ( 'cert' , 'Notify: ' + ev + ': ' + args ) ; } else { parent . debug ( 'cert' , 'Notify: ' + ev + ': ' + JSON . stringify ( args ) ) ; } } ,
2019-11-16 14:21:32 -05:00
staging : true ,
2019-11-18 17:17:27 -05:00
debug : ledebug
2019-11-16 14:21:32 -05:00
} ;
if ( obj . parent . args . debug == null ) { greenlockargsstaging . log = function ( debug ) { } ; } // If not in debug mode, ignore all console output from greenlock (makes things clean).
obj . leStaging = greenlock . create ( greenlockargsstaging ) ;
2018-01-15 00:01:06 -05:00
// Hook up GreenLock to the redirection server
2019-11-17 16:35:50 -05:00
if ( obj . parent . config . settings . rediraliasport === 80 ) { obj . redirWebServerHooked = true ; }
else if ( ( obj . parent . config . settings . rediraliasport == null ) && ( obj . parent . redirserver . port == 80 ) ) { obj . redirWebServerHooked = true ; }
2019-11-14 01:47:17 -05:00
// Respond to a challenge
obj . challenge = function ( token , hostname , func ) {
2019-11-16 14:21:32 -05:00
if ( obj . runAsProduction === true ) {
// Production
parent . debug ( 'cert' , "Challenge " + hostname + "/" + token ) ;
obj . le . challenges . get ( { type : 'http-01' , servername : hostname , token : token } )
. then ( function ( results ) { func ( results . keyAuthorization ) ; } )
. catch ( function ( e ) { console . log ( 'LE-ERROR' , e ) ; func ( null ) ; } ) ; // unexpected error, not related to renewal
} else {
// Staging
parent . debug ( 'cert' , "Challenge " + hostname + "/" + token ) ;
obj . leStaging . challenges . get ( { type : 'http-01' , servername : hostname , token : token } )
. then ( function ( results ) { func ( results . keyAuthorization ) ; } )
. catch ( function ( e ) { console . log ( 'LE-ERROR' , e ) ; func ( null ) ; } ) ; // unexpected error, not related to renewal
}
2019-11-14 01:47:17 -05:00
}
2018-01-12 14:41:26 -05:00
2019-11-16 14:21:32 -05:00
obj . getCertificate = function ( certs , func ) {
2019-11-14 01:47:17 -05:00
parent . debug ( 'cert' , "Getting certs from local store" ) ;
2019-03-05 02:48:45 -05:00
if ( certs . CommonName . indexOf ( '.' ) == - 1 ) { console . log ( "ERROR: Use --cert to setup the default server name before using Let's Encrypt." ) ; func ( certs ) ; return ; }
2018-01-15 00:01:06 -05:00
if ( obj . parent . config . letsencrypt == null ) { func ( certs ) ; return ; }
if ( obj . parent . config . letsencrypt . email == null ) { console . log ( "ERROR: Let's Encrypt email address not specified." ) ; func ( certs ) ; return ; }
2019-11-17 16:35:50 -05:00
if ( ( obj . parent . redirserver == null ) || ( ( typeof obj . parent . config . settings . rediraliasport === 'number' ) && ( obj . parent . config . settings . rediraliasport !== 80 ) ) || ( ( obj . parent . config . settings . rediraliasport == null ) && ( obj . parent . redirserver . port !== 80 ) ) ) { console . log ( "ERROR: Redirection web server must be active on port 80 for Let's Encrypt to work." ) ; func ( certs ) ; return ; }
2018-01-15 00:01:06 -05:00
if ( obj . redirWebServerHooked !== true ) { console . log ( "ERROR: Redirection web server not setup for Let's Encrypt to work." ) ; func ( certs ) ; return ; }
if ( ( obj . parent . config . letsencrypt . rsakeysize != null ) && ( obj . parent . config . letsencrypt . rsakeysize !== 2048 ) && ( obj . parent . config . letsencrypt . rsakeysize !== 3072 ) ) { console . log ( "ERROR: Invalid Let's Encrypt certificate key size, must be 2048 or 3072." ) ; func ( certs ) ; return ; }
2018-01-12 14:41:26 -05:00
2018-01-15 00:01:06 -05:00
// Get the list of domains
2019-11-14 01:47:17 -05:00
obj . leDomains = [ certs . CommonName ] ;
2018-01-15 00:01:06 -05:00
if ( obj . parent . config . letsencrypt . names != null ) {
if ( typeof obj . parent . config . letsencrypt . names == 'string' ) { obj . parent . config . letsencrypt . names = obj . parent . config . letsencrypt . names . split ( ',' ) ; }
2018-08-29 20:40:30 -04:00
obj . parent . config . letsencrypt . names . map ( function ( s ) { return s . trim ( ) ; } ) ; // Trim each name
2018-01-15 00:01:06 -05:00
if ( ( typeof obj . parent . config . letsencrypt . names != 'object' ) || ( obj . parent . config . letsencrypt . names . length == null ) ) { console . log ( "ERROR: Let's Encrypt names must be an array in config.json." ) ; func ( certs ) ; return ; }
obj . leDomains = obj . parent . config . letsencrypt . names ;
}
2018-01-12 14:41:26 -05:00
2019-11-16 14:21:32 -05:00
if ( obj . parent . config . letsencrypt . production !== true ) {
// We are in staging mode, just go ahead
obj . getCertificateEx ( certs , func ) ;
} else {
// We are really in production mode
if ( obj . runAsProduction === true ) {
// Staging cert check must have been done already, move to production
obj . getCertificateEx ( certs , func ) ;
} else {
// Perform staging certificate check
parent . debug ( 'cert' , "Checking staging certificate " + obj . leDomains [ 0 ] + "..." ) ;
obj . leStaging . get ( { servername : obj . leDomains [ 0 ] } )
. then ( function ( results ) {
if ( results != null ) {
// We have a staging certificate, move to production for real
parent . debug ( 'cert' , "Staging certificate is present, moving to production..." ) ;
obj . runAsProduction = true ;
obj . getCertificateEx ( certs , func ) ;
} else {
// No staging certificate
parent . debug ( 'cert' , "No staging certificate present" ) ;
func ( certs ) ;
setTimeout ( obj . checkRenewCertificate , 10000 ) ; // Check the certificate in 10 seconds.
}
} )
. catch ( function ( e ) {
// No staging certificate
parent . debug ( 'cert' , "No staging certificate present" ) ;
func ( certs ) ;
setTimeout ( obj . checkRenewCertificate , 10000 ) ; // Check the certificate in 10 seconds.
} ) ;
}
}
}
obj . getCertificateEx = function ( certs , func ) {
2019-11-14 01:47:17 -05:00
// Get the Let's Encrypt certificate from our own storage
2019-11-16 14:21:32 -05:00
const xle = ( obj . runAsProduction === true ) ? obj . le : obj . leStaging ;
xle . get ( { servername : obj . leDomains [ 0 ] } )
2019-11-14 01:47:17 -05:00
. then ( function ( results ) {
2019-11-16 14:21:32 -05:00
// If we already have real certificates, use them
2019-11-14 01:47:17 -05:00
if ( results ) {
if ( results . site . altnames . indexOf ( certs . CommonName ) >= 0 ) {
certs . web . cert = results . pems . cert ;
certs . web . key = results . pems . privkey ;
certs . web . ca = [ results . pems . chain ] ;
}
for ( var i in obj . parent . config . domains ) {
if ( ( obj . parent . config . domains [ i ] . dns != null ) && ( obj . parent . certificateOperations . compareCertificateNames ( results . site . altnames , obj . parent . config . domains [ i ] . dns ) ) ) {
certs . dns [ i ] . cert = results . pems . cert ;
certs . dns [ i ] . key = results . pems . privkey ;
certs . dns [ i ] . ca = [ results . pems . chain ] ;
}
2018-12-20 15:12:24 -05:00
}
}
2019-11-16 14:21:32 -05:00
parent . debug ( 'cert' , "Got certs from local store (" + ( obj . runAsProduction ? "Production" : "Staging" ) + ")" ) ;
2018-01-15 00:01:06 -05:00
func ( certs ) ;
// Check if the Let's Encrypt certificate needs to be renewed.
2018-04-19 21:19:15 -04:00
setTimeout ( obj . checkRenewCertificate , 60000 ) ; // Check in 1 minute.
2018-01-15 00:01:06 -05:00
setInterval ( obj . checkRenewCertificate , 86400000 ) ; // Check again in 24 hours and every 24 hours.
return ;
2019-11-14 01:47:17 -05:00
} )
. catch ( function ( e ) {
2019-11-16 14:21:32 -05:00
parent . debug ( 'cert' , "Unable to get certs from local store (" + ( obj . runAsProduction ? "Production" : "Staging" ) + ")" ) ;
2019-11-14 01:47:17 -05:00
setTimeout ( obj . checkRenewCertificate , 10000 ) ; // Check the certificate in 10 seconds.
2018-01-15 00:01:06 -05:00
func ( certs ) ;
} ) ;
2019-11-14 01:47:17 -05:00
}
2018-01-15 00:01:06 -05:00
// Check if we need to renew the certificate, call this every day.
obj . checkRenewCertificate = function ( ) {
2019-11-19 14:18:08 -05:00
parent . debug ( 'cert' , "Checking certificate for " + obj . leDomains [ 0 ] + " (" + ( obj . runAsProduction ? "Production" : "Staging" ) + ")" ) ;
2019-11-18 17:17:27 -05:00
2019-11-14 01:47:17 -05:00
// Setup renew options
2019-11-19 14:18:08 -05:00
obj . certCheckStart = Date . now ( ) ;
2019-11-18 17:17:27 -05:00
const xle = ( obj . runAsProduction === true ) ? obj . le : obj . leStaging ;
var renewOptions = { servername : obj . leDomains [ 0 ] , altnames : obj . leDomains } ;
try {
xle . renew ( renewOptions )
. then ( function ( results ) {
if ( ( results == null ) || ( typeof results != 'object' ) || ( results . length == 0 ) || ( results [ 0 ] . error != null ) ) {
parent . debug ( 'cert' , "Unable to get a certificate (" + ( obj . runAsProduction ? "Production" : "Staging" ) + ", " + ( Date . now ( ) - obj . certCheckStart ) + "ms): " + JSON . stringify ( results ) ) ;
} else {
parent . debug ( 'cert' , "Checks completed (" + ( obj . runAsProduction ? "Production" : "Staging" ) + ", " + ( Date . now ( ) - obj . certCheckStart ) + "ms): " + JSON . stringify ( results ) ) ;
if ( obj . performRestart === true ) { parent . debug ( 'cert' , "Certs changed, restarting..." ) ; obj . parent . performServerCertUpdate ( ) ; } // Reset the server, TODO: Reset all peers
else if ( obj . performMoveToProduction == true ) {
parent . debug ( 'cert' , "Staging certificate received, moving to production..." ) ;
obj . runAsProduction = true ;
obj . performMoveToProduction = false ;
obj . performRestart = true ;
setTimeout ( obj . checkRenewCertificate , 10000 ) ; // Check the certificate in 10 seconds.
}
}
} )
. catch ( function ( ex ) {
parent . debug ( 'cert' , "checkCertificate exception: (" + JSON . stringify ( ex ) + ")" ) ;
console . log ( ex ) ;
} ) ;
} catch ( ex ) {
parent . debug ( 'cert' , "checkCertificate main exception: (" + JSON . stringify ( ex ) + ")" ) ;
console . log ( ex ) ;
2020-03-04 17:57:03 -05:00
return ex ;
2019-11-18 17:17:27 -05:00
}
2020-03-04 17:57:03 -05:00
return null ;
2019-11-14 01:47:17 -05:00
}
2018-01-12 14:41:26 -05:00
2018-08-29 20:40:30 -04:00
return obj ;
2018-11-29 20:59:29 -05:00
} catch ( ex ) { console . log ( ex ) ; } // Unable to start Let's Encrypt
2018-08-29 20:40:30 -04:00
return null ;
2019-11-14 01:47:17 -05:00
} ;
// GreenLock v3 Manager
module . exports . create = function ( options ) {
2020-03-02 15:36:52 -05:00
//console.log('xxx-create', options);
2020-01-25 15:14:14 -05:00
var manager = { parent : globalLetsEncrypt } ;
2019-11-14 01:47:17 -05:00
manager . find = async function ( options ) {
2020-03-02 15:36:52 -05:00
try {
// GreenLock sometimes has the bad behavior of adding a wildcard cert request, remove it here if needed.
if ( ( options . wildname != null ) && ( options . wildname != '' ) ) { options . wildname = '' ; }
if ( options . altnames ) {
var altnames2 = [ ] ;
for ( var i in options . altnames ) { if ( options . altnames [ i ] . indexOf ( '*' ) == - 1 ) { altnames2 . push ( options . altnames [ i ] ) ; } }
options . altnames = altnames2 ;
}
if ( options . servernames ) {
var servernames2 = [ ] ;
for ( var i in options . servernames ) { if ( options . servernames [ i ] . indexOf ( '*' ) == - 1 ) { servernames2 . push ( options . servernames [ i ] ) ; } }
options . servernames = servernames2 ;
}
} catch ( ex ) { console . log ( ex ) ; }
2019-11-15 20:55:05 -05:00
return Promise . resolve ( [ { subject : options . servername , altnames : options . altnames } ] ) ;
2019-11-14 01:47:17 -05:00
} ;
manager . set = function ( options ) {
2020-03-02 15:36:52 -05:00
//console.log('xxx-set', options);
2019-11-18 17:17:27 -05:00
manager . parent . parent . debug ( 'cert' , "Certificate has been set: " + JSON . stringify ( options ) ) ;
2019-11-16 14:21:32 -05:00
if ( manager . parent . parent . config . letsencrypt . production == manager . parent . runAsProduction ) { manager . parent . performRestart = true ; }
else if ( ( manager . parent . parent . config . letsencrypt . production === true ) && ( manager . parent . runAsProduction === false ) ) { manager . parent . performMoveToProduction = true ; }
2019-11-14 01:47:17 -05:00
return null ;
} ;
manager . remove = function ( options ) {
2020-03-02 15:36:52 -05:00
//console.log('xxx-remove', options);
2019-11-18 17:17:27 -05:00
manager . parent . parent . debug ( 'cert' , "Certificate has been removed: " + JSON . stringify ( options ) ) ;
2019-11-16 14:21:32 -05:00
if ( manager . parent . parent . config . letsencrypt . production == manager . parent . runAsProduction ) { manager . parent . performRestart = true ; }
else if ( ( manager . parent . parent . config . letsencrypt . production === true ) && ( manager . parent . runAsProduction === false ) ) { manager . parent . performMoveToProduction = true ; }
2019-11-14 01:47:17 -05:00
return null ;
} ;
// set the global config
manager . defaults = async function ( options ) {
2020-03-02 15:36:52 -05:00
//console.log('xxx-defaults', options);
2019-11-16 14:21:32 -05:00
var r ;
if ( manager . parent . runAsProduction === true ) {
// Production
//console.log('LE-DEFAULTS-Production', options);
if ( options != null ) { for ( var i in options ) { if ( manager . parent . leDefaults [ i ] == null ) { manager . parent . leDefaults [ i ] = options [ i ] ; } } }
r = manager . parent . leDefaults ;
r . subscriberEmail = manager . parent . parent . config . letsencrypt . email ;
2019-11-19 14:18:08 -05:00
r . sites = { mainsite : { subject : manager . parent . leDomains [ 0 ] , altnames : manager . parent . leDomains } } ;
2019-11-16 14:21:32 -05:00
} else {
// Staging
//console.log('LE-DEFAULTS-Staging', options);
if ( options != null ) { for ( var i in options ) { if ( manager . parent . leDefaultsStaging [ i ] == null ) { manager . parent . leDefaultsStaging [ i ] = options [ i ] ; } } }
r = manager . parent . leDefaultsStaging ;
r . subscriberEmail = manager . parent . parent . config . letsencrypt . email ;
2019-11-19 14:18:08 -05:00
r . sites = { mainsite : { subject : manager . parent . leDomains [ 0 ] , altnames : manager . parent . leDomains } } ;
2019-11-16 14:21:32 -05:00
}
2019-11-14 01:47:17 -05:00
return r ;
} ;
return manager ;
2020-03-05 04:39:40 -05:00
} ;
// ACME-Client Implementation
var globalLetsEncrypt = null ;
module . exports . CreateLetsEncrypt2 = function ( parent ) {
const acme = require ( 'acme-client' ) ;
var obj = { } ;
obj . lib = 'acme-client' ;
obj . fs = require ( 'fs' ) ;
obj . path = require ( 'path' ) ;
obj . parent = parent ;
obj . forge = obj . parent . certificateOperations . forge ;
obj . leDomains = null ;
obj . challenges = { } ;
obj . runAsProduction = false ;
obj . redirWebServerHooked = false ;
2020-03-05 14:18:50 -05:00
obj . configErr = null ;
obj . configOk = false ;
2020-03-06 17:06:33 -05:00
obj . pendingRequest = false ;
2020-03-05 14:18:50 -05:00
// Let's Encrypt debug logging
obj . log = function ( str ) {
parent . debug ( 'cert' , 'LE: ' + str ) ;
var d = new Date ( ) ;
obj . events . push ( d . toLocaleDateString ( ) + ' ' + d . toLocaleTimeString ( ) + ' - ' + str ) ;
while ( obj . events . length > 200 ) { obj . events . shift ( ) ; } // Keep only 200 last events.
}
obj . events = [ ] ;
2020-03-05 04:39:40 -05:00
// Setup the certificate storage paths
obj . certPath = obj . path . join ( obj . parent . datapath , 'letsencrypt-certs' ) ;
try { obj . parent . fs . mkdirSync ( obj . certPath ) ; } catch ( e ) { }
// Hook up GreenLock to the redirection server
if ( obj . parent . config . settings . rediraliasport === 80 ) { obj . redirWebServerHooked = true ; }
else if ( ( obj . parent . config . settings . rediraliasport == null ) && ( obj . parent . redirserver . port == 80 ) ) { obj . redirWebServerHooked = true ; }
// Deal with HTTP challenges
function challengeCreateFn ( authz , challenge , keyAuthorization ) { if ( challenge . type === 'http-01' ) { obj . challenges [ challenge . token ] = keyAuthorization ; } }
function challengeRemoveFn ( authz , challenge , keyAuthorization ) { if ( challenge . type === 'http-01' ) { delete obj . challenges [ challenge . token ] ; } }
2020-03-06 17:06:33 -05:00
obj . challenge = function ( token , hostname , func ) { if ( obj . challenges [ token ] != null ) { obj . log ( "Succesful response to challenge." ) ; } else { obj . log ( "Failed to respond to challenge, token: " + token + ", table: " + JSON . stringify ( obj . challenges ) + "." ) ; } func ( obj . challenges [ token ] ) ; }
2020-03-05 04:39:40 -05:00
// Get the current certificate
obj . getCertificate = function ( certs , func ) {
obj . runAsProduction = ( obj . parent . config . letsencrypt . production === true ) ;
2020-03-05 14:18:50 -05:00
obj . log ( "Getting certs from local store (" + ( obj . runAsProduction ? "Production" : "Staging" ) + ")" ) ;
if ( certs . CommonName . indexOf ( '.' ) == - 1 ) { obj . configErr = "ERROR: Use --cert to setup the default server name before using Let's Encrypt." ; obj . log ( obj . configErr ) ; console . log ( obj . configErr ) ; func ( certs ) ; return ; }
if ( obj . parent . config . letsencrypt == null ) { obj . configErr = "No Let's Encrypt configuration" ; obj . log ( obj . configErr ) ; console . log ( obj . configErr ) ; func ( certs ) ; return ; }
if ( obj . parent . config . letsencrypt . email == null ) { obj . configErr = "ERROR: Let's Encrypt email address not specified." ; obj . log ( obj . configErr ) ; console . log ( obj . configErr ) ; func ( certs ) ; return ; }
if ( ( obj . parent . redirserver == null ) || ( ( typeof obj . parent . config . settings . rediraliasport === 'number' ) && ( obj . parent . config . settings . rediraliasport !== 80 ) ) || ( ( obj . parent . config . settings . rediraliasport == null ) && ( obj . parent . redirserver . port !== 80 ) ) ) { obj . configErr = "ERROR: Redirection web server must be active on port 80 for Let's Encrypt to work." ; obj . log ( obj . configErr ) ; console . log ( obj . configErr ) ; func ( certs ) ; return ; }
if ( obj . redirWebServerHooked !== true ) { obj . configErr = "ERROR: Redirection web server not setup for Let's Encrypt to work." ; obj . log ( obj . configErr ) ; console . log ( obj . configErr ) ; func ( certs ) ; return ; }
if ( ( obj . parent . config . letsencrypt . rsakeysize != null ) && ( obj . parent . config . letsencrypt . rsakeysize !== 2048 ) && ( obj . parent . config . letsencrypt . rsakeysize !== 3072 ) ) { obj . configErr = "ERROR: Invalid Let's Encrypt certificate key size, must be 2048 or 3072." ; obj . log ( obj . configErr ) ; console . log ( obj . configErr ) ; func ( certs ) ; return ; }
if ( obj . checkInterval == null ) { obj . checkInterval = setInterval ( obj . checkRenewCertificate , 86400000 ) ; } // Call certificate check every 24 hours.
obj . configOk = true ;
2020-03-05 04:39:40 -05:00
// Get the list of domains
obj . leDomains = [ certs . CommonName ] ;
if ( obj . parent . config . letsencrypt . names != null ) {
if ( typeof obj . parent . config . letsencrypt . names == 'string' ) { obj . parent . config . letsencrypt . names = obj . parent . config . letsencrypt . names . split ( ',' ) ; }
obj . parent . config . letsencrypt . names . map ( function ( s ) { return s . trim ( ) ; } ) ; // Trim each name
if ( ( typeof obj . parent . config . letsencrypt . names != 'object' ) || ( obj . parent . config . letsencrypt . names . length == null ) ) { console . log ( "ERROR: Let's Encrypt names must be an array in config.json." ) ; func ( certs ) ; return ; }
obj . leDomains = obj . parent . config . letsencrypt . names ;
}
// Read TLS certificate from the configPath
var certFile = obj . path . join ( obj . certPath , ( obj . runAsProduction ? 'production.crt' : 'staging.crt' ) ) ;
var keyFile = obj . path . join ( obj . certPath , ( obj . runAsProduction ? 'production.key' : 'staging.key' ) ) ;
if ( obj . fs . existsSync ( certFile ) && obj . fs . existsSync ( keyFile ) ) {
2020-03-05 14:18:50 -05:00
obj . log ( "Reading certificate files" ) ;
2020-03-05 04:39:40 -05:00
// Read the certificate and private key
var certPem = obj . fs . readFileSync ( certFile ) . toString ( 'utf8' ) ;
var cert = obj . forge . pki . certificateFromPem ( certPem ) ;
var keyPem = obj . fs . readFileSync ( keyFile ) . toString ( 'utf8' ) ;
var key = obj . forge . pki . privateKeyFromPem ( keyPem ) ;
// Decode the certificate common and alt names
obj . certNames = [ cert . subject . getField ( 'CN' ) . value ] ;
var altNames = cert . getExtension ( 'subjectAltName' ) ;
if ( altNames ) { for ( i = 0 ; i < altNames . altNames . length ; i ++ ) { var acn = altNames . altNames [ i ] . value . toLowerCase ( ) ; if ( obj . certNames . indexOf ( acn ) == - 1 ) { obj . certNames . push ( acn ) ; } } }
// Decode the certificate expire time
obj . certExpire = cert . validity . notAfter ;
// Use this certificate when possible on any domain
if ( obj . certNames . indexOf ( certs . CommonName ) >= 0 ) {
2020-03-05 14:18:50 -05:00
obj . log ( "Setting LE cert for default domain." ) ;
2020-03-05 04:39:40 -05:00
certs . web . cert = certPem ;
certs . web . key = keyPem ;
//certs.web.ca = [results.pems.chain];
}
for ( var i in obj . parent . config . domains ) {
if ( ( obj . parent . config . domains [ i ] . dns != null ) && ( obj . parent . certificateOperations . compareCertificateNames ( obj . certNames , obj . parent . config . domains [ i ] . dns ) ) ) {
2020-03-05 14:18:50 -05:00
obj . log ( "Setting LE cert for domain " + i + "." ) ;
2020-03-05 04:39:40 -05:00
certs . dns [ i ] . cert = certPem ;
certs . dns [ i ] . key = keyPem ;
//certs.dns[i].ca = [results.pems.chain];
}
}
} else {
2020-03-05 14:18:50 -05:00
obj . log ( "No certificate files found" ) ;
2020-03-05 04:39:40 -05:00
}
func ( certs ) ;
2020-03-05 14:18:50 -05:00
setTimeout ( obj . checkRenewCertificate , 5000 ) ; // Hold 5 seconds and check if we need to request a certificate.
2020-03-05 04:39:40 -05:00
}
// Check if we need to get a new certificate
// Return 0 = CertOK, 1 = Request:NoCert, 2 = Request:Expire, 3 = Request:MissingNames
obj . checkRenewCertificate = function ( ) {
2020-03-06 17:06:33 -05:00
if ( obj . pendingRequest == true ) { obj . log ( "Request for certificate is in process." ) ; return 4 ; }
2020-03-05 04:39:40 -05:00
if ( obj . certNames == null ) {
2020-03-05 14:18:50 -05:00
obj . log ( "Got no certificates, asking for one now." ) ;
2020-03-05 04:39:40 -05:00
obj . requestCertificate ( ) ;
return 1 ;
} else {
// Look at the existing certificate to see if we need to renew it
var daysLeft = Math . floor ( ( obj . certExpire - new Date ( ) ) / 86400000 ) ;
2020-03-05 14:18:50 -05:00
obj . log ( "Certificate has " + daysLeft + " day(s) left." ) ;
2020-03-05 04:39:40 -05:00
if ( daysLeft < 45 ) {
2020-03-05 14:18:50 -05:00
obj . log ( "Asking for new certificate because of expire time." ) ;
2020-03-05 04:39:40 -05:00
obj . requestCertificate ( ) ;
return 2 ;
} else {
var missingDomain = false ;
for ( var i in obj . leDomains ) {
if ( obj . parent . certificateOperations . compareCertificateNames ( obj . certNames , obj . leDomains [ i ] ) == false ) {
2020-03-05 14:18:50 -05:00
obj . log ( "Missing name \"" + obj . leDomains [ i ] + "\"." ) ;
2020-03-05 04:39:40 -05:00
missingDomain = true ;
}
}
if ( missingDomain ) {
2020-03-05 14:18:50 -05:00
obj . log ( "Asking for new certificate because of missing names." ) ;
2020-03-05 04:39:40 -05:00
obj . requestCertificate ( ) ;
return 3 ;
} else {
2020-03-05 14:18:50 -05:00
obj . log ( "Certificate is ok." ) ;
2020-03-05 04:39:40 -05:00
}
}
}
return 0 ;
}
obj . requestCertificate = function ( ) {
2020-03-06 17:06:33 -05:00
if ( obj . pendingRequest == true ) return ;
2020-03-05 14:18:50 -05:00
if ( obj . configOk == false ) { obj . log ( "Can't request cert, invalid configuration." ) ; return ; }
2020-03-06 17:06:33 -05:00
obj . pendingRequest = true ;
2020-03-05 14:18:50 -05:00
2020-03-05 04:39:40 -05:00
// Create a private key
2020-03-05 14:18:50 -05:00
obj . log ( "Generating private key..." ) ;
2020-03-05 04:39:40 -05:00
acme . forge . createPrivateKey ( ) . then ( function ( accountKey ) {
// Create the ACME client
2020-03-05 14:18:50 -05:00
obj . log ( "Setting up ACME client..." ) ;
2020-03-05 04:39:40 -05:00
obj . client = new acme . Client ( {
directoryUrl : obj . runAsProduction ? acme . directory . letsencrypt . production : acme . directory . letsencrypt . staging ,
accountKey : accountKey
} ) ;
// Create Certificate Request (CSR)
2020-03-05 14:18:50 -05:00
obj . log ( "Creating certificate request..." ) ;
2020-03-05 04:39:40 -05:00
acme . forge . createCsr ( {
commonName : obj . leDomains [ 0 ] ,
altNames : obj . leDomains
} ) . then ( function ( r ) {
var csr = r [ 1 ] ;
obj . tempPrivateKey = r [ 0 ] ;
2020-03-05 14:18:50 -05:00
obj . log ( "Requesting certificate from Let's Encrypt..." ) ;
2020-03-05 04:39:40 -05:00
obj . client . auto ( {
csr ,
email : obj . parent . config . letsencrypt . email ,
termsOfServiceAgreed : true ,
challengeCreateFn ,
challengeRemoveFn
} ) . then ( function ( cert ) {
2020-03-05 14:18:50 -05:00
obj . log ( "Got certificate." ) ;
2020-03-05 04:39:40 -05:00
// Save certificate and private key to PEM files
var certFile = obj . path . join ( obj . certPath , ( obj . runAsProduction ? 'production.crt' : 'staging.crt' ) ) ;
var keyFile = obj . path . join ( obj . certPath , ( obj . runAsProduction ? 'production.key' : 'staging.key' ) ) ;
obj . fs . writeFileSync ( certFile , cert ) ;
obj . fs . writeFileSync ( keyFile , obj . tempPrivateKey ) ;
delete obj . tempPrivateKey ;
// Cause a server restart
2020-03-05 14:18:50 -05:00
obj . log ( "Performing server restart..." ) ;
2020-03-05 04:39:40 -05:00
obj . parent . performServerCertUpdate ( ) ;
} , function ( err ) {
2020-03-05 14:18:50 -05:00
obj . log ( "Failed to obtain certificate: " + err . message ) ;
2020-03-06 17:06:33 -05:00
obj . pendingRequest = false ;
delete obj . client ;
2020-03-05 04:39:40 -05:00
} ) ;
} , function ( err ) {
2020-03-05 14:18:50 -05:00
obj . log ( "Failed to generate certificate request: " + err . message ) ;
2020-03-06 17:06:33 -05:00
obj . pendingRequest = false ;
delete obj . client ;
2020-03-05 04:39:40 -05:00
} ) ;
} , function ( err ) {
2020-03-05 14:18:50 -05:00
obj . log ( "Failed to generate private key: " + err . message ) ;
2020-03-06 17:06:33 -05:00
obj . pendingRequest = false ;
delete obj . client ;
2020-03-05 04:39:40 -05:00
} ) ;
}
// Return the status of this module
obj . getStats = function ( ) {
var r = {
lib : 'acme-client' ,
2020-03-05 14:18:50 -05:00
configOk : obj . configOk ,
2020-03-05 04:39:40 -05:00
leDomains : obj . leDomains ,
challenges : obj . challenges ,
production : obj . runAsProduction ,
webServer : obj . redirWebServerHooked ,
2020-03-05 14:18:50 -05:00
certPath : obj . certPath
2020-03-05 04:39:40 -05:00
} ;
2020-03-05 14:18:50 -05:00
if ( obj . configErr ) { r . error = obj . configErr ; }
if ( obj . certExpire ) { r . cert = 'Present' ; r . daysLeft = Math . floor ( ( obj . certExpire - new Date ( ) ) / 86400000 ) ; } else { r . cert = 'None' ; }
2020-03-05 04:39:40 -05:00
return r ;
}
return obj ;
}