102 lines
3.1 KiB
JavaScript
102 lines
3.1 KiB
JavaScript
'use strict';
|
|
|
|
const { isIP, isIPv4 } = require('net');
|
|
const { createSocket } = require('dgram');
|
|
const { ADDRCONFIG } = require('dns');
|
|
const { lookup } = require('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') return false;
|
|
|
|
// Check if given string is an IP address
|
|
if (isIP(ipOrHostname)) {
|
|
if (IP_TESTER_RE.test(ipOrHostname) && !canBind) return true;
|
|
return canBindToIp(ipOrHostname);
|
|
}
|
|
|
|
// May it be a hostname?
|
|
if (!VALID_HOSTNAME.test(ipOrHostname)) return false;
|
|
|
|
// it's a DNS name
|
|
try {
|
|
const addresses = await lookup(ipOrHostname, {
|
|
all: true,
|
|
family: 0,
|
|
verbatim: true,
|
|
hints: ADDRCONFIG,
|
|
});
|
|
if (!Array.isArray(addresses)) return false;
|
|
for (const { address } of addresses) {
|
|
if (await isLocalhost(address, canBind)) return true;
|
|
}
|
|
// eslint-disable-next-line no-empty
|
|
} catch {}
|
|
return false;
|
|
}
|
|
|
|
module.exports = isLocalhost;
|
|
module.exports.VALID_HOSTNAME = VALID_HOSTNAME; // for tests
|