UNPKG

@open-condo/miniapp-utils

Version:

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

441 lines (433 loc) 16.1 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/helpers/proxying/index.ts var proxying_exports = {}; __export(proxying_exports, { createProxy: () => createProxy, getProxyHeadersForIp: () => getProxyHeadersForIp, getRequestIp: () => getRequestIp }); module.exports = __toCommonJS(proxying_exports); // src/helpers/proxying/utils.ts var import_jsonwebtoken = __toESM(require("jsonwebtoken")); var import_proxy_addr = __toESM(require("proxy-addr")); var import_zod = require("zod"); // 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); }; } // 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 specialNetsCache = {}; function isIPv4MappedAddress(address) { if (!("mapped" in specialNetsCache)) { specialNetsCache["mapped"] = createChecker2("::ffff:0:0/96"); } if (specialNetsCache["mapped"](address)) { const matches = address.match(mappedIpv4); return Boolean(matches && isIPv4(matches[2])); } return false; } // src/helpers/ip/index.ts function isInSubnet(address, subnetOrSubnets) { return createChecker3(subnetOrSubnets)(address); } function createChecker3(subnetOrSubnets) { if (!Array.isArray(subnetOrSubnets)) { return createChecker3([subnetOrSubnets]); } const subnetsByVersion = subnetOrSubnets.reduce( (acc, subnet) => { const ip = subnet.split("/")[0]; acc[isIP(ip)].push(subnet); return acc; }, { 0: [], 4: [], 6: [] } ); if (subnetsByVersion[0].length !== 0) { throw new Error(`some subnets are not valid IP addresses: ${subnetsByVersion[0]}`); } const check4 = createChecker(subnetsByVersion[4]); const check6 = createChecker2(subnetsByVersion[6]); return (address) => { if (!isIP(address)) { throw new Error(`not a valid IPv4 or IPv6 address: ${address}`); } if (isIPv6(address) && isIPv4MappedAddress(address)) { return check6(address) || check4(extractMappedIpv4(address)); } if (isIPv6(address)) { return check6(address); } else { return check4(address); } }; } // src/helpers/proxying/utils.ts var _ipSchema = import_zod.z.union([import_zod.z.ipv4(), import_zod.z.ipv6()]); var _timeStampBasicRegexp = /^\d+$/; var DEFAULT_PROXY_TIMEOUT_IN_MS = 5e3; var X_PROXY_ID_HEADER = "x-proxy-id"; var X_PROXY_IP_HEADER = "x-proxy-ip"; var X_PROXY_TIMESTAMP_HEADER = "x-proxy-timestamp"; var X_PROXY_SIGNATURE_HEADER = "x-proxy-signature"; function _getTimestampFromHeader(timestamp) { if (!_timeStampBasicRegexp.test(timestamp)) return Number.NaN; return new Date(parseInt(timestamp)).getTime(); } function _isProxyIP(ip, proxyConfig) { const addresses = Array.isArray(proxyConfig.address) ? proxyConfig.address : [proxyConfig.address]; const config = addresses.reduce((acc, addr) => { const isSubnet = addr.split("/").length > 1; if (isSubnet) { acc.subnets.push(addr); } else { acc.ips.push(addr); } return acc; }, { ips: [], subnets: [] }); if (config.ips.length && config.ips.includes(ip)) { return true; } return !!(config.subnets.length && isInSubnet(ip, config.subnets)); } function getRequestIp(req, trustProxyFn, knownProxies) { const originalIP = (0, import_proxy_addr.default)(req, trustProxyFn); if (!knownProxies) return originalIP; const xProxyId = req.headers[X_PROXY_ID_HEADER]; const xProxyIp = req.headers[X_PROXY_IP_HEADER]; const xProxyTimestamp = req.headers[X_PROXY_TIMESTAMP_HEADER]; const xProxySignature = req.headers[X_PROXY_SIGNATURE_HEADER]; if (typeof xProxyId !== "string" || typeof xProxyIp !== "string" || typeof xProxyTimestamp !== "string" || typeof xProxySignature !== "string") { return originalIP; } const { success: isValidIp } = _ipSchema.safeParse(xProxyIp); if (!isValidIp) { return originalIP; } const timestamp = _getTimestampFromHeader(xProxyTimestamp); const now = Date.now(); if (Number.isNaN(timestamp) || timestamp > now || now - timestamp > DEFAULT_PROXY_TIMEOUT_IN_MS) { return originalIP; } if (!Object.hasOwn(knownProxies, xProxyId)) { return originalIP; } const proxyConfig = knownProxies[xProxyId]; if (!proxyConfig || !_isProxyIP(originalIP, proxyConfig)) { return originalIP; } try { const jwtPayload = import_jsonwebtoken.default.verify(xProxySignature, proxyConfig.secret, { algorithms: ["HS256"] }); const expectedPayloadSchema = import_zod.z.object({ [X_PROXY_TIMESTAMP_HEADER]: import_zod.z.literal(xProxyTimestamp), [X_PROXY_IP_HEADER]: import_zod.z.literal(xProxyIp), [X_PROXY_ID_HEADER]: import_zod.z.literal(xProxyId), method: import_zod.z.literal(req.method), url: import_zod.z.literal(req.url) }); const { success: isMatchingSignature } = expectedPayloadSchema.safeParse(jwtPayload); return isMatchingSignature ? xProxyIp : originalIP; } catch { return originalIP; } } function getProxyHeadersForIp(method, url, ip, proxyId, secret) { const timestampString = String(Date.now()); return { [X_PROXY_IP_HEADER]: ip, [X_PROXY_ID_HEADER]: proxyId, [X_PROXY_TIMESTAMP_HEADER]: timestampString, [X_PROXY_SIGNATURE_HEADER]: import_jsonwebtoken.default.sign({ [X_PROXY_IP_HEADER]: ip, [X_PROXY_ID_HEADER]: proxyId, [X_PROXY_TIMESTAMP_HEADER]: timestampString, method, url }, secret, { expiresIn: Math.round(DEFAULT_PROXY_TIMEOUT_IN_MS / 1e3), algorithm: "HS256" }) }; } function isRelativeUrl(url) { return url.startsWith("/"); } function replaceUpstreamEndpoint({ endpoint, proxyPrefix, upstreamPrefix, upstreamOrigin, rewrites = {} }) { const isRelativeLocation = isRelativeUrl(endpoint); const locationUrl = new URL(endpoint, "https://_"); let targetLocation; const lookupUrl = new URL(endpoint, upstreamOrigin); lookupUrl.search = ""; if (isRelativeLocation || lookupUrl.origin === upstreamOrigin) { targetLocation ??= rewrites[lookupUrl.pathname]; } targetLocation ??= rewrites[lookupUrl.toString()]; if (targetLocation) { const isRelativeTarget = isRelativeUrl(targetLocation); const targetUrl = new URL(targetLocation, upstreamOrigin); const targetSearchParams = new URLSearchParams(targetUrl.searchParams); if (!targetUrl.hash) { targetUrl.hash = locationUrl.hash; } targetUrl.search = locationUrl.search; for (const [name] of targetSearchParams.entries()) { targetUrl.searchParams.delete(name); } for (const [name, value] of targetSearchParams.entries()) { targetUrl.searchParams.append(name, value); } if (isRelativeTarget) { return targetUrl.pathname + targetUrl.search + targetUrl.hash; } else { return targetUrl.toString(); } } if ((isRelativeLocation || locationUrl.origin === upstreamOrigin) && locationUrl.pathname.startsWith(upstreamPrefix)) { locationUrl.pathname = proxyPrefix + locationUrl.pathname.slice(upstreamPrefix.length); return locationUrl.pathname + locationUrl.search + locationUrl.hash; } return endpoint; } // src/helpers/proxying/proxy.ts var import_http_proxy = __toESM(require("http-proxy")); function createProxy(options) { const { name, proxyPrefix, upstreamOrigin, upstreamPrefix, ipProxying, locationRewrites, cookiePathRewrites, logger = console } = options; const proxy = import_http_proxy.default.createProxy({ target: upstreamOrigin, changeOrigin: true }); const trustProxyFn = (ipProxying == null ? void 0 : ipProxying.trustProxyFn) ?? (() => false); proxy.on("proxyReq", (proxyReq, req) => { var _a; if ((_a = req.url) == null ? void 0 : _a.startsWith(proxyPrefix)) { proxyReq.path = upstreamPrefix + req.url.slice(proxyPrefix.length); } proxyReq.setHeader("via", name); if (req.url && req.method && ipProxying) { const ip = getRequestIp(req, trustProxyFn, ipProxying.knownProxies); const headers = getProxyHeadersForIp(req.method, proxyReq.path, ip, ipProxying.proxyId, ipProxying.proxySecret); for (const [headerName, headerValue] of Object.entries(headers)) { proxyReq.setHeader(headerName, headerValue); } } }); proxy.on("proxyRes", (proxyRes, _req, _res) => { if (proxyRes.headers.location) { proxyRes.headers.location = replaceUpstreamEndpoint({ endpoint: proxyRes.headers.location, proxyPrefix, upstreamPrefix, upstreamOrigin, rewrites: locationRewrites }); } const setCookieHeaders = proxyRes.headers["set-cookie"]; if (setCookieHeaders) { proxyRes.headers["set-cookie"] = setCookieHeaders.map((cookieString) => { return cookieString.replace(/;\s*Path=([^;]+)/i, (match, pathValue) => { const rewrittenPath = replaceUpstreamEndpoint({ endpoint: pathValue, proxyPrefix, upstreamPrefix, upstreamOrigin, rewrites: cookiePathRewrites }); return match.replace(pathValue, rewrittenPath); }); }); } }); return function syncProxyHandler(req, res) { proxy.web(req, res, {}, (err) => { if (err) { logger.error({ msg: "Proxy error", err }); if (!res.headersSent) { res.writeHead(502, { "Content-Type": "application/json" }); res.end(JSON.stringify({ errors: [{ message: "Proxy error" }] })); } else { res.end(); } } }); }; } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { createProxy, getProxyHeadersForIp, getRequestIp }); //# sourceMappingURL=index.js.map