Skip to content

Instantly share code, notes, and snippets.

@slayer
Created March 31, 2025 17:02
Show Gist options
  • Save slayer/e7aa2781d1e23b82fab3b73b0c1b40cb to your computer and use it in GitHub Desktop.
Save slayer/e7aa2781d1e23b82fab3b73b0c1b40cb to your computer and use it in GitHub Desktop.
(() => {
// Async function helper
var createAsyncPromise = (context, args, asyncFn) => new Promise((resolve, reject) => {
var handleNext = result => {
try {
handleResult(asyncFn.next(result))
} catch (error) {
reject(error)
}
};
var handleError = error => {
try {
handleResult(asyncFn.throw(error))
} catch (innerError) {
reject(innerError)
}
};
var handleResult = result => result.done ? resolve(result.value) : Promise.resolve(result.value).then(handleNext, handleError);
handleResult((asyncFn = asyncFn.apply(context, args)).next())
});
// Global configuration object
var config = window.__mmapiws || {};
/**
* MurmurHash3 implementation for fingerprint generation
*/
function generateHash(input, seed) {
let remainder = input.length & 3;
let mainLength = input.length - remainder;
let hash = seed;
let tempHash;
const c1 = 3432918353;
const c2 = 461845907;
let position = 0;
let value;
// Process the input in 4-byte chunks
for (; position < mainLength;) {
value = input.charCodeAt(position) & 255 |
(input.charCodeAt(++position) & 255) << 8 |
(input.charCodeAt(++position) & 255) << 16 |
(input.charCodeAt(++position) & 255) << 24;
++position;
value = (value & 65535) * c1 + (((value >>> 16) * c1 & 65535) << 16) & 4294967295;
value = value << 15 | value >>> 17;
value = (value & 65535) * c2 + (((value >>> 16) * c2 & 65535) << 16) & 4294967295;
hash ^= value;
hash = hash << 13 | hash >>> 19;
tempHash = (hash & 65535) * 5 + (((hash >>> 16) * 5 & 65535) << 16) & 4294967295;
hash = (tempHash & 65535) + 27492 + (((tempHash >>> 16) + 58964 & 65535) << 16);
}
// Process remaining bytes
switch (value = 0, remainder) {
case 3:
value ^= (input.charCodeAt(position + 2) & 255) << 16;
case 2:
value ^= (input.charCodeAt(position + 1) & 255) << 8;
case 1:
value ^= input.charCodeAt(position) & 255;
value = (value & 65535) * c1 + (((value >>> 16) * c1 & 65535) << 16) & 4294967295;
value = value << 15 | value >>> 17;
value = (value & 65535) * c2 + (((value >>> 16) * c2 & 65535) << 16) & 4294967295;
hash ^= value;
}
// Finalization
hash ^= input.length;
hash ^= hash >>> 16;
hash = (hash & 65535) * 2246822507 + (((hash >>> 16) * 2246822507 & 65535) << 16) & 4294967295;
hash ^= hash >>> 13;
hash = (hash & 65535) * 3266489909 + (((hash >>> 16) * 3266489909 & 65535) << 16) & 4294967295;
hash ^= hash >>> 16;
return hash >>> 0;
}
/**
* Creates a promise that resolves after specified delay
*/
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Checks if the current window is in an iframe
*/
function isInIframe() {
try {
return window.top.location.origin !== window.self.location.origin;
} catch (error) {
return true;
}
}
/**
* Creates a 2D canvas fingerprint
*/
function getCanvasFingerprint() {
let canvas, context;
try {
canvas = document.createElement("canvas");
context = canvas.getContext("2d");
} catch (error) {}
if (!context) return null;
// Draw complex shapes and text to generate a unique fingerprint
context.fillStyle = "red";
context.fillRect(30, 10, 200, 100);
context.strokeStyle = "#1a3bc1";
context.lineWidth = 6;
context.lineCap = "round";
context.arc(50, 50, 20, 0, Math.PI, false);
context.stroke();
context.fillStyle = "#42e1a2";
context.font = "15.4px 'Arial'";
context.textBaseline = "alphabetic";
context.fillText("PR flacks quiz gym: TV DJ box when? ☠", 15, 60);
context.shadowOffsetX = 1;
context.shadowOffsetY = 2;
context.shadowColor = "white";
context.fillStyle = "rgba(0, 0, 200, 0.5)";
context.font = "60px 'Not a real font'";
context.fillText("No骗", 40, 80);
// Generate a hash from the canvas data URL
return generateHash(canvas.toDataURL(), 0);
}
/**
* Detects supported media codecs
*/
function getMediaCodecs() {
let videoCodecs = [
'video/mp4; codecs="avc1.42c00d"',
'video/ogg; codecs="theora"',
'video/webm; codecs="vorbis,vp8"',
'video/webm; codecs="vorbis,vp9"',
'video/mp2t; codecs="avc1.42E01E,mp4a.40.2"'
];
return {
audio: testCodecSupport([
"audio/mpeg",
'audio/mp4; codecs="mp4a.40.2"',
'audio/ogg; codecs="vorbis"',
'audio/ogg; codecs="opus"',
'audio/webm; codecs="vorbis"',
'audio/wav; codecs="1"'
]),
video: testCodecSupport(videoCodecs)
};
}
/**
* Tests which codecs are supported
*/
function testCodecSupport(codecs) {
let results = {};
for (let i = 0; i < codecs.length; i++) {
let codec = codecs[i];
if (window.MediaSource) {
results[codec] = window.MediaSource.isTypeSupported(codec);
}
}
return results;
}
/**
* Gets system colors
*/
function getSystemColors() {
let colorValues = {};
let canvas, context;
try {
canvas = document.createElement("canvas");
context = canvas.getContext("2d");
} catch (error) {}
if (!context) return colorValues;
let colorNames = [
"ActiveBorder", "ActiveCaption", "AppWorkspace", "Background",
"ButtonFace", "ButtonHighlight", "ButtonShadow", "ButtonText",
"CaptionText", "GrayText", "Highlight", "HighlightText",
"InactiveBorder", "InactiveCaption", "InactiveCaptionText",
"InfoBackground", "InfoText", "Menu", "MenuText", "Scrollbar",
"ThreeDDarkShadow", "ThreeDFace", "ThreeDHighlight", "ThreeDLightShadow",
"ThreeDShadow", "Window", "WindowFrame", "WindowText"
];
for (let i = 0; i < colorNames.length; i++) {
context.fillStyle = colorNames[i];
colorValues[colorNames[i]] = context.fillStyle;
}
return colorValues;
}
// CSS styles to test for rendering fingerprint
var cssTestStyles = [{
width: "150px",
height: "100px",
border: "2px solid rgba(0,150,255,0.91)",
color: "rgba(255,255,255,1)",
background: "rgb(255,255,255)",
position: "absolute",
transform: "skewY(29.1755628deg) rotate3d(11.000000007, 90, 0.100000000000009, 60000000000033.0000000006deg)"
}, /* More complex CSS styles... */];
/**
* Gets CSS rendering fingerprint
*/
function getCSSFingerprint() {
return createAsyncPromise(this, null, function* () {
try {
let iframe = document.createElement("iframe");
iframe.style.visibility = "hidden";
document.body.appendChild(iframe);
return new Promise((resolve, reject) => {
iframe.onload = function () {
// Create document in iframe and test CSS rendering
// ... [implementation] ...
};
iframe.src = "about:blank";
});
} catch (error) {
return null;
}
});
}
// Common fonts to detect
var commonFonts = [
"Abadi MT Condensed Light", "Adobe Fangsong Std", "Adobe Hebrew",
/* Many more fonts... */
];
/**
* Detects installed fonts
*/
function detectInstalledFonts() {
let canvasResult = detectFontsUsingCanvas();
return canvasResult !== null ? canvasResult : detectFontsUsingSpanElements();
}
/**
* Detects fonts using canvas measurements
*/
function detectFontsUsingCanvas() {
let canvas, context;
try {
canvas = document.createElement("canvas");
context = canvas.getContext("2d");
} catch (error) {}
if (!context) return null;
let baseFonts = ["sans-serif", "serif", "monospace"];
let baseWidths = [];
let fontSize = "72px";
let testText = "mmmmmmmmmmlli";
// Measure text with base fonts
for (let i = 0; i < baseFonts.length; i++) {
context.font = fontSize + ' "' + baseFonts[i] + '"';
baseWidths[i] = context.measureText(testText);
}
// Test each font by comparing measurements
let detectedFonts = [];
for (let i = 0; i < commonFonts.length; i++) {
let font = commonFonts[i];
let fontDetected = false;
for (let j = 0; j < baseWidths.length; j++) {
context.font = fontSize + ' "' + font + '", "' + baseFonts[j] + '"';
let width = context.measureText(testText);
if (width.width !== baseWidths[j].width ||
width.fontBoundingBoxAscent !== baseWidths[j].fontBoundingBoxAscent) {
fontDetected = true;
break;
}
}
if (fontDetected) {
detectedFonts.push(font);
}
}
return detectedFonts;
}
/**
* Font detector class using DOM elements
*/
function FontDetector() {
let baseFonts = ["monospace", "sans-serif", "serif"];
let baseWidths = [];
let testText = "mmmmmmmmmmlli";
let fontSize = "72px";
let body = document.body;
let testSpan = document.createElement("span");
let spanStyle = testSpan.style;
spanStyle.fontSize = fontSize;
spanStyle.visibility = "hidden";
testSpan.innerText = testText;
body.appendChild(testSpan);
let measureFont = function (fontFamily) {
spanStyle.fontFamily = fontFamily;
return {
height: testSpan.offsetHeight,
width: testSpan.offsetWidth
};
};
// Get base measurements
for (let i = 0; i < baseFonts.length; i++) {
baseWidths[i] = measureFont(baseFonts[i]);
}
this.detect = function (font) {
// Compare with base fonts to detect if font is installed
for (let i = 0; i < baseWidths.length; i++) {
let dimensions = measureFont('"' + font + '",' + baseFonts[i]);
let reference = baseWidths[i];
if (dimensions.height !== reference.height || dimensions.width !== reference.width) {
return true;
}
}
return false;
};
this.finish = function () {
body.removeChild(testSpan);
};
}
// Math functions and test values for fingerprinting
var mathFunctions = [
Math.acos, Math.acosh, Math.asin, Math.atan, Math.atanh,
Math.cos, Math.cosh, Math.exp, Math.expm1, Math.fround,
Math.log, Math.log10, Math.sin, Math.sinh, Math.sqrt,
Math.tan, Math.tanh
];
var mathTestValues = [
-1, 0, 1, Math.E, Math.PI, Math.SQRT2, 1e300, -1e300, 1e-300, -1e-300
];
/**
* Tests math functions for fingerprinting
*/
function getMathFingerprint() {
let results = {};
for (let func of mathFunctions) {
if (func) {
let funcResults = results[func.name] = {};
for (let testValue of mathTestValues) {
let result = func(testValue);
if (!Number.isNaN(result)) {
funcResults[testValue] = formatInfinityValues(result);
}
}
}
}
return results;
}
/**
* Formats infinity values for consistent representation
*/
function formatInfinityValues(value) {
return value == Number.NEGATIVE_INFINITY ? "-Infinity" :
value == Number.POSITIVE_INFINITY ? "Infinity" : value;
}
// List of browser plugins to check for
var knownPlugins = [
"4game", "AdblockPlugin", "AdobeExManCCDetect",
/* Many more plugin names... */
];
/**
* Collects browser plugin information
*/
function getPluginData() {
let pluginList = [];
let detectedPlugins = {};
// Get installed plugins
for (let i = 0; i < navigator.plugins.length; i++) {
let plugin = navigator.plugins[i];
// Skip Chrome's built-in Flash
if (plugin.name === "Shockwave Flash" && navigator.userAgent.indexOf("Chrome") > -1) {
continue;
}
detectedPlugins[plugin.name] = 1;
pluginList.push(formatPluginData(plugin));
}
// Try to detect additional plugins
for (let i = 0; i < knownPlugins.length; i++) {
let pluginName = knownPlugins[i];
if (!detectedPlugins[pluginName]) {
let plugin = navigator.plugins[pluginName];
if (plugin) {
pluginList.push(formatPluginData(plugin));
}
}
}
return pluginList;
}
/**
* Formats plugin data into a structured object
*/
function formatPluginData(plugin) {
let pluginInfo = {
name: plugin.name,
filename: plugin.filename.toLowerCase(),
description: plugin.description
};
if (typeof plugin.version != "undefined") {
pluginInfo.version = plugin.version;
}
pluginInfo.mimeTypes = [];
for (let i = 0; i < plugin.length; i++) {
let mimeType = plugin[i];
pluginInfo.mimeTypes.push({
description: mimeType.description,
suffixes: mimeType.suffixes,
type: mimeType.type
});
}
return pluginInfo;
}
/**
* Collects navigator object properties
*/
function getNavigatorInfo() {
return createAsyncPromise(this, null, function* () {
let navigatorProps = serializeObject(navigator);
let nav = navigator;
let connection = nav.connection || nav.mozConnection || nav.webkitConnection;
if (connection) {
navigatorProps.connection = serializeObject(connection);
}
// Collect gamepads
if ("getGamepads" in navigator) {
try {
navigatorProps.gamepads = Array.from(
navigator.getGamepads()
.filter(gp => !!gp)
.map(gp => gp.id)
);
} catch (error) {}
}
// Collect keyboard layout if available
if (!isInIframe() && nav.keyboard && nav.keyboard.getLayoutMap) {
navigatorProps.keyboard = navigatorProps.keyboard || {};
navigatorProps.keyboard.layoutMap = Array.from(
(yield nav.keyboard.getLayoutMap()).entries()
);
}
// Collect media devices info
if (navigator.mediaDevices) {
if (navigator.mediaDevices.getSupportedConstraints) {
navigatorProps.mediaDevices = navigatorProps.mediaDevices || {};
navigatorProps.mediaDevices.supportedConstraints =
navigator.mediaDevices.getSupportedConstraints();
}
if (navigator.mediaDevices.enumerateDevices) {
navigatorProps.mediaDevices = navigatorProps.mediaDevices || {};
navigatorProps.mediaDevices.enumerateDevices =
(yield navigator.mediaDevices.enumerateDevices()).map(device => device.kind);
}
}
return navigatorProps;
});
}
/**
* Collects screen properties
*/
function getScreenInfo() {
let screenProps = {};
let propNames = [];
for (let prop in window.screen) {
if (typeof window.screen[prop] != "object") {
screenProps[prop] = window.screen[prop];
}
propNames.push(prop);
}
screenProps.enumerationOrder = propNames;
return screenProps;
}
/**
* Creates a plain object with all enumerable properties
*/
function serializeObject(obj) {
let propNames = [];
let result = {};
for (let prop in obj) {
if (typeof obj[prop] != "object" || isArray(obj[prop])) {
result[prop] = obj[prop];
}
propNames.push(prop);
}
result.enumerationOrder = propNames;
return result;
}
/**
* Checks if object is an array
*/
function isArray(obj) {
return Object.prototype.toString.call(obj) === "[object Array]";
}
/**
* Collects speech synthesis capabilities
*/
function getSpeechSynthesisInfo() {
return createAsyncPromise(this, null, function* () {
if (!window.speechSynthesis) return null;
try {
return {
voices: (yield getVoicesList()).slice(0, 20).map(voice => serializeObject(voice))
};
} catch (error) {
return null;
}
});
}
/**
* Gets available speech synthesis voices
*/
function getVoicesList() {
return createAsyncPromise(this, null, function* () {
let voices = speechSynthesis.getVoices();
if (voices.length) {
return voices;
}
// Wait for voices to load if none are available
yield Promise.race([
new Promise(resolve =>
speechSynthesis.addEventListener("voiceschanged", resolve, { once: true })
),
delay(50)
]);
return speechSynthesis.getVoices();
});
}
/**
* Collects WebGL capabilities and generates fingerprint
*/
function getWebGLInfo() {
let canvas = document.createElement("canvas");
let context;
let webglInfo = {};
try {
context = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
} catch (error) {}
if (!context) return webglInfo;
try {
webglInfo.extensions = context.getSupportedExtensions();
} catch (error) {}
try {
if (!config.disableWebglHash) {
webglInfo.hash = generateWebGLHash(context, canvas);
}
} catch (error) {}
return webglInfo;
}
/**
* Generates a hash based on WebGL rendering
*/
function generateWebGLHash(gl, canvas) {
// GLSL shader program for fingerprinting
let vertexShader = "attribute vec2 attrVertex; varying vec2 varyinTexCoordinate; uniform vec2 uniformOffset; void main() { varyinTexCoordinate = attrVertex + uniformOffset; gl_Position = vec4(attrVertex, 0, 1); }";
let fragmentShader = "precision mediump float; varying vec2 varyinTexCoordinate; void main() { gl_FragColor = vec4(varyinTexCoordinate, 0, 1); }";
// Create buffer with vertex data
let buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
let vertices = new Float32Array([-.2, -.9, 0, .4, -.26, 0, 0, .732134444, 0]);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
let vertSize = 3;
let numVertices = 3;
// Create and compile shader program
let program = gl.createProgram();
let vShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vShader, vertexShader);
gl.compileShader(vShader);
let fShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fShader, fragmentShader);
gl.compileShader(fShader);
gl.attachShader(program, vShader);
gl.attachShader(program, fShader);
gl.linkProgram(program);
gl.useProgram(program);
// Set up attributes and uniforms
let vertexAttrib = gl.getAttribLocation(program, "attrVertex");
let offsetUniform = gl.getUniformLocation(program, "uniformOffset");
gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(vertexAttrib, vertSize, gl.FLOAT, false, 0, 0);
gl.uniform2f(offsetUniform, 1, 1);
// Draw the shape to the canvas
gl.drawArrays(gl.TRIANGLE_STRIP, 0, numVertices);
// Create a hash from the resulting image
return generateHash(canvas.toDataURL(), 0);
}
/**
* Main fingerprinting function
*/
function collectFingerprint() {
return createAsyncPromise(this, null, function* () {
let currentDate = new Date();
let systemColors;
try {
systemColors = getSystemColors();
} catch (error) {}
// Collect all fingerprint data
let fingerprintData = {
accountId: getAccountIdentifier(),
applePay: checkApplePaySupport(),
canvas: {
"2dHash": getCanvasFingerprint(),
webgl: getWebGLInfo()
},
codecs: getMediaCodecs(),
deviceTime: currentDate.getTime() / 1000,
documentUrl: document.URL,
fonts: {
css: detectInstalledFonts()
},
hasGPC: hasGlobalPrivacyControl(),
heapSizeLimit: getJSHeapLimit(),
math: getMathFingerprint(),
navigator: yield getNavigatorInfo(),
plugins: getPluginData(),
screen: getScreenInfo(),
speechSynthesis: yield getSpeechSynthesisInfo(),
systemColors: systemColors,
timezoneOffset: currentDate.getTimezoneOffset(),
touchInput: hasTouchSupport(),
rects: yield getCSSFingerprint()
};
// Add battery info if available
if (navigator.getBattery) {
let battery = yield navigator.getBattery();
fingerprintData.battery = {
charging: battery.charging,
chargingTime: battery.chargingTime,
dischargingTime: battery.dischargingTime,
level: battery.level
};
}
return fingerprintData;
});
}
/**
* Checks for Apple Pay support
*/
function checkApplePaySupport() {
if (isInIframe()) return null;
let applePaySession = window.ApplePaySession;
return applePaySession ? {
canMakePayments: applePaySession.canMakePayments()
} : null;
}
/**
* Checks if Global Privacy Control is enabled
*/
function hasGlobalPrivacyControl() {
return !!navigator.globalPrivacyControl;
}
/**
* Gets JavaScript heap size limit
*/
function getJSHeapLimit() {
let memory = performance.memory;
return memory ? memory.jsHeapSizeLimit : 0;
}
/**
* Checks if device has touch support
*/
function hasTouchSupport() {
try {
return navigator.maxTouchPoints > 0 ||
"ontouchstart" in window ||
window.matchMedia("(-webkit-touch-enabled),(-moz-touch-enabled),(-o-touch-enabled),(-ms-touch-enabled)").matches;
} catch (error) {
return false;
}
}
/**
* Gets account identifier
*/
function getAccountIdentifier() {
return typeof window.survey_height_follow != "undefined" ? window.survey_height_follow :
typeof window.maxmind_user_id != "undefined" ? window.maxmind_user_id :
config.accountId;
}
// Main execution - start fingerprinting when document is ready
waitForDocumentBody().catch(error => console.error(error));
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment