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

Fastify adapter #1076

Open
blaine-arcjet opened this issue Jul 3, 2024 · 9 comments
Open

Fastify adapter #1076

blaine-arcjet opened this issue Jul 3, 2024 · 9 comments

Comments

@blaine-arcjet
Copy link
Contributor

We should create a fastify adapter.

One thing I noticed that would be cool is that fastify has an official plugin for generating swagger for your API, which we could convert to a postman collection and test the requests against an API.

@davidmytton
Copy link
Contributor

Logging that we've had a user request for NestJS + Fastify.

@davidmytton davidmytton changed the title Support for Fastify Fastify adapter Nov 11, 2024
@davidmytton
Copy link
Contributor

davidmytton commented Nov 11, 2024

We had another request for Fastify.

@blaine-arcjet
Copy link
Contributor Author

@davidmytton The Fastify Request object is not the same as the Node.js request object. They stash the Node.js request object on .raw field, which may be usable but I worry that something like the body would be unusable because they probably have their own way of reading it.

@davidmytton
Copy link
Contributor

It didn't show as a type error when I tried this code, so this may not work properly with sensitive info detection then.

@blaine-arcjet
Copy link
Contributor Author

It didn't show as a type error when I tried this code

You will rarely get a type error by stuffing different types of requests into the node adapter because we define the request type ourselves:

interface ArcjetNodeRequest {
  headers?: Record<string, string | string[] | undefined>;
  socket?: Partial<{ remoteAddress: string; encrypted: boolean }>;
  method?: string;
  httpVersion?: string;
  url?: string;
  // Things needed for getting a body
  body?: unknown;
  on?: EventHandlerLike;
  removeListener?: EventHandlerLike;
  readable?: boolean;
}

You'll notice that all the fields in the type are partial, which means the only type errors you will see are if the object you are passing in shares nothing in common or if a field conflicts, but not if some of these fields are missing. We define this ourselves because we don't want to be coupled to 1 version of the IncomingMessage type in @types/node package. This allows us to use older or newer fields for the different versions of the Node.js runtime.

In addition to not knowing how body will be handled, the most significant thing here is that socket is going to be undefined and we don't look for ip, so your IP is going to be missing in production.

@davidmytton
Copy link
Contributor

We had another request for this in Discord so I created an example that manually pulls the IP from the headers and tested it on Fly:

// ESM
import Fastify from "fastify";
import ip from "@arcjet/ip";

const fastify = Fastify({
  logger: true,
});

import arcjet, { detectBot, shield, tokenBucket } from "@arcjet/node";

const aj = arcjet({
  key: process.env.ARCJET_KEY!, // Get your site key from https://app.arcjet.com
  characteristics: ["ip.src"], // Track requests by IP
  rules: [
    // Shield protects your app from common attacks e.g. SQL injection
    shield({ mode: "LIVE" }),
    // Create a bot detection rule
    detectBot({
      mode: "LIVE", // Blocks requests. Use "DRY_RUN" to log only
      // Block all bots except search engine crawlers. See
      // https://arcjet.com/bot-list
      allow: ["CATEGORY:SEARCH_ENGINE"],
    }),
    // Create a token bucket rate limit. Other algorithms are supported.
    tokenBucket({
      mode: "LIVE",
      refillRate: 5, // Refill 5 tokens per interval
      interval: 10, // Refill every 10 seconds
      capacity: 10, // Bucket capacity of 10 tokens
    }),
  ],
});

fastify.get("/", async (request, reply) => {
  // Use the Arcjet utility package to extract the client IP address
  // Installed with `npm install @arcjet/ip`
  // Import with `import ip from "@arcjet/ip";`
  // @ts-ignore
  const clientIP = ip(request, request.headers);

  // Construct a request object to send to Arcjet with the required fields
  // until https://github.com/arcjet/arcjet-js/issues/1076 is resolved
  const arcjetRequest = {
    ip: clientIP,
    method: request.method,
    host: request.hostname,
    url: request.url,
    headers: request.headers,
  };

  console.log("Arcjet request", arcjetRequest);

  const decision = await aj.protect(arcjetRequest, { requested: 5 }); // Deduct 5 tokens from the bucket
  //console.log("Arcjet decision", decision);

  if (decision.isDenied()) {
    if (decision.reason.isRateLimit()) {
      reply.code(429).send({ error: "Too many requests" });
    } else if (decision.reason.isBot()) {
      reply.code(403).send({ error: "No bots allowed" });
    } else {
      reply.code(403).send({ error: "Forbidden" });
    }
  }

  return { hello: "world" };
});

/**
 * Run the server!
 */
const start = async () => {
  try {
    await fastify.listen({ host: "0.0.0.0", port: 3000 });
  } catch (err) {
    fastify.log.error(err);
    process.exit(1);
  }
};
start();
{
  "name": "fastify",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "dev": "npx tsx --env-file .env.local index.ts"
  },
  "author": "",
  "type": "module",
  "description": "",
  "dependencies": {
    "@arcjet/ip": "^1.0.0-alpha.28",
    "@arcjet/node": "^1.0.0-alpha.28",
    "fastify": "^5.1.0"
  },
  "devDependencies": {
    "@types/node": "^22.9.0"
  }
}

@blaine-arcjet
Copy link
Contributor Author

@davidmytton I believe that usage of @arcjet/ip is outdated. We only accept a single argument now, see #2018

@davidmytton
Copy link
Contributor

Ah, we missed updating the readme then:

const globalIp = ip(request, headers);

@blaine-arcjet
Copy link
Contributor Author

Good catch. I'll do that for today's release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants