Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: validate hostname and improve errors #59

Merged
merged 7 commits into from
Sep 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 87 additions & 54 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,85 +20,108 @@ export type GetPortInput = Partial<GetPortOptions> | number | string;
export type HostAddress = undefined | string;
export type PortNumber = number;

function log(...arguments_) {
// eslint-disable-next-line no-console
console.log("[get-port]", ...arguments_);
}
const HOSTNAME_RE = /^(?!-)[\d.A-Za-z-]{1,63}(?<!-)$/;

export async function getPort(config: GetPortInput = {}): Promise<PortNumber> {
if (typeof config === "number" || typeof config === "string") {
config = { port: Number.parseInt(config + "") || 0 };
export async function getPort(
_userOptions: GetPortInput = {},
): Promise<PortNumber> {
if (typeof _userOptions === "number" || typeof _userOptions === "string") {
_userOptions = { port: Number.parseInt(_userOptions + "") || 0 };
}

const _port = Number(config.port ?? process.env.PORT ?? 3000);
const _port = Number(_userOptions.port ?? process.env.PORT ?? 3000);

const options = {
name: "default",
random: _port === 0,
ports: [],
portRange: [],
alternativePortRange: config.port ? [] : [3000, 3100],
alternativePortRange: _userOptions.port ? [] : [3000, 3100],
host: undefined,
verbose: false,
...config,
..._userOptions,
port: _port,
} as GetPortOptions;

if (options.host && !HOSTNAME_RE.test(options.host)) {
throw new GetPortError(`Invalid host: ${JSON.stringify(options.host)}`);
}

if (options.random) {
return getRandomPort(options.host);
}

// Ports to check

// Generate list of ports to check
const portsToCheck: PortNumber[] = [
options.port,
...options.ports,
...generateRange(...options.portRange),
..._generateRange(...options.portRange),
].filter((port) => {
if (!port) {
return false;
}
if (!isSafePort(port)) {
if (options.verbose) {
log("Ignoring unsafe port:", port);
}
_log(options.verbose, `Ignoring unsafe port: ${port}`);
return false;
}
return true;
});

// Try to find a port
let availablePort = await findPort(
let availablePort = await _findPort(
portsToCheck,
options.host,
options.verbose,
false,
);

// Try fallback port range
if (!availablePort) {
availablePort = await findPort(
generateRange(...options.alternativePortRange),
if (!availablePort && options.alternativePortRange.length > 0) {
availablePort = await _findPort(
_generateRange(...options.alternativePortRange),
options.host,
options.verbose,
);
if (options.verbose) {
log(
`Unable to find an available port (tried ${
portsToCheck.join(", ") || "-"
}). Using alternative port:`,
availablePort,
);
_log(
options.verbose,
`Unable to find an available port (tried ${options.alternativePortRange.join(
"-",
)} ${_fmtOnHost(options.host)}). Using alternative port ${availablePort}`,
);
}

// Try random port
if (!availablePort && _userOptions.random !== false) {
availablePort = await getRandomPort(options.host);
if (availablePort) {
_log(options.verbose, `Using random port ${availablePort}`);
}
}

// Throw error if no port is available
if (!availablePort) {
const triedRanges = [
options.port,
options.portRange.join("-"),
options.alternativePortRange.join("-"),
]
.filter(Boolean)
.join(", ");
throw new GetPortError(
`Unable to find find available port ${_fmtOnHost(
options.host,
)} (tried ${triedRanges})`,
);
}

return availablePort;
}

export async function getRandomPort(host?: HostAddress) {
const port = await checkPort(0, host);
if (port === false) {
throw new Error("Unable to obtain an available random port number!");
throw new GetPortError(
`Unable to find any random port ${_fmtOnHost(host)}`,
);
}
return port;
}
Expand All @@ -120,27 +143,32 @@ export async function waitForPort(
}
await new Promise((resolve) => setTimeout(resolve, delay));
}
throw new Error(
throw new GetPortError(
`Timeout waiting for port ${port} after ${retries} retries with ${delay}ms interval.`,
);
}

export async function checkPort(
port: PortNumber,
host: HostAddress | HostAddress[] = process.env.HOST,
_verbose?: boolean,
verbose?: boolean,
): Promise<PortNumber | false> {
if (!host) {
host = getLocalHosts([undefined /* default */, "0.0.0.0"]);
host = _getLocalHosts([undefined /* default */, "0.0.0.0"]);
}
if (!Array.isArray(host)) {
return _checkPort(port, host);
}
for (const _host of host) {
const _port = await _checkPort(port, _host);
if (_port === false) {
if (port < 1024 && _verbose) {
log("Unable to listen to priviliged port:", `${_host}:${port}`);
if (port < 1024 && verbose) {
_log(
verbose,
`Unable to listen to the priviliged port ${port} ${_fmtOnHost(
_host,
)}`,
);
}
return false;
}
Expand All @@ -153,7 +181,23 @@ export async function checkPort(

// ----- Internal -----

function generateRange(from: number, to: number): number[] {
class GetPortError extends Error {
name = "GetPortError";
constructor(
public message: string,
opts?: any,
) {
super(message, opts);
}
}

function _log(showLogs: boolean, message: string) {
if (showLogs) {
console.log("[get-port]", message);
}
}

function _generateRange(from: number, to: number): number[] {
if (to < from) {
return [];
}
Expand Down Expand Up @@ -188,7 +232,7 @@ function _checkPort(
});
}

function getLocalHosts(additional?: HostAddress[]): HostAddress[] {
function _getLocalHosts(additional: HostAddress[]): HostAddress[] {
const hosts = new Set<HostAddress>(additional);
for (const _interface of Object.values(networkInterfaces())) {
for (const config of _interface || []) {
Expand All @@ -198,30 +242,19 @@ function getLocalHosts(additional?: HostAddress[]): HostAddress[] {
return [...hosts];
}

async function findPort(
async function _findPort(
ports: number[],
host?: HostAddress,
_verbose = false,
_random = true,
host: HostAddress,
_verbose: boolean,
): Promise<PortNumber> {
for (const port of ports) {
const r = await checkPort(port, host, _verbose);
if (r) {
return r;
}
}
if (_random) {
const randomPort = await getRandomPort(host);
if (_verbose) {
log(
`Unable to find an available port (tried ${
ports.join(", ") || "-"
}). Using random port:`,
randomPort,
);
}
return randomPort;
} else {
return 0;
}
}

function _fmtOnHost(hostname: string | undefined) {
return hostname ? `on host ${JSON.stringify(hostname)}` : "on any host";
}
30 changes: 30 additions & 0 deletions test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,33 @@ describe("getPort: random", () => {
expect(port).not.toBe(3000);
});
});

describe("errors", () => {
test("invalid hostname", async () => {
const error = await getPort({ host: "http://localhost:8080" }).catch(
(error) => error,
);
expect(error.toString()).toMatchInlineSnapshot(
'"GetPortError: Invalid host: \\"http://localhost:8080\\""',
);
});

test("unavailable hostname", async () => {
const error = await getPort({
host: "192.168.1.999",
}).catch((error) => error);
expect(error.toString()).toMatchInlineSnapshot(
'"GetPortError: Unable to find any random port on host \\"192.168.1.999\\""',
);
});

test("unavailable hostname (no random)", async () => {
const error = await getPort({
host: "192.168.1.999",
random: false,
}).catch((error) => error);
expect(error.toString()).toMatchInlineSnapshot(
'"GetPortError: Unable to find find available port on host \\"192.168.1.999\\" (tried 3000, 3000-3100)"',
);
});
});