From cb619415fab51c52bfbbdaf1ea09c2ee3039d5fd Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Tue, 15 Nov 2022 12:36:29 +0100 Subject: [PATCH] refactor: Move WebSocket URL handling to utility functions --- src/index.ts | 1 + .../WebSocket2021Listener.ts | 11 +++--- .../WebSocket2021Util.ts | 34 +++++++++++++++++++ .../WebSocketSubscription2021.ts | 3 +- .../WebSocket2021Util.test.ts | 27 +++++++++++++++ 5 files changed, 68 insertions(+), 8 deletions(-) create mode 100644 src/server/notifications/WebSocketSubscription2021/WebSocket2021Util.ts create mode 100644 test/unit/server/notifications/WebSocketSubscription2021/WebSocket2021Util.test.ts diff --git a/src/index.ts b/src/index.ts index 82c9ba4d5..2411bace3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -325,6 +325,7 @@ export * from './server/notifications/WebSocketSubscription2021/WebSocket2021Emi export * from './server/notifications/WebSocketSubscription2021/WebSocket2021Handler'; export * from './server/notifications/WebSocketSubscription2021/WebSocket2021Listener'; export * from './server/notifications/WebSocketSubscription2021/WebSocket2021Storer'; +export * from './server/notifications/WebSocketSubscription2021/WebSocket2021Util'; export * from './server/notifications/WebSocketSubscription2021/WebSocketMap'; export * from './server/notifications/WebSocketSubscription2021/WebSocketSubscription2021'; diff --git a/src/server/notifications/WebSocketSubscription2021/WebSocket2021Listener.ts b/src/server/notifications/WebSocketSubscription2021/WebSocket2021Listener.ts index 9d29e5c75..ad55533d3 100644 --- a/src/server/notifications/WebSocketSubscription2021/WebSocket2021Listener.ts +++ b/src/server/notifications/WebSocketSubscription2021/WebSocket2021Listener.ts @@ -5,6 +5,7 @@ import { getLoggerFor } from '../../../logging/LogUtil'; import { WebSocketServerConfigurator } from '../../WebSocketServerConfigurator'; import type { SubscriptionStorage } from '../SubscriptionStorage'; import type { WebSocket2021Handler } from './WebSocket2021Handler'; +import { parseWebSocketRequest } from './WebSocket2021Util'; /** * Listens for WebSocket connections and verifies if they are valid WebSocketSubscription2021 connections, @@ -25,22 +26,18 @@ export class WebSocket2021Listener extends WebSocketServerConfigurator { } protected async handleConnection(webSocket: WebSocket, upgradeRequest: IncomingMessage): Promise { - // Base doesn't matter since we just want the path and query parameter - const { pathname, searchParams } = new URL(upgradeRequest.url ?? '', 'http://example.com'); + const { path, id } = parseWebSocketRequest(upgradeRequest); - if (pathname !== this.path) { + if (path !== this.path) { webSocket.send('Unknown WebSocket target.'); return webSocket.close(); } - const auth = searchParams.get('auth'); - - if (!auth) { + if (!id) { webSocket.send('Missing auth parameter from WebSocket URL.'); return webSocket.close(); } - const id = decodeURI(auth); const info = await this.storage.get(id); if (!info) { diff --git a/src/server/notifications/WebSocketSubscription2021/WebSocket2021Util.ts b/src/server/notifications/WebSocketSubscription2021/WebSocket2021Util.ts new file mode 100644 index 000000000..77bc34fb4 --- /dev/null +++ b/src/server/notifications/WebSocketSubscription2021/WebSocket2021Util.ts @@ -0,0 +1,34 @@ +import type { IncomingMessage } from 'http'; + +/** + * Generates a WebSocket URL by converting an HTTP(S) URL into a WS(S) URL + * and adding the `auth` query parameter using the identifier. + * @param url - The HTTP(S) URL. + * @param id - The identifier to use as `auth` parameter. + */ +export function generateWebSocketUrl(url: string, id: string): string { + return `ws${url.slice('http'.length)}?auth=${encodeURIComponent(id)}`; +} + +/** + * Parses a {@link IncomingMessage} to extract both its path and the identifier used for authentication. + * The returned path is relative to the host. + * + * E.g., a request to `ws://example.com/foo/bar?auth=123456` would return `{ path: '/foo/bar', id: '123456' }`. + * + * @param request - The request to parse. + */ +export function parseWebSocketRequest(request: IncomingMessage): { path: string; id?: string } { + // Base doesn't matter since we just want the path and query parameter + const { pathname, searchParams } = new URL(request.url ?? '', 'http://example.com'); + + let auth: string | undefined; + if (searchParams.has('auth')) { + auth = decodeURIComponent(searchParams.get('auth')!); + } + + return { + path: pathname, + id: auth, + }; +} diff --git a/src/server/notifications/WebSocketSubscription2021/WebSocketSubscription2021.ts b/src/server/notifications/WebSocketSubscription2021/WebSocketSubscription2021.ts index 8936dfec0..ce29c913c 100644 --- a/src/server/notifications/WebSocketSubscription2021/WebSocketSubscription2021.ts +++ b/src/server/notifications/WebSocketSubscription2021/WebSocketSubscription2021.ts @@ -11,6 +11,7 @@ import type { Subscription } from '../Subscription'; import { SUBSCRIBE_SCHEMA } from '../Subscription'; import type { SubscriptionStorage } from '../SubscriptionStorage'; import type { SubscriptionResponse, SubscriptionType } from '../SubscriptionType'; +import { generateWebSocketUrl } from './WebSocket2021Util'; const type = 'WebSocketSubscription2021'; const schema = SUBSCRIBE_SCHEMA.shape({ @@ -48,7 +49,7 @@ export class WebSocketSubscription2021 implements SubscriptionType { + describe('#generateWebSocketUrl', (): void => { + it('generates a WebSocket link with a query parameter.', async(): Promise => { + expect(generateWebSocketUrl('http://example.com/', '123456')).toBe('ws://example.com/?auth=123456'); + + expect(generateWebSocketUrl('https://example.com/foo/bar', '123456')) + .toBe('wss://example.com/foo/bar?auth=123456'); + }); + }); + + describe('#parseWebSocketRequest', (): void => { + it('parses the request.', async(): Promise => { + const request: IncomingMessage = { url: '/foo/bar?auth=123%24456' } as any; + expect(parseWebSocketRequest(request)).toEqual({ path: '/foo/bar', id: '123$456' }); + }); + + it('returns an empty path and no id if the url parameter is undefined.', async(): Promise => { + const request: IncomingMessage = {} as any; + expect(parseWebSocketRequest(request)).toEqual({ path: '/' }); + }); + }); +});