@kanadi/core
Version:
Multi-Layer CAPTCHA Framework with customizable validators and challenge bundles
72 lines (62 loc) • 1.97 kB
text/typescript
import { createHash } from "crypto";
export interface ServerFingerprintData {
ip: string;
userAgent: string;
acceptLanguage?: string;
acceptEncoding?: string;
connection?: string;
headers: Record<string, string>;
}
function normalizeIP(ip: string): string {
if (ip.startsWith("::ffff:")) {
return ip.substring(7);
}
return ip;
}
function parseUserAgent(userAgent: string): string {
const parts = userAgent.split(/[\s/()]+/);
return parts
.filter((p) => p && p.length > 2)
.slice(0, 5)
.join("|");
}
export function generateServerFingerprint(data: {
ip: string;
userAgent?: string;
headers?: Record<string, string | string[] | undefined>;
}): { fingerprint: string; data: ServerFingerprintData } {
const normalizedIP = normalizeIP(data.ip);
const userAgent = data.userAgent || "";
const headers = data.headers || {};
const getHeader = (key: string): string => {
const value = headers[key.toLowerCase()];
if (Array.isArray(value)) return value[0] || "";
return value || "";
};
const fingerprintData: ServerFingerprintData = {
ip: normalizedIP,
userAgent: parseUserAgent(userAgent),
acceptLanguage: getHeader("accept-language"),
acceptEncoding: getHeader("accept-encoding"),
connection: getHeader("connection"),
headers: {
"accept-language": getHeader("accept-language"),
"accept-encoding": getHeader("accept-encoding"),
"sec-ch-ua": getHeader("sec-ch-ua"),
"sec-ch-ua-mobile": getHeader("sec-ch-ua-mobile"),
"sec-ch-ua-platform": getHeader("sec-ch-ua-platform"),
},
};
const fingerprintString = JSON.stringify(fingerprintData);
const fingerprint = createHash("sha256")
.update(fingerprintString)
.digest("hex");
return { fingerprint, data: fingerprintData };
}
export function combineFingerprints(
clientFingerprint: string,
serverFingerprint: string,
): string {
const combined = `${clientFingerprint}:${serverFingerprint}`;
return createHash("sha256").update(combined).digest("hex");
}