diff --git a/package-lock.json b/package-lock.json index 2e62da7..e450988 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "async": "3.2.6", "cassandra-driver": "4.7.2", "fast-xml-parser": "4.4.1", + "ip": "2.0.1", "jks-js": "1.1.3", "lodash": "4.17.21" }, @@ -2695,6 +2696,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.4", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", diff --git a/package.json b/package.json index 78da233..a93f940 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,8 @@ "cassandra-driver": "4.7.2", "fast-xml-parser": "4.4.1", "jks-js": "1.1.3", - "lodash": "4.17.21" + "lodash": "4.17.21", + "ip": "2.0.1" }, "lint-staged": { "*.{js,json}": "prettier --write" diff --git a/reverse_engineering/cassandraHelper.js b/reverse_engineering/cassandraHelper.js index e015db6..c07fd79 100644 --- a/reverse_engineering/cassandraHelper.js +++ b/reverse_engineering/cassandraHelper.js @@ -7,6 +7,7 @@ const { getEntityLevelConfig } = require('../helpers/levelConfigHelper'); const CassandraRetryPolicy = require('./cassandraRetryPolicy'); const xmlParser = require('fast-xml-parser'); const filterComplexUdt = require('./helpers/filterComplexUdt'); +const { escapeV6IpForURL } = require('./helpers/escapeV6IPForURL'); const state = { client: null, @@ -270,7 +271,7 @@ module.exports = () => { ? { username: 'token', password: info.astraToken } : { username: info.user, password: info.password }; const authProvider = new cassandra.auth.PlainTextAuthProvider(credentials.username, credentials.password); - const contactPoints = info.hosts.map(item => `${item.host}:${item.port}`); + const contactPoints = info.hosts.map(item => `${escapeV6IpForURL({ host: item.host })}:${item.port}`); const readTimeout = validateRequestTimeout(info.requestTimeout, info.queryRequestTimeout); return getSslOptions(info, app, logger).then(sslOptions => { diff --git a/reverse_engineering/helpers/escapeV6IPForURL.js b/reverse_engineering/helpers/escapeV6IPForURL.js new file mode 100644 index 0000000..6ee75ef --- /dev/null +++ b/reverse_engineering/helpers/escapeV6IPForURL.js @@ -0,0 +1,64 @@ +const ip = require('ip'); + +/** + * @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/ + * + * @param {{ +* host: string +* }} param +* @returns {string} +*/ +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 } = urlWithIpV6HostRegExp.exec(host)?.groups ?? {}; + + 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, +};