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
2019-01-03 19:22:15 -05:00
* @ copyright Intel Corporation 2018 - 2019
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
2019-11-14 01:47:17 -05:00
module . exports . CreateLetsEncrypt = function ( parent ) {
2018-01-15 00:01:06 -05:00
try {
2019-11-14 01:47:17 -05:00
parent . debug ( 'cert' , "Initializing Let's Encrypt support" ) ;
// Check the current node version
if ( Number ( process . version . match ( /^v(\d+\.\d+)/ ) [ 1 ] ) < 8 ) { return null ; }
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 = { } ;
obj . parent = parent ;
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-14 01:47:17 -05:00
obj . performRestart = false ;
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-14 01:47:17 -05:00
// Setup Let's Encrypt default configuration
obj . leDefaults = {
agreeToTerms : true ,
//serverKeyType: 'RSA-2048', // Seems like only "RSA-2048" or "P-256" is supported.
store : {
module : 'greenlock-store-fs' ,
basePath : obj . configPath
}
} ;
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 ;
}
2018-01-15 00:01:06 -05:00
// Create the main GreenLock code module.
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 ) ) ; } } ,
staging : ( obj . parent . config . letsencrypt . production !== true ) ,
debug : ( obj . parent . args . debug > 0 )
2018-08-29 20:40:30 -04:00
} ;
2019-11-14 01:47:17 -05:00
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
2018-01-15 00:01:06 -05:00
// Hook up GreenLock to the redirection server
2019-11-14 01:47:17 -05:00
if ( obj . parent . redirserver . port == 80 ) { obj . redirWebServerHooked = true ; }
// Respond to a challenge
obj . challenge = function ( token , hostname , func ) {
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
}
2018-01-12 14:41:26 -05:00
2018-01-15 00:01:06 -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 ; }
if ( ( obj . parent . redirserver == 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 ; }
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 ;
obj . leDomains . sort ( ) ; // Sort the array so it's always going to be in the same order.
}
2018-01-12 14:41:26 -05:00
2019-11-14 01:47:17 -05:00
// Get altnames
obj . altnames = [ ] ;
obj . servername = certs . CommonName ;
for ( var i in obj . leDomains ) { if ( obj . leDomains [ i ] != certs . CommonName ) { obj . altnames . push ( obj . leDomains [ i ] ) ; } }
2018-01-12 14:41:26 -05:00
2019-11-14 01:47:17 -05:00
// Get the Let's Encrypt certificate from our own storage
obj . le . get ( { servername : certs . CommonName } )
. then ( function ( results ) {
2018-01-15 00:01:06 -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-14 01:47:17 -05:00
parent . debug ( 'cert' , "Got certs from local store" ) ;
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 ) {
parent . debug ( 'cert' , "Unable to get certs from local store" ) ;
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-14 01:47:17 -05:00
parent . debug ( 'cert' , "Checking certs" ) ;
// Setup renew options
var renewOptions = { servername : obj . servername } ;
if ( obj . altnames . length > 0 ) { renewOptions . altnames = obj . altnames ; }
obj . le . renew ( renewOptions )
. then ( function ( results ) {
parent . debug ( 'cert' , "Checks completed" ) ;
if ( obj . performRestart === true ) { parent . debug ( 'cert' , "Certs changed, restarting..." ) ; obj . parent . performServerCertUpdate ( ) ; } // Reset the server, TODO: Reset all peers
} )
. catch ( function ( e ) { console . log ( e ) ; func ( certs ) ; } ) ;
}
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 ) {
var manager = { parent : options . parent } ;
manager . find = async function ( options ) {
//console.log('LE-FIND', options);
return Promise . resolve ( [ { subject : options . servername , altnames : options . altnames } ] ) ;
} ;
manager . set = function ( options ) {
manager . parent . parent . debug ( 'cert' , "Certificate has been set" ) ;
manager . parent . performRestart = true ;
return null ;
} ;
manager . remove = function ( options ) {
manager . parent . parent . debug ( 'cert' , "Certificate has been removed" ) ;
manager . parent . performRestart = true ;
return null ;
} ;
// set the global config
manager . defaults = async function ( options ) {
//console.log('LE-DEFAULTS', options);
if ( options != null ) { for ( var i in options ) { if ( manager . parent . leDefaults [ i ] == null ) { manager . parent . leDefaults [ i ] = options [ i ] ; } } }
var r = manager . parent . leDefaults ;
var mainsite = { subject : manager . parent . servername } ;
if ( manager . parent . altnames . length > 0 ) { mainsite . altnames = manager . parent . altnames ; }
r . subscriberEmail = manager . parent . parent . config . letsencrypt . email ;
r . sites = { mainsite : mainsite } ;
return r ;
} ;
return manager ;
2018-08-29 20:40:30 -04:00
} ;