mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00

* Fallback to X-Forwarded-* headers This uses the first value from X-Forwarded-Host and X-Forwarded-Proto if they're present and the standard Forwarded header is not. * Update parseForwarded to handle X-Forwarded-* This updates the signature for parseForwarded to take in the headers and handle the logic of falling back to X-Forwarded-* headers. * Update src/util/HeaderUtil.ts Co-authored-by: Ruben Verborgh <ruben@verborgh.org> * Inline parseXForwarded helper Additionally fixes a typo, updates a unit test, and removes a typing that is no longer necessary. * Tweak handling of X-Forwarded value checking and assignment * Fix: terminology & consistency suggestions from review Co-authored-by: Ruben Verborgh <ruben@verborgh.org> Co-authored-by: Ruben Verborgh <ruben@verborgh.org> Co-authored-by: Wouter Termont <woutermont@gmail.com>
147 lines
4.8 KiB
TypeScript
147 lines
4.8 KiB
TypeScript
import { EventEmitter } from 'events';
|
|
import type WebSocket from 'ws';
|
|
import { getLoggerFor } from '../logging/LogUtil';
|
|
import type { HttpRequest } from '../server/HttpRequest';
|
|
import { WebSocketHandler } from '../server/WebSocketHandler';
|
|
import { parseForwarded } from '../util/HeaderUtil';
|
|
import type { ResourceIdentifier } from './representation/ResourceIdentifier';
|
|
|
|
const VERSION = 'solid-0.1';
|
|
|
|
/**
|
|
* Implementation of Solid WebSockets API Spec solid-0.1
|
|
* at https://github.com/solid/solid-spec/blob/master/api-websockets.md
|
|
*/
|
|
class WebSocketListener extends EventEmitter {
|
|
private host = '';
|
|
private protocol = '';
|
|
private readonly socket: WebSocket;
|
|
private readonly subscribedPaths = new Set<string>();
|
|
private readonly logger = getLoggerFor(this);
|
|
|
|
public constructor(socket: WebSocket) {
|
|
super();
|
|
this.socket = socket;
|
|
socket.addListener('error', (): void => this.stop());
|
|
socket.addListener('close', (): void => this.stop());
|
|
socket.addListener('message', (message: string): void => this.onMessage(message));
|
|
}
|
|
|
|
public start({ headers, socket }: HttpRequest): void {
|
|
// Greet the client
|
|
this.sendMessage('protocol', VERSION);
|
|
|
|
// Verify the WebSocket protocol version
|
|
const protocolHeader = headers['sec-websocket-protocol'];
|
|
if (!protocolHeader) {
|
|
this.sendMessage('warning', `Missing Sec-WebSocket-Protocol header, expected value '${VERSION}'`);
|
|
} else {
|
|
const supportedProtocols = protocolHeader.split(/\s*,\s*/u);
|
|
if (!supportedProtocols.includes(VERSION)) {
|
|
this.sendMessage('error', `Client does not support protocol ${VERSION}`);
|
|
this.stop();
|
|
}
|
|
}
|
|
|
|
// Store the HTTP host and protocol
|
|
const forwarded = parseForwarded(headers);
|
|
this.host = forwarded.host ?? headers.host ?? 'localhost';
|
|
this.protocol = forwarded.proto === 'https' || (socket as any).secure ? 'https:' : 'http:';
|
|
}
|
|
|
|
private stop(): void {
|
|
try {
|
|
this.socket.close();
|
|
} catch {
|
|
// Ignore
|
|
}
|
|
this.subscribedPaths.clear();
|
|
this.socket.removeAllListeners();
|
|
this.emit('closed');
|
|
}
|
|
|
|
public onResourceChanged({ path }: ResourceIdentifier): void {
|
|
if (this.subscribedPaths.has(path)) {
|
|
this.sendMessage('pub', path);
|
|
}
|
|
}
|
|
|
|
private onMessage(message: string): void {
|
|
// Parse the message
|
|
const match = /^(\w+)\s+(.+)$/u.exec(message);
|
|
if (!match) {
|
|
this.sendMessage('warning', `Unrecognized message format: ${message}`);
|
|
return;
|
|
}
|
|
|
|
// Process the message
|
|
const [ , type, value ] = match;
|
|
switch (type) {
|
|
case 'sub':
|
|
this.subscribe(value);
|
|
break;
|
|
default:
|
|
this.sendMessage('warning', `Unrecognized message type: ${type}`);
|
|
}
|
|
}
|
|
|
|
private subscribe(path: string): void {
|
|
try {
|
|
// Resolve and verify the URL
|
|
const resolved = new URL(path, `${this.protocol}${this.host}`);
|
|
if (resolved.host !== this.host) {
|
|
throw new Error(`Mismatched host: ${resolved.host} instead of ${this.host}`);
|
|
}
|
|
if (resolved.protocol !== this.protocol) {
|
|
throw new Error(`Mismatched protocol: ${resolved.protocol} instead of ${this.protocol}`);
|
|
}
|
|
// Subscribe to the URL
|
|
const url = resolved.href;
|
|
this.subscribedPaths.add(url);
|
|
this.sendMessage('ack', url);
|
|
this.logger.debug(`WebSocket subscribed to changes on ${url}`);
|
|
} catch (error: unknown) {
|
|
// Report errors to the socket
|
|
const errorText: string = (error as any).message;
|
|
this.sendMessage('error', errorText);
|
|
this.logger.warn(`WebSocket could not subscribe to ${path}: ${errorText}`);
|
|
}
|
|
}
|
|
|
|
private sendMessage(type: string, value: string): void {
|
|
this.socket.send(`${type} ${value}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Provides live update functionality following
|
|
* the Solid WebSockets API Spec solid-0.1
|
|
*/
|
|
export class UnsecureWebSocketsProtocol extends WebSocketHandler {
|
|
private readonly logger = getLoggerFor(this);
|
|
private readonly listeners = new Set<WebSocketListener>();
|
|
|
|
public constructor(source: EventEmitter) {
|
|
super();
|
|
source.on('changed', (changed: ResourceIdentifier): void => this.onResourceChanged(changed));
|
|
}
|
|
|
|
public async handle(input: { webSocket: WebSocket; upgradeRequest: HttpRequest }): Promise<void> {
|
|
const listener = new WebSocketListener(input.webSocket);
|
|
this.listeners.add(listener);
|
|
this.logger.info(`New WebSocket added, ${this.listeners.size} in total`);
|
|
|
|
listener.on('closed', (): void => {
|
|
this.listeners.delete(listener);
|
|
this.logger.info(`WebSocket closed, ${this.listeners.size} remaining`);
|
|
});
|
|
listener.start(input.upgradeRequest);
|
|
}
|
|
|
|
private onResourceChanged(changed: ResourceIdentifier): void {
|
|
for (const listener of this.listeners) {
|
|
listener.onResourceChanged(changed);
|
|
}
|
|
}
|
|
}
|