UNPKG

pick-port

Version:

Get a free TCP or UDP port for the given IP address

106 lines (105 loc) 4.02 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.pickPort = pickPort; const net = require("node:net"); const crypto = require("node:crypto"); const Logger_1 = require("./Logger"); const tcp_1 = require("./tcp"); const udp_1 = require("./udp"); const logger = new Logger_1.Logger(); // Store picked ports for the specified reserveTimeout time. // This Set stores strings/hashes with the form "type:ip:port". const reserved = new Set(); // Last reserved port (used to optimize the random port lookup). let lastReservedPort = undefined; async function pickPort({ type, ip = '0.0.0.0', minPort = 10000, maxPort = 20000, reserveTimeout = 5, }) { logger.debug(`pickPort() [type:${type}, ip:${ip}, minPort:${minPort}, maxPort:${maxPort}, reserveTimeout:${reserveTimeout}]`); // Sanity checks. type = type.toLowerCase(); const family = net.isIP(ip); if (type !== 'udp' && type !== 'tcp') { throw new TypeError('invalid type parameter'); } else if (family !== 4 && family !== 6) { throw new TypeError('invalid ip parameter'); } else if (typeof minPort !== 'number' || typeof maxPort !== 'number' || minPort > maxPort) { throw new TypeError('invalid minPort/maxPort parameter'); } else if (typeof reserveTimeout !== 'number') { throw new TypeError('invalid reserveTimeout parameter'); } // If last reserved port is not in the given min/max port range, unset it. if (lastReservedPort !== undefined && (lastReservedPort < minPort || lastReservedPort > maxPort)) { lastReservedPort = undefined; } // Take a random port in the range. // NOTE: Use last reserved port (if any) as initial value since it will be // incremented at the end of the loop below. let port = lastReservedPort ?? crypto.randomInt(minPort, maxPort + 1); let retries = maxPort - minPort + 1; while (--retries >= 0) { // Keep the port within the range. if (++port > maxPort) { port = minPort; } const hash = generateHash(type, ip, port); // If current port is reserved, try next one. if (isReserved(hash)) { continue; } // Optimistically reserve the port. reserve(hash); try { switch (type) { case 'tcp': { await (0, tcp_1.reserve)(ip, port); break; } case 'udp': { await (0, udp_1.reserve)(ip, port, family); break; } } logger.debug(`pickPort() | got available port [type:${type}, ip:${ip}, port:${port}]`); lastReservedPort = port; // Unreserve the reserved port after given timeout. setTimeout(() => unreserve(hash), reserveTimeout * 1000); return port; } catch (error) { unreserve(hash); if (error.code === 'EADDRINUSE') { logger.debug(`pickPort() | port in use [type:${type}, ip:${ip}, port:${port}]`); continue; } else { logger.warn(`pickPort() | unexpected error trying to bind a port [type:${type}, ip:${ip}, port:${port}]: ${error}`); throw error; } } } logger.warn(`pickPort() | no available port in the given port range [type:${type}, ip:${ip}]`); throw new Error('no available port in the given port range'); } function generateHash(type, ip, port) { return `${type}:${ip}:${port}`; } function reserve(hash) { if (isReserved(hash)) { throw new Error(`reserve() | hash '${hash}' is already reserved`); } reserved.add(hash); } function unreserve(hash) { if (!isReserved(hash)) { throw new Error(`unreserve() | hash '${hash}' is not reserved`); } reserved.delete(hash); } function isReserved(hash) { return reserved.has(hash); }