mirror of
https://github.com/avwo/whistle.git
synced 2026-10-17 22:19:14 +08:00
852 lines
21 KiB
JavaScript
852 lines
21 KiB
JavaScript
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 ONE_YEAR = ONE_DAY * 365;
|
|
var MIN_DATE = ONE_DAY * 7;
|
|
var MAX_DATE = ONE_DAY * 36;
|
|
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 startTime = Date.now();
|
|
var INTERVAL = ONE_DAY;
|
|
var MAX_AGE = MAX_DATE - INTERVAL * 2;
|
|
var timer = setInterval(function () {
|
|
var now = Date.now();
|
|
if (now - startTime >= MAX_AGE) {
|
|
startTime = now;
|
|
resetAllCerts();
|
|
}
|
|
}, 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;
|
|
};
|
|
|
|
var rootSkiBytes;
|
|
|
|
function getRootSKI() {
|
|
if (!rootSkiBytes) {
|
|
try {
|
|
rootSkiBytes = ROOT_CRT.generateSubjectKeyIdentifier().getBytes();
|
|
} catch (e) {
|
|
rootSkiBytes = null;
|
|
}
|
|
}
|
|
return rootSkiBytes;
|
|
}
|
|
|
|
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: 'basicConstraints',
|
|
cA: false
|
|
},
|
|
{
|
|
name: 'keyUsage',
|
|
digitalSignature: true,
|
|
keyEncipherment: true
|
|
},
|
|
{
|
|
name: 'extKeyUsage',
|
|
serverAuth: true,
|
|
clientAuth: true
|
|
},
|
|
{
|
|
name: 'subjectAltName',
|
|
altNames: [
|
|
net.isIP(hostname)
|
|
? {
|
|
type: 7,
|
|
ip: hostname
|
|
}
|
|
: {
|
|
type: 2,
|
|
value: hostname
|
|
}
|
|
]
|
|
},
|
|
{
|
|
name: 'authorityKeyIdentifier',
|
|
keyIdentifier: getRootSKI() // Get SKI from ROOT_CRT for authorityKeyIdentifier extension
|
|
}
|
|
]);
|
|
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 now = Date.now();
|
|
// 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
|
|
if (isShortPeriod) {
|
|
cert.validity.notBefore = new Date(now - MIN_DATE);
|
|
cert.validity.notAfter = new Date(now + MAX_DATE);
|
|
} else {
|
|
cert.validity.notBefore = new Date(now - ONE_YEAR);
|
|
cert.validity.notAfter = new Date(now + ONE_YEAR * 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 = !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;
|
|
};
|