Skip to content

Commit eea50f3

Browse files
authoredApr 14, 2022
fix: replace portfinder with custom implementation and fix security problem (#4384)
1 parent c9b6433 commit eea50f3

File tree

8 files changed

+201
-105
lines changed

8 files changed

+201
-105
lines changed
 

‎lib/Server.js

+3-4
Original file line numberDiff line numberDiff line change
@@ -393,9 +393,8 @@ class Server {
393393
}
394394

395395
const pRetry = require("p-retry");
396-
const portfinder = require("portfinder");
397-
398-
portfinder.basePort =
396+
const getPort = require("./getPort");
397+
const basePort =
399398
typeof process.env.WEBPACK_DEV_SERVER_BASE_PORT !== "undefined"
400399
? parseInt(process.env.WEBPACK_DEV_SERVER_BASE_PORT, 10)
401400
: 8080;
@@ -407,7 +406,7 @@ class Server {
407406
? parseInt(process.env.WEBPACK_DEV_SERVER_PORT_RETRY, 10)
408407
: 3;
409408

410-
return pRetry(() => portfinder.getPortPromise(), {
409+
return pRetry(() => getPort(basePort), {
411410
retries: defaultPortRetry,
412411
});
413412
}

‎lib/getPort.js

+122
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
"use strict";
2+
3+
/*
4+
* Based on the packages get-port https://www.npmjs.com/package/get-port
5+
* and portfinder https://www.npmjs.com/package/portfinder
6+
* The code structure is similar to get-port, but it searches
7+
* ports deterministically like portfinder
8+
*/
9+
const net = require("net");
10+
const os = require("os");
11+
12+
const minPort = 1024;
13+
const maxPort = 65_535;
14+
15+
/**
16+
* @return {Set<string|undefined>}
17+
*/
18+
const getLocalHosts = () => {
19+
const interfaces = os.networkInterfaces();
20+
21+
// Add undefined value for createServer function to use default host,
22+
// and default IPv4 host in case createServer defaults to IPv6.
23+
// eslint-disable-next-line no-undefined
24+
const results = new Set([undefined, "0.0.0.0"]);
25+
26+
for (const _interface of Object.values(interfaces)) {
27+
if (_interface) {
28+
for (const config of _interface) {
29+
results.add(config.address);
30+
}
31+
}
32+
}
33+
34+
return results;
35+
};
36+
37+
/**
38+
* @param {number} basePort
39+
* @param {string | undefined} host
40+
* @return {Promise<number>}
41+
*/
42+
const checkAvailablePort = (basePort, host) =>
43+
new Promise((resolve, reject) => {
44+
const server = net.createServer();
45+
server.unref();
46+
server.on("error", reject);
47+
48+
server.listen(basePort, host, () => {
49+
// Next line should return AdressInfo because we're calling it after listen() and before close()
50+
const { port } = /** @type {import("net").AddressInfo} */ (
51+
server.address()
52+
);
53+
server.close(() => {
54+
resolve(port);
55+
});
56+
});
57+
});
58+
59+
/**
60+
* @param {number} port
61+
* @param {Set<string|undefined>} hosts
62+
* @return {Promise<number>}
63+
*/
64+
const getAvailablePort = async (port, hosts) => {
65+
/**
66+
* Errors that mean that host is not available.
67+
* @type {Set<string | undefined>}
68+
*/
69+
const nonExistentInterfaceErrors = new Set(["EADDRNOTAVAIL", "EINVAL"]);
70+
/* Check if the post is available on every local host name */
71+
for (const host of hosts) {
72+
try {
73+
await checkAvailablePort(port, host); // eslint-disable-line no-await-in-loop
74+
} catch (error) {
75+
/* We throw an error only if the interface exists */
76+
if (
77+
!nonExistentInterfaceErrors.has(
78+
/** @type {NodeJS.ErrnoException} */ (error).code
79+
)
80+
) {
81+
throw error;
82+
}
83+
}
84+
}
85+
86+
return port;
87+
};
88+
89+
/**
90+
* @param {number} basePort
91+
* @return {Promise<number>}
92+
*/
93+
async function getPorts(basePort) {
94+
if (basePort < minPort || basePort > maxPort) {
95+
throw new Error(`Port number must lie between ${minPort} and ${maxPort}`);
96+
}
97+
98+
let port = basePort;
99+
const hosts = getLocalHosts();
100+
/** @type {Set<string | undefined>} */
101+
const portUnavailableErrors = new Set(["EADDRINUSE", "EACCES"]);
102+
while (port <= maxPort) {
103+
try {
104+
const availablePort = await getAvailablePort(port, hosts); // eslint-disable-line no-await-in-loop
105+
return availablePort;
106+
} catch (error) {
107+
/* Try next port if port is busy; throw for any other error */
108+
if (
109+
!portUnavailableErrors.has(
110+
/** @type {NodeJS.ErrnoException} */ (error).code
111+
)
112+
) {
113+
throw error;
114+
}
115+
port += 1;
116+
}
117+
}
118+
119+
throw new Error("No available ports found");
120+
}
121+
122+
module.exports = getPorts;

‎package-lock.json

+8-81
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json

-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@
5555
"ipaddr.js": "^2.0.1",
5656
"open": "^8.0.9",
5757
"p-retry": "^4.5.0",
58-
"portfinder": "^1.0.28",
5958
"rimraf": "^3.0.2",
6059
"schema-utils": "^4.0.0",
6160
"selfsigned": "^2.0.1",

‎test/e2e/api.test.js

+4-5
Original file line numberDiff line numberDiff line change
@@ -803,11 +803,10 @@ describe("API", () => {
803803
it("should throw the error when the port isn't found", async () => {
804804
expect.assertions(1);
805805

806-
jest.mock("portfinder", () => {
807-
return {
808-
getPortPromise: () => Promise.reject(new Error("busy")),
809-
};
810-
});
806+
jest.mock(
807+
"../../lib/getPort",
808+
() => () => Promise.reject(new Error("busy"))
809+
);
811810

812811
process.env.WEBPACK_DEV_SERVER_PORT_RETRY = 1;
813812

‎test/server/get-port.test.js

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"use strict";
2+
3+
const net = require("net");
4+
const util = require("util");
5+
const getPort = require("../../lib/getPort");
6+
7+
it("it should bind to the preferred port", async () => {
8+
const preferredPort = 8080;
9+
const port = await getPort(8080);
10+
expect(port).toBe(preferredPort);
11+
});
12+
13+
it("should pick the next port if the preferred port is unavailable", async () => {
14+
const preferredPort = 8345;
15+
const server = net.createServer();
16+
server.unref();
17+
await util.promisify(server.listen.bind(server))(preferredPort);
18+
const port = await getPort(preferredPort);
19+
expect(port).toBe(preferredPort + 1);
20+
});
21+
22+
it("should reject privileged ports", async () => {
23+
try {
24+
await getPort(80);
25+
} catch (e) {
26+
expect(e.message).toBeDefined();
27+
}
28+
});
29+
30+
it("should reject too high port numbers", async () => {
31+
try {
32+
await getPort(65536);
33+
} catch (e) {
34+
expect(e.message).toBeDefined();
35+
}
36+
});

0 commit comments

Comments
 (0)
Please sign in to comment.