Skip to content

Commit

Permalink
Merging 2d3bb19 into trunk-temp/pr-1016/33c2f549-2c11-443c-b963-22e57…
Browse files Browse the repository at this point in the history
…ebe2e7a
  • Loading branch information
trunk-io[bot] authored Jun 28, 2024
2 parents c17a101 + 2d3bb19 commit f63ade6
Show file tree
Hide file tree
Showing 19 changed files with 597 additions and 190 deletions.
60 changes: 8 additions & 52 deletions analyze/edge-light.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ArcjetLogger } from "@arcjet/protocol";
import type { ArcjetLogger, ArcjetRequestDetails } from "@arcjet/protocol";

import * as core from "./wasm/arcjet_analyze_js_req.component.js";
import type {
Expand All @@ -14,6 +14,7 @@ import componentCore3Wasm from "./wasm/arcjet_analyze_js_req.component.core3.was

interface AnalyzeContext {
log: ArcjetLogger;
characteristics: string[];
}

async function moduleFromPath(path: string): Promise<WebAssembly.Module> {
Expand Down Expand Up @@ -72,45 +73,21 @@ export {
/**
* Generate a fingerprint for the client. This is used to identify the client
* across multiple requests.
* @param ip - The IP address of the client.
* @param context - The Arcjet Analyze context.
* @param request - The request to fingerprint.
* @returns A SHA-256 string fingerprint.
*/
export async function generateFingerprint(
context: AnalyzeContext,
ip: string,
request: Partial<ArcjetRequestDetails>,
): Promise<string> {
if (ip == "") {
return "";
}

const analyze = await init(context);

if (typeof analyze !== "undefined") {
return analyze.generateFingerprint(ip);
}

if (hasSubtleCryptoDigest()) {
// Fingerprint v1 is just the IP address
const fingerprintRaw = `fp_1_${ip}`;

// Based on MDN example at
// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest#converting_a_digest_to_a_hex_string

// Encode the raw fingerprint into a utf-8 Uint8Array
const fingerprintUint8 = new TextEncoder().encode(fingerprintRaw);
// Hash the message with SHA-256
const fingerprintArrayBuffer = await crypto.subtle.digest(
"SHA-256",
fingerprintUint8,
return analyze.generateFingerprint(
JSON.stringify(request),
context.characteristics,
);
// Convert the ArrayBuffer to a byte array
const fingerprintArray = Array.from(new Uint8Array(fingerprintArrayBuffer));
// Convert the bytes to a hex string
const fingerprint = fingerprintArray
.map((b) => b.toString(16).padStart(2, "0"))
.join("");

return fingerprint;
}

return "";
Expand Down Expand Up @@ -149,24 +126,3 @@ export async function detectBot(
};
}
}

function hasSubtleCryptoDigest() {
if (typeof crypto === "undefined") {
return false;
}

if (!("subtle" in crypto)) {
return false;
}
if (typeof crypto.subtle === "undefined") {
return false;
}
if (!("digest" in crypto.subtle)) {
return false;
}
if (typeof crypto.subtle.digest !== "function") {
return false;
}

return true;
}
60 changes: 8 additions & 52 deletions analyze/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ArcjetLogger } from "@arcjet/protocol";
import type { ArcjetLogger, ArcjetRequestDetails } from "@arcjet/protocol";

import * as core from "./wasm/arcjet_analyze_js_req.component.js";
import type {
Expand All @@ -14,6 +14,7 @@ import { wasm as componentCore3Wasm } from "./wasm/arcjet_analyze_js_req.compone

interface AnalyzeContext {
log: ArcjetLogger;
characteristics: string[];
}

// TODO: Do we actually need this wasmCache or does `import` cache correctly?
Expand Down Expand Up @@ -86,45 +87,21 @@ export {
/**
* Generate a fingerprint for the client. This is used to identify the client
* across multiple requests.
* @param ip - The IP address of the client.
* @param context - The Arcjet Analyze context.
* @param request - The request to fingerprint.
* @returns A SHA-256 string fingerprint.
*/
export async function generateFingerprint(
context: AnalyzeContext,
ip: string,
request: Partial<ArcjetRequestDetails>,
): Promise<string> {
if (ip == "") {
return "";
}

const analyze = await init(context);

if (typeof analyze !== "undefined") {
return analyze.generateFingerprint(ip);
}

if (hasSubtleCryptoDigest()) {
// Fingerprint v1 is just the IP address
const fingerprintRaw = `fp_1_${ip}`;

// Based on MDN example at
// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest#converting_a_digest_to_a_hex_string

// Encode the raw fingerprint into a utf-8 Uint8Array
const fingerprintUint8 = new TextEncoder().encode(fingerprintRaw);
// Hash the message with SHA-256
const fingerprintArrayBuffer = await crypto.subtle.digest(
"SHA-256",
fingerprintUint8,
return analyze.generateFingerprint(
JSON.stringify(request),
context.characteristics,
);
// Convert the ArrayBuffer to a byte array
const fingerprintArray = Array.from(new Uint8Array(fingerprintArrayBuffer));
// Convert the bytes to a hex string
const fingerprint = fingerprintArray
.map((b) => b.toString(16).padStart(2, "0"))
.join("");

return fingerprint;
}

return "";
Expand Down Expand Up @@ -163,24 +140,3 @@ export async function detectBot(
};
}
}

function hasSubtleCryptoDigest() {
if (typeof crypto === "undefined") {
return false;
}

if (!("subtle" in crypto)) {
return false;
}
if (typeof crypto.subtle === "undefined") {
return false;
}
if (!("digest" in crypto.subtle)) {
return false;
}
if (typeof crypto.subtle.digest !== "function") {
return false;
}

return true;
}
Binary file modified analyze/wasm/arcjet_analyze_js_req.component.core.wasm
Binary file not shown.
Binary file modified analyze/wasm/arcjet_analyze_js_req.component.core2.wasm
Binary file not shown.
Binary file modified analyze/wasm/arcjet_analyze_js_req.component.core3.wasm
Binary file not shown.
2 changes: 1 addition & 1 deletion analyze/wasm/arcjet_analyze_js_req.component.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export interface ImportObject {
}
export interface Root {
detectBot(headers: string, patternsAdd: string, patternsRemove: string): BotDetectionResult,
generateFingerprint(ip: string): string,
generateFingerprint(request: string, characteristics: string[]): string,
isValidEmail(candidate: string, options: EmailValidationConfig | undefined): boolean,
}

Expand Down
22 changes: 16 additions & 6 deletions analyze/wasm/arcjet_analyze_js_req.component.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,15 +163,25 @@ async function instantiate(getCoreModule, imports, instantiateCore = WebAssembly
return variant5.val;
}

function generateFingerprint(arg0) {
function generateFingerprint(arg0, arg1) {
var ptr0 = utf8Encode(arg0, realloc0, memory0);
var len0 = utf8EncodedLen;
const ret = exports1['generate-fingerprint'](ptr0, len0);
var ptr1 = dataView(memory0).getInt32(ret + 0, true);
var len1 = dataView(memory0).getInt32(ret + 4, true);
var result1 = utf8Decoder.decode(new Uint8Array(memory0.buffer, ptr1, len1));
var vec2 = arg1;
var len2 = vec2.length;
var result2 = realloc0(0, 0, 4, len2 * 8);
for (let i = 0; i < vec2.length; i++) {
const e = vec2[i];
const base = result2 + i * 8;var ptr1 = utf8Encode(e, realloc0, memory0);
var len1 = utf8EncodedLen;
dataView(memory0).setInt32(base + 4, len1, true);
dataView(memory0).setInt32(base + 0, ptr1, true);
}
const ret = exports1['generate-fingerprint'](ptr0, len0, result2, len2);
var ptr3 = dataView(memory0).getInt32(ret + 0, true);
var len3 = dataView(memory0).getInt32(ret + 4, true);
var result3 = utf8Decoder.decode(new Uint8Array(memory0.buffer, ptr3, len3));
postReturn1(ret);
return result1;
return result3;
}

function isValidEmail(arg0, arg1) {
Expand Down
Binary file modified analyze/wasm/arcjet_analyze_js_req.component.wasm
Binary file not shown.
60 changes: 8 additions & 52 deletions analyze/workerd.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ArcjetLogger } from "@arcjet/protocol";
import type { ArcjetLogger, ArcjetRequestDetails } from "@arcjet/protocol";

import * as core from "./wasm/arcjet_analyze_js_req.component.js";
import type {
Expand All @@ -14,6 +14,7 @@ import componentCore3Wasm from "./wasm/arcjet_analyze_js_req.component.core3.was

interface AnalyzeContext {
log: ArcjetLogger;
characteristics: string[];
}

async function moduleFromPath(path: string): Promise<WebAssembly.Module> {
Expand Down Expand Up @@ -72,45 +73,21 @@ export {
/**
* Generate a fingerprint for the client. This is used to identify the client
* across multiple requests.
* @param ip - The IP address of the client.
* @param context - The Arcjet Analyze context.
* @param request - The request to fingerprint.
* @returns A SHA-256 string fingerprint.
*/
export async function generateFingerprint(
context: AnalyzeContext,
ip: string,
request: Partial<ArcjetRequestDetails>,
): Promise<string> {
if (ip == "") {
return "";
}

const analyze = await init(context);

if (typeof analyze !== "undefined") {
return analyze.generateFingerprint(ip);
}

if (hasSubtleCryptoDigest()) {
// Fingerprint v1 is just the IP address
const fingerprintRaw = `fp_1_${ip}`;

// Based on MDN example at
// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest#converting_a_digest_to_a_hex_string

// Encode the raw fingerprint into a utf-8 Uint8Array
const fingerprintUint8 = new TextEncoder().encode(fingerprintRaw);
// Hash the message with SHA-256
const fingerprintArrayBuffer = await crypto.subtle.digest(
"SHA-256",
fingerprintUint8,
return analyze.generateFingerprint(
JSON.stringify(request),
context.characteristics,
);
// Convert the ArrayBuffer to a byte array
const fingerprintArray = Array.from(new Uint8Array(fingerprintArrayBuffer));
// Convert the bytes to a hex string
const fingerprint = fingerprintArray
.map((b) => b.toString(16).padStart(2, "0"))
.join("");

return fingerprint;
}

return "";
Expand Down Expand Up @@ -149,24 +126,3 @@ export async function detectBot(
};
}
}

function hasSubtleCryptoDigest() {
if (typeof crypto === "undefined") {
return false;
}

if (!("subtle" in crypto)) {
return false;
}
if (typeof crypto.subtle === "undefined") {
return false;
}
if (!("digest" in crypto.subtle)) {
return false;
}
if (typeof crypto.subtle.digest !== "function") {
return false;
}

return true;
}
36 changes: 26 additions & 10 deletions arcjet/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,12 @@ import {
ArcjetSlidingWindowRateLimitRule,
ArcjetShieldRule,
ArcjetLogger,
ArcjetRateLimitRule,
} from "@arcjet/protocol";
import { ArcjetBotTypeToProtocol } from "@arcjet/protocol/convert.js";
import {
ArcjetBotTypeToProtocol,
isRateLimitRule,
} from "@arcjet/protocol/convert.js";
import { Client } from "@arcjet/protocol/client.js";
import * as analyze from "@arcjet/analyze";
import * as duration from "@arcjet/duration";
Expand Down Expand Up @@ -784,6 +788,10 @@ export interface ArcjetOptions<Rules extends [...(Primitive | Product)[]]> {
* Rules to apply when protecting a request.
*/
rules: readonly [...Rules];
/**
* Characteristics to be used to uniquely identify clients.
*/
characteristics?: string[];
/**
* The client used to make requests to the Arcjet API. This must be set
* when creating the SDK, such as inside @arcjet/next or mocked in tests.
Expand Down Expand Up @@ -890,21 +898,19 @@ export default function arcjet<
log.time?.("local");

log.time?.("fingerprint");
let ip = "";
if (typeof details.ip === "string") {
ip = details.ip;
}
if (details.ip === "") {
log.warn("generateFingerprint: ip is empty");
}

const characteristics = options.characteristics
? options.characteristics
: [];

const baseContext = {
key,
log,
characteristics,
...ctx,
};

const fingerprint = await analyze.generateFingerprint(baseContext, ip);
const fingerprint = await analyze.generateFingerprint(baseContext, details);
log.debug("fingerprint (%s): %s", rt, fingerprint);
log.timeEnd?.("fingerprint");

Expand Down Expand Up @@ -945,14 +951,24 @@ export default function arcjet<
}

const results: ArcjetRuleResult[] = [];
// Default all rules to NOT_RUN/ALLOW before doing anything
for (let idx = 0; idx < rules.length; idx++) {
// Default all rules to NOT_RUN/ALLOW before doing anything
results[idx] = new ArcjetRuleResult({
ttl: 0,
state: "NOT_RUN",
conclusion: "ALLOW",
reason: new ArcjetReason(),
});

// Add top-level characteristics to all Rate Limit rules that don't already have
// their own set of characteristics.
const candidate_rule = rules[idx];
if (isRateLimitRule(candidate_rule)) {
if (typeof candidate_rule.characteristics === "undefined") {
candidate_rule.characteristics = characteristics;
rules[idx] = candidate_rule;
}
}
}

// We have our own local cache which we check first. This doesn't work in
Expand Down
Loading

0 comments on commit f63ade6

Please sign in to comment.