From cf9590ecbcd4b0a7c01f153cacade619518f84f0 Mon Sep 17 00:00:00 2001 From: Justin Wind Date: Tue, 9 Aug 2022 15:38:24 -0700 Subject: [PATCH] separate validateClientIdentifier from fetchClientIdentifier, add validateProfile --- README.md | 3 + index.js | 2 + lib/common.js | 35 +++ lib/communication.js | 227 +++++++++++++++++ lib/errors.js | 16 ++ package-lock.json | 502 +++++++++++++++++++++++++++++++++++--- package.json | 7 +- test/lib/common.js | 32 +++ test/lib/communication.js | 172 +++++++++++++ test/lib/errors.js | 14 ++ 10 files changed, 969 insertions(+), 41 deletions(-) create mode 100644 lib/errors.js create mode 100644 test/lib/errors.js diff --git a/README.md b/README.md index 985c6b0..e2df28d 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,9 @@ Notable methods on the Communication class: - `async fetchProfile(urlObject)` Retrieve profile information from an endpoint. +- `async validateClientIdentifier(url)` + Check that a url meets specification requirements to be a client identifier. + - `async fetchClientIdentifier(urlObject)` Retrieve client identifier information from an endpoint. diff --git a/index.js b/index.js index 912dab4..0f13a92 100644 --- a/index.js +++ b/index.js @@ -2,8 +2,10 @@ const Communication = require('./lib/communication'); const Enum = require('./lib/enum'); +const Errors = require('./lib/errors'); module.exports = { Communication, Enum, + Errors, }; diff --git a/lib/common.js b/lib/common.js index cfdf9db..dc05854 100644 --- a/lib/common.js +++ b/lib/common.js @@ -64,9 +64,44 @@ const pick = (obj, props) => { }; +/** + * Return a set containing non-shared items between two sets. + * @param {Set} a + * @param {Set} b + * @returns {Set} + */ +const setSymmetricDifference = (a, b) => { + const d = new Set(a); + for (const x of b) { + if (d.has(x)) { + d.delete(x); + } else { + d.add(x); + } + } + return d; +}; + + +/** + * URL objects have weird names. + * @param {String} component + * @returns {String} + */ +const properURLComponentName = (component) => { + // eslint-disable-next-line security/detect-object-injection + return { + hash: 'fragment', + protocol: 'scheme', + }[component] || component; +} + + module.exports = { fileScope, axiosResponseLogData, logTruncate, pick, + setSymmetricDifference, + properURLComponentName, }; \ No newline at end of file diff --git a/lib/communication.js b/lib/communication.js index 540eaaf..0116548 100644 --- a/lib/communication.js +++ b/lib/communication.js @@ -10,11 +10,19 @@ const { performance } = require('perf_hooks'); const { randomBytes, createHash } = require('crypto'); const { promisify } = require('util'); const randomBytesAsync = promisify(randomBytes); +const { Address4, Address6 } = require('ip-address'); +const dns = require('dns'); +dns.lookupAsync = dns.lookupAsync || promisify(dns.lookup); const common = require('./common'); const Enum = require('./enum'); +const { ValidationError } = require('./errors'); const _fileScope = common.fileScope(__filename); +const noDotPathRE = /(\/\.\/|\/\.\.\/)/; +const v6HostRE = /\[[0-9a-f:]+\]/; +const loopback4 = new Address4('127.0.0.0/8'); + class Communication { /** * @param {Console} logger @@ -327,6 +335,211 @@ class Communication { } + /** + * Validate a url has a specific schema. + * @param {URL} urlObj + * @param {String[]} validSchemes + */ + static _urlValidScheme(urlObj, validSchemes = ['http:', 'https:']) { + if (!validSchemes.includes(urlObj.protocol)) { + throw new ValidationError(`unsupported url scheme '${urlObj.protocol}'`); + } + } + + + /** + * Validate a url does not include some components. + * @param {URL} urlObj + * @param {String[]} disallowed + */ + static _urlPartsDisallowed(urlObj, disallowed) { + disallowed.forEach((part) => { + if (urlObj[part]) { // eslint-disable-line security/detect-object-injection + throw new ValidationError(`url cannot contain ${common.properURLComponentName(part)}`); + } + }); + } + + + /** + * Validate a url does not have relative path. + * @param {String} url + */ + static _urlPathNoDots(url) { + if (noDotPathRE.test(url)) { + throw new ValidationError('relative path segment not valid'); + } + } + + + /** + * Validate a url does not have a hostname which is an ip address. + * N.B. Sets isLoopback on urlObj + * @param {URL} urlObj + * @param {Boolean} allowLoopback + */ + static async _urlNamedHost(urlObj, allowLoopback, resolveHostname) { + let address; + if (v6HostRE.test(urlObj.hostname)) { + /** + * We do not need to worry about the Address6() failing to parse, + * as if it looks like an ipv6 addr but is not valid, the URL() + * call would already have failed. + */ + address = new Address6(urlObj.hostname.slice(1, urlObj.hostname.length - 1)); + /* succeeded parsing as ipv6, reject unless loopback */ + urlObj.isLoopback = address.isLoopback(); + } else { + try { + address = new Address4(urlObj.hostname); + /* succeeded parsing as ipv4, reject unless loopback */ + urlObj.isLoopback = address.isInSubnet(loopback4); + } catch (e) { + /* did not parse as ip, carry on */ + } + } + + if (resolveHostname && !urlObj.isLoopback) { + /** + * Resolve hostname to check for localhost. + * This is more complicated due to SSRF mitigation: + * If the hostname does not end with a ., we also resolve that, + * and complain if the two resolutions do not match, assuming + * malicious intent for the server to resolve a local record. + */ + const hostnames = [urlObj.hostname]; + if (!urlObj.hostname.endsWith('.')) { + hostnames.push(urlObj.hostname + '.'); + } + const settledResolutions = await Promise.allSettled(hostnames.map((hostname) => dns.lookupAsync(hostname, { + all: true, + verbatim: true, + }))); + // If any resolution failed, bail. + if (settledResolutions + .map((resolution) => resolution.status) + .includes('rejected')) { + throw new ValidationError('could not resolve hostname'); + } + + // extract each resolution value, array of {address,family} + const resolutions = settledResolutions.map((resolution) => resolution.value); + + // If there were two resolutions, ensure they returned identical results. + if (resolutions.length > 1) { + // create set of addresses for each resolution + const addressSets = resolutions.map((addrs) => { + return new Set((addrs || []).map((a) => a.address)); + }); + const differences = common.setSymmetricDifference(...addressSets); + if (differences.size) { + throw new ValidationError('inconsistent hostname resolution'); + } + } + const resolvedHost = resolutions[0] || []; + + // Persist the loopback state + urlObj.isLoopback = resolvedHost.reduce((acc, resolved) => { + let addr; + switch (resolved.family) { + case 4: + addr = new Address4(resolved.address); + return acc || addr.isInSubnet(loopback4); + case 6: + addr = new Address6(resolved.address); + return acc || addr.isLoopback(); + default: + return acc; + } + }, false); + } + + if (address + && (!urlObj.isLoopback || !allowLoopback)) { + throw new ValidationError('hostname cannot be IP'); + } + } + + + /** + * Ensure a url meets the requirements to be a profile uri. + * @param {String} url + * @param {Object} validationOptions + * @param {Boolean} validationOptions.allowLoopback + */ + async validateProfile(url, validationOptions) { + const _scope = _fileScope('validateProfile'); + const errorScope = 'invalid profile url'; + + const options = Object.assign({}, { + allowLoopback: false, + resolveHostname: false, + }, validationOptions); + + let profile; + try { + profile = new URL(url); + } catch (e) { + this.logger.debug(_scope, 'failed to parse url', { error: e, url }); + throw new ValidationError(`${errorScope}: unparsable`); + } + profile.isLoopback = false; + + try { + Communication._urlValidScheme(profile); + Communication._urlPartsDisallowed(profile, ['hash', 'username', 'password', 'port']); + Communication._urlPathNoDots(url); + Communication._urlNamedHost(profile, options.allowLoopback, options.resolveHostname); + } catch (e) { + this.logger.debug(_scope, 'profile url not valid', { url, error: e }); + throw new ValidationError(`${errorScope}: ${e.message}`); + } + + return profile; + } + + + /** + * Ensure a url meets the requirements to be a client identifier. + * Sets 'isLoopback' on returned URL object to true if hostname is or resolves to a loopback ip. + * @param {String} url + * @param {Object} validationOptions + * @param {Boolean} validationOptions.allowLoopback + * @param {Boolean} validationOptions.resolveHostname + * @returns {URL} + */ + async validateClientIdentifier(url, validationOptions) { + const _scope = _fileScope('validateClientIdentifier'); + const errorScope = 'invalid client identifier url'; + + const options = Object.assign({}, { + allowLoopback: true, + resolveHostname: true, + }, validationOptions); + + let clientId; + try { + clientId = new URL(url); + } catch (e) { + this.logger.debug(_scope, 'failed to parse url', { error: e, url }); + throw new ValidationError('invalid client identifier url: unparsable'); + } + clientId.isLoopback = false; + + try { + Communication._urlValidScheme(clientId); + Communication._urlPartsDisallowed(clientId, ['hash', 'username', 'password']); + Communication._urlPathNoDots(url); + await Communication._urlNamedHost(clientId, options.allowLoopback, options.resolveHostname); + } catch (e) { + this.logger.debug(_scope, 'client identifier url not valid', { url, error: e }); + throw new ValidationError(`${errorScope}: ${e.message}`); + } + + return clientId; + } + + /** * @typedef {Object} ClientIdentifierData * @property {Object} rels - keyed by relation to array of uris @@ -334,10 +547,24 @@ class Communication { */ /** * Retrieve and parse client identifier endpoint data. + * N.B. Assumes urlObj has passed validateClientIdentifier. * @param {URL} urlObj * @returns {ClientIdentifierData|undefined} mf2 data filtered for h-app items, or undefined if url could not be fetched */ async fetchClientIdentifier(urlObj) { + const _scope = _fileScope('fetchClientIdentifier'); + + // Loopback address will eschew client fetch, return empty data. + const isLoopbackResult = { + rels: {}, + items: [], + }; + + // Set by validation method in case of loopback ip hostname + if (urlObj.isLoopback) { + return isLoopbackResult; + } + const mfData = await this.fetchMicroformat(urlObj); if (!mfData) { return undefined; diff --git a/lib/errors.js b/lib/errors.js new file mode 100644 index 0000000..faccd70 --- /dev/null +++ b/lib/errors.js @@ -0,0 +1,16 @@ +'use strict'; + +class ValidationError extends Error { + constructor(...args) { + super(...args); + delete this.stack; + } + + get name() { + return this.constructor.name; + } +} + +module.exports = { + ValidationError, +}; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 2799a67..aeec15a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,13 +13,14 @@ "@squeep/web-linking": "^1.0.7", "axios": "^0.27.2", "iconv": "^3.0.1", + "ip-address": "^8.1.0", "microformats-parser": "^1.4.1" }, "devDependencies": { - "eslint": "^8.20.0", + "eslint": "^8.21.0", "eslint-plugin-node": "^11.1.0", "eslint-plugin-security": "^1.5.0", - "eslint-plugin-sonarjs": "^0.14.0", + "eslint-plugin-sonarjs": "^0.15.0", "mocha": "^10.0.0", "nyc": "^15.1.0", "pre-commit": "^1.2.2", @@ -435,9 +436,9 @@ } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.9.5", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz", - "integrity": "sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw==", + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.4.tgz", + "integrity": "sha512-mXAIHxZT3Vcpg83opl1wGlVZ9xydbfZO3r5YfRSH6Gpp2J/PfdBP0wbDa2sO6/qRbcalpoevVyW6A/fI6LfeMw==", "dev": true, "dependencies": { "@humanwhocodes/object-schema": "^1.2.1", @@ -448,6 +449,16 @@ "node": ">=10.10.0" } }, + "node_modules/@humanwhocodes/gitignore-to-minimatch": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/gitignore-to-minimatch/-/gitignore-to-minimatch-1.0.2.tgz", + "integrity": "sha512-rSqmMJDdLFUsyxR6FMtD00nfQKKLFb1kv+qBbOVKqErvloEIJLo5bDTJTQNTYgeyp78JsA7u/NPi5jT1GR/MuA==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@humanwhocodes/object-schema": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", @@ -609,6 +620,41 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@sinonjs/commons": { "version": "1.8.3", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", @@ -667,9 +713,9 @@ "dev": true }, "node_modules/acorn": { - "version": "8.7.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz", - "integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==", + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", + "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -786,6 +832,15 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -1147,6 +1202,18 @@ "node": ">=0.3.1" } }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -1199,13 +1266,14 @@ } }, "node_modules/eslint": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.20.0.tgz", - "integrity": "sha512-d4ixhz5SKCa1D6SCPrivP7yYVi7nyD6A4vs6HIAul9ujBzcEmZVM3/0NN/yu5nKhmO1wjp5xQ46iRfmDGlOviA==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.21.0.tgz", + "integrity": "sha512-/XJ1+Qurf1T9G2M5IHrsjp+xrGT73RZf23xA1z5wB1ZzzEAWSZKvRwhWxTFp1rvkvCfwcvAUNAP31bhKTTGfDA==", "dev": true, "dependencies": { "@eslint/eslintrc": "^1.3.0", - "@humanwhocodes/config-array": "^0.9.2", + "@humanwhocodes/config-array": "^0.10.4", + "@humanwhocodes/gitignore-to-minimatch": "^1.0.2", "ajv": "^6.10.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", @@ -1215,14 +1283,17 @@ "eslint-scope": "^7.1.1", "eslint-utils": "^3.0.0", "eslint-visitor-keys": "^3.3.0", - "espree": "^9.3.2", + "espree": "^9.3.3", "esquery": "^1.4.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", "functional-red-black-tree": "^1.0.1", "glob-parent": "^6.0.1", "globals": "^13.15.0", + "globby": "^11.1.0", + "grapheme-splitter": "^1.0.4", "ignore": "^5.2.0", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", @@ -1347,9 +1418,9 @@ } }, "node_modules/eslint-plugin-sonarjs": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-sonarjs/-/eslint-plugin-sonarjs-0.14.0.tgz", - "integrity": "sha512-0X0q3fB8ghppms19cR2oIK2ajoFp7DEy3AVGDqO7WX02r1aWOzkrHa+veatGZw+R7amgBvfcF0qHCG66p9Zoag==", + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-sonarjs/-/eslint-plugin-sonarjs-0.15.0.tgz", + "integrity": "sha512-LuxHdAe6VqSbi1phsUvNjbmXLuvlobmryQJJNyQYbdubCfz6K8tmgoqNiJPnz0pP2AbYDbtuPm0ajOMgMrC+dQ==", "dev": true, "engines": { "node": ">=12" @@ -1408,17 +1479,20 @@ } }, "node_modules/espree": { - "version": "9.3.2", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.2.tgz", - "integrity": "sha512-D211tC7ZwouTIuY5x9XnS0E9sWNChB7IYKX/Xp5eQj3nFXhqmiUDB9q27y76oFl8jTg3pXcQx/bpxMfs3CIZbA==", + "version": "9.3.3", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.3.tgz", + "integrity": "sha512-ORs1Rt/uQTqUKjDdGCyrtYxbazf5umATSf/K4qxjmZHORR6HJk+2s/2Pqe+Kk49HHINC/xNIrGfgh8sZcll0ng==", "dev": true, "dependencies": { - "acorn": "^8.7.1", + "acorn": "^8.8.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.3.0" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/esprima": { @@ -1482,6 +1556,34 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "node_modules/fast-glob": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", + "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -1494,6 +1596,15 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -1750,12 +1861,38 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/graceful-fs": { "version": "4.2.10", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", "dev": true }, + "node_modules/grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "dev": true + }, "node_modules/has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -1885,6 +2022,23 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, + "node_modules/ip-address": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-8.1.0.tgz", + "integrity": "sha512-Wz91gZKpNKoXtqvY8ScarKYwhXoK4r/b5QuT+uywe/azv0/nUCo7Bh0IRRI7F9DHR06kJNWtzMGLIbXavngbKA==", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "1.1.2" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ip-address/node_modules/sprintf-js": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", + "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==" + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -2120,6 +2274,11 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==" + }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -2249,6 +2408,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, "node_modules/microformats-parser": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/microformats-parser/-/microformats-parser-1.4.1.tgz", @@ -2260,6 +2428,19 @@ "node": ">=10" } }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -2766,6 +2947,15 @@ "isarray": "0.0.1" } }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -2946,6 +3136,26 @@ "node": ">=6" } }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -3062,6 +3272,16 @@ "node": ">=4" } }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -3077,6 +3297,29 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -3161,6 +3404,15 @@ "url": "https://opencollective.com/sinon" } }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -3910,9 +4162,9 @@ } }, "@humanwhocodes/config-array": { - "version": "0.9.5", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz", - "integrity": "sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw==", + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.4.tgz", + "integrity": "sha512-mXAIHxZT3Vcpg83opl1wGlVZ9xydbfZO3r5YfRSH6Gpp2J/PfdBP0wbDa2sO6/qRbcalpoevVyW6A/fI6LfeMw==", "dev": true, "requires": { "@humanwhocodes/object-schema": "^1.2.1", @@ -3920,6 +4172,12 @@ "minimatch": "^3.0.4" } }, + "@humanwhocodes/gitignore-to-minimatch": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/gitignore-to-minimatch/-/gitignore-to-minimatch-1.0.2.tgz", + "integrity": "sha512-rSqmMJDdLFUsyxR6FMtD00nfQKKLFb1kv+qBbOVKqErvloEIJLo5bDTJTQNTYgeyp78JsA7u/NPi5jT1GR/MuA==", + "dev": true + }, "@humanwhocodes/object-schema": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", @@ -4047,6 +4305,32 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, "@sinonjs/commons": { "version": "1.8.3", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", @@ -4099,9 +4383,9 @@ "dev": true }, "acorn": { - "version": "8.7.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz", - "integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==", + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", + "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", "dev": true }, "acorn-jsx": { @@ -4185,6 +4469,12 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -4452,6 +4742,15 @@ "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", "dev": true }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + } + }, "doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -4492,13 +4791,14 @@ "dev": true }, "eslint": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.20.0.tgz", - "integrity": "sha512-d4ixhz5SKCa1D6SCPrivP7yYVi7nyD6A4vs6HIAul9ujBzcEmZVM3/0NN/yu5nKhmO1wjp5xQ46iRfmDGlOviA==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.21.0.tgz", + "integrity": "sha512-/XJ1+Qurf1T9G2M5IHrsjp+xrGT73RZf23xA1z5wB1ZzzEAWSZKvRwhWxTFp1rvkvCfwcvAUNAP31bhKTTGfDA==", "dev": true, "requires": { "@eslint/eslintrc": "^1.3.0", - "@humanwhocodes/config-array": "^0.9.2", + "@humanwhocodes/config-array": "^0.10.4", + "@humanwhocodes/gitignore-to-minimatch": "^1.0.2", "ajv": "^6.10.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", @@ -4508,14 +4808,17 @@ "eslint-scope": "^7.1.1", "eslint-utils": "^3.0.0", "eslint-visitor-keys": "^3.3.0", - "espree": "^9.3.2", + "espree": "^9.3.3", "esquery": "^1.4.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", "functional-red-black-tree": "^1.0.1", "glob-parent": "^6.0.1", "globals": "^13.15.0", + "globby": "^11.1.0", + "grapheme-splitter": "^1.0.4", "ignore": "^5.2.0", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", @@ -4602,9 +4905,9 @@ } }, "eslint-plugin-sonarjs": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-sonarjs/-/eslint-plugin-sonarjs-0.14.0.tgz", - "integrity": "sha512-0X0q3fB8ghppms19cR2oIK2ajoFp7DEy3AVGDqO7WX02r1aWOzkrHa+veatGZw+R7amgBvfcF0qHCG66p9Zoag==", + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-sonarjs/-/eslint-plugin-sonarjs-0.15.0.tgz", + "integrity": "sha512-LuxHdAe6VqSbi1phsUvNjbmXLuvlobmryQJJNyQYbdubCfz6K8tmgoqNiJPnz0pP2AbYDbtuPm0ajOMgMrC+dQ==", "dev": true, "requires": {} }, @@ -4642,12 +4945,12 @@ "dev": true }, "espree": { - "version": "9.3.2", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.2.tgz", - "integrity": "sha512-D211tC7ZwouTIuY5x9XnS0E9sWNChB7IYKX/Xp5eQj3nFXhqmiUDB9q27y76oFl8jTg3pXcQx/bpxMfs3CIZbA==", + "version": "9.3.3", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.3.tgz", + "integrity": "sha512-ORs1Rt/uQTqUKjDdGCyrtYxbazf5umATSf/K4qxjmZHORR6HJk+2s/2Pqe+Kk49HHINC/xNIrGfgh8sZcll0ng==", "dev": true, "requires": { - "acorn": "^8.7.1", + "acorn": "^8.8.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.3.0" } @@ -4694,6 +4997,30 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "fast-glob": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", + "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, "fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -4706,6 +5033,15 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, "file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -4873,12 +5209,32 @@ "type-fest": "^0.20.2" } }, + "globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + } + }, "graceful-fs": { "version": "4.2.10", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", "dev": true }, + "grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "dev": true + }, "has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -4973,6 +5329,22 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, + "ip-address": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-8.1.0.tgz", + "integrity": "sha512-Wz91gZKpNKoXtqvY8ScarKYwhXoK4r/b5QuT+uywe/azv0/nUCo7Bh0IRRI7F9DHR06kJNWtzMGLIbXavngbKA==", + "requires": { + "jsbn": "1.1.0", + "sprintf-js": "1.1.2" + }, + "dependencies": { + "sprintf-js": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", + "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==" + } + } + }, "is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -5148,6 +5520,11 @@ "argparse": "^2.0.1" } }, + "jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==" + }, "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -5244,6 +5621,12 @@ "semver": "^6.0.0" } }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, "microformats-parser": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/microformats-parser/-/microformats-parser-1.4.1.tgz", @@ -5252,6 +5635,16 @@ "parse5": "^6.0.0" } }, + "micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, "mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -5649,6 +6042,12 @@ "isarray": "0.0.1" } }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + }, "picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -5790,6 +6189,12 @@ "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", "dev": true }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -5881,6 +6286,12 @@ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, "rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -5890,6 +6301,15 @@ "glob": "^7.1.3" } }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -5961,6 +6381,12 @@ "supports-color": "^7.2.0" } }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", diff --git a/package.json b/package.json index 824c0de..5a8ec23 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@squeep/indieauth-helper", "version": "1.1.5", - "description": "A minimal convenience-wrapper for the http requests and content parsing needed to talk IndieAuth.", + "description": "An opinionated convenience-wrapper for the http requests and content parsing needed to talk IndieAuth.", "main": "index.js", "scripts": { "coverage": "nyc npm test", @@ -29,13 +29,14 @@ "@squeep/web-linking": "^1.0.7", "axios": "^0.27.2", "iconv": "^3.0.1", + "ip-address": "^8.1.0", "microformats-parser": "^1.4.1" }, "devDependencies": { - "eslint": "^8.20.0", + "eslint": "^8.21.0", "eslint-plugin-node": "^11.1.0", "eslint-plugin-security": "^1.5.0", - "eslint-plugin-sonarjs": "^0.14.0", + "eslint-plugin-sonarjs": "^0.15.0", "mocha": "^10.0.0", "nyc": "^15.1.0", "pre-commit": "^1.2.2", diff --git a/test/lib/common.js b/test/lib/common.js index 05a0cfc..fe02407 100644 --- a/test/lib/common.js +++ b/test/lib/common.js @@ -88,4 +88,36 @@ describe('common', function () { }); }); // axiosResponseLogData + describe('setSymmetricDifference', function () { + it('covers difference', function () { + const setA = new Set([1, 2, 3]); + const setB = new Set([2, 3, 4]); + const expected = new Set([1, 4]); + const result = common.setSymmetricDifference(setA, setB); + assert(result.size); + assert.deepStrictEqual(result, expected); + }); + it('covers no difference', function () { + const setA = new Set([1, 2, 3, 4]); + const setB = new Set([1, 2, 3, 4]); + const expected = new Set(); + const result = common.setSymmetricDifference(setA, setB); + assert(!result.size); + assert.deepStrictEqual(result, expected); + }); + }); // setSymmetricDifference + + describe('properURLComponentName', function () { + it('maps proper names', function () { + [ + ['hash', 'fragment'], + ['protocol', 'scheme'], + ['host', 'host'], + ].forEach(([name, expected]) => { + const result = common.properURLComponentName(name); + assert.strictEqual(result, expected); + }); + }); + }); // properURLComponentName + }); // common \ No newline at end of file diff --git a/test/lib/communication.js b/test/lib/communication.js index 5c41469..f544d80 100644 --- a/test/lib/communication.js +++ b/test/lib/communication.js @@ -7,6 +7,8 @@ const assert = require('assert'); const sinon = require('sinon'); // eslint-disable-line node/no-unpublished-require const Communication = require('../../lib/communication'); +const { ValidationError } = require('../../lib/errors'); +const dns = require('dns'); const stubLogger = require('../stub-logger'); const testData = require('../test-data/communication'); @@ -516,6 +518,165 @@ describe('Communication', function () { }); }); // fetchJSON + describe('validateProfile', function () { + let url, validationOptions; + beforeEach(function () { + url = 'https://example.com/'; + options = {}; + sinon.stub(dns, 'lookupAsync').resolves([{ family: 4, address: '10.11.12.14' }]); + }); + it('rejects invalid url', async function () { + url = 'bad url'; + await assert.rejects(() => communication.validateProfile(url, validationOptions), ValidationError); + }); + it('covers success', async function () { + const result = await communication.validateProfile(url, validationOptions); + assert.strictEqual(result.isLoopback, false); + }); + it('rejects invalid', async function () { + url = 'ftp://example.com/'; + await assert.rejects(() => communication.validateProfile(url, validationOptions), ValidationError); + }); + + }); // validateProfile + + describe('validateClientIdentifier', function () { + let url, validationOptions; + beforeEach(function () { + url = 'https://example.com/'; + options = {}; + sinon.stub(dns, 'lookupAsync').resolves([{ family: 4, address: '10.11.12.13' }]); + }); + it('rejects invalid url', async function () { + try { + await communication.validateClientIdentifier('bad url'); + assert.fail(noExpectedException); + } catch (e) { + assert(e instanceof ValidationError); + } + }); + it('rejects invalid scheme', async function () { + url = 'ftp://example.com/'; + try { + await communication.validateClientIdentifier(url, validationOptions); + assert.fail(noExpectedException); + } catch (e) { + assert(e instanceof ValidationError); + } + }); + it('rejects fragment', async function () { + url = 'https://example.com/#foo'; + try { + await communication.validateClientIdentifier(url, validationOptions); + assert.fail(noExpectedException); + } catch (e) { + assert(e instanceof ValidationError); + } + }); + it('rejects username', async function () { + url = 'https://user@example.com/'; + try { + await communication.validateClientIdentifier(url, validationOptions); + assert.fail(noExpectedException); + } catch (e) { + assert(e instanceof ValidationError); + } + }); + it('rejects password', async function () { + url = 'https://:foo@example.com/'; + try { + await communication.validateClientIdentifier(url, validationOptions); + assert.fail(noExpectedException); + } catch (e) { + assert(e instanceof ValidationError); + } + }); + it('rejects relative path', async function () { + url = 'https://example.com/client/../sneaky'; + try { + await communication.validateClientIdentifier(url, validationOptions); + assert.fail(noExpectedException); + } catch (e) { + assert(e instanceof ValidationError); + } + }); + it('rejects ipv4', async function () { + url = 'https://10.11.12.13/'; + try { + await communication.validateClientIdentifier(url, validationOptions); + assert.fail(noExpectedException); + } catch (e) { + assert(e instanceof ValidationError); + } + }); + it('rejects ipv6', async function () { + url = 'https://[fd64:defa:00e5:caf4:0dff::ad39]/'; + try { + await communication.validateClientIdentifier(url, validationOptions); + assert.fail(noExpectedException); + } catch (e) { + assert(e instanceof ValidationError); + } + }); + it('accepts ipv4 loopback', async function () { + url = 'https://127.0.0.1/'; + const result = await communication.validateClientIdentifier(url, validationOptions); + assert.strictEqual(result.isLoopback, true); + }); + it('accepts ipv6 loopback', async function () { + url = 'https://[::1]/'; + const result = await communication.validateClientIdentifier(url, validationOptions); + assert.strictEqual(result.isLoopback, true); + }); + it('accepts resolved ipv4 loopback', async function () { + dns.lookupAsync.resolves([{ family: 4, address: '127.0.0.1' }]); + const result = await communication.validateClientIdentifier(url, validationOptions); + assert.strictEqual(result.isLoopback, true); + }); + it('accepts resolved ipv6 loopback', async function () { + dns.lookupAsync.resolves([{ family: 6, address: '::1' }]); + const result = await communication.validateClientIdentifier(url, validationOptions); + assert.strictEqual(result.isLoopback, true); + }); + it('covers success', async function () { + const result = await communication.validateClientIdentifier(url, validationOptions); + assert.strictEqual(result.isLoopback, false); + }); + it('rejects resolution failure', async function () { + dns.lookupAsync.rejects(new Error('oh no')); + try { + await communication.validateClientIdentifier(url, validationOptions); + assert.fail(noExpectedException); + } catch (e) { + assert(e instanceof ValidationError); + } + }); + it('rejects mismatched resolutions', async function () { + dns.lookupAsync.onCall(1).resolves([{ family: 4, address: '10.9.8.7' }]); + try { + await communication.validateClientIdentifier(url, validationOptions); + assert.fail(noExpectedException); + } catch (e) { + assert(e instanceof ValidationError); + } + }); + it('ignores unknown dns family', async function () { + dns.lookupAsync.resolves([{ family: 5, address: '10.9.8.7' }]); + const result = await communication.validateClientIdentifier(url, validationOptions); + assert.strictEqual(result.isLoopback, false); + }); + it('covers rooted hostname', async function() { + url = 'https://example.com./'; + const result = await communication.validateClientIdentifier(url, validationOptions); + assert.strictEqual(result.isLoopback, false); + }); + it('covers unresolved', async function () { + dns.lookupAsync.resolves(); + const result = await communication.validateClientIdentifier(url, validationOptions); + assert.strictEqual(result.isLoopback, false); + }); + }); // validateClientIdentifier + describe('fetchClientIdentifier', function () { let expected, response, result, urlObj; beforeEach(function () { @@ -592,6 +753,17 @@ describe('Communication', function () { result = await communication.fetchClientIdentifier(urlObj); assert.deepStrictEqual(result, expected); }); + it('covers loopback', async function () { + sinon.spy(communication, 'fetchMicroformat'); + urlObj.isLoopback = true; + expected = { + rels: {}, + items: [], + }; + result = await communication.fetchClientIdentifier(urlObj); + assert.deepStrictEqual(result, expected); + assert(communication.fetchMicroformat.notCalled); + }); }); // fetchClientIdentifier describe('fetchProfile', function () { diff --git a/test/lib/errors.js b/test/lib/errors.js new file mode 100644 index 0000000..2da1f80 --- /dev/null +++ b/test/lib/errors.js @@ -0,0 +1,14 @@ +/* eslint-env mocha */ +'use strict'; + +const assert = require('assert'); +const { ValidationError } = require('../../lib/errors'); + +describe('Errors', function () { + describe('ValidationError', function () { + it('covers', function () { + const e = new ValidationError(); + assert.strictEqual(e.name, 'ValidationError'); + }); + }); // ValidationError +}); // Errors \ No newline at end of file -- 2.43.2