mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Use WebSocket2023Channel identifier for WebSocket URL
This commit is contained in:
parent
26f24aa76c
commit
69af7c4e16
@ -6,7 +6,7 @@
|
|||||||
"@id": "urn:solid-server:default:WebSocket2023Listener",
|
"@id": "urn:solid-server:default:WebSocket2023Listener",
|
||||||
"@type": "WebSocket2023Listener",
|
"@type": "WebSocket2023Listener",
|
||||||
"storage": { "@id": "urn:solid-server:default:SubscriptionStorage" },
|
"storage": { "@id": "urn:solid-server:default:SubscriptionStorage" },
|
||||||
"route": { "@id": "urn:solid-server:default:WebSocket2023Route" },
|
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
|
||||||
"handler": {
|
"handler": {
|
||||||
"@type": "SequenceHandler",
|
"@type": "SequenceHandler",
|
||||||
"handlers": [
|
"handlers": [
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import type { IncomingMessage } from 'http';
|
import type { IncomingMessage } from 'http';
|
||||||
import type { WebSocket } from 'ws';
|
import type { WebSocket } from 'ws';
|
||||||
import type { InteractionRoute } from '../../../identity/interaction/routing/InteractionRoute';
|
|
||||||
import { getLoggerFor } from '../../../logging/LogUtil';
|
import { getLoggerFor } from '../../../logging/LogUtil';
|
||||||
import { WebSocketServerConfigurator } from '../../WebSocketServerConfigurator';
|
import { WebSocketServerConfigurator } from '../../WebSocketServerConfigurator';
|
||||||
import type { NotificationChannelStorage } from '../NotificationChannelStorage';
|
import type { NotificationChannelStorage } from '../NotificationChannelStorage';
|
||||||
@ -16,27 +15,17 @@ export class WebSocket2023Listener extends WebSocketServerConfigurator {
|
|||||||
|
|
||||||
private readonly storage: NotificationChannelStorage;
|
private readonly storage: NotificationChannelStorage;
|
||||||
private readonly handler: WebSocket2023Handler;
|
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();
|
super();
|
||||||
this.storage = storage;
|
this.storage = storage;
|
||||||
this.handler = handler;
|
this.handler = handler;
|
||||||
this.path = new URL(route.getPath()).pathname;
|
this.baseUrl = baseUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async handleConnection(webSocket: WebSocket, upgradeRequest: IncomingMessage): Promise<void> {
|
protected async handleConnection(webSocket: WebSocket, upgradeRequest: IncomingMessage): Promise<void> {
|
||||||
const { path, id } = parseWebSocketRequest(upgradeRequest);
|
const id = parseWebSocketRequest(this.baseUrl, 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 channel = await this.storage.get(id);
|
const channel = await this.storage.get(id);
|
||||||
|
|
||||||
|
@ -1,34 +1,32 @@
|
|||||||
import type { IncomingMessage } from 'http';
|
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
|
* 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 id - The identifier of the channel. Needs to be a URL.
|
||||||
* @param url - The HTTP(S) URL.
|
|
||||||
* @param id - The identifier to use as `auth` parameter.
|
|
||||||
*/
|
*/
|
||||||
export function generateWebSocketUrl(url: string, id: string): string {
|
export function generateWebSocketUrl(id: string): string {
|
||||||
return `ws${url.slice('http'.length)}?auth=${encodeURIComponent(id)}`;
|
return `ws${id.slice('http'.length)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses a {@link IncomingMessage} to extract both its path and the identifier used for authentication.
|
* Parses a {@link IncomingMessage} to extract its path 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 baseUrl - The base URL of the server.
|
||||||
* @param request - The request to parse.
|
* @param request - The request to parse.
|
||||||
*/
|
*/
|
||||||
export function parseWebSocketRequest(request: IncomingMessage): { path: string; id?: string } {
|
export function parseWebSocketRequest(baseUrl: string, request: IncomingMessage): string {
|
||||||
// Base doesn't matter since we just want the path and query parameter
|
const path = request.url;
|
||||||
const { pathname, searchParams } = new URL(request.url ?? '', 'http://example.com');
|
|
||||||
|
|
||||||
let auth: string | undefined;
|
if (!path) {
|
||||||
if (searchParams.has('auth')) {
|
throw new BadRequestHttpError('Missing url parameter in WebSocket request');
|
||||||
auth = decodeURIComponent(searchParams.get('auth')!);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
// Use dummy base and then explicitly set the host and protocol from the base URL.
|
||||||
path: pathname,
|
const id = new URL(path, 'http://example.com');
|
||||||
id: auth,
|
const base = new URL(baseUrl);
|
||||||
};
|
id.host = base.host;
|
||||||
|
id.protocol = base.protocol;
|
||||||
|
|
||||||
|
return id.href;
|
||||||
}
|
}
|
||||||
|
@ -43,7 +43,7 @@ export class WebSocketChannel2023Type extends BaseChannelType {
|
|||||||
return {
|
return {
|
||||||
...channel,
|
...channel,
|
||||||
type: NOTIFY.WebSocketChannel2023,
|
type: NOTIFY.WebSocketChannel2023,
|
||||||
receiveFrom: generateWebSocketUrl(this.path, channel.id),
|
receiveFrom: generateWebSocketUrl(channel.id),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import type { Server } from 'http';
|
import type { Server } from 'http';
|
||||||
import type { WebSocket } from 'ws';
|
import type { WebSocket } from 'ws';
|
||||||
import {
|
|
||||||
AbsolutePathInteractionRoute,
|
|
||||||
} from '../../../../../src/identity/interaction/routing/AbsolutePathInteractionRoute';
|
|
||||||
import type { HttpRequest } from '../../../../../src/server/HttpRequest';
|
import type { HttpRequest } from '../../../../../src/server/HttpRequest';
|
||||||
import type { NotificationChannel } from '../../../../../src/server/notifications/NotificationChannel';
|
import type { NotificationChannel } from '../../../../../src/server/notifications/NotificationChannel';
|
||||||
import type {
|
import type {
|
||||||
@ -32,13 +30,12 @@ describe('A WebSocket2023Listener', (): void => {
|
|||||||
topic: 'http://example.com/foo',
|
topic: 'http://example.com/foo',
|
||||||
type: 'type',
|
type: 'type',
|
||||||
};
|
};
|
||||||
const auth = '123456';
|
|
||||||
let server: Server;
|
let server: Server;
|
||||||
let webSocket: WebSocket;
|
let webSocket: WebSocket;
|
||||||
let upgradeRequest: HttpRequest;
|
let upgradeRequest: HttpRequest;
|
||||||
let storage: jest.Mocked<NotificationChannelStorage>;
|
let storage: jest.Mocked<NotificationChannelStorage>;
|
||||||
let handler: jest.Mocked<WebSocket2023Handler>;
|
let handler: jest.Mocked<WebSocket2023Handler>;
|
||||||
const route = new AbsolutePathInteractionRoute('http://example.com/foo');
|
const baseUrl = 'http://example.com/';
|
||||||
let listener: WebSocket2023Listener;
|
let listener: WebSocket2023Listener;
|
||||||
|
|
||||||
beforeEach(async(): Promise<void> => {
|
beforeEach(async(): Promise<void> => {
|
||||||
@ -47,7 +44,7 @@ describe('A WebSocket2023Listener', (): void => {
|
|||||||
webSocket.send = jest.fn();
|
webSocket.send = jest.fn();
|
||||||
webSocket.close = jest.fn();
|
webSocket.close = jest.fn();
|
||||||
|
|
||||||
upgradeRequest = { url: `/foo?auth=${auth}` } as any;
|
upgradeRequest = { url: `/foo/123456` } as any;
|
||||||
|
|
||||||
storage = {
|
storage = {
|
||||||
get: jest.fn().mockResolvedValue(channel),
|
get: jest.fn().mockResolvedValue(channel),
|
||||||
@ -57,47 +54,11 @@ describe('A WebSocket2023Listener', (): void => {
|
|||||||
handleSafe: jest.fn(),
|
handleSafe: jest.fn(),
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
listener = new WebSocket2023Listener(storage, handler, route);
|
listener = new WebSocket2023Listener(storage, handler, baseUrl);
|
||||||
await listener.handle(server);
|
await listener.handle(server);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('rejects request targeting an unknown path.', async(): Promise<void> => {
|
it('rejects requests with an unknown target.', async(): Promise<void> => {
|
||||||
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<void> => {
|
|
||||||
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<void> => {
|
|
||||||
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<void> => {
|
|
||||||
storage.get.mockResolvedValue(undefined);
|
storage.get.mockResolvedValue(undefined);
|
||||||
server.emit('upgrade', upgradeRequest, webSocket);
|
server.emit('upgrade', upgradeRequest, webSocket);
|
||||||
|
|
||||||
|
@ -2,26 +2,32 @@ import type { IncomingMessage } from 'http';
|
|||||||
import {
|
import {
|
||||||
generateWebSocketUrl, parseWebSocketRequest,
|
generateWebSocketUrl, parseWebSocketRequest,
|
||||||
} from '../../../../../src/server/notifications/WebSocketChannel2023/WebSocket2023Util';
|
} from '../../../../../src/server/notifications/WebSocketChannel2023/WebSocket2023Util';
|
||||||
|
import { BadRequestHttpError } from '../../../../../src/util/errors/BadRequestHttpError';
|
||||||
|
|
||||||
describe('WebSocket2023Util', (): void => {
|
describe('WebSocket2023Util', (): void => {
|
||||||
describe('#generateWebSocketUrl', (): void => {
|
describe('#generateWebSocketUrl', (): void => {
|
||||||
it('generates a WebSocket link with a query parameter.', async(): Promise<void> => {
|
it('generates a WebSocket link.', async(): Promise<void> => {
|
||||||
expect(generateWebSocketUrl('http://example.com/', '123456')).toBe('ws://example.com/?auth=123456');
|
expect(generateWebSocketUrl('http://example.com/123456')).toBe('ws://example.com/123456');
|
||||||
|
|
||||||
expect(generateWebSocketUrl('https://example.com/foo/bar', '123456'))
|
expect(generateWebSocketUrl('https://example.com/foo/bar/123456'))
|
||||||
.toBe('wss://example.com/foo/bar?auth=123456');
|
.toBe('wss://example.com/foo/bar/123456');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('#parseWebSocketRequest', (): void => {
|
describe('#parseWebSocketRequest', (): void => {
|
||||||
it('parses the request.', async(): Promise<void> => {
|
it('parses the request.', async(): Promise<void> => {
|
||||||
const request: IncomingMessage = { url: '/foo/bar?auth=123%24456' } as any;
|
const request: IncomingMessage = { url: '/foo/bar/123%24456' } as any;
|
||||||
expect(parseWebSocketRequest(request)).toEqual({ path: '/foo/bar', id: '123$456' });
|
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<void> => {
|
it('throws an error if the url parameter is not defined.', async(): Promise<void> => {
|
||||||
const request: IncomingMessage = {} as any;
|
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<void> => {
|
||||||
|
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');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -39,7 +39,7 @@ describe('A WebSocketChannel2023', (): void => {
|
|||||||
id,
|
id,
|
||||||
type: NOTIFY.WebSocketChannel2023,
|
type: NOTIFY.WebSocketChannel2023,
|
||||||
topic,
|
topic,
|
||||||
receiveFrom: generateWebSocketUrl(route.getPath(), id),
|
receiveFrom: generateWebSocketUrl(id),
|
||||||
};
|
};
|
||||||
|
|
||||||
channelType = new WebSocketChannel2023Type(route);
|
channelType = new WebSocketChannel2023Type(route);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user