-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
14 changed files
with
480 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
const t = require("tap"); | ||
const { spawn } = require("child_process"); | ||
const { resolve } = require("path"); | ||
const timeout = require("../timeout"); | ||
|
||
const pathToApp = resolve( | ||
__dirname, | ||
"../../sample-apps/hono-mongodb", | ||
"app.js" | ||
); | ||
|
||
t.setTimeout(60000); | ||
|
||
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(() => { | ||
return Promise.all([ | ||
fetch("http://localhost:4000/?search[$ne]=null", { | ||
signal: AbortSignal.timeout(5000), | ||
}), | ||
fetch("http://localhost:4000/?search=title", { | ||
signal: AbortSignal.timeout(5000), | ||
}), | ||
]); | ||
}) | ||
.then(([noSQLInjection, normalSearch]) => { | ||
t.equal(noSQLInjection.status, 500); | ||
t.equal(normalSearch.status, 200); | ||
t.match(stdout, /Starting agent/); | ||
t.match(stderr, /Aikido runtime has blocked a NoSQL injection/); | ||
}) | ||
.catch((error) => { | ||
t.fail(error.message); | ||
}) | ||
.finally(() => { | ||
server.kill(); | ||
}); | ||
}); | ||
|
||
t.test("it does not block in dry mode", (t) => { | ||
const server = spawn(`node`, [pathToApp, "4001"], { | ||
env: { ...process.env, AIKIDO_DEBUG: "true" }, | ||
}); | ||
|
||
server.on("close", () => { | ||
t.end(); | ||
}); | ||
|
||
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(() => | ||
Promise.all([ | ||
fetch("http://localhost:4001/?search[$ne]=null", { | ||
signal: AbortSignal.timeout(5000), | ||
}), | ||
fetch("http://localhost:4001/?search=title", { | ||
signal: AbortSignal.timeout(5000), | ||
}), | ||
]) | ||
) | ||
.then(([noSQLInjection, normalSearch]) => { | ||
t.equal(noSQLInjection.status, 200); | ||
t.equal(normalSearch.status, 200); | ||
t.match(stdout, /Starting agent/); | ||
t.notMatch(stderr, /Aikido runtime has blocked a NoSQL injection/); | ||
}) | ||
.catch((error) => { | ||
t.fail(error.message); | ||
}) | ||
.finally(() => { | ||
server.kill(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
8 changes: 4 additions & 4 deletions
8
...sources/express/shouldRateLimitRequest.ts → ...ry/ratelimiting/shouldRateLimitRequest.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
/* eslint-disable prefer-rest-params */ | ||
import type { MiddlewareHandler } from "hono"; | ||
import { Agent } from "../agent/Agent"; | ||
import { Hooks } from "../agent/hooks/Hooks"; | ||
import { Wrapper } from "../agent/Wrapper"; | ||
import { wrapRequestHandler } from "./hono/wrapRequestHandler"; | ||
|
||
export class Hono implements Wrapper { | ||
// Wrap all the functions passed to hono.METHOD(...) | ||
// Examples: | ||
// hono.METHOD(path, handler) | ||
// hono.METHOD(path, middleware, handler) | ||
// hono.METHOD(path, middleware, middleware, ..., handler) | ||
// hono.use(middleware) | ||
// hono.use(middleware, middleware, ...) | ||
private wrapArgs(args: unknown[], agent: Agent) { | ||
return args.map((arg) => { | ||
// Ignore non-function arguments | ||
if (typeof arg !== "function") { | ||
return arg; | ||
} | ||
|
||
return wrapRequestHandler(arg as MiddlewareHandler, agent); | ||
}); | ||
} | ||
|
||
wrap(hooks: Hooks) { | ||
const hono = hooks | ||
.addPackage("hono") | ||
.withVersion("^4.0.0") | ||
.addFile("hono-base"); | ||
|
||
hono | ||
.addSubject((exports) => { | ||
return exports.HonoBase.prototype; | ||
}) | ||
.modifyArguments("addRoute", (args, original, agent) => { | ||
return this.wrapArgs(args, agent); | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import type { Context as HonoContext } from "hono"; | ||
import { Context } from "../../agent/Context"; | ||
import { parse } from "../../helpers/parseCookies"; | ||
|
||
export async function contextFromRequest(c: HonoContext): Promise<Context> { | ||
const { req } = c; | ||
|
||
let route = undefined; | ||
if (req.routePath) { | ||
route = req.routePath; | ||
} | ||
|
||
const cookieHeader = req.header("cookie"); | ||
|
||
return { | ||
method: c.req.method, | ||
remoteAddress: undefined, // TODO | ||
body: undefined, // TODO | ||
url: req.url, | ||
headers: req.header(), | ||
routeParams: req.param(), | ||
query: req.query(), | ||
/* c8 ignore next */ | ||
cookies: cookieHeader ? parse(cookieHeader) : {}, | ||
source: "hono", | ||
route: route, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import type { Handler, MiddlewareHandler } from "hono"; | ||
import { Agent } from "../../agent/Agent"; | ||
import { getContext, runWithContext } from "../../agent/Context"; | ||
import { escapeHTML } from "../../helpers/escapeHTML"; | ||
import { contextFromRequest } from "./contextFromRequest"; | ||
import { shouldRateLimitRequest } from "../../ratelimiting/shouldRateLimitRequest"; | ||
|
||
export function wrapRequestHandler( | ||
handler: Handler | MiddlewareHandler, | ||
agent: Agent | ||
): MiddlewareHandler { | ||
return async (c, next) => { | ||
const context = await contextFromRequest(c); | ||
|
||
if (context.route) { | ||
agent.onRouteExecute(c.req.method, context.route); | ||
} | ||
|
||
return await runWithContext(context, async () => { | ||
// Even though we already have the context, we need to get it again | ||
// The context from `contextFromRequest` will never return a user | ||
// The user will be carried over from the previous context | ||
const context = getContext(); | ||
|
||
if (!context) { | ||
return await handler(c, next); | ||
} | ||
|
||
if (context.user && agent.getConfig().isUserBlocked(context.user.id)) { | ||
return c.text("You are blocked by Aikido runtime.", 403); | ||
} | ||
|
||
const result = shouldRateLimitRequest(context, agent); | ||
|
||
if (result.block) { | ||
let message = "You are rate limited by Aikido runtime."; | ||
if (result.trigger === "ip") { | ||
message += ` (Your IP: ${escapeHTML(context.remoteAddress!)})`; | ||
} | ||
|
||
return c.text(message, 429); | ||
} | ||
|
||
return await handler(c, next); | ||
}); | ||
}; | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
``` | ||
npm install | ||
npm run dev | ||
``` | ||
|
||
``` | ||
open http://localhost:3000 | ||
``` |
Oops, something went wrong.