separate modules
This commit is contained in:
		
							parent
							
								
									24c9884965
								
							
						
					
					
						commit
						64f1b71b4f
					
				
							
								
								
									
										132
									
								
								lib/acme-client.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								lib/acme-client.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,132 @@ | ||||
| /*! | ||||
|  * letiny | ||||
|  * Copyright(c) 2015 Anatol Sommer <anatol@anatol.at> | ||||
|  * Some code used from https://github.com/letsencrypt/boulder/tree/master/test/js
 | ||||
|  * MPL 2.0 | ||||
| */ | ||||
| 
 | ||||
| 'use strict'; | ||||
| 
 | ||||
| var NOOP=function () {}; | ||||
| var log=NOOP; | ||||
| var request=require('request'); | ||||
| var cryptoUtil=require('./crypto-util'); | ||||
| 
 | ||||
| function Acme(privateKey) { | ||||
|   this.privateKey=privateKey; | ||||
|   this.nonces=[]; | ||||
| } | ||||
| 
 | ||||
| Acme.prototype.getNonce=function(url, cb) { | ||||
|   var self=this; | ||||
| 
 | ||||
|   request.head({ | ||||
|     url:url, | ||||
|   }, function(err, res/*, body*/) { | ||||
|     if (err) { | ||||
|       return cb(err); | ||||
|     } | ||||
|     if (res && 'replay-nonce' in res.headers) { | ||||
|       log('Storing nonce: '+res.headers['replay-nonce']); | ||||
|       self.nonces.push(res.headers['replay-nonce']); | ||||
|       cb(); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     cb(new Error('Failed to get nonce for request')); | ||||
|   }); | ||||
| }; | ||||
| 
 | ||||
| Acme.prototype.post=function(url, body, cb) { | ||||
|   var self=this, payload, jws, signed; | ||||
| 
 | ||||
|   if (this.nonces.length===0) { | ||||
|     this.getNonce(url, function(err) { | ||||
|       if (err) { | ||||
|         return cb(err); | ||||
|       } | ||||
|       self.post(url, body, cb); | ||||
|     }); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   log('Using nonce: '+this.nonces[0]); | ||||
|   payload=JSON.stringify(body, null, 2); | ||||
|   jws=cryptoUtil.generateSignature( | ||||
|     this.privateKey, new Buffer(payload), this.nonces.shift() | ||||
|   ); | ||||
|   signed=JSON.stringify(jws, null, 2); | ||||
| 
 | ||||
|   log('Posting to '+url); | ||||
|   log(signed.green); | ||||
|   log('Payload:'+payload.blue); | ||||
| 
 | ||||
|   return request.post({ | ||||
|     url:url, | ||||
|     body:signed, | ||||
|     encoding:null | ||||
|   }, function(err, res, body) { | ||||
|     var parsed; | ||||
| 
 | ||||
|     if (err) { | ||||
|       console.error(err); | ||||
|       return cb(err); | ||||
|     } | ||||
|     if (res) { | ||||
|       log(('HTTP/1.1 '+res.statusCode).yellow); | ||||
|     } | ||||
| 
 | ||||
|     Object.keys(res.headers).forEach(function(key) { | ||||
|       var value, upcased; | ||||
|       value=res.headers[key]; | ||||
|       upcased=key.charAt(0).toUpperCase()+key.slice(1); | ||||
|       log((upcased+': '+value).yellow); | ||||
|     }); | ||||
| 
 | ||||
|     if (body && !body.toString().match(/[^\x00-\x7F]/)) { | ||||
|       try { | ||||
|         parsed=JSON.parse(body); | ||||
|         log(JSON.stringify(parsed, null, 2).cyan); | ||||
|       } catch(err) { | ||||
|         log(body.toString().cyan); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     if ('replay-nonce' in res.headers) { | ||||
|       log('Storing nonce: '+res.headers['replay-nonce']); | ||||
|       self.nonces.push(res.headers['replay-nonce']); | ||||
|     } | ||||
| 
 | ||||
|     cb(err, res, body); | ||||
|   }); | ||||
| }; | ||||
| 
 | ||||
| Acme.parseLink = function parseLink(link) { | ||||
|   var links; | ||||
|   try { | ||||
|     links=link.split(',').map(function(link) { | ||||
|       var parts, url, info; | ||||
|       parts=link.trim().split(';'); | ||||
|       url=parts.shift().replace(/[<>]/g, ''); | ||||
|       info=parts.reduce(function(acc, p) { | ||||
|         var m=p.trim().match(/(.+) *= *"(.+)"/); | ||||
|         if (m) { | ||||
|           acc[m[1]]=m[2]; | ||||
|         } | ||||
|         return acc; | ||||
|       }, {}); | ||||
|       info.url=url; | ||||
|       return info; | ||||
|     }).reduce(function(acc, link) { | ||||
|       if ('rel' in link) { | ||||
|         acc[link.rel]=link.url; | ||||
|       } | ||||
|       return acc; | ||||
|     }, {}); | ||||
|     return links; | ||||
|   } catch(err) { | ||||
|     return null; | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| module.exports = Acme; | ||||
							
								
								
									
										473
									
								
								lib/client.js
									
									
									
									
									
								
							
							
						
						
									
										473
									
								
								lib/client.js
									
									
									
									
									
								
							| @ -7,473 +7,8 @@ | ||||
| 
 | ||||
| 'use strict'; | ||||
| 
 | ||||
| var _DEBUG, NOOP=new Function(), log=NOOP, | ||||
|   mkdirp=require('mkdirp').sync, request=require('request'), | ||||
|   forge=require('node-forge'), pki=forge.pki, | ||||
|   cryptoUtil=require('./crypto-util'), util=require('./acme-util'), | ||||
|   fs=require('fs'), path=require('path'), child=require('child_process'); | ||||
| 
 | ||||
| function Acme(privateKey) { | ||||
|   this.privateKey=privateKey; | ||||
|   this.nonces=[]; | ||||
| } | ||||
| 
 | ||||
| Acme.prototype.getNonce=function(url, cb) { | ||||
|   var self=this; | ||||
| 
 | ||||
|   request.head({ | ||||
|     url:url, | ||||
|   }, function(err, res, body) { | ||||
|     if (err) { | ||||
|       return cb(err); | ||||
|     } | ||||
|     if (res && 'replay-nonce' in res.headers) { | ||||
|       log('Storing nonce: '+res.headers['replay-nonce']); | ||||
|       self.nonces.push(res.headers['replay-nonce']); | ||||
|       cb(); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     cb(new Error('Failed to get nonce for request')); | ||||
|   }); | ||||
| exports.Acme = require('acme-client'); | ||||
| exports.registerNewAccount = require('register-new-account'); | ||||
| exports.getCertificate = require('get-certificate'); | ||||
| exports.getCert=function (options) { | ||||
| }; | ||||
| 
 | ||||
| Acme.prototype.post=function(url, body, cb) { | ||||
|   var self=this, payload, jws, signed; | ||||
| 
 | ||||
|   if (this.nonces.length===0) { | ||||
|     this.getNonce(url, function(err) { | ||||
|       if (err) { | ||||
|         return cb(err); | ||||
|       } | ||||
|       self.post(url, body, cb); | ||||
|     }); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   log('Using nonce: '+this.nonces[0]); | ||||
|   payload=JSON.stringify(body, null, 2); | ||||
|   jws=cryptoUtil.generateSignature( | ||||
|     this.privateKey, new Buffer(payload), this.nonces.shift() | ||||
|   ); | ||||
|   signed=JSON.stringify(jws, null, 2); | ||||
| 
 | ||||
|   log('Posting to '+url); | ||||
|   log(signed.green); | ||||
|   log('Payload:'+payload.blue); | ||||
| 
 | ||||
|   return request.post({ | ||||
|     url:url, | ||||
|     body:signed, | ||||
|     encoding:null | ||||
|   }, function(err, res, body) { | ||||
|     var parsed; | ||||
| 
 | ||||
|     if (err) { | ||||
|       console.error(err); | ||||
|       return cb(err); | ||||
|     } | ||||
|     if (res) { | ||||
|       log(('HTTP/1.1 '+res.statusCode).yellow); | ||||
|     } | ||||
| 
 | ||||
|     Object.keys(res.headers).forEach(function(key) { | ||||
|       var value, upcased; | ||||
|       value=res.headers[key]; | ||||
|       upcased=key.charAt(0).toUpperCase()+key.slice(1); | ||||
|       log((upcased+': '+value).yellow); | ||||
|     }); | ||||
| 
 | ||||
|     if (body && !body.toString().match(/[^\x00-\x7F]/)) { | ||||
|       try { | ||||
|         parsed=JSON.parse(body); | ||||
|         log(JSON.stringify(parsed, null, 2).cyan); | ||||
|       } catch(err) { | ||||
|         log(body.toString().cyan); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     if ('replay-nonce' in res.headers) { | ||||
|       log('Storing nonce: '+res.headers['replay-nonce']); | ||||
|       self.nonces.push(res.headers['replay-nonce']); | ||||
|     } | ||||
| 
 | ||||
|     cb(err, res, body); | ||||
|   }); | ||||
| }; | ||||
| 
 | ||||
| 
 | ||||
| function registerNewAccount(options, cb) { | ||||
|   var state = {}; | ||||
| 
 | ||||
|   if (!options.accountPrivateKeyPem) { | ||||
|     return handleErr(new Error("options.accountPrivateKeyPem must be an ascii private key pem")); | ||||
|   } | ||||
|   if (!options.agreeToTerms) { | ||||
|     cb(new Error("options.agreeToTerms must be function (tosUrl, fn => (err, true))")); | ||||
|     return; | ||||
|   } | ||||
|   if (!options.newReg) { | ||||
|     cb(new Error("options.newReg must be the a new registration url")); | ||||
|     return; | ||||
|   } | ||||
|   if (!options.email) { | ||||
|     cb(new Error("options.email must be an email")); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   state.accountKeyPem=options.accountPrivateKeyPem; | ||||
|   state.accountKeyPair=cryptoUtil.importPemPrivateKey(state.accountKeyPem); | ||||
|   state.acme=new Acme(state.accountKeyPair); | ||||
| 
 | ||||
|   register(); | ||||
| 
 | ||||
|   function register() { | ||||
|     state.acme.post(options.newReg, { | ||||
|       resource:'new-reg', | ||||
|       contact:['mailto:'+options.email] | ||||
|     }, getTerms); | ||||
|   } | ||||
| 
 | ||||
|   function getTerms(err, res) { | ||||
|     var links; | ||||
| 
 | ||||
|     if (err || Math.floor(res.statusCode/100)!==2) { | ||||
|       return handleErr(err, 'Registration request failed: ' + res.body.toString('utf8')); | ||||
|     } | ||||
| 
 | ||||
|     links=parseLink(res.headers['link']); | ||||
|     if (!links || !('next' in links)) { | ||||
|       return handleErr(err, 'Server didn\'t provide information to proceed (1)'); | ||||
|     } | ||||
| 
 | ||||
|     state.registrationURL=res.headers['location']; | ||||
|     state.newAuthorizationURL=links['next']; | ||||
|     state.termsRequired=('terms-of-service' in links); | ||||
| 
 | ||||
|     if (state.termsRequired) { | ||||
|       state.termsURL=links['terms-of-service']; | ||||
|       options.agreeToTerms(state.termsURL, function (err, agree) { | ||||
|         if (err) { | ||||
|           return handleErr(err); | ||||
|         } | ||||
|         if (!agree) { | ||||
|           return handleErr(new Error("You must agree to the terms of use at '" + state.termsURL + "'")); | ||||
|         } | ||||
| 
 | ||||
|         state.agreeTerms = agree; | ||||
|         state.termsURL=links['terms-of-service']; | ||||
|         log(state.termsURL); | ||||
|         request.get(state.termsURL, getAgreement); | ||||
|       }); | ||||
|     } else { | ||||
|       cb(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   function getAgreement(err/*, res, body*/) { | ||||
|     if (err) { | ||||
|       return handleErr(err, 'Couldn\'t get agreement'); | ||||
|     } | ||||
|     log('The CA requires your agreement to terms:\n'+state.termsURL); | ||||
|     sendAgreement(); | ||||
|   } | ||||
| 
 | ||||
|   function sendAgreement() { | ||||
|     if (state.termsRequired && !state.agreeTerms) { | ||||
|       return handleErr(null, 'The CA requires your agreement to terms: '+state.termsURL); | ||||
|     } | ||||
| 
 | ||||
|     log('Posting agreement to: '+state.registrationURL); | ||||
| 
 | ||||
|     state.acme.post(state.registrationURL, { | ||||
|       resource:'reg', | ||||
|       agreement:state.termsURL | ||||
|     }, function(err, res, body) { | ||||
|       if (err || Math.floor(res.statusCode/100)!==2) { | ||||
|         return handleErr(err, 'Couldn\'t POST agreement back to server', body); | ||||
|       } else { | ||||
|         cb(null, body); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   function handleErr(err, text, info) { | ||||
|     log(text, err, info); | ||||
|     cb(err || new Error(text)); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function getCert(options, cb) { | ||||
|   var state={ | ||||
|     validatedDomains:[], | ||||
|     validAuthorizationURLs:[] | ||||
|   }; | ||||
| 
 | ||||
|   if (!options.accountPrivateKeyPem) { | ||||
|     return handleErr(new Error("options.accountPrivateKeyPem must be an ascii private key pem")); | ||||
|   } | ||||
|   if (!options.domainPrivateKeyPem) { | ||||
|     return handleErr(new Error("options.domainPrivateKeyPem must be an ascii private key pem")); | ||||
|   } | ||||
|   if (!options.setChallenge) { | ||||
|     return handleErr(new Error("options.setChallenge must be function(hostname, challengeKey, tokenValue, done) {}")); | ||||
|   } | ||||
|   if (!options.removeChallenge) { | ||||
|     return handleErr(new Error("options.removeChallenge must be function(hostname, challengeKey, done) {}")); | ||||
|   } | ||||
|   if (!(options.domains && options.domains.length)) { | ||||
|     return handleErr(new Error("options.domains must be an array of domains such as ['example.com', 'www.example.com']")); | ||||
|   } | ||||
| 
 | ||||
|   state.domains = options.domains.slice(0); // copy array
 | ||||
|   try { | ||||
|     state.accountKeyPem=options.accountPrivateKeyPem; | ||||
|     state.accountKeyPair=cryptoUtil.importPemPrivateKey(state.accountKeyPem); | ||||
|     state.acme=new Acme(state.accountKeyPair); | ||||
|     state.certPrivateKeyPem=options.domainPrivateKeyPem; | ||||
|     state.certPrivateKey=cryptoUtil.importPemPrivateKey(state.certPrivateKeyPem); | ||||
|   } catch(err) { | ||||
|     return handleErr(err, 'Failed to parse privateKey'); | ||||
|   } | ||||
| 
 | ||||
|   nextDomain(); | ||||
| 
 | ||||
|   function nextDomain() { | ||||
|     if (state.domains.length > 0) { | ||||
|       getChallenges(state.domains.shift()); | ||||
|       return; | ||||
|     } else { | ||||
|       getCertificate(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   function getChallenges(domain) { | ||||
|     state.domain=domain; | ||||
| 
 | ||||
|     state.acme.post(state.newAuthorizationURL, { | ||||
|       resource:'new-authz', | ||||
|       identifier:{ | ||||
|         type:'dns', | ||||
|         value:state.domain, | ||||
|       } | ||||
|     }, getReadyToValidate); | ||||
|   } | ||||
| 
 | ||||
|   function getReadyToValidate(err, res, body) { | ||||
|     var links, authz, httpChallenges, challenge, thumbprint, keyAuthorization, challengePath; | ||||
| 
 | ||||
|     if (err || Math.floor(res.statusCode/100)!==2) { | ||||
|       return handleErr(err, 'Authorization request failed ('+res.statusCode+')'); | ||||
|     } | ||||
| 
 | ||||
|     links=parseLink(res.headers['link']); | ||||
|     if (!links || !('next' in links)) { | ||||
|       return handleErr(err, 'Server didn\'t provide information to proceed (2)'); | ||||
|     } | ||||
| 
 | ||||
|     state.authorizationURL=res.headers['location']; | ||||
|     state.newCertificateURL=links['next']; | ||||
| 
 | ||||
|     authz=JSON.parse(body); | ||||
| 
 | ||||
|     httpChallenges=authz.challenges.filter(function(x) { | ||||
|       return x.type==='http-01'; | ||||
|     }); | ||||
|     if (httpChallenges.length===0) { | ||||
|       return handleErr(null, 'Server didn\'t offer any challenge we can handle.'); | ||||
|     } | ||||
|     challenge=httpChallenges[0]; | ||||
| 
 | ||||
|     thumbprint=cryptoUtil.thumbprint(state.accountKeyPair.publicKey); | ||||
|     keyAuthorization=challenge.token+'.'+thumbprint; | ||||
|     challengePath='.well-known/acme-challenge/'+challenge.token; | ||||
|     state.responseURL=challenge['uri']; | ||||
|     state.path=challengePath; | ||||
| 
 | ||||
|     options.setChallenge(state.domain, '/'+challengePath, keyAuthorization, challengeDone); | ||||
| 
 | ||||
|     function challengeDone() { | ||||
|       state.acme.post(state.responseURL, { | ||||
|         resource:'challenge', | ||||
|         keyAuthorization:keyAuthorization | ||||
|       }, function(err, res, body) { | ||||
|         ensureValidation(err, res, body, function unlink() { | ||||
|           options.removeChallenge(state.domain, '/'+challengePath, function () { | ||||
|             // ignore
 | ||||
|           }); | ||||
|         }); | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   function ensureValidation(err, res, body, unlink) { | ||||
|     var authz; | ||||
| 
 | ||||
|     if (err || Math.floor(res.statusCode/100)!==2) { | ||||
|       unlink(); | ||||
|       return handleErr(err, 'Authorization status request failed ('+res.statusCode+')'); | ||||
|     } | ||||
| 
 | ||||
|     authz=JSON.parse(body); | ||||
| 
 | ||||
|     if (authz.status==='pending') { | ||||
|       setTimeout(function() { | ||||
|         request.get(state.authorizationURL, {}, function(err, res, body) { | ||||
|           ensureValidation(err, res, body, unlink); | ||||
|         }); | ||||
|       }, 1000); | ||||
|     } else if (authz.status==='valid') { | ||||
|       log('Validating domain ... done'); | ||||
|       state.validatedDomains.push(state.domain); | ||||
|       state.validAuthorizationURLs.push(state.authorizationURL); | ||||
|       unlink(); | ||||
|       nextDomain(); | ||||
|     } else if (authz.status==='invalid') { | ||||
|       unlink(); | ||||
|       return handleErr(null, 'The CA was unable to validate the file you provisioned', body); | ||||
|     } else { | ||||
|       unlink(); | ||||
|       return handleErr(null, 'CA returned an authorization in an unexpected state', authz); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   function getCertificate() { | ||||
|     var csr=cryptoUtil.generateCSR(state.certPrivateKey, state.validatedDomains); | ||||
|     log('Requesting certificate...'); | ||||
|     state.acme.post(state.newCertificateURL, { | ||||
|       resource:'new-cert', | ||||
|       csr:csr, | ||||
|       authorizations:state.validAuthorizationURLs | ||||
|     }, downloadCertificate); | ||||
|   } | ||||
| 
 | ||||
|   function downloadCertificate(err, res, body) { | ||||
|     var links, certURL; | ||||
| 
 | ||||
|     if (err || Math.floor(res.statusCode/100)!==2) { | ||||
|       log('Certificate request failed with error ', err); | ||||
|       if (body) { | ||||
|         log(body.toString()); | ||||
|       } | ||||
|       return handleErr(err, 'Certificate request failed'); | ||||
|     } | ||||
| 
 | ||||
|     links=parseLink(res.headers['link']); | ||||
|     if (!links || !('up' in links)) { | ||||
|       return handleErr(err, 'Failed to fetch issuer certificate'); | ||||
|     } | ||||
| 
 | ||||
|     log('Requesting certificate: done'); | ||||
| 
 | ||||
|     state.certificate=body; | ||||
|     certURL=res.headers['location']; | ||||
|     request.get({ | ||||
|       url:certURL, | ||||
|       encoding:null | ||||
|     }, function(err, res, body) { | ||||
|       if (err) { | ||||
|         return handleErr(err, 'Failed to fetch cert from '+certURL); | ||||
|       } | ||||
|       if (res.statusCode!==200) { | ||||
|         return handleErr(err, 'Failed to fetch cert from '+certURL, res.body.toString()); | ||||
|       } | ||||
|       if (body.toString()!==state.certificate.toString()) { | ||||
|         handleErr(null, 'Cert at '+certURL+' did not match returned cert'); | ||||
|       } else { | ||||
|         log('Successfully verified cert at '+certURL); | ||||
|         log('Requesting issuer certificate...'); | ||||
|         request.get({ | ||||
|           url:links['up'], | ||||
|           encoding:null | ||||
|         }, function(err, res, body) { | ||||
|           if (err || res.statusCode!==200) { | ||||
|             return handleErr(err, 'Failed to fetch issuer certificate'); | ||||
|           } | ||||
|           state.caCert=certBufferToPEM(body); | ||||
|           log('Requesting issuer certificate: done'); | ||||
|           done(); | ||||
|         }); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   function done() { | ||||
|     var cert; | ||||
| 
 | ||||
|     try { | ||||
|       cert=certBufferToPEM(state.certificate); | ||||
|     } catch(e) { | ||||
|       console.error(e.stack); | ||||
|       //cb(new Error("Could not write output files. Please check permissions!"));
 | ||||
|       handleErr(e, 'Could not write output files. Please check permissions!'); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     cb(null, { | ||||
|       cert: cert | ||||
|     , key: state.certPrivateKeyPem | ||||
|     , ca: state.caCert | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   function handleErr(err, text, info) { | ||||
|     log(text, err, info); | ||||
|     cb(err || new Error(text)); | ||||
|   } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| function certBufferToPEM(cert) { | ||||
|   cert=util.toStandardB64(cert.toString('base64')); | ||||
|   cert=cert.match(/.{1,64}/g).join('\n'); | ||||
|   return '-----BEGIN CERTIFICATE-----\n'+cert+'\n-----END CERTIFICATE-----'; | ||||
| } | ||||
| 
 | ||||
| function parseLink(link) { | ||||
|   var links; | ||||
|   try { | ||||
|     links=link.split(',').map(function(link) { | ||||
|       var parts, url, info; | ||||
|       parts=link.trim().split(';'); | ||||
|       url=parts.shift().replace(/[<>]/g, ''); | ||||
|       info=parts.reduce(function(acc, p) { | ||||
|         var m=p.trim().match(/(.+) *= *"(.+)"/); | ||||
|         if (m) { | ||||
|           acc[m[1]]=m[2]; | ||||
|         } | ||||
|         return acc; | ||||
|       }, {}); | ||||
|       info['url']=url; | ||||
|       return info; | ||||
|     }).reduce(function(acc, link) { | ||||
|       if ('rel' in link) { | ||||
|         acc[link['rel']]=link['url']; | ||||
|       } | ||||
|       return acc; | ||||
|     }, {}); | ||||
|     return links; | ||||
|   } catch(err) { | ||||
|     return null; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| if (~process.argv.indexOf('--letiny-fork')) { | ||||
|   process.on('message', function(msg) { | ||||
|     if (msg.request) { | ||||
|       getCert(msg.request.options, function(err, cert, key, ca) { | ||||
|         process.send({ | ||||
|           result:{ | ||||
|             err:err ? err.stack : null, | ||||
|             cert:cert, | ||||
|             key:key, | ||||
|             ca:ca | ||||
|           } | ||||
|         }); | ||||
|       }); | ||||
|     } | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| exports.registerNewAccount=registerNewAccount; | ||||
| exports.getCert=getCert; | ||||
|  | ||||
							
								
								
									
										16
									
								
								lib/fork-old.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								lib/fork-old.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,16 @@ | ||||
| if (~process.argv.indexOf('--letiny-fork')) { | ||||
|   process.on('message', function(msg) { | ||||
|     if (msg.request) { | ||||
|       getCert(msg.request.options, function(err, cert, key, ca) { | ||||
|         process.send({ | ||||
|           result:{ | ||||
|             err:err ? err.stack : null, | ||||
|             cert:cert, | ||||
|             key:key, | ||||
|             ca:ca | ||||
|           } | ||||
|         }); | ||||
|       }); | ||||
|     } | ||||
|   }); | ||||
| } | ||||
							
								
								
									
										243
									
								
								lib/get-certificate.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										243
									
								
								lib/get-certificate.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,243 @@ | ||||
| /*! | ||||
|  * letiny | ||||
|  * Copyright(c) 2015 Anatol Sommer <anatol@anatol.at> | ||||
|  * Some code used from https://github.com/letsencrypt/boulder/tree/master/test/js
 | ||||
|  * MPL 2.0 | ||||
| */ | ||||
| 
 | ||||
| 'use strict'; | ||||
| 
 | ||||
| var NOOP=function () {}, log=NOOP; | ||||
| var request=require('request'); | ||||
| var util=require('./acme-util'); | ||||
| var cryptoUtil=require('./crypto-util'); | ||||
| 
 | ||||
| var Acme = require('./acme-client'); | ||||
| 
 | ||||
| function getCert(options, cb) { | ||||
|   var state={ | ||||
|     validatedDomains:[], | ||||
|     validAuthorizationURLs:[] | ||||
|   }; | ||||
| 
 | ||||
|   if (!options.accountPrivateKeyPem) { | ||||
|     return handleErr(new Error("options.accountPrivateKeyPem must be an ascii private key pem")); | ||||
|   } | ||||
|   if (!options.domainPrivateKeyPem) { | ||||
|     return handleErr(new Error("options.domainPrivateKeyPem must be an ascii private key pem")); | ||||
|   } | ||||
|   if (!options.setChallenge) { | ||||
|     return handleErr(new Error("options.setChallenge must be function(hostname, challengeKey, tokenValue, done) {}")); | ||||
|   } | ||||
|   if (!options.removeChallenge) { | ||||
|     return handleErr(new Error("options.removeChallenge must be function(hostname, challengeKey, done) {}")); | ||||
|   } | ||||
|   if (!(options.domains && options.domains.length)) { | ||||
|     return handleErr(new Error("options.domains must be an array of domains such as ['example.com', 'www.example.com']")); | ||||
|   } | ||||
| 
 | ||||
|   state.domains = options.domains.slice(0); // copy array
 | ||||
|   try { | ||||
|     state.accountKeyPem=options.accountPrivateKeyPem; | ||||
|     state.accountKeyPair=cryptoUtil.importPemPrivateKey(state.accountKeyPem); | ||||
|     state.acme=new Acme(state.accountKeyPair); | ||||
|     state.certPrivateKeyPem=options.domainPrivateKeyPem; | ||||
|     state.certPrivateKey=cryptoUtil.importPemPrivateKey(state.certPrivateKeyPem); | ||||
|   } catch(err) { | ||||
|     return handleErr(err, 'Failed to parse privateKey'); | ||||
|   } | ||||
| 
 | ||||
|   nextDomain(); | ||||
| 
 | ||||
|   function nextDomain() { | ||||
|     if (state.domains.length > 0) { | ||||
|       getChallenges(state.domains.shift()); | ||||
|       return; | ||||
|     } else { | ||||
|       getCertificate(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   function getChallenges(domain) { | ||||
|     state.domain=domain; | ||||
| 
 | ||||
|     state.acme.post(state.newAuthorizationURL, { | ||||
|       resource:'new-authz', | ||||
|       identifier:{ | ||||
|         type:'dns', | ||||
|         value:state.domain, | ||||
|       } | ||||
|     }, getReadyToValidate); | ||||
|   } | ||||
| 
 | ||||
|   function getReadyToValidate(err, res, body) { | ||||
|     var links, authz, httpChallenges, challenge, thumbprint, keyAuthorization, challengePath; | ||||
| 
 | ||||
|     if (err || Math.floor(res.statusCode/100)!==2) { | ||||
|       return handleErr(err, 'Authorization request failed ('+res.statusCode+')'); | ||||
|     } | ||||
| 
 | ||||
|     links=Acme.parseLink(res.headers.link); | ||||
|     if (!links || !('next' in links)) { | ||||
|       return handleErr(err, 'Server didn\'t provide information to proceed (2)'); | ||||
|     } | ||||
| 
 | ||||
|     state.authorizationURL=res.headers.location; | ||||
|     state.newCertificateURL=links.next; | ||||
| 
 | ||||
|     authz=JSON.parse(body); | ||||
| 
 | ||||
|     httpChallenges=authz.challenges.filter(function(x) { | ||||
|       return x.type==='http-01'; | ||||
|     }); | ||||
|     if (httpChallenges.length===0) { | ||||
|       return handleErr(null, 'Server didn\'t offer any challenge we can handle.'); | ||||
|     } | ||||
|     challenge=httpChallenges[0]; | ||||
| 
 | ||||
|     thumbprint=cryptoUtil.thumbprint(state.accountKeyPair.publicKey); | ||||
|     keyAuthorization=challenge.token+'.'+thumbprint; | ||||
|     challengePath='.well-known/acme-challenge/'+challenge.token; | ||||
|     state.responseURL=challenge.uri; | ||||
|     state.path=challengePath; | ||||
| 
 | ||||
|     options.setChallenge(state.domain, '/'+challengePath, keyAuthorization, challengeDone); | ||||
| 
 | ||||
|     function challengeDone() { | ||||
|       state.acme.post(state.responseURL, { | ||||
|         resource:'challenge', | ||||
|         keyAuthorization:keyAuthorization | ||||
|       }, function(err, res, body) { | ||||
|         ensureValidation(err, res, body, function unlink() { | ||||
|           options.removeChallenge(state.domain, '/'+challengePath, function () { | ||||
|             // ignore
 | ||||
|           }); | ||||
|         }); | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   function ensureValidation(err, res, body, unlink) { | ||||
|     var authz; | ||||
| 
 | ||||
|     if (err || Math.floor(res.statusCode/100)!==2) { | ||||
|       unlink(); | ||||
|       return handleErr(err, 'Authorization status request failed ('+res.statusCode+')'); | ||||
|     } | ||||
| 
 | ||||
|     authz=JSON.parse(body); | ||||
| 
 | ||||
|     if (authz.status==='pending') { | ||||
|       setTimeout(function() { | ||||
|         request.get(state.authorizationURL, {}, function(err, res, body) { | ||||
|           ensureValidation(err, res, body, unlink); | ||||
|         }); | ||||
|       }, 1000); | ||||
|     } else if (authz.status==='valid') { | ||||
|       log('Validating domain ... done'); | ||||
|       state.validatedDomains.push(state.domain); | ||||
|       state.validAuthorizationURLs.push(state.authorizationURL); | ||||
|       unlink(); | ||||
|       nextDomain(); | ||||
|     } else if (authz.status==='invalid') { | ||||
|       unlink(); | ||||
|       return handleErr(null, 'The CA was unable to validate the file you provisioned', body); | ||||
|     } else { | ||||
|       unlink(); | ||||
|       return handleErr(null, 'CA returned an authorization in an unexpected state', authz); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   function getCertificate() { | ||||
|     var csr=cryptoUtil.generateCSR(state.certPrivateKey, state.validatedDomains); | ||||
|     log('Requesting certificate...'); | ||||
|     state.acme.post(state.newCertificateURL, { | ||||
|       resource:'new-cert', | ||||
|       csr:csr, | ||||
|       authorizations:state.validAuthorizationURLs | ||||
|     }, downloadCertificate); | ||||
|   } | ||||
| 
 | ||||
|   function downloadCertificate(err, res, body) { | ||||
|     var links, certURL; | ||||
| 
 | ||||
|     if (err || Math.floor(res.statusCode/100)!==2) { | ||||
|       log('Certificate request failed with error ', err); | ||||
|       if (body) { | ||||
|         log(body.toString()); | ||||
|       } | ||||
|       return handleErr(err, 'Certificate request failed'); | ||||
|     } | ||||
| 
 | ||||
|     links=Acme.parseLink(res.headers.link); | ||||
|     if (!links || !('up' in links)) { | ||||
|       return handleErr(err, 'Failed to fetch issuer certificate'); | ||||
|     } | ||||
| 
 | ||||
|     log('Requesting certificate: done'); | ||||
| 
 | ||||
|     state.certificate=body; | ||||
|     certURL=res.headers.location; | ||||
|     request.get({ | ||||
|       url:certURL, | ||||
|       encoding:null | ||||
|     }, function(err, res, body) { | ||||
|       if (err) { | ||||
|         return handleErr(err, 'Failed to fetch cert from '+certURL); | ||||
|       } | ||||
|       if (res.statusCode!==200) { | ||||
|         return handleErr(err, 'Failed to fetch cert from '+certURL, res.body.toString()); | ||||
|       } | ||||
|       if (body.toString()!==state.certificate.toString()) { | ||||
|         handleErr(null, 'Cert at '+certURL+' did not match returned cert'); | ||||
|       } else { | ||||
|         log('Successfully verified cert at '+certURL); | ||||
|         log('Requesting issuer certificate...'); | ||||
|         request.get({ | ||||
|           url:links.up, | ||||
|           encoding:null | ||||
|         }, function(err, res, body) { | ||||
|           if (err || res.statusCode!==200) { | ||||
|             return handleErr(err, 'Failed to fetch issuer certificate'); | ||||
|           } | ||||
|           state.caCert=certBufferToPem(body); | ||||
|           log('Requesting issuer certificate: done'); | ||||
|           done(); | ||||
|         }); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   function done() { | ||||
|     var cert; | ||||
| 
 | ||||
|     try { | ||||
|       cert=certBufferToPem(state.certificate); | ||||
|     } catch(e) { | ||||
|       console.error(e.stack); | ||||
|       //cb(new Error("Could not write output files. Please check permissions!"));
 | ||||
|       handleErr(e, 'Could not write output files. Please check permissions!'); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     cb(null, { | ||||
|       cert: cert | ||||
|     , key: state.certPrivateKeyPem | ||||
|     , ca: state.caCert | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   function handleErr(err, text, info) { | ||||
|     log(text, err, info); | ||||
|     cb(err || new Error(text)); | ||||
|   } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| function certBufferToPem(cert) { | ||||
|   cert=util.toStandardB64(cert.toString('base64')); | ||||
|   cert=cert.match(/.{1,64}/g).join('\n'); | ||||
|   return '-----BEGIN CERTIFICATE-----\n'+cert+'\n-----END CERTIFICATE-----'; | ||||
| } | ||||
| 
 | ||||
| module.exports = getCert; | ||||
							
								
								
									
										116
									
								
								lib/register-new-account.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								lib/register-new-account.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,116 @@ | ||||
| /*! | ||||
|  * letiny | ||||
|  * Copyright(c) 2015 Anatol Sommer <anatol@anatol.at> | ||||
|  * Some code used from https://github.com/letsencrypt/boulder/tree/master/test/js
 | ||||
|  * MPL 2.0 | ||||
| */ | ||||
| 
 | ||||
| 'use strict'; | ||||
| 
 | ||||
| var NOOP=function () {}, log=NOOP; | ||||
| var request=require('request'); | ||||
| var cryptoUtil=require('./crypto-util'); | ||||
| var Acme = require('./acme-client'); | ||||
| 
 | ||||
| function registerNewAccount(options, cb) { | ||||
|   var state = {}; | ||||
| 
 | ||||
|   if (!options.accountPrivateKeyPem) { | ||||
|     return handleErr(new Error("options.accountPrivateKeyPem must be an ascii private key pem")); | ||||
|   } | ||||
|   if (!options.agreeToTerms) { | ||||
|     cb(new Error("options.agreeToTerms must be function (tosUrl, fn => (err, true))")); | ||||
|     return; | ||||
|   } | ||||
|   if (!options.newReg) { | ||||
|     cb(new Error("options.newReg must be the a new registration url")); | ||||
|     return; | ||||
|   } | ||||
|   if (!options.email) { | ||||
|     cb(new Error("options.email must be an email")); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   state.accountKeyPem=options.accountPrivateKeyPem; | ||||
|   state.accountKeyPair=cryptoUtil.importPemPrivateKey(state.accountKeyPem); | ||||
|   state.acme=new Acme(state.accountKeyPair); | ||||
| 
 | ||||
|   register(); | ||||
| 
 | ||||
|   function register() { | ||||
|     state.acme.post(options.newReg, { | ||||
|       resource:'new-reg', | ||||
|       contact:['mailto:'+options.email] | ||||
|     }, getTerms); | ||||
|   } | ||||
| 
 | ||||
|   function getTerms(err, res) { | ||||
|     var links; | ||||
| 
 | ||||
|     if (err || Math.floor(res.statusCode/100)!==2) { | ||||
|       return handleErr(err, 'Registration request failed: ' + res.body.toString('utf8')); | ||||
|     } | ||||
| 
 | ||||
|     links=Acme.parseLink(res.headers.link); | ||||
|     if (!links || !('next' in links)) { | ||||
|       return handleErr(err, 'Server didn\'t provide information to proceed (1)'); | ||||
|     } | ||||
| 
 | ||||
|     state.registrationURL=res.headers.location; | ||||
|     state.newAuthorizationURL=links.next; | ||||
|     state.termsRequired=('terms-of-service' in links); | ||||
| 
 | ||||
|     if (state.termsRequired) { | ||||
|       state.termsURL=links['terms-of-service']; | ||||
|       options.agreeToTerms(state.termsURL, function (err, agree) { | ||||
|         if (err) { | ||||
|           return handleErr(err); | ||||
|         } | ||||
|         if (!agree) { | ||||
|           return handleErr(new Error("You must agree to the terms of use at '" + state.termsURL + "'")); | ||||
|         } | ||||
| 
 | ||||
|         state.agreeTerms = agree; | ||||
|         state.termsURL=links['terms-of-service']; | ||||
|         log(state.termsURL); | ||||
|         request.get(state.termsURL, getAgreement); | ||||
|       }); | ||||
|     } else { | ||||
|       cb(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   function getAgreement(err/*, res, body*/) { | ||||
|     if (err) { | ||||
|       return handleErr(err, 'Couldn\'t get agreement'); | ||||
|     } | ||||
|     log('The CA requires your agreement to terms:\n'+state.termsURL); | ||||
|     sendAgreement(); | ||||
|   } | ||||
| 
 | ||||
|   function sendAgreement() { | ||||
|     if (state.termsRequired && !state.agreeTerms) { | ||||
|       return handleErr(null, 'The CA requires your agreement to terms: '+state.termsURL); | ||||
|     } | ||||
| 
 | ||||
|     log('Posting agreement to: '+state.registrationURL); | ||||
| 
 | ||||
|     state.acme.post(state.registrationURL, { | ||||
|       resource:'reg', | ||||
|       agreement:state.termsURL | ||||
|     }, function(err, res, body) { | ||||
|       if (err || Math.floor(res.statusCode/100)!==2) { | ||||
|         return handleErr(err, 'Couldn\'t POST agreement back to server', body); | ||||
|       } else { | ||||
|         cb(null, body); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   function handleErr(err, text, info) { | ||||
|     log(text, err, info); | ||||
|     cb(err || new Error(text)); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| module.exports = registerNewAccount; | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user