Skip to content

Commit 836ca3c

Browse files
authored
fix: validate hostname and improve errors (#59)
1 parent 8d48ec6 commit 836ca3c

File tree

2 files changed

+117
-54
lines changed

2 files changed

+117
-54
lines changed

src/index.ts

Lines changed: 87 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -20,85 +20,108 @@ export type GetPortInput = Partial<GetPortOptions> | number | string;
2020
export type HostAddress = undefined | string;
2121
export type PortNumber = number;
2222

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

28-
export async function getPort(config: GetPortInput = {}): Promise<PortNumber> {
29-
if (typeof config === "number" || typeof config === "string") {
30-
config = { port: Number.parseInt(config + "") || 0 };
25+
export async function getPort(
26+
_userOptions: GetPortInput = {},
27+
): Promise<PortNumber> {
28+
if (typeof _userOptions === "number" || typeof _userOptions === "string") {
29+
_userOptions = { port: Number.parseInt(_userOptions + "") || 0 };
3130
}
3231

33-
const _port = Number(config.port ?? process.env.PORT ?? 3000);
32+
const _port = Number(_userOptions.port ?? process.env.PORT ?? 3000);
3433

3534
const options = {
3635
name: "default",
3736
random: _port === 0,
3837
ports: [],
3938
portRange: [],
40-
alternativePortRange: config.port ? [] : [3000, 3100],
39+
alternativePortRange: _userOptions.port ? [] : [3000, 3100],
4140
host: undefined,
4241
verbose: false,
43-
...config,
42+
..._userOptions,
4443
port: _port,
4544
} as GetPortOptions;
4645

46+
if (options.host && !HOSTNAME_RE.test(options.host)) {
47+
throw new GetPortError(`Invalid host: ${JSON.stringify(options.host)}`);
48+
}
49+
4750
if (options.random) {
4851
return getRandomPort(options.host);
4952
}
5053

51-
// Ports to check
52-
54+
// Generate list of ports to check
5355
const portsToCheck: PortNumber[] = [
5456
options.port,
5557
...options.ports,
56-
...generateRange(...options.portRange),
58+
..._generateRange(...options.portRange),
5759
].filter((port) => {
5860
if (!port) {
5961
return false;
6062
}
6163
if (!isSafePort(port)) {
62-
if (options.verbose) {
63-
log("Ignoring unsafe port:", port);
64-
}
64+
_log(options.verbose, `Ignoring unsafe port: ${port}`);
6565
return false;
6666
}
6767
return true;
6868
});
6969

7070
// Try to find a port
71-
let availablePort = await findPort(
71+
let availablePort = await _findPort(
7272
portsToCheck,
7373
options.host,
7474
options.verbose,
75-
false,
7675
);
7776

7877
// Try fallback port range
79-
if (!availablePort) {
80-
availablePort = await findPort(
81-
generateRange(...options.alternativePortRange),
78+
if (!availablePort && options.alternativePortRange.length > 0) {
79+
availablePort = await _findPort(
80+
_generateRange(...options.alternativePortRange),
8281
options.host,
8382
options.verbose,
8483
);
85-
if (options.verbose) {
86-
log(
87-
`Unable to find an available port (tried ${
88-
portsToCheck.join(", ") || "-"
89-
}). Using alternative port:`,
90-
availablePort,
91-
);
84+
_log(
85+
options.verbose,
86+
`Unable to find an available port (tried ${options.alternativePortRange.join(
87+
"-",
88+
)} ${_fmtOnHost(options.host)}). Using alternative port ${availablePort}`,
89+
);
90+
}
91+
92+
// Try random port
93+
if (!availablePort && _userOptions.random !== false) {
94+
availablePort = await getRandomPort(options.host);
95+
if (availablePort) {
96+
_log(options.verbose, `Using random port ${availablePort}`);
9297
}
9398
}
9499

100+
// Throw error if no port is available
101+
if (!availablePort) {
102+
const triedRanges = [
103+
options.port,
104+
options.portRange.join("-"),
105+
options.alternativePortRange.join("-"),
106+
]
107+
.filter(Boolean)
108+
.join(", ");
109+
throw new GetPortError(
110+
`Unable to find find available port ${_fmtOnHost(
111+
options.host,
112+
)} (tried ${triedRanges})`,
113+
);
114+
}
115+
95116
return availablePort;
96117
}
97118

98119
export async function getRandomPort(host?: HostAddress) {
99120
const port = await checkPort(0, host);
100121
if (port === false) {
101-
throw new Error("Unable to obtain an available random port number!");
122+
throw new GetPortError(
123+
`Unable to find any random port ${_fmtOnHost(host)}`,
124+
);
102125
}
103126
return port;
104127
}
@@ -120,27 +143,32 @@ export async function waitForPort(
120143
}
121144
await new Promise((resolve) => setTimeout(resolve, delay));
122145
}
123-
throw new Error(
146+
throw new GetPortError(
124147
`Timeout waiting for port ${port} after ${retries} retries with ${delay}ms interval.`,
125148
);
126149
}
127150

128151
export async function checkPort(
129152
port: PortNumber,
130153
host: HostAddress | HostAddress[] = process.env.HOST,
131-
_verbose?: boolean,
154+
verbose?: boolean,
132155
): Promise<PortNumber | false> {
133156
if (!host) {
134-
host = getLocalHosts([undefined /* default */, "0.0.0.0"]);
157+
host = _getLocalHosts([undefined /* default */, "0.0.0.0"]);
135158
}
136159
if (!Array.isArray(host)) {
137160
return _checkPort(port, host);
138161
}
139162
for (const _host of host) {
140163
const _port = await _checkPort(port, _host);
141164
if (_port === false) {
142-
if (port < 1024 && _verbose) {
143-
log("Unable to listen to priviliged port:", `${_host}:${port}`);
165+
if (port < 1024 && verbose) {
166+
_log(
167+
verbose,
168+
`Unable to listen to the priviliged port ${port} ${_fmtOnHost(
169+
_host,
170+
)}`,
171+
);
144172
}
145173
return false;
146174
}
@@ -153,7 +181,23 @@ export async function checkPort(
153181

154182
// ----- Internal -----
155183

156-
function generateRange(from: number, to: number): number[] {
184+
class GetPortError extends Error {
185+
name = "GetPortError";
186+
constructor(
187+
public message: string,
188+
opts?: any,
189+
) {
190+
super(message, opts);
191+
}
192+
}
193+
194+
function _log(showLogs: boolean, message: string) {
195+
if (showLogs) {
196+
console.log("[get-port]", message);
197+
}
198+
}
199+
200+
function _generateRange(from: number, to: number): number[] {
157201
if (to < from) {
158202
return [];
159203
}
@@ -188,7 +232,7 @@ function _checkPort(
188232
});
189233
}
190234

191-
function getLocalHosts(additional?: HostAddress[]): HostAddress[] {
235+
function _getLocalHosts(additional: HostAddress[]): HostAddress[] {
192236
const hosts = new Set<HostAddress>(additional);
193237
for (const _interface of Object.values(networkInterfaces())) {
194238
for (const config of _interface || []) {
@@ -198,30 +242,19 @@ function getLocalHosts(additional?: HostAddress[]): HostAddress[] {
198242
return [...hosts];
199243
}
200244

201-
async function findPort(
245+
async function _findPort(
202246
ports: number[],
203-
host?: HostAddress,
204-
_verbose = false,
205-
_random = true,
247+
host: HostAddress,
248+
_verbose: boolean,
206249
): Promise<PortNumber> {
207250
for (const port of ports) {
208251
const r = await checkPort(port, host, _verbose);
209252
if (r) {
210253
return r;
211254
}
212255
}
213-
if (_random) {
214-
const randomPort = await getRandomPort(host);
215-
if (_verbose) {
216-
log(
217-
`Unable to find an available port (tried ${
218-
ports.join(", ") || "-"
219-
}). Using random port:`,
220-
randomPort,
221-
);
222-
}
223-
return randomPort;
224-
} else {
225-
return 0;
226-
}
256+
}
257+
258+
function _fmtOnHost(hostname: string | undefined) {
259+
return hostname ? `on host ${JSON.stringify(hostname)}` : "on any host";
227260
}

test/index.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,33 @@ describe("getPort: random", () => {
8181
expect(port).not.toBe(3000);
8282
});
8383
});
84+
85+
describe("errors", () => {
86+
test("invalid hostname", async () => {
87+
const error = await getPort({ host: "http://localhost:8080" }).catch(
88+
(error) => error,
89+
);
90+
expect(error.toString()).toMatchInlineSnapshot(
91+
'"GetPortError: Invalid host: \\"http://localhost:8080\\""',
92+
);
93+
});
94+
95+
test("unavailable hostname", async () => {
96+
const error = await getPort({
97+
host: "192.168.1.999",
98+
}).catch((error) => error);
99+
expect(error.toString()).toMatchInlineSnapshot(
100+
'"GetPortError: Unable to find any random port on host \\"192.168.1.999\\""',
101+
);
102+
});
103+
104+
test("unavailable hostname (no random)", async () => {
105+
const error = await getPort({
106+
host: "192.168.1.999",
107+
random: false,
108+
}).catch((error) => error);
109+
expect(error.toString()).toMatchInlineSnapshot(
110+
'"GetPortError: Unable to find find available port on host \\"192.168.1.999\\" (tried 3000, 3000-3100)"',
111+
);
112+
});
113+
});

0 commit comments

Comments
 (0)