mirror of
				https://github.com/therootcompany/greenlock.js.git
				synced 2024-11-16 17:29:00 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			282 lines
		
	
	
		
			6.0 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			282 lines
		
	
	
		
			6.0 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| 'use strict';
 | |
| 
 | |
| var U = module.exports;
 | |
| 
 | |
| var promisify = require('util').promisify;
 | |
| //var resolveSoa = promisify(require('dns').resolveSoa);
 | |
| var resolveMx = promisify(require('dns').resolveMx);
 | |
| var punycode = require('punycode');
 | |
| var Keypairs = require('@root/keypairs');
 | |
| // TODO move to @root
 | |
| var certParser = require('cert-info');
 | |
| 
 | |
| U._parseDuration = function(str) {
 | |
| 	if ('number' === typeof str) {
 | |
| 		return str;
 | |
| 	}
 | |
| 
 | |
| 	var pattern = /^(\-?\d+(\.\d+)?)([wdhms]|ms)$/;
 | |
| 	var matches = str.match(pattern);
 | |
| 	if (!matches || !matches[0]) {
 | |
| 		throw new Error('invalid duration string: ' + str);
 | |
| 	}
 | |
| 
 | |
| 	var n = parseInt(matches[1], 10);
 | |
| 	var unit = matches[3];
 | |
| 
 | |
| 	switch (unit) {
 | |
| 		case 'w':
 | |
| 			n *= 7;
 | |
| 		/*falls through*/
 | |
| 		case 'd':
 | |
| 			n *= 24;
 | |
| 		/*falls through*/
 | |
| 		case 'h':
 | |
| 			n *= 60;
 | |
| 		/*falls through*/
 | |
| 		case 'm':
 | |
| 			n *= 60;
 | |
| 		/*falls through*/
 | |
| 		case 's':
 | |
| 			n *= 1000;
 | |
| 		/*falls through*/
 | |
| 		case 'ms':
 | |
| 			n *= 1; // for completeness
 | |
| 	}
 | |
| 
 | |
| 	return n;
 | |
| };
 | |
| 
 | |
| U._encodeName = function(str) {
 | |
| 	return punycode.toASCII(str.toLowerCase(str));
 | |
| };
 | |
| 
 | |
| U._validName = function(str) {
 | |
| 	// A quick check of the 38 and two ½ valid characters
 | |
| 	// 253 char max full domain, including dots
 | |
| 	// 63 char max each label segment
 | |
| 	// Note: * is not allowed, but it's allowable here
 | |
| 	// Note: _ (underscore) is only allowed for "domain names", not "hostnames"
 | |
| 	// Note: - (hyphen) is not allowed as a first character (but a number is)
 | |
| 	return (
 | |
| 		/^(\*\.)?[a-z0-9_\.\-]+$/.test(str) &&
 | |
| 		str.length < 254 &&
 | |
| 		str.split('.').every(function(label) {
 | |
| 			return label.length > 0 && label.length < 64;
 | |
| 		})
 | |
| 	);
 | |
| };
 | |
| 
 | |
| U._validMx = function(email) {
 | |
| 	var host = email.split('@').slice(1)[0];
 | |
| 	// try twice, just because DNS hiccups sometimes
 | |
| 	// Note: we don't care if the domain exists, just that it *can* exist
 | |
| 	return resolveMx(host).catch(function() {
 | |
| 		return U._timeout(1000).then(function() {
 | |
| 			return resolveMx(host);
 | |
| 		});
 | |
| 	});
 | |
| };
 | |
| 
 | |
| // should be called after _validName
 | |
| U._validDomain = function(str) {
 | |
| 	// TODO use @root/dns (currently dns-suite)
 | |
| 	// because node's dns can't read Authority records
 | |
| 	return Promise.resolve(str);
 | |
| 	/*
 | |
| 	// try twice, just because DNS hiccups sometimes
 | |
| 	// Note: we don't care if the domain exists, just that it *can* exist
 | |
| 	return resolveSoa(str).catch(function() {
 | |
| 		return U._timeout(1000).then(function() {
 | |
| 			return resolveSoa(str);
 | |
| 		});
 | |
| 	});
 | |
|   */
 | |
| };
 | |
| 
 | |
| // foo.example.com and *.example.com overlap
 | |
| // should be called after _validName
 | |
| // (which enforces *. or no *)
 | |
| U._uniqueNames = function(altnames) {
 | |
| 	var dups = {};
 | |
| 	var wilds = {};
 | |
| 	if (
 | |
| 		altnames.some(function(w) {
 | |
| 			if ('*.' !== w.slice(0, 2)) {
 | |
| 				return;
 | |
| 			}
 | |
| 			if (wilds[w]) {
 | |
| 				return true;
 | |
| 			}
 | |
| 			wilds[w] = true;
 | |
| 		})
 | |
| 	) {
 | |
| 		return false;
 | |
| 	}
 | |
| 
 | |
| 	return altnames.every(function(name) {
 | |
| 		var w;
 | |
| 		if ('*.' !== name.slice(0, 2)) {
 | |
| 			w =
 | |
| 				'*.' +
 | |
| 				name
 | |
| 					.split('.')
 | |
| 					.slice(1)
 | |
| 					.join('.');
 | |
| 		} else {
 | |
| 			return true;
 | |
| 		}
 | |
| 
 | |
| 		if (!dups[name] && !dups[w]) {
 | |
| 			dups[name] = true;
 | |
| 			return true;
 | |
| 		}
 | |
| 	});
 | |
| };
 | |
| 
 | |
| U._timeout = function(d) {
 | |
| 	return new Promise(function(resolve) {
 | |
| 		setTimeout(resolve, d);
 | |
| 	});
 | |
| };
 | |
| 
 | |
| U._genKeypair = function(keyType) {
 | |
| 	var keyopts;
 | |
| 	var len = parseInt(keyType.replace(/.*?(\d)/, '$1') || 0, 10);
 | |
| 	if (/RSA/.test(keyType)) {
 | |
| 		keyopts = {
 | |
| 			kty: 'RSA',
 | |
| 			modulusLength: len || 2048
 | |
| 		};
 | |
| 	} else if (/^(EC|P\-?\d)/i.test(keyType)) {
 | |
| 		keyopts = {
 | |
| 			kty: 'EC',
 | |
| 			namedCurve: 'P-' + (len || 256)
 | |
| 		};
 | |
| 	} else {
 | |
| 		// TODO put in ./errors.js
 | |
| 		throw new Error('invalid key type: ' + keyType);
 | |
| 	}
 | |
| 
 | |
| 	return Keypairs.generate(keyopts).then(function(pair) {
 | |
| 		return U._jwkToSet(pair.private);
 | |
| 	});
 | |
| };
 | |
| 
 | |
| // TODO use ACME._importKeypair ??
 | |
| U._importKeypair = function(keypair) {
 | |
| 	// this should import all formats equally well:
 | |
| 	// 'object' (JWK), 'string' (private key pem), kp.privateKeyPem, kp.privateKeyJwk
 | |
| 	if (keypair.private || keypair.d) {
 | |
| 		return U._jwkToSet(keypair.private || keypair);
 | |
| 	}
 | |
| 	if (keypair.privateKeyJwk) {
 | |
| 		return U._jwkToSet(keypair.privateKeyJwk);
 | |
| 	}
 | |
| 
 | |
| 	if ('string' !== typeof keypair && !keypair.privateKeyPem) {
 | |
| 		// TODO put in errors
 | |
| 		throw new Error('missing private key');
 | |
| 	}
 | |
| 
 | |
| 	return Keypairs.import({ pem: keypair.privateKeyPem || keypair }).then(
 | |
| 		function(priv) {
 | |
| 			if (!priv.d) {
 | |
| 				throw new Error('missing private key');
 | |
| 			}
 | |
| 			return U._jwkToSet(priv);
 | |
| 		}
 | |
| 	);
 | |
| };
 | |
| 
 | |
| U._jwkToSet = function(jwk) {
 | |
| 	var keypair = {
 | |
| 		privateKeyJwk: jwk
 | |
| 	};
 | |
| 	return Promise.all([
 | |
| 		Keypairs.export({
 | |
| 			jwk: jwk,
 | |
| 			encoding: 'pem'
 | |
| 		}).then(function(pem) {
 | |
| 			keypair.privateKeyPem = pem;
 | |
| 		}),
 | |
| 		Keypairs.export({
 | |
| 			jwk: jwk,
 | |
| 			encoding: 'pem',
 | |
| 			public: true
 | |
| 		}).then(function(pem) {
 | |
| 			keypair.publicKeyPem = pem;
 | |
| 		}),
 | |
| 		Keypairs.publish({
 | |
| 			jwk: jwk
 | |
| 		}).then(function(pub) {
 | |
| 			keypair.publicKeyJwk = pub;
 | |
| 		})
 | |
| 	]).then(function() {
 | |
| 		return keypair;
 | |
| 	});
 | |
| };
 | |
| 
 | |
| U._attachCertInfo = function(results) {
 | |
| 	var certInfo = certParser.info(results.cert);
 | |
| 
 | |
| 	// subject, altnames, issuedAt, expiresAt
 | |
| 	Object.keys(certInfo).forEach(function(key) {
 | |
| 		results[key] = certInfo[key];
 | |
| 	});
 | |
| 
 | |
| 	return results;
 | |
| };
 | |
| 
 | |
| U._certHasDomain = function(certInfo, _domain) {
 | |
| 	var names = (certInfo.altnames || []).slice(0);
 | |
| 	return names.some(function(name) {
 | |
| 		var domain = _domain.toLowerCase();
 | |
| 		name = name.toLowerCase();
 | |
| 		if ('*.' === name.substr(0, 2)) {
 | |
| 			name = name.substr(2);
 | |
| 			domain = domain
 | |
| 				.split('.')
 | |
| 				.slice(1)
 | |
| 				.join('.');
 | |
| 		}
 | |
| 		return name === domain;
 | |
| 	});
 | |
| };
 | |
| 
 | |
| // a bit heavy to be labeled 'utils'... perhaps 'common' would be better?
 | |
| U._getOrCreateKeypair = function(db, subject, query, keyType, mustExist) {
 | |
| 	var exists = false;
 | |
| 	return db
 | |
| 		.checkKeypair(query)
 | |
| 		.then(function(kp) {
 | |
| 			if (kp) {
 | |
| 				exists = true;
 | |
| 				return U._importKeypair(kp);
 | |
| 			}
 | |
| 
 | |
| 			if (mustExist) {
 | |
| 				// TODO put in errors
 | |
| 				throw new Error(
 | |
| 					'required keypair not found: ' +
 | |
| 						(subject || '') +
 | |
| 						' ' +
 | |
| 						JSON.stringify(query)
 | |
| 				);
 | |
| 			}
 | |
| 
 | |
| 			return U._genKeypair(keyType);
 | |
| 		})
 | |
| 		.then(function(keypair) {
 | |
| 			return { exists: exists, keypair: keypair };
 | |
| 		});
 | |
| };
 | |
| 
 | |
| U._getKeypair = function(db, subject, query) {
 | |
| 	return U._getOrCreateKeypair(db, subject, query, '', true).then(function(
 | |
| 		result
 | |
| 	) {
 | |
| 		return result.keypair;
 | |
| 	});
 | |
| };
 |