Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add WebSocket support (ws) #270

Open
wants to merge 27 commits into
base: beta
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
c7d2ccc
WIP: Start wrapping ws
timokoessler Jul 5, 2024
0e29774
Fix ws event context, parse message event
timokoessler Jul 5, 2024
bfded04
Decode binary web socket messages
timokoessler Jul 5, 2024
d5c119c
Check data of ping and pong events
timokoessler Jul 11, 2024
7f85dd1
Wrap other ws listener methods
timokoessler Jul 11, 2024
afff1f2
Wrap ws.onevent functions
timokoessler Jul 11, 2024
972901f
fix: Node.js v16 does not support Blob
timokoessler Jul 11, 2024
5fd0431
Parse ws close reason
timokoessler Jul 11, 2024
8a43de0
Add sample ws chat app
timokoessler Jul 12, 2024
5fb7fce
Add ws to source, use AsyncResource
timokoessler Jul 12, 2024
141033a
Add ws e2e tests
timokoessler Jul 12, 2024
3d09625
Add supported package version
timokoessler Jul 12, 2024
2993202
Add AIKIDO_MAX_WS_MSG_SIZE_MB
timokoessler Jul 12, 2024
20cd235
Implement rate limiting and user blocking (ws)
timokoessler Jul 12, 2024
46e0bc2
Merge branch 'main' into ws
timokoessler Jul 12, 2024
37790ad
Remove console.log
timokoessler Jul 12, 2024
9def302
Fix AIKIDO_MAX_WS_MSG_SIZE_MB test
timokoessler Jul 12, 2024
f349c20
Improve test coverage, update readme
timokoessler Jul 15, 2024
6a92c2b
Wrap handleUpgrade instead of events
timokoessler Jul 15, 2024
07ef845
Refactor and comment code
timokoessler Jul 15, 2024
905e6e9
Apply requested changes
timokoessler Jul 15, 2024
d88dcf4
Refactor websocket data parsing
timokoessler Jul 16, 2024
2a2e252
Fix linting
timokoessler Jul 16, 2024
a533e10
Add test for ws data parsing
timokoessler Jul 16, 2024
43aea41
Rename checkWsDataSize to isMessageDataTooLarge
timokoessler Jul 16, 2024
7816dd8
Merge branch 'beta' into ws
timokoessler Nov 25, 2024
0236881
Fix branch after merging beta
timokoessler Nov 25, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ hono-sqlite3:
hapi-postgres:
cd sample-apps/hapi-postgres && AIKIDO_DEBUG=true AIKIDO_BLOCKING=true node app.js


.PHONY: ws-postgres
ws-postgres:
cd sample-apps/ws-postgres && AIKIDO_DEBUG=true AIKIDO_BLOCKING=true node app.js

.PHONY: install
install:
mkdir -p build && cp library/package.json build/package.json
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ Aikido Firewall for Node.js 16+ is compatible with:
* ✅ Google Cloud Functions
* ✅ AWS Lambda

### Real time communication

* ✅ [`ws`](https://www.npmjs.com/package/ws) 8.x, 7.x

### ORMs and query builders

See list above for supported database drivers.
Expand Down
3 changes: 2 additions & 1 deletion end2end/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
"private": true,
"dependencies": {
"@supercharge/promise-pool": "^3.1.1",
"tap": "^18.7.0"
"tap": "^18.7.0",
"ws": "^8.18.0"
},
"scripts": {
"test": "tap tests/*.js --allow-empty-coverage -j 1"
Expand Down
181 changes: 181 additions & 0 deletions end2end/tests/ws-postgres.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
const t = require("tap");
const { spawn } = require("child_process");
const { resolve } = require("path");
const timeout = require("../timeout");
const { WebSocket } = require("ws");

const pathToApp = resolve(__dirname, "../../sample-apps/ws-postgres", "app.js");

t.test("it blocks in blocking mode", (t) => {
const server = spawn(`node`, [pathToApp, "4000"], {
env: { ...process.env, AIKIDO_DEBUG: "true", AIKIDO_BLOCKING: "true" },
});

server.on("close", () => {
t.end();
});

server.on("error", (err) => {
t.fail(err.message);
});

let stdout = "";
server.stdout.on("data", (data) => {
stdout += data.toString();
});

let stderr = "";
server.stderr.on("data", (data) => {
stderr += data.toString();
});

// Wait for the server to start
timeout(2000)
.then(async () => {
// Does not block normal messages
return await new Promise((resolve, reject) => {
const ws = new WebSocket(`ws://localhost:4000`);

ws.on("error", (err) => {
reject(err);
});

ws.on("open", () => {
ws.send("Hello world!");
});

ws.on("message", (data) => {
const str = data.toString();
if (str.includes("Welcome")) {
return;
}
t.match(str, /Hello world!/);
ws.close();
resolve();
});
});
})
.then(async () => {
// Does block sql injection
return await new Promise((resolve, reject) => {
const ws = new WebSocket(`ws://localhost:4000`);

ws.on("error", (err) => {
reject(err);
});

ws.on("open", () => {
ws.send("Bye'); DELETE FROM messages;--");
});

ws.on("message", (data) => {
const str = data.toString();
if (
str.includes("Welcome") ||
str.includes("Hello") ||
str.includes("Bye")
) {
return;
}
t.match(str, /An error occurred/);
ws.close();
resolve();
});
});
})
.then(() => {
t.match(stdout, /Starting agent/);
t.match(stderr, /Aikido firewall has blocked an SQL injection/);
})
.catch((error) => {
t.fail(error.message);
})
.finally(() => {
server.kill();
});
});

t.test("it does not block in non-blocking mode", (t) => {
const server = spawn(`node`, [pathToApp, "4001"], {
env: { ...process.env, AIKIDO_DEBUG: "true" },
});

server.on("close", () => {
t.end();
});

server.on("error", (err) => {
t.fail(err.message);
});

let stdout = "";
server.stdout.on("data", (data) => {
stdout += data.toString();
});

let stderr = "";
server.stderr.on("data", (data) => {
stderr += data.toString();
});

// Wait for the server to start
timeout(2000)
.then(async () => {
// Does not block normal messages
return await new Promise((resolve, reject) => {
const ws = new WebSocket(`ws://localhost:4001`);

ws.on("error", (err) => {
reject(err);
});

ws.on("open", () => {
ws.send("Hello world!");
});

ws.on("message", (data) => {
const str = data.toString();
if (str.includes("Welcome")) {
return;
}
t.match(str, /Hello world!/);
ws.close();
resolve();
});
});
})
.then(async () => {
// Does block sql injection
return await new Promise((resolve, reject) => {
const ws = new WebSocket(`ws://localhost:4001`);

ws.on("error", (err) => {
reject(err);
});

ws.on("open", () => {
ws.send("Bye'); DELETE FROM messages;--");
});

ws.on("message", (data) => {
const str = data.toString();
if (str.includes("Welcome") || str.includes("Hello")) {
return;
}
t.match(str, /Bye/);
ws.close();
resolve();
});
});
})
.then(() => {
t.match(stdout, /Starting agent/);
t.notMatch(stderr, /Aikido firewall has blocked an SQL injection/);
})
.catch((error) => {
t.fail(error.message);
})
.finally(() => {
server.kill();
});
});
2 changes: 2 additions & 0 deletions library/agent/Context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export type Context = {
graphql?: string[];
xml?: unknown;
subdomains?: string[]; // https://expressjs.com/en/5x/api.html#req.subdomains
ws?: unknown; // Additional data related to WebSocket connections, like the last message received
};

/**
Expand Down Expand Up @@ -56,6 +57,7 @@ export function runWithContext<T>(context: Context, fn: () => T) {
current.graphql = context.graphql;
current.xml = context.xml;
current.subdomains = context.subdomains;
current.ws = context.ws;

return fn();
}
Expand Down
1 change: 1 addition & 0 deletions library/agent/Source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const SOURCES = [
"graphql",
"xml",
"subdomains",
"ws",
] as const;

export type Source = (typeof SOURCES)[number];
2 changes: 2 additions & 0 deletions library/agent/protect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import { XmlMinusJs } from "../sources/XmlMinusJs";
import { Hapi } from "../sources/Hapi";
import { Shelljs } from "../sinks/Shelljs";
import { Ws } from "../sources/Ws";

function isDebugging() {
return (
Expand Down Expand Up @@ -135,10 +136,11 @@
new XmlMinusJs(),
new Shelljs(),
new Hapi(),
new Ws(),
];
}

export function protect() {

Check warning on line 143 in library/agent/protect.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

exported declaration 'protect' not used within other modules
const agent = getAgent({
serverless: undefined,
});
Expand Down
2 changes: 2 additions & 0 deletions library/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"@types/shell-quote": "^1.7.5",
"@types/sinonjs__fake-timers": "^8.1.5",
"@types/supertest": "^6.0.2",
"@types/ws": "^8.5.10",
"@typescript-eslint/eslint-plugin": "^6.19.0",
"@typescript-eslint/parser": "^6.19.0",
"aws-sdk": "^2.1595.0",
Expand All @@ -72,6 +73,7 @@
"tap": "^18.6.1",
"typescript": "^5.3.3",
"undici": "^6.12.0",
"ws": "^8.18.0",
"xml-js": "^1.6.11",
"xml2js": "^0.6.2"
},
Expand Down
11 changes: 7 additions & 4 deletions library/sources/HTTPServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { Agent } from "../agent/Agent";
import { Hooks } from "../agent/hooks/Hooks";
import { Wrapper } from "../agent/Wrapper";
import { isPackageInstalled } from "../helpers/isPackageInstalled";
import { isPlainObject } from "../helpers/isPlainObject";
import { createRequestListener } from "./http-server/createRequestListener";
import { createUpgradeListener } from "./http-server/createUpgradeListener";

export class HTTPServer implements Wrapper {
private wrapRequestListener(args: unknown[], module: string, agent: Agent) {
Expand Down Expand Up @@ -40,10 +40,13 @@ export class HTTPServer implements Wrapper {
) {
return args;
}
if (args[0] !== "request") {
return args;
if (args[0] === "request") {
return this.wrapRequestListener(args, module, agent);
}
if (args[0] === "upgrade") {
return [args[0], createUpgradeListener(args[1], module, agent)];
}
return this.wrapRequestListener(args, module, agent);
return args;
}

wrap(hooks: Hooks) {
Expand Down
Loading
Loading