Source Code
The entire project, readable in your browser. No hidden JavaScript, no sneaky beacons. If we claimed we don't track you, this is where you check.
|
Pages/
Feeds/
Backend/
Includes/
Stylesheet/
JavaScript/
data/counter.txt and data/guestbook.txt hold visitor-supplied content
(a number and signed messages), so they are not shown here.
|
1 /* ============================================================================ 2 scan.js -- Orchestration, dashboard rendering, scoring, fun facts 3 ---------------------------------------------------------------------------- 4 Ties detect.js + fingerprint.js + api/scan.php together. Runs the retro boot 5 animation, then renders the bordered "dashboard" sections, computes risk 6 levels + a 0-100 privacy score, and generates personalized fun facts. 7 8 Exposes on window.EYES: 9 BASE - site base URL (set inline by the page) 10 fetchServer() - Promise: GET api/scan.php 11 buildReport() - Promise: {server, browser, fingerprint, ts} 12 computePrivacy(report) -> {score, categories[]} 13 funFacts(report) -> [strings] 14 renderDashboard(el, report) 15 runBoot(termEl, onDone) - types the boot sequence, then calls onDone() 16 report - last built report (also used by terminal.js / export.js) 17 18 Stores nothing server-side. Everything computed here stays in the page. 19 ========================================================================== */ 20 (function () { 21 "use strict"; 22 23 var EYES = window.EYES = window.EYES || {}; 24 EYES.BASE = EYES.BASE || ''; 25 26 /* ---- small DOM / string helpers --------------------------------------- */ 27 function esc(s) { 28 return String(s == null ? '' : s) 29 .replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') 30 .replace(/"/g, '"'); 31 } 32 function yn(v) { 33 if (v === true) return '<span class="val-yes">Yes</span>'; 34 if (v === false) return '<span class="val-no">No</span>'; 35 if (v == null || v === '' ) return '<span class="val-unk">Unknown</span>'; 36 return esc(v); 37 } 38 function riskBadge(level) { 39 var l = String(level || 'INFO').toUpperCase(); 40 var cls = l === 'HIGH' ? 'risk-high' : l === 'MEDIUM' ? 'risk-medium' 41 : l === 'LOW' ? 'risk-low' : 'risk-info'; 42 return '<span class="risk ' + cls + '">' + esc(l) + '</span>'; 43 } 44 function row(k, v) { 45 return '<tr><td class="k">' + esc(k) + '</td><td class="v">' + (v == null ? '<span class="val-unk">Unknown</span>' : v) + '</td></tr>'; 46 } 47 function kvTable(rows) { 48 return '<table class="data kv">' + rows.join('') + '</table>'; 49 } 50 function asciiBar(pct) { 51 pct = Math.max(0, Math.min(100, pct | 0)); 52 var total = 20, filled = Math.round((pct / 100) * total); 53 var s = '['; 54 for (var i = 0; i < total; i++) s += (i < filled ? '#' : '<span class="empty">-</span>'); 55 s += '] ' + pct + '%'; 56 return '<span class="asciibar">' + s + '</span>'; 57 } 58 59 EYES._esc = esc; 60 61 /* ====================================================================== 62 DATA COLLECTION 63 ====================================================================== */ 64 EYES.fetchServer = function () { 65 var url = (EYES.BASE || '') + '/api/scan.php'; 66 return fetch(url, { credentials: 'omit', cache: 'no-store' }) 67 .then(function (r) { return r.json(); }) 68 .catch(function () { return { ok: false, error: 'server unreachable' }; }); 69 }; 70 71 EYES.buildReport = function () { 72 var jobs = [ 73 EYES.fetchServer(), 74 (EYES.detect ? EYES.detect() : Promise.resolve(null)), 75 (EYES.fingerprint ? EYES.fingerprint() : Promise.resolve(null)) 76 ]; 77 return Promise.all(jobs).then(function (res) { 78 var report = { server: res[0], browser: res[1], fingerprint: res[2], ts: new Date().toISOString() }; 79 EYES.report = report; 80 EYES.lastReport = report; 81 return report; 82 }); 83 }; 84 85 /* ====================================================================== 86 RISK CATALOG -- what/why/impact/fix per item, and severity 87 ====================================================================== */ 88 EYES.riskInfo = { 89 ip: { level: 'HIGH', what: 'Your public IP address.', 90 why: 'Every server you connect to sees it -- it is how replies find you.', 91 impact: 'Reveals your approximate location and identifies your connection to any site or logger.', 92 fix: 'Use a trustworthy VPN or the Tor Browser to replace it with a shared address.' }, 93 geo: { level: 'HIGH', what: 'Geolocation derived from your IP.', 94 why: 'A public database maps IP ranges to cities and providers.', 95 impact: 'Sites can localize, price-discriminate, or region-block you without asking.', 96 fix: 'A VPN in another region changes this. GPS-level location still requires a permission prompt.' }, 97 isp: { level: 'MEDIUM', what: 'Your Internet Service Provider / organization / ASN.', 98 why: 'It is attached to the IP block your address comes from.', 99 impact: 'Narrows who you are, especially on corporate or small-ISP networks.', 100 fix: 'VPN/Tor hide your real ISP behind the provider network instead.' }, 101 ua: { level: 'MEDIUM', what: 'User-Agent string: browser, version, OS.', 102 why: 'Your browser sends it on every single request, by design.', 103 impact: 'Feeds fingerprinting and lets sites target or block specific setups.', 104 fix: 'Firefox resistFingerprinting or the Tor Browser normalize this.' }, 105 headers: { level: 'LOW', what: 'The full set of HTTP request headers.', 106 why: 'They negotiate language, compression, and content with the server.', 107 impact: 'Order and contents add a few bits of fingerprinting entropy.', 108 fix: 'Little you can do; anti-fingerprinting browsers minimize the variation.' }, 109 canvas: { level: 'HIGH', what: 'Canvas fingerprint.', 110 why: 'Your GPU/driver/font stack renders the same text/shapes slightly differently.', 111 impact: 'A very stable, cookie-free identifier used widely by trackers.', 112 fix: 'Firefox privacy.resistFingerprinting, Brave, or the Tor Browser randomize/block it.' }, 113 webgl: { level: 'HIGH', what: 'WebGL vendor/renderer + capabilities.', 114 why: 'The 3D API exposes your exact GPU model and driver limits.', 115 impact: 'Strongly identifying, and reveals your graphics hardware.', 116 fix: 'Anti-fingerprinting modes spoof or disable WebGL details.' }, 117 audio: { level: 'MEDIUM', what: 'Audio-stack fingerprint.', 118 why: 'Floating-point audio processing differs subtly per device/OS.', 119 impact: 'Adds stable entropy to a cross-site fingerprint.', 120 fix: 'resistFingerprinting / Tor Browser neutralize it.' }, 121 fonts: { level: 'MEDIUM', what: 'Installed fonts (probed indirectly).', 122 why: 'Text rendered in a font only changes size if that font exists.', 123 impact: 'Your installed-software footprint is fairly unique.', 124 fix: 'Browsers that restrict font enumeration reduce this.' }, 125 screen: { level: 'MEDIUM', what: 'Screen resolution, color depth, pixel ratio.', 126 why: 'Exposed so pages can lay themselves out.', 127 impact: 'Combined with other signals, narrows you considerably.', 128 fix: 'Non-maximized windows and standardized sizes (Tor) help.' }, 129 hardware: { level: 'MEDIUM', what: 'CPU cores, device memory, platform.', 130 why: 'Exposed to help pages tune performance.', 131 impact: 'Adds hardware entropy to your fingerprint.', 132 fix: 'Firefox caps or spoofs these with resistFingerprinting.' }, 133 permissions: { level: 'LOW', what: 'Permission states (camera, mic, geo...).', 134 why: 'Queryable so sites can avoid re-prompting.', 135 impact: 'Prior grants can be detected silently.', 136 fix: 'Review and revoke site permissions periodically.' }, 137 webrtc: { level: 'HIGH', what: 'WebRTC.', 138 why: 'Real-time media API that can enumerate local network addresses.', 139 impact: 'Classic "leak" that can reveal your real IP even behind a VPN.', 140 fix: 'Disable WebRTC or use an extension that prevents the local-IP leak.' }, 141 dnt: { level: 'LOW', what: 'Do Not Track / Global Privacy Control.', 142 why: 'A voluntary request you send; most sites ignore DNT.', 143 impact: 'Ironically, sending it is itself one more fingerprint bit.', 144 fix: 'GPC has some legal weight in a few regions; DNT largely does not.' }, 145 cookies: { level: 'MEDIUM', what: 'Cookie support.', 146 why: 'The original stateful tracking mechanism.', 147 impact: 'Enables persistent identifiers across visits.', 148 fix: 'Block third-party cookies; clear on exit; use container tabs.' } 149 }; 150 151 /* ====================================================================== 152 PRIVACY SCORE (0 = fully exposed, 100 = well protected) 153 ====================================================================== */ 154 EYES.computePrivacy = function (report) { 155 var s = report.server || {}, b = report.browser || {}, fp = report.fingerprint || {}; 156 var geo = (s.geo) || {}; 157 var cats = []; 158 159 function cat(name, score, notes) { cats.push({ name: name, score: Math.max(0, Math.min(100, Math.round(score))), notes: notes }); } 160 161 /* Tracking: cookies + DNT + storage */ 162 (function () { 163 var score = 100, notes = []; 164 if (b.apis && b.apis.cookies) { score -= 30; notes.push('Cookies enabled'); } 165 if (b.storage && b.storage.localStorage) { score -= 15; notes.push('localStorage writable'); } 166 if (b.storage && b.storage.indexedDB) { score -= 10; notes.push('IndexedDB available'); } 167 var dnt = b.platform && b.platform.doNotTrack; 168 if (dnt === '1' || dnt === 'yes') { score += 10; notes.push('DNT signal sent'); } 169 cat('Tracking', score, notes); 170 })(); 171 172 /* Identity: IP exposure + VPN/proxy status */ 173 (function () { 174 var score = 100, notes = []; 175 if (geo && geo.ok) { 176 score -= 40; notes.push('IP geolocated to ' + (geo.city || geo.country || 'a location')); 177 if (geo.proxy || geo.vpn) { score += 25; notes.push('Behind VPN/proxy'); } 178 else if (geo.hosting) { score += 15; notes.push('Hosting/datacenter IP'); } 179 else { notes.push('Residential IP (directly identifiable)'); } 180 } else { 181 notes.push('IP not geolocated'); 182 } 183 cat('Identity', score, notes); 184 })(); 185 186 /* Fingerprint: how identifiable */ 187 (function () { 188 var notes = []; 189 var bits = fp && fp.entropyBits ? fp.entropyBits : 0; 190 // 40 bits ~ fully unique; invert to a protection score. 191 var score = 100 - Math.min(100, (bits / 40) * 100); 192 notes.push('~' + bits + ' bits of entropy'); 193 if (fp && fp.trackingRisk) notes.push('Risk: ' + fp.trackingRisk); 194 cat('Fingerprint', score, notes); 195 })(); 196 197 /* Browser security: HTTPS + up-to-date-ish + webdriver */ 198 (function () { 199 var score = 100, notes = []; 200 if (!(s.request && s.request.is_https)) { score -= 40; notes.push('Connection is plaintext HTTP'); } 201 else notes.push('HTTPS in use'); 202 if (b.platform && b.platform.webdriver) { score -= 10; notes.push('Automation flag set'); } 203 if (b.ua && b.ua.browser === 'Internet Explorer') { score -= 30; notes.push('Ancient browser'); } 204 cat('Browser Security', score, notes); 205 })(); 206 207 /* Permissions: fewer granted = better */ 208 (function () { 209 var score = 100, notes = []; 210 var perms = b.permissions || {}; 211 Object.keys(perms).forEach(function (k) { 212 if (perms[k] === 'granted') { score -= 15; notes.push(k + ' granted'); } 213 }); 214 if (notes.length === 0) notes.push('No sensitive permissions granted'); 215 cat('Permissions', score, notes); 216 })(); 217 218 /* Network: VPN + WebRTC leak surface */ 219 (function () { 220 var score = 100, notes = []; 221 if (b.apis && b.apis.webRTC) { score -= 25; notes.push('WebRTC present (possible IP leak)'); } 222 if (s.network && s.network.behind_cloudflare) { notes.push('Behind Cloudflare'); } 223 if (geo && (geo.proxy || geo.vpn)) { score += 10; notes.push('VPN/proxy active'); } 224 cat('Network', score, notes); 225 })(); 226 227 var overall = Math.round(cats.reduce(function (a, c) { return a + c.score; }, 0) / cats.length); 228 return { score: overall, categories: cats }; 229 }; 230 231 /* ====================================================================== 232 FUN FACTS 233 ====================================================================== */ 234 EYES.funFacts = function (report) { 235 var b = report.browser || {}, fp = report.fingerprint || {}, s = report.server || {}; 236 var geo = s.geo || {}; 237 var facts = []; 238 function push(f) { if (f) facts.push(f); } 239 240 if (b.preferences && b.preferences.darkMode) push('Your browser prefers dark mode -- a person of taste.'); 241 if (b.preferences && b.preferences.reducedMotion) push('You have "reduce motion" enabled; the blinking cursor apologizes.'); 242 if (b.platform && b.platform.cores) push('You are exposing ' + b.platform.cores + ' CPU cores to every website you visit.'); 243 if (b.platform && b.platform.memory) push('Your device reports roughly ' + b.platform.memory + ' GB of RAM to the web.'); 244 if (b.codecs && /probably|maybe/.test(b.codecs.av1)) push('Your browser can decode AV1 video -- fairly modern hardware.'); 245 if (b.webgl && b.webgl.webgl2) push('Your GPU supports WebGL2: ' + (b.webgl.renderer || 'a real GPU') + '.'); 246 if (b.webgl && b.webgl.renderer) push('Websites can read your exact GPU: "' + b.webgl.renderer + '".'); 247 if (fp && fp.supported && fp.supported.canvas) push('Canvas fingerprinting works in your browser -- you can be tracked without cookies.'); 248 if (fp && fp.entropyBits) push('Your fingerprint carries about ' + fp.entropyBits + ' bits of entropy (1 in ' + fp.oneIn.toLocaleString() + ').'); 249 if (b.fonts && b.fonts.length) push('We detected ' + b.fonts.length + ' installed fonts -- a surprisingly personal detail.'); 250 if (b.screen && b.screen.width) push('Your screen is ' + b.screen.width + 'x' + b.screen.height + ' at ' + (b.screen.pixelRatio || 1) + 'x pixel ratio.'); 251 if (b.time && b.time.timezone) push('Your timezone gives you away as ' + b.time.timezone + '.'); 252 if (geo && geo.ok && geo.city) push('Your IP places you near ' + geo.city + ', ' + (geo.country || '') + '.'); 253 if (geo && geo.ok && geo.isp) push('Your connection appears to run through ' + geo.isp + '.'); 254 if (geo && (geo.proxy || geo.vpn)) push('Nice: you appear to be behind a VPN or proxy.'); 255 if (b.apis && b.apis.webRTC) push('WebRTC is enabled -- a classic way to leak your real IP past a VPN.'); 256 if (b.apis && b.apis.bluetooth) push('The Web Bluetooth API is available in your browser.'); 257 if (b.apis && b.apis.usb) push('The WebUSB API is available -- pages can request access to USB devices.'); 258 if (b.platform && b.platform.languages && b.platform.languages.length) push('You advertise these languages: ' + b.platform.languages.join(', ') + '.'); 259 if (b.network && b.network.effectiveType) push('Your connection reports itself as roughly "' + b.network.effectiveType + '".'); 260 if (b.battery) push('Your battery is at ' + b.battery.level + '% and ' + (b.battery.charging ? 'charging' : 'discharging') + '.'); 261 if (b.platform && b.platform.doNotTrack === '1') push('You send Do Not Track. Most sites will politely ignore it.'); 262 263 // Guarantee at least ten. 264 var filler = [ 265 'Everything on this page was computed on your machine, not ours.', 266 'No cookie was set to show you any of this.', 267 'This report will vanish the moment you close the tab.', 268 'A plain <marquee> tag is scrolling above. Respect.', 269 'The whole site is a few PHP files and some vanilla JavaScript.' 270 ]; 271 var i = 0; 272 while (facts.length < 10 && i < filler.length) push(filler[i++]); 273 return facts; 274 }; 275 276 /* ====================================================================== 277 DASHBOARD RENDERING 278 ====================================================================== */ 279 function panel(title, iconHtml, bodyHtml) { 280 return '<div class="panel"><div class="panel-title">' + (iconHtml || '') + 281 ' ' + esc(title) + '</div><div class="panel-body">' + bodyHtml + '</div></div>'; 282 } 283 function icon(name, color) { 284 // Uses the same 8x8 grids as PHP by drawing simple inline SVG here too. 285 // For simplicity in JS we reuse a filled box; PHP-rendered icons appear in 286 // the static page chrome. Keep it lightweight. 287 color = color || '#00FF00'; 288 return '<span style="display:inline-block;width:12px;height:12px;background:' + color + ';vertical-align:middle"></span>'; 289 } 290 function riskLine(key) { 291 var r = EYES.riskInfo[key]; 292 if (!r) return ''; 293 return '<details><summary>' + riskBadge(r.level) + ' ' + esc(r.what) + '</summary>' + 294 '<div class="details-body">' + 295 '<div><b>Why sites know it:</b> ' + esc(r.why) + '</div>' + 296 '<div class="impact"><b>Privacy impact:</b> ' + esc(r.impact) + '</div>' + 297 '<div class="fix"><b>How to reduce it:</b> ' + esc(r.fix) + '</div>' + 298 '</div></details>'; 299 } 300 301 EYES.renderDashboard = function (el, report) { 302 if (!el) return; 303 var s = report.server || {}, b = report.browser || {}, fp = report.fingerprint || {}; 304 var geo = s.geo || {}, req = s.request || {}, net = s.network || {}; 305 var html = []; 306 307 /* ----- GENERAL ----- */ 308 html.push(panel('General', icon('globe', '#00FFFF'), kvTable([ 309 row('Your IP Address', '<b class="yellow">' + esc(s.ip || req.ip || '?') + '</b>'), 310 row('Connection', (req.is_https ? '<span class="val-yes">HTTPS (encrypted)</span>' : '<span class="val-no">HTTP (plaintext)</span>')), 311 row('Protocol', esc(req.protocol || '?')), 312 row('Host', esc((s.server && s.server.host) || '?')), 313 row('Server Software', esc((s.server && s.server.software) || '?')), 314 row('Report Generated', esc(report.ts)) 315 ]) + riskLine('ip'))); 316 317 /* ----- NETWORK / GEO ----- */ 318 var geoRows = []; 319 if (geo && geo.ok) { 320 geoRows = [ 321 row('Country', esc(geo.country) + (geo.country_code ? ' (' + esc(geo.country_code) + ')' : '')), 322 row('City / Region', esc([geo.city, geo.region].filter(Boolean).join(', ') || '?')), 323 row('Continent', esc(geo.continent || '?')), 324 row('Coordinates', geo.lat != null ? esc(geo.lat + ', ' + geo.lon) : '<span class="val-unk">Unknown</span>'), 325 row('Timezone', esc(geo.timezone || '?')), 326 row('Currency', esc(geo.currency || '?')), 327 row('ISP', esc(geo.isp || '?')), 328 row('Organization', esc(geo.org || '?')), 329 row('ASN', esc(geo.asn || '?') + (geo.as_name ? ' (' + esc(geo.as_name) + ')' : '')), 330 row('VPN / Proxy', yn(geo.proxy || geo.vpn)), 331 row('Hosting / Datacenter', yn(geo.hosting)), 332 row('Tor Exit', yn(geo.tor)), 333 row('Mobile Network', yn(geo.mobile)), 334 row('Lookup Provider', esc(geo.provider || '?')) 335 ]; 336 } else { 337 geoRows = [row('Geolocation', '<span class="val-unk">' + esc((geo && geo.note) || 'Unavailable') + '</span>')]; 338 } 339 geoRows.push(row('Behind Proxy', yn(net.behind_proxy))); 340 geoRows.push(row('Behind Cloudflare', yn(net.behind_cloudflare))); 341 if (b.network && b.network.supported) { 342 geoRows.push(row('Connection Type', esc(b.network.effectiveType || b.network.type || '?'))); 343 geoRows.push(row('Downlink (Mbps)', b.network.downlink != null ? esc(b.network.downlink) : '<span class="val-unk">Unknown</span>')); 344 geoRows.push(row('RTT (ms)', b.network.rtt != null ? esc(b.network.rtt) : '<span class="val-unk">Unknown</span>')); 345 geoRows.push(row('Save-Data', yn(b.network.saveData))); 346 } 347 html.push(panel('Network', icon('globe'), kvTable(geoRows) + riskLine('geo') + riskLine('isp'))); 348 349 /* ----- BROWSER ----- */ 350 if (b.ua) { 351 html.push(panel('Browser', icon('monitor', '#00FFFF'), kvTable([ 352 row('Browser', '<b>' + esc(b.ua.browser) + ' ' + esc(b.ua.version) + '</b>'), 353 row('Engine', esc(b.ua.engine)), 354 row('Operating System', esc(b.ua.os) + ' ' + esc(b.ua.osVersion)), 355 row('Platform', esc(b.platform.platform)), 356 row('Architecture', esc(b.platform.architecture)), 357 row('Languages', esc((b.platform.languages || []).join(', '))), 358 row('Vendor', esc(b.ua.vendor || '?')), 359 row('Cookies Enabled', yn(b.apis.cookies)), 360 row('Do Not Track', esc(b.platform.doNotTrack || '(not set)')), 361 row('WebDriver (automation)', yn(b.platform.webdriver)), 362 row('User-Agent', '<span style="font-size:14px">' + esc(b.ua.raw) + '</span>') 363 ]) + riskLine('ua') + riskLine('cookies') + riskLine('dnt'))); 364 } 365 366 /* ----- DISPLAY ----- */ 367 if (b.screen) { 368 html.push(panel('Display', icon('monitor'), kvTable([ 369 row('Screen Resolution', esc(b.screen.width + ' x ' + b.screen.height)), 370 row('Available Screen', esc(b.screen.availWidth + ' x ' + b.screen.availHeight)), 371 row('Viewport', esc(b.screen.viewportW + ' x ' + b.screen.viewportH)), 372 row('Pixel Ratio', esc(b.screen.pixelRatio)), 373 row('Color Depth', esc(b.screen.colorDepth + '-bit')), 374 row('Orientation', esc(b.screen.orientation || '?')), 375 row('Dark Mode', yn(b.preferences.darkMode)), 376 row('Reduced Motion', yn(b.preferences.reducedMotion)), 377 row('Touch Support', yn(b.preferences.touch)), 378 row('Fine Pointer', yn(b.preferences.pointerFine)) 379 ]) + riskLine('screen'))); 380 } 381 382 /* ----- HARDWARE ----- */ 383 if (b.platform) { 384 var hwRows = [ 385 row('CPU Cores', esc(b.platform.cores || 'Unknown')), 386 row('Device Memory', b.platform.memory ? esc(b.platform.memory + ' GB (approx)') : '<span class="val-unk">Unknown</span>'), 387 row('Max Touch Points', esc(b.platform.maxTouchPoints)), 388 row('JS Heap Limit', b.jsHeapLimit ? esc(Math.round(b.jsHeapLimit / 1048576) + ' MB') : '<span class="val-unk">Unknown</span>') 389 ]; 390 if (b.battery) { 391 hwRows.push(row('Battery Level', esc(b.battery.level + '%'))); 392 hwRows.push(row('Battery Charging', yn(b.battery.charging))); 393 } 394 if (b.mediaDevices && b.mediaDevices.supported) { 395 hwRows.push(row('Cameras', esc(b.mediaDevices.cameras))); 396 hwRows.push(row('Microphones', esc(b.mediaDevices.microphones))); 397 hwRows.push(row('Speakers', esc(b.mediaDevices.speakers))); 398 } 399 html.push(panel('Hardware', icon('chip', '#FFFF00'), kvTable(hwRows) + riskLine('hardware'))); 400 } 401 402 /* ----- GRAPHICS ----- */ 403 if (b.webgl) { 404 var gRows = [ 405 row('WebGL', yn(b.webgl.supported)), 406 row('WebGL2', yn(b.webgl.webgl2)), 407 row('GPU Vendor', esc(b.webgl.vendor || '?')), 408 row('GPU Renderer', esc(b.webgl.renderer || '?')), 409 row('GLSL Version', esc(b.webgl.shading || '?')), 410 row('Max Texture Size', esc(b.webgl.maxTexture || '?')), 411 row('WebGL Extensions', esc(b.webgl.extensions)) 412 ]; 413 if (b.webgpu) gRows.push(row('WebGPU', yn(b.webgpu.supported) + (b.webgpu.vendor ? ' (' + esc(b.webgpu.vendor) + ')' : ''))); 414 html.push(panel('Graphics', icon('chip'), kvTable(gRows) + riskLine('webgl'))); 415 } 416 417 /* ----- STORAGE ----- */ 418 if (b.storage) { 419 html.push(panel('Storage', icon('disk', '#00FFAA'), kvTable([ 420 row('Cookies', yn(b.storage.cookiesEnabled)), 421 row('localStorage', yn(b.storage.localStorage)), 422 row('sessionStorage', yn(b.storage.sessionStorage)), 423 row('IndexedDB', yn(b.storage.indexedDB)), 424 row('Cache API', yn(b.storage.cacheAPI)), 425 row('CookieStore API', yn(b.storage.cookieStore)), 426 row('Estimated Quota', (b.storageQuota && b.storageQuota.quota) ? esc(Math.round(b.storageQuota.quota / 1048576) + ' MB') : '<span class="val-unk">Unknown</span>'), 427 row('Estimated Usage', (b.storageQuota && b.storageQuota.usage != null) ? esc(Math.round(b.storageQuota.usage / 1024) + ' KB') : '<span class="val-unk">Unknown</span>') 428 ]))); 429 } 430 431 /* ----- PERMISSIONS ----- */ 432 if (b.permissions) { 433 var pRows = Object.keys(b.permissions).map(function (k) { 434 var st = b.permissions[k]; 435 var cls = st === 'granted' ? 'val-yes' : st === 'denied' ? 'val-no' : 'val-unk'; 436 return row(k, '<span class="' + cls + '">' + esc(st) + '</span>'); 437 }); 438 if (!pRows.length) pRows = [row('Permissions API', '<span class="val-unk">Not available</span>')]; 439 html.push(panel('Permissions', icon('key', '#FFFF00'), kvTable(pRows) + riskLine('permissions'))); 440 } 441 442 /* ----- SECURITY / HEADERS ----- */ 443 var hdrs = s.headers || {}; 444 var hdrRows = Object.keys(hdrs).map(function (k) { 445 return '<tr><td class="k">' + esc(k) + '</td><td class="v" style="font-size:14px">' + esc(hdrs[k]) + '</td></tr>'; 446 }); 447 html.push(panel('Security & Headers', icon('lock', '#00FF00'), 448 kvTable([ 449 row('HTTPS', yn(req.is_https)), 450 row('WebRTC Present', yn(b.apis && b.apis.webRTC)), 451 row('Service Workers', yn(b.apis && b.apis.serviceWorker)), 452 row('Notifications API', yn(b.apis && b.apis.notification)), 453 row('Credentials API', yn(b.apis && b.apis.credentials)) 454 ]) + riskLine('webrtc') + 455 '<details><summary>' + riskBadge('LOW') + ' All request headers (' + hdrRows.length + ')</summary>' + 456 '<div class="details-body"><table class="data">' + hdrRows.join('') + '</table></div></details>' 457 )); 458 459 /* ----- FINGERPRINT ----- */ 460 if (fp && fp.id) { 461 var sigRows = Object.keys(fp.signals).map(function (k) { 462 return row(k, '<span class="mono" style="font-size:13px">' + esc(fp.signals[k].short) + '...</span> ' + 463 (fp.signals[k].value && fp.signals[k].value !== '(none)' ? '' : '<span class="val-unk">(unavailable)</span>')); 464 }); 465 html.push(panel('Fingerprint', icon('eye', '#FF3030'), 466 '<div class="badbx"><b>Your fingerprint ID (SHA-256):</b><br>' + 467 '<span class="mono" style="font-size:14px;word-break:break-all">' + esc(fp.id) + '</span></div>' + 468 kvTable([ 469 row('Estimated Entropy', esc(fp.entropyBits + ' bits')), 470 row('Approx. Uniqueness', asciiBar(fp.uniqueness)), 471 row('You are ~1 in', esc(fp.oneIn.toLocaleString())), 472 row('Tracking Risk', riskBadge(fp.trackingRisk)) 473 ]) + 474 '<h4>Per-signal hashes</h4>' + kvTable(sigRows) + 475 riskLine('canvas') + riskLine('webgl') + riskLine('audio') + riskLine('fonts') 476 )); 477 } 478 479 /* ----- EXPERIMENTAL APIS ----- */ 480 if (b.apis) { 481 var expKeys = ['bluetooth','usb','hid','serial','nfc','webGPU','webXR','wakeLock', 482 'idleDetection','fileSystemAccess','eyeDropper','share','contacts','virtualKeyboard', 483 'ambientLight','accelerometer','gyroscope','magnetometer','speechRecognition', 484 'speechSynthesis','paymentRequest','vibration']; 485 var expRows = expKeys.map(function (k) { return row(k, yn(b.apis[k])); }); 486 html.push(panel('Experimental APIs', icon('plug', '#FF3030'), kvTable(expRows))); 487 } 488 489 /* ----- CODECS & FORMATS ----- */ 490 if (b.codecs) { 491 var cRows = Object.keys(b.codecs).map(function (k) { 492 var v = b.codecs[k]; 493 var ok = v && v !== 'no'; 494 return row(k.toUpperCase(), ok ? '<span class="val-yes">' + esc(v) + '</span>' : '<span class="val-no">no</span>'); 495 }); 496 var iRows = Object.keys(b.imageFormats).map(function (k) { return row(k.toUpperCase(), yn(b.imageFormats[k])); }); 497 html.push(panel('Codecs & Image Formats', icon('monitor'), 498 '<h4>Video / Audio Codecs</h4>' + kvTable(cRows) + 499 '<h4>Image Formats</h4>' + kvTable(iRows))); 500 } 501 502 /* ----- FONTS ----- */ 503 if (b.fonts && b.fonts.length) { 504 html.push(panel('Detected Fonts (' + b.fonts.length + ')', icon('folder'), 505 '<div style="font-size:15px">' + b.fonts.map(esc).join(' · ') + '</div>' + riskLine('fonts'))); 506 } 507 508 /* ----- PRIVACY SCORE ----- */ 509 var priv = EYES.computePrivacy(report); 510 var scoreClass = priv.score >= 66 ? '' : priv.score >= 33 ? 'warn' : 'bad'; 511 var catRows = priv.categories.map(function (c) { 512 var cc = c.score >= 66 ? '' : c.score >= 33 ? 'warn' : 'bad'; 513 return '<tr><td class="k">' + esc(c.name) + '</td><td class="v">' + 514 '<div class="bar ' + cc + '"><div class="bar-fill" style="width:' + c.score + '%"></div>' + 515 '<div class="bar-label">' + c.score + '/100</div></div>' + 516 '<div class="dim" style="font-size:13px">' + esc(c.notes.join('; ')) + '</div></td></tr>'; 517 }); 518 html.unshift(panel('Privacy Score', icon('lock', '#00FF00'), 519 '<div class="center"><pre class="' + (priv.score >= 66 ? 'green' : priv.score >= 33 ? 'yellow' : 'red') + 520 '" style="font-size:22px;margin:0">OVERALL: ' + priv.score + ' / 100</pre></div>' + 521 '<div class="bar ' + scoreClass + '" style="height:26px"><div class="bar-fill" style="width:' + priv.score + '%"></div>' + 522 '<div class="bar-label" style="font-size:16px">' + priv.score + ' / 100</div></div>' + 523 '<table class="data kv" style="margin-top:8px">' + catRows.join('') + '</table>' + 524 '<p class="dim">Higher is better. 100 = well protected, 0 = fully exposed. This is a rough, honest heuristic, not a certification.</p>' 525 )); 526 527 /* ----- FUN FACTS ----- */ 528 var facts = EYES.funFacts(report); 529 html.push(panel('Fun Facts About You', icon('warning', '#FFFF00'), 530 '<ul class="facts">' + facts.map(function (f) { return '<li>' + esc(f) + '</li>'; }).join('') + '</ul>')); 531 532 el.innerHTML = html.join('\n'); 533 }; 534 535 /* ====================================================================== 536 BOOT ANIMATION 537 ====================================================================== */ 538 EYES.runBoot = function (termEl, onDone) { 539 if (!termEl) { if (onDone) onDone(); return; } 540 var lines = [ 541 { t: 'Initializing...', d: 250 }, 542 { t: 'Loading browser modules...', d: 350, ok: true }, 543 { t: 'Enumerating HTTP headers...', d: 400, ok: true }, 544 { t: 'Analyzing network...', d: 450, ok: true }, 545 { t: 'Resolving IP intelligence...', d: 500, ok: true }, 546 { t: 'Probing 60+ Web APIs...', d: 450, ok: true }, 547 { t: 'Generating browser fingerprint...', d: 550, ok: true }, 548 { t: 'Hashing signals (SHA-256)...', d: 400, ok: true }, 549 { t: 'Calculating privacy score...', d: 400, ok: true }, 550 { t: 'Generating report...', d: 350, ok: true } 551 ]; 552 termEl.innerHTML = '<div class="dim">WhatCanILearn scanner v1.0 -- starting...</div>'; 553 var i = 0; 554 function step() { 555 if (i >= lines.length) { 556 var d = document.createElement('div'); 557 d.innerHTML = '<span class="ok">All modules complete.</span> Rendering dashboard <span class="cursor"></span>'; 558 termEl.appendChild(d); 559 termEl.scrollTop = termEl.scrollHeight; 560 setTimeout(function () { if (onDone) onDone(); }, 500); 561 return; 562 } 563 var ln = lines[i++]; 564 var div = document.createElement('div'); 565 div.innerHTML = '<span class="prompt">$</span> ' + esc(ln.t) + ' <span class="typing"></span>'; 566 termEl.appendChild(div); 567 termEl.scrollTop = termEl.scrollHeight; 568 setTimeout(function () { 569 div.innerHTML = '<span class="prompt">$</span> ' + esc(ln.t) + 570 (ln.ok !== false ? ' <span class="ok">[ DONE ]</span>' : ' <span class="warn">[ .. ]</span>'); 571 termEl.scrollTop = termEl.scrollHeight; 572 step(); 573 }, ln.d); 574 } 575 step(); 576 }; 577 578 /* ====================================================================== 579 AUTO-WIRE the scan page if the expected containers exist. 580 ====================================================================== */ 581 EYES.autoScan = function () { 582 var termEl = document.getElementById('bootTerminal'); 583 var dashEl = document.getElementById('dashboard'); 584 if (!termEl || !dashEl) return; 585 EYES.runBoot(termEl, function () { 586 EYES.buildReport().then(function (report) { 587 EYES.renderDashboard(dashEl, report); 588 var status = document.getElementById('scanStatus'); 589 if (status) status.innerHTML = '<span class="ok">Scan complete.</span> ' + 590 'Nothing was stored. <a href="' + (EYES.BASE || '') + '/fingerprint.php">See your fingerprint</a> · ' + 591 '<a href="' + (EYES.BASE || '') + '/privacy.php">Privacy score</a>'; 592 var exp = document.getElementById('exportBar'); 593 if (exp) exp.style.display = 'block'; 594 }).catch(function (err) { 595 dashEl.innerHTML = '<div class="badbx">Scan failed: ' + esc(err && err.message ? err.message : err) + '</div>'; 596 }); 597 }); 598 }; 599 600 })(); 601 You are reading the real file being executed on the server. What you see is what runs. |