From bec78e02438c92ad78c47ba8a98c2ebd6981788d Mon Sep 17 00:00:00 2001 From: Jamie Magee Date: Tue, 28 Nov 2023 21:25:38 -0800 Subject: [PATCH] Allow serving under a non-root base path This allows for scenarios where zigbee2mqtt is behind a reverse proxy. For example `example.com/zigbee2mqtt` instead of a root path like `zigbee2mqtt.example.com`. Related issues: - nurikk/zigbee2mqtt-frontend#275 - nurikk/zigbee2mqtt-frontend#971 - nurikk/zigbee2mqtt-frontend#1690 --- lib/extension/frontend.ts | 11 ++++++++--- lib/types/types.d.ts | 1 + lib/util/settings.schema.json | 8 ++++++++ lib/util/settings.ts | 2 +- test/frontend.test.js | 16 +++++++++++++--- test/settings.test.js | 2 +- 6 files changed, 32 insertions(+), 8 deletions(-) diff --git a/lib/extension/frontend.ts b/lib/extension/frontend.ts index 4dc324fb06..6cc382ecf5 100644 --- a/lib/extension/frontend.ts +++ b/lib/extension/frontend.ts @@ -21,6 +21,7 @@ export default class Frontend extends Extension { private mqttBaseTopic = settings.get().mqtt.base_topic; private host = settings.get().frontend.host; private port = settings.get().frontend.port; + private path = settings.get().frontend.path; private sslCert = settings.get().frontend.ssl_cert; private sslKey = settings.get().frontend.ssl_key; private authToken = settings.get().frontend.auth_token; @@ -71,7 +72,7 @@ export default class Frontend extends Extension { }, }; this.fileServer = gzipStatic(frontend.getPath(), options); - this.wss = new WebSocket.Server({noServer: true}); + this.wss = new WebSocket.Server({noServer: true, path: this.path}); this.wss.on('connection', this.onWebSocketConnection); if (this.host.startsWith('/')) { @@ -97,8 +98,12 @@ export default class Frontend extends Extension { } @bind private onRequest(request: http.IncomingMessage, response: http.ServerResponse): void { - // @ts-ignore - this.fileServer(request, response, finalhandler(request, response)); + if (request.url === this.path) { + // @ts-ignore + this.fileServer(request, response, finalhandler(request, response)); + } else { + response.writeHead(404).end(); + } } private authenticate(request: http.IncomingMessage, cb: (authenticate: boolean) => void): void { diff --git a/lib/types/types.d.ts b/lib/types/types.d.ts index 48d866c522..2057ef14dd 100644 --- a/lib/types/types.d.ts +++ b/lib/types/types.d.ts @@ -239,6 +239,7 @@ declare global { auth_token?: string, host?: string, port?: number, + path?: string, url?: string, ssl_cert?: string, ssl_key?: string, diff --git a/lib/util/settings.schema.json b/lib/util/settings.schema.json index e1cb739141..1ec2bf3165 100644 --- a/lib/util/settings.schema.json +++ b/lib/util/settings.schema.json @@ -378,6 +378,14 @@ "default": "0.0.0.0", "requiresRestart": true }, + "path":{ + "type": "string", + "title": "Path", + "description": "Path under which the frontend is available.", + "examples": ["/zigbee2mqtt"], + "default": "/", + "requiresRestart": true + }, "auth_token": { "type": ["string", "null"], "title": "Auth token", diff --git a/lib/util/settings.ts b/lib/util/settings.ts index 162bca8022..bcddb64347 100644 --- a/lib/util/settings.ts +++ b/lib/util/settings.ts @@ -150,7 +150,7 @@ function loadSettingsWithDefaults(): void { } if (_settingsWithDefaults.frontend) { - const defaults = {port: 8080, auth_token: false, host: '0.0.0.0'}; + const defaults = {port: 8080, auth_token: false, host: '0.0.0.0', path: '/'}; const s = typeof _settingsWithDefaults.frontend === 'object' ? _settingsWithDefaults.frontend : {}; // @ts-ignore _settingsWithDefaults.frontend = {}; diff --git a/test/frontend.test.js b/test/frontend.test.js index b164123c75..fe21405efd 100644 --- a/test/frontend.test.js +++ b/test/frontend.test.js @@ -54,6 +54,16 @@ const mockNodeStatic = { events: {}, }; +const mockRequest = { + url: '/', +}; + +const mockResponse = { + writeHead: jest.fn(() => ({ + end: jest.fn() + })), +}; + jest.mock('http', () => ({ createServer: jest.fn().mockImplementation((onRequest) => { mockHTTP.variables.onRequest = onRequest; @@ -99,7 +109,7 @@ describe('Frontend', () => { data.writeDefaultConfiguration(); data.writeDefaultState(); settings.reRead(); - settings.set(['frontend'], {port: 8081, host: "127.0.0.1"}); + settings.set(['frontend'], {port: 8081, host: "127.0.0.1", path: "/"}); settings.set(['homeassistant'], true); zigbeeHerdsman.devices.bulb.linkquality = 10; }); @@ -269,9 +279,9 @@ describe('Frontend', () => { mockWS.implementation.handleUpgrade.mock.calls[0][3](99); expect(mockWS.implementation.emit).toHaveBeenCalledWith('connection', 99, {"url": "http://localhost:8080/api"}); - mockHTTP.variables.onRequest(1, 2); + mockHTTP.variables.onRequest(mockRequest, mockResponse); expect(mockNodeStatic.implementation).toHaveBeenCalledTimes(1); - expect(mockNodeStatic.implementation).toHaveBeenCalledWith(1, 2, expect.any(Function)); + expect(mockNodeStatic.implementation).toHaveBeenCalledWith(mockRequest, mockResponse, expect.any(Function)); }); it('Static server', async () => { diff --git a/test/settings.test.js b/test/settings.test.js index 8171799a70..44acae8f70 100644 --- a/test/settings.test.js +++ b/test/settings.test.js @@ -935,7 +935,7 @@ describe('Settings', () => { }); settings.reRead(); - expect(settings.get().frontend).toStrictEqual({port: 8080, auth_token: false, host: '0.0.0.0'}) + expect(settings.get().frontend).toStrictEqual({port: 8080, auth_token: false, host: '0.0.0.0', path: '/'}) }); it('Baudrate config', () => {