UNPKG

is-localhost-ip

Version:

Checks whether given DNS name or IPv4/IPv6 address belongs to a local machine

109 lines (94 loc) 3.4 kB
"use strict"; const { isIP, isIPv4 } = require("node:net"); const { createSocket } = require("node:dgram"); const { ADDRCONFIG } = require("node:dns"); const { lookup } = require("node:dns").promises; /** * Addresses reserved for private networks * @see {@link https://en.wikipedia.org/wiki/Private_network} * @see {@link https://en.wikipedia.org/wiki/Unique_local_address} */ const IP_RANGES = [ // 10.0.0.0 - 10.255.255.255 /^(:{2}f{4}:)?10(?:\.\d{1,3}){3}$/, // 127.0.0.0 - 127.255.255.255 /^(:{2}f{4}:)?127(?:\.\d{1,3}){3}$/, // 169.254.1.0 - 169.254.254.255 /^(::f{4}:)?169\.254\.([1-9]|1?\d\d|2[0-4]\d|25[0-4])\.\d{1,3}$/, // 172.16.0.0 - 172.31.255.255 /^(:{2}f{4}:)?(172\.1[6-9]|172\.2\d|172\.3[01])(?:\.\d{1,3}){2}$/, // 192.168.0.0 - 192.168.255.255 /^(:{2}f{4}:)?192\.168(?:\.\d{1,3}){2}$/, // fc00::/7 /^f[cd][\da-f]{2}(::1?$|:[\da-f]{1,4}){1,7}$/, // fe80::/10 /^fe[89ab][\da-f](::1?$|:[\da-f]{1,4}){1,7}$/, ]; // Concat all RegExes from above into one const IP_TESTER_RE = new RegExp(`^(${IP_RANGES.map((re) => re.source).join("|")})$`); /** * Syntax validation RegExp for possible valid host names. Permits underscore. * Maximum total length 253 symbols, maximum segment length 63 symbols * @see {@link https://en.wikipedia.org/wiki/Hostname} */ const VALID_HOSTNAME = // eslint-disable-next-line regexp/no-dupe-disjunctions /(?![\w-]{64})((^(?=[-\w.]{1,253}\.?$)((\w{1,63}|(\w[-\w]{0,61}\w))\.?)+$)(?<!\.{2}))/; /** * * @param {string} ip * @returns {Promise<boolean>} */ async function canBindToIp(ip) { const socket = createSocket(isIPv4(ip) ? "udp4" : "udp6"); return new Promise((resolve) => { try { socket .once("error", () => socket.close(() => resolve(false))) .once("listening", () => socket.close(() => resolve(true))) .unref() .bind(0, ip); } catch { socket.close(() => resolve(false)); } }); } /** * Checks if given strings is a local IP address or a DNS name that resolve into a local IP * * @param {string} ipOrHostname * @param {boolean} [canBind=false] - should check whether an interface with such address exists on the local machine * @returns {Promise.<boolean>} - true, if given strings is a local IP address or DNS names that resolves to local IP */ async function isLocalhost(ipOrHostname, canBind = false) { if (typeof ipOrHostname !== "string") { throw new TypeError("Invalid ip or hostname provided"); } // Removes [ and ] around ipv6 hostnames const normalizedIpOrHostname = ipOrHostname.replaceAll(/^\[|\]$/g, ""); // Check if given string is an IP address if (isIP(normalizedIpOrHostname)) { if (IP_TESTER_RE.test(normalizedIpOrHostname) && !canBind) return true; return canBindToIp(normalizedIpOrHostname); } // May it be a hostname? if (!VALID_HOSTNAME.test(normalizedIpOrHostname)) { throw new Error("Invalid ip or hostname provided"); } // it's a DNS name const addresses = await lookup(normalizedIpOrHostname, { all: true, family: 0, verbatim: true, hints: ADDRCONFIG, }); if (!Array.isArray(addresses)) { throw new TypeError("DNS Lookup failed."); } for (const { address } of addresses) { if (await isLocalhost(address, canBind)) return true; } return false; } module.exports = isLocalhost; module.exports.VALID_HOSTNAME = VALID_HOSTNAME; // for tests