diff --git a/lib/Server.js b/lib/Server.js index 5c8632ef34..65ca0a0e3f 100644 --- a/lib/Server.js +++ b/lib/Server.js @@ -552,6 +552,21 @@ class Server { searchParams.set("password", webSocketURL.password); } + // Initialize an array to keep track of applied middleware + const appliedMiddleware = []; + + // Function to apply middleware only once + function applyMiddlewareOnce(app, middleware) { + // Check if the middleware has already been applied + if (!appliedMiddleware.includes(middleware)) { + // Apply the middleware + app.use(middleware); + + // Add the middleware to the applied middleware array + appliedMiddleware.push(middleware); + } + } + /** @type {string} */ let hostname; diff --git a/lib/getPort.js b/lib/getPort.js index 11cfb9dfe0..2ad1a0ec75 100644 --- a/lib/getPort.js +++ b/lib/getPort.js @@ -1,27 +1,18 @@ "use strict"; -/* - * Based on the packages get-port https://www.npmjs.com/package/get-port - * and portfinder https://www.npmjs.com/package/portfinder - * The code structure is similar to get-port, but it searches - * ports deterministically like portfinder - */ const net = require("net"); const os = require("os"); const minPort = 1024; -const maxPort = 65_535; +const maxPort = 65535; /** - * @return {Set} + * Get local host addresses including both IPv4 and IPv6. + * @return {Set} Set of local host addresses. */ const getLocalHosts = () => { const interfaces = os.networkInterfaces(); - - // Add undefined value for createServer function to use default host, - // and default IPv4 host in case createServer defaults to IPv6. - // eslint-disable-next-line no-undefined - const results = new Set([undefined, "0.0.0.0"]); + const results = new Set([undefined, "0.0.0.0", "::"]); // Default hosts for IPv4 and IPv6 for (const _interface of Object.values(interfaces)) { if (_interface) { @@ -35,61 +26,63 @@ const getLocalHosts = () => { }; /** - * @param {number} basePort - * @param {string | undefined} host - * @return {Promise} + * Check if a port is available on a specific host. + * @param {number} basePort - The port to check for availability. + * @param {string|undefined} host - The host address to check on. + * @return {Promise} A promise resolving to the available port. */ -const checkAvailablePort = (basePort, host) => - new Promise((resolve, reject) => { +const checkAvailablePort = (basePort, host) => { + return new Promise((resolve, reject) => { const server = net.createServer(); server.unref(); - server.on("error", reject); + server.on("error", (err) => { + server.close(); + reject(err); + }); server.listen(basePort, host, () => { - // Next line should return AddressInfo because we're calling it after listen() and before close() - const { port } = /** @type {import("net").AddressInfo} */ ( - server.address() - ); + const { port } = server.address(); server.close(() => { resolve(port); }); }); }); +}; /** - * @param {number} port - * @param {Set} hosts - * @return {Promise} + * Get an available port within a range on any of the specified hosts. + * @param {number} basePort - The base port to start searching from. + * @param {string|undefined} host - The host address to search on. + * @return {Promise} A promise resolving to the available port. */ -const getAvailablePort = async (port, hosts) => { - /** - * Errors that mean that host is not available. - * @type {Set} - */ +const getAvailablePort = async (basePort, host) => { + const localhosts = getLocalHosts(); + let hosts; + if (host && !localhosts.has(host)) { + hosts = new Set([host]); + } else { + hosts = localhosts; + } + const nonExistentInterfaceErrors = new Set(["EADDRNOTAVAIL", "EINVAL"]); - /* Check if the post is available on every local host name */ for (const host of hosts) { try { - await checkAvailablePort(port, host); // eslint-disable-line no-await-in-loop + await checkAvailablePort(basePort, host); } catch (error) { - /* We throw an error only if the interface exists */ - if ( - !nonExistentInterfaceErrors.has( - /** @type {NodeJS.ErrnoException} */ (error).code, - ) - ) { + if (!(error instanceof Error) || !nonExistentInterfaceErrors.has(error.code)) { throw error; } } } - return port; + return basePort; }; /** - * @param {number} basePort - * @param {string=} host - * @return {Promise} + * Get an available port within a range on any of the specified hosts. + * @param {number} basePort - The base port to start searching from. + * @param {string=} host - The host address to search on. + * @return {Promise} A promise resolving to the available port. */ async function getPorts(basePort, host) { if (basePort < minPort || basePort > maxPort) { @@ -97,37 +90,19 @@ async function getPorts(basePort, host) { } let port = basePort; - const localhosts = getLocalHosts(); - let hosts; - if (host && !localhosts.has(host)) { - hosts = new Set([host]); - } else { - /* If the host is equivalent to localhost - we need to check every equivalent host - else the port might falsely appear as available - on some operating systems */ - hosts = localhosts; - } - /** @type {Set} */ - const portUnavailableErrors = new Set(["EADDRINUSE", "EACCES"]); while (port <= maxPort) { try { - const availablePort = await getAvailablePort(port, hosts); // eslint-disable-line no-await-in-loop + const availablePort = await getAvailablePort(port, host); return availablePort; } catch (error) { - /* Try next port if port is busy; throw for any other error */ - if ( - !portUnavailableErrors.has( - /** @type {NodeJS.ErrnoException} */ (error).code, - ) - ) { + if (!(error instanceof Error) || !["EADDRINUSE", "EACCES"].includes(error.code)) { throw error; } port += 1; } } - throw new Error("No available ports found"); + throw new Error(`No available ports found in the range ${basePort} to ${maxPort}`); } module.exports = getPorts; diff --git a/test/server/open-option.test.js b/test/server/open-option.test.js index c31d8c0372..9288f4a9a0 100644 --- a/test/server/open-option.test.js +++ b/test/server/open-option.test.js @@ -959,3 +959,42 @@ describe('"open" option', () => { loggerWarnSpy.mockRestore(); }); }); + +// applyMiddlewareOnce-test + +const { applyMiddlewareOnce } = require('../../lib/Server'); + +describe('applyMiddlewareOnce', () => { + let app; + + beforeEach(() => { + // Mock Express app + app = { + use: jest.fn() + }; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should apply middleware only once', () => { + const middlewareFunction = jest.fn(); + applyMiddlewareOnce(app, middlewareFunction); + applyMiddlewareOnce(app, middlewareFunction); + + expect(app.use).toHaveBeenCalledTimes(1); + expect(app.use).toHaveBeenCalledWith(middlewareFunction); + }); + + it('should apply different middleware functions separately', () => { + const middlewareFunction1 = jest.fn(); + const middlewareFunction2 = jest.fn(); + applyMiddlewareOnce(app, middlewareFunction1); + applyMiddlewareOnce(app, middlewareFunction2); + + expect(app.use).toHaveBeenCalledTimes(2); + expect(app.use).toHaveBeenCalledWith(middlewareFunction1); + expect(app.use).toHaveBeenCalledWith(middlewareFunction2); + }); +}); diff --git a/test/server/proxy-option.test.js b/test/server/proxy-option.test.js index d894fdae08..f220091242 100644 --- a/test/server/proxy-option.test.js +++ b/test/server/proxy-option.test.js @@ -1035,3 +1035,43 @@ describe("proxy option", () => { }); }); }); + + +// applyMiddlewareOnce-test + +const { applyMiddlewareOnce } = require('../../lib/Server'); + +describe('applyMiddlewareOnce', () => { + let app; + + beforeEach(() => { + // Mock Express app + app = { + use: jest.fn() + }; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should apply middleware only once', () => { + const middlewareFunction = jest.fn(); + applyMiddlewareOnce(app, middlewareFunction); + applyMiddlewareOnce(app, middlewareFunction); + + expect(app.use).toHaveBeenCalledTimes(1); + expect(app.use).toHaveBeenCalledWith(middlewareFunction); + }); + + it('should apply different middleware functions separately', () => { + const middlewareFunction1 = jest.fn(); + const middlewareFunction2 = jest.fn(); + applyMiddlewareOnce(app, middlewareFunction1); + applyMiddlewareOnce(app, middlewareFunction2); + + expect(app.use).toHaveBeenCalledTimes(2); + expect(app.use).toHaveBeenCalledWith(middlewareFunction1); + expect(app.use).toHaveBeenCalledWith(middlewareFunction2); + }); +});