@controlplane/cli
Version:
Control Plane Corporation CLI
715 lines • 36.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PortForwarder = void 0;
exports.deduplicateAddresses = deduplicateAddresses;
exports.bindServer = bindServer;
const net = require("net");
const WebSocket = require("ws");
const objects_1 = require("../util/objects");
const resolver_1 = require("../commands/resolver");
const client_1 = require("../session/client");
const helpers_1 = require("../terminal-server/helpers");
const async_mutex_1 = require("async-mutex");
const auth_1 = require("../login/auth");
// ANCHOR - Constants
const PORT_PATTERN = /^(6553[0-5]|655[0-2]\d|65[0-4]\d{2}|6[0-4]\d{3}|[1-5]\d{4}|\d{1,4})$/;
const LOCALHOST_IPV4_ADDRESS = '127.0.0.1';
const LOCALHOST_IPV6_ADDRESS = '::1';
var PortForwardErrorType;
(function (PortForwardErrorType) {
PortForwardErrorType[PortForwardErrorType["Fatal"] = 0] = "Fatal";
PortForwardErrorType[PortForwardErrorType["TicketExpired"] = 1] = "TicketExpired";
})(PortForwardErrorType || (PortForwardErrorType = {}));
// ANCHOR - PortForwarder
/**
* Forwards one or more local ports to a workload over the terminal server's
* WebSocket port-forward protocol.
*/
class PortForwarder {
constructor(client, session, quiet = false) {
this.client = client;
this.session = session;
this.portRequestMap = new Map();
this.portMutexMap = new Map();
this.portTicketMap = new Map();
this.servers = [];
this.quiet = quiet;
}
// Public Methods //
/**
* Validates inputs, resolves the location, and binds a local TCP listener per port mapping.
* Resolves once all listeners are bound; the caller keeps this instance and calls {@link stop}
* to tear it down.
*
* @param options - The workload, port mappings, and bind addresses to forward.
* @returns The bound local addresses (address + OS-assigned port) per listener .
*/
async start(options) {
// Format ports correctly
const ports = (0, objects_1.toArray)(options.ports);
// Validate options
this.validate(ports, options.addresses);
// Resolve to the concrete bind addresses, tracking which ones came from localhost expansion
const finalBindAddresses = deduplicateAddresses(options.addresses);
// Default to first location if no location is specified
const location = await this.resolveLocation(options.location);
// Create servers for each address and listen on ports, returning the bound addresses
return await this.listenOnAddresses(ports, finalBindAddresses, options.workload, location, options.replica);
}
/**
* Stops the forward, closing every TCP server it opened.
*/
stop() {
for (const server of this.servers) {
try {
server.close();
}
catch (_a) {
// Ignore close errors; the server may already be closed
}
}
}
// Private Methods //
printDefaults(type, name) {
if (this.quiet) {
return;
}
this.session.err((0, helpers_1.defaultingToMessage)(type, name));
}
/**
* Iterates over the provided ports and addresses, setting up TCP servers
* to listen on the specified addresses and forward traffic via WebSocket.
*
* @param ports - List of port mappings to listen on.
* @param addresses - List of addresses to bind the listeners to.
* @param workloadName - The workload name to forward traffic to.
* @param location - The location in which the workload exists.
* @param replica - Optional replica of the specified workload.
* @returns The bound local addresses (address + OS-assigned port) for every listener, so
* callers can recover an OS-assigned local port (see {@link start}).
*/
async listenOnAddresses(ports, addresses, workloadName, location, replica) {
// Get deployment remote endpoint using the specified location
const remoteHost = await this.resolveDeploymentEndpoint(workloadName, location);
// Create request configuration
const config = await this.createConfig(remoteHost, workloadName, replica);
// Collect each successful bind's local address (address + OS-assigned port) so start() can
// hand it back. A caller that requested an OS-assigned port (empty local port) otherwise
// has no way to learn which port the OS chose.
const boundAddresses = [];
// For each port mapping, parse and start a TCP server
for (const mappingStr of ports) {
let mapping;
// Try to parse the port mapping string to an object
try {
mapping = this.parsePortMapping(mappingStr);
}
catch (e) {
throw new Error(`Invalid port mapping "${mappingStr}": ${e.message}`);
}
// Initialize the request object
let request;
// Use cached request or create a new one
if (this.portRequestMap.has(mapping.remotePort)) {
request = this.portRequestMap.get(mapping.remotePort);
}
else {
request = {
...config.request,
port: mapping.remotePort,
};
// Cache the request so it can be uniquely tied to the remote port
this.portRequestMap.set(mapping.remotePort, request);
}
// Add a new record to the port mutex map so that we have a unique mutex for each remote port
if (!this.portMutexMap.has(mapping.remotePort)) {
this.portMutexMap.set(mapping.remotePort, new async_mutex_1.Mutex());
}
// Per-group bind counters mirroring kubectl's failureMode grouping in
// k8s.io/client-go/tools/portforward.listenOnPort: the localhost-expanded group
// tolerates a partial bind as long as at least one of its addresses succeeded;
// an explicit address failure aborts immediately on its own.
let localhostSuccesses = 0;
let localhostFailures = 0;
// For each deduplicated bind address, create a TCP server
for (const { address: bindAddress, fromLocalhostExpansion } of addresses) {
// Create a TCP server for this port mapping
const server = net.createServer((localSocket) => {
// Save the ticket for this connection to determine if a refresh is needed,
// preventing simultaneous refresh attempts from multiple connections
let connectionTicket = undefined;
// Extract port number from the local socket
const localPort = localSocket.address().port;
// Log the connection
if (!this.quiet) {
this.session.out(`Handling connection for ${localPort}`);
}
// Create a new WebSocket client
const client = new WebSocket(config.remoteSocket, { origin: 'http://localhost' });
// Handle WebSocket events
client.on('open', async () => {
// Refresh token if necessary for the defined request
request.token = await this.refreshToken();
// Print part of the token for debug purposes
client_1.wire.debug('<<<<<<< WS Open Event with Token: ' + (0, client_1.convertAuthTokenForDebug)(request.token));
// Notify the user of the start of event locking
client_1.wire.debug('<<<<<<< WS Open Event Locking...');
// Use the mutex to prevent race conditions
const release = await this.portMutexMap.get(mapping.remotePort).acquire();
// Create a ticket and attach it to the request object ONLY if it is not specified there
if (!request.ticket) {
// Create a ticket so we can pass it on every port-forward request
const ticket = await this.resolveTicketForPort(remoteHost, workloadName, mapping.remotePort, replica);
// Attach the ticket to the request object before sending it,
// this way the ticket creation is done only once throughout the whole on-going connection to the specified remote port
request.ticket = ticket;
}
// After creating a ticket, release the lock
// If a waiting connection has the same remote port, it will use the new ticket
release();
// Notify the user that the event lock ended
client_1.wire.debug('<<<<<<< WS Open Event Lock Released!');
// Set the connection ticket
connectionTicket = request.ticket;
// Initialize Request
client.send(JSON.stringify(request));
// Handle local server events
localSocket.on('data', (chunk) => {
client.send(chunk);
});
localSocket.on('error', (err) => {
if (!err.message) {
// If we are unable to extract the error message, then log the entire error object using console.error
console.error(err);
return;
}
// Log the error message to the user
this.session.err(`Local server error during connection handling on port ${localPort}: ${err.message}`);
});
});
client.on('error', (event) => {
client_1.wire.debug('<<<<<<< WS Error Event info');
// Handle connection refused error differently
if (event.hasOwnProperty('code') && event.code === 'ECONNREFUSED') {
this.session.err(`error: connection refused by server, please try again.`);
return;
}
// Show the error object for any other error
this.session.err('error: lost connection to the server, details below:');
console.error(event);
});
client.on('close', () => {
client_1.wire.debug('<<<<<<< WS Close Event Triggered');
localSocket.end();
localSocket.destroy();
});
// Handle WebSocket events
client.on('message', async (data) => {
client_1.wire.debug('<<<<<<< WS Message Received');
// Check if the received message is representing an error
const message = data.toString();
// Detect if there is a backend error
const errorType = this.detectPortForwardError(message);
// Handle each error type differently
switch (errorType) {
case PortForwardErrorType.TicketExpired:
client_1.wire.debug('<<<<<<< WS Message Event Locking...');
// Lock so we avoid refreshing the ticket multiple times when there is a race condition
const release = await this.portMutexMap.get(mapping.remotePort).acquire();
// After a lock is released, check if the ticket was refreshed. The way we will achieve this is by
// comparing the connection's ticket (initially matching the request ticket) with the current request ticket.
// If they differ, update the connection's ticket and return, indicating that a refresh is unnecessary
if (connectionTicket != request.ticket) {
connectionTicket = request.ticket;
release();
return;
}
// Refresh the ticket for the specified request
await this.refreshTicket(request, remoteHost, workloadName, mapping.remotePort, replica);
// Unlock after refreshing the ticket
release();
client_1.wire.debug('<<<<<<< WS Message Event Lock Released!');
return;
case PortForwardErrorType.Fatal:
// Signal the process to terminate on fatal errors
this.session.abort({ message });
}
// Write incoming data to the local socket
if (typeof data === 'string') {
localSocket.write(data);
}
else if (data instanceof Buffer) {
localSocket.write(data);
}
});
});
// Determine local port: if not provided, use 0 (random)
const localPortToBind = mapping.localPort !== undefined ? mapping.localPort : 0;
// Attempt to bind the server; the helper attaches its own one-shot 'error' / 'listening'
// handlers so a bind failure resolves the promise instead of crashing the process.
const bindResult = await bindServer(server, localPortToBind, bindAddress);
// Handle bind failure
if (!bindResult.ok) {
// For localhost-expanded addresses (the default), tolerate a single-family failure
// (e.g. ::1 unbindable in containers where IPv6 loopback is disabled) and continue
if (fromLocalhostExpansion) {
localhostFailures++;
this.session.err(`WARN: Could not bind on ${bindAddress}:${localPortToBind} (${bindResult.error.code || bindResult.error.message}). Continuing on remaining addresses.`);
continue;
}
// For an address the user explicitly requested, treat the bind failure as fatal
throw new Error(`ERROR: Failed to bind on ${bindAddress}:${localPortToBind}: ${bindResult.error.message}`);
}
// Track the bound server so the caller can close it later
this.servers.push(server);
// Record the bound local address so start() can report it back to the caller
boundAddresses.push(bindResult.info);
// Count this bind in the appropriate group for the post-loop kubectl-style decision
if (fromLocalhostExpansion) {
localhostSuccesses++;
}
// Attach a long-lived error handler so post-listen failures are logged instead of
// crashing the process via an unhandled 'error' event
server.on('error', (err) => {
this.session.err(`Server error on ${bindAddress}:${bindResult.info.port}: ${err.message}`);
});
// Extract the bind address for display
let bindAddrStr = bindResult.info.address;
// Wrap IPv6 addresses with square brackets
if (bindResult.info.family === 'IPv6') {
bindAddrStr = `[${bindAddrStr}]`;
}
// Log the forwarding information
if (!this.quiet) {
this.session.out(`Forwarding from ${bindAddrStr}:${bindResult.info.port} -> ${mapping.remotePort}`);
}
}
// Mirror kubectl's localhost-group check: if the user asked for localhost (so at
// least one expanded address was attempted) but none of those expansions succeeded,
// the documented contract is to fail — even if some other explicit address bound.
if (localhostFailures > 0 && localhostSuccesses === 0) {
throw new Error(`ERROR: Could not bind any of the localhost addresses (127.0.0.1, ::1) for port mapping "${mappingStr}".`);
}
}
return boundAddresses;
}
/**
* Create a port forward configuration object.
*
* @param {string} remoteHost The remote endpoint of the selected deployment.
* @param {string} workloadName The workload name.
* @param {string} replica The name of the workload deployment replica.
* @returns {Promise<PortForwardConfig>} A promise that resolves to a PortForwardConfig object.
*/
async createConfig(remoteHost, workloadName, replica) {
// Determine WebSocket endpoint
const remoteSocket = `${remoteHost.replace('https://', 'wss://')}/portforward`;
// Prepare token
const token = await this.refreshToken();
// Construct the result and return it
return {
remoteSocket: remoteSocket,
request: {
contextId: this.session.id,
token,
org: this.session.context.org,
gvc: this.session.context.gvc,
workload: workloadName,
pod: replica,
},
};
}
/**
* Refreshes the authentication token by generating a new authorization header and extracting the token.
*
* @returns {Promise<string>} The raw token string without the 'Bearer ' prefix.
*/
async refreshToken() {
// Generate authorization header string using current session profile
const authHeader = await (0, auth_1.makeAuthorizationHeader)(this.session.profile);
// Strip the 'Bearer ' prefix from the header to get the token
return authHeader.replace('Bearer ', '');
}
/**
* Resolves the provided location or defaults to the first available location within the GVC.
*
* @param location - The optional location to be resolved.
* @returns The resolved location name.
*/
async resolveLocation(location) {
var _a, _b;
// If location is specified, no need to find one
if (location) {
return location;
}
// Resolve GVC link
const gvcSelfLink = (0, resolver_1.resolveToLink)('gvc', this.session.context.gvc, this.session.context);
// Fetch GVC
const gvc = await this.client.get(gvcSelfLink);
// Determine location self link
let locationSelfLink = '';
// Extract the first location out of the GVC, abort if GVC has no locations
if (((_b = (_a = gvc.spec) === null || _a === void 0 ? void 0 : _a.staticPlacement) === null || _b === void 0 ? void 0 : _b.locationLinks) && gvc.spec.staticPlacement.locationLinks.length > 0) {
locationSelfLink = gvc.spec.staticPlacement.locationLinks[0];
}
else {
throw new Error('ERROR: Gvc has no locations');
}
// Extract location name from location self link
location = locationSelfLink.split('/location/')[1];
// Log the location that was picked
this.printDefaults('location', location);
// Return the location that has been picked
return location;
}
/**
* Retrieves the remote deployment endpoint for a given workload in a specified location.
*
* @param workloadName - The name of the workload.
* @param location - The location of the deployment.
* @returns The remote host endpoint of the deployment.
*/
async resolveDeploymentEndpoint(workloadName, location) {
// Use the remote host from an environment variable if specified
if (process.env.TERMINAL_SERVER_URL) {
return process.env.TERMINAL_SERVER_URL;
}
// Construct workload self link
const workloadSelfLink = (0, resolver_1.kindResolver)('workload').resourceLink(workloadName, this.session.context);
// Determine deployment self link
const deploymentSelfLink = `${workloadSelfLink}/deployment/${location}`;
// Fetch deployment
const deployment = await this.client.get(deploymentSelfLink);
// Fail if the deployment is invalid
if (!deployment.status || !deployment.status.remote) {
throw new Error('ERROR: No deployment is present in the workload.');
}
// Extract and return remote host endpoint from deployment status
return deployment.status.remote;
}
/**
* Resolves a port-forwarding ticket for a given port, either by retrieving an
* existing ticket or creating a new one.
*
* @param {string} remoteHost - The remote endpoint of the selected deployment.
* @param {string} workloadName - The name of the workload associated with the port.
* @param {number} port - The port that requires a forwarding ticket.
* @param {string | undefined} replica - (Optional) The specific workload replica, if applicable.
* @returns {string} A promise that resolves to the port-forwarding ticket.
*/
async resolveTicketForPort(remoteHost, workloadName, port, replica) {
// Use the cached ticket for the remote port if it has been created before
if (this.portTicketMap.has(port)) {
return this.portTicketMap.get(port);
}
// Create the ticket
const ticket = await this.createTicket(remoteHost, workloadName, port, replica);
// Cache and store the ticket so we don't have to create another ticket for the same port
this.portTicketMap.set(port, ticket);
// Return the ticket
return ticket;
}
/**
* Creates a new ticket, assigns it to the request object and updates the port ticket map.
*
* @param {PortForwardRequest} request - The request object that is being used to establish the port-forwarding connection.
* @param {string} remoteHost - The remote endpoint of the selected deployment.
* @param {string} workloadName - The name of the workload associated with the port.
* @param {number} port - The port that requires a forwarding ticket.
* @param {string | undefined} replica - (Optional) The specific workload replica, if applicable.
*/
async refreshTicket(request, remoteHost, workloadName, port, replica) {
client_1.wire.debug(`<<<<<<< Port-forward refreshing ticket for port: ${port}`);
// Create a new ticket
const ticket = await this.createTicket(remoteHost, workloadName, port, replica);
// Update the request object so on future connections, the new ticket will be used
request.ticket = ticket;
// Update the port ticket mapping so on future connections, the new ticket will be used.
// (Use the Map API — `this.portTicketMap[port] = …` sets a plain property that
// `resolveTicketForPort`'s `.get()`/`.has()` would never see, leaving the cache stale.)
this.portTicketMap.set(port, ticket);
}
/**
* Creates a port-forwarding ticket for a given port.
*
* @param {string} remoteHost - The remote endpoint of the selected deployment.
* @param {string} workloadName - The name of the workload associated with the port.
* @param {number} port - The port that requires a forwarding ticket.
* @param {string | undefined} replica - (Optional) The specific workload replica, if applicable.
* @returns {string} A promise that resolves to the port-forwarding ticket.
*/
async createTicket(remoteHost, workloadName, port, replica) {
// Construct the request object
const requestBody = {
org: this.session.context.org,
gvc: this.session.context.gvc,
workload: workloadName,
pod: replica,
port,
};
// Try to create a ticket, surfacing failures to the caller
try {
// Make a POST request
const response = await this.client.post(`${remoteHost}/portforward-ticket`, requestBody);
// Extract the ticket
const ticket = response.ticket;
// Show the ticket in the debug logs
client_1.wire.debug('<<<<<<< Port-forward ticket created: ' + (0, client_1.convertAuthTokenForDebug)(ticket));
// Return the ticket
return ticket;
}
catch (e) {
this.session.err(`ERROR: Failed to create ticket, details:`);
throw e;
}
}
/**
* Detect if the message indicates a port-forward error, and return the error type
*
* @param {string} message The WebSocket message.
* @returns {PortForwardErrorType | null} The error type to handle later.
*/
detectPortForwardError(message) {
// The port-forward error starts with the following, if it is not found then there are no errors
if (!message.startsWith('Port-forward error:')) {
return null;
}
// Attempt to extract a JSON error object from the message; default to fatal on failure
try {
// Extract the JSON from the error message
const portForwardError = JSON.parse(this.extractPortForwardErrorJson(message));
// Log the message before returning the error type
client_1.wire.debug(`Handling port-forward error: ${portForwardError.message}. code: ${portForwardError.code}`);
// Determine the error type from the error code
if (portForwardError.code === PortForwardErrorType.TicketExpired) {
return PortForwardErrorType.TicketExpired;
}
// For every other case, return fatal type for now
return PortForwardErrorType.Fatal;
}
catch (e) {
// Log the error for debugging if necessary
client_1.wire.debug('ERROR: Failed to extract/parse port-forward error JSON:', e);
// Default to fatal, this is how the terminal server is signaling the CLI to simply die
return PortForwardErrorType.Fatal;
}
}
/**
* Parses a port mapping string into an object containing local and remote ports.
*
* @param {string} mapping - The port mapping string in the format "LOCAL:REMOTE", ":REMOTE", or "REMOTE".
* @returns {PortMapping} An object containing the localPort (if specified) and remotePort.
*/
parsePortMapping(mapping) {
// Split the input string by ':' to separate local and remote ports
const parts = mapping.split(':');
// If there's only one part, it must be a remote port
if (parts.length === 1) {
// Check if the single part is a valid port number
if (!PORT_PATTERN.test(parts[0])) {
throw new Error(`Invalid port value: ${mapping}`);
}
// Extract the port
const targetPort = parseInt(parts[0], 10);
// Return an object with only the remotePort property
return { localPort: targetPort, remotePort: targetPort };
}
// If there are two parts, it could be a valid "LOCAL:REMOTE" format
else if (parts.length === 2) {
// Destructure local and remote port values from the array
const [localStr, remoteStr] = parts;
// Ensure the remote port is valid and not empty
if (remoteStr === '' || !PORT_PATTERN.test(remoteStr)) {
throw new Error(`Invalid remote port in mapping: ${mapping}`);
}
// Convert the remote port string to a number
const remotePort = parseInt(remoteStr, 10);
// Initialize localPort as undefined by default
let localPort = undefined;
// If a local port is specified, validate it
if (localStr !== '') {
if (!PORT_PATTERN.test(localStr)) {
throw new Error(`Invalid local port in mapping: ${mapping}`);
}
// Convert the local port string to a number
localPort = parseInt(localStr, 10);
}
// Return an object containing both localPort and remotePort
return { localPort, remotePort };
}
// If there are more than two parts, the format is invalid
throw new Error(`Invalid port mapping format: ${mapping}`);
}
/**
* Validates the provided ports and addresses against the current session context.
*
* @param ports - The list of port mapping strings.
* @param addresses - The list of addresses to validate.
*/
validate(ports, addresses) {
// Validate org
if (!this.session.context.org) {
throw new Error('ERROR: An organization is required. Please, specify one using the --org option.');
}
// Validate gvc
if (!this.session.context.gvc) {
throw new Error('ERROR: A GVC is required. Please, specify one using the --gvc option.');
}
// Validate ports
if (!this.arePortsValid(ports)) {
throw new Error('Invalid port mapping. Each port must be between 1 and 65535. Correct format: "[LOCAL_PORT:]REMOTE_PORT". Example: "8080:80".');
}
// Validate address
if (!this.areAddressesValid(addresses)) {
throw new Error('An invalid address option was specified, an address can only be a valid IP address or the value localhost.');
}
}
/**
* Validates an array of port mappings to ensure they follow the correct format.
*
* @param {string[]} ports - An array of port mapping strings.
* @returns {boolean} True if all port mappings are valid, false otherwise.
*/
arePortsValid(ports) {
// Iterate over each port mapping and validate it
for (const portMapping of ports) {
// If the ports are separated by a colon, then separate them
const parts = portMapping.split(':');
// Handle different cases
if (parts.length === 1) {
// Single port case (REMOTE_PORT)
if (!PORT_PATTERN.test(parts[0])) {
return false;
}
}
else if (parts.length === 2) {
// Port mapping case (LOCAL_PORT:REMOTE_PORT)
const [localPort, remotePort] = parts;
// Empty localPort (":80") is valid (random local port)
if (localPort !== '' && !PORT_PATTERN.test(localPort)) {
return false;
}
// Validate remote port
if (!PORT_PATTERN.test(remotePort)) {
return false;
}
}
else {
// Invalid format (e.g., "8080:80:443")
return false;
}
}
// If all ports are valid, return true
return true;
}
/**
* Validates an array of addresses to ensure they are either "localhost" or a valid IP address.
*
* @param {string[]} addresses - An array of address strings to validate.
* @returns {boolean} True if all addresses are valid, false otherwise.
*/
areAddressesValid(addresses) {
// Iterate over each address specified and check if it is invalid
for (const address of addresses) {
// If it is not localhost and it is not even a valid IP address, return false
if (address != 'localhost' && net.isIP(address) == 0) {
return false;
}
}
// All specified addresses are valid
return true;
}
/**
* Extracts the JSON portion from an error message string.
*
* @param errorMessage - The full port-forward error message that contains a JSON object.
* @returns The JSON portion of the error message as a string, or an empty string if not found.
*/
extractPortForwardErrorJson(errorMessage) {
// Find the index of the first '{' character which indicates the start of the JSON object
const jsonStartIndex = errorMessage.indexOf('{');
// If found, extract and return the JSON string; otherwise, return an empty string
return jsonStartIndex !== -1 ? errorMessage.substring(jsonStartIndex) : '';
}
}
exports.PortForwarder = PortForwarder;
// SECTION - Exported Helpers
/**
* Expands the provided list of addresses into the concrete bind addresses that should be used.
*
* When 'localhost' is supplied, it is expanded into both 127.0.0.1 and ::1. Each expanded
* address is marked with fromLocalhostExpansion=true so callers can decide that a bind
* failure on one of them is recoverable. Explicitly supplied addresses keep the flag at false.
*
* @param {string[]} addresses - The raw address values supplied by the user.
* @returns {ResolvedBindAddress[]} The deduplicated, resolved bind addresses.
*/
function deduplicateAddresses(addresses) {
// Track which addresses we've already added so duplicates collapse,
// and remember the original source so explicit > expansion wins.
const seen = new Map();
// Walk the input in order so the output preserves the user's listed addresses
for (const raw of addresses) {
// Trim whitespace from the input value
const address = raw.trim();
// When the user passed "localhost", expand it into the IPv4 and IPv6 loopbacks
if (address === 'localhost') {
// Only add the IPv4 loopback if it wasn't already supplied explicitly
if (!seen.has(LOCALHOST_IPV4_ADDRESS)) {
seen.set(LOCALHOST_IPV4_ADDRESS, true);
}
// Only add the IPv6 loopback if it wasn't already supplied explicitly
if (!seen.has(LOCALHOST_IPV6_ADDRESS)) {
seen.set(LOCALHOST_IPV6_ADDRESS, true);
}
continue;
}
// Explicit address: overrides any prior expansion-flagged entry for the same address
seen.set(address, false);
}
// Convert the map back to the structured result list
const result = [];
for (const [address, fromLocalhostExpansion] of seen) {
result.push({ address, fromLocalhostExpansion });
}
return result;
}
/**
* Binds the given TCP server to the supplied address and port using promise semantics.
*
* Resolves with the bound AddressInfo on success or with a structured failure on bind error.
* The 'error' and 'listening' listeners that drive this promise are removed once it settles
* so the caller can attach its own long-lived 'error' listener afterwards.
*
* @param {net.Server} server - The TCP server to bind.
* @param {number} port - The local port to bind to (0 lets the OS assign one).
* @param {string} address - The local address to bind to.
* @returns {Promise<BindResult>} A promise that resolves with the bind outcome.
*/
function bindServer(server, port, address) {
return new Promise((resolve) => {
// Handler that resolves the promise with a successful bind result
const onListening = () => {
// Detach the error handler so it does not fire for unrelated post-listen failures
server.removeListener('error', onError);
// Extract the bound address information from the server
const info = server.address();
// Resolve the promise with the bound address details
resolve({ ok: true, info });
};
// Handler that resolves the promise with the structured bind failure
const onError = (err) => {
// Detach the listening handler so a late "listening" event cannot resolve twice
server.removeListener('listening', onListening);
// Resolve the promise with the bind error
resolve({ ok: false, error: err });
};
// Wire up the one-shot handlers
server.once('listening', onListening);
server.once('error', onError);
// Begin listening — events above drive the promise resolution
server.listen(port, address);
});
}
// !SECTION
//# sourceMappingURL=port-forwarder.js.map