<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Fingerprint</title>
<style>
  * { margin: 0; padding: 0; box-sizing: border-box; }
  body { font-family: 'SF Mono', 'Cascadia Code', 'Consolas', monospace; background: #0a0a0a; color: #e0e0e0; padding: 20px; max-width: 960px; margin: 0 auto; }
  h1 { color: #7cc5ff; margin-bottom: 4px; font-size: 1.4em; }
  h2 { color: #888; font-size: 0.95em; margin-bottom: 20px; font-weight: normal; }
  .section { background: #141414; border: 1px solid #2a2a2a; border-radius: 8px; padding: 16px; margin-bottom: 16px; overflow-x: auto; }
  .section-title { color: #7cc5ff; font-size: 1em; margin-bottom: 12px; }
  .fp-box { background: #1a1a2e; border: 1px solid #333366; border-radius: 6px; padding: 12px; margin-bottom: 8px; word-break: break-all; font-size: 0.85em; }
  .fp-label { color: #888; font-size: 0.8em; margin-bottom: 2px; }
  .fp-value { color: #ffcc57; font-size: 1.1em; font-weight: bold; word-break: break-all; font-size: 0.75em; }
  table { width: 100%; border-collapse: collapse; table-layout: fixed; }
  th { text-align: left; color: #888; font-size: 0.8em; padding: 6px 8px; border-bottom: 1px solid #2a2a2a; }
  th:nth-child(1) { width: 35%; }
  th:nth-child(2) { width: 50%; }
  th:nth-child(3) { width: 15%; }
  td { padding: 6px 8px; font-size: 0.85em; border-bottom: 1px solid #1a1a1a; }
  td:nth-child(1) { white-space: nowrap; }
  td:nth-child(2) { word-break: break-all; }
  .tag { display: inline-block; padding: 1px 6px; border-radius: 3px; font-size: 0.75em; white-space: nowrap; }
  .tag-stable { background: #1a3a1a; color: #66cc66; }
  .tag-volatile { background: #3a1a1a; color: #cc6666; }
  .tag-unknown { background: #2a2a1a; color: #cccc66; }
  .js-section { margin-top: 8px; }
  .js-row { display: flex; justify-content: space-between; padding: 4px 0; border-bottom: 1px solid #1a1a1a; gap: 8px; }
  .js-key { color: #888; flex-shrink: 0; }
  .js-val { color: #e0e0e0; text-align: right; word-break: break-all; min-width: 0; }
  #canvas-fp, #webgl-fp { display: none; }
  .loading { color: #888; font-style: italic; }

  @media (max-width: 600px) {
    body { padding: 10px; }
    h1 { font-size: 1.1em; }
    h2 { font-size: 0.8em; margin-bottom: 14px; }
    .section { padding: 10px; margin-bottom: 10px; }
    .fp-box { padding: 8px; }
    .fp-value { font-size: 0.65em; }
    .fp-label { font-size: 0.7em; }
    table { display: block; }
    thead { display: none; }
    tr { display: flex; flex-direction: column; padding: 8px 0; border-bottom: 1px solid #2a2a2a; }
    td { padding: 2px 0; font-size: 0.8em; }
    td:first-child { color: #7cc5ff; font-weight: bold; }
    td:last-child { margin-top: 2px; }
    .js-row { flex-direction: column; gap: 2px; }
    .js-val { text-align: left; font-size: 0.8em; }
    .js-key { font-size: 0.8em; }
  }
</style>
</head>
<body>
<h1>Browser Fingerprint</h1>
<h2>Headers + JS-based identification</h2>

<div class="section">
  <div class="section-title">Fingerprint Hashes</div>
  <div class="fp-box">
    <div class="fp-label">Header fingerprint (stable headers only)</div>
    <div class="fp-value" id="header-fp">loading...</div>
  </div>
  <div class="fp-box">
    <div class="fp-label">JS fingerprint (canvas + webgl + screen + fonts)</div>
    <div class="fp-value" id="js-fp">computing...</div>
  </div>
  <div class="fp-box">
    <div class="fp-label">Combined fingerprint</div>
    <div class="fp-value" id="combined-fp">computing...</div>
  </div>
</div>

<div class="section">
  <div class="section-title">HTTP Headers</div>
  <table>
    <tr><th>Name</th><th>Value</th><th>Type</th></tr>
    <tbody id="headers-table"></tbody>
  </table>
</div>

<div class="section">
  <div class="section-title">JS Signals</div>
  <div id="js-signals" class="js-section"><span class="loading">computing...</span></div>
</div>

<canvas id="canvas-fp" width="280" height="40"></canvas>
<canvas id="webgl-fp" width="1" height="1"></canvas>

<script>
async function main() {
  // 1. Fetch header data from /api/fingerprint
  const resp = await fetch('/api/fingerprint');
  const data = await resp.json();

  document.getElementById('header-fp').textContent = data.header_fingerprint;

  const tbody = document.getElementById('headers-table');
  for (const h of data.headers) {
    const tr = document.createElement('tr');
    const tagClass = 'tag-' + h.stability;
    const nameCell = document.createElement('td');
    nameCell.textContent = h.name;
    const valueCell = document.createElement('td');
    valueCell.textContent = h.value;
    const typeCell = document.createElement('td');
    const tag = document.createElement('span');
    tag.className = 'tag ' + tagClass;
    tag.textContent = h.stability;
    typeCell.appendChild(tag);
    tr.appendChild(nameCell);
    tr.appendChild(valueCell);
    tr.appendChild(typeCell);
    tbody.appendChild(tr);
  }

  // 2. Compute JS fingerprint
  const jsSignals = collectJsSignals();
  const jsContainer = document.getElementById('js-signals');
  jsContainer.innerHTML = '';
  const entries = Object.entries(jsSignals);
  for (const [k, v] of entries) {
    const row = document.createElement('div');
    row.className = 'js-row';
    row.innerHTML = `<span class="js-key">${k}</span><span class="js-val">${v}</span>`;
    jsContainer.appendChild(row);
  }

  // 3. Hash JS signals
  const jsString = entries.map(([k,v]) => k + '=' + v).join('|');
  const jsFp = await sha256(jsString);
  document.getElementById('js-fp').textContent = jsFp;

  // 4. Combined
  const combined = await sha256(data.header_fingerprint + ':' + jsFp);
  document.getElementById('combined-fp').textContent = combined;
}

function collectJsSignals() {
  const s = {};
  s['screen'] = screen.width + 'x' + screen.height;
  s['colorDepth'] = screen.colorDepth;
  s['pixelRatio'] = window.devicePixelRatio;
  s['timezone'] = Intl.DateTimeFormat().resolvedOptions().timeZone;
  s['timezoneOffset'] = new Date().getTimezoneOffset();
  s['language'] = navigator.language;
  s['languages'] = (navigator.languages || []).join(',');
  s['platform'] = navigator.platform;
  s['hardwareConcurrency'] = navigator.hardwareConcurrency || 'n/a';
  s['deviceMemory'] = navigator.deviceMemory || 'n/a';
  s['maxTouchPoints'] = navigator.maxTouchPoints || 0;
  s['cookieEnabled'] = navigator.cookieEnabled;
  s['doNotTrack'] = navigator.doNotTrack || 'n/a';
  s['pdfViewer'] = navigator.pdfViewerEnabled || false;

  // Canvas fingerprint
  try {
    const canvas = document.getElementById('canvas-fp');
    const ctx = canvas.getContext('2d');
    ctx.textBaseline = 'top';
    ctx.font = '14px Arial';
    ctx.fillStyle = '#f60';
    ctx.fillRect(125, 1, 62, 20);
    ctx.fillStyle = '#069';
    ctx.fillText('Fingerprint!', 2, 15);
    ctx.fillStyle = 'rgba(102, 204, 0, 0.7)';
    ctx.fillText('Fingerprint!', 4, 17);
    s['canvas'] = canvas.toDataURL().slice(0, 100) + '...';
  } catch(e) {
    s['canvas'] = 'blocked';
  }

  // WebGL
  try {
    const gl = document.getElementById('webgl-fp').getContext('webgl');
    if (gl) {
      s['webglVendor'] = gl.getParameter(gl.VENDOR);
      s['webglRenderer'] = gl.getParameter(gl.RENDERER);
      const dbg = gl.getExtension('WEBGL_debug_renderer_info');
      if (dbg) {
        s['webglUnmaskedVendor'] = gl.getParameter(dbg.UNMASKED_VENDOR_WEBGL);
        s['webglUnmaskedRenderer'] = gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL);
      }
    }
  } catch(e) {
    s['webgl'] = 'blocked';
  }

  // Fonts detection (basic)
  try {
    const testFonts = ['monospace','sans-serif','serif','Arial','Courier New','Georgia','Times New Roman','Verdana','Helvetica','Comic Sans MS','Impact','Trebuchet MS'];
    const detected = [];
    const span = document.createElement('span');
    span.style.position = 'absolute';
    span.style.left = '-9999px';
    span.style.fontSize = '72px';
    span.textContent = 'mmmmmmmmlli';
    document.body.appendChild(span);
    const baseFonts = ['monospace', 'sans-serif', 'serif'];
    const baseWidths = {};
    for (const bf of baseFonts) {
      span.style.fontFamily = bf;
      baseWidths[bf] = span.offsetWidth;
    }
    for (const f of testFonts) {
      for (const bf of baseFonts) {
        span.style.fontFamily = `"${f}", ${bf}`;
        if (span.offsetWidth !== baseWidths[bf]) {
          detected.push(f);
          break;
        }
      }
    }
    document.body.removeChild(span);
    s['fonts'] = detected.join(',');
  } catch(e) {
    s['fonts'] = 'error';
  }

  return s;
}

async function sha256(str) {
  const buf = new TextEncoder().encode(str);
  const hash = await crypto.subtle.digest('SHA-256', buf);
  return Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, '0')).join('');
}

main();
</script>
</body>
</html>
