Skip to content

Commit

Permalink
Merge pull request #44 from mandatoryprogrammer/fix-proxyauth-errors
Browse files Browse the repository at this point in the history
Fixes some issues with certificate generation causing requests to fai…
  • Loading branch information
mandatoryprogrammer authored Nov 6, 2021
2 parents e88fd21 + f8b97a4 commit 17b0031
Show file tree
Hide file tree
Showing 10 changed files with 480 additions and 13 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ node_modules/*
.DS_Store
*.key
*.crt
*.env
dev.sh
2 changes: 1 addition & 1 deletion anyproxy/lib/certMgr.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use strict'

const EasyCert = require('node-easy-cert');
const EasyCert = require('./node-easy-cert/index.js');
const co = require('co');
const os = require('os');
const inquirer = require('inquirer');
Expand Down
126 changes: 126 additions & 0 deletions anyproxy/lib/node-easy-cert/certGenerator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
'use strict'

const forge = require('node-forge');
const Util = require('./util');

let defaultAttrs = [
{ name: 'countryName', value: 'CN' },
{ name: 'organizationName', value: 'EasyCert' },
{ shortName: 'ST', value: 'SH' },
{ shortName: 'OU', value: 'EasyCert SSL' }
];

/**
* different domain format needs different SAN
*
*/
function getExtensionSAN(domain = '') {
const isIpDomain = Util.isIpDomain(domain);
if (isIpDomain) {
return {
name: 'subjectAltName',
altNames: [{ type: 7, ip: domain }]
};
} else {
return {
name: 'subjectAltName',
altNames: [{ type: 2, value: domain }]
};
}
}

function getKeysAndCert(serialNumber) {
const keys = forge.pki.rsa.generateKeyPair(2048);
const cert = forge.pki.createCertificate();
cert.publicKey = keys.publicKey;
cert.serialNumber = (Math.floor(Math.random() * 100000) + '');
console.log(`serial #${cert.serialNumber}`)
var now = Date.now();
// compatible with apple's updated cert policy: https://support.apple.com/en-us/HT210176
cert.validity.notBefore = new Date(now - 24 * 60 * 60 * 1000); // 1 day before
cert.validity.notAfter = new Date(now + 824 * 24 * 60 * 60 * 1000); // 824 days after
return {
keys,
cert
};
}

function generateRootCA(commonName) {
const keysAndCert = getKeysAndCert();
const keys = keysAndCert.keys;
const cert = keysAndCert.cert;

commonName = commonName || 'CertManager';

const attrs = defaultAttrs.concat([
{
name: 'commonName',
value: commonName
}
]);
cert.setSubject(attrs);
cert.setIssuer(attrs);
cert.setExtensions([
{ name: 'basicConstraints', cA: true },
// { name: 'keyUsage', keyCertSign: true, digitalSignature: true, nonRepudiation: true, keyEncipherment: true, dataEncipherment: true },
// { name: 'extKeyUsage', serverAuth: true, clientAuth: true, codeSigning: true, emailProtection: true, timeStamping: true },
// { name: 'nsCertType', client: true, server: true, email: true, objsign: true, sslCA: true, emailCA: true, objCA: true },
// { name: 'subjectKeyIdentifier' }
]);

cert.sign(keys.privateKey, forge.md.sha256.create());

return {
privateKey: forge.pki.privateKeyToPem(keys.privateKey),
publicKey: forge.pki.publicKeyToPem(keys.publicKey),
certificate: forge.pki.certificateToPem(cert)
};
}

function generateCertsForHostname(domain, rootCAConfig) {
// generate a serialNumber for domain
const md = forge.md.md5.create();
md.update(domain);

const keysAndCert = getKeysAndCert(md.digest().toHex());
const keys = keysAndCert.keys;
const cert = keysAndCert.cert;

const caCert = forge.pki.certificateFromPem(rootCAConfig.cert);
const caKey = forge.pki.privateKeyFromPem(rootCAConfig.key);

// issuer from CA
cert.setIssuer(caCert.subject.attributes);

const attrs = defaultAttrs.concat([
{
name: 'commonName',
value: domain
}
]);

const extensions = [
{ name: 'basicConstraints', cA: false },
getExtensionSAN(domain)
];

cert.setSubject(attrs);
cert.setExtensions(extensions);

cert.sign(caKey, forge.md.sha256.create());

return {
privateKey: forge.pki.privateKeyToPem(keys.privateKey),
publicKey: forge.pki.publicKeyToPem(keys.publicKey),
certificate: forge.pki.certificateToPem(cert)
};
}

// change the default attrs
function setDefaultAttrs(attrs) {
defaultAttrs = attrs;
}

module.exports.generateRootCA = generateRootCA;
module.exports.generateCertsForHostname = generateCertsForHostname;
module.exports.setDefaultAttrs = setDefaultAttrs;
12 changes: 12 additions & 0 deletions anyproxy/lib/node-easy-cert/errorConstants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Map all the error code here
*
*/

'use strict';

module.exports = {
ROOT_CA_NOT_EXISTS: 'ROOT_CA_NOT_EXISTS', // root CA has not been generated yet
ROOT_CA_EXISTED: 'ROOT_CA_EXISTED', // root CA was existed, be ware that it will be overwrited
ROOT_CA_COMMON_NAME_UNSPECIFIED: 'ROOT_CA_COMMON_NAME_UNSPECIFIED' // commonName for rootCA is required
};
235 changes: 235 additions & 0 deletions anyproxy/lib/node-easy-cert/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
'use strict'

delete require.cache['./certGenerator'];

const path = require('path'),
fs = require('fs'),
color = require('colorful'),
certGenerator = require('./certGenerator'),
util = require('./util'),
Errors = require('./errorConstants'),
https = require('https'),
AsyncTask = require('async-task-mgr'),
winCertUtil = require('./winCertUtil'),
exec = require('child_process').exec;

const DOMAIN_TO_VERIFY_HTTPS = 'localtest.me';

function getPort() {
return new Promise((resolve, reject) => {
const server = require('net').createServer();
server.unref();
server.on('error', reject);
server.listen(0, () => {
const port = server.address().port;
server.close(() => {
resolve(port);
});
});
});
}

function CertManager(options) {
options = options || {};
const rootDirName = util.getDefaultRootDirName();
const rootDirPath = options.rootDirPath || path.join(util.getUserHome(), '/' + rootDirName + '/');

if (options.defaultCertAttrs) {
certGenerator.setDefaultAttrs(options.defaultCertAttrs);
}

const certDir = rootDirPath,
rootCAcrtFilePath = path.join(certDir, 'rootCA.crt'),
rootCAkeyFilePath = path.join(certDir, 'rootCA.key'),
createCertTaskMgr = new AsyncTask();
let cacheRootCACrtFileContent,
cacheRootCAKeyFileContent;
let rootCAExists = false;

if (!fs.existsSync(certDir)) {
try {
fs.mkdirSync(certDir, '0777');
} catch (e) {
console.log('===========');
console.log('failed to create cert dir ,please create one by yourself - ' + certDir);
console.log('===========');
}
}

function getCertificate(hostname, certCallback) {
if (!_checkRootCA()) {
console.log(color.yellow('please generate root CA before getting certificate for sub-domains'));
certCallback && certCallback(Errors.ROOT_CA_NOT_EXISTS);
return;
}
const keyFile = path.join(certDir, '__hostname.key'.replace(/__hostname/, hostname)),
crtFile = path.join(certDir, '__hostname.crt'.replace(/__hostname/, hostname));

if (!cacheRootCACrtFileContent || !cacheRootCAKeyFileContent) {
cacheRootCACrtFileContent = fs.readFileSync(rootCAcrtFilePath, { encoding: 'utf8' });
cacheRootCAKeyFileContent = fs.readFileSync(rootCAkeyFilePath, { encoding: 'utf8' });
}

createCertTaskMgr.addTask(hostname, (callback) => {
if (!fs.existsSync(keyFile) || !fs.existsSync(crtFile)) {
try {
const result = certGenerator.generateCertsForHostname(hostname, {
cert: cacheRootCACrtFileContent,
key: cacheRootCAKeyFileContent
});
fs.writeFileSync(keyFile, result.privateKey);
fs.writeFileSync(crtFile, result.certificate);
callback(null, result.privateKey, result.certificate);
} catch (e) {
callback(e);
}
} else {
callback(null, fs.readFileSync(keyFile), fs.readFileSync(crtFile));
}
}, (err, keyContent, crtContent) => {
if (!err) {
certCallback(null, keyContent, crtContent);
} else {
certCallback(err);
}
});
}

function clearCerts(cb) {
util.deleteFolderContentsRecursive(certDir);
cb && cb();
}

function isRootCAFileExists() {
return (fs.existsSync(rootCAcrtFilePath) && fs.existsSync(rootCAkeyFilePath));
}

function generateRootCA(config, certCallback) {
if (!config || !config.commonName) {
console.error(color.red('The "config.commonName" for rootCA is required, please specify.'));
certCallback(Errors.ROOT_CA_COMMON_NAME_UNSPECIFIED);
return;
}

if (isRootCAFileExists()) {
if (config.overwrite) {
startGenerating(config.commonName, certCallback);
} else {
console.error(color.red('The rootCA exists already, if you want to overwrite it, please specify the "config.overwrite=true"'));
certCallback(Errors.ROOT_CA_EXISTED);
}
} else {
startGenerating(config.commonName, certCallback);
}

function startGenerating(commonName, cb) {
// clear old certs
clearCerts(() => {
console.log(color.green('temp certs cleared'));
try {
const result = certGenerator.generateRootCA(commonName);
fs.writeFileSync(rootCAkeyFilePath, result.privateKey);
fs.writeFileSync(rootCAcrtFilePath, result.certificate);

console.log(color.green('rootCA generated'));
console.log(color.green(color.bold('PLEASE TRUST the rootCA.crt in ' + certDir)));

cb && cb(null, rootCAkeyFilePath, rootCAcrtFilePath);
} catch (e) {
console.log(color.red(e));
console.log(color.red(e.stack));
console.log(color.red('fail to generate root CA'));
cb && cb(e);
}
});
}
}

function getRootCAFilePath() {
return isRootCAFileExists() ? rootCAcrtFilePath : '';
}

function getRootDirPath() {
return rootDirPath;
}

function _checkRootCA() {
if (rootCAExists) {
return true;
}

if (!isRootCAFileExists()) {
console.log(color.red('can not find rootCA.crt or rootCA.key'));
console.log(color.red('you may generate one'));
return false;
} else {
rootCAExists = true;
return true;
}
}

function ifRootCATrusted(callback) {
if (!isRootCAFileExists()) {
callback && callback(new Error('ROOTCA_NOT_EXIST'));
} else if (/^win/.test(process.platform)) {
winCertUtil.ifWinRootCATrusted()
.then((ifTrusted) => {
callback && callback(null, ifTrusted)
})
.catch((e) => {
callback && callback(null, false);
})
} else {
const HTTPS_RESPONSE = 'HTTPS Server is ON';
// localtest.me --> 127.0.0.1
getCertificate(DOMAIN_TO_VERIFY_HTTPS, (e, key, cert) => {
getPort()
.then(port => {
if (e) {
callback && callback(e);
return;
}
const server = https
.createServer(
{
ca: fs.readFileSync(rootCAcrtFilePath),
key,
cert
},
(req, res) => {
res.end(HTTPS_RESPONSE);
}
)
.listen(port);

// do not use node.http to test the cert. Ref: https://github.com/nodejs/node/issues/4175
const testCmd = `curl https://${DOMAIN_TO_VERIFY_HTTPS}:${port}`;
exec(testCmd, { timeout: 1000 }, (error, stdout, stderr) => {
server.close();
if (error) {
callback && callback(null, false);
}
if (stdout && stdout.indexOf(HTTPS_RESPONSE) >= 0) {
callback && callback(null, true);
} else {
callback && callback(null, false);
}
});
})
.catch(callback);
});
}
}

return {
getRootCAFilePath,
generateRootCA,
getCertificate,
clearCerts,
isRootCAFileExists,
ifRootCATrusted,
getRootDirPath,
};
}

module.exports = CertManager;
Loading

0 comments on commit 17b0031

Please sign in to comment.