From 15234b98e4fc9d41b45fb2d5bda705204fd42ed3 Mon Sep 17 00:00:00 2001 From: Justus Perlwitz Date: Sun, 8 Oct 2023 21:12:48 +0900 Subject: [PATCH] Validate URLs when constructing Sarus object This will better inform a user about invalid URLs when constructing a new Sarus object. It will already give an error during construction instead of the connect step. That way, we won't repeatedly try to connect to the same URL, despite it being invalid the whole time. Add test case in __tests__ for - Completely invalid URL (fails to be fed into new URL()) - Invalid protocol (must be ws / wss) We use stock JS functionality for this, so no custom URL parser. --- __tests__/index/connectionOptions.test.ts | 20 +++++++++++++++ src/index.ts | 31 +++++++++++++++++++++-- src/lib/constants.ts | 2 ++ 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/__tests__/index/connectionOptions.test.ts b/__tests__/index/connectionOptions.test.ts index 4b26373..699bb71 100644 --- a/__tests__/index/connectionOptions.test.ts +++ b/__tests__/index/connectionOptions.test.ts @@ -18,6 +18,26 @@ describe("connection options", () => { server.close(); }); + it("should correctly validate invalid WebSocket URLs", () => { + // Testing with jest-websocket-mock will not give us a TypeError here. + // We re-throw the error therefore. Testing it in a browser we can + // see that a TypeError is handled correctly. + expect(() => { + new Sarus({ url: "invalid-url" }); + }).toThrow("invalid"); + + expect(() => { + new Sarus({ url: "http://wrong-protocol" }); + }).toThrow("have protocol"); + + expect(() => { + new Sarus({ url: "https://also-wrong-protocol" }); + }).toThrow("have protocol"); + + new Sarus({ url: "ws://this-will-pass" }); + new Sarus({ url: "wss://this-too-shall-pass" }); + }); + it("should set the WebSocket protocols value to an empty string if nothing is passed", async () => { const sarus: Sarus = new Sarus({ url }); await server.connected; diff --git a/src/index.ts b/src/index.ts index 83b69ff..aa9fe00 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,6 @@ // File Dependencies import { + ALLOWED_PROTOCOLS, WS_EVENT_NAMES, DATA_STORAGE_TYPES, DEFAULT_EVENT_LISTENERS_OBJECT, @@ -46,6 +47,32 @@ const getMessagesFromStore = ({ storageType, storageKey }: StorageParams) => { } }; +const validateWebSocketUrl = (rawUrl: string): URL => { + let url: URL; + try { + // Alternatively, we can also check with URL.canParse(), but since we need + // the URL object anyway to validate the protocol, we go ahead and parse it + // here. + url = new URL(rawUrl); + } catch (e) { + // TypeError, as specified by WHATWG URL Standard: + // https://url.spec.whatwg.org/#url-class (see constructor steps) + if (!(e instanceof TypeError)) { + throw e; + } + // Untested - our URL mock does not give us an instance of TypeError + const { message } = e; + throw new Error(`The WebSocket URL is not valid: ${message}`); + } + const { protocol } = url; + if (!ALLOWED_PROTOCOLS.includes(protocol)) { + throw new Error( + `Expected the WebSocket URL to have protocol 'ws:' or 'wss:', got '${protocol}' instead.`, + ); + } + return url; +}; + export interface SarusClassParams { url: string; binaryType?: BinaryType; @@ -75,7 +102,7 @@ export interface SarusClassParams { */ export default class Sarus { // Constructor params - url: string; + url: URL; binaryType?: BinaryType; protocols?: string | Array; eventListeners: EventListenersInterface; @@ -106,7 +133,7 @@ export default class Sarus { this.eventListeners = this.auditEventListeners(eventListeners); // Sets the WebSocket server url for the client to connect to. - this.url = url; + this.url = validateWebSocketUrl(url); // Sets the binaryType of the data being sent over the connection this.binaryType = binaryType; diff --git a/src/lib/constants.ts b/src/lib/constants.ts index e0fcb07..9153ff0 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -1,6 +1,8 @@ // Dependencies import { EventListenersInterface } from "./validators"; +export const ALLOWED_PROTOCOLS: Array = ["ws:", "wss:"]; + /** * A definitive list of events for a WebSocket client to listen on * @constant