Skip to content
42 changes: 32 additions & 10 deletions apps/playwright-browser-tunnel/src/HttpServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,24 @@
// See LICENSE in the project root for license information.

import http from 'node:http';
import type { AddressInfo } from 'node:net';

import { WebSocketServer, type WebSocket, type AddressInfo } from 'ws';
import { WebSocketServer, type WebSocket } from 'ws';

import type { ITerminal } from '@rushstack/terminal';

const LOCALHOST_IP: string = '127.0.0.1';
const LOCALHOST: string = 'localhost';

/**
* Formats an address info object into a WebSocket-compatible address string.
* IPv6 addresses are formatted with brackets: [address]:port
* IPv4 addresses are formatted as: address:port
*/
function formatAddress(addressInfo: AddressInfo): string {
return addressInfo.family === 'IPv6'
? `[${addressInfo.address}]:${addressInfo.port}`
: `${addressInfo.address}:${addressInfo.port}`;
}

/**
* This HttpServer is used for the localProxyWs WebSocketServer.
Expand All @@ -17,7 +29,7 @@ const LOCALHOST_IP: string = '127.0.0.1';
export class HttpServer {
private readonly _server: http.Server;
private readonly _wsServer: WebSocketServer; // local proxy websocket server accepting browser clients
private _listeningPort: number | undefined;
private _listeningAddress: string | undefined;
private _logger: ITerminal;

public constructor(logger: ITerminal) {
Expand All @@ -37,22 +49,32 @@ export class HttpServer {

public listen(): Promise<void> {
return new Promise((resolve) => {
this._server.listen(0, LOCALHOST_IP, () => {
this._listeningPort = (this._server.address() as AddressInfo).port;
// Bind to 'localhost' which resolves to IPv4 (127.0.0.1) or IPv6 (::1)
// depending on system configuration and DNS resolution
this._server.listen(0, LOCALHOST, () => {
const addressInfo = this._server.address();
if (!addressInfo) {
throw new Error('Server address is null - server may not be bound properly');
}
if (typeof addressInfo === 'string') {
throw new Error(
`Server address is a pipe/socket path (${addressInfo}), expected an IP address`
);
}
const formattedAddress: string = formatAddress(addressInfo);
this._listeningAddress = formattedAddress;
// This MUST be printed to terminal so VS Code can auto-port forward
this._logger.writeLine(
`Local proxy HttpServer listening at ws://${LOCALHOST_IP}:${this._listeningPort}`
);
this._logger.writeLine(`Local proxy HttpServer listening at ws://${formattedAddress}`);
resolve();
});
});
}

public get endpoint(): string {
if (this._listeningPort === undefined) {
if (this._listeningAddress === undefined) {
throw new Error('HttpServer not listening yet');
}
return `ws://${LOCALHOST_IP}:${this._listeningPort}`;
return `ws://${this._listeningAddress}`;
}

public get wsServer(): WebSocketServer {
Expand Down