aiwaf-js 0.0.7 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +191 -0
  2. package/index.js +15 -2
  3. package/lib/adonisMiddleware.js +82 -0
  4. package/lib/anomalyDetector.js +18 -5
  5. package/lib/fastifyPlugin.js +49 -0
  6. package/lib/featureUtils.js +11 -1
  7. package/lib/hapiPlugin.js +92 -0
  8. package/lib/headerValidation.js +9 -0
  9. package/lib/koaMiddleware.js +68 -0
  10. package/lib/nestMiddleware.js +12 -0
  11. package/lib/nextMiddleware.js +78 -0
  12. package/lib/rateLimiter.js +3 -0
  13. package/lib/wafMiddleware.js +160 -12
  14. package/lib/wasmAdapter.js +187 -0
  15. package/package.json +34 -2
  16. package/train.js +15 -2
  17. package/.dockerignore +0 -6
  18. package/.github/workflows/node.js.yml +0 -31
  19. package/.github/workflows/npm-publish.yml +0 -27
  20. package/aiwaf.sqlite +0 -0
  21. package/examples/sandbox/README.md +0 -53
  22. package/examples/sandbox/aiwaf-proxy/Dockerfile +0 -21
  23. package/examples/sandbox/aiwaf-proxy/package.json +0 -15
  24. package/examples/sandbox/aiwaf-proxy/server.js +0 -44
  25. package/examples/sandbox/attack-suite.js +0 -293
  26. package/examples/sandbox/compare-results.js +0 -86
  27. package/examples/sandbox/docker-compose.yml +0 -27
  28. package/examples/sandbox/run-and-compare.js +0 -91
  29. package/geolock/ipinfo_lite.mmdb +0 -0
  30. package/knexfile.js +0 -9
  31. package/migrations/001_create_blocked_ips.js +0 -11
  32. package/migrations/002_create_dynamic_keywords.js +0 -11
  33. package/test/anomaly-detector.test.js +0 -36
  34. package/test/cli.test.js +0 -125
  35. package/test/csv-fallback.test.js +0 -165
  36. package/test/dynamic-keyword-integration.test.js +0 -24
  37. package/test/dynamic-keyword-store.test.js +0 -78
  38. package/test/exemptions-db.test.js +0 -38
  39. package/test/geo-mmdb.test.js +0 -77
  40. package/test/header-validation.test.js +0 -66
  41. package/test/honeypot-detector.test.js +0 -42
  42. package/test/isolation-forest.test.js +0 -38
  43. package/test/middleware-behavior.test.js +0 -75
  44. package/test/model-store-db.test.js +0 -22
  45. package/test/model-store.test.js +0 -31
  46. package/test/redis-client.test.js +0 -35
  47. package/test/settingsCompat.test.js +0 -95
  48. package/test/train.test.js +0 -137
  49. package/test/uuid-detector.test.js +0 -20
  50. package/test/waf.test.js +0 -327
  51. package/test-anomaly.js +0 -77
  52. package/test-complete-waf.js +0 -147
  53. package/test-simple.js +0 -79
package/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # aiwaf-js
2
2
 
3
3
  AIWAF-JS is a Node.js/Express Web Application Firewall that combines deterministic protections with anomaly detection and continuous learning. It ships as middleware, a CLI for ops workflows, and an offline trainer for IsolationForest models.
4
+ Supported frameworks: Express (native), Fastify, Hapi, Koa, NestJS (Express/Fastify wrappers), Next.js (API route wrapper), and AdonisJS.
4
5
 
5
6
  ## What It Does
6
7
 
@@ -62,6 +63,15 @@ AIWAF-JS is a Node.js/Express Web Application Firewall that combines determinist
62
63
  npm install aiwaf-js
63
64
  ```
64
65
 
66
+ ### Optional WASM Acceleration
67
+
68
+ AIWAF can use the `aiwaf-wasm` optional dependency for faster IsolationForest scoring and deterministic feature validation.
69
+ If the WASM module fails to load, it automatically falls back to the JS implementation.
70
+
71
+ ```bash
72
+ npm install aiwaf-wasm
73
+ ```
74
+
65
75
  ## Quick Start
66
76
 
67
77
  ```js
@@ -88,6 +98,184 @@ app.get('/', (req, res) => res.send('Protected'));
88
98
  app.listen(3000);
89
99
  ```
90
100
 
101
+ ## Fastify Usage
102
+
103
+ ```js
104
+ const fastify = require('fastify')({ logger: true });
105
+ const aiwaf = require('aiwaf-js');
106
+
107
+ fastify.register(aiwaf.fastify, {
108
+ staticKeywords: ['.php', '.env', '.git'],
109
+ dynamicTopN: 10,
110
+ WINDOW_SEC: 10,
111
+ MAX_REQ: 20,
112
+ FLOOD_REQ: 40,
113
+ HONEYPOT_FIELD: 'hp_field'
114
+ });
115
+
116
+ fastify.get('/', async () => 'Protected');
117
+ fastify.listen({ port: 3000 });
118
+ ```
119
+
120
+ ## Hapi Usage
121
+
122
+ ```js
123
+ const Hapi = require('@hapi/hapi');
124
+ const aiwaf = require('aiwaf-js');
125
+
126
+ const server = Hapi.server({ port: 3000 });
127
+ await server.register({
128
+ plugin: aiwaf.hapi,
129
+ options: {
130
+ staticKeywords: ['.php', '.env', '.git'],
131
+ dynamicTopN: 10,
132
+ WINDOW_SEC: 10,
133
+ MAX_REQ: 20,
134
+ FLOOD_REQ: 40,
135
+ HONEYPOT_FIELD: 'hp_field'
136
+ }
137
+ });
138
+
139
+ server.route({ method: 'GET', path: '/', handler: () => 'Protected' });
140
+ await server.start();
141
+ ```
142
+
143
+ ## Koa Usage
144
+
145
+ ```js
146
+ const Koa = require('koa');
147
+ const bodyParser = require('koa-bodyparser');
148
+ const aiwaf = require('aiwaf-js');
149
+
150
+ const app = new Koa();
151
+ app.use(bodyParser());
152
+
153
+ app.use(aiwaf.koa({
154
+ staticKeywords: ['.php', '.env', '.git'],
155
+ dynamicTopN: 10,
156
+ WINDOW_SEC: 10,
157
+ MAX_REQ: 20,
158
+ FLOOD_REQ: 40,
159
+ HONEYPOT_FIELD: 'hp_field'
160
+ }));
161
+
162
+ app.use(ctx => {
163
+ ctx.body = 'Protected';
164
+ });
165
+
166
+ app.listen(3000);
167
+ ```
168
+
169
+ ## NestJS (Express) Usage
170
+
171
+ ```ts
172
+ import { Module, MiddlewareConsumer, NestModule } from '@nestjs/common';
173
+ import aiwaf from 'aiwaf-js';
174
+
175
+ @Module({})
176
+ export class AppModule implements NestModule {
177
+ configure(consumer: MiddlewareConsumer) {
178
+ consumer
179
+ .apply(aiwaf.nest({
180
+ staticKeywords: ['.php', '.env', '.git'],
181
+ dynamicTopN: 10,
182
+ WINDOW_SEC: 10,
183
+ MAX_REQ: 20,
184
+ FLOOD_REQ: 40,
185
+ HONEYPOT_FIELD: 'hp_field'
186
+ }))
187
+ .forRoutes('*');
188
+ }
189
+ }
190
+ ```
191
+
192
+ If you need to guarantee ordering before other middleware/proxies, you can also attach the Express middleware directly in `main.ts`:
193
+
194
+ ```ts
195
+ import { NestFactory } from '@nestjs/core';
196
+ import aiwaf from 'aiwaf-js';
197
+ import { AppModule } from './app.module';
198
+
199
+ async function bootstrap() {
200
+ const app = await NestFactory.create(AppModule);
201
+ app.use(aiwaf({
202
+ staticKeywords: ['.php', '.env', '.git'],
203
+ dynamicTopN: 10,
204
+ WINDOW_SEC: 10,
205
+ MAX_REQ: 20,
206
+ FLOOD_REQ: 40,
207
+ HONEYPOT_FIELD: 'hp_field'
208
+ }));
209
+ await app.listen(3000);
210
+ }
211
+ bootstrap();
212
+ ```
213
+
214
+ ## NestJS (Fastify) Usage
215
+
216
+ Use the Fastify plugin when running Nest with `FastifyAdapter`:
217
+
218
+ ```ts
219
+ import { NestFactory } from '@nestjs/core';
220
+ import { FastifyAdapter } from '@nestjs/platform-fastify';
221
+ import aiwaf from 'aiwaf-js';
222
+ import { AppModule } from './app.module';
223
+
224
+ async function bootstrap() {
225
+ const app = await NestFactory.create(AppModule, new FastifyAdapter());
226
+ await app.register(aiwaf.fastify, {
227
+ staticKeywords: ['.php', '.env', '.git'],
228
+ dynamicTopN: 10,
229
+ WINDOW_SEC: 10,
230
+ MAX_REQ: 20,
231
+ FLOOD_REQ: 40,
232
+ HONEYPOT_FIELD: 'hp_field'
233
+ });
234
+ await app.listen(3000, '0.0.0.0');
235
+ }
236
+ bootstrap();
237
+ ```
238
+
239
+ ## Next.js (API Routes) Usage
240
+
241
+ Use the `aiwaf.next` helper to wrap a Next.js API route handler.
242
+
243
+ ```ts
244
+ import aiwaf from 'aiwaf-js';
245
+
246
+ function handler(req, res) {
247
+ res.status(200).json({ ok: true });
248
+ }
249
+
250
+ export default aiwaf.next(handler, {
251
+ staticKeywords: ['.php', '.env', '.git'],
252
+ dynamicTopN: 10,
253
+ WINDOW_SEC: 10,
254
+ MAX_REQ: 20,
255
+ FLOOD_REQ: 40,
256
+ HONEYPOT_FIELD: 'hp_field'
257
+ });
258
+ ```
259
+
260
+ ## AdonisJS Usage
261
+
262
+ Register the middleware in your Adonis middleware stack:
263
+
264
+ ```ts
265
+ import aiwaf from 'aiwaf-js';
266
+
267
+ export const middleware = [
268
+ () => aiwaf.adonis({
269
+ staticKeywords: ['.php', '.env', '.git'],
270
+ dynamicTopN: 10,
271
+ WINDOW_SEC: 10,
272
+ MAX_REQ: 20,
273
+ FLOOD_REQ: 40,
274
+ HONEYPOT_FIELD: 'hp_field'
275
+ })
276
+ ];
277
+ ```
278
+
91
279
  ## Configuration
92
280
 
93
281
  ### Core Controls
@@ -105,6 +293,8 @@ app.listen(3000);
105
293
  | `cache` | fallback memory cache | Custom cache backend used by limiter/features |
106
294
  | `nTrees` | `100` | IsolationForest trees when model is initialized in-process |
107
295
  | `sampleSize` | `256` | IsolationForest sample size |
296
+ | `AIWAF_WASM_VALIDATION` | `true` | Enable WASM validation when available (headers, URL, content, recent) |
297
+ | `AIWAF_WASM_VALIDATE_RECENT` | `false` | Run WASM recent-behavior validation on recent request logs |
108
298
 
109
299
  ### Header Validation
110
300
 
@@ -318,6 +508,7 @@ node examples/sandbox/run-and-compare.js http://localhost:3001 http://localhost:
318
508
  ```
319
509
 
320
510
  The comparison output includes per‑attack block rates and total blocked requests.
511
+ Fastify proxy is also available on `http://localhost:3002`.
321
512
 
322
513
  ## License
323
514
 
package/index.js CHANGED
@@ -1,3 +1,16 @@
1
1
  // aiwaf‑js/index.js
2
- // Export the middleware factory from lib/wafMiddleware.js
3
- module.exports = require('./lib/wafMiddleware'); // :contentReference[oaicite:0]{index=0}​:contentReference[oaicite:1]{index=1}
2
+ const createExpressMiddleware = require('./lib/wafMiddleware');
3
+ const createFastifyPlugin = require('./lib/fastifyPlugin');
4
+ const createHapiPlugin = require('./lib/hapiPlugin');
5
+ const createKoaMiddleware = require('./lib/koaMiddleware');
6
+ const createNestMiddleware = require('./lib/nestMiddleware');
7
+ const createNextHandler = require('./lib/nextMiddleware');
8
+ const createAdonisMiddleware = require('./lib/adonisMiddleware');
9
+
10
+ module.exports = createExpressMiddleware;
11
+ module.exports.fastify = createFastifyPlugin;
12
+ module.exports.hapi = createHapiPlugin;
13
+ module.exports.koa = createKoaMiddleware;
14
+ module.exports.nest = createNestMiddleware;
15
+ module.exports.next = createNextHandler;
16
+ module.exports.adonis = createAdonisMiddleware;
@@ -0,0 +1,82 @@
1
+ const createExpressMiddleware = require('./wafMiddleware');
2
+
3
+ function createExpressLikeResponse(ctx) {
4
+ const rawRes = ctx.response?.response;
5
+ const res = {
6
+ locals: {},
7
+ on: (...args) => rawRes?.on?.(...args),
8
+ get statusCode() {
9
+ return rawRes?.statusCode ?? ctx.response?.statusCode ?? 200;
10
+ },
11
+ set statusCode(code) {
12
+ if (rawRes) rawRes.statusCode = code;
13
+ if (ctx.response) ctx.response.statusCode = code;
14
+ },
15
+ status(code) {
16
+ if (ctx.response?.status) {
17
+ ctx.response.status(code);
18
+ } else {
19
+ res.statusCode = code;
20
+ }
21
+ res._handled = true;
22
+ return res;
23
+ },
24
+ json(payload) {
25
+ if (ctx.response?.json) {
26
+ ctx.response.json(payload);
27
+ } else if (ctx.response?.send) {
28
+ ctx.response.send(payload);
29
+ }
30
+ res._handled = true;
31
+ return res;
32
+ },
33
+ send(payload) {
34
+ if (ctx.response?.send) {
35
+ ctx.response.send(payload);
36
+ } else if (rawRes?.end) {
37
+ rawRes.end(payload);
38
+ }
39
+ res._handled = true;
40
+ return res;
41
+ }
42
+ };
43
+
44
+ return res;
45
+ }
46
+
47
+ module.exports = function createAdonisMiddleware(opts = {}) {
48
+ const middleware = createExpressMiddleware(opts);
49
+
50
+ return async (ctx, next) => {
51
+ const req = ctx.request?.request || ctx.request || {};
52
+ const res = createExpressLikeResponse(ctx);
53
+
54
+ // Only set path/url/ip if not already available
55
+ if (!req.path) req.path = ctx.request?.url?.() || ctx.request?.url || req.url;
56
+ if (!req.url) req.url = req.path;
57
+ if (!req.ip) req.ip = ctx.request?.ip?.() || ctx.request?.ip;
58
+
59
+ // Don't override headers - the raw request already has them from the HTTP server
60
+
61
+ await new Promise(resolve => {
62
+ let resolved = false;
63
+ const finish = () => {
64
+ if (resolved) return;
65
+ resolved = true;
66
+ resolve();
67
+ };
68
+ const maybePromise = middleware(req, res, finish);
69
+ if (res._handled) {
70
+ finish();
71
+ return;
72
+ }
73
+ Promise.resolve(maybePromise).then(finish).catch(finish);
74
+ });
75
+
76
+ if (res._handled || ctx.response?.response?.writableEnded || ctx.response?.response?.headersSent) {
77
+ return;
78
+ }
79
+
80
+ await next();
81
+ };
82
+ };
@@ -1,4 +1,5 @@
1
1
  const { IsolationForest } = require('./isolationForest');
2
+ const { createIsolationForest, getWasmStatus } = require('./wasmAdapter');
2
3
  const modelStore = require('./modelStore');
3
4
  const requestLogStore = require('./requestLogStore');
4
5
  const { STATIC_KW } = require('./featureUtils');
@@ -12,6 +13,7 @@ let loadStarted = false;
12
13
  let minAiLogs = 0;
13
14
  let aiLogsSufficient = true;
14
15
  let aiLogCount = null;
16
+ let wasmStatus = { loaded: false, error: null };
15
17
 
16
18
  async function loadModel(opts = {}) {
17
19
  if (loadStarted) return;
@@ -150,20 +152,25 @@ module.exports = {
150
152
  async init(opts = {}) {
151
153
  minAiLogs = Number.isFinite(Number(opts.AIWAF_MIN_AI_LOGS))
152
154
  ? Number(opts.AIWAF_MIN_AI_LOGS)
153
- : 0;
155
+ : 10000; // Default: require 10k training samples
154
156
  await checkAiLogSufficiency();
155
157
  await loadModel(opts);
156
158
  if (!model) {
157
- model = new IsolationForest({ nTrees: opts.nTrees || 100, sampleSize: opts.sampleSize || 256 });
159
+ model = await createIsolationForest({
160
+ nTrees: opts.nTrees || 100,
161
+ sampleSize: opts.sampleSize || 256,
162
+ threshold: opts.threshold || 0.5
163
+ });
164
+ wasmStatus = getWasmStatus();
158
165
  }
159
166
 
160
167
  if (model && !aiLogsSufficient) {
161
168
  model = null;
162
169
  trained = false;
163
170
  if (aiLogCount !== null) {
164
- console.log(`AIWAF AI model disabled due to insufficient logs (${aiLogCount}/${minAiLogs}).`);
171
+ console.log(`AIWAF AI model disabled due to insufficient logs (${aiLogCount}/${minAiLogs}). Require at least ${minAiLogs} logs before using AI detection.`);
165
172
  } else {
166
- console.log(`AIWAF AI model disabled due to insufficient logs (unknown/${minAiLogs}).`);
173
+ console.log(`AIWAF AI model disabled due to insufficient logs (unknown/${minAiLogs}). Require at least ${minAiLogs} logs before using AI detection.`);
167
174
  }
168
175
  }
169
176
  },
@@ -177,6 +184,11 @@ module.exports = {
177
184
  return !!model && trained;
178
185
  },
179
186
 
187
+ isModelSufficientlyTrained() {
188
+ // Model must exist AND have enough training data
189
+ return !!model && trained && aiLogsSufficient;
190
+ },
191
+
180
192
  // Expects a feature vector: [pathLen, kwHits, statusIdx, responseTime, burst, total404]
181
193
  isAnomalous(features, threshold = 0.5) {
182
194
  if (!trained || !model) {
@@ -222,7 +234,8 @@ module.exports = {
222
234
  threshold: 0.5,
223
235
  minAiLogs,
224
236
  aiLogsSufficient,
225
- aiLogCount
237
+ aiLogCount,
238
+ wasm: wasmStatus
226
239
  };
227
240
  }
228
241
  };
@@ -0,0 +1,49 @@
1
+ const createExpressMiddleware = require('./wafMiddleware');
2
+
3
+ function createExpressLikeResponse(reply) {
4
+ const raw = reply.raw;
5
+ const res = {
6
+ locals: {},
7
+ on: (...args) => raw.on(...args),
8
+ get statusCode() {
9
+ return raw.statusCode;
10
+ },
11
+ set statusCode(code) {
12
+ raw.statusCode = code;
13
+ },
14
+ status(code) {
15
+ reply.code(code);
16
+ return res;
17
+ },
18
+ json(payload) {
19
+ reply.type('application/json').send(payload);
20
+ return res;
21
+ },
22
+ send(payload) {
23
+ reply.send(payload);
24
+ return res;
25
+ }
26
+ };
27
+ return res;
28
+ }
29
+
30
+ function fastifyPlugin(fastify, opts = {}, done) {
31
+ const middleware = createExpressMiddleware(opts);
32
+
33
+ fastify.addHook('onRequest', async (request, reply) => {
34
+ await new Promise(resolve => {
35
+ const res = createExpressLikeResponse(reply);
36
+ middleware(request.raw, res, resolve);
37
+ });
38
+
39
+ if (reply.sent) {
40
+ return reply;
41
+ }
42
+ });
43
+
44
+ done();
45
+ }
46
+
47
+ fastifyPlugin[Symbol.for('skip-override')] = true;
48
+
49
+ module.exports = fastifyPlugin;
@@ -33,6 +33,15 @@ function init(opts = {}) {
33
33
  }
34
34
  }
35
35
 
36
+ function cleanup() {
37
+ if (cleanupInterval) {
38
+ clearInterval(cleanupInterval);
39
+ cleanupInterval = null;
40
+ }
41
+ ipRequestHistory.clear();
42
+ ip404Counts.clear();
43
+ }
44
+
36
45
  function recordRequest(ip, statusCode) {
37
46
  const now = Date.now();
38
47
 
@@ -83,7 +92,7 @@ function getResponseTime(req) {
83
92
 
84
93
  async function extractFeatures(req, res = null) {
85
94
  const uri = req.path || req.url;
86
- const ip = req.ip || req.connection?.remoteAddress || req.socket?.remoteAddress || 'unknown';
95
+ const ip = req.ip || req.socket?.remoteAddress || req.connection?.remoteAddress || 'unknown';
87
96
 
88
97
  // Get status code from response if available, otherwise default to 200
89
98
  let statusCode = 200;
@@ -168,6 +177,7 @@ module.exports = {
168
177
  get404Count,
169
178
  markRequestStart,
170
179
  getResponseTime,
180
+ cleanup,
171
181
  STATIC_KW,
172
182
  STATUS_IDX
173
183
  };
@@ -0,0 +1,92 @@
1
+ const createExpressMiddleware = require('./wafMiddleware');
2
+
3
+ function createExpressLikeResponse(h, rawRes) {
4
+ const res = {
5
+ locals: {},
6
+ on: (...args) => rawRes.on(...args),
7
+ get statusCode() {
8
+ return rawRes.statusCode;
9
+ },
10
+ set statusCode(code) {
11
+ rawRes.statusCode = code;
12
+ },
13
+ status(code) {
14
+ res._statusCode = code;
15
+ res._handled = true;
16
+ return res;
17
+ },
18
+ json(payload) {
19
+ res._payload = payload;
20
+ res._contentType = 'application/json';
21
+ res._handled = true;
22
+ return res;
23
+ },
24
+ send(payload) {
25
+ res._payload = payload;
26
+ res._handled = true;
27
+ return res;
28
+ }
29
+ };
30
+
31
+ res.toResponse = () => {
32
+ const response = h.response(res._payload);
33
+ if (res._contentType) {
34
+ response.type(res._contentType);
35
+ }
36
+ if (res._statusCode) {
37
+ response.code(res._statusCode);
38
+ }
39
+ return response;
40
+ };
41
+
42
+ return res;
43
+ }
44
+
45
+ module.exports = {
46
+ name: 'aiwaf',
47
+ version: '1.0.0',
48
+ register: async (server, opts = {}) => {
49
+ const middleware = createExpressMiddleware(opts);
50
+
51
+ server.ext('onRequest', async (request, h) => {
52
+ const res = createExpressLikeResponse(h, request.raw.res);
53
+ const req = request.raw.req;
54
+
55
+ // Set path and URL from Hapi's request object
56
+ req.path = request.path || req.path;
57
+ req.url = request.url?.pathname || req.url;
58
+
59
+ // For headers: prefer the framework's parsed headers, but merge them
60
+ // Don't try to replace req.headers entirely as it's read-only on raw requests
61
+ if (request.headers) {
62
+ // Copy headers to the req.headers object (which is read-only but properties can be added)
63
+ try {
64
+ Object.keys(request.headers).forEach(key => {
65
+ if (!req.headers[key]) {
66
+ req.headers[key] = request.headers[key];
67
+ }
68
+ });
69
+ } catch (err) {
70
+ // If we can't modify headers, continue anyway
71
+ }
72
+ }
73
+
74
+ req.ip = request.info?.remoteAddress || req.ip;
75
+
76
+ return new Promise(resolve => {
77
+ let resolved = false;
78
+ const finish = () => {
79
+ if (resolved) return;
80
+ resolved = true;
81
+ if (res._payload !== undefined || res._statusCode) {
82
+ resolve(res.toResponse().takeover());
83
+ } else {
84
+ resolve(h.continue);
85
+ }
86
+ };
87
+ const maybePromise = middleware(req, res, finish);
88
+ Promise.resolve(maybePromise).then(finish).catch(finish);
89
+ });
90
+ });
91
+ }
92
+ };
@@ -203,6 +203,15 @@ module.exports = {
203
203
  );
204
204
  headers['server-protocol'] = req.httpVersion ? `HTTP/${req.httpVersion}` : headers['server-protocol'];
205
205
 
206
+ if (process.env.AIWAF_DEBUG_HEADERS) {
207
+ console.error(`[HEADER-VALIDATION] headers keys: ${Object.keys(headers).join(', ')}`);
208
+ console.error(`[HEADER-VALIDATION] user-agent: ${headers['user-agent']}`);
209
+ console.error(`[HEADER-VALIDATION] accept: ${headers['accept']}`);
210
+ console.error(`[HEADER-VALIDATION] accept-language: ${headers['accept-language']}`);
211
+ console.error(`[HEADER-VALIDATION] accept-encoding: ${headers['accept-encoding']}`);
212
+ console.error(`[HEADER-VALIDATION] connection: ${headers['connection']}`);
213
+ }
214
+
206
215
  if (isStaticRequest(req.path || req.url || '')) return null;
207
216
 
208
217
  const capReason = enforceHeaderCaps(headers);
@@ -0,0 +1,68 @@
1
+ const createExpressMiddleware = require('./wafMiddleware');
2
+
3
+ function createExpressLikeResponse(ctx) {
4
+ const res = {
5
+ locals: {},
6
+ on: (...args) => ctx.res.on(...args),
7
+ get statusCode() {
8
+ return ctx.status;
9
+ },
10
+ set statusCode(code) {
11
+ ctx.status = code;
12
+ },
13
+ status(code) {
14
+ ctx.status = code;
15
+ res._handled = true;
16
+ return res;
17
+ },
18
+ json(payload) {
19
+ ctx.type = 'application/json';
20
+ ctx.body = payload;
21
+ res._handled = true;
22
+ return res;
23
+ },
24
+ send(payload) {
25
+ ctx.body = payload;
26
+ res._handled = true;
27
+ return res;
28
+ }
29
+ };
30
+ return res;
31
+ }
32
+
33
+ module.exports = function createKoaMiddleware(opts = {}) {
34
+ const middleware = createExpressMiddleware(opts);
35
+
36
+ return async (ctx, next) => {
37
+ const res = createExpressLikeResponse(ctx);
38
+ const req = ctx.req; // Raw Node.js http.IncomingMessage - already has headers
39
+
40
+ // Only set these if not already available
41
+ if (!req.path) req.path = ctx.path;
42
+ if (!req.url) req.url = ctx.url;
43
+ if (!req.ip) req.ip = ctx.ip;
44
+
45
+ // Don't override headers - use what's already on the raw request
46
+ // ctx.headers is Koa's parsed version, but req.headers from Node.js has the originals
47
+
48
+ await new Promise(resolve => {
49
+ let resolved = false;
50
+ const finish = () => {
51
+ if (resolved) return;
52
+ resolved = true;
53
+ resolve();
54
+ };
55
+ const maybePromise = middleware(req, res, finish);
56
+ if (res._handled) {
57
+ finish();
58
+ }
59
+ Promise.resolve(maybePromise).then(finish).catch(finish);
60
+ });
61
+
62
+ if (ctx.body !== undefined) {
63
+ return;
64
+ }
65
+
66
+ await next();
67
+ };
68
+ };
@@ -0,0 +1,12 @@
1
+ const createExpressMiddleware = require('./wafMiddleware');
2
+
3
+ module.exports = function createNestMiddleware(opts = {}) {
4
+ const middleware = createExpressMiddleware(opts);
5
+
6
+ return class AIWAFNestMiddleware {
7
+ use(req, res, next) {
8
+ // Express middleware - req/res already have headers set by Express itself
9
+ return middleware(req, res, next);
10
+ }
11
+ };
12
+ };