Skip to content

Commit

Permalink
Initial commit Hono support
Browse files Browse the repository at this point in the history
  • Loading branch information
hansott committed Jun 4, 2024
1 parent 405f6be commit 11e5df4
Show file tree
Hide file tree
Showing 14 changed files with 480 additions and 6 deletions.
106 changes: 106 additions & 0 deletions end2end/tests/hono-mongodb.test.js
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();
});
});
2 changes: 2 additions & 0 deletions library/agent/protect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
createCloudFunctionWrapper,
FunctionsFramework,
} from "../sources/FunctionsFramework";
import { Hono } from "../sources/Hono";
import { HTTPServer } from "../sources/HTTPServer";
import { createLambdaWrapper } from "../sources/Lambda";
import { PubSub } from "../sources/PubSub";
Expand Down Expand Up @@ -117,6 +118,7 @@ function getWrappers() {
new Undici(),
new Path(),
new HTTPServer(),
new Hono(),
];
}

Expand Down
3 changes: 2 additions & 1 deletion library/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@
"supertest": "^6.3.4",
"tap": "^18.6.1",
"typescript": "^5.3.3",
"undici": "^6.12.0"
"undici": "^6.12.0",
"hono": "^4.4.2"
},
"scripts": {
"test": "tap --allow-incomplete-coverage",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Agent } from "../../agent/Agent";
import { Context } from "../../agent/Context";
import { isLocalhostIP } from "../../helpers/isLocalhostIP";
import { tryParseURL } from "../../helpers/tryParseURL";
import { Agent } from "../agent/Agent";
import { Context } from "../agent/Context";
import { isLocalhostIP } from "../helpers/isLocalhostIP";
import { tryParseURL } from "../helpers/tryParseURL";

type Result =
| {
Expand Down
41 changes: 41 additions & 0 deletions library/sources/Hono.ts
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);
});
}
}
2 changes: 1 addition & 1 deletion library/sources/express/wrapRequestHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Agent } from "../../agent/Agent";
import { getContext, runWithContext } from "../../agent/Context";
import { escapeHTML } from "../../helpers/escapeHTML";
import { contextFromRequest } from "./contextFromRequest";
import { shouldRateLimitRequest } from "./shouldRateLimitRequest";
import { shouldRateLimitRequest } from "../../ratelimiting/shouldRateLimitRequest";

export function wrapRequestHandler(
handler: RequestHandler,
Expand Down
28 changes: 28 additions & 0 deletions library/sources/hono/contextFromRequest.ts
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

Check failure on line 17 in library/sources/hono/contextFromRequest.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

Unexpected 'todo' comment: 'TODO'
body: undefined, // TODO

Check failure on line 18 in library/sources/hono/contextFromRequest.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

Unexpected 'todo' comment: 'TODO'
url: req.url,
headers: req.header(),
routeParams: req.param(),
query: req.query(),
/* c8 ignore next */
cookies: cookieHeader ? parse(cookieHeader) : {},
source: "hono",
route: route,
};
}
47 changes: 47 additions & 0 deletions library/sources/hono/wrapRequestHandler.ts
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);
});
};
}
34 changes: 34 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions sample-apps/hono-mongodb/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
```
npm install
npm run dev
```

```
open http://localhost:3000
```
Loading

0 comments on commit 11e5df4

Please sign in to comment.