diff options
author | EuAndreh <eu@euandre.org> | 2024-03-16 06:03:40 -0300 |
---|---|---|
committer | EuAndreh <eu@euandre.org> | 2024-03-16 06:03:40 -0300 |
commit | c3387c891a4376a558f3c09c804f7aa45422ad41 (patch) | |
tree | 303eb83bf1064e51a0023a93331550d952d678b6 | |
parent | src/hero.mjs: Add "upgrade" and "socket" keys to `req` (diff) | |
download | papod-c3387c891a4376a558f3c09c804f7aa45422ad41.tar.gz papod-c3387c891a4376a558f3c09c804f7aa45422ad41.tar.xz |
src/hero.mjs: Add validateUpgrade(), computeHash() and their helper functions
-rw-r--r-- | src/hero.mjs | 99 | ||||
-rw-r--r-- | tests/js/hero.mjs | 171 |
2 files changed, 268 insertions, 2 deletions
diff --git a/src/hero.mjs b/src/hero.mjs index 859d86f..6df57ea 100644 --- a/src/hero.mjs +++ b/src/hero.mjs @@ -38,6 +38,105 @@ export const makeLogger = (writerFn = console.error) => ({ export const log = makeLogger(); +export const isValidMethod = method => + method === "GET"; + +export const isValidUpgrade = val => + val.toLowerCase() === "websocket"; + +export const isValidKey = key => + /^[0-9a-zA-Z+/]{22}==$/.test(key); + +export const isValidVersion = n => + n === 13; + +export const validateUpgrade = (method, headers) => { + const upgrade = headers["upgrade"]; + const key = headers["sec-websocket-key"]; + const versionStr = headers["sec-websocket-version"]; + const version = parseInt(versionStr); + + if (!isValidMethod(method)) { + return { + isValid: false, + response: { + status: 405, + }, + }; + } + + if (!upgrade) { + return { + isValid: false, + response: { + status: 400, + body: 'Missing "Upgrade" header\n', + }, + }; + } + + if (!isValidUpgrade(upgrade)) { + return { + isValid: false, + response: { + status: 400, + body: 'Invalid "Upgrade" value\n', + }, + }; + } + + if (!key) { + return { + isValid: false, + response: { + status: 400, + body: 'Missing "Sec-WebSocket-Key" header\n', + }, + }; + } + + if (!isValidKey(key)) { + return { + isValid: false, + response: { + status: 400, + body: 'Invalid "Sec-WebSocket-Key" value\n', + }, + }; + } + + if (!version) { + return { + isValid: false, + response: { + status: 400, + body: 'Missing "Sec-WebSocket-Version" header\n', + }, + }; + } + + if (!isValidVersion(version)) { + return { + isValid: false, + response: { + status: 400, + body: 'Invalid "Sec-WebSocket-Version" value\n', + }, + }; + } + + return { + isValid: true, + }; +}; + +const GUID_MAGIC_NUMBER = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; +export const computeHash = key => + crypto + .createHash("SHA1") + .update(key + GUID_MAGIC_NUMBER) + .digest("base64"); + export const interceptorsFn = ({ uuidFn = crypto.randomUUID, logger = log, diff --git a/tests/js/hero.mjs b/tests/js/hero.mjs index b183e9a..bbb03ec 100644 --- a/tests/js/hero.mjs +++ b/tests/js/hero.mjs @@ -1,8 +1,7 @@ import assert from "node:assert/strict"; -import child_process from "node:child_process"; import fs from "node:fs"; import http from "node:http"; -import procees from "node:process"; +import process from "node:process"; import * as runner from "../runner.mjs"; import * as u from "../../src/utils.mjs"; @@ -12,6 +11,12 @@ import { configLogger, logit, makeLogger, + isValidMethod, + isValidUpgrade, + isValidKey, + isValidVersion, + validateUpgrade, + computeHash, interceptorsFn, interceptors, defaultInterceptors, @@ -192,6 +197,162 @@ const test_makeLogger = async t => { }); }; +const test_isValidMethod = async t => { + t.start("isValidMethod()"); + + await t.test("we only accept a single value", () => { + assert.ok(isValidMethod("GET")); + assert.ok(!isValidMethod("get")); + assert.ok(!isValidMethod("PUT")); + }) +}; + +const test_isValidUpgrade = async t => { + t.start("isValidUpgrade()"); + + await t.test("we ignoee the case", () => { + assert.ok(isValidUpgrade("Websocket")); + assert.ok(isValidUpgrade("WebSocket")); + assert.ok(isValidUpgrade("websocket")); + assert.ok(!isValidUpgrade("web socket")); + }); +}; + +const test_isValidKey = async t => { + t.start("isValidKey()"); + + await t.test("RFC example value", () => { + const key = "dGhlIHNhbXBsZSBub25jZQ=="; + assert.ok(isValidKey(key)); + }); + + await t.test("wrong example values", () => { + const key1 = "_GhlIHNhbXBsZSBub25jZQ=="; + const key2 = "dGhlIHNhbXBsZSBub25jZQ="; + assert.ok(!isValidKey(key1)); + assert.ok(!isValidKey(key2)); + }); +}; + +const test_isValidVersion = async t => { + t.start("isValidVersion()"); + + await t.test("we only accept a single value", () => { + assert.ok(isValidVersion(13)); + assert.ok(!isValidVersion(9)); + assert.ok(!isValidVersion(10)); + assert.ok(!isValidVersion(11)); + assert.ok(!isValidVersion(12)); + }); +}; + +const test_validateUpgrade = async t => { + t.start("validateUpgrade()"); + + await t.test("invalid method", () => { + assert.deepEqual(validateUpgrade("POST", {}), { + isValid: false, + response: { + status: 405, + }, + }); + }); + + await t.test("missing upgrade", () => { + assert.deepEqual(validateUpgrade("GET", {}), { + isValid: false, + response: { + status: 400, + body: 'Missing "Upgrade" header\n', + }, + }); + }); + + await t.test("invalid upgrade", () => { + assert.deepEqual(validateUpgrade("GET", { + "upgrade": "web socket", + }), { + isValid: false, + response: { + status: 400, + body: 'Invalid "Upgrade" value\n', + }, + }); + }); + + await t.test("missing sec-websocket-key", () => { + assert.deepEqual(validateUpgrade("GET", { + "upgrade": "websocket", + }), { + isValid: false, + response: { + status: 400, + body: 'Missing "Sec-WebSocket-Key" header\n', + }, + }); + }); + + await t.test("invalid sec-websocket-key", () => { + assert.deepEqual(validateUpgrade("GET", { + "upgrade": "websocket", + "sec-websocket-key": "bad value", + }), { + isValid: false, + response: { + status: 400, + body: 'Invalid "Sec-WebSocket-Key" value\n', + }, + }); + }); + + await t.test("missing sec-websocket-version", () => { + assert.deepEqual(validateUpgrade("GET", { + "upgrade": "websocket", + "sec-websocket-key": "aaaaabbbbbcccccdddddee==", + }), { + isValid: false, + response: { + status: 400, + body: 'Missing "Sec-WebSocket-Version" header\n', + }, + }); + }); + + await t.test("invalid sec-websocket-version", () => { + assert.deepEqual(validateUpgrade("GET", { + "upgrade": "websocket", + "sec-websocket-key": "aaaaabbbbbcccccdddddee==", + "sec-websocket-version": "12", + }), { + isValid: false, + response: { + status: 400, + body: 'Invalid "Sec-WebSocket-Version" value\n', + }, + }); + }); + + await t.test("valid upgrade", () => { + assert.deepEqual(validateUpgrade("GET", { + "upgrade": "websocket", + "sec-websocket-key": "aaaaabbbbbcccccdddddee==", + "sec-websocket-version": "13", + }), { + isValid: true, + }); + }); +}; + +const test_computeHash = async t => { + t.start("computeHash()"); + + await t.test("RFC example value", () => { + const key = "dGhlIHNhbXBsZSBub25jZQ=="; + const hash = "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="; + assert.equal(computeHash(key), hash); + }); +}; + const test_interceptorsFn = async t => { const next = x => x; @@ -2025,6 +2186,12 @@ await runner.runTests([ test_configLogger, test_logit, test_makeLogger, + test_isValidMethod, + test_isValidUpgrade, + test_isValidKey, + test_isValidVersion, + test_validateUpgrade, + test_computeHash, test_interceptorsFn, test_chainInterceptors, test_wrapHandler, |