2018-01-12 22:41:26 +03:00
/ * *
2018-01-15 08:01:06 +03:00
* @ description MeshCentral letsEncrypt module , uses GreenLock to do all the work .
2018-01-12 22:41:26 +03:00
* @ author Ylian Saint - Hilaire
2020-01-03 05:30:12 +03:00
* @ copyright Intel Corporation 2018 - 2020
2018-01-12 22:41:26 +03:00
* @ license Apache - 2.0
2018-01-15 08:01:06 +03:00
* @ version v0 . 0.2
2018-01-12 22:41:26 +03:00
* /
2018-08-30 03:40:30 +03:00
/*xjslint node: true */
/*xjslint plusplus: true */
/*xjslint maxlen: 256 */
/*jshint node: true */
/*jshint strict: false */
/*jshint esversion: 6 */
2019-11-14 09:47:17 +03:00
'use strict' ;
2018-08-27 22:24:15 +03:00
2020-01-25 23:14:14 +03:00
var globalLetsEncrypt = null ;
2019-11-16 04:55:05 +03:00
module . exports . CreateLetsEncrypt = function ( parent ) {
2018-01-15 08:01:06 +03:00
try {
2019-11-18 22:42:09 +03: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 09:47:17 +03:00
2019-11-19 22:18:08 +03: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 09:47:17 +03:00
2019-01-12 01:01:36 +03: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 09:47:17 +03: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-12 01:01:36 +03:00
} catch ( ex ) { }
2018-01-15 08:01:06 +03:00
2019-01-12 01:01:36 +03:00
// Get GreenLock setup and running.
const greenlock = require ( 'greenlock' ) ;
2018-01-15 08:01:06 +03:00
var obj = { } ;
2020-01-25 23:14:14 +03:00
globalLetsEncrypt = obj ;
2018-01-15 08:01:06 +03:00
obj . parent = parent ;
2019-11-14 09:47:17 +03:00
obj . path = require ( 'path' ) ;
2018-01-15 08:01:06 +03:00
obj . redirWebServerHooked = false ;
obj . leDomains = null ;
obj . leResults = null ;
2019-11-16 22:21:32 +03: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 08:01:06 +03:00
// Setup the certificate storage paths
2019-11-14 09:59:33 +03:00
obj . configPath = obj . path . join ( obj . parent . datapath , 'letsencrypt3' ) ;
2018-01-15 08:01:06 +03:00
try { obj . parent . fs . mkdirSync ( obj . configPath ) ; } catch ( e ) { }
2019-11-16 22:21:32 +03:00
obj . configPathStaging = obj . path . join ( obj . parent . datapath , 'letsencrypt3-staging' ) ;
try { obj . parent . fs . mkdirSync ( obj . configPathStaging ) ; } catch ( e ) { }
2018-01-15 08:01:06 +03:00
2019-11-14 09:47:17 +03:00
// Setup Let's Encrypt default configuration
2019-11-16 22:21:32 +03: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 08:01:06 +03:00
2019-11-14 09:47:17 +03: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-16 04:55:05 +03:00
2019-11-19 01:17:27 +03: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 22:21:32 +03:00
// Create the main GreenLock code module for production.
2018-01-15 08:01:06 +03:00
var greenlockargs = {
2019-11-14 09:47:17 +03: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 22:21:32 +03:00
staging : false ,
2019-11-19 01:17:27 +03:00
debug : ledebug
2018-08-30 03:40:30 +03: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 08:01:06 +03:00
obj . le = greenlock . create ( greenlockargs ) ;
2018-01-12 22:41:26 +03:00
2019-11-16 22:21:32 +03: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-19 01:17:27 +03: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 22:21:32 +03:00
staging : true ,
2019-11-19 01:17:27 +03:00
debug : ledebug
2019-11-16 22:21:32 +03: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 08:01:06 +03:00
// Hook up GreenLock to the redirection server
2019-11-18 00:35:50 +03: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 09:47:17 +03:00
// Respond to a challenge
obj . challenge = function ( token , hostname , func ) {
2019-11-16 22:21:32 +03: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 09:47:17 +03:00
}
2018-01-12 22:41:26 +03:00
2019-11-16 22:21:32 +03:00
obj . getCertificate = function ( certs , func ) {
2019-11-14 09:47:17 +03:00
parent . debug ( 'cert' , "Getting certs from local store" ) ;
2019-03-05 10:48:45 +03: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 08:01:06 +03: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-18 00:35:50 +03: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 08:01:06 +03: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 22:41:26 +03:00
2018-01-15 08:01:06 +03:00
// Get the list of domains
2019-11-14 09:47:17 +03:00
obj . leDomains = [ certs . CommonName ] ;
2018-01-15 08:01:06 +03: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-30 03:40:30 +03:00
obj . parent . config . letsencrypt . names . map ( function ( s ) { return s . trim ( ) ; } ) ; // Trim each name
2018-01-15 08:01:06 +03: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 22:41:26 +03:00
2019-11-16 22:21:32 +03: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 09:47:17 +03:00
// Get the Let's Encrypt certificate from our own storage
2019-11-16 22:21:32 +03:00
const xle = ( obj . runAsProduction === true ) ? obj . le : obj . leStaging ;
xle . get ( { servername : obj . leDomains [ 0 ] } )
2019-11-14 09:47:17 +03:00
. then ( function ( results ) {
2019-11-16 22:21:32 +03:00
// If we already have real certificates, use them
2019-11-14 09:47:17 +03: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 23:12:24 +03:00
}
}
2019-11-16 22:21:32 +03:00
parent . debug ( 'cert' , "Got certs from local store (" + ( obj . runAsProduction ? "Production" : "Staging" ) + ")" ) ;
2018-01-15 08:01:06 +03:00
func ( certs ) ;
// Check if the Let's Encrypt certificate needs to be renewed.
2018-04-20 04:19:15 +03:00
setTimeout ( obj . checkRenewCertificate , 60000 ) ; // Check in 1 minute.
2018-01-15 08:01:06 +03:00
setInterval ( obj . checkRenewCertificate , 86400000 ) ; // Check again in 24 hours and every 24 hours.
return ;
2019-11-14 09:47:17 +03:00
} )
. catch ( function ( e ) {
2019-11-16 22:21:32 +03:00
parent . debug ( 'cert' , "Unable to get certs from local store (" + ( obj . runAsProduction ? "Production" : "Staging" ) + ")" ) ;
2019-11-14 09:47:17 +03:00
setTimeout ( obj . checkRenewCertificate , 10000 ) ; // Check the certificate in 10 seconds.
2018-01-15 08:01:06 +03:00
func ( certs ) ;
} ) ;
2019-11-14 09:47:17 +03:00
}
2018-01-15 08:01:06 +03:00
// Check if we need to renew the certificate, call this every day.
obj . checkRenewCertificate = function ( ) {
2019-11-19 22:18:08 +03:00
parent . debug ( 'cert' , "Checking certificate for " + obj . leDomains [ 0 ] + " (" + ( obj . runAsProduction ? "Production" : "Staging" ) + ")" ) ;
2019-11-19 01:17:27 +03:00
2019-11-14 09:47:17 +03:00
// Setup renew options
2019-11-19 22:18:08 +03:00
obj . certCheckStart = Date . now ( ) ;
2019-11-19 01:17:27 +03: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 ) ;
}
2019-11-14 09:47:17 +03:00
}
2018-01-12 22:41:26 +03:00
2018-08-30 03:40:30 +03:00
return obj ;
2018-11-30 04:59:29 +03:00
} catch ( ex ) { console . log ( ex ) ; } // Unable to start Let's Encrypt
2018-08-30 03:40:30 +03:00
return null ;
2019-11-14 09:47:17 +03:00
} ;
// GreenLock v3 Manager
module . exports . create = function ( options ) {
2020-03-02 23:36:52 +03:00
//console.log('xxx-create', options);
2020-01-25 23:14:14 +03:00
var manager = { parent : globalLetsEncrypt } ;
2019-11-14 09:47:17 +03:00
manager . find = async function ( options ) {
2020-03-02 23:36:52 +03: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-16 04:55:05 +03:00
return Promise . resolve ( [ { subject : options . servername , altnames : options . altnames } ] ) ;
2019-11-14 09:47:17 +03:00
} ;
manager . set = function ( options ) {
2020-03-02 23:36:52 +03:00
//console.log('xxx-set', options);
2019-11-19 01:17:27 +03:00
manager . parent . parent . debug ( 'cert' , "Certificate has been set: " + JSON . stringify ( options ) ) ;
2019-11-16 22:21:32 +03: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 09:47:17 +03:00
return null ;
} ;
manager . remove = function ( options ) {
2020-03-02 23:36:52 +03:00
//console.log('xxx-remove', options);
2019-11-19 01:17:27 +03:00
manager . parent . parent . debug ( 'cert' , "Certificate has been removed: " + JSON . stringify ( options ) ) ;
2019-11-16 22:21:32 +03: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 09:47:17 +03:00
return null ;
} ;
// set the global config
manager . defaults = async function ( options ) {
2020-03-02 23:36:52 +03:00
//console.log('xxx-defaults', options);
2019-11-16 22:21:32 +03: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 22:18:08 +03:00
r . sites = { mainsite : { subject : manager . parent . leDomains [ 0 ] , altnames : manager . parent . leDomains } } ;
2019-11-16 22:21:32 +03: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 22:18:08 +03:00
r . sites = { mainsite : { subject : manager . parent . leDomains [ 0 ] , altnames : manager . parent . leDomains } } ;
2019-11-16 22:21:32 +03:00
}
2019-11-14 09:47:17 +03:00
return r ;
} ;
return manager ;
2018-08-30 03:40:30 +03:00
} ;