diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 360318d..308a31c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,5 +1,6 @@ name: CI on: + workflow_dispatch: schedule: - cron: "0 13 * * 5" # Every Friday at 13:00 UTC jobs: diff --git a/index.js b/index.js index 8921000..995f57e 100644 --- a/index.js +++ b/index.js @@ -2,7 +2,7 @@ const async = require('async') const data = require('./data/data.json') const libThing = require('./connectors/librarything') const openLibrary = require('./connectors/openlibrary') -const syswidecas = require('syswide-cas'); +const syswidecas = require('./syswide-cas.js'); // Intermediate certificate that's often incomplete in SSL chains. syswidecas.addCAs('./SectigoRSADomainValidationSecureServerCA.cer'); diff --git a/package-lock.json b/package-lock.json index 08cedaf..c6eedec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,6 @@ "cheerio": "^1.0.0-rc.12", "node-polyfill-webpack-plugin": "^2.0.1", "superagent": "^8.0.0", - "syswide-cas": "^5.3.0", "tough-cookie": "^4.0.0", "uuid": "^8.3.2", "xml2js": "^0.4.23" @@ -5433,14 +5432,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/syswide-cas": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/syswide-cas/-/syswide-cas-5.3.0.tgz", - "integrity": "sha512-+RLgS6VInsX8rBpL+gy5qpa7phngecbK7NABelBZpqYpBTwOIK1y7CqHlXK5Vy/rA4erD9q/FyKzMjx2uX3zYg==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", @@ -10156,11 +10147,6 @@ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true }, - "syswide-cas": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/syswide-cas/-/syswide-cas-5.3.0.tgz", - "integrity": "sha512-+RLgS6VInsX8rBpL+gy5qpa7phngecbK7NABelBZpqYpBTwOIK1y7CqHlXK5Vy/rA4erD9q/FyKzMjx2uX3zYg==" - }, "tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", diff --git a/package.json b/package.json index 30e6b56..a754a86 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,6 @@ "cheerio": "^1.0.0-rc.12", "node-polyfill-webpack-plugin": "^2.0.1", "superagent": "^8.0.0", - "syswide-cas": "^5.3.0", "tough-cookie": "^4.0.0", "uuid": "^8.3.2", "xml2js": "^0.4.23" diff --git a/syswide-cas.js b/syswide-cas.js new file mode 100644 index 0000000..f19d7b9 --- /dev/null +++ b/syswide-cas.js @@ -0,0 +1,102 @@ +const fs = require("fs"); +const path = require("path"); +const tls = require("tls"); + +const rootCAs = []; + +// for node 7.2 and up, trapping method must be used. +var useTrap = false; +const parts = process.versions.node.split("."); +const major = parseInt(parts[0]); +const minor = parseInt(parts[1]); +if (major > 7 || (major == 7 && minor >= 2) || (major === 6 && minor >= 10)) { + useTrap = true; +} + +// create an empty secure context loaded with the root CAs +const rootSecureContext = tls.createSecureContext ? tls.createSecureContext() : require("crypto").createCredentials(); + +function addDefaultCA(file) { + try { + var cert, match; + var content = fs.readFileSync(file, { encoding: "ascii" }).trim(); + content = content.replace(/\r\n/g, "\n"); // Handles certificates that have been created in Windows + var regex = /-----BEGIN CERTIFICATE-----\n[\s\S]+?\n-----END CERTIFICATE-----/g; + var results = content.match(regex); + if (!results) throw new Error("Could not parse certificate"); + results.forEach(function(match) { + var cert = match.trim(); + rootCAs.push(cert); + // this will add the cert to the root certificate authorities list + // which will be used by all subsequent secure contexts with root CAs. + // this only works up to node 6. node 7 and up it has no affect. + if (!useTrap) { + rootSecureContext.context.addCACert(cert); + } + }); + } catch (e) { + if (e.code !== "ENOENT") { + console.log("failed reading file " + file + ": " + e.message); + } + } +} + +exports.addCAs = function(dirs) { + if (!dirs) { + return; + } + + if (typeof dirs === "string") { + dirs = dirs.split(",").map(function(dir) { + return dir.trim(); + }); + } + + var files, stat, file, i, j; + for (i = 0; i < dirs.length; ++i) { + try { + stat = fs.statSync(dirs[i]); + if (stat.isDirectory()) { + files = fs.readdirSync(dirs[i]); + for (j = 0; j < files.length; ++j) { + file = path.resolve(dirs[i], files[j]); + try { + stat = fs.statSync(file); + if (stat.isFile()) { + addDefaultCA(file); + } + } catch (e) { + if (e.code !== "ENOENT") { + console.log("failed reading " + file + ": " + e.message); + } + } + } + } else { + addDefaultCA(dirs[i]); + } + } catch (e) { + if (e.code !== "ENOENT") { + console.log("failed reading " + dirs[i] + ": " + e.message); + } + } + } +}; + +if (useTrap) { + // trap the createSecureContext method and inject custom root CAs whenever invoked + const origCreateSecureContext = tls.createSecureContext; + tls.createSecureContext = function(options) { + var c = origCreateSecureContext.apply(null, arguments); + if (!options?.ca && rootCAs.length > 0) { + rootCAs.forEach(function(ca) { + // add to the created context our own root CAs + c.context.addCACert(ca); + }); + } + return c; + }; +} + +const defaultCALocations = ["/etc/ssl/ca-node.pem"]; + +exports.addCAs(defaultCALocations); \ No newline at end of file