UNPKG

@deviceidio/deviceid

Version:

deviceID client

1,537 lines (1,485 loc) 93.1 kB
const UAParser = require("ua-parser-js"); const CryptoJS = require("crypto-js"); class DeviceIDError extends Error { constructor(message) { super(message); this.type = message.type; this.val = message.val; this.code = message.code; } } function checkOS(res) { if (res.os == null || res.os == undefined) return "-"; const Desktop = [ "AIX", "Amiga OS", "Arch", "BeOS", "CentOS", "Chromium OS", "Contiki", "Fedora", "FreeBSD", "Debian", "Deepin", "DragonFly", "elementary OS", "Gentoo", "GhostBSD", "GNU", "Haiku", "HP-UX", "Hurd", "Joli", "Linpus", "Linux", "Linspire", "Mageia", "Mandriva", "Manjaro", "MeeGo", "Minix", "Mint", "Morph OS", "NetBSD", "OpenBSD", "OpenVMS", "OS/2", "PC-BSD", "PCLinuxOS", "Plan9", "RedHat", "RISC OS", "Sabayon", "SerenityOS", "Slackware", "Solaris", "SUSE", "Ubuntu", "VectorLinux", "Zenwalk", ]; const mobile = [ "Android", "Android-x86", "Bada", "BlackBerry", "Firefox OS", "Fuchsia", "HarmonyOS", "iOS", "KaiOS", "Maemo", "QNX", "RIM Tablet OS", "Sailfish", "Series40", "Symbian", "Tizen", "WebOS", "Windows Phone", "Windows Mobile", ]; if (Desktop.includes(res.os.name)) { return 0; } else if (mobile.includes(res.os.name)) { return 6; } else { return 7; } } const { Identification, Print, Audio, AudioData, Private, FragmentShader, Rasterizer, FrameBuffer, WebGLData, VertexShader, WebGLContextInfo, Response, } = require("./bundle.js"); var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function () { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [], }, f, y, t, g; return ( (g = { next: verb(0), throw: verb(1), return: verb(2) }), typeof Symbol === "function" && (g[Symbol.iterator] = function () { return this; }), g ); function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while ((g && ((g = 0), op[0] && (_ = 0)), _)) try { if ( ((f = 1), y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) ) return t; if (((y = 0), t)) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if ( !((t = _.trys), (t = t.length > 0 && t[t.length - 1])) && (op[0] === 6 || op[0] === 2) ) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; function detectIncognito() { return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) { switch (_a.label) { case 0: return [ 4 /*yield*/, new Promise(function (resolve, reject) { var browserName = "Unknown"; function __callback(isPrivate) { resolve({ isPrivate: isPrivate, browserName: browserName, }); } function identifyChromium() { var ua = navigator.userAgent; if (ua.match(/Chrome/)) { if (navigator.brave !== undefined) { return "Brave"; } else if (ua.match(/Edg/)) { return "Edge"; } else if (ua.match(/OPR/)) { return "Opera"; } return "Chrome"; } else { return "Chromium"; } } function assertEvalToString(value) { return value === eval.toString().length; } function isSafari() { var v = navigator.vendor; return ( v !== undefined && v.indexOf("Apple") === 0 && assertEvalToString(37) ); } function isChrome() { var v = navigator.vendor; return ( v !== undefined && v.indexOf("Google") === 0 && assertEvalToString(33) ); } function isFirefox() { return ( document.documentElement !== undefined && document.documentElement.style.MozAppearance !== undefined && assertEvalToString(37) ); } function isMSIE() { return ( navigator.msSaveBlob !== undefined && assertEvalToString(39) ); } /** * Safari (Safari for iOS & macOS) **/ function newSafariTest() { var tmp_name = String(Math.random()); try { var db = window.indexedDB.open(tmp_name, 1); db.onupgradeneeded = function (i) { var _a, _b; var res = (_a = i.target) === null || _a === void 0 ? void 0 : _a.result; try { res .createObjectStore("test", { autoIncrement: true, }) .put(new Blob()); __callback(false); } catch (e) { var message = e; if (e instanceof Error) { message = (_b = e.message) !== null && _b !== void 0 ? _b : e; } if (typeof message !== "string") { __callback(false); return; } var matchesExpectedError = message.includes( "BlobURLs are not yet supported", ); __callback(matchesExpectedError); return; } finally { res.close(); window.indexedDB.deleteDatabase(tmp_name); } }; } catch (e) { __callback(false); } } function oldSafariTest() { var openDB = window.openDatabase; var storage = window.localStorage; try { openDB(null, null, null, null); } catch (e) { __callback(true); return; } try { storage.setItem("test", "1"); storage.removeItem("test"); } catch (e) { __callback(true); return; } __callback(false); } function safariPrivateTest() { if (navigator.maxTouchPoints !== undefined) { newSafariTest(); } else { oldSafariTest(); } } /** * Chrome **/ function getQuotaLimit() { var w = window; if ( w.performance !== undefined && w.performance.memory !== undefined && w.performance.memory.jsHeapSizeLimit !== undefined ) { return performance.memory.jsHeapSizeLimit; } return 1073741824; } // >= 76 function storageQuotaChromePrivateTest() { navigator.webkitTemporaryStorage.queryUsageAndQuota( function (_, quota) { var quotaInMib = Math.round(quota / (1024 * 1024)); var quotaLimitInMib = Math.round(getQuotaLimit() / (1024 * 1024)) * 2; __callback(quotaInMib < quotaLimitInMib); }, function (e) { reject( new Error( "detectIncognito somehow failed to query storage quota: " + e.message, ), ); }, ); } // 50 to 75 function oldChromePrivateTest() { var fs = window.webkitRequestFileSystem; var success = function () { __callback(false); }; var error = function () { __callback(true); }; fs(0, 1, success, error); } function chromePrivateTest() { if (Promise !== undefined && Promise.allSettled !== undefined) { storageQuotaChromePrivateTest(); } else { oldChromePrivateTest(); } } /** * Firefox **/ function firefoxPrivateTest() { __callback(navigator.serviceWorker === undefined); } /** * MSIE **/ function msiePrivateTest() { __callback(window.indexedDB === undefined); } function main() { if (isSafari()) { browserName = "Safari"; safariPrivateTest(); } else if (isChrome()) { browserName = identifyChromium(); chromePrivateTest(); } else if (isFirefox()) { browserName = "Firefox"; firefoxPrivateTest(); } else if (isMSIE()) { browserName = "Internet Explorer"; msiePrivateTest(); } else { reject( new Error("detectIncognito cannot determine the browser"), ); } } main(); }), ]; case 1: return [2 /*return*/, _a.sent()]; } }); }); } (function (name, context, definition) { "use strict"; if (typeof module !== "undefined" && module.exports) { module.exports = definition(); } else if (typeof define === "function" && define.amd) { define(definition); } else { context[name] = definition(); } })("DeviceID", this, function () { "use strict"; // for IE8 and older if (!Array.prototype.indexOf) { Array.prototype.indexOf = function (searchElement, fromIndex) { var k; if (this == null) { throw new TypeError("'this' is null or undefined"); } var O = Object(this); var len = O.length >>> 0; if (len === 0) { return -1; } var n = +fromIndex || 0; if (Math.abs(n) === Infinity) { n = 0; } if (n >= len) { return -1; } k = Math.max(n >= 0 ? n : len - Math.abs(n), 0); while (k < len) { if (k in O && O[k] === searchElement) { return k; } k++; } return -1; }; } var DeviceID = function (options) { this.loaded = ""; // loaded token for communication this.stored_id = ""; // visit identifier for TLS this.old = null; // stored localStorage ID this.cookie = null; // stored cookie ID this.url = "https://api.deviceid.io"; //this.url = 'https://api.deviceid.io'; this.options = options; this.uap = new UAParser(); this.tokenErrorCount = 0; this.iv = CryptoJS.enc.Utf8.parse("c7VEVapazCwNVcWgi1Ej"); }; DeviceID.prototype = { load: async function (done) { try { this.stored_id = localStorage.getItem("deviceID_identifier"); if ( this.stored_id == null || this.stored_id == undefined || this.stored_id.length != 20 ) { this.stored_id = this.makeid(20); localStorage.setItem("deviceID_identifier", this.stored_id); } // return done(new Promise(async (resolve, reject) => { if (typeof this.options === "object") { if (!("apiKey" in this.options)) { return done( false, new DeviceIDError({ type: "LOAD ERROR", code: 0, val: "NO API KEY PROVIDED", }), ); // return done(false); } else if (!("secret" in this.options)) { return done( false, new DeviceIDError({ type: "LOAD ERROR", code: 1, val: "NO SECRET KEY PROVIDED", }), ); // return done(false); } } else { return done( false, new DeviceIDError({ type: "LOAD ERROR", code: 2, val: "NO DATA PROVIDED", }), ); // return done(false); } const xhr1 = new XMLHttpRequest(); xhr1.open("POST", "https://api.deviceid.io:3005/index.json"); xhr1.setRequestHeader("Content-Type", "text/plain"); xhr1.send(JSON.stringify({ id: this.stored_id })); const xhr = new XMLHttpRequest(); xhr.onreadystatechange = () => { if (xhr.readyState === XMLHttpRequest.DONE) { if (xhr.status === 200) { try { this.loaded = CryptoJS.AES.decrypt( xhr.responseText, this.options.secret, ).toString(CryptoJS.enc.Utf8); } catch (e) { return done( false, new DeviceIDError({ code: 10, val: "SECRET KEY DECRYPTION FAILED", type: "LOAD ERROR", }), ); } // this.key = this.options.secret; if (typeof Storage !== "undefined") { this.old = localStorage.getItem("c:GkK?_5eVdQdiQT0Fb?"); } if (navigator.cookieEnabled) { this.cookieStored = this.getCookie("-BAL4_z*-wQ=6TYqCA!U"); } return done(true, null); } else { try { const dt = xhr.responseText.split(":"); return done( false, new DeviceIDError({ type: "LOAD ERROR", code: dt[1], val: dt[0], }), ); } catch (err) { return done( false, new DeviceIDError({ type: "LOAD ERROR", code: xhr.status, val: xhr.responseText, }), ); } } } }; xhr.open( "GET", "https://api.deviceid.io/load?key=" + encodeURIComponent(this.options.apiKey), ); xhr.send(); try { detectIncognito().then((res) => { this.prv = res; }); } catch (e) { this.prv = { isPrivate: false, browserName: "-", }; } this.device = this.device(); } catch (e) { return done( false, new DeviceIDError({ code: 99, val: e, type: "Load Error" }), ); } }, id: async function (done) { try { var obj = undefined; var str = (typeof this.old === "string" && this.old.length > 0) || (typeof this.cookieStored === "string" && this.cookieStored.length > 0); const start = performance.now(); if (!str) { const audioData = await this.getAudio(this); obj = { a: this.fonts(), b: this.cryptoSupport(), d: this.blending(), i: this.osCpu(), j: this.getLanguages(), k: window.screen.colorDepth, l: navigator.deviceMemory, m: window.screen.width + "x" + window.screen.height, c: [window.screen.availHeight, window.screen.availWidth], n: this.getHardwareConcurrency(), o: new Date().getTimezoneOffset(), t: this.getNavigatorCpuClass(), v: this.getMimeTypes(), w: this.getCanvas(), x: this.getTouchSupport(), bb: this.colorGamut(), cc: this.invertedColors(), dd: this.forcedColors(), ee: this.monochrome(), ff: this.contrast(), gg: this.reducedMotion(), hh: this.hdr(), ii: this.getMathsConstants(), jj: await this.webGLParameters(), ll: this.arch(), b0: this.reducedTransparency(), b2: this.webGL(), }; obj["zz"] = this.x64hash128(JSON.stringify(obj), 31); /* const mini_print = { a: obj.a, d: obj.g, f: obj.ii, h: obj.b2, }; obj["b1"] = this.x64hash128(JSON.stringify(mini_print), 31); */ obj["b1"] = obj.jj[0].gpuHash + "&" + obj.jj[1].gpuHash; obj["a2"] = navigator.appCodeName; obj["a3"] = navigator.appName; obj["a4"] = navigator.appVersion; obj["a5"] = navigator.product; // 1, 1 obj["a6"] = navigator.productSub; // 1, 2 obj["a7"] = this.getNavigatorPrototype(); // 1, 4 obj["a9"] = navigator.buildID; obj["kk"] = navigator.pdfViewerEnabled; obj["aa"] = navigator.cookieEnabled; obj["p"] = this.sessionStorage(); obj["q"] = this.localStorage(); obj["r"] = this.indexedDB(); obj["s"] = Boolean(window.openDatabase); obj["ua"] = navigator.userAgent.split(" ").join(""); obj["y"] = navigator.vendor; obj["z"] = navigator.vendorSub; audioData.vc = AudioData.fromObject({ a: audioData.vc["ac-baseLatency"], b: audioData.vc["ac-channelCount"], c: audioData.vc["ac-channelCountMode"], d: audioData.vc["ac-channelInterpretation"], e: audioData.vc["ac-maxChannelCount"], f: audioData.vc["ac-numberOfInputs"], g: audioData.vc["ac-numberOfOutputs"], h: audioData.vc["ac-outputLatency"], i: audioData.vc["ac-sampleRate"], j: audioData.vc["ac-sinkId"], k: audioData.vc["ac-state"], l: audioData.vc["an-channelCount"], m: audioData.vc["an-channelCountMode"], n: audioData.vc["an-channelInterpretation"], o: audioData.vc["an-fftSize"], p: audioData.vc["an-frequencyBinCount"], q: audioData.vc["an-maxDecibels"], r: audioData.vc["an-minDecibels"], s: audioData.vc["an-numberOfInputs"], t: audioData.vc["an-numberOfOutputs"], u: audioData.vc["an-smoothingTimeConstant"], }); const msg = Audio.fromObject(audioData); obj["g"] = msg; } const end = performance.now(); var res = !str ? { tls: this.stored_id, dev: this.device, url: window.location.href, platform: {}, private: this.prv, print: Print.fromObject(obj), clientTiming: end - start, local: obj.q, } : { tls: this.stored_id, dev: this.device, url: window.location.href, platform: {}, private: this.prv, print: null, clientTiming: end - start, local: this.localStorage(), old: this.old, cookie: this.cookieStored, }; if (this.options != undefined) { if (this.options.request_id != undefined) { res["id"] = params.request_id; } if (this.options.data != undefined) { res["tag"] = JSON.stringify(params.data); } } const message = Identification.fromObject(res); const buffer = Identification.encode(message).finish(); const resp = await fetch(this.url + "/id", { method: "POST", body: buffer, headers: { Authorization: "Bearer " + this.loaded }, }); if (resp.status === 444) { if (this.tokenErrorCount > 2) { return new DeviceIDError({ code: 201, type: "ID Error", val: "AUTH TOKEN PRESISTED AFTER MULTIPLE FAILURES", }); } else this.tokenErrorCount++; localStorage.removeItem("c:GkK?_5eVdQdiQT0Fb?"); this.setCookie("-BAL4_z*-wQ=6TYqCA!U", "", { secure: true, expires: 3600, }); this.old = null; this.cookieStored = null; this.id(function (res) { return done(res, null); }); } else if (resp.status !== 200) return done( null, new DeviceIDError({ code: resp.status, val: resp.responseText, type: "ID Error", }), ); const timing = performance.now() - end; const msg1 = Response.decode(new Uint8Array(await resp.arrayBuffer())); const parsed = Response.toObject(msg1); if (parsed.j != null) { localStorage.setItem("c:GkK?_5eVdQdiQT0Fb?", parsed["j"]); this.setCookie("-BAL4_z*-wQ=6TYqCA!U", parsed["j"], { secure: true, expires: 3600, }); } parsed.visit_id = parsed.a; parsed.device_id = parsed.b; parsed.device_found = parsed.c; parsed.threat_level = parsed.e; parsed.confidence = parsed.f; parsed.tempered = parsed.g; parsed.blocked = parsed.h; parsed.ip = parsed.i; delete parsed.j; delete parsed.a; delete parsed.b; delete parsed.c; delete parsed.e; delete parsed.f; delete parsed.g; delete parsed.h; delete parsed.i; parsed["private"] = this.prv; parsed["platform"] = this.uapRes; parsed["adblock"] = !str ? obj.c : false; parsed["dev"] = this.device; setTimeout(() => { const xhr1 = new XMLHttpRequest(); xhr1.open("POST", this.url + "/updateTime"); xhr1.setRequestHeader("Content-Type", "text/plain"); xhr1.send(JSON.stringify({ timing, visit_id: parsed["visit_id"] })); }, 1); return done(parsed, null); //xhr.send(buffer); } catch (e) { return done( null, new DeviceIDError({ code: 20, val: e, type: "ID Error" }), ); } }, device: function () { this.uapRes = this.uap.getResult(); var device = 0; const arch = this.uapRes.cpu.architecture; if ( (this.uapRes.os != undefined && this.uapRes.os != null && this.uapRes.os.name == "macOS") || this.uapRes.device.model == "Macintosh" ) { device = 1; } else if ( this.uapRes.device != null && this.uapRes.device != undefined && this.uapRes.device.vendor == "Apple" ) { if (this.uapRes.os.name == "iOS") { device = 3; } else if (uap.os.name == "watchOS") { device = 2; } else { device = 1; } } else if ( arch == "amd64" || arch == "ia32" || arch == "ia64" || arch == "pa-risc" || arch == "sparc" || arch == "sparch64" ) { // Desktop device = 0; } else if (arch == "68k") { // mobile device = 6; } else if (arch == "arm64") { // ipad / iphone device = 3; } else if (arch == "ppc") { // mac device = 1; } else if ( arch == "avr" || arch == "armhf" || arch == "irix" || arch == "irix64" || arch == "mips" || arch == "mips64" ) { // something weird device = 7; } else { // cpu arch undefined if (this.uapRes.device.type == undefined) { if (this.uapRes.os.name == undefined) { device = 7; } else { device = checkOS(this.uapRes); } } else { if (this.uapRes.device == null || this.uapRes.device == undefined) { device = 0; } else { const type = this.uapRes.device.type; if (type == "mobile") { device = 6; } else if (type == "tablet") { device = 5; } else if (type == "werable") { device = 2; } else if (this.uapRes.os != null && this.uapRes.os != undefined) { if ( this.uapRes.os == null || this.uapRes.os == undefined || this.uapRes.os.name == undefined ) { device = 7; } else { device = checkOS(this.uapRes); } } } } } return device; }, indexedDB: function () { try { return Boolean(window.indexedDB); } catch (e) { return true; } }, localStorage: function () { try { return Boolean(window.localStorage); } catch (e) { return true; } }, sessionStorage: function () { try { return Boolean(window.sessionStorage); } catch (e) { return true; } }, getNavigatorPrototype: function () { try { var obj = window.navigator; var protoNavigator = []; do Object.getOwnPropertyNames(obj).forEach(function (name) { protoNavigator.push(name); }); while ((obj = Object.getPrototypeOf(obj))); var res; var finalProto = []; protoNavigator.forEach(function (prop) { var objDesc = Object.getOwnPropertyDescriptor( Object.getPrototypeOf(navigator), prop, ); if (objDesc != undefined) { if (objDesc.value != undefined) { res = objDesc.value.toString(); } else if (objDesc.get != undefined) { res = objDesc.get.toString(); } } else { res = ""; } finalProto.push(prop + "~~~" + res); }); return finalProto.join(";;;"); } catch (e) { return ""; } }, webGL: function () { var canvas, ctx, width = 256, height = 128; canvas = document.createElement("canvas"); (canvas.width = width), (canvas.height = height), (ctx = canvas.getContext("webgl2") || canvas.getContext("experimental-webgl2") || canvas.getContext("webgl") || canvas.getContext("experimental-webgl") || canvas.getContext("moz-webgl")); if (ctx == null || ctx == undefined) return ""; try { var f = "attribute vec2 attrVertex;varying vec2 varyinTexCoordinate;uniform vec2 uniformOffset;void main(){varyinTexCoordinate=attrVertex+uniformOffset;gl_Position=vec4(attrVertex,0,1);}"; var g = "precision mediump float;varying vec2 varyinTexCoordinate;void main() {gl_FragColor=vec4(varyinTexCoordinate,0,1);}"; var h = ctx.createBuffer(); ctx.bindBuffer(ctx.ARRAY_BUFFER, h); var i = new Float32Array([-0.2, -0.9, 0, 0.4, -0.26, 0, 0, 0.7321, 0]); ctx.bufferData(ctx.ARRAY_BUFFER, i, ctx.STATIC_DRAW), (h.itemSize = 3), (h.numItems = 3); var j = ctx.createProgram(); var k = ctx.createShader(ctx.VERTEX_SHADER); ctx.shaderSource(k, f); ctx.compileShader(k); var l = ctx.createShader(ctx.FRAGMENT_SHADER); ctx.shaderSource(l, g); ctx.compileShader(l); ctx.attachShader(j, k); ctx.attachShader(j, l); ctx.linkProgram(j); ctx.useProgram(j); j.vertexPosAttrib = ctx.getAttribLocation(j, "attrVertex"); j.offsetUniform = ctx.getUniformLocation(j, "uniformOffset"); ctx.enableVertexAttribArray(j.vertexPosArray); ctx.vertexAttribPointer( j.vertexPosAttrib, h.itemSize, ctx.FLOAT, !1, 0, 0, ); ctx.uniform2f(j.offsetUniform, 1, 1); ctx.drawArrays(ctx.TRIANGLE_STRIP, 0, h.numItems); } catch (e) {} var m = ""; var n = new Uint8Array(width * height * 4); ctx.readPixels(0, 0, width, height, ctx.RGBA, ctx.UNSIGNED_BYTE, n); // m = JSON.stringify(n).replace(/,?"[0-9]+":/g, ""); m = JSON.stringify(n); // console.log(m); return this.x64hash128(m, 31); }, arch: function () { const f = new Float32Array(1); const u = new Uint8Array(f.buffer); f[0] = Infinity / Infinity; return u[3]; }, webGLParameters: async function () { // Based on and inspired by https://github.com/CesiumGS/webglreport // https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Constants const WebGLConstants = [ "ALIASED_LINE_WIDTH_RANGE", "ALIASED_POINT_SIZE_RANGE", "ALPHA_BITS", "BLUE_BITS", "DEPTH_BITS", "GREEN_BITS", "MAX_COMBINED_TEXTURE_IMAGE_UNITS", "MAX_CUBE_MAP_TEXTURE_SIZE", "MAX_FRAGMENT_UNIFORM_VECTORS", "MAX_RENDERBUFFER_SIZE", "MAX_TEXTURE_IMAGE_UNITS", "MAX_TEXTURE_SIZE", "MAX_VARYING_VECTORS", "MAX_VERTEX_ATTRIBS", "MAX_VERTEX_TEXTURE_IMAGE_UNITS", "MAX_VERTEX_UNIFORM_VECTORS", "MAX_VIEWPORT_DIMS", "RED_BITS", "RENDERER", "SHADING_LANGUAGE_VERSION", "STENCIL_BITS", "VERSION", ]; const WebGL2Constants = [ "MAX_VARYING_COMPONENTS", "MAX_VERTEX_UNIFORM_COMPONENTS", "MAX_VERTEX_UNIFORM_BLOCKS", "MAX_VERTEX_OUTPUT_COMPONENTS", "MAX_PROGRAM_TEXEL_OFFSET", "MAX_3D_TEXTURE_SIZE", "MAX_ARRAY_TEXTURE_LAYERS", "MAX_COLOR_ATTACHMENTS", "MAX_COMBINED_FRAGMENT_UNIFORM_COMPONENTS", "MAX_COMBINED_UNIFORM_BLOCKS", "MAX_COMBINED_VERTEX_UNIFORM_COMPONENTS", "MAX_DRAW_BUFFERS", "MAX_ELEMENT_INDEX", "MAX_FRAGMENT_INPUT_COMPONENTS", "MAX_FRAGMENT_UNIFORM_COMPONENTS", "MAX_FRAGMENT_UNIFORM_BLOCKS", "MAX_SAMPLES", "MAX_SERVER_WAIT_TIMEOUT", "MAX_TEXTURE_LOD_BIAS", "MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS", "MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS", "MAX_TRANSFORM_FEEDBACK_SEPARATE_COMPONENTS", "MAX_UNIFORM_BLOCK_SIZE", "MAX_UNIFORM_BUFFER_BINDINGS", "MIN_PROGRAM_TEXEL_OFFSET", "UNIFORM_BUFFER_OFFSET_ALIGNMENT", ]; const Categories = { uniformBuffers: [ "MAX_UNIFORM_BUFFER_BINDINGS", "MAX_UNIFORM_BLOCK_SIZE", "UNIFORM_BUFFER_OFFSET_ALIGNMENT", "MAX_COMBINED_FRAGMENT_UNIFORM_COMPONENTS", "MAX_COMBINED_UNIFORM_BLOCKS", "MAX_COMBINED_VERTEX_UNIFORM_COMPONENTS", ], debugRendererInfo: ["UNMASKED_VENDOR_WEBGL", "UNMASKED_RENDERER_WEBGL"], fragmentShader: [ "MAX_FRAGMENT_UNIFORM_VECTORS", "MAX_TEXTURE_IMAGE_UNITS", "MAX_FRAGMENT_INPUT_COMPONENTS", "MAX_FRAGMENT_UNIFORM_COMPONENTS", "MAX_FRAGMENT_UNIFORM_BLOCKS", "FRAGMENT_SHADER_BEST_FLOAT_PRECISION", "MIN_PROGRAM_TEXEL_OFFSET", "MAX_PROGRAM_TEXEL_OFFSET", ], frameBuffer: [ "MAX_DRAW_BUFFERS", "MAX_COLOR_ATTACHMENTS", "MAX_SAMPLES", "RGBA_BITS", "DEPTH_STENCIL_BITS", "MAX_RENDERBUFFER_SIZE", "MAX_VIEWPORT_DIMS", ], rasterizer: ["ALIASED_LINE_WIDTH_RANGE", "ALIASED_POINT_SIZE_RANGE"], textures: [ "MAX_TEXTURE_SIZE", "MAX_CUBE_MAP_TEXTURE_SIZE", "MAX_COMBINED_TEXTURE_IMAGE_UNITS", "MAX_TEXTURE_MAX_ANISOTROPY_EXT", "MAX_3D_TEXTURE_SIZE", "MAX_ARRAY_TEXTURE_LAYERS", "MAX_TEXTURE_LOD_BIAS", ], transformFeedback: [ "MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS", "MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS", "MAX_TRANSFORM_FEEDBACK_SEPARATE_COMPONENTS", ], vertexShader: [ "MAX_VARYING_VECTORS", "MAX_VERTEX_ATTRIBS", "MAX_VERTEX_TEXTURE_IMAGE_UNITS", "MAX_VERTEX_UNIFORM_VECTORS", "MAX_VERTEX_UNIFORM_COMPONENTS", "MAX_VERTEX_UNIFORM_BLOCKS", "MAX_VERTEX_OUTPUT_COMPONENTS", "MAX_VARYING_COMPONENTS", "VERTEX_SHADER_BEST_FLOAT_PRECISION", ], webGLContextInfo: [ "CONTEXT", "ANTIALIAS", "DIRECT_3D", "MAJOR_PERFORMANCE_CAVEAT", "RENDERER", "SHADING_LANGUAGE_VERSION", "VERSION", ], }; /* parameter helpers */ // https://developer.mozilla.org/en-US/docs/Web/API/EXT_texture_filter_anisotropic const getMaxAnisotropy = (context) => { try { const extension = context.getExtension("EXT_texture_filter_anisotropic") || context.getExtension("WEBKIT_EXT_texture_filter_anisotropic") || context.getExtension("MOZ_EXT_texture_filter_anisotropic"); return context.getParameter(extension.MAX_TEXTURE_MAX_ANISOTROPY_EXT); } catch (error) { console.error(error); return undefined; } }; // https://developer.mozilla.org/en-US/docs/Web/API/WEBGL_draw_buffers const getMaxDrawBuffers = (context) => { try { const extension = context.getExtension("WEBGL_draw_buffers") || context.getExtension("WEBKIT_WEBGL_draw_buffers") || context.getExtension("MOZ_WEBGL_draw_buffers"); return context.getParameter(extension.MAX_DRAW_BUFFERS_WEBGL); } catch (error) { return undefined; } }; // https://developer.mozilla.org/en-US/docs/Web/API/WebGLShaderPrecisionFormat/precision // https://developer.mozilla.org/en-US/docs/Web/API/WebGLShaderPrecisionFormat/rangeMax // https://developer.mozilla.org/en-US/docs/Web/API/WebGLShaderPrecisionFormat/rangeMin const getShaderData = (shader) => { const shaderData = {}; try { for (const prop in shader) { const shaderPrecisionFormat = shader[prop]; shaderData[prop] = { precision: shaderPrecisionFormat.precision, rangeMax: shaderPrecisionFormat.rangeMax, rangeMin: shaderPrecisionFormat.rangeMin, }; } return shaderData; } catch (error) { return undefined; } }; // https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/getShaderPrecisionFormat const getShaderPrecisionFormat = (context, shaderType) => { const props = ["LOW_FLOAT", "MEDIUM_FLOAT", "HIGH_FLOAT"]; const precisionFormat = {}; try { props.forEach((prop) => { precisionFormat[prop] = context.getShaderPrecisionFormat( context[shaderType], context[prop], ); return; }); return precisionFormat; } catch (error) { return undefined; } }; // https://developer.mozilla.org/en-US/docs/Web/API/WEBGL_debug_renderer_info const getUnmasked = (context, constant) => { try { const extension = context.getExtension("WEBGL_debug_renderer_info"); const unmasked = context.getParameter(extension[constant]); return unmasked; } catch (error) { return undefined; } }; // Takes the parameter object and generate a fingerprint of sorted numeric values function getNumericValues(parameters) { if (!parameters) return; return [ ...new Set( Object.values(parameters) .filter((val) => val && typeof val != "string") .flat() .map((val) => Number(val) || 0), ), ].sort((a, b) => a - b); } // Highlight common GPU brands function getGpuBrand(gpu) { if (!gpu) return; const gpuBrandMatcher = /(adreno|amd|apple|intel|llvm|mali|microsoft|nvidia|parallels|powervr|samsung|swiftshader|virtualbox|vmware)/i; const brand = /radeon/i.test(gpu) ? "AMD" : /geforce/i.test(gpu) ? "NVIDIA" : (gpuBrandMatcher.exec(gpu) || [])[0] || "Other"; return brand; } /* get WebGLRenderingContext or WebGL2RenderingContext */ // https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext // https://developer.mozilla.org/en-US/docs/Web/API/WebGL2RenderingContext function getWebGL(contextType) { const errors = []; let data = {}; const isWebGL = /^(experimental-)?webgl$/; const isWebGL2 = /^(experimental-)?webgl2$/; const supportsWebGL = isWebGL.test(contextType) && "WebGLRenderingContext" in window; const supportsWebGL2 = isWebGL2.test(contextType) && "WebGLRenderingContext" in window; // detect support if (!supportsWebGL && !supportsWebGL2) { errors.push("not supported"); return [data, errors]; } // get canvas context let canvas; let context; let hasMajorPerformanceCaveat; try { canvas = document.createElement("canvas"); context = canvas.getContext(contextType, { failIfMajorPerformanceCaveat: true, }); if (!context) { hasMajorPerformanceCaveat = true; context = canvas.getContext(contextType); if (!context) { throw new Error(`context of type ${typeof context}`); } } } catch (err) { console.error(err); errors.push("context blocked"); return [data, errors]; } // get supported extensions // https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/getSupportedExtensions // https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Using_Extensions let webGLExtensions; try { webGLExtensions = context.getSupportedExtensions(); } catch (error) { console.error(error); errors.push("extensions blocked"); } // get parameters let parameters; try { const VERTEX_SHADER = getShaderData( getShaderPrecisionFormat(context, "VERTEX_SHADER"), ); const FRAGMENT_SHADER = getShaderData( getShaderPrecisionFormat(context, "FRAGMENT_SHADER"), ); parameters = { ANTIALIAS: context.getContextAttributes().antialias, CONTEXT: contextType, MAJOR_PERFORMANCE_CAVEAT: hasMajorPerformanceCaveat, MAX_TEXTURE_MAX_ANISOTROPY_EXT: getMaxAnisotropy(context), MAX_DRAW_BUFFERS_WEBGL: getMaxDrawBuffers(context), VERTEX_SHADER, VERTEX_SHADER_BEST_FLOAT_PRECISION: Object.values( VERTEX_SHADER.HIGH_FLOAT, ), FRAGMENT_SHADER, FRAGMENT_SHADER_BEST_FLOAT_PRECISION: Object.values( FRAGMENT_SHADER.HIGH_FLOAT, ), UNMASKED_VENDOR_WEBGL: getUnmasked( context, "UNMASKED_VENDOR_WEBGL", ), UNMASKED_RENDERER_WEBGL: getUnmasked( context, "UNMASKED_RENDERER_WEBGL", ), }; const glConstants = [ ...WebGLConstants, ...(supportsWebGL2 ? WebGL2Constants : []), ]; glConstants.forEach((key) => { const result = context.getParameter(context[key]); const typedArray = result && (result.constructor === Float32Array || result.constructor === Int32Array); parameters[key] = typedArray ? [...result] : result; }); parameters.RGBA_BITS = [ parameters.RED_BITS, parameters.GREEN_BITS, parameters.BLUE_BITS, parameters.ALPHA_BITS, ]; parameters.DEPTH_STENCIL_BITS = [ parameters.DEPTH_BITS, parameters.STENCIL_BITS, ]; parameters.DIRECT_3D = /Direct3D|D3D(\d+)/.test( parameters.UNMASKED_RENDERER_WEBGL, ); } catch (error) { console.error(error); errors.push("parameters blocked"); } const gpu = String([ parameters.UNMASKED_VENDOR_WEBGL, parameters.UNMASKED_RENDERER_WEBGL, ]); const gpuBrand = getGpuBrand(gpu); // Structure parameter data let components = {}; if (parameters) { Object.keys(Categories).forEach((name) => { const componentData = Categories[name].reduce((acc, key) => { if (parameters[key] !== undefined) { acc[key] = parameters[key]; } return acc; }, {}); // Only compile if the data exists if (Object.keys(componentData).length) { components[name] = componentData; } }); } data = { gpuHash: !parameters ? undefined : [gpuBrand, ...getNumericValues(parameters)].join(":"), gpu, gpuBrand, ...components, webGLExtensions, }; return [data, errors]; } const value = await Promise.all([ getWebGL("webgl"), getWebGL("webgl2"), getWebGL("experimental-webgl"), ]) .then((response) => { const [webGL, webGL2, experimentalWebGL] = response; // Extract both data and errors const [webGLData, webGLErrors] = webGL; const [webGL2Data, webGL2Errors] = webGL2; const [experimentalWebGLData, experimentalWebGLErrors] = experimentalWebGL; // Show the data /* console.log('WebGLRenderingContext: ', webGLData) console.log('WebGL2RenderingContext: ', webGL2Data) console.log('Experimental: ', experimentalWebGLData) */ // return(XXH64(JSON.stringify([webGLData, webGL2Data, experimentalWebGLData]), 0xA3FC ).toString(16)); webGLData.fragmentShader = FragmentShader.fromObject({ a: webGLData.fragmentShader.FRAGMENT_SHADER_BEST_FLOAT_PRECISION, b: webGLData.fragmentShader.MAX_FRAGMENT_UNIFORM_VECTORS, c: webGLData.fragmentShader.MAX_TEXTURE_IMAGE_UNITS, }); webGLData.frameBuffer = FrameBuffer.fromObject({ a: webGLData.frameBuffer.DEPTH_STENCIL_BITS, b: webGLData.frameBuffer.MAX_RENDERBUFFER_SIZE, c: webGLData.frameBuffer.MAX_VIEWPORT_DIMS, d: webGLData.frameBuffer.RGBA_BITS, }); webGLData.rasterizer = Rasterizer.fromObject({ a: webGLData.rasterizer.ALIASED_LINE_WIDTH_RANGE, b: webGLData.rasterizer.ALIASED_POINT_SIZE_RANGE, }); webGLData.vertexShader = VertexShader.fromObject({ a: webGLData.vertexShader.MAX_VARYING_VECTORS, b: webGLData.vertexShader.MAX_VERTEX_ATTRIBS, c: webGLData.vertexShader.MAX_VERTEX_TEXTURE_IMAGE_UNITS, d: webGLData.vertexShader.MAX_VERTEX_UNIFORM_VECTORS, e: webGLData.vertexShader.VERTEX_SHADER_BEST_FLOAT_PRECISION, }); webGLData.webGLContextInfo = WebGLContextInfo.fromObject({ a: webGLData.webGLContextInfo.ANTIALIAS, b: webGLData.webGLContextInfo.CONTEXT, c: webGLData.webGLContextInfo.DIRECT_3D, d: webGLData.webGLContextInfo.RENDERER, e: webGLData.webGLContextInfo.SHADING_LANGUAGE_VERSION, f: webGLData.webGLContextInfo.VERSION, }); const msg = WebGLData.fromObject(webGLData); webGL2Data.fragmentShader = FragmentShader.fromObject({ a: webGL2Data.fragmentShader.FRAGMENT_SHADER_BEST_FLOAT_PRECISION, b: webGL2Data.fragmentShader.MAX_FRAGMENT_UNIFORM_VECTORS, c: webGL2Data.fragmentShader.MAX_TEXTURE_IMAGE_UNITS, d: webGL2Data.fragmentShader.MAX_FRAGMENT_INPUT_COMPONENTS, e: webGL2Data.fragmentShader.MAX_FRAGMENT_UNIFORM_BLOCKS, f: webGL2Data.fragmentShader.MAX_FRAGMENT_UNIFORM_COMPONENTS, g: webGL2Data.fragmentShader.MAX_PROGRAM_TEXEL_OFFSET, h: webGL2Data.fragmentShader.MIN_PROGRAM_TEXEL_OFFSET, }); webGL2Data.frameBuffer = FrameBuffer.fromObject({ a: webGL2Data.frameBuffer.DEPTH_STENCIL_BITS, b: webGL2Data.frameBuffer.MAX_RENDERBUFFER_SIZE, c: webGL2Data.frameBuffer.MAX_VIEWPORT_DIMS, d: webGL2Data.frameBuffer.RGBA_BITS, e: webGL2Data.frameBuffer.MAX_COLOR_ATTACHMENTS, f: webGL2Data.frameBuffer.MAX_DRAW_BUFFERS, g: webGL2Data.frameBuffer.MAX_SAMPLES, }); webGL2Data.rasterizer = Rasterizer.fromObject({ a: webGL2Data.rasterizer.ALIASED_LINE_WIDTH_RANGE, b: webGL2Data.rasterizer.ALIASED_POINT_SIZE_RANGE, }); webGL2Data.vertexShader = VertexShader.fromObject({ a: webGL2Data.vertexShader.MAX_VARYING_VECTORS, b: webGL2Data.vertexShader.MAX_VERTEX_ATTRIBS, c: webGL2Data.vertexShader.MAX_VERTEX_TEXTURE_IMAGE_UNITS, d: webGL2Data.vertexShader.MAX_VERTEX_UNIFORM_VECTORS, e: webGL2Data.vertexShader.VERTEX_SHADER_BEST_FLOAT_PRECISION, f: webGL2Data.vertexShader.MAX_VARYING_COMPONENTS, g: webGL2Data.vertexShader.MAX_VERTEX_OUTPUT_COMPONENTS, h: webGL2Data.vertexShader.MAX_VERTEX_TEXTURE_IMAGE_UNITS, i: webGL2Data.vertexShader.MAX_VERTEX_UNIFORM_BLOCKS, j: webGL2Data.vertexShader.MAX_VE