UNPKG

@open-condo/miniapp-utils

Version:

A set of helper functions / components / hooks used to build new condo apps fast

394 lines (389 loc) 13.3 kB
// src/helpers/ip/ranges.ts var ranges_default = { /** localhost IP ranges */ localhost: { /** the localhost address ranges for IPv4 */ ipv4: ["127.0.0.0/8"], /** the localhost address ranges for IPv6 */ ipv6: ["::1/128"] }, /** private IP ranges */ private: { /** private address ranges for IPv4 */ ipv4: [ "10.0.0.0/8", // RFC 1918 "172.16.0.0/12", // RFC 1918 "192.168.0.0/16" // RFC 1918 ], /** private address ranges for IPv6 */ ipv6: [ "fe80::/10", // link-local address "fc00::/7" // unique local address (ULA) ] }, /** reserved IP ranges */ reserved: { /** reserved address ranges for IPv4 */ ipv4: [ "0.0.0.0/8", // broadcast "this" "100.64.0.0/10", // carrier-grade NAT "169.254.0.0/16", // DHCP fallback "192.0.0.0/24", // IANA Special Purpose Address Registry "192.0.2.0/24", // TEST-NET-1 for documentation examples "192.88.99.0/24", // deprecated 6to4 anycast relays "198.18.0.0/15", // for testing inter-network comms between two subnets "198.51.100.0/24", // TEST-NET-2 for documentation examples "203.0.113.0/24", // TEST-NET-3 for documentation examples "224.0.0.0/4", // multicast "240.0.0.0/4", // reserved unspecified "255.255.255.255/32" // limited broadcast address ], /** reserved address ranges for IPv6 */ ipv6: [ "::/128", // unspecified address "64:ff9b::/96", // IPv4/IPv6 translation "100::/64", // discard prefix "2001::/32", // Teredo tunneling "2001:10::/28", // deprecated "2001:20::/28", // ORCHIDv2 "2001:db8::/32", // for documentation and examples "2002::/16", // 6to4 "ff00::/8" // multicast ] } }; // src/helpers/ip/utils.ts var v4Seg = "(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])"; var v4Str = `(${v4Seg}[.]){3}${v4Seg}`; var IPv4Reg = new RegExp(`^${v4Str}$`); var v6Seg = "(?:[0-9a-fA-F]{1,4})"; var IPv6Reg = new RegExp( `^((?:${v6Seg}:){7}(?:${v6Seg}|:)|(?:${v6Seg}:){6}(?:${v4Str}|:${v6Seg}|:)|(?:${v6Seg}:){5}(?::${v4Str}|(:${v6Seg}){1,2}|:)|(?:${v6Seg}:){4}(?:(:${v6Seg}){0,1}:${v4Str}|(:${v6Seg}){1,3}|:)|(?:${v6Seg}:){3}(?:(:${v6Seg}){0,2}:${v4Str}|(:${v6Seg}){1,4}|:)|(?:${v6Seg}:){2}(?:(:${v6Seg}){0,3}:${v4Str}|(:${v6Seg}){1,5}|:)|(?:${v6Seg}:){1}(?:(:${v6Seg}){0,4}:${v4Str}|(:${v6Seg}){1,6}|:)|(?::((?::${v6Seg}){0,5}:${v4Str}|(?::${v6Seg}){1,7}|:)))(%[0-9a-zA-Z]{1,})?$` ); function isIPv4(s) { return IPv4Reg.test(s); } function isIPv6(s) { return IPv6Reg.test(s); } function isIP(s) { if (isIPv4(s)) return 4; if (isIPv6(s)) return 6; return 0; } // src/helpers/ip/ipv4.ts function ipv4ToLong(ip) { if (!isIPv4(ip)) { throw new Error(`not a valid IPv4 address: ${ip}`); } const octets = ip.split("."); return (parseInt(octets[0], 10) << 24) + (parseInt(octets[1], 10) << 16) + (parseInt(octets[2], 10) << 8) + parseInt(octets[3], 10) >>> 0; } function createLongChecker(subnet) { const [subnetAddress, prefixLengthString] = subnet.split("/"); const prefixLength = parseInt(prefixLengthString, 10); if (!subnetAddress || !Number.isInteger(prefixLength)) { throw new Error(`not a valid IPv4 subnet: ${subnet}`); } if (prefixLength < 0 || prefixLength > 32) { throw new Error(`not a valid IPv4 prefix length: ${prefixLength} (from ${subnet})`); } const subnetLong = ipv4ToLong(subnetAddress); return (addressLong) => { if (prefixLength === 0) { return true; } const subnetPrefix = subnetLong >> 32 - prefixLength; const addressPrefix = addressLong >> 32 - prefixLength; return subnetPrefix === addressPrefix; }; } function createChecker(subnetOrSubnets) { if (Array.isArray(subnetOrSubnets)) { const checks = subnetOrSubnets.map((subnet) => createLongChecker(subnet)); return (address) => { const addressLong = ipv4ToLong(address); return checks.some((check2) => check2(addressLong)); }; } const check = createLongChecker(subnetOrSubnets); return (address) => { const addressLong = ipv4ToLong(address); return check(addressLong); }; } var specialNetsCache = {}; function isLocalhost(address) { if (!("localhost" in specialNetsCache)) { specialNetsCache["localhost"] = createChecker(ranges_default.localhost.ipv4); } return specialNetsCache["localhost"](address); } function isSpecial(address) { if (!("special" in specialNetsCache)) { specialNetsCache["special"] = createChecker([ ...ranges_default.private.ipv4, ...ranges_default.localhost.ipv4, ...ranges_default.reserved.ipv4 ]); } return specialNetsCache["special"](address); } // src/helpers/ip/ipv6.ts var dot = /\./; var mappedIpv4 = /^(.+:ffff:)(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(?:%.+)?$/; var colon = /:/; var doubleColon = /::/; function mappedIpv4ToIpv6(ip) { const matches = ip.match(mappedIpv4); if (!matches || !isIPv4(matches[2])) { throw new Error(`not a mapped IPv4 address: ${ip}`); } const prefix = matches[1]; const ipv4 = matches[2]; const parts = ipv4.split(dot).map((x) => parseInt(x, 10)); const segment7 = ((parts[0] << 8) + parts[1]).toString(16); const segment8 = ((parts[2] << 8) + parts[3]).toString(16); return `${prefix}${segment7}:${segment8}`; } function extractMappedIpv4(ip) { const matches = ip.match(mappedIpv4); if (!matches || !isIPv4(matches[2])) { throw new Error(`not a mapped IPv4 address: ${ip}`); } return matches[2]; } function getIpv6Segments(ip) { if (!isIPv6(ip)) { throw new Error(`not a valid IPv6 address: ${ip}`); } if (dot.test(ip)) { return getIpv6Segments(mappedIpv4ToIpv6(ip)); } const [beforeChunk, afterChunk] = ip.split(doubleColon); const beforeParts = beforeChunk && beforeChunk.split(colon) || []; const afterParts = afterChunk && afterChunk.split(colon) || []; const missingSegments = new Array(8 - (beforeParts.length + afterParts.length)); return beforeParts.concat(missingSegments, afterParts); } function createChecker2(subnetOrSubnets) { if (Array.isArray(subnetOrSubnets)) { const checks = subnetOrSubnets.map((subnet) => createSegmentChecker(subnet)); return (address) => { const segments = getIpv6Segments(address); return checks.some((check2) => check2(segments)); }; } const check = createSegmentChecker(subnetOrSubnets); return (address) => { const segments = getIpv6Segments(address); return check(segments); }; } function createSegmentChecker(subnet) { const [subnetAddress, prefixLengthString] = subnet.split("/"); const prefixLength = parseInt(prefixLengthString, 10); if (!subnetAddress || !Number.isInteger(prefixLength)) { throw new Error(`not a valid IPv6 CIDR subnet: ${subnet}`); } if (prefixLength < 0 || prefixLength > 128) { throw new Error(`not a valid IPv6 prefix length: ${prefixLength} (from ${subnet})`); } const subnetSegments = getIpv6Segments(subnetAddress); return (addressSegments) => { for (let i = 0; i < 8; ++i) { const bitCount = Math.min(prefixLength - i * 16, 16); if (bitCount <= 0) { break; } const subnetPrefix = (subnetSegments[i] && parseInt(subnetSegments[i], 16) || 0) >> 16 - bitCount; const addressPrefix = (addressSegments[i] && parseInt(addressSegments[i], 16) || 0) >> 16 - bitCount; if (subnetPrefix !== addressPrefix) { return false; } } return true; }; } var specialNetsCache2 = {}; function isLocalhost2(address) { if (!("localhost" in specialNetsCache2)) { specialNetsCache2["localhost"] = createChecker2(ranges_default.localhost.ipv6); } return specialNetsCache2["localhost"](address); } function isIPv4MappedAddress(address) { if (!("mapped" in specialNetsCache2)) { specialNetsCache2["mapped"] = createChecker2("::ffff:0:0/96"); } if (specialNetsCache2["mapped"](address)) { const matches = address.match(mappedIpv4); return Boolean(matches && isIPv4(matches[2])); } return false; } function isSpecial2(address) { if (!("special" in specialNetsCache2)) { specialNetsCache2["special"] = createChecker2([ ...ranges_default.private.ipv6, ...ranges_default.localhost.ipv6, ...ranges_default.reserved.ipv6 ]); } return specialNetsCache2["special"](address); } // src/helpers/ip/index.ts function isLocalhost3(address) { if (isIPv6(address)) { if (isIPv4MappedAddress(address)) { return isLocalhost(extractMappedIpv4(address)); } return isLocalhost2(address); } else { return isLocalhost(address); } } function isSpecial3(address) { if (isIPv6(address)) { if (isIPv4MappedAddress(address)) { return isSpecial(extractMappedIpv4(address)); } return isSpecial2(address); } else { return isSpecial(address); } } // src/helpers/urls.ts var REGEXP_ESCAPE_CHARS = /[\\^$.*+?()[\]{}|]/g; var WILDCARD_REGEXP_PART = "([a-zA-Z0-9-]{1,63})"; var WILDCARD_REGEXP_PART_ESCAPED = _escapeRegexp(WILDCARD_REGEXP_PART); function isSafeUrl(url) { if (!url || typeof url !== "string") return false; let decodedUrl; try { decodedUrl = decodeURI(url); } catch (error) { return false; } const normalizedUrl = decodedUrl.replace(/[\u0000-\u001F\s]/g, "").toLowerCase(); return !normalizedUrl.includes("javascript:"); } function replaceDomainPrefix(originalUrl, prefix) { const url = new URL(originalUrl); const originalHostnameParts = url.hostname.split("."); const suffixParts = originalHostnameParts.length > 2 ? originalHostnameParts.slice(1) : originalHostnameParts; const suffix = suffixParts.join("."); url.hostname = `${prefix}.${suffix}`; return url.toString(); } function _escapeRegexp(source) { return source.replace(REGEXP_ESCAPE_CHARS, "\\$&"); } function _getUrl(strUrl, cache) { if (!cache) return new URL(strUrl); let parsed = cache.get(strUrl); if (!parsed) { parsed = new URL(strUrl); cache.set(strUrl, parsed); } return parsed; } function _getCachedRegexp(pattern, cache) { if (!cache) return new RegExp(pattern, "g"); let re = cache.get(pattern); if (!re) { re = new RegExp(pattern, "g"); cache.set(pattern, re); } re.lastIndex = 0; return re; } function getWildcardDomain(domain, cache) { const url = _getUrl(domain, cache); const [prefix, ...rest] = url.hostname.split("."); const port = url.port ? `:${url.port}` : ""; return { wildcardDomain: `${url.protocol}//*.${rest.join(".")}${port}`, prefix }; } function replaceDomain(source, from, to, options = {}) { const { encoded = false, urlsCache, regexpsCache } = options; const fromUrl = _getUrl(from, urlsCache); const decodedHostname = decodeURIComponent(fromUrl.hostname); if (!decodedHostname.startsWith("*.")) { const escapedFrom2 = _escapeRegexp(from); const fromWithBoundary2 = `${escapedFrom2}(?!\\.${WILDCARD_REGEXP_PART}|${WILDCARD_REGEXP_PART})`; const fromSearch2 = _getCachedRegexp(fromWithBoundary2, regexpsCache); let replaced2 = source.replace(fromSearch2, to); if (encoded) { const fromEncoded = encodeURIComponent(from); const escapedFromEncoded = _escapeRegexp(fromEncoded); const fromEncodedWithBoundary = `${escapedFromEncoded}(?!%2E${WILDCARD_REGEXP_PART}|${WILDCARD_REGEXP_PART})`; const fromEncodedSearch = _getCachedRegexp(fromEncodedWithBoundary, regexpsCache); replaced2 = replaced2.replace(fromEncodedSearch, encodeURIComponent(to)); } return replaced2; } const fromPattern = from.replace("*", WILDCARD_REGEXP_PART); const toPattern = to.replace("*", "$1"); const escapedFrom = _escapeRegexp(fromPattern).replace(WILDCARD_REGEXP_PART_ESCAPED, WILDCARD_REGEXP_PART); const fromWithBoundary = `${escapedFrom}(?!\\.${WILDCARD_REGEXP_PART}|${WILDCARD_REGEXP_PART})`; const fromSearch = _getCachedRegexp(fromWithBoundary, regexpsCache); let replaced = source.replace(fromSearch, toPattern); if (encoded) { const fromEncoded = encodeURIComponent(from).replace("*", WILDCARD_REGEXP_PART); const toEncoded = encodeURIComponent(to).replace("*", "$1"); const escapedFromEncoded = _escapeRegexp(fromEncoded).replace(WILDCARD_REGEXP_PART_ESCAPED, WILDCARD_REGEXP_PART); const fromEncodedWithBoundary = `${escapedFromEncoded}(?!%2E${WILDCARD_REGEXP_PART}|${WILDCARD_REGEXP_PART})`; const fromEncodedSearch = _getCachedRegexp(fromEncodedWithBoundary, regexpsCache); replaced = replaced.replace(fromEncodedSearch, toEncoded); } return replaced; } function getUrlMeta(url) { try { const u = new URL(url); const hostnameUnWrapped = u.hostname.startsWith("[") && u.hostname.endsWith("]") ? u.hostname.slice(1, -1) : u.hostname; u.isIP = isIP(hostnameUnWrapped) > 0; u.isSpecialIP = u.isIP && isSpecial3(hostnameUnWrapped); u.isLocalhost = u.hostname === "localhost" || u.isIP && isLocalhost3(hostnameUnWrapped); return u; } catch { return null; } } export { getUrlMeta, getWildcardDomain, isSafeUrl, replaceDomain, replaceDomainPrefix }; //# sourceMappingURL=urls.mjs.map