Changeset 3765708
- Timestamp:
- 10/26/2026 07:17:30 AM (26 hours ago)
- Location:
- spooflens-fraud-check
- Files:
-
- 8 edited
- 1 copied
-
tags/0.2.1 (copied) (copied from spooflens-fraud-check/trunk )
-
tags/0.2.1/assets/sl.js (modified) (6 diffs)
-
tags/0.2.1/includes/admin.php (modified) (1 diff)
-
tags/0.2.1/readme.txt (modified) (4 diffs)
-
tags/0.2.1/spooflens.php (modified) (2 diffs)
-
trunk/assets/sl.js (modified) (6 diffs)
-
trunk/includes/admin.php (modified) (1 diff)
-
trunk/readme.txt (modified) (4 diffs)
-
trunk/spooflens.php (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
spooflens-fraud-check/tags/0.2.1/assets/sl.js
r3765684 r3765708 330 330 } 331 331 332 // speech voices: the count, and which system the on-device voices come with (Microsoft ones with Windows, 333 // Apple ones with macOS and iOS); network voices (Google in desktop Chrome, "Online" in Edge) are left out. 334 // Only the counts leave the browser, not the names. 335 var APPLE_VOICES = /^(Samantha|Alex|Daniel|Karen|Moira|Tessa|Fiona|Fred|Victoria|Rishi|Veena|Thomas|Anna|Milena|Yuri|Amelie|Amélie|Kyoko|Ting-Ting)( |$)/; 336 function voiceSummary(list) { 337 var k = { win: 0, apple: 0, other: 0 }, names = []; 338 for (var i = 0; i < list.length; i++) { 339 var v = list[i]; 340 if (!v || v.localService !== true) continue; 341 var n = String(v.name || ''), u = String(v.voiceURI || ''); 342 names.push(n); 343 if (/^Microsoft /.test(n)) k.win++; 344 else if (/^com\.apple\./.test(u) || APPLE_VOICES.test(n)) k.apple++; 345 else k.other++; 346 } 347 // the names of the system's own voices, as one hash (0.18.0): the same in every browser of a device, and voices 348 // the owner downloaded tell two phones of one model apart; the names themselves are not sent 349 return { n: list.length, k: k, h: names.length ? hash(names.sort().join('|')) : '' }; 350 } 332 351 function voices() { 333 352 if (!window.speechSynthesis) return Promise.resolve(null); 334 353 var v = speechSynthesis.getVoices(); 335 if (v && v.length) return Promise.resolve(v .length);354 if (v && v.length) return Promise.resolve(voiceSummary(v)); 336 355 return timeout(new Promise(function (r) { 337 speechSynthesis.addEventListener('voiceschanged', function () { r(speechSynthesis.getVoices().length); }, { once: true }); 338 }), 800, 0); 356 speechSynthesis.addEventListener('voiceschanged', function () { r(voiceSummary(speechSynthesis.getVoices())); }, { once: true }); 357 }), 800, { n: 0, k: null }); 358 } 359 360 // the JavaScript engine words its own TypeError: V8 (Chromium), SpiderMonkey (Firefox), JavaScriptCore (Safari, every iOS browser) 361 function jsEngine() { 362 try { var o = null; o.x = 1; } catch (e) { 363 var m = String((e && e.message) || ''); 364 if (/Cannot set propert/.test(m)) return 'v8'; 365 if (/can't access property|has no properties|is null/.test(m)) return 'gecko'; 366 if (/is not an object/.test(m)) return 'jsc'; 367 return 'other'; 368 } 369 return ''; 370 } 371 372 // globals and document properties that automation frameworks leave behind 373 function autoTraces() { 374 var out = []; 375 ['__playwright__binding__', '__pwInitScripts', '_phantom', 'callPhantom', '__nightmare', 'domAutomation', 'domAutomationController', 376 '_selenium', 'callSelenium', '_Selenium_IDE_Recorder', '__webdriver_evaluate', '__selenium_evaluate', '__webdriver_script_fn', 377 '__driver_evaluate', '__fxdriver_evaluate', '__lastWatirAlert'].forEach(function (k) { 378 try { if (k in window) out.push(k); } catch (e) {} 379 }); 380 try { 381 Object.keys(document).forEach(function (k) { 382 if (/^\$?cdc_|^\$wdc_|^__(webdriver|selenium|fxdriver|driver)_/.test(k)) out.push('document.' + k); 383 }); 384 ['webdriver', 'selenium', 'driver'].forEach(function (a) { if (document.documentElement.hasAttribute(a)) out.push('html[' + a + ']'); }); 385 } catch (e) {} 386 return out.slice(0, 8); 339 387 } 340 388 … … 346 394 return o; 347 395 }), 1500, null); 396 } 397 398 /* ------------------------------------------------ device traits (0.18.0) --- */ 399 // What comes from the machine and the system rather than from the browser engine, so that two browsers of one 400 // device can be told apart from two devices (Chrome and Firefox on one computer share almost no other trait). 401 402 // WebGL limits and shader precision without the names: the graphics card and its driver. p: the limits and 403 // precision; e: the extension list (it differs more between engines, kept apart). 404 function glParams() { 405 var o = { p: '', e: '' }; 406 try { 407 var c = document.createElement('canvas'); 408 var gl = c.getContext('webgl') || c.getContext('experimental-webgl'); 409 if (!gl) return o; 410 var P = ['MAX_TEXTURE_SIZE', 'MAX_CUBE_MAP_TEXTURE_SIZE', 'MAX_RENDERBUFFER_SIZE', 'MAX_VERTEX_ATTRIBS', 'MAX_VERTEX_UNIFORM_VECTORS', 411 'MAX_FRAGMENT_UNIFORM_VECTORS', 'MAX_VARYING_VECTORS', 'MAX_COMBINED_TEXTURE_IMAGE_UNITS', 'MAX_TEXTURE_IMAGE_UNITS', 412 'MAX_VERTEX_TEXTURE_IMAGE_UNITS', 'MAX_VIEWPORT_DIMS', 'ALIASED_LINE_WIDTH_RANGE', 'ALIASED_POINT_SIZE_RANGE', 'SUBPIXEL_BITS']; 413 var out = P.map(function (k) { var v = gl.getParameter(gl[k]); return v && typeof v === 'object' && v.length !== undefined ? Array.prototype.join.call(v, 'x') : String(v); }); 414 ['VERTEX_SHADER', 'FRAGMENT_SHADER'].forEach(function (sh) { 415 ['LOW_FLOAT', 'MEDIUM_FLOAT', 'HIGH_FLOAT', 'LOW_INT', 'MEDIUM_INT', 'HIGH_INT'].forEach(function (pr) { 416 var x = gl.getShaderPrecisionFormat(gl[sh], gl[pr]); 417 out.push(x ? x.rangeMin + '/' + x.rangeMax + '/' + x.precision : '-'); 418 }); 419 }); 420 o.p = hash(out.join('|')); 421 o.e = hash((gl.getSupportedExtensions() || []).slice().sort().join(',')); 422 var lose = gl.getExtension('WEBGL_lose_context'); 423 if (lose) lose.loseContext(); 424 } catch (e) {} 425 return o; 426 } 427 428 // Scene 2.0: five small drawings, each leaning on another part of the graphics card (float maths, derivatives, 429 // blending, texture filtering, trigonometry at its edges). Browsers of one computer go through one driver, so 430 // our first scene already matched across engines; five give more to tell devices apart. Drawn twice: an 431 // unmodified browser gives the same pixels (noise = something rewrote them). 432 function scene2() { 433 var o = { h: '', parts: [], noise: false }; 434 try { 435 var c = document.createElement('canvas'); c.width = 32; c.height = 32; 436 var opt = { antialias: false, preserveDrawingBuffer: true }; 437 var gl = c.getContext('webgl', opt) || c.getContext('experimental-webgl', opt); 438 if (!gl) return o; 439 var hp = gl.getShaderPrecisionFormat && gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.HIGH_FLOAT); 440 var prec = 'precision ' + (hp && hp.precision > 0 ? 'highp' : 'mediump') + ' float;varying vec2 v;'; 441 var der = gl.getExtension('OES_standard_derivatives'); 442 var vs = 'attribute vec2 a;varying vec2 v;void main(){v=a*0.5+0.5;gl_Position=vec4(a,0.0,1.0);}'; 443 var frags = [ 444 prec + 'void main(){float x=v.x*1000.0+v.y*7.3;gl_FragColor=vec4(fract(sin(x)*1e4),fract(exp(v.y*9.0)),fract(pow(v.x+1.0,13.7)),1.0);}', 445 der ? '#extension GL_OES_standard_derivatives : enable\n' + prec + 'void main(){float f=sin(v.x*50.0)*cos(v.y*31.0);gl_FragColor=vec4(fwidth(f)*8.0,abs(dFdx(f))*20.0,abs(dFdy(f))*20.0,1.0);}' : '', 446 prec + 'uniform float k;void main(){gl_FragColor=vec4(v.x,v.y*k,1.0-v.x*k,0.37);}', 447 prec + 'uniform sampler2D t;void main(){gl_FragColor=texture2D(t,v*0.73+0.11);}', 448 prec + 'void main(){float a=atan(v.y*2.0-1.0,v.x*2.0-1.0);gl_FragColor=vec4(fract(a*7.0),fract(acos(v.x*1.999-0.9995)*5.0),fract(tan(v.y*1.5)),1.0);}' 449 ]; 450 var sh = function (type, src) { var x = gl.createShader(type); gl.shaderSource(x, src); gl.compileShader(x); return x; }; 451 var buf = gl.createBuffer(); 452 gl.bindBuffer(gl.ARRAY_BUFFER, buf); 453 gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]), gl.STATIC_DRAW); 454 var tex = gl.createTexture(); 455 gl.bindTexture(gl.TEXTURE_2D, tex); 456 var tp = new Uint8Array(64); 457 for (var i = 0; i < 64; i++) tp[i] = (i * 53 + 17) % 256; 458 gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 4, 4, 0, gl.RGBA, gl.UNSIGNED_BYTE, tp); 459 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); 460 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); 461 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 462 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 463 gl.disable(gl.DITHER); gl.viewport(0, 0, 32, 32); 464 var px = new Uint8Array(32 * 32 * 4); 465 var progs = frags.map(function (src) { 466 if (!src) return null; 467 var p = gl.createProgram(); 468 gl.attachShader(p, sh(gl.VERTEX_SHADER, vs)); gl.attachShader(p, sh(gl.FRAGMENT_SHADER, src)); gl.linkProgram(p); 469 return gl.getProgramParameter(p, gl.LINK_STATUS) ? p : null; 470 }); 471 var pass = function () { 472 return progs.map(function (p, n) { 473 if (!p) return '-'; 474 gl.useProgram(p); 475 var loc = gl.getAttribLocation(p, 'a'); 476 gl.enableVertexAttribArray(loc); gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0); 477 gl.clearColor(0.1, 0.2, 0.3, 1.0); gl.clear(gl.COLOR_BUFFER_BIT); 478 if (n === 2) { 479 // three half-transparent layers on top of each other 480 gl.enable(gl.BLEND); gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); 481 var k = gl.getUniformLocation(p, 'k'); 482 [0.3, 0.6, 0.9].forEach(function (x) { gl.uniform1f(k, x); gl.drawArrays(gl.TRIANGLES, 0, 6); }); 483 gl.disable(gl.BLEND); 484 } else { 485 if (n === 3) gl.uniform1i(gl.getUniformLocation(p, 't'), 0); 486 gl.drawArrays(gl.TRIANGLES, 0, 6); 487 } 488 gl.readPixels(0, 0, 32, 32, gl.RGBA, gl.UNSIGNED_BYTE, px); 489 var str = []; 490 for (var i2 = 0; i2 < px.length; i2 += 1024) str.push(String.fromCharCode.apply(null, px.subarray(i2, i2 + 1024))); 491 return hash(str.join('')).slice(0, 8); 492 }); 493 }; 494 var a = pass(), b = pass(); 495 o.parts = a; 496 o.noise = a.join() !== b.join(); 497 o.h = hash(a.join('|')); 498 var lose = gl.getExtension('WEBGL_lose_context'); 499 if (lose) lose.loseContext(); 500 } catch (e) {} 501 return o; 502 } 503 504 // The screen as the system lays it out: the size in device pixels, the strip the taskbar or dock takes, the 505 // menu bar at the top (macOS), the colour range of the display and HDR, and whether more than one display is on. 506 function screen2() { 507 var sc = window.screen || {}, dpr = window.devicePixelRatio || 1; 508 var on = function (q) { return mq(q) === true; }; 509 return { 510 pw: Math.round((sc.width || 0) * dpr), ph: Math.round((sc.height || 0) * dpr), 511 tb: (sc.height || 0) - (sc.availHeight || 0), tw: (sc.width || 0) - (sc.availWidth || 0), 512 at: typeof sc.availTop === 'number' ? sc.availTop : null, al: typeof sc.availLeft === 'number' ? sc.availLeft : null, 513 gamut: on('(color-gamut: rec2020)') ? 'rec2020' : (on('(color-gamut: p3)') ? 'p3' : (on('(color-gamut: srgb)') ? 'srgb' : '')), 514 hdr: on('(dynamic-range: high)'), ext: sc.isExtended === true 515 }; 516 } 517 518 // The sound hardware: the default sample rate, the output channels and the latency of the system's audio. 519 function audioHw() { 520 var AC = window.AudioContext || window.webkitAudioContext; 521 if (!AC) return Promise.resolve(null); 522 try { 523 var ac = new AC(); 524 var o = { sr: ac.sampleRate || 0, ch: ac.destination ? ac.destination.maxChannelCount : null, 525 bl: typeof ac.baseLatency === 'number' ? Math.round(ac.baseLatency * 10000) / 10 : null }; 526 try { ac.close(); } catch (e) {} 527 return Promise.resolve(o); 528 } catch (e) { return Promise.resolve(null); } 529 } 530 531 // Emoji clock: one emoji of each Unicode emoji version. The newest one the system draws in colour as a single 532 // glyph tells the version of its emoji font, which follows the version of the system: browsers and iOS 26+ 533 // freeze the system version in the User-Agent, the emoji do not. m: the sizes of three emoji (the font itself). 534 var EMOJI = [['11', '\uD83E\uDD70'], ['12', '\uD83E\uDD71'], ['13', '\uD83E\uDD72'], ['13.1', '\u2764\uFE0F\u200D\uD83D\uDD25'], ['14', '\uD83E\uDEE0'], 535 ['15', '\uD83E\uDEE8'], ['15.1', '\uD83D\uDE42\u200D\u2194\uFE0F'], ['16', '\uD83E\uDEE9']]; 536 function emojiClock() { 537 var o = { v: '', ok: '', m: '' }; 538 try { 539 var c = document.createElement('canvas'); c.width = 64; c.height = 40; 540 var x = c.getContext('2d'); 541 x.font = '28px sans-serif'; x.textBaseline = 'top'; 542 var one = x.measureText('\uD83D\uDE00').width; 543 var ok = []; 544 EMOJI.forEach(function (e) { 545 x.clearRect(0, 0, 64, 40); 546 x.fillText(e[1], 0, 2); 547 var d = x.getImageData(0, 0, 64, 40).data, col = 0; 548 for (var i = 0; i < d.length; i += 4) { 549 if (d[i + 3] > 128 && (Math.abs(d[i] - d[i + 1]) > 40 || Math.abs(d[i + 1] - d[i + 2]) > 40)) col++; 550 } 551 // in colour and as one glyph: a sequence the font does not know falls apart into two or more 552 if (col > 20 && x.measureText(e[1]).width < one * 1.5) ok.push(e[0]); 553 }); 554 o.ok = ok.join(','); 555 o.v = ok.length ? ok[ok.length - 1] : ''; 556 var m = x.measureText('\uD83D\uDE00\uD83E\uDEE0\uD83D\uDC4D\uD83C\uDFFD'); 557 o.m = hash([one, m.width, m.actualBoundingBoxAscent, m.actualBoundingBoxDescent].map(function (n) { return Math.round((n || 0) * 10); }).join(',')); 558 } catch (e) {} 559 return o; 560 } 561 562 // How much the browser lets a site keep, as a class only (0: under 1 GB ... 8: 1.6 TB and more). In Chromium 563 // it follows the size of the disk, which tells two phones of one model with different storage apart. 564 function storeTier() { 565 if (!navigator.storage || !navigator.storage.estimate) return Promise.resolve(null); 566 return timeout(navigator.storage.estimate().then(function (e) { 567 var gb = ((e && e.quota) || 0) / 1e9, edges = [1, 10, 50, 100, 200, 400, 800, 1600], t = 0; 568 while (t < edges.length && gb >= edges[t]) t++; 569 return { t: t }; 570 }), 1000, null); 348 571 } 349 572 … … 410 633 * (the request itself fails, there is no certificate for that name), then /dns/<id> says who asked; 411 634 * - STUN: the public address WebRTC's UDP traffic comes from (srflx candidates). 635 * - QUIC (0.19.0, on its own host): /a offers HTTP/3 in an Alt-Svc header and closes its TCP connection, three 636 * /b requests show whether the browser moves to HTTP/3, /r returns what the server saw, signed. 412 637 * The tokens are verified on spooflens.com; the browser only carries them. 413 638 */ 414 function edgeProbe(host, zone ) {639 function edgeProbe(host, zone, qhost) { 415 640 // on spooflens.com the page names the edge (data-edge); sl.js on client sites passes it in 416 641 host = host || document.documentElement.getAttribute('data-edge'); 417 642 zone = zone || document.documentElement.getAttribute('data-edge-zone'); 643 qhost = qhost || document.documentElement.getAttribute('data-edge-quic'); 418 644 if (!host || !window.fetch) return Promise.resolve(null); 419 645 var base = 'https://' + host, out = { tls: '', dns: '', stun: [] }; 420 646 var get = function (u) { return fetch(base + u, { cache: 'no-store', credentials: 'omit' }).then(function (r) { return r.json(); }); }; 421 var tlsP = timeout(get('/fp').then(function (j) { out.tls = j.token || ''; }), 3000, null).catch(function () {}); 647 // round trip as the page sees it (0.16.0): three pings on the connection /fp has just used, the fastest kept. 648 // The server compares it with the round trip the edge's kernel measured for that connection. 649 var tag = Math.random().toString(36).slice(2, 8); // this run's own timing entries 650 var ping = function (n) { 651 var u = base + '/ping?n=' + tag + n, t0 = performance.now(); 652 return fetch(u, { cache: 'no-store', credentials: 'omit' }).then(function (r) { return r.text(); }).then(function () { 653 var ms = performance.now() - t0; 654 // the network stack's own timing (the edge sends Timing-Allow-Origin), free of main-thread delays 655 try { 656 var e = performance.getEntriesByName(u).pop(); 657 if (e && e.requestStart > 0 && e.responseStart >= e.requestStart) ms = e.responseStart - e.requestStart; 658 } catch (x) {} 659 return ms; 660 }); 661 }; 662 var pings = function () { 663 var ms = []; 664 var keep = function (v) { ms.push(v); }; 665 return ping(1).then(keep).then(function () { return ping(2); }).then(keep).then(function () { return ping(3); }).then(keep) 666 .catch(function () {}) 667 .then(function () { if (ms.length) out.rtt = Math.round(Math.min.apply(null, ms) * 10) / 10; }); 668 }; 669 // QUIC after the pings, so its traffic does not sit in their timing. nextHopProtocol of each request is the 670 // browser's own word (the QUIC host sends Timing-Allow-Origin); the server's account is in the token. 671 var quic = function () { 672 if (!qhost) return null; 673 var qb = 'https://' + qhost, qid = '', urls = []; 674 for (var k = 0; k < 16; k++) qid += '0123456789abcdef'[Math.floor(Math.random() * 16)]; 675 var wait = function (ms) { return new Promise(function (r) { setTimeout(r, ms); }); }; 676 var q = function (p) { 677 var u = qb + p + (p.indexOf('?') >= 0 ? '&' : '?') + 'id=' + qid; 678 if (p !== '/r') urls.push(u); 679 return fetch(u, { cache: 'no-store', credentials: 'omit' }); 680 }; 681 // the timing entry of a request is written after fetch() resolves, so the protocols are read at the end 682 var protocols = function () { 683 return urls.map(function (u) { try { var e = performance.getEntriesByName(u).pop(); return e ? e.nextHopProtocol || '' : ''; } catch (x) { return ''; } }); 684 }; 685 return timeout(q('/a').then(function () { return wait(50); }) 686 .then(function () { return q('/b?n=1'); }).then(function () { return wait(300); }) 687 .then(function () { return q('/b?n=2'); }).then(function () { return wait(200); }) 688 .then(function () { return q('/b?n=3'); }) 689 .then(function () { return q('/r'); }).then(function (r) { return r.json(); }) 690 .then(function (j) { if (j && j.token) out.quic = { token: j.token, np: protocols() }; }), 4000, null).catch(function () {}); 691 }; 692 var tlsP = timeout(get('/fp').then(function (j) { out.tls = j.token || ''; }), 3000, null).catch(function () {}) 693 .then(function () { return out.tls ? timeout(pings(), 2000, null) : null; }).catch(function () {}) 694 .then(quic); 422 695 var id = ''; 423 696 for (var i = 0; i < 16; i++) id += '0123456789abcdef'[Math.floor(Math.random() * 16)]; … … 534 807 media: { 535 808 dark: mq('(prefers-color-scheme: dark)'), reduced: mq('(prefers-reduced-motion: reduce)'), 536 pointer: mq('(pointer: fine)') ? 'fine' : (mq('(pointer: coarse)') ? 'coarse' : 'none'), hover: mq('(hover: hover)') 809 pointer: mq('(pointer: fine)') ? 'fine' : (mq('(pointer: coarse)') ? 'coarse' : 'none'), hover: mq('(hover: hover)'), 810 // settings of the system (0.18.0): contrast, transparency, inverted and forced colours 811 contrast: mq('(prefers-contrast: more)') ? 'more' : (mq('(prefers-contrast: less)') ? 'less' : (mq('(prefers-contrast: custom)') ? 'custom' : 'no')), 812 transp: mq('(prefers-reduced-transparency: reduce)'), invert: mq('(inverted-colors: inverted)'), forced: mq('(forced-colors: active)') 537 813 }, 814 scr2: screen2(), 538 815 gfx: { vendor: gl.vendor || '', renderer: gl.renderer || '', version: gl.version || '', glsl: gl.glsl || '', 539 maxTex: gl.maxTex || null, extCount: gl.extCount || 0, hash: gl.hash || '' },816 maxTex: gl.maxTex || null, extCount: gl.extCount || 0, hash: gl.hash || '', params: glParams() }, 540 817 canvas: { h1: c1, h2: c2, noise: !!(c1 && c2 && c1 !== c2), flat: canvasFlat() }, 541 818 scene: webglScene(roads), 819 scene2: scene2(), 820 emoji: emojiClock(), 542 821 fonts: fonts(), 543 auto: { webdriver: nav.webdriver === true, chromeObj: !!window.chrome, tamper: tamperCheck() }, 822 auto: { webdriver: nav.webdriver === true, chromeObj: !!window.chrome, tamper: tamperCheck(), engine: jsEngine(), traces: autoTraces() }, 823 // WebTransport (HTTP/3 for pages, 0.19.0): exposed only in a secure context, so elsewhere nothing is said 824 wt: window.isSecureContext ? typeof window.WebTransport === 'function' : null, 544 825 iframe: iframeProbe(), 545 826 priv: { … … 548 829 } 549 830 }; 550 return Promise.all([audioHash(), audioHash(), webgpu(), voices(), devices(), rtc(), workerProbe(workerSrc), uaData(), edgeProbe(edgeConf && edgeConf.host, edgeConf && edgeConf.zone ), motion(), sceneRoads(roads)])831 return Promise.all([audioHash(), audioHash(), webgpu(), voices(), devices(), rtc(), workerProbe(workerSrc), uaData(), edgeProbe(edgeConf && edgeConf.host, edgeConf && edgeConf.zone, edgeConf && edgeConf.quic), motion(), sceneRoads(roads), audioHw(), storeTier()]) 551 832 .then(function (r) { 552 833 if (r[10]) s.scene.roads = r[10]; 553 834 s.audio = { h1: r[0] ? hash(r[0]) : '', h2: r[1] ? hash(r[1]) : '', noise: !!(r[0] && r[1] && r[0] !== r[1]) }; 554 835 if (r[2]) { s.gfx.webgpuVendor = r[2].vendor || ''; s.gfx.webgpuArch = r[2].arch || ''; } 555 s.voices = r[3] ; s.devices = r[4]; s.rtc = r[5]; s.worker = r[6]; s.nav.uad = r[7];836 s.voices = r[3] ? r[3].n : r[3]; s.vo = r[3] ? r[3].k : null; s.devices = r[4]; s.rtc = r[5]; s.worker = r[6]; s.nav.uad = r[7]; 556 837 if (r[8]) s.edge = r[8]; 557 838 s.motion = r[9]; 839 s.audio2 = r[11]; 840 s.store = r[12]; 841 s.vh = r[3] ? r[3].h || '' : ''; 842 // the voice list often arrives late: read it once more now that everything else is done 843 try { 844 var vl = window.speechSynthesis ? speechSynthesis.getVoices() : []; 845 if (vl && vl.length > (s.voices || 0)) { var vs2 = voiceSummary(vl); s.voices = vs2.n; s.vo = vs2.k; s.vh = vs2.h; } 846 } catch (e) {} 558 847 s.fp = hash([s.canvas.h1, s.gfx.hash, s.audio.h1, s.fonts.join(','), s.scr.w + 'x' + s.scr.h + '@' + s.scr.dpr, 559 848 s.nav.hc, s.nav.dm, s.tz.name, s.nav.languages.join(','), s.nav.platform].join('|')); … … 561 850 }) 562 851 .then(waitOuter); 852 } 853 854 // Research links (0.16.1): two more quick passes over the drawing, sound, font and voice tests. Only how many 855 // times each part came out different is kept (s.rep), which shows the natural noise of this device. 856 function repeatPasses(s) { 857 var rep = { n: 0, canvas: 0, webgl: 0, scene: 0, audio: 0, fonts: 0, voices: 0 }; 858 var fontsKey = (s.fonts || []).join(','), sceneH = (s.scene && s.scene.h1) || ''; 859 var one = function () { 860 var c = canvasHash(), g = webgl(), sc = webglScene({}), fk = fonts().join(','); 861 return Promise.all([audioHash(), voices()]).then(function (r) { 862 rep.n++; 863 if (c !== s.canvas.h1) rep.canvas++; 864 if ((g.hash || '') !== (s.gfx.hash || '')) rep.webgl++; 865 if (((sc && sc.h1) || '') !== sceneH) rep.scene++; 866 if ((r[0] ? hash(r[0]) : '') !== ((s.audio && s.audio.h1) || '')) rep.audio++; 867 if (fk !== fontsKey) rep.fonts++; 868 if ((r[1] ? r[1].n : r[1]) !== s.voices) rep.voices++; 869 }); 870 }; 871 return timeout(one().then(one), 6000, null).catch(function () {}).then(function () { if (rep.n) s.rep = rep; }); 563 872 } 564 873 -
spooflens-fraud-check/tags/0.2.1/includes/admin.php
r3765684 r3765708 97 97 printf( 98 98 /* translators: %s: link to the SpoofLens account */ 99 esc_html__( 'Free key: %s (1,000 checks a month during the beta). Leave empty to keep the saved key.', 'spooflens-fraud-check' ),99 esc_html__( 'Free key: %s (1,000 checks a month free). Leave empty to keep the saved key.', 'spooflens-fraud-check' ), 100 100 '<a href="https://spooflens.com/en/app/api" target="_blank" rel="noopener">spooflens.com/en/app/api</a>' 101 101 ); -
spooflens-fraud-check/tags/0.2.1/readme.txt
r3765684 r3765708 5 5 Tested up to: 7.1 6 6 Requires PHP: 7.4 7 Stable tag: 0.2. 07 Stable tag: 0.2.1 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 26 26 Works with WordPress registration, WooCommerce My Account registration, both the classic and the block WooCommerce checkout, Contact Form 7 and WPForms. Compatible with High-Performance Order Storage. A blocked form submission is treated as spam: Contact Form 7 marks it as spam, WPForms shows an error and does not save the entry. 27 27 28 A free key gives 1,000 checks a month during the beta: https://spooflens.com/en/app/api28 A free key gives 1,000 checks a month: https://spooflens.com/en/app/api 29 29 30 30 == Installation == … … 68 68 This plugin relies on the SpoofLens service (https://spooflens.com), operated by SpoofLens. 69 69 70 * On the registration page, on the WooCommerce checkout and My Account pages and on pages with a Contact Form 7 or WPForms form, the visitor's browser runs the collector script that ships with the plugin (assets/sl.js, MIT; its signal part is also published at https://github.com/spooflens/spooflens-collector). The script collects technical characteristics the browser reports to websites (system , screen, languages, time zone, fonts, graphics card, canvas, WebGL and audio fingerprints) and connects to edge.spooflens.com and a random name under l.spooflens.com to read the TLS handshake, the DNS resolver and the public WebRTC address.70 * On the registration page, on the WooCommerce checkout and My Account pages and on pages with a Contact Form 7 or WPForms form, the visitor's browser runs the collector script that ships with the plugin (assets/sl.js, MIT; its signal part is also published at https://github.com/spooflens/spooflens-collector). The script collects technical characteristics the browser reports to websites (system and its display settings, screen, languages, time zone, fonts, graphics card and its parameters, sound output, the number and a hash of the names of speech voices, which emoji the system can draw, a class of the storage quota, canvas, WebGL and audio fingerprints) and connects to edge.spooflens.com and a random name under l.spooflens.com to read the TLS handshake, the first TCP packet of the connection, the round trip of a few short requests, the DNS resolver and the public WebRTC address. 71 71 * When a user registers, an order is placed or a form is sent, your server sends to https://spooflens.com/v1/assess: the collected data, the visitor's IP address and request headers (User-Agent, Accept-Language, Client Hints), and a SHA-256 hash of the email address. The answer (score, verdict, reasons) is stored with the user or order on your site. 72 72 * SpoofLens does not store the browser data or the full IP address. Its request log, visible to you in your SpoofLens account, keeps for 30 days the time, score, decision, reasons, device identifier, the shortened IP address, country and network type; it also keeps a hash of the device identifier and of the account reference for 30 days to count accounts per device. … … 76 76 77 77 == Changelog == 78 79 = 0.2.1 = 80 * Updated collector: more traits of the device and its system (display settings, sound output, speech voices, emoji support, a class of the storage quota, graphics card parameters, a second WebGL test scene), the JavaScript engine, traces of automation tools and the round trip to the checking server. The new checks run on the SpoofLens side; your settings do not change. 81 * Settings: the free key gives 1,000 checks a month, also after the beta. 78 82 79 83 = 0.2.0 = -
spooflens-fraud-check/tags/0.2.1/spooflens.php
r3765684 r3765708 4 4 * Plugin URI: https://spooflens.com/developers 5 5 * Description: Scores sign-ups, WooCommerce orders and Contact Form 7 / WPForms submissions for spoofed browsers, bots, VPNs and several accounts on one device, using the SpoofLens API. 6 * Version: 0.2. 06 * Version: 0.2.1 7 7 * Requires at least: 6.0 8 8 * Requires PHP: 7.4 … … 17 17 defined( 'ABSPATH' ) || exit; 18 18 19 define( 'SPOOFLENS_VERSION', '0.2. 0' );19 define( 'SPOOFLENS_VERSION', '0.2.1' ); 20 20 define( 'SPOOFLENS_FILE', __FILE__ ); 21 21 // Can be set in wp-config.php to point the plugin at a test server. -
spooflens-fraud-check/trunk/assets/sl.js
r3765684 r3765708 330 330 } 331 331 332 // speech voices: the count, and which system the on-device voices come with (Microsoft ones with Windows, 333 // Apple ones with macOS and iOS); network voices (Google in desktop Chrome, "Online" in Edge) are left out. 334 // Only the counts leave the browser, not the names. 335 var APPLE_VOICES = /^(Samantha|Alex|Daniel|Karen|Moira|Tessa|Fiona|Fred|Victoria|Rishi|Veena|Thomas|Anna|Milena|Yuri|Amelie|Amélie|Kyoko|Ting-Ting)( |$)/; 336 function voiceSummary(list) { 337 var k = { win: 0, apple: 0, other: 0 }, names = []; 338 for (var i = 0; i < list.length; i++) { 339 var v = list[i]; 340 if (!v || v.localService !== true) continue; 341 var n = String(v.name || ''), u = String(v.voiceURI || ''); 342 names.push(n); 343 if (/^Microsoft /.test(n)) k.win++; 344 else if (/^com\.apple\./.test(u) || APPLE_VOICES.test(n)) k.apple++; 345 else k.other++; 346 } 347 // the names of the system's own voices, as one hash (0.18.0): the same in every browser of a device, and voices 348 // the owner downloaded tell two phones of one model apart; the names themselves are not sent 349 return { n: list.length, k: k, h: names.length ? hash(names.sort().join('|')) : '' }; 350 } 332 351 function voices() { 333 352 if (!window.speechSynthesis) return Promise.resolve(null); 334 353 var v = speechSynthesis.getVoices(); 335 if (v && v.length) return Promise.resolve(v .length);354 if (v && v.length) return Promise.resolve(voiceSummary(v)); 336 355 return timeout(new Promise(function (r) { 337 speechSynthesis.addEventListener('voiceschanged', function () { r(speechSynthesis.getVoices().length); }, { once: true }); 338 }), 800, 0); 356 speechSynthesis.addEventListener('voiceschanged', function () { r(voiceSummary(speechSynthesis.getVoices())); }, { once: true }); 357 }), 800, { n: 0, k: null }); 358 } 359 360 // the JavaScript engine words its own TypeError: V8 (Chromium), SpiderMonkey (Firefox), JavaScriptCore (Safari, every iOS browser) 361 function jsEngine() { 362 try { var o = null; o.x = 1; } catch (e) { 363 var m = String((e && e.message) || ''); 364 if (/Cannot set propert/.test(m)) return 'v8'; 365 if (/can't access property|has no properties|is null/.test(m)) return 'gecko'; 366 if (/is not an object/.test(m)) return 'jsc'; 367 return 'other'; 368 } 369 return ''; 370 } 371 372 // globals and document properties that automation frameworks leave behind 373 function autoTraces() { 374 var out = []; 375 ['__playwright__binding__', '__pwInitScripts', '_phantom', 'callPhantom', '__nightmare', 'domAutomation', 'domAutomationController', 376 '_selenium', 'callSelenium', '_Selenium_IDE_Recorder', '__webdriver_evaluate', '__selenium_evaluate', '__webdriver_script_fn', 377 '__driver_evaluate', '__fxdriver_evaluate', '__lastWatirAlert'].forEach(function (k) { 378 try { if (k in window) out.push(k); } catch (e) {} 379 }); 380 try { 381 Object.keys(document).forEach(function (k) { 382 if (/^\$?cdc_|^\$wdc_|^__(webdriver|selenium|fxdriver|driver)_/.test(k)) out.push('document.' + k); 383 }); 384 ['webdriver', 'selenium', 'driver'].forEach(function (a) { if (document.documentElement.hasAttribute(a)) out.push('html[' + a + ']'); }); 385 } catch (e) {} 386 return out.slice(0, 8); 339 387 } 340 388 … … 346 394 return o; 347 395 }), 1500, null); 396 } 397 398 /* ------------------------------------------------ device traits (0.18.0) --- */ 399 // What comes from the machine and the system rather than from the browser engine, so that two browsers of one 400 // device can be told apart from two devices (Chrome and Firefox on one computer share almost no other trait). 401 402 // WebGL limits and shader precision without the names: the graphics card and its driver. p: the limits and 403 // precision; e: the extension list (it differs more between engines, kept apart). 404 function glParams() { 405 var o = { p: '', e: '' }; 406 try { 407 var c = document.createElement('canvas'); 408 var gl = c.getContext('webgl') || c.getContext('experimental-webgl'); 409 if (!gl) return o; 410 var P = ['MAX_TEXTURE_SIZE', 'MAX_CUBE_MAP_TEXTURE_SIZE', 'MAX_RENDERBUFFER_SIZE', 'MAX_VERTEX_ATTRIBS', 'MAX_VERTEX_UNIFORM_VECTORS', 411 'MAX_FRAGMENT_UNIFORM_VECTORS', 'MAX_VARYING_VECTORS', 'MAX_COMBINED_TEXTURE_IMAGE_UNITS', 'MAX_TEXTURE_IMAGE_UNITS', 412 'MAX_VERTEX_TEXTURE_IMAGE_UNITS', 'MAX_VIEWPORT_DIMS', 'ALIASED_LINE_WIDTH_RANGE', 'ALIASED_POINT_SIZE_RANGE', 'SUBPIXEL_BITS']; 413 var out = P.map(function (k) { var v = gl.getParameter(gl[k]); return v && typeof v === 'object' && v.length !== undefined ? Array.prototype.join.call(v, 'x') : String(v); }); 414 ['VERTEX_SHADER', 'FRAGMENT_SHADER'].forEach(function (sh) { 415 ['LOW_FLOAT', 'MEDIUM_FLOAT', 'HIGH_FLOAT', 'LOW_INT', 'MEDIUM_INT', 'HIGH_INT'].forEach(function (pr) { 416 var x = gl.getShaderPrecisionFormat(gl[sh], gl[pr]); 417 out.push(x ? x.rangeMin + '/' + x.rangeMax + '/' + x.precision : '-'); 418 }); 419 }); 420 o.p = hash(out.join('|')); 421 o.e = hash((gl.getSupportedExtensions() || []).slice().sort().join(',')); 422 var lose = gl.getExtension('WEBGL_lose_context'); 423 if (lose) lose.loseContext(); 424 } catch (e) {} 425 return o; 426 } 427 428 // Scene 2.0: five small drawings, each leaning on another part of the graphics card (float maths, derivatives, 429 // blending, texture filtering, trigonometry at its edges). Browsers of one computer go through one driver, so 430 // our first scene already matched across engines; five give more to tell devices apart. Drawn twice: an 431 // unmodified browser gives the same pixels (noise = something rewrote them). 432 function scene2() { 433 var o = { h: '', parts: [], noise: false }; 434 try { 435 var c = document.createElement('canvas'); c.width = 32; c.height = 32; 436 var opt = { antialias: false, preserveDrawingBuffer: true }; 437 var gl = c.getContext('webgl', opt) || c.getContext('experimental-webgl', opt); 438 if (!gl) return o; 439 var hp = gl.getShaderPrecisionFormat && gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.HIGH_FLOAT); 440 var prec = 'precision ' + (hp && hp.precision > 0 ? 'highp' : 'mediump') + ' float;varying vec2 v;'; 441 var der = gl.getExtension('OES_standard_derivatives'); 442 var vs = 'attribute vec2 a;varying vec2 v;void main(){v=a*0.5+0.5;gl_Position=vec4(a,0.0,1.0);}'; 443 var frags = [ 444 prec + 'void main(){float x=v.x*1000.0+v.y*7.3;gl_FragColor=vec4(fract(sin(x)*1e4),fract(exp(v.y*9.0)),fract(pow(v.x+1.0,13.7)),1.0);}', 445 der ? '#extension GL_OES_standard_derivatives : enable\n' + prec + 'void main(){float f=sin(v.x*50.0)*cos(v.y*31.0);gl_FragColor=vec4(fwidth(f)*8.0,abs(dFdx(f))*20.0,abs(dFdy(f))*20.0,1.0);}' : '', 446 prec + 'uniform float k;void main(){gl_FragColor=vec4(v.x,v.y*k,1.0-v.x*k,0.37);}', 447 prec + 'uniform sampler2D t;void main(){gl_FragColor=texture2D(t,v*0.73+0.11);}', 448 prec + 'void main(){float a=atan(v.y*2.0-1.0,v.x*2.0-1.0);gl_FragColor=vec4(fract(a*7.0),fract(acos(v.x*1.999-0.9995)*5.0),fract(tan(v.y*1.5)),1.0);}' 449 ]; 450 var sh = function (type, src) { var x = gl.createShader(type); gl.shaderSource(x, src); gl.compileShader(x); return x; }; 451 var buf = gl.createBuffer(); 452 gl.bindBuffer(gl.ARRAY_BUFFER, buf); 453 gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]), gl.STATIC_DRAW); 454 var tex = gl.createTexture(); 455 gl.bindTexture(gl.TEXTURE_2D, tex); 456 var tp = new Uint8Array(64); 457 for (var i = 0; i < 64; i++) tp[i] = (i * 53 + 17) % 256; 458 gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 4, 4, 0, gl.RGBA, gl.UNSIGNED_BYTE, tp); 459 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); 460 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); 461 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 462 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 463 gl.disable(gl.DITHER); gl.viewport(0, 0, 32, 32); 464 var px = new Uint8Array(32 * 32 * 4); 465 var progs = frags.map(function (src) { 466 if (!src) return null; 467 var p = gl.createProgram(); 468 gl.attachShader(p, sh(gl.VERTEX_SHADER, vs)); gl.attachShader(p, sh(gl.FRAGMENT_SHADER, src)); gl.linkProgram(p); 469 return gl.getProgramParameter(p, gl.LINK_STATUS) ? p : null; 470 }); 471 var pass = function () { 472 return progs.map(function (p, n) { 473 if (!p) return '-'; 474 gl.useProgram(p); 475 var loc = gl.getAttribLocation(p, 'a'); 476 gl.enableVertexAttribArray(loc); gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0); 477 gl.clearColor(0.1, 0.2, 0.3, 1.0); gl.clear(gl.COLOR_BUFFER_BIT); 478 if (n === 2) { 479 // three half-transparent layers on top of each other 480 gl.enable(gl.BLEND); gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); 481 var k = gl.getUniformLocation(p, 'k'); 482 [0.3, 0.6, 0.9].forEach(function (x) { gl.uniform1f(k, x); gl.drawArrays(gl.TRIANGLES, 0, 6); }); 483 gl.disable(gl.BLEND); 484 } else { 485 if (n === 3) gl.uniform1i(gl.getUniformLocation(p, 't'), 0); 486 gl.drawArrays(gl.TRIANGLES, 0, 6); 487 } 488 gl.readPixels(0, 0, 32, 32, gl.RGBA, gl.UNSIGNED_BYTE, px); 489 var str = []; 490 for (var i2 = 0; i2 < px.length; i2 += 1024) str.push(String.fromCharCode.apply(null, px.subarray(i2, i2 + 1024))); 491 return hash(str.join('')).slice(0, 8); 492 }); 493 }; 494 var a = pass(), b = pass(); 495 o.parts = a; 496 o.noise = a.join() !== b.join(); 497 o.h = hash(a.join('|')); 498 var lose = gl.getExtension('WEBGL_lose_context'); 499 if (lose) lose.loseContext(); 500 } catch (e) {} 501 return o; 502 } 503 504 // The screen as the system lays it out: the size in device pixels, the strip the taskbar or dock takes, the 505 // menu bar at the top (macOS), the colour range of the display and HDR, and whether more than one display is on. 506 function screen2() { 507 var sc = window.screen || {}, dpr = window.devicePixelRatio || 1; 508 var on = function (q) { return mq(q) === true; }; 509 return { 510 pw: Math.round((sc.width || 0) * dpr), ph: Math.round((sc.height || 0) * dpr), 511 tb: (sc.height || 0) - (sc.availHeight || 0), tw: (sc.width || 0) - (sc.availWidth || 0), 512 at: typeof sc.availTop === 'number' ? sc.availTop : null, al: typeof sc.availLeft === 'number' ? sc.availLeft : null, 513 gamut: on('(color-gamut: rec2020)') ? 'rec2020' : (on('(color-gamut: p3)') ? 'p3' : (on('(color-gamut: srgb)') ? 'srgb' : '')), 514 hdr: on('(dynamic-range: high)'), ext: sc.isExtended === true 515 }; 516 } 517 518 // The sound hardware: the default sample rate, the output channels and the latency of the system's audio. 519 function audioHw() { 520 var AC = window.AudioContext || window.webkitAudioContext; 521 if (!AC) return Promise.resolve(null); 522 try { 523 var ac = new AC(); 524 var o = { sr: ac.sampleRate || 0, ch: ac.destination ? ac.destination.maxChannelCount : null, 525 bl: typeof ac.baseLatency === 'number' ? Math.round(ac.baseLatency * 10000) / 10 : null }; 526 try { ac.close(); } catch (e) {} 527 return Promise.resolve(o); 528 } catch (e) { return Promise.resolve(null); } 529 } 530 531 // Emoji clock: one emoji of each Unicode emoji version. The newest one the system draws in colour as a single 532 // glyph tells the version of its emoji font, which follows the version of the system: browsers and iOS 26+ 533 // freeze the system version in the User-Agent, the emoji do not. m: the sizes of three emoji (the font itself). 534 var EMOJI = [['11', '\uD83E\uDD70'], ['12', '\uD83E\uDD71'], ['13', '\uD83E\uDD72'], ['13.1', '\u2764\uFE0F\u200D\uD83D\uDD25'], ['14', '\uD83E\uDEE0'], 535 ['15', '\uD83E\uDEE8'], ['15.1', '\uD83D\uDE42\u200D\u2194\uFE0F'], ['16', '\uD83E\uDEE9']]; 536 function emojiClock() { 537 var o = { v: '', ok: '', m: '' }; 538 try { 539 var c = document.createElement('canvas'); c.width = 64; c.height = 40; 540 var x = c.getContext('2d'); 541 x.font = '28px sans-serif'; x.textBaseline = 'top'; 542 var one = x.measureText('\uD83D\uDE00').width; 543 var ok = []; 544 EMOJI.forEach(function (e) { 545 x.clearRect(0, 0, 64, 40); 546 x.fillText(e[1], 0, 2); 547 var d = x.getImageData(0, 0, 64, 40).data, col = 0; 548 for (var i = 0; i < d.length; i += 4) { 549 if (d[i + 3] > 128 && (Math.abs(d[i] - d[i + 1]) > 40 || Math.abs(d[i + 1] - d[i + 2]) > 40)) col++; 550 } 551 // in colour and as one glyph: a sequence the font does not know falls apart into two or more 552 if (col > 20 && x.measureText(e[1]).width < one * 1.5) ok.push(e[0]); 553 }); 554 o.ok = ok.join(','); 555 o.v = ok.length ? ok[ok.length - 1] : ''; 556 var m = x.measureText('\uD83D\uDE00\uD83E\uDEE0\uD83D\uDC4D\uD83C\uDFFD'); 557 o.m = hash([one, m.width, m.actualBoundingBoxAscent, m.actualBoundingBoxDescent].map(function (n) { return Math.round((n || 0) * 10); }).join(',')); 558 } catch (e) {} 559 return o; 560 } 561 562 // How much the browser lets a site keep, as a class only (0: under 1 GB ... 8: 1.6 TB and more). In Chromium 563 // it follows the size of the disk, which tells two phones of one model with different storage apart. 564 function storeTier() { 565 if (!navigator.storage || !navigator.storage.estimate) return Promise.resolve(null); 566 return timeout(navigator.storage.estimate().then(function (e) { 567 var gb = ((e && e.quota) || 0) / 1e9, edges = [1, 10, 50, 100, 200, 400, 800, 1600], t = 0; 568 while (t < edges.length && gb >= edges[t]) t++; 569 return { t: t }; 570 }), 1000, null); 348 571 } 349 572 … … 410 633 * (the request itself fails, there is no certificate for that name), then /dns/<id> says who asked; 411 634 * - STUN: the public address WebRTC's UDP traffic comes from (srflx candidates). 635 * - QUIC (0.19.0, on its own host): /a offers HTTP/3 in an Alt-Svc header and closes its TCP connection, three 636 * /b requests show whether the browser moves to HTTP/3, /r returns what the server saw, signed. 412 637 * The tokens are verified on spooflens.com; the browser only carries them. 413 638 */ 414 function edgeProbe(host, zone ) {639 function edgeProbe(host, zone, qhost) { 415 640 // on spooflens.com the page names the edge (data-edge); sl.js on client sites passes it in 416 641 host = host || document.documentElement.getAttribute('data-edge'); 417 642 zone = zone || document.documentElement.getAttribute('data-edge-zone'); 643 qhost = qhost || document.documentElement.getAttribute('data-edge-quic'); 418 644 if (!host || !window.fetch) return Promise.resolve(null); 419 645 var base = 'https://' + host, out = { tls: '', dns: '', stun: [] }; 420 646 var get = function (u) { return fetch(base + u, { cache: 'no-store', credentials: 'omit' }).then(function (r) { return r.json(); }); }; 421 var tlsP = timeout(get('/fp').then(function (j) { out.tls = j.token || ''; }), 3000, null).catch(function () {}); 647 // round trip as the page sees it (0.16.0): three pings on the connection /fp has just used, the fastest kept. 648 // The server compares it with the round trip the edge's kernel measured for that connection. 649 var tag = Math.random().toString(36).slice(2, 8); // this run's own timing entries 650 var ping = function (n) { 651 var u = base + '/ping?n=' + tag + n, t0 = performance.now(); 652 return fetch(u, { cache: 'no-store', credentials: 'omit' }).then(function (r) { return r.text(); }).then(function () { 653 var ms = performance.now() - t0; 654 // the network stack's own timing (the edge sends Timing-Allow-Origin), free of main-thread delays 655 try { 656 var e = performance.getEntriesByName(u).pop(); 657 if (e && e.requestStart > 0 && e.responseStart >= e.requestStart) ms = e.responseStart - e.requestStart; 658 } catch (x) {} 659 return ms; 660 }); 661 }; 662 var pings = function () { 663 var ms = []; 664 var keep = function (v) { ms.push(v); }; 665 return ping(1).then(keep).then(function () { return ping(2); }).then(keep).then(function () { return ping(3); }).then(keep) 666 .catch(function () {}) 667 .then(function () { if (ms.length) out.rtt = Math.round(Math.min.apply(null, ms) * 10) / 10; }); 668 }; 669 // QUIC after the pings, so its traffic does not sit in their timing. nextHopProtocol of each request is the 670 // browser's own word (the QUIC host sends Timing-Allow-Origin); the server's account is in the token. 671 var quic = function () { 672 if (!qhost) return null; 673 var qb = 'https://' + qhost, qid = '', urls = []; 674 for (var k = 0; k < 16; k++) qid += '0123456789abcdef'[Math.floor(Math.random() * 16)]; 675 var wait = function (ms) { return new Promise(function (r) { setTimeout(r, ms); }); }; 676 var q = function (p) { 677 var u = qb + p + (p.indexOf('?') >= 0 ? '&' : '?') + 'id=' + qid; 678 if (p !== '/r') urls.push(u); 679 return fetch(u, { cache: 'no-store', credentials: 'omit' }); 680 }; 681 // the timing entry of a request is written after fetch() resolves, so the protocols are read at the end 682 var protocols = function () { 683 return urls.map(function (u) { try { var e = performance.getEntriesByName(u).pop(); return e ? e.nextHopProtocol || '' : ''; } catch (x) { return ''; } }); 684 }; 685 return timeout(q('/a').then(function () { return wait(50); }) 686 .then(function () { return q('/b?n=1'); }).then(function () { return wait(300); }) 687 .then(function () { return q('/b?n=2'); }).then(function () { return wait(200); }) 688 .then(function () { return q('/b?n=3'); }) 689 .then(function () { return q('/r'); }).then(function (r) { return r.json(); }) 690 .then(function (j) { if (j && j.token) out.quic = { token: j.token, np: protocols() }; }), 4000, null).catch(function () {}); 691 }; 692 var tlsP = timeout(get('/fp').then(function (j) { out.tls = j.token || ''; }), 3000, null).catch(function () {}) 693 .then(function () { return out.tls ? timeout(pings(), 2000, null) : null; }).catch(function () {}) 694 .then(quic); 422 695 var id = ''; 423 696 for (var i = 0; i < 16; i++) id += '0123456789abcdef'[Math.floor(Math.random() * 16)]; … … 534 807 media: { 535 808 dark: mq('(prefers-color-scheme: dark)'), reduced: mq('(prefers-reduced-motion: reduce)'), 536 pointer: mq('(pointer: fine)') ? 'fine' : (mq('(pointer: coarse)') ? 'coarse' : 'none'), hover: mq('(hover: hover)') 809 pointer: mq('(pointer: fine)') ? 'fine' : (mq('(pointer: coarse)') ? 'coarse' : 'none'), hover: mq('(hover: hover)'), 810 // settings of the system (0.18.0): contrast, transparency, inverted and forced colours 811 contrast: mq('(prefers-contrast: more)') ? 'more' : (mq('(prefers-contrast: less)') ? 'less' : (mq('(prefers-contrast: custom)') ? 'custom' : 'no')), 812 transp: mq('(prefers-reduced-transparency: reduce)'), invert: mq('(inverted-colors: inverted)'), forced: mq('(forced-colors: active)') 537 813 }, 814 scr2: screen2(), 538 815 gfx: { vendor: gl.vendor || '', renderer: gl.renderer || '', version: gl.version || '', glsl: gl.glsl || '', 539 maxTex: gl.maxTex || null, extCount: gl.extCount || 0, hash: gl.hash || '' },816 maxTex: gl.maxTex || null, extCount: gl.extCount || 0, hash: gl.hash || '', params: glParams() }, 540 817 canvas: { h1: c1, h2: c2, noise: !!(c1 && c2 && c1 !== c2), flat: canvasFlat() }, 541 818 scene: webglScene(roads), 819 scene2: scene2(), 820 emoji: emojiClock(), 542 821 fonts: fonts(), 543 auto: { webdriver: nav.webdriver === true, chromeObj: !!window.chrome, tamper: tamperCheck() }, 822 auto: { webdriver: nav.webdriver === true, chromeObj: !!window.chrome, tamper: tamperCheck(), engine: jsEngine(), traces: autoTraces() }, 823 // WebTransport (HTTP/3 for pages, 0.19.0): exposed only in a secure context, so elsewhere nothing is said 824 wt: window.isSecureContext ? typeof window.WebTransport === 'function' : null, 544 825 iframe: iframeProbe(), 545 826 priv: { … … 548 829 } 549 830 }; 550 return Promise.all([audioHash(), audioHash(), webgpu(), voices(), devices(), rtc(), workerProbe(workerSrc), uaData(), edgeProbe(edgeConf && edgeConf.host, edgeConf && edgeConf.zone ), motion(), sceneRoads(roads)])831 return Promise.all([audioHash(), audioHash(), webgpu(), voices(), devices(), rtc(), workerProbe(workerSrc), uaData(), edgeProbe(edgeConf && edgeConf.host, edgeConf && edgeConf.zone, edgeConf && edgeConf.quic), motion(), sceneRoads(roads), audioHw(), storeTier()]) 551 832 .then(function (r) { 552 833 if (r[10]) s.scene.roads = r[10]; 553 834 s.audio = { h1: r[0] ? hash(r[0]) : '', h2: r[1] ? hash(r[1]) : '', noise: !!(r[0] && r[1] && r[0] !== r[1]) }; 554 835 if (r[2]) { s.gfx.webgpuVendor = r[2].vendor || ''; s.gfx.webgpuArch = r[2].arch || ''; } 555 s.voices = r[3] ; s.devices = r[4]; s.rtc = r[5]; s.worker = r[6]; s.nav.uad = r[7];836 s.voices = r[3] ? r[3].n : r[3]; s.vo = r[3] ? r[3].k : null; s.devices = r[4]; s.rtc = r[5]; s.worker = r[6]; s.nav.uad = r[7]; 556 837 if (r[8]) s.edge = r[8]; 557 838 s.motion = r[9]; 839 s.audio2 = r[11]; 840 s.store = r[12]; 841 s.vh = r[3] ? r[3].h || '' : ''; 842 // the voice list often arrives late: read it once more now that everything else is done 843 try { 844 var vl = window.speechSynthesis ? speechSynthesis.getVoices() : []; 845 if (vl && vl.length > (s.voices || 0)) { var vs2 = voiceSummary(vl); s.voices = vs2.n; s.vo = vs2.k; s.vh = vs2.h; } 846 } catch (e) {} 558 847 s.fp = hash([s.canvas.h1, s.gfx.hash, s.audio.h1, s.fonts.join(','), s.scr.w + 'x' + s.scr.h + '@' + s.scr.dpr, 559 848 s.nav.hc, s.nav.dm, s.tz.name, s.nav.languages.join(','), s.nav.platform].join('|')); … … 561 850 }) 562 851 .then(waitOuter); 852 } 853 854 // Research links (0.16.1): two more quick passes over the drawing, sound, font and voice tests. Only how many 855 // times each part came out different is kept (s.rep), which shows the natural noise of this device. 856 function repeatPasses(s) { 857 var rep = { n: 0, canvas: 0, webgl: 0, scene: 0, audio: 0, fonts: 0, voices: 0 }; 858 var fontsKey = (s.fonts || []).join(','), sceneH = (s.scene && s.scene.h1) || ''; 859 var one = function () { 860 var c = canvasHash(), g = webgl(), sc = webglScene({}), fk = fonts().join(','); 861 return Promise.all([audioHash(), voices()]).then(function (r) { 862 rep.n++; 863 if (c !== s.canvas.h1) rep.canvas++; 864 if ((g.hash || '') !== (s.gfx.hash || '')) rep.webgl++; 865 if (((sc && sc.h1) || '') !== sceneH) rep.scene++; 866 if ((r[0] ? hash(r[0]) : '') !== ((s.audio && s.audio.h1) || '')) rep.audio++; 867 if (fk !== fontsKey) rep.fonts++; 868 if ((r[1] ? r[1].n : r[1]) !== s.voices) rep.voices++; 869 }); 870 }; 871 return timeout(one().then(one), 6000, null).catch(function () {}).then(function () { if (rep.n) s.rep = rep; }); 563 872 } 564 873 -
spooflens-fraud-check/trunk/includes/admin.php
r3765684 r3765708 97 97 printf( 98 98 /* translators: %s: link to the SpoofLens account */ 99 esc_html__( 'Free key: %s (1,000 checks a month during the beta). Leave empty to keep the saved key.', 'spooflens-fraud-check' ),99 esc_html__( 'Free key: %s (1,000 checks a month free). Leave empty to keep the saved key.', 'spooflens-fraud-check' ), 100 100 '<a href="https://spooflens.com/en/app/api" target="_blank" rel="noopener">spooflens.com/en/app/api</a>' 101 101 ); -
spooflens-fraud-check/trunk/readme.txt
r3765684 r3765708 5 5 Tested up to: 7.1 6 6 Requires PHP: 7.4 7 Stable tag: 0.2. 07 Stable tag: 0.2.1 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 26 26 Works with WordPress registration, WooCommerce My Account registration, both the classic and the block WooCommerce checkout, Contact Form 7 and WPForms. Compatible with High-Performance Order Storage. A blocked form submission is treated as spam: Contact Form 7 marks it as spam, WPForms shows an error and does not save the entry. 27 27 28 A free key gives 1,000 checks a month during the beta: https://spooflens.com/en/app/api28 A free key gives 1,000 checks a month: https://spooflens.com/en/app/api 29 29 30 30 == Installation == … … 68 68 This plugin relies on the SpoofLens service (https://spooflens.com), operated by SpoofLens. 69 69 70 * On the registration page, on the WooCommerce checkout and My Account pages and on pages with a Contact Form 7 or WPForms form, the visitor's browser runs the collector script that ships with the plugin (assets/sl.js, MIT; its signal part is also published at https://github.com/spooflens/spooflens-collector). The script collects technical characteristics the browser reports to websites (system , screen, languages, time zone, fonts, graphics card, canvas, WebGL and audio fingerprints) and connects to edge.spooflens.com and a random name under l.spooflens.com to read the TLS handshake, the DNS resolver and the public WebRTC address.70 * On the registration page, on the WooCommerce checkout and My Account pages and on pages with a Contact Form 7 or WPForms form, the visitor's browser runs the collector script that ships with the plugin (assets/sl.js, MIT; its signal part is also published at https://github.com/spooflens/spooflens-collector). The script collects technical characteristics the browser reports to websites (system and its display settings, screen, languages, time zone, fonts, graphics card and its parameters, sound output, the number and a hash of the names of speech voices, which emoji the system can draw, a class of the storage quota, canvas, WebGL and audio fingerprints) and connects to edge.spooflens.com and a random name under l.spooflens.com to read the TLS handshake, the first TCP packet of the connection, the round trip of a few short requests, the DNS resolver and the public WebRTC address. 71 71 * When a user registers, an order is placed or a form is sent, your server sends to https://spooflens.com/v1/assess: the collected data, the visitor's IP address and request headers (User-Agent, Accept-Language, Client Hints), and a SHA-256 hash of the email address. The answer (score, verdict, reasons) is stored with the user or order on your site. 72 72 * SpoofLens does not store the browser data or the full IP address. Its request log, visible to you in your SpoofLens account, keeps for 30 days the time, score, decision, reasons, device identifier, the shortened IP address, country and network type; it also keeps a hash of the device identifier and of the account reference for 30 days to count accounts per device. … … 76 76 77 77 == Changelog == 78 79 = 0.2.1 = 80 * Updated collector: more traits of the device and its system (display settings, sound output, speech voices, emoji support, a class of the storage quota, graphics card parameters, a second WebGL test scene), the JavaScript engine, traces of automation tools and the round trip to the checking server. The new checks run on the SpoofLens side; your settings do not change. 81 * Settings: the free key gives 1,000 checks a month, also after the beta. 78 82 79 83 = 0.2.0 = -
spooflens-fraud-check/trunk/spooflens.php
r3765684 r3765708 4 4 * Plugin URI: https://spooflens.com/developers 5 5 * Description: Scores sign-ups, WooCommerce orders and Contact Form 7 / WPForms submissions for spoofed browsers, bots, VPNs and several accounts on one device, using the SpoofLens API. 6 * Version: 0.2. 06 * Version: 0.2.1 7 7 * Requires at least: 6.0 8 8 * Requires PHP: 7.4 … … 17 17 defined( 'ABSPATH' ) || exit; 18 18 19 define( 'SPOOFLENS_VERSION', '0.2. 0' );19 define( 'SPOOFLENS_VERSION', '0.2.1' ); 20 20 define( 'SPOOFLENS_FILE', __FILE__ ); 21 21 // Can be set in wp-config.php to point the plugin at a test server.
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)