UNPKG

whistle

Version:

HTTP, HTTP2, HTTPS, Websocket debugging proxy

820 lines (764 loc) 21 kB
var forge = require('node-forge'); var fs = require('fs'); var net = require('net'); var path = require('path'); var crypto = require('crypto'); var LRU = require('lru-cache'); var hagent = require('hagent'); var extend = require('extend'); var h2 = require('./h2'); var createSecureContext = require('tls').createSecureContext || crypto.createCredentials; var util = require('../util'); var config = require('../config'); var common = require('../util/common'); var rulesUtil = require('../rules/util'); var pki = forge.pki; var HTTPS_DIR = mkdir(path.join(config.getDataDir(), 'certs')); var ROOT_NEW_KEY_FILE = path.join(HTTPS_DIR, 'root_new.key'); var ROOT_NEW_CRT_FILE = path.join(HTTPS_DIR, 'root_new.crt'); var CUSTOM_CERTS_DIR = config.disableCustomCerts ? null : config.CUSTOM_CERTS_DIR; var useNewKey = fs.existsSync(ROOT_NEW_KEY_FILE) && fs.existsSync(ROOT_NEW_CRT_FILE); var ROOT_KEY_FILE = useNewKey ? ROOT_NEW_KEY_FILE : path.join(HTTPS_DIR, 'root.key'); var ROOT_CRT_FILE = useNewKey ? ROOT_NEW_CRT_FILE : path.join(HTTPS_DIR, 'root.crt'); var customCertDir = config.certDir; var customPairs = {}; var customCertsInfo = {}; var customCertsFiles = {}; var allCustomCerts = {}; var customCertCount = 0; var cachePairs = new LRU({ max: 5120 }); var certsCache = new LRU({ max: 256 }); var remoteCerts = new LRU({ max: 1280 }); var KEY_SIZE = 2048; var ILEGAL_CHAR_RE = /[^a-z\d-]/i; // https://cloud.tencent.com/developer/ask/sof/58155 var RANDOM_SERIAL = '/' + Date.now() + '/' + process.pid + '/' + Math.floor(Math.random() * 100); var ONE_DAY = 1000 * 60 * 60 * 24; var MIN_DATE = ONE_DAY * 20; var CLEAR_CERTS_INTERVAL = ONE_DAY * 20; var MAX_INNTERFAL = 16; var PORT_RE = /:\d*$/; var pureCertFiles = {}; var customRoot; var ROOT_KEY, ROOT_CRT; var rootKey, rootCrt; var ILLEGAL_PATH_RE = /[/\\]/; exports.remoteCerts = remoteCerts; exports.createSecureContext = createSecureContext; exports.CUSTOM_CERTS_DIR = CUSTOM_CERTS_DIR; function notRootCa(name) { return name !== 'root'; } function checkFilename(name) { return name && !ILLEGAL_PATH_RE.test(name) && notRootCa(name); } function resetAllCerts() { cachePairs.reset(); certsCache.reset(); } // When delay is larger than 2147483647 or less than 1, the delay will be set to 1. Non-integer delays are truncated to an integer. var intervalCount = 0; var timer = setInterval(function () { if (++intervalCount >= MAX_INNTERFAL) { intervalCount = 0; resetAllCerts(); } }, CLEAR_CERTS_INTERVAL); if (timer && typeof timer.unref === 'function') { timer.unref(); } if (!useNewKey && !checkCertificate()) { try { fs.unlinkSync(ROOT_KEY_FILE); fs.unlinkSync(ROOT_CRT_FILE); } catch (e) {} } function mkdir(path) { !fs.existsSync(path) && fs.mkdirSync(path); return path; } function isExpiredCert(crt) { var now = Date.now(); try { var startDate = new Date(crt.notBefore); var endDate = new Date(crt.notAfter); return startDate.getTime() >= now || endDate.getTime() <= now; } catch (e) {} } function checkCertificate() { try { var crt = pki.certificateFromPem(fs.readFileSync(ROOT_CRT_FILE)); if (crt.publicKey.n.toString(2).length < KEY_SIZE || isExpiredCert(crt.validity)) { return false; } return /^whistle\.\d+$/.test(getCommonName(crt)); } catch (e) {} return true; } function getCommonName(crt) { var attrs = crt.issuer && crt.issuer.attributes; if (Array.isArray(attrs)) { for (var i = 0, len = attrs.length; i < len; i++) { var attr = attrs[i]; if (attr && attr.name === 'commonName') { return attr.value; } } } return ''; } function getDomain(hostname) { if (getCacheCert(hostname) || net.isIP(hostname)) { return hostname; } var list = hostname.split('.'); var prefix = list[0]; list[0] = '*'; var wildDomain = list.join('.'); if (getCacheCert(wildDomain)) { return wildDomain; } var len = list.length; if (len < 3) { return hostname; } if ( len > 3 || ILEGAL_CHAR_RE.test(prefix) || list[1].length > 3 || list[2] === 'com' || list[2] === 'net' || list[1] === 'url' ) { // For tencent cdn return wildDomain; } return hostname; } exports.getDomain = getDomain; exports.existsCustomCert = function (hostname) { if (!customCertCount) { return false; } hostname = hostname.replace(PORT_RE, ''); var cert = customPairs[hostname]; if (cert) { return true; } hostname = hostname.split('.'); hostname[0] = '*'; return customPairs[hostname.join('.')]; }; exports.hasCustomCerts = function () { return customCertCount; }; function getCacheCert(hostname) { return ( customPairs[hostname] || cachePairs.get(hostname) || certsCache.get(hostname) ); } function createSelfCert(hostname) { // https://blog.csdn.net/OnlyLove_/article/details/125815813 var serialNumber = crypto .createHash('sha1') .update(hostname + RANDOM_SERIAL, 'binary') .digest('hex'); serialNumber = (parseInt(serialNumber[0], 16) % 8) + serialNumber.substring(1); var cert = createCert( pki.setRsaPublicKey(ROOT_KEY.n, ROOT_KEY.e), serialNumber, true ); cert.setSubject([ { name: 'commonName', value: hostname } ]); cert.setIssuer(ROOT_CRT.subject.attributes); cert.setExtensions([ { name: 'subjectAltName', altNames: [ net.isIP(hostname) ? { type: 7, ip: hostname } : { type: 2, value: hostname } ] } ]); cert.sign(ROOT_KEY, forge.md.sha256.create()); return { key: pki.privateKeyToPem(ROOT_KEY), cert: pki.certificateToPem(cert) }; } exports.createCertificate = function (hostname) { hostname = getDomain(hostname); var cert = cachePairs.get(hostname); // 确保使用自己生成的证书,防止把用户证书下载出去 if (!cert) { cert = createSelfCert(hostname); certsCache.set(hostname, cert); } return cert; }; function parseCert(cert) { var pem = pki.certificateFromPem(cert.cert); var altNames = getAltNames(pem.extensions); if (!altNames || !altNames.length) { return; } return { cert: cert, altNames: altNames, validity: pem.validity }; } function parseAllCustomCerts() { var pairs = {}; var certsInfo = {}; var certFiles = {}; var keys = Object.keys(allCustomCerts).sort(function (key1, key2) { return util.compare( allCustomCerts[key1].cert.mtime, allCustomCerts[key2].cert.mtime ); }); keys.forEach(function (filename) { var info = allCustomCerts[filename]; var cert = info.cert; var mtime = cert.mtime; var validity = info.validity; var altNames = info.altNames; var dnsName = []; var disabled = (notRootCa(filename) && rulesUtil.isDisabledCertFile(filename)) || undefined; altNames.forEach(function (item) { if ((item.type === 2 || item.type === 7) && !pairs[item.value]) { if (!disabled) { var preCert = customPairs[item.value]; if (preCert && preCert.key === cert.key && preCert.cert === cert.cert) { if (preCert.mtime < mtime) { preCert.mtime = mtime; } pairs[item.value] = preCert; } else { pairs[item.value] = cert; } } dnsName.push(item.value); certsInfo[item.value] = extend( { filename: filename, type: cert.type, mtime: mtime, domain: item.value, disabled: disabled }, validity ); } }); if (dnsName.length) { certFiles[filename] = extend( { mtime: mtime, type: cert.type, dir: cert.dir, dnsName: dnsName.join(', '), disabled: disabled }, validity ); } }); certFiles.root = certFiles.root || customCertsFiles.root; customPairs = pairs; customCertsInfo = certsInfo; customCertsFiles = certFiles; customCertCount = Object.keys(customPairs).length; checkExpired(); } function loadCustomCerts(certDir, isCustom) { if (!certDir) { return; } var certs = {}; pureCertFiles = {}; try { fs.readdirSync(certDir).forEach(function (name) { if (!/^(.+)\.(crt|cer|pem|key)$/.test(name)) { return; } var filename = RegExp.$1; var type = RegExp.$2; var cert = (certs[filename] = certs[filename] || {}); var isCert = type !== 'key'; try { var filePath = path.join(certDir, name); var mtime = common.getStatSync(filePath).mtime.getTime(); if (isCert && cert.type && (cert.mtime == null || cert.mtime >= mtime)) { return; } cert.dir = certDir; if (isCert) { cert.mtime = mtime; cert.type = type; type = 'cert'; } cert[type] = fs.readFileSync(filePath, { encoding: 'utf8' }); } catch (e) {} }); } catch (e) {} var rootCA = certs.root; delete certs.root; if (rootCA && rootCA.key && rootCA.cert && !customRoot) { customRoot = rootCA; ROOT_KEY_FILE = path.join(certDir, 'root.key'); ROOT_CRT_FILE = path.join(certDir, 'root.' + rootCA.type); } Object.keys(certs).filter(function (key) { var cert = certs[key]; if (cert && cert.mtime != null && cert.key && cert.cert) { pureCertFiles[key] = { type: cert.type, name: key, key: cert.key, cert: cert.cert }; try { cert = parseCert(cert); if (cert) { allCustomCerts[isCustom ? 'z/' + key : key] = cert; } } catch (e) {} } }); } function getAltNames(exts) { for (var i = 0, len = exts.length; i < len; i++) { var item = exts[i]; if (item.name === 'subjectAltName') { return Array.isArray(item.altNames) && item.altNames.filter(util.noop); } } } function resetRootCA() { var cert = createCACert(); var key = pki.privateKeyToPem(cert.key).toString(); var crt = pki.certificateToPem(cert.cert).toString(); fs.writeFileSync(ROOT_KEY_FILE, key); fs.writeFileSync(ROOT_CRT_FILE, crt); rootKey = key; rootCrt = crt; ROOT_KEY = cert.key; ROOT_CRT = cert.cert; } function createRootCASafe() { if (isExpiredCert(ROOT_CRT.validity)) { try { resetRootCA(); resetAllCerts(); } catch (e) {} } } function createRootCA() { allCustomCerts = {}; loadCustomCerts(customCertDir, true); loadCustomCerts(CUSTOM_CERTS_DIR); parseAllCustomCerts(); if (ROOT_KEY && ROOT_CRT) { return; } try { ROOT_KEY = fs.readFileSync(ROOT_KEY_FILE); ROOT_CRT = fs.readFileSync(ROOT_CRT_FILE); rootKey = ROOT_KEY.toString(); rootCrt = ROOT_CRT.toString(); } catch (e) { ROOT_KEY = ROOT_CRT = null; } if (ROOT_KEY && ROOT_CRT && ROOT_KEY.length && ROOT_CRT.length) { ROOT_KEY = pki.privateKeyFromPem(ROOT_KEY); ROOT_CRT = pki.certificateFromPem(ROOT_CRT); if (customRoot) { customCertsFiles.root = extend( { mtime: customRoot.mtime, dir: customRoot.dir, type: customRoot.type, dnsName: '' }, ROOT_CRT.validity ); } try { var altNames = getAltNames(ROOT_CRT.extensions); var dnsName = []; altNames.forEach(function (item) { if ( (item.type === 2 || item.type === 7) && dnsName.indexOf(item.value) === -1 ) { dnsName.push(item.value); } }); customCertsFiles.root.dnsName = dnsName.join(', '); } catch (e) {} } else { resetRootCA(); } } function createCACert(opts) { opts = opts || {}; var keys = pki.rsa.generateKeyPair(KEY_SIZE); var cert = createCert(keys.publicKey); var now = Date.now() + common.padLeft(Math.floor(Math.random() * 1000), 3); var attrs = [ { name: 'commonName', value: opts.commonname || opts.commonName || 'whistle.' + now }, { name: 'countryName', value: opts.countryname || opts.countryName || 'CN' }, { shortName: 'ST', value: opts.st || opts.ST || 'ZJ' }, { name: 'localityName', value: opts.localityname || opts.localityName || 'HZ' }, { name: 'organizationName', value: opts.organizationname || opts.organizationName || now + '.wproxy.org' }, { shortName: 'OU', value: opts.ou || opts.OU || 'wproxy.org' } ]; 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 } ]); cert.sign(keys.privateKey, forge.md.sha256.create()); return { key: keys.privateKey, cert: cert }; } exports.createRootCA = function(opts) { var cert = createCACert(opts); cert.key = pki.privateKeyToPem(cert.key).toString(); cert.cert = pki.certificateToPem(cert.cert).toString(); return cert; }; function createCert(publicKey, serialNumber, isShortPeriod) { var cert = pki.createCertificate(); cert.publicKey = publicKey; cert.serialNumber = serialNumber || '01'; var curDate = new Date(); var curYear = curDate.getFullYear(); if (isShortPeriod) { cert.validity.notBefore = new Date(curDate.getTime() - MIN_DATE); } else { cert.validity.notBefore = new Date(); cert.validity.notBefore.setFullYear(curYear - 1); } // https://www.ssls.com/blog/apples-new-ssl-lifetime-limitation-and-what-it-means-for-you/ // https://chromium.googlesource.com/chromium/src/+/refs/heads/master/net/cert/cert_verify_proc.cc#900 cert.validity.notAfter = new Date(); cert.validity.notAfter.setFullYear(curYear + (isShortPeriod ? 1 : 10)); return cert; } function getRootCAFile() { createRootCASafe(); return ROOT_CRT_FILE; } createRootCA(); // 启动生成ca function getOrCreateCert(servername) { var requestCert = servername[0] === ':'; if (requestCert) { servername = servername.substring(1); } var cert = remoteCerts.get(servername); if (!cert) { servername = getDomain(servername); cert = getCacheCert(servername); if (!cert) { cert = createSelfCert(servername); cachePairs.set(servername, cert); } } return requestCert ? extend( { requestCert: true, rejectUnauthorized: false }, cert ) : cert; } hagent.serverAgent.createCertificate = getOrCreateCert; var getHttp2Server = hagent.create(h2.getHttpServer, 42900); var getHttpsServer = hagent.create(h2.getServer, 43900); var cbs = {}; var ports = {}; var TIMEOUT = 6000; var SNICallback = function (servername, cb) { var options = getOrCreateCert(servername); if (!options._ctx) { try { options._ctx = createSecureContext(options); } catch (e) {} } cb(null, options._ctx); }; exports.getRootCA = function () { createRootCASafe(); return { key: rootKey, cert: rootCrt }; }; exports.getCustomCertsInfo = function () { return customCertsInfo; }; exports.getCustomCertsFiles = function () { return customCertsFiles; }; exports.getRootCAFile = getRootCAFile; exports.serverAgent = hagent.serverAgent; exports.SNICallback = SNICallback; function addCallback(name, callback) { var cbList = cbs[name]; if (!cbList) { cbList = []; cbs[name] = cbList; } cbList.push(callback); return cbList; } function createServer(name, cbList, listener, options) { var removeServer = function () { ports[name] = null; try { this.close(); } catch (e) {} //重复关闭会导致异常 }; ports[name] = false; // pending var getServer = options ? getHttpsServer : getHttp2Server; getServer(options, listener, function (server, port) { server.on('error', removeServer); var timeout = setTimeout(removeServer, TIMEOUT); var clearup = function () { clearTimeout(timeout); }; if (options) { server.once('tlsClientError', clearup); server.once('secureConnection', clearup); } else { server.once('connection', clearup); } ports[name] = port; cbList.forEach(function (cb) { cb(port); }); cbs[name] = []; }); } exports.getHttp2Server = function (listener, callback) { var name = 'httpH2'; var curPort = ports[name]; if (curPort) { return callback(curPort); } var cbList = addCallback(name, callback); if (curPort === false) { return; } createServer(name, cbList, listener); }; exports.getSNIServer = function (listener, callback, disableH2, requestCert) { var enableH2 = config.enableH2 && !disableH2; var name = (enableH2 ? 'h2Sni' : 'sni') + (requestCert ? 'WithCert' : ''); var curPort = ports[name]; if (curPort) { return callback(curPort); } var cbList = addCallback(name, callback); if (curPort === false) { return; } var options = { SNICallback: SNICallback }; options.allowHTTP1 = enableH2; // 是否启用http2 if (requestCert) { options = extend( { requestCert: true, rejectUnauthorized: false }, options ); } createServer(name, cbList, listener, options); }; var checkTimer; function checkExpired() { clearTimeout(checkTimer); var files = Object.keys(customCertsFiles); exports.hasInvalidCerts = false; for (var i = 0, len = files.length; i < len; i++) { var file = customCertsFiles[files[i]]; if (file && isExpiredCert(file)) { exports.hasInvalidCerts = true; return; } } checkTimer = setTimeout(checkExpired, 600000); } function removeFile(filename) { fs.unlink(filename, function (err) { err && fs.unlink(filename, util.noop); }); } function writeFile(filename, ctn, callback) { fs.writeFile(filename, ctn, function (err) { if (!err) { return callback(); } fs.writeFile(filename, ctn, callback); }); } // 异步重试,出错重试即可 function removeCertFile(filename, type) { removeFile(path.join(CUSTOM_CERTS_DIR, filename + '.key')); removeFile(path.join(CUSTOM_CERTS_DIR, filename + type)); delete pureCertFiles[filename]; } // 异步写入,出错重试即可 function writeCertFile(filename, type, cert, mtime) { var keyFile = path.join(CUSTOM_CERTS_DIR, filename + '.key'); var certFile = path.join(CUSTOM_CERTS_DIR, filename + '.' + (type || 'crt')); writeFile(keyFile, cert.key, function () { fs.utimes && fs.utimes(keyFile, mtime, mtime, util.noop); }); writeFile(certFile, cert.cert, function () { fs.utimes && fs.utimes(certFile, mtime, mtime, util.noop); }); } function getCertType(type) { if (type !== 'cer' && type !== 'pem') { return 'crt'; } return type; } exports.removeCert = function (opts) { if (!CUSTOM_CERTS_DIR) { return; } var filename = opts.filename; var type = getCertType(opts.type); if (checkFilename(filename) && allCustomCerts[filename]) { removeCertFile(filename, type); delete allCustomCerts[filename]; parseAllCustomCerts(); } }; exports.setActiveCert = function (opts) { if (!CUSTOM_CERTS_DIR) { return; } var filename = opts.filename; if (notRootCa(filename) && allCustomCerts[filename]) { rulesUtil.setDisabledCertFile(filename, opts.disabled); parseAllCustomCerts(); } }; exports.uploadCerts = function (certs) { if (!CUSTOM_CERTS_DIR) { return; } var now = Date.now(); var hasChanged; var index = 0; Object.keys(certs).forEach(function (filename) { if (!checkFilename(filename) || filename.length > 128) { return; } var cert = certs[filename]; if (!cert) { return; } var keyStr, certStr, type; if (Array.isArray(cert)) { keyStr = cert[0]; certStr = cert[1]; type = getCertType(cert[2]); } else { keyStr = cert.key; certStr = cert.cert; type = getCertType(cert.type); } if (util.isString(keyStr) && util.isString(certStr)) { var mtime = now + index * 1000; ++index; try { cert = parseCert({ key: keyStr, type: type, cert: certStr, mtime: mtime }); if (cert) { writeCertFile(filename, type, cert.cert, new Date(mtime)); pureCertFiles[filename] = { type: type, name: filename, key: keyStr, cert: certStr }; allCustomCerts[filename] = cert; hasChanged = true; } } catch (e) {} } }); hasChanged && parseAllCustomCerts(); }; exports.getPureCertFiles = function () { return pureCertFiles; };