diff --git a/config/http/notifications/websockets/http.json b/config/http/notifications/websockets/http.json index a69f775e65..8198da882a 100644 --- a/config/http/notifications/websockets/http.json +++ b/config/http/notifications/websockets/http.json @@ -6,7 +6,7 @@ "@id": "urn:solid-server:default:WebSocket2023Listener", "@type": "WebSocket2023Listener", "storage": { "@id": "urn:solid-server:default:SubscriptionStorage" }, - "route": { "@id": "urn:solid-server:default:WebSocket2023Route" }, + "baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }, "handler": { "@type": "SequenceHandler", "handlers": [ diff --git a/src/server/notifications/WebSocketChannel2023/WebSocket2023Listener.ts b/src/server/notifications/WebSocketChannel2023/WebSocket2023Listener.ts index 38fc2afb03..d2216bb657 100644 --- a/src/server/notifications/WebSocketChannel2023/WebSocket2023Listener.ts +++ b/src/server/notifications/WebSocketChannel2023/WebSocket2023Listener.ts @@ -1,6 +1,5 @@ import type { IncomingMessage } from 'http'; import type { WebSocket } from 'ws'; -import type { InteractionRoute } from '../../../identity/interaction/routing/InteractionRoute'; import { getLoggerFor } from '../../../logging/LogUtil'; import { WebSocketServerConfigurator } from '../../WebSocketServerConfigurator'; import type { NotificationChannelStorage } from '../NotificationChannelStorage'; @@ -16,27 +15,17 @@ export class WebSocket2023Listener extends WebSocketServerConfigurator { private readonly storage: NotificationChannelStorage; private readonly handler: WebSocket2023Handler; - private readonly path: string; + private readonly baseUrl: string; - public constructor(storage: NotificationChannelStorage, handler: WebSocket2023Handler, route: InteractionRoute) { + public constructor(storage: NotificationChannelStorage, handler: WebSocket2023Handler, baseUrl: string) { super(); this.storage = storage; this.handler = handler; - this.path = new URL(route.getPath()).pathname; + this.baseUrl = baseUrl; } protected async handleConnection(webSocket: WebSocket, upgradeRequest: IncomingMessage): Promise { - const { path, id } = parseWebSocketRequest(upgradeRequest); - - if (path !== this.path) { - webSocket.send('Unknown WebSocket target.'); - return webSocket.close(); - } - - if (!id) { - webSocket.send('Missing auth parameter from WebSocket URL.'); - return webSocket.close(); - } + const id = parseWebSocketRequest(this.baseUrl, upgradeRequest); const channel = await this.storage.get(id); diff --git a/src/server/notifications/WebSocketChannel2023/WebSocket2023Util.ts b/src/server/notifications/WebSocketChannel2023/WebSocket2023Util.ts index 77bc34fb40..731884c2d9 100644 --- a/src/server/notifications/WebSocketChannel2023/WebSocket2023Util.ts +++ b/src/server/notifications/WebSocketChannel2023/WebSocket2023Util.ts @@ -1,34 +1,32 @@ import type { IncomingMessage } from 'http'; +import { BadRequestHttpError } from '../../../util/errors/BadRequestHttpError'; /** - * 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. + * Generates a WebSocket URL by converting an HTTP(S) URL into a WS(S) URL. + * @param id - The identifier of the channel. Needs to be a URL. */ -export function generateWebSocketUrl(url: string, id: string): string { - return `ws${url.slice('http'.length)}?auth=${encodeURIComponent(id)}`; +export function generateWebSocketUrl(id: string): string { + return `ws${id.slice('http'.length)}`; } /** - * 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' }`. + * Parses a {@link IncomingMessage} to extract its path used for authentication. * + * @param baseUrl - The base URL of the server. * @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'); +export function parseWebSocketRequest(baseUrl: string, request: IncomingMessage): string { + const path = request.url; - let auth: string | undefined; - if (searchParams.has('auth')) { - auth = decodeURIComponent(searchParams.get('auth')!); + if (!path) { + throw new BadRequestHttpError('Missing url parameter in WebSocket request'); } - return { - path: pathname, - id: auth, - }; + // Use dummy base and then explicitly set the host and protocol from the base URL. + const id = new URL(path, 'http://example.com'); + const base = new URL(baseUrl); + id.host = base.host; + id.protocol = base.protocol; + + return id.href; } diff --git a/src/server/notifications/WebSocketChannel2023/WebSocketChannel2023Type.ts b/src/server/notifications/WebSocketChannel2023/WebSocketChannel2023Type.ts index 1569872c98..907961e314 100644 --- a/src/server/notifications/WebSocketChannel2023/WebSocketChannel2023Type.ts +++ b/src/server/notifications/WebSocketChannel2023/WebSocketChannel2023Type.ts @@ -43,7 +43,7 @@ export class WebSocketChannel2023Type extends BaseChannelType { return { ...channel, type: NOTIFY.WebSocketChannel2023, - receiveFrom: generateWebSocketUrl(this.path, channel.id), + receiveFrom: generateWebSocketUrl(channel.id), }; } } diff --git a/test/unit/server/notifications/WebSocketChannel2023/WebSocket2023Listener.test.ts b/test/unit/server/notifications/WebSocketChannel2023/WebSocket2023Listener.test.ts index a3f316587b..e43eca85a9 100644 --- a/test/unit/server/notifications/WebSocketChannel2023/WebSocket2023Listener.test.ts +++ b/test/unit/server/notifications/WebSocketChannel2023/WebSocket2023Listener.test.ts @@ -1,9 +1,7 @@ import { EventEmitter } from 'events'; import type { Server } from 'http'; import type { WebSocket } from 'ws'; -import { - AbsolutePathInteractionRoute, -} from '../../../../../src/identity/interaction/routing/AbsolutePathInteractionRoute'; + import type { HttpRequest } from '../../../../../src/server/HttpRequest'; import type { NotificationChannel } from '../../../../../src/server/notifications/NotificationChannel'; import type { @@ -32,13 +30,12 @@ describe('A WebSocket2023Listener', (): void => { topic: 'http://example.com/foo', type: 'type', }; - const auth = '123456'; let server: Server; let webSocket: WebSocket; let upgradeRequest: HttpRequest; let storage: jest.Mocked; let handler: jest.Mocked; - const route = new AbsolutePathInteractionRoute('http://example.com/foo'); + const baseUrl = 'http://example.com/'; let listener: WebSocket2023Listener; beforeEach(async(): Promise => { @@ -47,7 +44,7 @@ describe('A WebSocket2023Listener', (): void => { webSocket.send = jest.fn(); webSocket.close = jest.fn(); - upgradeRequest = { url: `/foo?auth=${auth}` } as any; + upgradeRequest = { url: `/foo/123456` } as any; storage = { get: jest.fn().mockResolvedValue(channel), @@ -57,47 +54,11 @@ describe('A WebSocket2023Listener', (): void => { handleSafe: jest.fn(), } as any; - listener = new WebSocket2023Listener(storage, handler, route); + listener = new WebSocket2023Listener(storage, handler, baseUrl); await listener.handle(server); }); - it('rejects request targeting an unknown path.', async(): Promise => { - upgradeRequest.url = '/wrong'; - server.emit('upgrade', upgradeRequest, webSocket); - - await flushPromises(); - - expect(webSocket.send).toHaveBeenCalledTimes(1); - expect(webSocket.send).toHaveBeenLastCalledWith('Unknown WebSocket target.'); - expect(webSocket.close).toHaveBeenCalledTimes(1); - expect(handler.handleSafe).toHaveBeenCalledTimes(0); - }); - - it('rejects request with no url.', async(): Promise => { - delete upgradeRequest.url; - server.emit('upgrade', upgradeRequest, webSocket); - - await flushPromises(); - - expect(webSocket.send).toHaveBeenCalledTimes(1); - expect(webSocket.send).toHaveBeenLastCalledWith('Unknown WebSocket target.'); - expect(webSocket.close).toHaveBeenCalledTimes(1); - expect(handler.handleSafe).toHaveBeenCalledTimes(0); - }); - - it('rejects requests without an auth parameter.', async(): Promise => { - upgradeRequest.url = '/foo'; - server.emit('upgrade', upgradeRequest, webSocket); - - await flushPromises(); - - expect(webSocket.send).toHaveBeenCalledTimes(1); - expect(webSocket.send).toHaveBeenLastCalledWith('Missing auth parameter from WebSocket URL.'); - expect(webSocket.close).toHaveBeenCalledTimes(1); - expect(handler.handleSafe).toHaveBeenCalledTimes(0); - }); - - it('rejects requests with an unknown auth parameter.', async(): Promise => { + it('rejects requests with an unknown target.', async(): Promise => { storage.get.mockResolvedValue(undefined); server.emit('upgrade', upgradeRequest, webSocket); diff --git a/test/unit/server/notifications/WebSocketChannel2023/WebSocket2023Util.test.ts b/test/unit/server/notifications/WebSocketChannel2023/WebSocket2023Util.test.ts index 78530885b2..91c12a9ddf 100644 --- a/test/unit/server/notifications/WebSocketChannel2023/WebSocket2023Util.test.ts +++ b/test/unit/server/notifications/WebSocketChannel2023/WebSocket2023Util.test.ts @@ -2,26 +2,32 @@ import type { IncomingMessage } from 'http'; import { generateWebSocketUrl, parseWebSocketRequest, } from '../../../../../src/server/notifications/WebSocketChannel2023/WebSocket2023Util'; +import { BadRequestHttpError } from '../../../../../src/util/errors/BadRequestHttpError'; describe('WebSocket2023Util', (): void => { 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'); + it('generates a WebSocket link.', async(): Promise => { + expect(generateWebSocketUrl('http://example.com/123456')).toBe('ws://example.com/123456'); - expect(generateWebSocketUrl('https://example.com/foo/bar', '123456')) - .toBe('wss://example.com/foo/bar?auth=123456'); + expect(generateWebSocketUrl('https://example.com/foo/bar/123456')) + .toBe('wss://example.com/foo/bar/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' }); + const request: IncomingMessage = { url: '/foo/bar/123%24456' } as any; + expect(parseWebSocketRequest('http://example.com/', request)).toBe('http://example.com/foo/bar/123%24456'); }); - it('returns an empty path and no id if the url parameter is undefined.', async(): Promise => { + it('throws an error if the url parameter is not defined.', async(): Promise => { const request: IncomingMessage = {} as any; - expect(parseWebSocketRequest(request)).toEqual({ path: '/' }); + expect((): string => parseWebSocketRequest('http://example.com/', request)).toThrow(BadRequestHttpError); + }); + + it('can handle non-root base URLs.', async(): Promise => { + const request: IncomingMessage = { url: '/foo/bar/123%24456' } as any; + expect(parseWebSocketRequest('http://example.com/foo/bar/', request)).toBe('http://example.com/foo/bar/123%24456'); }); }); }); diff --git a/test/unit/server/notifications/WebSocketChannel2023/WebSocketChannel2023Type.test.ts b/test/unit/server/notifications/WebSocketChannel2023/WebSocketChannel2023Type.test.ts index 4643bfcb13..1e75053ef7 100644 --- a/test/unit/server/notifications/WebSocketChannel2023/WebSocketChannel2023Type.test.ts +++ b/test/unit/server/notifications/WebSocketChannel2023/WebSocketChannel2023Type.test.ts @@ -39,7 +39,7 @@ describe('A WebSocketChannel2023', (): void => { id, type: NOTIFY.WebSocketChannel2023, topic, - receiveFrom: generateWebSocketUrl(route.getPath(), id), + receiveFrom: generateWebSocketUrl(id), }; channelType = new WebSocketChannel2023Type(route);