From 24e3c792f399db7b1e9a42fd377332e45a64c682 Mon Sep 17 00:00:00 2001 From: Nazar Kovtun Date: Fri, 16 May 2025 17:05:00 +0300 Subject: [PATCH 1/3] HCK-10586: add ipv6 host braces escaping --- .../helpers/escapeUrlIpV6WithBraces.js | 47 +++++++++++++++++++ .../helpers/fetchIntrospectionSchema.js | 3 +- 2 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 reverse_engineering/helpers/escapeUrlIpV6WithBraces.js diff --git a/reverse_engineering/helpers/escapeUrlIpV6WithBraces.js b/reverse_engineering/helpers/escapeUrlIpV6WithBraces.js new file mode 100644 index 0000000..c9cba79 --- /dev/null +++ b/reverse_engineering/helpers/escapeUrlIpV6WithBraces.js @@ -0,0 +1,47 @@ +/** + * @param {string} url + * @returns {boolean} + */ +function isValidURL(url) { + try { + new URL(url); + + return true; + } catch { + return false; + } +} + +/** + * In case URL includes ip version 6 we need to escape the ip portion with square brackets before adding a port + * + * @param {{ + * url: string; + * }} param + * @returns {string} + */ +function escapeUrlIpV6WithBraces({ url }) { + const isUrlValid = isValidURL(url); + + if (isUrlValid) { + return url; + } + + const urlWithIpV6HostRegExp = new RegExp(/^http(s)?:\/\/(?([a-z0-9]{0,4}:?)+)/gim); + const { unescapedIpWithPort } = urlWithIpV6HostRegExp.exec(url)?.groups ?? {}; + + if (!unescapedIpWithPort) { + return url; + } + + const separatedIpPortionsAndPort = unescapedIpWithPort.split(':'); + const ipPortions = separatedIpPortionsAndPort.slice(0, separatedIpPortionsAndPort.length - 1); + const port = separatedIpPortionsAndPort.at(-1); + const escapedIpWithPort = `[${ipPortions.join(':')}]:${port}`; + + return url.replace(unescapedIpWithPort, escapedIpWithPort); +} + +module.exports = { + escapeUrlIpV6WithBraces, +}; diff --git a/reverse_engineering/helpers/fetchIntrospectionSchema.js b/reverse_engineering/helpers/fetchIntrospectionSchema.js index a41c9ad..a430933 100644 --- a/reverse_engineering/helpers/fetchIntrospectionSchema.js +++ b/reverse_engineering/helpers/fetchIntrospectionSchema.js @@ -6,6 +6,7 @@ const { getIntrospectionQuery } = require('graphql'); const { hckFetch } = require('@hackolade/fetch'); const { FetchIntrospectionSchemaError } = require('../errors/FetchIntrospectionSchemaError'); +const { escapeUrlIpV6WithBraces } = require('./escapeUrlIpV6WithBraces'); /** * Encode credentials to base64 for base authorization purposes @@ -78,7 +79,7 @@ async function fetchIntrospectionSchema({ connectionInfo }) { // If the URL is not provided, use the host keyword for backward compatibility with the less than 8.1.4 app version const url = connectionInfo.url || connectionInfo.host; - const response = await hckFetch(url, options); + const response = await hckFetch(escapeUrlIpV6WithBraces({ url }), options); return await parseResponse(response); } From 0bf87b9e6b1a32157c64b110055d9f29be230984 Mon Sep 17 00:00:00 2001 From: Nazar Kovtun Date: Fri, 16 May 2025 17:26:35 +0300 Subject: [PATCH 2/3] HCK-10586: replaced approach with groups --- reverse_engineering/helpers/escapeUrlIpV6WithBraces.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/reverse_engineering/helpers/escapeUrlIpV6WithBraces.js b/reverse_engineering/helpers/escapeUrlIpV6WithBraces.js index c9cba79..8bd5018 100644 --- a/reverse_engineering/helpers/escapeUrlIpV6WithBraces.js +++ b/reverse_engineering/helpers/escapeUrlIpV6WithBraces.js @@ -27,8 +27,8 @@ function escapeUrlIpV6WithBraces({ url }) { return url; } - const urlWithIpV6HostRegExp = new RegExp(/^http(s)?:\/\/(?([a-z0-9]{0,4}:?)+)/gim); - const { unescapedIpWithPort } = urlWithIpV6HostRegExp.exec(url)?.groups ?? {}; + const urlWithIpV6HostRegExp = new RegExp(/^http(s)?:\/\/(:?([a-z0-9]{0,4}:?)+)/gim); + const [unescapedIpWithPort] = url.match(urlWithIpV6HostRegExp) ?? []; if (!unescapedIpWithPort) { return url; From 721a6c1a83eb082b7ed224c69a9827f13cad266c Mon Sep 17 00:00:00 2001 From: Nazar Kovtun Date: Mon, 19 May 2025 13:51:57 +0300 Subject: [PATCH 3/3] HCK-10586: enchanced ipv6 escaping logic --- package-lock.json | 9 ++- package.json | 3 +- .../helpers/escapeUrlIpV6WithBraces.js | 47 -------------- .../helpers/escapeV6IpForURL.js | 64 +++++++++++++++++++ .../helpers/fetchIntrospectionSchema.js | 4 +- 5 files changed, 76 insertions(+), 51 deletions(-) delete mode 100644 reverse_engineering/helpers/escapeUrlIpV6WithBraces.js create mode 100644 reverse_engineering/helpers/escapeV6IpForURL.js diff --git a/package-lock.json b/package-lock.json index 07f20e4..368c0e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,8 @@ "hasInstallScript": true, "dependencies": { "@hackolade/fetch": "1.3.0", - "graphql": "16.10.0" + "graphql": "16.10.0", + "ip": "2.0.1" }, "devDependencies": { "@hackolade/hck-esbuild-plugins-pack": "0.0.1", @@ -3011,6 +3012,12 @@ "node": ">= 0.4" } }, + "node_modules/ip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz", + "integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==", + "license": "MIT" + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", diff --git a/package.json b/package.json index 7b6f296..9a5d237 100644 --- a/package.json +++ b/package.json @@ -98,7 +98,8 @@ "disabled": false, "dependencies": { "@hackolade/fetch": "1.3.0", - "graphql": "16.10.0" + "graphql": "16.10.0", + "ip": "2.0.1" }, "lint-staged": { "*.{js,ts,json}": "prettier --write" diff --git a/reverse_engineering/helpers/escapeUrlIpV6WithBraces.js b/reverse_engineering/helpers/escapeUrlIpV6WithBraces.js deleted file mode 100644 index 8bd5018..0000000 --- a/reverse_engineering/helpers/escapeUrlIpV6WithBraces.js +++ /dev/null @@ -1,47 +0,0 @@ -/** - * @param {string} url - * @returns {boolean} - */ -function isValidURL(url) { - try { - new URL(url); - - return true; - } catch { - return false; - } -} - -/** - * In case URL includes ip version 6 we need to escape the ip portion with square brackets before adding a port - * - * @param {{ - * url: string; - * }} param - * @returns {string} - */ -function escapeUrlIpV6WithBraces({ url }) { - const isUrlValid = isValidURL(url); - - if (isUrlValid) { - return url; - } - - const urlWithIpV6HostRegExp = new RegExp(/^http(s)?:\/\/(:?([a-z0-9]{0,4}:?)+)/gim); - const [unescapedIpWithPort] = url.match(urlWithIpV6HostRegExp) ?? []; - - if (!unescapedIpWithPort) { - return url; - } - - const separatedIpPortionsAndPort = unescapedIpWithPort.split(':'); - const ipPortions = separatedIpPortionsAndPort.slice(0, separatedIpPortionsAndPort.length - 1); - const port = separatedIpPortionsAndPort.at(-1); - const escapedIpWithPort = `[${ipPortions.join(':')}]:${port}`; - - return url.replace(unescapedIpWithPort, escapedIpWithPort); -} - -module.exports = { - escapeUrlIpV6WithBraces, -}; diff --git a/reverse_engineering/helpers/escapeV6IpForURL.js b/reverse_engineering/helpers/escapeV6IpForURL.js new file mode 100644 index 0000000..f5c2ac9 --- /dev/null +++ b/reverse_engineering/helpers/escapeV6IpForURL.js @@ -0,0 +1,64 @@ +const ip = require('ip'); + +/** + * @param {{ + * host: string; + * }} param + * @returns {string} + * @see https://en.wikipedia.org/wiki/IPv6_address + * Literal IPv6 addresses in resources (URLs): + * ------------------------------------------------ + * Colon (:) characters in IPv6 addresses may conflict with the established syntax of resource identifiers, + * such as URIs and URLs. The colon is conventionally used to terminate the host path before a port number.[10] + * To alleviate this conflict, literal IPv6 addresses are enclosed in square brackets in such resource identifiers; + * When the URL doesn't conatoin the port the notation is http://[2001:db8:85a3:8d3:1319:8a2e:370:7348]/ + * When the URL also contains a port number the notation is: https://[2001:db8:85a3:8d3:1319:8a2e:370:7348]:443/ + */ +function escapeV6IpForURL({ host }) { + /** + * If the host is already URL compatible then the ip lib will return false > ip.isV6Format('[::1]') false If the host + * is a proper ipv6 ip then the `new URL(host)` will fail with Uncaught TypeError: Invalid URL code: + * 'ERR_INVALID_URL', !ip.isV4Format(host) check required because isV6Format returns true for ipv4 address because of + * backward compatibility + */ + if (ip.isV6Format(host) && !ip.isV4Format(host)) { + return `[${host}]`; + } + + const isUrlValid = isValidURL(host); + if (isUrlValid) { + return host; + } + + const urlWithIpV6HostRegExp = new RegExp(/^http(s)?:\/\/(:?([a-z0-9]{0,4}:?)+)/gim); + const [unescapedIpWithPort] = host.match(urlWithIpV6HostRegExp) ?? []; + + if (!unescapedIpWithPort) { + return host; + } + + const separatedIpPortionsAndPort = unescapedIpWithPort.split(':'); + const ipPortions = separatedIpPortionsAndPort.slice(0, separatedIpPortionsAndPort.length - 1); + const port = separatedIpPortionsAndPort.at(-1); + const escapedIpWithPort = `[${ipPortions.join(':')}]:${port}`; + + return host.replace(unescapedIpWithPort, escapedIpWithPort); +} + +/** + * @param {string} url + * @returns {boolean} + */ +function isValidURL(url) { + try { + new URL(url); + + return true; + } catch { + return false; + } +} + +module.exports = { + escapeV6IpForURL, +}; diff --git a/reverse_engineering/helpers/fetchIntrospectionSchema.js b/reverse_engineering/helpers/fetchIntrospectionSchema.js index a430933..61612bb 100644 --- a/reverse_engineering/helpers/fetchIntrospectionSchema.js +++ b/reverse_engineering/helpers/fetchIntrospectionSchema.js @@ -6,7 +6,7 @@ const { getIntrospectionQuery } = require('graphql'); const { hckFetch } = require('@hackolade/fetch'); const { FetchIntrospectionSchemaError } = require('../errors/FetchIntrospectionSchemaError'); -const { escapeUrlIpV6WithBraces } = require('./escapeUrlIpV6WithBraces'); +const { escapeV6IpForURL } = require('./escapeV6IpForURL'); /** * Encode credentials to base64 for base authorization purposes @@ -79,7 +79,7 @@ async function fetchIntrospectionSchema({ connectionInfo }) { // If the URL is not provided, use the host keyword for backward compatibility with the less than 8.1.4 app version const url = connectionInfo.url || connectionInfo.host; - const response = await hckFetch(escapeUrlIpV6WithBraces({ url }), options); + const response = await hckFetch(escapeV6IpForURL({ host: url }), options); return await parseResponse(response); }