import ApiStatusCodes from '../../api/ApiStatusCodes' ; import DockerApi from '../../docker/DockerApi' ; import CaptainConstants from '../../utils/CaptainConstants' ; import Logger from '../../utils/Logger' ; import Utils from '../../utils/Utils' ; import fs = require ( 'fs-extra' ) ; const WEBROOT_PATH_IN_CERTBOT = '/captain-webroot' ; const WEBROOT_PATH_IN_CAPTAIN = CaptainConstants . captainStaticFilesDir + CaptainConstants . nginxDomainSpecificHtmlDir ; const shouldUseStaging = false ; class CertbotManager { private isOperationInProcess : boolean ; constructor ( private dockerApi ) { this . dockerApi = dockerApi ; } domainValidOrThrow ( domainName ) { if ( ! domainName ) { throw new Error ( 'Domain Name is empty' ) ; } const RegExpression = /^[a-z0-9\.\-]*$/ ; if ( ! RegExpression . test ( domainName ) ) { throw new Error ( 'Bad Domain Name!' ) ; } } getCertRelativePathForDomain ( domainName ) { const self = this ; self . domainValidOrThrow ( domainName ) ; return `/live/${ domainName }/fullchain.pem` ; } getKeyRelativePathForDomain ( domainName ) { const self = this ; self . domainValidOrThrow ( domainName ) ; return `/live/${ domainName }/privkey.pem` ; } enableSsl ( domainName ) { const self = this ; Logger . d ( `Enabling SSL for ${ domainName }` ) ; return Promise . resolve ( ) . then ( function ( ) { self . domainValidOrThrow ( domainName ) ; return self . ensureDomainHasDirectory ( domainName ) ; } ) . then ( function ( ) { const cmd = [ 'certbot' , 'certonly' , '--webroot' , '-w' , `${ WEBROOT_PATH_IN_CERTBOT }/${ domainName }` , '-d' , domainName , ] ; if ( shouldUseStaging ) { cmd . push ( '--staging' ) ; } return self . runCommand ( cmd ) . then ( function ( output ) { Logger . d ( output ) ; if ( output . indexOf ( 'Congratulations! Your certificate and chain have been saved' ) >= 0 ) { return true ; } if ( output . indexOf ( 'Certificate not yet due for renewal; no action taken' ) >= 0 ) { return true ; } throw ApiStatusCodes . createError ( ApiStatusCodes . VERIFICATION_FAILED , `Unexpected output when enabling SSL for${ domainName } with ACME Certbot \n ${ output }` ) ; } ) ; } ) ; } ensureRegistered ( emailAddress ) { const self = this ; return Promise . resolve ( ) . then ( function ( ) { const cmd = [ 'certbot' , 'register' , '--email' , emailAddress , '--agree-tos' , '--no-eff-email' , ] ; if ( shouldUseStaging ) { cmd . push ( '--staging' ) ; } return self . runCommand ( cmd ) ; } ) . then ( function ( registerOutput ) { if ( registerOutput . indexOf ( 'Your account credentials have been saved in your Certbot' ) >= 0 ) { return true ; } if ( registerOutput . indexOf ( 'There is an existing account' ) >= 0 ) { return true ; } throw new Error ( `Unexpected output when registering with ACME Certbot \n ${ registerOutput }` ) ; } ) ; } ensureAllCurrentlyRegisteredDomainsHaveDirs ( ) { const self = this ; return Promise . resolve ( ) . then ( function ( ) { return self . runCommand ( [ 'certbot' , 'certificates' ] ) . then ( function ( output ) { const lines = output . split ( '\n' ) ; const domains = [ ] ; lines . forEach ( ( l ) => { if ( l . indexOf ( 'Certificate Name:' ) >= 0 ) { domains . push ( l . replace ( 'Certificate Name:' , '' ) . trim ( ) ) ; } } ) ; return domains ; } ) ; } ) . then ( function ( allDomains ) { const p = Promise . resolve ( ) ; allDomains . forEach ( ( d ) => { p . then ( function ( ) { return self . ensureDomainHasDirectory ( d ) ; } ) ; } ) ; return p ; } ) ; } lock ( ) { if ( this . isOperationInProcess ) { throw ApiStatusCodes . createError ( ApiStatusCodes . STATUS_ERROR_GENERIC , 'Another operation is in process for Certbot. Please wait a few seconds and try again.' ) ; } this . isOperationInProcess = true ; } unlock ( ) { this . isOperationInProcess = false ; } runCommand ( cmd ) { const dockerApi = this . dockerApi ; const self = this ; return Promise . resolve ( ) . then ( function ( ) { self . lock ( ) ; const nonInterActiveCommand = [ ... cmd , '--non-interactive' ] ; return dockerApi . executeCommand ( CaptainConstants . certbotServiceName , nonInterActiveCommand ) . then ( function ( data ) { self . unlock ( ) ; Logger . dev ( data ) ; return data ; } ) . catch ( function ( error ) { self . unlock ( ) ; throw error ; } ) ; } ) ; } ensureDomainHasDirectory ( domainName ) { return Promise . resolve ( ) . then ( function ( ) { return fs . ensureDir ( `${ WEBROOT_PATH_IN_CAPTAIN }/${ domainName }` ) ; } ) ; } renewAllCerts ( ) { const self = this ; const cmd = [ 'certbot' , 'renew' ] ; if ( shouldUseStaging ) { cmd . push ( '--staging' ) ; } return Promise . resolve ( ) . then ( function ( ) { return self . ensureAllCurrentlyRegisteredDomainsHaveDirs ( ) ; } ) . then ( function ( ) { return self . runCommand ( cmd ) ; } ) . then ( function ( output ) { } ) . catch ( function ( err ) { Logger . e ( err ) ; } ) ; } init ( myNodeId ) { const dockerApi = this . dockerApi ; const self = this ; function createCertbotServiceOnNode ( nodeId ) { Logger . d ( 'Creating Certbot service' ) ; return dockerApi . createServiceOnNodeId ( CaptainConstants . certbotImageName , CaptainConstants . certbotServiceName , undefined , nodeId , undefined , undefined , undefined ) . then ( function ( ) { Logger . d ( 'Waiting for Certbot...' ) ; return Utils . getDelayedPromise ( 12000 ) ; } ) ; } return Promise . resolve ( ) . then ( function ( ) { return fs . ensureDir ( CaptainConstants . letsEncryptEtcPath ) ; } ) . then ( function ( ) { return fs . ensureDir ( CaptainConstants . letsEncryptLibPath ) ; } ) . then ( function ( ) { return fs . ensureDir ( WEBROOT_PATH_IN_CAPTAIN ) ; } ) . then ( function ( ) { return dockerApi . isServiceRunningByName ( CaptainConstants . certbotServiceName ) ; } ) . then ( function ( isRunning ) { if ( isRunning ) { Logger . d ( 'Captain Certbot is already running.. ' ) ; return dockerApi . getNodeIdByServiceName ( CaptainConstants . certbotServiceName , 0 ) ; } else { Logger . d ( 'No Captain Certbot service is running. Creating one...' ) ; return createCertbotServiceOnNode ( myNodeId ) . then ( function ( ) { return myNodeId ; } ) ; } } ) . then ( function ( nodeId ) { if ( nodeId !== myNodeId ) { Logger . d ( 'Captain Certbot is running on a different node. Removing...' ) ; return dockerApi . removeServiceByName ( CaptainConstants . certbotServiceName ) . then ( function ( ) { Logger . d ( 'Waiting for Certbot to be removed...' ) ; return Utils . getDelayedPromise ( 10000 ) ; } ) . then ( function ( ) { return createCertbotServiceOnNode ( myNodeId ) . then ( function ( ) { return true ; } ) ; } ) ; } else { return true ; } } ) . then ( function ( ) { Logger . d ( 'Updating Certbot service...' ) ; return dockerApi . updateService ( CaptainConstants . certbotServiceName , CaptainConstants . certbotImageName , [ { hostPath : CaptainConstants . letsEncryptEtcPath , containerPath : '/etc/letsencrypt' , } , { hostPath : CaptainConstants . letsEncryptLibPath , containerPath : '/var/lib/letsencrypt' , } , { hostPath : WEBROOT_PATH_IN_CAPTAIN , containerPath : WEBROOT_PATH_IN_CERTBOT , } , ] , undefined , undefined , undefined , undefined , undefined , undefined , undefined , undefined , undefined , undefined , undefined , undefined ) ; } ) . then ( function ( ) { return self . ensureAllCurrentlyRegisteredDomainsHaveDirs ( ) ; } ) ; } } export default CertbotManager ;