From 1db044579b4eb4a33e0faab8041894ff1ec170d9 Mon Sep 17 00:00:00 2001 From: Nandor Kraszlan Date: Fri, 29 Sep 2023 11:44:34 +0100 Subject: [PATCH] Add getFingerprint util Closes unjs/h3/issues/536 --- src/utils/request.ts | 20 ++++++++++++ test/utils.test.ts | 76 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+) diff --git a/src/utils/request.ts b/src/utils/request.ts index d4f1bda8..86993931 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -4,6 +4,7 @@ import type { HTTPMethod, InferEventInput, RequestHeaders } from "../types"; import type { H3Event } from "../event"; import { validateData, ValidateFunction } from "./internal/validate"; import { getRequestWebStream } from "./body"; +import crypto from "uncrypto"; export function getQuery< T, @@ -190,3 +191,22 @@ export function getRequestIP( return event.node.req.socket.remoteAddress; } } + +export async function getFingerprint(event: H3Event): Promise { + let fingerprint = event.toString(); + const ip = getRequestIP(event, { xForwardedFor: event.headers.has('x-forwarded-for') }); + + if (ip) { + fingerprint += `-${ip}`; + } + + if (event.headers.has('user-agent')) { + fingerprint += `-${event.headers.get('user-agent')}`; + } + + const buffer = await crypto.subtle.digest('SHA-1', new TextEncoder().encode(fingerprint)); + + return Array.from(new Uint8Array(buffer)) + .map(b => b.toString(16).padStart(2, '0')) + .join(''); +} diff --git a/test/utils.test.ts b/test/utils.test.ts index bee47495..78eb3d50 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -12,6 +12,7 @@ import { getRequestURL, readFormData, getRequestIP, + getFingerprint, } from "../src"; describe("", () => { @@ -187,6 +188,81 @@ describe("", () => { }); }); + describe("getFingerPrint", () => { + it("returns an hash", async () => { + app.use( + "/", + eventHandler(async (event) => { + return getFingerprint(event); + }), + ); + + const req = request.get("/"); + + // sha1 is 40 chars long + expect((await req).text).toHaveLength(40); + // and only uses hex chars + expect((await req).text).toMatch(/^[0-9A-Fa-f]+$/); + }); + + it("returns the same hash every time for same request", async () => { + app.use( + "/", + eventHandler(async (event) => { + return getFingerprint(event); + }), + ); + + const req = request.get("/"); + expect((await req).text).toBe("41af4f039ba1d0960689b305e23d39cd458f6cb2"); + expect((await req).text).toBe("41af4f039ba1d0960689b305e23d39cd458f6cb2"); + }); + + it("uses user agent when available", async () => { + app.use( + "/", + eventHandler(async (event) => { + return getFingerprint(event); + }), + ); + + const req = request.get("/"); + req.set("user-agent", "test"); + + expect((await req).text).toBe("6ae9b40f2df7f80b128ae283a6d60a3c7a81a342"); + }); + + it("uses x-forwarded-for ip when header set", async () => { + app.use( + "/", + eventHandler(async (event) => { + return getFingerprint(event); + }), + ); + + const req = request.get("/"); + req.set("x-forwarded-for", "something"); + + expect((await req).text).toBe("37d612d6f0ac50d2875b128dfa89bd9d1bcb9174"); + }); + + it('uses the request ip when no x-forwarded-for header set', async () => { + app.use( + "/", + eventHandler(async (event) => { + return getFingerprint(event); + }), + ); + app.options.onRequest = e => { + Object.defineProperty(e.node.req.socket, 'remoteAddress', { get(): any { return 'something' } }); + } + + const req = request.get("/"); + + expect((await req).text).toBe("37d612d6f0ac50d2875b128dfa89bd9d1bcb9174"); + }); + }) + describe("assertMethod", () => { it("only allow head and post", async () => { app.use(