Full Code of jonasstrehle/supercookie for AI

main 0fad0559405e cached
18 files
133.7 KB
32.3k tokens
80 symbols
1 requests
Download .txt
Repository: jonasstrehle/supercookie
Branch: main
Commit: 0fad0559405e
Files: 18
Total size: 133.7 KB

Directory structure:
gitextract_i4uyxewp/

├── .github/
│   └── FUNDING.yml
├── .gitignore
├── LICENSE
├── README.md
└── server/
    ├── docker-compose.yml
    ├── main.js
    ├── main.ts
    ├── package.json
    ├── tsconfig.json
    └── www/
        ├── 404.html
        ├── identity.html
        ├── index.html
        ├── launch.html
        ├── redirect.html
        ├── referrer-v2.html
        ├── referrer.html
        ├── tsconfig.json
        └── workwise.html

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/FUNDING.yml
================================================
github: [jonasstrehle]
patreon: unyt
open_collective: #
ko_fi: jonasstrehle
tidelift: #
community_bridge: #
liberapay: #
issuehunt: #
otechie: #
custom: ['https://www.buymeacoffee.com/jonasstrehle']


================================================
FILE: .gitignore
================================================
.DS_Store
/server/data.json


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2021 Jonas Strehle

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
<p align="center">
  <a href="https://supercookie.me">
    <img src="http://supercookie.me/favicon.ico" alt="supercookie" width="100" />
  </a>
</p>
<p align="center">
  <a href="https://supercookie.me/workwise">
    <img src="https://img.shields.io/badge/Docs-supercookie.me-blue" alt="Documentation">
  </a>
</p>
<p align="center">
  <a href="https://supercookie.me">
    <img src="https://img.shields.io/website?down_message=down&up_color=green&up_message=online&url=https%3A%2F%2Fsupercookie.me" alt="Website Status">
  </a>
  <a href="https://github.com/jonasstrehle/supercookie">
    <img src="https://img.shields.io/github/license/jonasstrehle/supercookie" alt="License">
  </a>
</p>
<p align="center">
  <a href="https://supercookie.me">
    <img src="https://img.shields.io/badge/dynamic/json?label=Fingerprints&query=index&url=https://supercookie.me/api&color=yellow" alt="Fingerprint index">
  </a>
  <a href="https://supercookie.me">
    <img src="https://img.shields.io/badge/dynamic/json?label=Current%20redirects&query=bits&url=https://supercookie.me/api&color=yellow" alt="N Redirects">
  </a>
</p>


**Supercookie** uses favicons to assign a unique identifier to website visitors.<br>
Unlike traditional tracking methods, this ID can be stored almost persistently and cannot be easily cleared by the user.

The tracking method works even in the browser's incognito mode and is not cleared by flushing the cache, closing the browser or restarting the operating system, using a VPN or installing AdBlockers. 🍿 [Live demo](https://supercookie.me).

## About

### 💭 Inspiration

- Paper by Scientists at University of Illinois, Chicago: [par.nsf.gov](https://par.nsf.gov/servlets/purl/10268961)
- Article by heise: [heise.de](https://heise.de/-5027814) 

### 🌱 Purpose

This repository is for **educational** and **demonstration purposes** only!

The demo of "supercookie" as well as the publication of the source code of this repository is intended to draw attention to the problem of tracking possibilities using favicons.

📕 [Full documentation](https://supercookie.me/workwise)

## Installation

### 🔧 Docker
**requirements**: 
<img src="https://www.docker.com/wp-content/uploads/2022/03/vertical-logo-monochromatic.png" width="12"> [Docker daemon](https://docs.docker.com/get-docker/)

1. Clone repository
```bash
git clone https://github.com/jonasstrehle/supercookie
```

2. Update .env file in [supercookie/server/.env](https://github.com/jonasstrehle/supercookie/blob/main/server/.env)
```env
HOST_MAIN=yourdomain.com #or localhost:10080
PORT_MAIN=10080

HOST_DEMO=demo.yourdomain.com #or localhost:10081
PORT_DEMO=10081
```

3. Run container
```bash
cd supercookie/server
docker-compose up
```

-> Webserver will be running at https://yourdomain.com



### 🤖 Local machine
**requirements**: 
<img src="https://seeklogo.com/images/N/nodejs-logo-FBE122E377-seeklogo.com.png" width="12"> [Node.js](https://nodejs.org/)

1. Clone repository
```bash
git clone https://github.com/jonasstrehle/supercookie
```

2. Update .env file in [supercookie/server/.env](https://github.com/jonasstrehle/supercookie/blob/main/server/.env)
```env
HOST_MAIN=localhost:10080
PORT_MAIN=10080

HOST_DEMO=localhost:10081
PORT_DEMO=10081
```

3. Run service
```bash
cd supercookie/server
node --experimental-json-modules main.js
```

-> Webserver will be running at http://localhost:10080


## Workwise of [supercookie](https://supercookie.me/workwise)


### [📖 Background](https://supercookie.me/workwise#content-background)

Modern browsers offer a wide range of features to improve and simplify the user experience.
One of these features are the so-called favicons: A favicon is a small (usually 16×16 or 32×32 pixels) logo used by web browsers to brand a website in a recognizable way. Favicons are usually shown by most browsers in the address bar and next to the page's name in a list of bookmarks.

To serve a favicon on their website, a developer has to include an <link rel> attribute in the webpage’s header. If this tag does exist, the browser requests the icon from the predefined source and if the server response contains an valid icon file that can be properly rendered this icon is displayed by the browser. In any other case, a blank favicon is shown.

```html
<link rel="icon" href="/favicon.ico" type="image/x-icon">
```

The favicons must be made very easily accessible by the browser. Therefore, they are cached in a separate local database on the system, called the favicon cache (F-Cache). A F-Cache data entries includes the visited URL (subdomain, domain, route, URL paramter), the favicon ID and the time to live (TTL).
While this provides web developers the ability to delineate parts of their website using a wide variety of icons for individual routes and subdomains, it also leads to a possible tracking scenario.

When a user visits a website, the browser checks if a favicon is needed by looking up the source of the shortcut icon link reference of the requested webpage.
The browser initialy checks the local F-cache for an entry containing the URL of the active website. If a favicon entry exists, the icon will be loaded from the cache and then displayed. However, if there is no entry, for example because no favicon has ever been loaded under this particular domain, or the data in the cache is out of date, the browser makes a GET request to the server to load the site's favicon.


### [💣 Threat Model](https://supercookie.me/workwise#content-threat-model)

In the article a possible threat model is explained that allows to assign a unique identifier to each browser in order to draw conclusions about the user and to be able to identify this user even in case of applied anti-fingerprint measures, such as the use of a VPN, deletion of cookies, deletion of the browser cache or manipulation of the client header information.

A web server can draw conclusions about whether a browser has already loaded a favicon or not:
So when the browser requests a web page, if the favicon is not in the local F-cache, another request for the favicon is made. If the icon already exists in the F-Cache, no further request is sent.
By combining the state of delivered and not delivered favicons for specific URL paths for a browser, a unique pattern (identification number) can be assigned to the client.
When the website is reloaded, the web server can reconstruct the identification number with the network requests sent by the client for the missing favicons and thus identify the browser.




<p align="center">
  <a href="https://supercookie.me">
    <img src="https://supercookie.me/assets/header.png" alt="Supercookie Header" width="600" />
  </a>
</p>

<table>
  <thead>
    <tr>
      <th></th>
      <th align="center"><img width="350" height="0"> <p>conventional cookies</p></th>
      <th align="center"><img width="350" height="0"> <p>supercookie</p></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Identification accuracy</td>
      <td align="center">-</td>
      <td align="center">100%</td>
    </tr>
    <tr>
      <td>Incognito / Private mode detection</td>
      <td align="center">❌</td>
      <td align="center">✅</td>
    </tr>
    <tr>
      <td>Persistent after flushed website cache and cookies</td>
      <td align="center">❌</td>
      <td align="center">✅</td>
    </tr>
    <tr>
      <td>Identify multiple windows</td>
      <td align="center">❌</td>
      <td align="center">✅</td>
    </tr>
    <tr>
      <td>Working with Anti-Tracking SW</td>
      <td align="center">❌</td>
      <td align="center">✅</td>
    </tr>
  </tbody>
</table>


### [🎯 Target](https://supercookie.me/workwise#content-target)

It looks like all top browsers (<img src="https://www.google.com/favicon.ico" width="12"> [Chrome](https://google.com/chrome/), <img src="https://www.mozilla.org/favicon.ico" width="12"> [Firefox](https://www.mozilla.org/en-US/firefox/new/), <img src="https://www.apple.com/favicon.ico" width="12"> [Safari](https://www.apple.com/safari/), <img src="https://www.microsoft.com/favicon.ico" width="12"> [Edge](https://www.microsoft.com/edge/)) are vulnerable to this attack scenario.<br>
Mobile browsers are also affected.

#### Current versions

<table>
  <thead>
    <tr>
      <th align="center"><p>Browser</p></th>
      <th align="center"><p>Windows</p></th>
      <th align="center"><p>MacOS</p></th>
      <th align="center"><p>Linux</p></th>
      <th align="center"><p>iOS</p></th>
      <th align="center"><p>Android</p></th>
      <th align="center"><i>Info</i></th>
    </tr>
  </thead>
  <tbody>
    <tr>
        <td>Chrome <em>(v 111.0)</em></td>
        <td align="center">✅</td>
        <td align="center">✅</td>
        <td align="center">✅</td>
        <td align="center">?</td>
        <td align="center">✅</td>
        <td>-</td>
    </tr>
    <tr>
        <td>Safari <em>(v 14.0)</em></td>
        <td align="center">-</td>
        <td align="center">✅</td>
        <td align="center">-</td>
        <td align="center">✅</td>
        <td align="center">-</td>
        <td>-</td>
    </tr>
    <tr>
        <td>Edge <em>(v 87.0)</em></td>
        <td align="center">✅</td>
        <td align="center">✅</td>
        <td align="center">❌</td>
        <td align="center">❌</td>
        <td align="center">✅</td>
        <td>-</td>
    </tr>
    <tr>
        <td>Firefox <em>(v 86.0)</em></td>
        <td align="center">✅</td>
        <td align="center">✅</td>
        <td align="center">❌</td>
        <td align="center">❌</td>
        <td align="center">❌</td>
        <td>Fingerprint different in incognito mode</td>
    </tr>
    <tr>
        <td>Brave <em>(v 1.19.92)</em></td>
        <td align="center">❌</td>
        <td align="center">❌</td>
        <td align="center">❌</td>
        <td align="center">❔</td>
        <td align="center">❌</td>
        <td>-</td>
    </tr>
  </tbody>
</table>


#### Previous versions

<table>
  <thead>
    <tr>
      <th align="center"><p>Browser</p></th>
      <th align="center"><p>Windows</p></th>
      <th align="center"><p>MacOS</p></th>
      <th align="center"><p>Linux</p></th>
      <th align="center"><p>iOS</p></th>
      <th align="center"><p>Android</p></th>
      <th align="center"><i>Info</i></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><b>Brave</b> (v 1.14.0)</td>
      <td align="center">✅</td>
      <td align="center">✅</td>
      <td align="center">✅</td>
      <td align="center">✅</td>
      <td align="center">✅</td>
      <td>-</td>
    </tr>
    <tr>
      <td><b>Firefox</b> (&lt; v 84.0)</td>
      <td align="center">✅</td>
      <td align="center">✅</td>
      <td align="center">❔</td>
      <td align="center">❌</td>
      <td align="center">✅</td>
      <td>-</td>
    </tr>
  </tbody>
</table>


### [⚙ Scalability & Performance](https://supercookie.me/workwise#content-scalability-performance)

By varying the number of bits that corresponds to the number of redirects to subpaths, this attack can be scaled almost arbitrarily.
It can distinguish 2^N unique users, where N is the number of redirects on the client side.
The time taken for the read and write operation increases as the number of distinguishable clients does.
<br>
In order to keep the number of redirects as minimal as possible, N can have a dynamic length. 
More about this [here](https://supercookie.me/workwise#content-scalability-performance).

### [📌How to defend against?](https://supercookie.me/workwise)

The most straightforward solution is to disable the favicon cache completely. As long as the browser vendors do not provide a feature against this vulnerability it's probably the best way to clear the F-cache.

* [Chrome](https://www.google.com/chrome/) • **MacOS**<br>
  - Delete `~/Library/Application Support/Google/Chrome/Default/Favicons`
  - Delete `~/Library/Application Support/Google/Chrome/Default/Favicons-journal`

* [Chrome](https://www.google.com/chrome/) • **Windows**<br>
  - Delete `C:\Users\username\AppData\Local\Google\Chrome\User Data\Default`

* [Safari](https://www.apple.com/safari/) • **MacOS**<br>
  - Delete content of `~/Library/Safari/Favicon Cache`

* [Edge](https://www.microsoft.com/edge) • **MacOS**<br>
  - Delete `~/Library/Application Support/Microsoft Edge/Default/Favicon`
  - Delete `~/Library/Application Support/Microsoft Edge/Default/Favicons-journal`

## Other

### 🙎‍♂️ About me

I am a twenty five year old student from 🇩🇪 Germany. I like to work in software design and development and have an interest in the IT security domain.

This repository, including the setup of a demonstration portal, was created within two days as part of a private research project on the topic of "Tracking on the Web".


### [💖 Support the project](https://ko-fi.com/jonasstrehle)

[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/jonasstrehle)

## Spread the world!

Liked the project? Just give it a star ⭐ and spread the world!

* [Bruce Schneier on schneier.com](https://www.schneier.com/crypto-gram/archives/2021/0315.html#cg5)
* [Matthew Gault on vice.com](https://www.vice.com/amp/en/article/n7v5y7/browser-favicons-can-be-used-as-undeletable-supercookies-to-track-you-online?__twitter_impression=true)
* [Rhett Jones on gizmodo.com](https://gizmodo.com/favicons-could-be-the-supercookie-that-tracks-you-every-1846229089/)
* [Dev Kundaliyaon on computing.co.uk](https://www.computing.co.uk/news/4027035/tiny-favicons-utilised-track-users-movements-online)
* [Barclay Ballard on techradar.com](https://www.techradar.com/news/these-tiny-icons-could-be-tracking-you-across-the-internet)
* [Discussion on ycombinator.com](https://news.ycombinator.com/item?id=26051370)
* 🇩🇪 [Andreas Proschofsky on derstandard.de](https://www.derstandard.de/story/2000124123751/supercookies-datensammler-finden-immer-neue-wege-die-nutzer-auszuspionieren)
* 🇩🇪 [Dieter Petereit on t3n.de](https://t3n.de/news/tracking-id-favicons-supercookie-1355514/)
* 🇪🇸 [ALVY on microsiervos.com](https://www.microsiervos.com/archivo/seguridad/supercookie-me-identificador-personal-imborrable-icono-favicon.html)
* 🇧🇷 [Felipe Demartini on canaltech.com.br](https://canaltech.com.br/seguranca/favicons-podem-ser-usados-para-rastrear-usuarios-online-permanentemente-178834/)
* 🇧🇬 [Daniel Despodov on kaldata.com](https://www.kaldata.com/it-%D0%BD%D0%BE%D0%B2%D0%B8%D0%BD%D0%B8/%D0%BD%D0%BE%D0%B2-%D0%BC%D0%B5%D1%82%D0%BE%D0%B4-%D0%B7%D0%B0-%D0%B8%D0%B4%D0%B5%D0%BD%D1%82%D0%B8%D1%84%D0%B8%D0%BA%D0%B0%D1%86%D0%B8%D1%8F-%D0%BD%D0%B0-%D0%BA%D0%BE%D0%BD%D0%BA%D1%80%D0%B5%D1%82-355279.html)
* 🇫🇷 [Guillaume Belfiore on clubic.com](https://www.clubic.com/navigateur-internet/actualite-353236-publicite-les-favicons-des-sites-web-pourraient-se-montrer-un-peu-trop-curieux.html)
* 🇨🇳 [study875 on cnbeta.com (Via archive.org)](https://web.archive.org/web/20220701105509/http://cnbeta.com/articles/tech/1089095.htm)
* 🇷🇺 [ITSumma on habr.com](https://habr.com/ru/company/itsumma/blog/542734/)
* 🇷🇺 [securitylab.ru](https://www.securitylab.ru/news/516436.php)
* <img src="https://youtube.com/favicon.ico" width="20"> [Seytonic on YouTube](https://youtu.be/X7OW5hTt5hY)





================================================
FILE: server/docker-compose.yml
================================================
version: "3"

services:
  proxy:
    image: "traefik:v2.0"
    container_name: supercookie-proxy
    hostname: supercookie-proxy
    restart: always
    command:
      - --api=true
      - --api.insecure=true
      - --ping
      - --providers.docker=true
      - --providers.docker.network=main
      - --providers.docker.exposedbydefault=false
      - --entrypoints.web.address=:80
      - --entrypoints.web-secure.address=:443
      - --certificatesresolvers.myhttpchallenge.acme.httpchallenge=true
      - --certificatesresolvers.myhttpchallenge.acme.httpchallenge.entrypoint=web
      - --certificatesresolvers.myhttpchallenge.acme.caserver=https://acme-v02.api.letsencrypt.org/directory
      - --certificatesresolvers.myhttpchallenge.acme.email=postmaster@unyt.cc
      - --certificatesresolvers.myhttpchallenge.acme.storage=/letsencrypt/acme.json
    ports:
      - "80:80"
      - "443:443"
    expose:
      - 80
    networks:
      - "main"
      - "internal"
    volumes:
      - ./letsencrypt:/letsencrypt
      - /var/run/docker.sock:/var/run/docker.sock:ro

  supercookie:
    container_name: supercookie-web
    hostname: supercookie-web
    restart: always
    image: "node"
    build: .
    working_dir: /home/node/app
    volumes:
      - ./:/home/node/app
      - ./node_modules:/home/node/app/node_modules
      - ./tsconfig.json:/home/node/app/tsconfig.json
      - ./.env:/home/node/app/.env
    expose:
      - ${PORT_MAIN}
      - ${PORT_DEMO}
    labels:
      - "traefik.enable=true"
      - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"

      # web main
      - "traefik.http.services.service-supercookie-web-1.loadbalancer.server.port=${PORT_MAIN}"
      - "traefik.http.routers.supercookie-web-1.rule=Host(`${HOST_MAIN}`)"
      - "traefik.http.routers.supercookie-web-1.entrypoints=web"
      - "traefik.http.routers.supercookie-web-1.middlewares=redirect-to-https@docker"
      - "traefik.http.routers.supercookie-web-1.service=service-supercookie-web-1"

      - "traefik.http.routers.supercookie-web-1-secured.rule=Host(`${HOST_MAIN}`)"
      - "traefik.http.routers.supercookie-web-1-secured.tls=true"
      - "traefik.http.routers.supercookie-web-1-secured.tls.certresolver=myhttpchallenge"
      - "traefik.http.routers.supercookie-web-1-secured.service=service-supercookie-web-1"

      # web demo
      - "traefik.http.services.service-supercookie-web-2.loadbalancer.server.port=${PORT_DEMO}"
      - "traefik.http.routers.supercookie-web-2.rule=Host(`${HOST_DEMO}`)"
      - "traefik.http.routers.supercookie-web-2.entrypoints=web"
      - "traefik.http.routers.supercookie-web-2.middlewares=redirect-to-https@docker"
      - "traefik.http.routers.supercookie-web-2.service=service-supercookie-web-2"

      - "traefik.http.routers.supercookie-web-2-secured.rule=Host(`${HOST_DEMO}`)"
      - "traefik.http.routers.supercookie-web-2-secured.tls=true"
      - "traefik.http.routers.supercookie-web-2-secured.tls.certresolver=myhttpchallenge"
      - "traefik.http.routers.supercookie-web-2-secured.service=service-supercookie-web-2"
    command: bash -c "node --experimental-json-modules ./main.js"
    networks:
      - main
      - internal

networks:
  main:
    external: true
  internal:

================================================
FILE: server/main.js
================================================
import express from "express";
import path from "path";
import fs from "fs";
import cookieParser from "cookie-parser";
import crypto from "crypto";
import cors from "cors";
import dotenv from "dotenv";
const generateUUID = (pattern = "xxxx-xxxx-xxxx-xxxx-xxxx", charset = "abcdefghijklmnopqrstuvwxyz0123456789") => pattern.replace(/[x]/g, () => charset[Math.floor(Math.random() * charset.length)]);
const hashNumber = (value) => crypto.createHash("MD5")
    .update(value.toString())
    .digest("hex").slice(-12).split(/(?=(?:..)*$)/)
    .join(' ').toUpperCase();
const createRoutes = (base, count) => {
    const array = [];
    for (let i = 0; i < count; i++)
        array.push(crypto.createHash("MD5")
            .update(`${base}${i.toString()}`).digest("base64")
            .replace(/(\=|\+|\/)/g, '0').substring(0, 22));
    return array;
};
class Storage {
    constructor() {
        this._path = path.join(path.resolve(), "data.json");
        this._content = {};
        if (!this.existsPersistent())
            this.createPersistent();
        this.read();
    }
    get content() {
        return this._contentProxy;
    }
    set content(data) {
        this._content = data;
        const _this = this;
        const proxy = {
            get(target, key) {
                if (typeof target[key] === 'object' && target[key] !== null)
                    return new Proxy(target[key], proxy);
                else
                    return target[key];
            },
            set(target, key, value) {
                target[key] = value;
                _this.write(_this.content);
                return true;
            }
        };
        this._contentProxy = new Proxy(this._content, proxy);
        _this.write(_this.content);
    }
    read() {
        return this.content = JSON.parse(fs.readFileSync(this._path).toString() || "{}"), this;
    }
    write(content) {
        fs.writeFileSync(this._path, JSON.stringify(content, null, '\t'));
        return this;
    }
    createPersistent() {
        this.write({});
    }
    existsPersistent() {
        return fs.existsSync(this._path);
    }
}
const STORAGE = new Storage().content;
dotenv.config();
const WEBSERVER_DOMAIN_1 = process.env["HOST_MAIN"] ?? "localhost:10080";
const WEBSERVER_DOMAIN_2 = process.env["HOST_DEMO"] ?? "localhost:10081";
const WEBSERVER_PORT_1 = +process.env["PORT_MAIN"] ?? 10080;
const WEBSERVER_PORT_2 = +process.env["PORT_DEMO"] ?? 10081;
const CACHE_IDENTIFIER = STORAGE.cacheID ?? generateUUID("xxxxxxxx", "0123456789abcdef");
const N = 32;
const FILE = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+ip1sAAAAASUVORK5CYII=";
const webserver_1 = express();
const webserver_2 = express();
const maxN = 2 ** N - 1;
webserver_1.options('*', cors());
webserver_2.options('*', cors());
console.info(`supercookie | Starting up using N=${N}, C-ID='${CACHE_IDENTIFIER}' ...`);
console.info(`supercookie | There are ${Math.max(maxN - 1 - (STORAGE.index ?? 1), 0)}/${maxN - 1} unique identifiers left.`);
let Webserver = (() => {
    class Webserver {
        static getVector(identifier) {
            const booleanVector = (identifier >>> 0).toString(2)
                .padStart(this.routes.length, '0').split('')
                .map((element) => element === '1')
                .reverse();
            const vector = new Array();
            booleanVector.forEach((value, index) => value ? vector.push(this.getRouteByIndex(index)) : void 0);
            return vector;
        }
        static getIdentifier(vector, size = vector.size) {
            return parseInt(this.routes.map((route) => vector.has(route) ? 0 : 1)
                .join('').slice(0, size).split('').reverse().join(''), 2);
        }
        static hasRoute(route) {
            return this.routes.includes(route);
        }
        static getRouteByIndex(index) {
            return this.routes[index] ?? null;
        }
        static getIndexByRoute(route) {
            return this.routes.indexOf(route) ?? null;
        }
        static getNextRoute(route) {
            const index = this.routes.indexOf(route);
            if (index === -1)
                throw "Route is not valid.";
            return this.getRouteByIndex(index + 1);
        }
        static setCookie(res, name, value, options = { httpOnly: false, expires: new Date(Date.now() + 60 * 1000) }) {
            return res.cookie(name, value, options), res;
        }
        static sendFile(res, route, options = {}, type = "html") {
            let content = fs.readFileSync(route).toString();
            Object.keys(options).sort((a, b) => b.length - a.length).forEach((key) => {
                content = content.replace(new RegExp(`\{\{${key}\}\}`, 'g'), (options[key]?.toString() || '')
                    .replace(/&/g, "&amp;")
                    .replace(/</g, "&lt;")
                    .replace(/>/g, "&gt;")
                    .replace(/"/g, "&quot;")
                    .replace(/'/g, "&#039;"));
            });
            res.header({
                "Cache-Control": "private, no-cache, no-store, must-revalidate",
                "Expires": -1,
                "Pragma": "no-cache"
            });
            res.type(type);
            return res.send(content), res;
        }
    }
    Webserver.routes = createRoutes(CACHE_IDENTIFIER, N).map((value) => `${CACHE_IDENTIFIER}:${value}`);
    return Webserver;
})();
let Profile = (() => {
    class Profile {
        constructor(uid, identifier = null) {
            this._identifier = null;
            this._visitedRoutes = new Set();
            this._storageSize = -1;
            this._uid = uid;
            if (identifier !== null)
                this._identifier = identifier,
                    this._vector = Webserver.getVector(identifier);
            Profile.list.add(this);
        }
        static get(uid) {
            return this.has(uid) ?
                Array.from(this.list).filter((profile) => profile.uid === uid)?.pop() :
                null;
        }
        static has(uid) {
            return Array.from(this.list).some((profile) => profile.uid === uid);
        }
        static from(uid, identifier) {
            return !this.has(uid) ? new Profile(uid, identifier) : null;
        }
        destructor() {
            Profile.list.delete(this);
        }
        get uid() {
            return this._uid;
        }
        get vector() {
            return this._vector;
        }
        get visited() {
            return this._visitedRoutes;
        }
        get identifier() {
            return this._identifier;
        }
        getRouteByIndex(index) {
            return this.vector[index] ?? null;
        }
        _isReading() {
            return this._identifier === null;
        }
        _visitRoute(route) {
            this._visitedRoutes.add(route);
        }
        _calcIdentifier() {
            return this._identifier = Webserver.getIdentifier(this._visitedRoutes, this._storageSize), this.identifier;
        }
        _setStorageSize(size) {
            this._storageSize = size;
        }
        get storageSize() {
            return this._storageSize;
        }
    }
    Profile.list = new Set();
    return Profile;
})();
;
webserver_2.set("trust proxy", 1);
webserver_2.use(cookieParser());
webserver_2.use((req, res, next) => {
    if (new RegExp(`https?:\/\/${WEBSERVER_DOMAIN_2}`).test(req.headers.origin))
        res.setHeader("Access-Control-Allow-Origin", req.headers.origin);
    res.header("Access-Control-Allow-Methods", "GET, OPTIONS");
    res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
    return next();
});
const midSet = new Set();
const generateWriteToken = () => {
    const uuid = generateUUID();
    setTimeout(() => midSet.delete(uuid), 1000 * 60);
    return midSet.add(uuid), uuid;
};
const deleteWriteToken = (token) => midSet.delete(token);
const hasWriteToken = (token) => midSet.has(token);
webserver_2.get("/read", (_req, res) => {
    const uid = generateUUID();
    console.info(`supercookie | Visitor uid='${uid}' is known • Read`);
    const profile = Profile.from(uid);
    profile._setStorageSize(Math.floor(Math.log2(STORAGE.index ?? 1)) + 1);
    if (profile === null)
        return res.redirect("/read");
    Webserver.setCookie(res, "uid", uid);
    res.redirect(`/t/${Webserver.getRouteByIndex(0)}?f=${generateUUID()}`);
});
webserver_2.get("/write/:mid", (req, res) => {
    const mid = req.params.mid;
    if (!hasWriteToken(mid))
        return res.redirect('/');
    res.clearCookie("mid");
    deleteWriteToken(mid);
    const uid = generateUUID();
    console.info(`supercookie | Visitor uid='${uid}' is unknown • Write`, STORAGE.index);
    const profile = Profile.from(uid, STORAGE.index);
    if (profile === null)
        return res.redirect('/');
    STORAGE.index++;
    Webserver.setCookie(res, "uid", uid);
    res.redirect(`/t/${Webserver.getRouteByIndex(0)}`);
});
webserver_2.get("/t/:ref", (req, res) => {
    const referrer = req.params.ref;
    const uid = req.cookies.uid;
    const profile = Profile.get(uid);
    if (!Webserver.hasRoute(referrer) || profile === null)
        return res.redirect('/');
    const route = Webserver.getNextRoute(referrer);
    if (profile._isReading() && profile.visited.has(referrer))
        return res.redirect('/');
    let nextReferrer = null;
    const redirectCount = profile._isReading() ?
        profile.storageSize :
        Math.floor(Math.log2(profile.identifier)) + 1;
    if (route)
        nextReferrer = `t/${route}?f=${generateUUID()}`;
    if (!profile._isReading()) {
        if (Webserver.getIndexByRoute(referrer) >= redirectCount - 1)
            nextReferrer = "read";
    }
    else if (Webserver.getIndexByRoute(referrer) >= redirectCount - 1 || nextReferrer === null)
        nextReferrer = "identity";
    const bit = !profile._isReading() ? profile.vector.includes(referrer) : "{}";
    Webserver.sendFile(res, path.join(path.resolve(), "www/referrer.html"), {
        delay: profile._isReading() ? 500 : 800,
        referrer: nextReferrer,
        favicon: referrer,
        bit: bit,
        index: `${Webserver.getIndexByRoute(referrer) + 1} / ${redirectCount}`
    });
});
webserver_2.get("/identity", (req, res) => {
    const uid = req.cookies.uid;
    const profile = Profile.get(uid);
    if (profile === null)
        return res.redirect('/');
    res.clearCookie("uid");
    res.clearCookie("vid");
    const identifier = profile._calcIdentifier();
    if (identifier === maxN || profile.visited.size === 0 || identifier === 0)
        return res.redirect(`/write/${generateWriteToken()}`);
    if (identifier !== 0) {
        const identifierHash = hashNumber(identifier);
        console.info(`supercookie | Visitor successfully identified as '${identifierHash}' • (#${identifier}).`);
        Webserver.sendFile(res, path.join(path.resolve(), "www/identity.html"), {
            hash: identifierHash,
            identifier: `#${identifier}`,
            url_workwise: `${WEBSERVER_DOMAIN_1}/workwise`,
            url_main: WEBSERVER_DOMAIN_1
        });
    }
    else
        Webserver.sendFile(res, path.join(path.resolve(), "www/identity.html"), {
            hash: "AN ON YM US",
            identifier: "browser not vulnerable",
            url_workwise: `${WEBSERVER_DOMAIN_1}/workwise`,
            url_main: WEBSERVER_DOMAIN_1
        });
});
webserver_2.get(`/${CACHE_IDENTIFIER}`, (req, res) => {
    const rid = !!req.cookies.rid;
    res.clearCookie("rid");
    if (!rid)
        Webserver.sendFile(res, path.join(path.resolve(), "www/redirect.html"), {
            url_demo: WEBSERVER_DOMAIN_2
        });
    else
        Webserver.sendFile(res, path.join(path.resolve(), "www/launch.html"), {
            favicon: CACHE_IDENTIFIER
        });
});
webserver_2.get('/', (_req, res) => {
    Webserver.setCookie(res, "rid", true);
    res.clearCookie("mid");
    res.redirect(`/${CACHE_IDENTIFIER}`);
});
webserver_2.get("/l/:ref", (_req, res) => {
    console.info(`supercookie | Unknown visitor detected.`);
    Webserver.setCookie(res, "mid", generateWriteToken());
    const data = Buffer.from(FILE, "base64");
    res.writeHead(200, {
        "Cache-Control": "public, max-age=31536000",
        "Expires": new Date(Date.now() + 31536000000).toUTCString(),
        "Content-Type": "image/png",
        "Content-Length": data.length
    });
    res.end(data);
});
webserver_2.get("/i/:ref", (req, res) => {
    const data = Buffer.from(FILE, "base64");
    res.writeHead(200, {
        "Cache-Control": "public, max-age=31536000",
        "Expires": new Date(Date.now() + 31536000000).toUTCString(),
        "Content-Type": "image/png",
        "Content-Length": data.length
    });
    res.end(data);
});
webserver_2.get("/f/:ref", (req, res) => {
    const referrer = req.params.ref;
    const uid = req.cookies.uid;
    if (!Profile.has(uid) || !Webserver.hasRoute(referrer))
        return res.status(404), res.end();
    const profile = Profile.get(uid);
    if (profile._isReading()) {
        profile._visitRoute(referrer);
        console.info(`supercookie | Favicon requested by uid='${uid}' • Read `, Webserver.getIndexByRoute(referrer), "•", Array.from(profile.visited).map(route => Webserver.getIndexByRoute(route)));
        return;
    }
    if (!profile.vector.includes(referrer)) {
        console.info(`supercookie | Favicon requested by uid='${uid}' • Write`, Webserver.getIndexByRoute(referrer), "•", Array.from(profile.vector).map(route => Webserver.getIndexByRoute(route)));
        return;
    }
    const data = Buffer.from(FILE, "base64");
    res.writeHead(200, {
        "Cache-Control": "public, max-age=31536000",
        "Expires": new Date(Date.now() + 31536000000).toUTCString(),
        "Content-Type": "image/png",
        "Content-Length": data.length
    });
    res.end(data);
});
webserver_1.use("/assets", express.static(path.join(path.resolve(), "www/assets"), { index: false }));
webserver_2.use("/assets", express.static(path.join(path.resolve(), "www/assets"), { index: false }));
webserver_1.get('/', (_req, res) => {
    Webserver.sendFile(res, path.join(path.resolve(), "www/index.html"), {
        url_demo: WEBSERVER_DOMAIN_2
    });
});
webserver_1.get("/favicon.ico", (_req, res) => {
    res.sendFile(path.join(path.resolve(), "www/favicon.ico"));
});
webserver_2.get("/favicon.ico", (_req, res) => {
    res.sendFile(path.join(path.resolve(), "www/favicon.ico"));
});
webserver_1.get("/workwise", (_req, res) => {
    Webserver.sendFile(res, path.join(path.resolve(), "www/workwise.html"), {
        url_main: WEBSERVER_DOMAIN_1
    });
});
webserver_1.get("/api", (_req, res) => {
    res.type("json");
    res.status(200);
    res.send({
        index: STORAGE.index,
        cache: STORAGE.cacheID,
        bits: Math.floor(Math.log2(STORAGE.index ?? 1)) + 1,
        N: N,
        maxN: maxN
    });
});
webserver_1.get('*', (_req, res) => {
    res.redirect('/');
});
webserver_2.get('*', (req, res) => {
    Webserver.sendFile(res, path.join(path.resolve(), "www/404.html"), {
        path: decodeURIComponent(req.path),
        url_main: WEBSERVER_DOMAIN_1
    });
});
webserver_1.listen(WEBSERVER_PORT_1, () => console.info(`express-web | Webserver-1 for '${WEBSERVER_DOMAIN_1}' running on port:`, WEBSERVER_PORT_1));
webserver_2.listen(WEBSERVER_PORT_2, () => console.info(`express-web | Webserver-2 for '${WEBSERVER_DOMAIN_2}' running on port:`, WEBSERVER_PORT_2));
STORAGE.index = STORAGE.index ?? 1;
STORAGE.cacheID = CACHE_IDENTIFIER;


================================================
FILE: server/main.ts
================================================
import express from "express";
import path from "path";
import fs from "fs";
import cookieParser from "cookie-parser";
import crypto from "crypto";
import cors from "cors";
import dotenv from "dotenv";

/**
 * Creates UUID in the specified pattern's
 * form using charset
 * @param pattern 
 * @param charset 
 */
const generateUUID = (
    pattern: string = "xxxx-xxxx-xxxx-xxxx-xxxx", 
    charset: string = "abcdefghijklmnopqrstuvwxyz0123456789"): string =>
	pattern.replace(/[x]/g, () => charset[Math.floor(Math.random() * charset.length)]);

/**
 * Creates HEX-hash from number 
 * @param value
 */
const hashNumber = (value: number): string => crypto.createHash("MD5")
    .update(value.toString())
    .digest("hex").slice(-12).split(/(?=(?:..)*$)/)
    .join(' ').toUpperCase();

/**
 * Creates string-array with length "count"
 * from value "base"
 * @param base 
 * @param count 
 */
const createRoutes = (base: string, count: number): Array<string> => {
    const array = [];
    for (let i=0; i<count; i++)
        array.push(crypto.createHash("MD5")
            .update(`${base}${i.toString()}`).digest("base64")
            .replace(/(\=|\+|\/)/g, '0').substring(0, 22));
    return array;
}

/**
 * @class Storage
 * For writing and reading
 * persistent JSON on file-system
 */
class Storage {
    private _path: string = path.join(path.resolve(), "data.json");
    private _content: object = {};
    private _contentProxy: object;
    constructor() {
        if (!this.existsPersistent())
            this.createPersistent();
        this.read();
    }
    public get content(): any {
        return this._contentProxy;
    }
    public set content(data: any) {
        this._content = data;
        const _this = this;
        const proxy = {
            get(target: any, key: any) {
                if (typeof target[key] === 'object' && target[key] !== null) 
                    return new Proxy(target[key], proxy)
                else return target[key];
            },
            set (target: any, key: any, value: any): any {
                target[key] = value;
                _this.write(_this.content);
                return true;
            }
        }
        this._contentProxy = new Proxy(this._content, proxy);
        _this.write(_this.content);
    }
    private read(): Storage {
        return this.content = JSON.parse(fs.readFileSync(this._path).toString() || "{}"), this;
    }
    private write(content: object): Storage {
        fs.writeFileSync(this._path, JSON.stringify(content, null, '\t'));
        return this;
    }
    private createPersistent() {
        this.write({});
    }
    private existsPersistent() {
        return fs.existsSync(this._path);
    }
}
const STORAGE: any = new Storage().content;
dotenv.config();

/****************************************************************************************************\
 * @global
 * User options (edit in .env file)
 */
const WEBSERVER_DOMAIN_1: string    = process.env["HOST_MAIN"] ?? "localhost:10080";
const WEBSERVER_DOMAIN_2: string    = process.env["HOST_DEMO"] ?? "localhost:10081";
const WEBSERVER_PORT_1: number      = +process.env["PORT_MAIN"] ?? 10080;
const WEBSERVER_PORT_2: number      = +process.env["PORT_DEMO"] ?? 10081;
const CACHE_IDENTIFIER: string      = STORAGE.cacheID ?? generateUUID("xxxxxxxx", "0123456789abcdef");

const N: number                     = 32; // max 2^N unique ids possible
/*****************************************************************************************************/


const FILE = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+ip1sAAAAASUVORK5CYII=";
const webserver_1: express.Express = express();
const webserver_2: express.Express = express();
const maxN: number = 2**N - 1;

webserver_1.options('*', cors());
webserver_2.options('*', cors());

console.info(`supercookie | Starting up using N=${N}, C-ID='${CACHE_IDENTIFIER}' ...`);
console.info(`supercookie | There are ${Math.max(maxN - 1 - (STORAGE.index ?? 1), 0)}/${maxN-1} unique identifiers left.`);


/**
 * @class Webserver
 * Webserver defaults
 */
class Webserver {
    public static routes: Array<string> = createRoutes(CACHE_IDENTIFIER, N).map((value: string) => `${CACHE_IDENTIFIER}:${value}`);
    
    public static getVector(identifier: number): Array<string> {
        const booleanVector: Array<boolean> = (identifier >>> 0).toString(2)
            .padStart(this.routes.length, '0').split('')
            .map((element: '0' | '1') => element === '1')
            .reverse();
        const vector = new Array<string>();
        booleanVector.forEach((value: boolean, index: number) => value ? vector.push(this.getRouteByIndex(index)) : void 0);
        return vector;
    }
    public static getIdentifier(vector: Set<string>, size: number = vector.size): number {
        return parseInt(this.routes.map((route: string) => vector.has(route) ? 0 : 1)
            .join('').slice(0, size).split('').reverse().join(''), 2);
    }
    public static hasRoute(route: string): boolean {
        return this.routes.includes(route);
    }
    public static getRouteByIndex(index: number): string {
        return this.routes[index] ?? null;
    }
    public static getIndexByRoute(route: string): number {
        return this.routes.indexOf(route) ?? null;
    }
    public static getNextRoute(route: string): string | null {
        const index = this.routes.indexOf(route);
        if (index === -1)
            throw "Route is not valid.";
        return this.getRouteByIndex(index+1);
    }
    public static setCookie(res: express.Response,
                            name: string, value: any, 
                            options: express.CookieOptions = { httpOnly: false, expires: new Date(Date.now() + 60 * 1000) }): express.Response {
        return res.cookie(name, value, options), res;
    }
    public static sendFile( res: express.Response, 
                            route: string, options: any = {}, type: string = "html"): express.Response {
        let content = fs.readFileSync(route).toString();
        Object.keys(options).sort((a: string, b: string) => b.length - a.length).forEach((key: string) => {
            content = content.replace(
                new RegExp(`\{\{${key}\}\}`, 'g'), 
                (options[key]?.toString() || '')
                .replace(/&/g, "&amp;")
                .replace(/</g, "&lt;")
                .replace(/>/g, "&gt;")
                .replace(/"/g, "&quot;")
                .replace(/'/g, "&#039;")
            );
        });
        res.header({
            "Cache-Control": "private, no-cache, no-store, must-revalidate",
            "Expires": -1,
            "Pragma": "no-cache"
        });
        res.type(type);
        return res.send(content), res;
    }
}

/**
 * @class Profile
 * Read / Write class
 */
class Profile {
    public static list: Set<Profile> = new Set<Profile>();
    public static get(uid: string): Profile {
        return this.has(uid) ? 
            Array.from(this.list).filter((profile: Profile) => profile.uid === uid)?.pop(): 
            null;
    }
    public static has(uid: string): boolean {
        return Array.from(this.list).some((profile: Profile) => profile.uid === uid);
    }
    public static from(uid: string, identifier?: number): Profile {
        return !this.has(uid) ? new Profile(uid, identifier): null;
    }

    private _uid: string;
    private _vector: Array<string>;
    private _identifier: number = null;
    private _visitedRoutes: Set<string> = new Set<string>();
    private _storageSize: number = -1;

    constructor(uid: string, identifier: number = null) {
        this._uid = uid;
        if (identifier !== null) 
            this._identifier = identifier,
            this._vector = Webserver.getVector(identifier);
        Profile.list.add(this);
    }
    public destructor() {
        Profile.list.delete(this);
    }
    public get uid(): string {
        return this._uid;
    }
    public get vector(): Array<string> {
        return this._vector;
    }
    public get visited(): Set<string> {
        return this._visitedRoutes;
    }
    public get identifier(): number {
        return this._identifier;
    }
    public getRouteByIndex(index: number): string {
        return this.vector[index] ?? null;
    }
    public _isReading(): boolean {
        return this._identifier === null;
    }
    public _visitRoute(route: string) {
        this._visitedRoutes.add(route);
    }
    public _calcIdentifier(): number {
        return this._identifier = Webserver.getIdentifier(this._visitedRoutes, this._storageSize), this.identifier;
    }
    public _setStorageSize(size: number) {
        this._storageSize = size;
    }
    public get storageSize(): number {
        return this._storageSize;
    }
};

webserver_2.set("trust proxy", 1);
webserver_2.use(cookieParser());
webserver_2.use((req: express.Request, res: express.Response, next: Function) => {  
    if (new RegExp(`https?:\/\/${WEBSERVER_DOMAIN_2}`).test(req.headers.origin))
        res.setHeader("Access-Control-Allow-Origin", req.headers.origin);
    res.header("Access-Control-Allow-Methods", "GET, OPTIONS");
    res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
    return next();
});


/**
 * @description
 * Using token based "write authentification" to avoid spam to /write path
 */
const midSet: Set<string> = new Set<string>();
const generateWriteToken = (): string => {
    const uuid = generateUUID();
    setTimeout(() => midSet.delete(uuid), 1_000 * 60);
    return midSet.add(uuid), uuid;
}
const deleteWriteToken = (token: string) => midSet.delete(token);
const hasWriteToken = (token: string): boolean => midSet.has(token);

/**
 * @description
 * When navigating to path /read the mode of an (known) visitor is set to "write". 
 * Assuming that the data has already been written to the browser, the webserver
 * is redirecting the user to the first route.
 */
webserver_2.get("/read", (_req: express.Request, res: express.Response) => {
    const uid = generateUUID();
    console.info(`supercookie | Visitor uid='${uid}' is known • Read`);
    const profile: Profile = Profile.from(uid);
    profile._setStorageSize(Math.floor(Math.log2(STORAGE.index ?? 1)) + 1);
    if (profile === null)
        return res.redirect("/read");
    Webserver.setCookie(res, "uid", uid);
    res.redirect(`/t/${Webserver.getRouteByIndex(0)}?f=${generateUUID()}`)
});

/**
 * @description
 * If a user navigates to path /write a new (unknown) visitor entry is created.
 * Assuming that the data has not been written to the browser, the webserver
 * is redirecting the user to the first route.
 */
webserver_2.get("/write/:mid", (req: express.Request, res: express.Response) => {
    const mid = req.params.mid;
    if (!hasWriteToken(mid))
        return res.redirect('/');
    res.clearCookie("mid");
    deleteWriteToken(mid);
    const uid = generateUUID();
    console.info(`supercookie | Visitor uid='${uid}' is unknown • Write`, STORAGE.index);
    const profile: Profile = Profile.from(uid, STORAGE.index);
    if (profile === null)
        return res.redirect('/');
    STORAGE.index++;
    Webserver.setCookie(res, "uid", uid);
    res.redirect(`/t/${Webserver.getRouteByIndex(0)}`);
});

/**
 * @description
 * Under the /t path, the user is redirected to the next possible route.
 */
webserver_2.get("/t/:ref", (req: express.Request, res: express.Response) => {
    const referrer: string = req.params.ref;
    const uid: string = req.cookies.uid;
    const profile: Profile = Profile.get(uid);

    if (!Webserver.hasRoute(referrer) || profile === null)
        return res.redirect('/');
    const route: string = Webserver.getNextRoute(referrer);

    /** reload issue */
    if (profile._isReading() && profile.visited.has(referrer))
        return res.redirect('/');
    let nextReferrer: string = null;
    const redirectCount: number = profile._isReading() ? 
        profile.storageSize: 
        Math.floor(Math.log2(profile.identifier)) + 1;

    if (route) 
        nextReferrer = `t/${route}?f=${generateUUID()}`;
    if (!profile._isReading()) {
        if (Webserver.getIndexByRoute(referrer) >= redirectCount - 1)
            nextReferrer = "read";
    } else if (Webserver.getIndexByRoute(referrer) >= redirectCount - 1 || nextReferrer === null)
        nextReferrer = "identity";

    const bit = !profile._isReading() ? profile.vector.includes(referrer) : "{}";
    Webserver.sendFile(res, path.join(path.resolve(), "www/referrer.html"), {
        delay: profile._isReading() ? 500 : 800,
        referrer: nextReferrer,
        favicon: referrer,
        bit: bit,
        index: `${Webserver.getIndexByRoute(referrer)+1} / ${redirectCount}`
    });
});

/**
 * @description
 * After finishing the reading process, the browser is redirected to the /identity route. 
 * Here, the browser is assigned the calculated identifier and displayed to the user.
 */
webserver_2.get("/identity", (req: express.Request, res: express.Response) => {
    const uid: string = req.cookies.uid;
    const profile: Profile = Profile.get(uid);
    if (profile === null)
        return res.redirect('/');
    res.clearCookie("uid");
    res.clearCookie("vid");
    const identifier = profile._calcIdentifier();
    if (identifier === maxN || profile.visited.size === 0 || identifier === 0)
        return res.redirect(`/write/${generateWriteToken()}`);
    if (identifier !== 0) {
        const identifierHash: string = hashNumber(identifier);
        console.info(`supercookie | Visitor successfully identified as '${identifierHash}' • (#${identifier}).`);
        Webserver.sendFile(res, path.join(path.resolve(), "www/identity.html"), {
            hash: identifierHash,
            identifier: `#${identifier}`,

            url_workwise: `${WEBSERVER_DOMAIN_1}/workwise`,
            url_main: WEBSERVER_DOMAIN_1
        });
    } else Webserver.sendFile(res, path.join(path.resolve(), "www/identity.html"), {
        hash: "AN ON YM US",
        identifier: "browser not vulnerable",

        url_workwise: `${WEBSERVER_DOMAIN_1}/workwise`,
        url_main: WEBSERVER_DOMAIN_1
    });
});

/**
 * @description
 * Fixing a chrome (v 87.0) problem using javascript redirect instead of 
 * express redirect (in redirect.html)
 */
webserver_2.get(`/${CACHE_IDENTIFIER}`, (req: express.Request, res: express.Response) => {
    const rid: boolean = !!req.cookies.rid;
    res.clearCookie("rid");
    if (!rid) 
        Webserver.sendFile(res, path.join(path.resolve(), "www/redirect.html"), {
            url_demo: WEBSERVER_DOMAIN_2
        });
    else
        Webserver.sendFile(res, path.join(path.resolve(), "www/launch.html"), {
            favicon: CACHE_IDENTIFIER
        });
});

/**
 * @description
 * Main route / is redirecting to /CACHE_IDENTIFIER
 */
webserver_2.get('/', (_req: express.Request, res: express.Response) => {
    Webserver.setCookie(res, "rid", true);
    res.clearCookie("mid");
    res.redirect(`/${CACHE_IDENTIFIER}`);
});

/**
 * @description
 * When requesting the favicon under /l, it is excluded that a user already has valid data in the cache.
 */
webserver_2.get("/l/:ref", (_req: express.Request, res: express.Response) => {
    console.info(`supercookie | Unknown visitor detected.`);
    Webserver.setCookie(res, "mid", generateWriteToken());
    const data = Buffer.from(FILE, "base64");
    res.writeHead(200, {
        "Cache-Control": "public, max-age=31536000",
        "Expires": new Date(Date.now() + 31536000000).toUTCString(),
        "Content-Type": "image/png",
        "Content-Length": data.length
    });
    res.end(data);
});


webserver_2.get("/i/:ref", (req: express.Request, res: express.Response) => {
    const data = Buffer.from(FILE, "base64");
    res.writeHead(200, {
        "Cache-Control": "public, max-age=31536000",
        "Expires": new Date(Date.now() + 31536000000).toUTCString(),
        "Content-Type": "image/png",
        "Content-Length": data.length
    });
    res.end(data);
});
/**
 * @description
 * /f route handles requests for favicons by the browser.
 * In write mode, some icons are delivered and other requests are aborted. 
 * In read mode every request fails to not corrupt the cache.
 */
webserver_2.get("/f/:ref", (req: express.Request, res: express.Response) => {
    const referrer: string = req.params.ref;
    const uid: string = req.cookies.uid;
    if (!Profile.has(uid) || !Webserver.hasRoute(referrer))
        return res.status(404), res.end();
    const profile: Profile = Profile.get(uid);
    if (profile._isReading()) {
        profile._visitRoute(referrer);
        console.info(`supercookie | Favicon requested by uid='${uid}' • Read `, Webserver.getIndexByRoute(referrer), "•", 
            Array.from(profile.visited).map(route => Webserver.getIndexByRoute(route)));
        return; // res.type("gif"), res.status(404), res.end();
    }
    if (!profile.vector.includes(referrer)) {
        console.info(`supercookie | Favicon requested by uid='${uid}' • Write`, Webserver.getIndexByRoute(referrer), "•", 
            Array.from(profile.vector).map(route => Webserver.getIndexByRoute(route)));
        return; // res.type("gif"), res.status(404), res.end();
    }
    const data = Buffer.from(FILE, "base64");
    res.writeHead(200, {
        "Cache-Control": "public, max-age=31536000",
        "Expires": new Date(Date.now() + 31536000000).toUTCString(),
        "Content-Type": "image/png",
        "Content-Length": data.length
    });
    res.end(data);
});

webserver_1.use("/assets", express.static(path.join(path.resolve(), "www/assets"), { index: false }));
webserver_2.use("/assets", express.static(path.join(path.resolve(), "www/assets"), { index: false }));
webserver_1.get('/', (_req: express.Request, res: express.Response) => {
    Webserver.sendFile(res, path.join(path.resolve(), "www/index.html"), {
        url_demo: WEBSERVER_DOMAIN_2
    });
});
webserver_1.get("/favicon.ico", (_req: express.Request, res: express.Response) => {
    res.sendFile(path.join(path.resolve(), "www/favicon.ico"));
});
webserver_2.get("/favicon.ico", (_req: express.Request, res: express.Response) => {
    res.sendFile(path.join(path.resolve(), "www/favicon.ico"));
});
webserver_1.get("/workwise", (_req: express.Request, res: express.Response) => {
    Webserver.sendFile(res, path.join(path.resolve(), "www/workwise.html"), {
        url_main: WEBSERVER_DOMAIN_1
    });
});
webserver_1.get("/api", (_req: express.Request, res: express.Response) => {
    res.type("json");
    res.status(200);
    res.send({
        index: STORAGE.index,
        cache: STORAGE.cacheID,
        bits: Math.floor(Math.log2(STORAGE.index ?? 1)) + 1,
        N: N,
        maxN: maxN
    });
});
webserver_1.get('*', (_req: express.Request, res: express.Response) => {
    res.redirect('/');
});
webserver_2.get('*', (req: express.Request, res: express.Response) => {
    Webserver.sendFile(res, path.join(path.resolve(), "www/404.html"), {
        path: decodeURIComponent(req.path),
        url_main: WEBSERVER_DOMAIN_1
    });
});

webserver_1.listen(WEBSERVER_PORT_1, () => 
    console.info(`express-web | Webserver-1 for '${WEBSERVER_DOMAIN_1}' running on port:`, WEBSERVER_PORT_1));
webserver_2.listen(WEBSERVER_PORT_2, () => 
    console.info(`express-web | Webserver-2 for '${WEBSERVER_DOMAIN_2}' running on port:`, WEBSERVER_PORT_2));
STORAGE.index = STORAGE.index ?? 1;
STORAGE.cacheID = CACHE_IDENTIFIER;

================================================
FILE: server/package.json
================================================
{
  "type": "module",
  "dependencies": {
    "@types/cookie-parser": "^1.4.2",
    "@types/cors": "^2.8.9",
    "@types/express": "^4.17.11",
    "cookie-parser": "^1.4.5",
    "cors": "^2.8.5",
    "dotenv": "^8.2.0",
    "express": "^4.17.1"
  }
}


================================================
FILE: server/tsconfig.json
================================================
{
    "compilerOptions": {
        "allowSyntheticDefaultImports": true,
        "sourceMap": false,
        "removeComments": true,
        "target": "ESNext",
        "module": "ESNext",
        "moduleResolution": "Node",
        "allowJs": false,
        "experimentalDecorators": true,
        "lib": [
            "esnext.array",
            "esnext",
            "dom"
        ]
    }
  }

================================================
FILE: server/www/404.html
================================================
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8"/>
        <title>supercookie • progress</title>
        <meta name="author" content="Jonas Strehle"/>
        <meta name="robots" content="noindex"/>
        <meta name="viewport" content="viewport-fit=cover, user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1">
        <style>
            html, body {
                margin: 0px;
                width: 100%;
                height: 100%;
                padding: 0px;
                display: flex;
                justify-content: center;
                align-items: center;
                flex-direction: column;
                background-color: #202020;
                color: rgb(158, 158, 158);
                font-size: large;
                font-family: sans-serif;
                user-select: none;
            }
            img {
                width: 100px;
            }
            img:hover {
                transform: scale(1.01);
            }
            * {
                transition: all 0.4s;
            }
            b {
                color: orange;
            }
            .background {
                position: absolute;
                width: 100%;
                height: 100%;
                background-image: url(/assets/background.png);
                opacity: 0.05;
            }
            .button {
                width: 100px;
                height: 40px;
                text-align: center;
                display: flex;
                justify-content: center;
                z-index: 2;
                align-items: center;
                color: white!important;
                text-decoration: none!important;
                border-radius: 57px;
                background: linear-gradient(145deg, #1c1c1c, #212121);
                box-shadow:  20px 20px 60px #1a1a1a,
                            -20px -20px 60px #242424;
            }
            .button:hover {
                transform: scale(0.95);
            }
            .button:active {
                transform: scale(0.9);
            }
            .image-container {
                z-index: 2;
            }
        </style>
    </head>
    
    <body>
        <div class="background"></div>
        <a class="image-container" href="//{{url_main}}">
            <img src="/assets/404.png">
        </a>
        <p>
            <b>{{path}}</b> not found!
        </p>
        <a class="button" href="//{{url_main}}">
            Back
        </a>
    </body>
</html>

================================================
FILE: server/www/identity.html
================================================
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8"/>
        <title>id • {{hash}}</title>
        <meta name="author" content="Jonas Strehle"/>
        <meta name="robots" content="noindex"/>
        <meta name="viewport" content="viewport-fit=cover, user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1">
        <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
        <link href="https://fonts.googleapis.com/css?family=Muli:400,700&display=swap" rel="stylesheet dns-prefetch preconnect" crossorigin="">
        <link rel="shortcut icon" href="/favicon.ico" type="image/x-icon"/>
        <style>
            html, body {
                margin: 0px;
                width: 100%;
                height: 100%;
                padding: 0px;
                display: flex;
                justify-content: center;
                align-items: center;
                background-color: rgb(32, 32, 32);
                color: white;
                font-size: large;
                font-family: Muli, sans-serif;
            }
            .main {
                position: absolute;
                z-index: 1;
                width: 88%;
                display: flex;
                left: 0;
                right: 0;
                margin: auto;
                justify-content: center;
                align-items: center;
                flex-direction: column;
                border-radius: 50px;
                background: #202020;
                box-shadow: 11px 11px 22px #1b1b1b, -11px -11px 22px #252525;
                padding: 2%;
            }
            .identifier-container div {
                font-weight: bold;
                text-align: center;
            }
            .identifier-container a {
                color: #ff0358;
                font-weight: normal;
                white-space: nowrap;
            }
            .identifier-container img {
                width: 100%;
                min-width: 50%;
            }
            .button-container {
                width: 80%;
                margin-top: 20px;
                display: flex;
                justify-content: center;
                flex-wrap: wrap;
                align-items: center;
            }
            .button-container > .button {
                width: 150px;
                height: 60px;
                margin: 5px;
                background-color: #73c53c;
                display: flex;
                justify-content: center;
                align-items: center;
                border-radius: 10px;
                font-size: medium;
                color: white!important;
                text-decoration: none!important;
                text-align: center;
                display: flex;
                transition: all 0.3s;
            }
            .button-container > .button.blue {
                background-color: #20aee4;
                font-weight: bold;
            }
            .button-container > .button.red {
                background-color: #de3f37
            }
            .button:hover {
                opacity: 0.8;
                transform: scale(0.95);
            }
            .button:active {
                opacity: 0.5;
                transform: scale(1.01);
            }
            * {
                -webkit-user-select: none;
                -moz-user-select: none;
                -ms-user-select: none;
                user-select: none;
            }
            .button > i {
                width: 25%;
                height: 100%;
                display: flex;
                justify-content: center;
                align-items: center;
                background-color: rgba(0, 0, 0, 0.1);
            }
            .button > div {
                width: 75%;
            }
            .identifier-hash {
                -webkit-user-select: text;
                -moz-user-select: text;
                -ms-user-select: text;
                user-select: text;
            }
            .identifier-hash:hover {
                transform: scale(1.1);
            }
            .speechbubble {
                position: relative;
                max-width: 30em;
                background-color: #292929;
                padding: 1.125em 1.5em;
                font-size: 1.25em;
                border-radius: 1rem;
                box-shadow:	0 0.125rem 0.5rem rgba(0, 0, 0, .3), 0 0.0625rem 0.125rem rgba(0, 0, 0, .2);
            }
            .speechbubble::before {
                content: '';
                position: absolute;
                width: 0;
                height: 0;
                bottom: 100%;
                left: 2.5em;
                border: .75rem solid transparent;
                border-top: none;
                border-bottom-color: #292929;
                filter: drop-shadow(0 -0.0625rem 0.0625rem rgba(0, 0, 0, .1));
            }
            .background {
                position: absolute;
                width: 100%;
                height: 100%;
                background-image: url(/assets/background.png);
                opacity: 0.05;
            }
            small {
                color: white;
                font-size: x-small;
            }
        </style>
    </head>
    
    <body>
        <div class="background"></div>
        <div class="main">
            <div class="identifier-container">
                <div>
                    <a href="//{{url_main}}" target="_blank">
                        <img src="/assets/header.png">
                    </a>
                    <p class="speechbubble">
                        Your ID: <a class="identifier-hash" onclick="window.getSelection().selectAllChildren(this); document.execCommand('copy');">{{hash}}</a> <small>({{identifier}})</small>
                    </p>
                </div>
            </div>
            <div class="button-container">
                <a class="button" href="/">
                    <i class="material-icons">cached</i>
                    <div>Try Again</div>
                </a>
                <a class="button blue" target="_blank" href="//{{url_workwise}}" id="workwise">
                    <i class="material-icons">help</i>
                    <div>How does it work?</div>
                </a>
                <a class="button red" target="_blank" href="https://github.com/jonasstrehle/supercookie">
                    <i class="material-icons">source</i>
                    <div>Get Source-Code</div>
                </a>
            </div>
        </div>
    </body>
</html>

================================================
FILE: server/www/index.html
================================================
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8"/>
        <title>supercookie • welcome</title>
        <meta name="description" content="Favicons as supercookies! Check out this cache-based fingerprinting method in our demonstration."/>
        <meta name="keywords" content="JavaScript, Web, Fingerprinting, Favicons, Cache, Anonym, Security, Browser"/>
        <meta name="author" content="Jonas Strehle"/>
        <meta name="viewport" content="viewport-fit=cover, user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1">
        <link rel="shortcut icon" href="/favicon.ico" type="image/x-icon"/>
        <link href="https://fonts.googleapis.com/icon?family=Material+Icons+Outlined" rel="stylesheet">
        <link href="https://fonts.googleapis.com/css?family=Muli:400,700&display=swap" rel="stylesheet dns-prefetch preconnect" crossorigin="">
        <script async defer src="https://buttons.github.io/buttons.js"></script>
        <style>
            html, body {
                margin: 0px;
                width: 100%;
                height: 100%;
                padding: 0px;
                display: flex;
                justify-content: center;
                align-items: center;
                background-color: rgb(32, 32, 32);
                color: white;
                font-size: large;
                font-family: Muli, sans-serif;
            }
            .main {
                position: absolute;
                z-index: 1;
                width: 90%;
                display: flex;
                left: 0;
                right: 0;
                margin: auto;
                justify-content: center;
                align-items: center;
                flex-direction: column;
            }
            h1 {
                text-align: center;
            }
            h1 b {
                color: orange;
            }
            .background {
                position: absolute;
                width: 100%;
                height: 100%;
                background-image: url(/assets/background.png);
                opacity: 0.05;
            }
            .action-button {
                text-decoration: none!important;
                color: white!important;
                display: flex;
                justify-content: center;
                align-items: center;
                border-radius: 50px;
                background: #202020;
                box-shadow: 20px 20px 60px #0e0e0e, -20px -20px 60px #323232;
                padding: 20px;
            }
            .action-button i {
                color: orange!important;
            }
            .action-button:hover, .action-button:active {
                opacity: 0.8;
                color: orange!important;
                box-shadow: 20px 20px 80px #0e0e0e, -20px -20px 80px #323232;
            }
            .action-button:hover > i, .action-button:active > i {
                opacity: 0.8;
                transform: scale(1.2);
                color: white!important;
            } 
            * {
                transition: all 0.4s;
            }
            .action-button i {
                margin-right: 8px;
                color: orange;
            }
            .tag-container {
                position: absolute;
                top: 10px;
                left: 10px;
                display: flex;
                justify-content: center;
                align-items: center;
                flex-direction: column;
            }
            .tag {
                margin: 3px;
                width: 40px;
                height: 40px;
                display: flex;
                justify-content: center;
                align-items: center;
                border-radius: 20px;
                background: #1a1a1a;
            }
            .tag a {
                text-decoration: none!important;
                color: #656565!important;
                display: flex;
                justify-content: center;
                align-items: center;
            }
            .tag:hover, .tag:active {
                transform: scale(0.92);
            }
            .tag:hover a .tag:active a {
                color: rgba(255, 255, 255, 0.6)!important;
            }
            .tag > img {
                width: 90%;
            }

            .footer {
                position: absolute;
                bottom: 30px;
                left: 0;
                right: 0;
                margin: auto;
                width: 90%;
                display: flex;
                justify-content: space-between;
                align-items: center;
                flex-direction: row;
            }
            @media only screen and (max-width: 767px), only screen and (max-device-width: 767px) {
                .footer {
                    display: none;
                }
            }
            .bmc-btn {
                height: 50px!important;
            }
            .footer > div {
                display: flex;
                flex-direction: row;
                align-items: center;
                justify-content: center;
            }
            .footer > div > div {
                margin-right: 18px;
            }
            span {
                display: flex;
                justify-content: center;
                align-items: center;
            }
            .github-button {
                color: white!important;
                text-decoration: none!important;
            }
        </style>
    </head>
    
    <body>

        <div class="background"></div>
        <div class="tag-container">
            <div class="tag">
                <a href="/workwise">
                    <span class="material-icons-outlined">help</span>
                </a>
            </div>
            <div class="tag">
                <a href="https://jonas.strehles.info">
                    <span class="material-icons-outlined">add</span>
                </a>
            </div>
        </div>


        <div class="main">
            <h1>Browser-Fingerprinting<br>via <b>Favicon</b></h1>
            
            <a class="action-button" href="//{{url_demo}}"><i class="material-icons-outlined">visibility</i>To the Demo!</a>
        </div>

        <div class="footer">
            <script type="text/javascript" src="https://cdnjs.buymeacoffee.com/1.0.0/button.prod.min.js" data-name="bmc-button" data-slug="jonasstrehle" data-color="#f4a00a" data-emoji="" data-font="Cookie" data-text="Buy me a cookie" data-outline-color="#000000" data-font-color="#000000" data-coffee-color="#ffffff" ></script>
            <div>
                <div>This project is now <b>Open-Source</b>!</div>
                <a class="github-button" href="https://github.com/jonasstrehle/supercookie" data-icon="octicon-star" data-size="large" data-show-count="true" aria-label="Star jonasstrehle/supercookie on GitHub">Star on GitHub</a>
            </div>
        </div>
    </body>
</html>

================================================
FILE: server/www/launch.html
================================================
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8"/>
        <title>supercookie</title>
        <meta name="author" content="Jonas Strehle"/>
        <meta name="robots" content="noindex"/>
        <meta name="viewport" content="viewport-fit=cover, user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1">
        <link rel="shortcut icon" href="/l/{{favicon}}" type="image/x-icon"/>
        <style>
            html, body {
                margin: 0px;
                width: 100%;
                height: 100%;
                padding: 0px;
                display: flex;
                justify-content: center;
                align-items: center;
                background-color: rgb(32, 32, 32);
                color: white;
                font-size: large;
                font-family: sans-serif;
            }
        </style>
    </head>
    
    <body>
        <h1>...</h1>
        <script type="module">
            window.onload = async () => {
                await new Promise((resolve) => setTimeout(resolve, 500));
                const mid = (document.cookie.match(new RegExp(`(^| )mid=([^;]+)`)) || [])[2];
                const route = !!mid ? `/write/${mid}` : "/read";
                document.cookie.split(";").forEach((c) => document.cookie = c.replace(/^ +/, "").replace(/=.*/, `=;expires=${new Date().toUTCString()};path=/`));
                window.location.href = route;
            }
        </script>
    </body>
</html>

================================================
FILE: server/www/redirect.html
================================================
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8"/>
        <title>supercookie • progress</title>
        <meta name="author" content="Jonas Strehle"/>
        <meta name="robots" content="noindex"/>
        <meta name="viewport" content="viewport-fit=cover, user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1">
        <style>
            html, body {
                margin: 0px;
                width: 100%;
                height: 100%;
                padding: 0px;
                display: flex;
                justify-content: center;
                align-items: center;
                background-color: rgb(32, 32, 32);
                color: orange;
                font-size: large;
                font-family: sans-serif;
            }
        </style>
    </head>
    
    <body>
        <h1>...</h1>
        <script type="module">
            window.onload = () =>
                setTimeout(() => window.location.href = "//{{url_demo}}", 3_000);
        </script>
    </body>
</html>

================================================
FILE: server/www/referrer-v2.html
================================================
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8"/>
        <title>supercookie • {{index}}</title>
        <meta name="author" content="Jonas Strehle"/>
        <meta name="robots" content="noindex"/>
        <meta name="viewport" content="viewport-fit=cover, user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1">
        <style>
            html, body {
                margin: 0px;
                width: 100%;
                height: 100%;
                padding: 0px;
                display: flex;
                justify-content: center;
                align-items: center;
                background-color: rgb(32, 32, 32);
                color: #ff0358;
                font-size: large;
                font-family: sans-serif;
            }
        </style>
    </head>
    
    <body>
        <h1>{{index}}</h1>
        <script type="module">
            console.info(`{{index}} • bit v2`, JSON.parse("{{bit}}"));
            if (JSON.parse("{{bit}}"))
                document.location.href = `${document.location.origin}/{{referrer}}`;
            const favicon = document.createElement('link');
            favicon.rel = "icon preload";
            favicon.as = "image";
            favicon.type = "image/x-icon";
            favicon.onload = favicon.onerror = () => 
                document.location.href = `${document.location.origin}/{{referrer}}`;
            document.head.appendChild(favicon);
            globalThis.f = favicon;
            favicon.href = `//${window.location.host}/f/{{favicon}}`;
        </script>
    </body>
</html>

================================================
FILE: server/www/referrer.html
================================================
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8"/>
        <title>supercookie • {{index}}</title>
        <meta name="author" content="Jonas Strehle"/>
        <meta name="robots" content="noindex"/>
        <meta name="viewport" content="viewport-fit=cover, user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1">
        <link rel="shortcut icon" href="/f/{{favicon}}" type="image/x-icon"/>
        <style>
            html, body {
                margin: 0px;
                width: 100%;
                height: 100%;
                padding: 0px;
                display: flex;
                justify-content: center;
                align-items: center;
                background-color: rgb(32, 32, 32);
                color: #ff0358;
                font-size: large;
                font-family: sans-serif;
            }
        </style>
    </head>
    
    <body>
        <h1>{{index}}</h1>
        <script>
            console.info(`{{index}} • bit v1`, JSON.parse("{{bit}}"));

            window.addEventListener("DOMContentLoaded", () => {
                setTimeout(()=>{
                    document.location.href = `${document.location.origin}/{{referrer}}`;
                }, +"{{delay}}");
            });
        </script>
    </body>
</html>


================================================
FILE: server/www/tsconfig.json
================================================
{
    "compilerOptions": {
        "sourceMap": false,
        "removeComments": true,
        "target": "ESNext",
        "module": "ESNext",
        "allowJs": false,
        "experimentalDecorators": true,
        "lib": [
            "esnext.array",
            "esnext",
            "dom"
        ]
    }
  }

================================================
FILE: server/www/workwise.html
================================================
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8"/>
        <title>supercookie • workwise</title>
        <meta name="description" content="Favicons as supercookies! Check out this cache-based fingerprinting method in our demonstration."/>
        <meta name="keywords" content="JavaScript, Web, Fingerprinting, Favicons, Cache, Anonym, Security, Browser"/>
        <meta name="author" content="Jonas Strehle"/>
        <meta name="robots" content="noindex"/>
        <meta name="viewport" content="viewport-fit=cover, user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1">
        <link href="https://fonts.googleapis.com/icon?family=Material+Icons+Outlined" rel="stylesheet">
        <link href="https://fonts.googleapis.com/css?family=Muli:400,700&display=swap" rel="stylesheet dns-prefetch preconnect" crossorigin="">
        <link rel="shortcut icon" href="/favicon.ico" type="image/x-icon"/>
        <style>
            html, body {
                margin: 0px;
                width: 100%;
                height: 100%;
                padding: 0px;
                display: flex;
                justify-content: center;
                align-items: center;
                background-color: #151d32;
                color: white;
                font-size: large;
                font-family: Muli, sans-serif;
            }
            .main {
                width: 100%;
                display: flex;
                left: 0;
                right: 0;
                margin: auto;
                justify-content: center;
                align-items: center;
                flex-direction: column;
                padding: 3%;
                position: relative;
                overflow: hidden;
            }

            .background {
                width: 100%;
                height: 100%;
                position: absolute;
                top: 0;
                background-image: url(/assets/background.png);
                opacity: 0.05;
            }
            .markdown-body {
                z-index: 2;
                width: 98%;
            }
            .diagram-image {
                width: 90%;
                margin-top: 20px;
                background-color: #151D32!important;
                border-radius: 40px;
                border-radius: 28px;
                padding: 20px;
                box-shadow:  12px 12px 24px #0d111e,
                            -12px -12px 24px #1d2946;
            }
        </style>

        <style>
            .markdown-body {
                -ms-text-size-adjust: 100%;
                -webkit-text-size-adjust: 100%;
                line-height: 1.5;
                font-size: 16px;
                line-height: 1.5;
                word-wrap: break-word;
                color: white;
            }
            .markdown-body .octicon {
                display: inline-block;
                fill: currentColor;
                vertical-align: text-bottom;
            }
            .markdown-body .anchor {
                float: left;
                line-height: 1;
                margin-left: -20px;
                padding-right: 4px;
            }
            .markdown-body .anchor:focus {
                outline: none;
            }
            .markdown-body h1 .octicon-link,
            .markdown-body h2 .octicon-link,
            .markdown-body h3 .octicon-link,
            .markdown-body h4 .octicon-link,
            .markdown-body h5 .octicon-link,
            .markdown-body h6 .octicon-link {
                color: #1b1f23;
                vertical-align: middle;
                visibility: hidden;
            }
            .markdown-body h1:hover .anchor,
            .markdown-body h2:hover .anchor,
            .markdown-body h3:hover .anchor,
            .markdown-body h4:hover .anchor,
            .markdown-body h5:hover .anchor,
            .markdown-body h6:hover .anchor {
                text-decoration: none;
            }
            
            .markdown-body h1:hover .anchor .octicon-link,
            .markdown-body h2:hover .anchor .octicon-link,
            .markdown-body h3:hover .anchor .octicon-link,
            .markdown-body h4:hover .anchor .octicon-link,
            .markdown-body h5:hover .anchor .octicon-link,
            .markdown-body h6:hover .anchor .octicon-link {
                visibility: visible;
            }
            
            .markdown-body h1:hover .anchor .octicon-link:before,
            .markdown-body h2:hover .anchor .octicon-link:before,
            .markdown-body h3:hover .anchor .octicon-link:before,
            .markdown-body h4:hover .anchor .octicon-link:before,
            .markdown-body h5:hover .anchor .octicon-link:before,
            .markdown-body h6:hover .anchor .octicon-link:before {
                width: 16px;
                height: 16px;
                content: ' ';
                display: inline-block;
                background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' version='1.1' width='16' height='16' aria-hidden='true'%3E%3Cpath fill='white' fill-rule='evenodd' d='M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z'%3E%3C/path%3E%3C/svg%3E");
            }
            .markdown-body details {
                display: block;
            }
            .markdown-body summary {
                display: list-item;
            }
            .markdown-body a {
                background-color: initial;
            }
            .markdown-body a:active,
            .markdown-body a:hover {
                outline-width: 0;
            }
            .markdown-body strong {
                font-weight: inherit;
                font-weight: bolder;
            }
            .markdown-body h1 {
                font-size: 2em;
                margin: .67em 0;
            }
            .markdown-body img {
                border-style: none;
            }
            .markdown-body code,
            .markdown-body kbd,
            .markdown-body pre {
                font-family: monospace,monospace;
                font-size: 1em;
            }
            .markdown-body hr {
                box-sizing: initial;
                height: 0;
                overflow: visible;
            }
            .markdown-body input {
                font: inherit;
                margin: 0;
            }
            .markdown-body input {
                overflow: visible;
            }
            .markdown-body [type=checkbox] {
                box-sizing: border-box;
                padding: 0;
            }
            .markdown-body * {
                box-sizing: border-box;
            }
            .markdown-body input {
                font-family: inherit;
                font-size: inherit;
                line-height: inherit;
            }
            .markdown-body a {
                color: #0366d6;
                text-decoration: none;
            }
            .markdown-body a:hover {
                text-decoration: underline;
            }
            .markdown-body strong {
                font-weight: 600;
            }
            .markdown-body hr {
                height: 0;
                margin: 15px 0;
                overflow: hidden;
                background: transparent;
                border: 0;
                border-bottom: 1px solid #dfe2e5;
            }
            .markdown-body hr:after,
            .markdown-body hr:before {
                display: table;
                content: "";
            }
            .markdown-body hr:after {
                clear: both;
            }
            .markdown-body table {
                border-spacing: 0;
                border-collapse: collapse;
            }
            .markdown-body td,
            .markdown-body th {
                padding: 0;
            }
            .markdown-body details summary {
                cursor: pointer;
            }
            .markdown-body kbd {
                display: inline-block;
                padding: 3px 5px;
                font: 11px SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;
                line-height: 10px;
                color: #444d56;
                vertical-align: middle;
                background-color: #fafbfc;
                border: 1px solid #d1d5da;
                border-radius: 3px;
                box-shadow: inset 0 -1px 0 #d1d5da;
            }
            .markdown-body h1,
            .markdown-body h2,
            .markdown-body h3,
            .markdown-body h4,
            .markdown-body h5,
            .markdown-body h6 {
                margin-top: 0;
                margin-bottom: 0;
            }
            .markdown-body h1 {
                font-size: 32px;
            }
            .markdown-body h1,
            .markdown-body h2 {
                font-weight: 600;
            }
            .markdown-body h2 {
                font-size: 24px;
            }
            .markdown-body h3 {
                font-size: 20px;
            }
            .markdown-body h3,
            .markdown-body h4 {
                font-weight: 600;
            }
            .markdown-body h4 {
                font-size: 16px;
            }
            .markdown-body h5 {
                font-size: 14px;
            }
            .markdown-body h5,
            .markdown-body h6 {
                font-weight: 600;
            }
            .markdown-body h6 {
                font-size: 12px;
            }
            .markdown-body p {
                margin-top: 0;
                margin-bottom: 10px;
            }
            .markdown-body blockquote {
                margin: 0;
            }
            .markdown-body ol,
            .markdown-body ul {
                padding-left: 0;
                margin-top: 0;
                margin-bottom: 0;
            }
            .markdown-body ol ol,
            .markdown-body ul ol {
                list-style-type: lower-roman;
            }
            .markdown-body ol ol ol,
            .markdown-body ol ul ol,
            .markdown-body ul ol ol,
            .markdown-body ul ul ol {
                list-style-type: lower-alpha;
            }
            .markdown-body dd {
                margin-left: 0;
            }
            .markdown-body code,
            .markdown-body pre {
                font-family: SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;
                font-size: 12px;
            }
            .markdown-body pre {
                margin-top: 0;
                margin-bottom: 0;
            }
            .markdown-body input::-webkit-inner-spin-button,
            .markdown-body input::-webkit-outer-spin-button {
                margin: 0;
                -webkit-appearance: none;
                appearance: none;
            }
            .markdown-body :checked+.radio-label {
                position: relative;
                z-index: 1;
                border-color: #0366d6;
            }
            .markdown-body .border {
                border: 1px solid #e1e4e8!important;
            }
            .markdown-body .border-0 {
                border: 0!important;
            }
            .markdown-body .border-bottom {
                border-bottom: 1px solid #e1e4e8!important;
            }
            .markdown-body .rounded-1 {
                border-radius: 3px!important;
            }
            .markdown-body .bg-white {
                background-color: #fff!important;
            }
            .markdown-body .bg-gray-light {
                background-color: #fafbfc!important;
            }
            .markdown-body .mb-0 {
                margin-bottom: 0!important;
            }
            .markdown-body .my-2 {
                margin-top: 8px!important;
                margin-bottom: 8px!important;
            }
            .markdown-body .pl-0 {
                padding-left: 0!important;
            }
            .markdown-body .py-0 {
                padding-top: 0!important;
                padding-bottom: 0!important;
            }
            .markdown-body .pl-1 {
                padding-left: 4px!important;
            }
            .markdown-body .pl-2 {
                padding-left: 8px!important;
            }
            .markdown-body .py-2 {
                padding-top: 8px!important;
                padding-bottom: 8px!important;
            }
            .markdown-body .pl-3,
            .markdown-body .px-3 {
                padding-left: 16px!important;
            }
            .markdown-body .px-3 {
                padding-right: 16px!important;
            }
            .markdown-body .pl-4 {
                padding-left: 24px!important;
            }
            .markdown-body .pl-5 {
                padding-left: 32px!important;
            }
            .markdown-body .pl-6 {
                padding-left: 40px!important;
            }
            .markdown-body .f6 {
                font-size: 12px!important;
            }
            .markdown-body .lh-condensed {
                line-height: 1.25!important;
            }
            .markdown-body .text-bold {
                font-weight: 600!important;
            }
            .markdown-body .pl-c {
                color: #6a737d;
            }
            .markdown-body .pl-c1,
            .markdown-body .pl-s .pl-v {
                color: #005cc5;
            }
            .markdown-body .pl-e,
            .markdown-body .pl-en {
                color: #6f42c1;
            }
            .markdown-body .pl-s .pl-s1,
            .markdown-body .pl-smi {
                color: #24292e;
            }
            .markdown-body .pl-ent {
                color: #22863a;
            }
            .markdown-body .pl-k {
                color: #d73a49;
            }
            .markdown-body .pl-pds,
            .markdown-body .pl-s,
            .markdown-body .pl-s .pl-pse .pl-s1,
            .markdown-body .pl-sr,
            .markdown-body .pl-sr .pl-cce,
            .markdown-body .pl-sr .pl-sra,
            .markdown-body .pl-sr .pl-sre {
                color: #e9c730;
            }
            .markdown-body .pl-smw,
            .markdown-body .pl-v {
                color: #e36209;
            }
            .markdown-body .pl-bu {
                color: #b31d28;
            }
            .markdown-body .pl-ii {
                color: #fafbfc;
                background-color: #b31d28;
            }
            .markdown-body .pl-c2 {
                color: #fafbfc;
                background-color: #d73a49;
            }
            .markdown-body .pl-c2:before {
                content: "^M";
            }
            .markdown-body .pl-sr .pl-cce {
                font-weight: 700;
                color: #22863a;
            }
            .markdown-body .pl-ml {
                color: #735c0f;
            }
            .markdown-body .pl-mh,
            .markdown-body .pl-mh .pl-en,
            .markdown-body .pl-ms {
                font-weight: 700;
                color: #005cc5;
            }
            .markdown-body .pl-mi {
                font-style: italic;
                color: #24292e;
            }
            .markdown-body .pl-mb {
                font-weight: 700;
                color: #62686e;
            }
            .markdown-body .pl-md {
                color: #b31d28;
                background-color: #ffeef0;
            }
            .markdown-body .pl-mi1 {
                color: #22863a;
                background-color: #f0fff4;
            }
            .markdown-body .pl-mc {
                color: #e36209;
                background-color: #ffebda;
            }
            .markdown-body .pl-mi2 {
                color: #f6f8fa;
                background-color: #005cc5;
            }
            .markdown-body .pl-mdr {
                font-weight: 700;
                color: #6f42c1;
            }
            .markdown-body .pl-ba {
                color: #586069;
            }
            .markdown-body .pl-sg {
                color: #959da5;
            }
            .markdown-body .pl-corl {
                text-decoration: underline;
                color: #032f62;
            }
            .markdown-body .pl {
                color: white;
            }
            .markdown-body .mb-0 {
                margin-bottom: 0!important;
            }
            .markdown-body .my-2 {
                margin-bottom: 8px!important;
            }
            .markdown-body .my-2 {
                margin-top: 8px!important;
            }
            .markdown-body .pl-0 {
                padding-left: 0!important;
            }
            .markdown-body .py-0 {
                padding-top: 0!important;
                padding-bottom: 0!important;
            }
            .markdown-body .pl-1 {
                padding-left: 4px!important;
            }
            .markdown-body .pl-2 {
                padding-left: 8px!important;
            }
            .markdown-body .py-2 {
                padding-top: 8px!important;
                padding-bottom: 8px!important;
            }
            .markdown-body .pl-3 {
                padding-left: 16px!important;
            }
            .markdown-body .pl-4 {
                padding-left: 24px!important;
            }
            .markdown-body .pl-5 {
                padding-left: 32px!important;
            }
            .markdown-body .pl-6 {
                padding-left: 40px!important;
            }
            .markdown-body .pl-7 {
                padding-left: 48px!important;
            }
            .markdown-body .pl-8 {
                padding-left: 64px!important;
            }
            .markdown-body .pl-9 {
                padding-left: 80px!important;
            }
            .markdown-body .pl-10 {
                padding-left: 96px!important;
            }
            .markdown-body .pl-11 {
                padding-left: 112px!important;
            }
            .markdown-body .pl-12 {
                padding-left: 128px!important;
            }
            .markdown-body hr {
                border-bottom-color: #eee;
            }
            .markdown-body kbd {
                display: inline-block;
                padding: 3px 5px;
                font: 11px SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace;
                line-height: 10px;
                color: #444d56;
                vertical-align: middle;
                background-color: #fafbfc;
                border: 1px solid #d1d5da;
                border-radius: 3px;
                box-shadow: inset 0 -1px 0 #d1d5da;
            }
            .markdown-body:after,
            .markdown-body:before {
                display: table;
                content: "";
            }
            .markdown-body:after {
                clear: both;
            }
            .markdown-body>:first-child {
                margin-top: 0!important;
            }
            .markdown-body>:last-child {
                margin-bottom: 0!important;
            }
            .markdown-body a:not([href]) {
                color: inherit;
                text-decoration: none;
            }
            .markdown-body blockquote,
            .markdown-body details,
            .markdown-body dl,
            .markdown-body ol,
            .markdown-body p,
            .markdown-body pre,
            .markdown-body table,
            .markdown-body ul {
                margin-top: 0;
                margin-bottom: 16px;
            }
            .markdown-body hr {
                height: .25em;
                padding: 0;
                margin: 24px 0;
                background-color: #e1e4e8;
                border: 0;
            }
            .markdown-body blockquote {
                padding: 0 1em;
                color: #bdbdbd;
                border-left: .25em solid #dfe2e5;
            }
            .markdown-body blockquote>:first-child {
                margin-top: 0;
            }
            .markdown-body blockquote>:last-child {
                margin-bottom: 0;
            }
            .markdown-body h1,
            .markdown-body h2,
            .markdown-body h3,
            .markdown-body h4,
            .markdown-body h5,
            .markdown-body h6 {
                margin-top: 24px;
                margin-bottom: 16px;
                font-weight: 600;
                line-height: 1.25;
            }
            .markdown-body h1 {
                font-size: 2em;
            }
            .markdown-body h1,
            .markdown-body h2 {
                padding-bottom: .3em;
                border-bottom: 1px solid #eaecef;
            }
            .markdown-body h2 {
                font-size: 1.5em;
            }
            .markdown-body h3 {
                font-size: 1.25em;
            }
            .markdown-body h4 {
                font-size: 1em;
            }
            .markdown-body h5 {
                font-size: .875em;
            }
            .markdown-body h6 {
                font-size: .85em;
            }
            .markdown-body ol,
            .markdown-body ul {
                padding-left: 2em;
            }
            .markdown-body ol ol,
            .markdown-body ol ul,
            .markdown-body ul ol,
            .markdown-body ul ul {
                margin-top: 0;
                margin-bottom: 0;
            }
            .markdown-body li {
                word-wrap: break-all;
            }
            .markdown-body li>p {
                margin-top: 16px;
            }
            .markdown-body li+li {
                margin-top: .25em;
            }
            .markdown-body dl {
                padding: 0;
            }
            .markdown-body dl dt {
                padding: 0;
                margin-top: 16px;
                font-size: 1em;
                font-style: italic;
                font-weight: 600;
            }
            .markdown-body dl dd {
                padding: 0 16px;
                margin-bottom: 16px;
            }
            .markdown-body table {
                display: block;
                width: 100%;
                overflow: auto;
            }
            .markdown-body table th {
                font-weight: 600;
            }
            .markdown-body table td,
            .markdown-body table th {
                padding: 6px 13px;
                border: 1px solid #070911;
            }
            .markdown-body table tr {
                background-color: #0d1221;
                border-top: 1px solid #c6cbd1;
            }
            .markdown-body table tr:nth-child(2n) {
                background-color: #141c33;
            }
            .markdown-body img {
                max-width: 100%;
                box-sizing: initial;
                background-color: #fff;
            }
            .markdown-body img[align=right] {
                padding-left: 20px;
            }
            .markdown-body img[align=left] {
                padding-right: 20px;
            }
            .markdown-body code {
                padding: .2em .4em;
                margin: 0;
                font-size: 85%;
                background-color: rgba(27, 31, 35, .05);
                border-radius: 3px;
            }
            .markdown-body pre {
                word-wrap: normal;
            }
            .markdown-body pre>code {
                padding: 0;
                margin: 0;
                font-size: 100%;
                word-break: normal;
                white-space: pre;
                background: transparent;
                border: 0;
            }
            .markdown-body .highlight {
                margin-bottom: 16px;
            }
            .markdown-body .highlight pre {
                margin-bottom: 0;
                word-break: normal;
            }
            .markdown-body .highlight pre,
            .markdown-body pre {
                padding: 16px;
                overflow: auto;
                font-size: 85%;
                line-height: 1.45;
                background-color: #0d1221;
                border-radius: 3px;
            }
            .markdown-body pre code {
                display: inline;
                max-width: auto;
                padding: 0;
                margin: 0;
                overflow: visible;
                line-height: inherit;
                word-wrap: normal;
                background-color: initial;
                border: 0;
            }
            .markdown-body .commit-tease-sha {
                display: inline-block;
                font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace;
                font-size: 90%;
                color: #444d56;
            }
            .markdown-body .full-commit .btn-outline:not(:disabled):hover {
                color: #005cc5;
                border-color: #005cc5;
            }
            .markdown-body .blob-wrapper {
                overflow-x: auto;
                overflow-y: hidden;
            }
            .markdown-body .blob-wrapper-embedded {
                max-height: 240px;
                overflow-y: auto;
            }
            .markdown-body .blob-num {
                width: 1%;
                min-width: 50px;
                padding-right: 10px;
                padding-left: 10px;
                font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace;
                font-size: 12px;
                line-height: 20px;
                color: rgba(27, 31, 35, .3);
                text-align: right;
                white-space: nowrap;
                vertical-align: top;
                cursor: pointer;
                -webkit-user-select: none;
                -moz-user-select: none;
                -ms-user-select: none;
                user-select: none;
            }
            .markdown-body .blob-num:hover {
                color: rgba(27, 31, 35, .6);
            }
            .markdown-body .blob-num:before {
                content: attr(data-line-number);
            }
            .markdown-body .blob-code {
                position: relative;
                padding-right: 10px;
                padding-left: 10px;
                line-height: 20px;
                vertical-align: top;
            }
            .markdown-body .blob-code-inner {
                overflow: visible;
                font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace;
                font-size: 12px;
                color: #24292e;
                word-wrap: normal;
                white-space: pre;
            }
            .markdown-body .pl-token.active,
            .markdown-body .pl-token:hover {
                cursor: pointer;
                background: #ffea7f;
            }
            .markdown-body .tab-size[data-tab-size="1"] {
                -moz-tab-size: 1;
                tab-size: 1;
            }
            .markdown-body .tab-size[data-tab-size="2"] {
                -moz-tab-size: 2;
                tab-size: 2;
            }
            .markdown-body .tab-size[data-tab-size="3"] {
                -moz-tab-size: 3;
                tab-size: 3;
            }
            .markdown-body .tab-size[data-tab-size="4"] {
                -moz-tab-size: 4;
                tab-size: 4;
            }
            .markdown-body .tab-size[data-tab-size="5"] {
                -moz-tab-size: 5;
                tab-size: 5;
            }
            .markdown-body .tab-size[data-tab-size="6"] {
                -moz-tab-size: 6;
                tab-size: 6;
            }
            .markdown-body .tab-size[data-tab-size="7"] {
                -moz-tab-size: 7;
                tab-size: 7;
            }
            .markdown-body .tab-size[data-tab-size="8"] {
                -moz-tab-size: 8;
                tab-size: 8;
            }
            .markdown-body .tab-size[data-tab-size="9"] {
                -moz-tab-size: 9;
                tab-size: 9;
            }
            .markdown-body .tab-size[data-tab-size="10"] {
                -moz-tab-size: 10;
                tab-size: 10;
            }
            .markdown-body .tab-size[data-tab-size="11"] {
                -moz-tab-size: 11;
                tab-size: 11;
            }
            .markdown-body .tab-size[data-tab-size="12"] {
                -moz-tab-size: 12;
                tab-size: 12;
            }
            .markdown-body .task-list-item {
                list-style-type: none;
            }
            .markdown-body .task-list-item+.task-list-item {
                margin-top: 3px;
            }
            .markdown-body .task-list-item input {
                margin: 0 .2em .25em -1.6em;
                vertical-align: middle;
            }


            .tag {
                margin: 3px;
                width: 40px;
                height: 40px;
                display: flex;
                justify-content: center;
                align-items: center;
                border-radius: 20px;
                background: #101625;
                position: absolute;
                right: 10px;
                top: 10px;
                transition: all 0.3s;
                z-index: 3;
            }
            .tag a {
                text-decoration: none!important;
                color: #656565!important;
                display: flex;
                justify-content: center;
                align-items: center;
            }
            .tag:hover {
                transform: scale(0.92);
            }
            .tag:hover a {
                color: rgba(255, 255, 255, 0.6)!important;
            }
            .tag > img {
                width: 90%;
            }

        </style>
    </head>
    
    <body>
        <div class="main">
            <div class="tag">
                <a href="//{{url_main}}">
                    <span class="material-icons-outlined">home</span>
                </a>
            </div>
            <div class="background"></div>
            <article class="markdown-body">
                <h1>
                    <a class="anchor" href="#" aria-hidden="true">
                        <span class="octicon octicon-link"></span>
                    </a>
                    Workwise • <i>supercookie</i> 
                </h1>
                
                <div>📚 Please have a look at this <b>elaboration</b> from University of Illinois:
                    <s><a href="https://www.cs.uic.edu/~polakis/papers/solomos-ndss21.pdf" target="_blank">www.cs.uic.edu</a></s>
                </div>

                <h3>
                    <a id="content-introduction" class="anchor" href="#content-introduction" aria-hidden="true">
                        <span class="octicon octicon-link"></span>
                    </a>
                    Introduction
                </h3>
                
                <blockquote>
                    <p>
                        <strong>Data is the new gold!</strong><br>
                    </p>
                </blockquote>

                
                Browsers are the most widespread access medium that makes it incredibly easy for us humans to connect to the <a href="https://en.wikipedia.org/wiki/World_Wide_Web" target="_blank">Word Wide Web</a>.<br>
                Due to the constant development of the Internet, such as the continuous elaboration of new standards and features, the introduction of powerful APIs and further interfaces on the browser side, the possibilities for collecting and analyzing data have also significantly expanded over the last few decades!
                <br>
                <br>
                First and foremost, there is nothing wrong with collecting data at all. All of us collect data, whether unconsciously in private everyday life or completely consciously in school or at work - collecting data, interpreting it and drawing conclusions is actually incredibly important!
                <br>
                <br>
                With the launch of the WWW for the public and the development of the first online services, data collection also started to become interesting for the various website providers, according to the motto <i>if I own a website, I also want to know who is surfing it</i>.
                <br>
                However, in most cases we as consumers only want to disclose as little as possible and only the data necessary for the intended service - in fact, <i>my private data is no one else's business</i>.
                <br>
                <br>
                The above-mentioned further development of the WWW's capabilities has allowed data to be assigned to individual profiles, enabling the recognition of unique users and the ability to trace their browsing activities even across different pages - the so called <a href="https://en.wikipedia.org/wiki/Device_fingerprint" target="_blank">device fingerprinting</a>.
                <br>
                Some known methods for assigning a unique fingerprint to browsers are <a href="https://en.wikipedia.org/wiki/Device_fingerprint#Hardware_benchmarking" target="_blank">hardware benchmarking</a>, <a href="https://en.wikipedia.org/wiki/Device_fingerprint#Canvas_and_WebGL" target="_blank">fingerprinting via Canvas and WebGL</a> or <a href="https://en.wikipedia.org/wiki/Device_fingerprint#Browser_extensions" target="_blank">analysis of active browser extensions</a>. 
                <br><b>This article is about a less known way to achieve something similar!</b>
                <br>
                
                <h3>
                    <a id="content-background" class="anchor" href="#content-background" aria-hidden="true">
                        <span class="octicon octicon-link"></span>
                    </a>
                    Background
                </h3>


                Modern browsers offer a wide range of features to improve and simplify the user experience.<br>
                One of these features are the so-called favicons: A <a href="https://en.wikipedia.org/wiki/Favicon" target="_blank">favicon</a> is a small (usually 16×16 or 32×32 pixels) logo used by web browsers to brand a website in a recognizable way. 
                Favicons are usually shown by most browsers in the address bar and next to the page's name in a list of bookmarks.

                <br><br>
                To serve a favicon on their website, a developer has to include an <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link" target="_blank">&lt;link rel&gt;</a> attribute in the webpage’s header.
                If this tag does exist, the browser requests the icon from the predefined source and if the server response contains an valid icon file that can be properly rendered this icon is displayed by the browser. In any other case, a blank favicon is shown.
                <br><br>
                <div class="highlight highlight-source-js">
                    <pre><span class="pl-ent">&lt;link </span><span class="pl-c1">rel</span><span class="pl-mb">=</span><span class="pl-k">"icon" </span><span class="pl-c1">href</span><span class="pl-mb">=</span><span class="pl-k">"/favicon.ico"</span> </span><span class="pl-c1">type</span><span class="pl-mb">=</span><span class="pl-k">"image/x-icon"</span><span class="pl-ent">&gt;</span></pre>
                </div>

                The favicons must be made very easily accessible by the browser. Therefore, they are cached in a separate local database on the system, called the favicon cache (F-Cache).
                A F-Cache data entries includes the visited URL (subdomain, domain, route, URL paramter), the favicon ID and the time to live (TTL). 
                <br>While this provides web developers the ability to delineate parts of their website using a wide variety of icons for individual routes and subdomains, it also leads to a possible <b>tracking scenario</b>.
                <br>
                <br>
                    When a user visits a website, the browser checks if a favicon is needed by looking up the source of the shortcut icon link reference of the requested webpage.<br>
                    The browser initialy checks the local F-cache for an entry containing the URL of the active website. If a favicon entry exists, the icon will be loaded from the cache and then displayed. 
                    However, if there is no entry, for example because no favicon has ever been loaded under this particular domain, or the data in the cache is out of date, the browser makes a GET request to the server to load the site's favicon.
                <br>
                
                <h3>
                    <a id="content-threat-model" class="anchor" href="#content-threat-model" aria-hidden="true">
                        <span class="octicon octicon-link"></span>
                    </a>
                    Threat Model
                </h3>
                In the article a possible threat model is explained that allows to assign a <b>unique identifier to each browser</b> in order to draw conclusions about the user and to be able to identify this user even in case of applied anti-fingerprint measures, such as the use of a <a href="https://en.wikipedia.org/wiki/Virtual_private_network" target="_blank">VPN</a>, deletion of <a href="https://en.wikipedia.org/wiki/HTTP_cookie" target="_blank">cookies</a>, deletion of the <a href="https://en.wikipedia.org/wiki/Web_cache" target="_blank">browser cache</a> or manipulation of the <a href="https://developer.mozilla.org/en-US/docs/Glossary/Request_header" target="_blank">client header information</a>.
                <br><br>
                A web server can draw conclusions about whether a browser has already loaded a favicon or not:<br>
                So when the browser requests a web page, if the favicon is not in the local F-cache, another request for the favicon is made. If the icon already exists in the F-Cache, no further request is sent.<br>
                By combining the state of delivered and not delivered favicons for specific URL paths for a browser, a unique pattern (identification number) can be assigned to the client.
                <br>When the website is reloaded, the web server can reconstruct the identification number with the network requests sent by the client for the missing favicons and thus identify the browser.
                <br>
                <br>
                <br>

                <ol>
                    <li>
                        <b>Write</b> identification
                        <p>
                            The goal of the <b>write</b> operation is to generate a unique identifier and store it on the client side.<br>
                            First step is to create a new N-bit ID on the server and translate it to a path vector as shown below.<br><br>
                            <strong>Example:</strong>
                            <div class="highlight highlight-source-js">
                                <pre><span class="pl-k">const</span> N<span class="pl-k"> = </span><span class="pl-s">4</span>;<br><span class="pl-k">const</span> ROUTES<span class="pl-k"> = </span>[<span class="pl-s"><span class="pl-pds">"</span>/a<span class="pl-pds">"</span></span>, <span class="pl-s"><span class="pl-pds">"</span>/b<span class="pl-pds">"</span></span>, <span class="pl-s"><span class="pl-pds">"</span>/c<span class="pl-pds">"</span></span>, <span class="pl-s"><span class="pl-pds">"</span>/d<span class="pl-pds">"</span></span>];<br><span class="pl-k">const</span> ID<span class="pl-k"> = </span><span class="pl-ms">generateNewID()</span>;<span class="pl-mb"> // -> 1010 • (select unassigned decimal number, here ten: 10 -> 1010b in binary)</span></pre>
                            </div>
                            <div class="highlight highlight-source-js">
                                <pre><span class="pl-k">const</span> vector<span class="pl-k"> = </span><span class="pl-ms">generateVectorFromID(<span class="pl">ID</span>)</span>;<span class="pl-mb"> // -> ["/a", "/c"] • (because [a, b, c, d] where [1, 0, 1, 0] is 1 -> a, c)</span></pre>
                            </div>
                        </p>
                        Second step is to store the actual data inside the browser:<br>
                        The user will be redirected along all of the website paths, starting at <i>/a</i>, navigating to <i>/b</i>, to <i>/c</i> and finally to <i>/d</i>.
                        <br>
                        <ul>
                            <li>/a</li>
                            <li>/b</li>
                            <li>/c</li>
                            <li>/d</li>
                        </ul>
                        <br>
                       
                        While the user is redirected on every load the browser requests a favicon for the respective route, going the same way like<i>/a/favicon.ico</i>, to <i>/b/favicon.ico</i>, to <i>/c/favicon.ico</i> and finally to <i>/d/favicon.ico</i>.
                        <br>
                        <ul>
                            <li>/a/favicon.ico</li>
                            <li>/b/favicon.ico</li>
                            <li>/c/favicon.ico</li>
                            <li>/d/favicon.ico</li>
                        </ul>

                        <br>

                        The webserver will now only process those favicon requests whose path is present in the previously created path vector. If the route is present the webserver answers with the favicon file and <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/200" target="_blank">Status 200 OK</a>.<br>
                        If the requested route is not in the path vector, the webserver aborts the request with an <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404" target="_blank">Error 404 Not Found</a>, or sends no response.<br>
                        Since the browser - as described earlier - only stores the delivered favicons in the F-Cache, we have now stored our unique identification number and the writing process is complete. <br><br>
                        In the above example, the webserver only responds to requests for the favicons under paths <i>/a/favicon.ico</i> and <i>/c/favicon.ico</i>. The F-Cache only has favicons-entries for these two paths.<br>


                        <a href="/assets/diagram-write.png" target="_blank">
                            <img class="diagram-image" src="/assets/diagram-write.svg">
                        </a>

                        <br>
                        
                    </li>
                    <br>
                    <br>
                    <li>
                        <b>Read</b> identification
                        <p>
                            Here the goal is to re-identify a returning user based on his existing F-Cache entries.<br><br>
                            In read mode the server always responds to favicon requests with an <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404" target="_blank">Error 404 Not Found</a> status, but responds normally to all other requests. 
                            This preserves the <b>integrity of the cached favicons</b> during the read operation, since no new F-cache entry is created by the browser.<br>
                            To reconstruct a visitor's identifier, the browser must be routed through all available routes. The server records which favions are requested by the browser (those that are not present in the browsers F-cache) and which are not.
                            <br>
                            <br>
                            <strong>Example:</strong>
                            <div class="highlight highlight-source-js">
                                <pre><span class="pl-k">const</span> visitedRoutes<span class="pl-k"> = </span><span class="pl">[]</span>;<br><span class="pl-k">Webserver.onvisit</span><span class="pl-k"> = <span class="pl">(route)</span> => </span><span class="pl-s"><span class="pl">visitedRoutes.<span class="pl-s">push(</span></span></span>route<span class="pl-s">)</span>; <span class="pl-mb"> // -> ["/b", "/d"]</span><br><span class="pl-k">Webserver.ondone</span><span class="pl-k"> = <span class="pl">()</span> => </span><span class="pl-s"><span class="pl">{ <span class="pl-k">const</span> ID = <span class="pl-ms">getIDFromVector(</span></span></span>visitedRoutes<span class="pl-ms">)</span> };<span class="pl-mb"> // -> 10 • (because "/a" and "/b" are missing -> 1010b)</span></pre>
                            </div>
                            The server can thus reconstruct the identification from the missing favicon requests and the reading process is complete.
                            <br>
                        </p>

                        <a href="/assets/diagram-read.png" target="_blank">
                            <img class="diagram-image" src="/assets/diagram-read.svg">
                        </a>
                        <br>
                    </li>
                </ol>
                
                <h3>
                    <a id="content-target" class="anchor" href="#content-target" aria-hidden="true">
                        <span class="octicon octicon-link"></span>
                    </a>
                    Target
                </h3>

                It looks like all top browsers are vulnerable to this attack scenario. <br>Mobile browsers are also affected.<br><br>
                <table>
                    <thead>
                      <tr>
                        <th align="center"><p>Browser</p></th>
                        <th align="center"><p>Windows</p></th>
                        <th align="center"><p>MacOS</p></th>
                        <th align="center"><p>Linux</p></th>
                        <th align="center"><p>iOS</p></th>
                        <th align="center"><p>Android</p></th>
                        <th align="center"><i>Info</i></th>
                      </tr>
                    </thead>
                    <tbody>
                      <tr>
                          <td>Chrome <em>(v 87.0)</em></td>
                          <td align="center">✅</td>
                          <td align="center">✅</td>
                          <td align="center">✅</td>
                          <td align="center">✅</td>
                          <td align="center">✅</td>
                          <td>-</td>
                      </tr>
                      <tr>
                          <td>Safari <em>(v 14.0)</em></td>
                          <td align="center">-</td>
                          <td align="center">✅</td>
                          <td align="center">-</td>
                          <td align="center">✅</td>
                          <td align="center">-</td>
                          <td>-</td>
                      </tr>
                      <tr>
                          <td>Edge <em>(v 87.0)</em></td>
                          <td align="center">✅</td>
                          <td align="center">✅</td>
                          <td align="center">❌</td>
                          <td align="center">❌</td>
                          <td align="center">✅</td>
                          <td>-</td>
                      </tr>
                      <tr>
                          <td>Firefox <em>(v 85.0)</em></td>
                          <td align="center">✅</td>
                          <td align="center">✅</td>
                          <td align="center">❌</td>
                          <td align="center">❌</td>
                          <td align="center">❌</td>
                          <td>Fingerprint different in incognito mode</td>
                      </tr>
                      <tr>
                          <td>Brave <em>(v 1.19.92)</em></td>
                          <td align="center">❌</td>
                          <td align="center">❌</td>
                          <td align="center">❌</td>
                          <td align="center">❔</td>
                          <td align="center">❌</td>
                          <td>-</td>
                      </tr>
                    </tbody>
                </table>
                <table>
                    <thead>
                      <tr>
                        <th align="center"><p>Browser</p></th>
                        <th align="center"><p>Windows</p></th>
                        <th align="center"><p>MacOS</p></th>
                        <th align="center"><p>Linux</p></th>
                        <th align="center"><p>iOS</p></th>
                        <th align="center"><p>Android</p></th>
                        <th align="center"><i>Info</i></th>
                      </tr>
                    </thead>
                    <tbody>
                      <tr>
                        <td><b>Brave</b> (v 1.14.0)</td>
                        <td align="center">✅</td>
                        <td align="center">✅</td>
                        <td align="center">✅</td>
                        <td align="center">✅</td>
                        <td align="center">✅</td>
                        <td>-</td>
                      </tr>
                      <tr>
                        <td><b>Firefox</b> (&lt; v 84.0)</td>
                        <td align="center">✅</td>
                        <td align="center">✅</td>
                        <td align="center">❔</td>
                        <td align="center">❌</td>
                        <td align="center">✅</td>
                        <td>-</td>
                      </tr>
                    </tbody>
                </table>

                <br><br>

                The demonstration also impressively shows that applying anti-tracking software, adblockers, VPN or surfing in incognito mode does not offer any significant improvement and the browser remains vulnerable to the tracking even with these measures:<br><br>
                <table>
                    <thead>
                        <tr>
                            <th>Browser</th>
                            <th align="center">Incognito / Private mode</th>
                            <th align="center">Clear Website Data</th>
                            <th align="center">VPN</th>
                            <th align="center">Adblock / Anti-Tracking</th>
                        </tr>
                    </thead>
                    <tbody>
                        <tr>
                            <td>Chrome</td>
                            <td align="center">✅</td>
                            <td align="center">✅</td>
                            <td align="center">✅</td>
                            <td align="center">✅</td>
                        </tr>
                        <tr>
                            <td>Safari</td>
                            <td align="center">✅</td>
                            <td align="center">✅</td>
                            <td align="center">✅</td>
                            <td align="center">✅</td>
                        </tr>
                        <tr>
                            <td>Edge</td>
                            <td align="center">✅</td>
                            <td align="center">✅</td>
                            <td align="center">✅</td>
                            <td align="center">✅</td>
                        </tr>
                        <tr>
                            <td>Firefox</td>
                            <td align="center">✅</td>
                            <td align="center">✅</td>
                            <td align="center">✅</td>
                            <td align="center">✅</td>
                        </tr>
                    </tbody>
                </table>


                <h3>
                    <a id="content-scalability-performance" class="anchor" href="#content-scalability-performance" aria-hidden="true">
                        <span class="octicon octicon-link"></span>
                    </a>
                    Scalability & Performance
                </h3>

                By varying the number of bits that corresponds to the number of redirects to subpaths, this attack can be scaled almost arbitrarily.<br>
                It can distinguish 2^N unique users, where N is the number of redirects on the client side.<br>
                
                <br>

                Since each subpath redirection increases the duration of the identification, the performance of the attack the webserver can dynamically increase the number of redirects. This is done trivially by appending a new subpath in the sequence of subpaths.
                <br>The calculation of the number of redirects (N) is done by the operation: "<i>floor(log2(id))+1</i>", where id corresponds to the decimal identification number.
                <br>
                For example, if the server changes from 3-bit identifiers to 4-bit identifiers, the subpath vector will change from ["/a", "/b", "/c"] to ["/a", "/b", "/c", "/d"] and
                the identifier of a client (here dec. 6) changes from "011" to "0110" without changing the actual value of already written F-Cache identifiers. 
                <br><br>This leads to the fact that only the minimum number of redirections is necessary for the attack.

                <a href="/assets/diagram-scalability.png" target="_blank">
                    <img class="diagram-image" src="/assets/diagram-scalability.svg">
                </a>

                <br>
                <br>
                
                
                The time taken for the read and write operation increases as the number of distinguishable clients and redirects does.<br><br>
                <i>The following measured times prove to be the minimum time required for this attack to work. The actual time required in practice depends on many more factors, such as Internet speed, location, hardware setup and browser type.</i>
                <br><br>
                <table>
                    <thead>
                        <tr>
                            <th>Redirects<br>(N bit)</th>
                            <th>distinguishable clients</th>
                            <th>write time</th>
                            <th>read time</th>
                            <th>scale information</th>
                        </tr>
                    </thead>
                    <tbody>
                        <tr>
                            <td align="center">2</em></td>
                            <td align="center">4</td>
                            <td>&lt; 300<small>ms</small></td>
                            <td>&lt; 300<small>ms</small></td>
                            <td>One user with four browsers</td>
                        </tr>
                        <tr>
                            <td align="center">3</em></td>
                            <td align="center">8</td>
                            <td>&lt; 300<small>ms</small></td>
                            <td>~ 300<small>ms</small></td>
                            <td>About the amount of Kardashians</td>
                        </tr>
                        <tr>
                            <td align="center">4</em></td>
                            <td align="center">16</td>
                            <td>&lt; 1<small>s</small></td>
                            <td>~ 1<small>s</small></td>
                            <td>Bunch of your neighbors</td>
                        </tr>
                        <tr>
                            <td align="center">8</em></td>
                            <td align="center">256</td>
                            <td>&lt; 1<small>s</small></td>
                            <td>~ 1<small>s</small></td>
                            <td>All your facebook-friends</td>
                        </tr>
                        <tr>
                            <td align="center">10</em></td>
                            <td align="center">1024</td>
                            <td>&lt; 1.2<small>s</small></td>
                            <td>~ 1<small>s</small></td>
                            <td>Really small village</td>
                        </tr>
                        <tr>
                            <td align="center">20</em></td>
                            <td align="center">1,048,576</td>
                            <td>&lt; 1.8<small>s</small></td>
                            <td>&lt; 1.5<small>s</small></td>
                            <td>Small city (San Jose, California)</td>
                        </tr>
                        <tr>
                            <td align="center">24</em></td>
                            <td align="center">16,777,216</td>
                            <td>&lt; 2.4<small>s</small></td>
                            <td>&lt; 2<small>s</small></td>
                            <td>Whole Netherlands</td>
                        </tr>
                        <tr>
                            <td align="center">32</em></td>
                            <td align="center">4,294,967,296</td>
                            <td>~ 3<small>s</small></td>
                            <td>&lt; 3<small>s</small></td>
                            <td>All people with internet access</td>
                        </tr>
                        <tr>
                            <td align="center">34</em></td>
                            <td align="center">17,179,869,184</td>
                            <td>~ 4<small>s</small></td>
                            <td>~ 4<small>s</small></td>
                            <td>All people with internet access each using 4 different browsers</td>
                        </tr>
                    </tbody>
                </table>


                <h3>
                    <a id="content-related-work" class="anchor" href="#content-related-work" aria-hidden="true">
                        <span class="octicon octicon-link"></span>
                    </a>
                    Related work
                </h3>

                <ul>
                    <li>
                        <s><a target="_blank" href="https://www.cs.uic.edu/~polakis/papers/solomos-ndss21.pdf">cs.uic.edu</a></s>:
                        Study by Scientists at the University of Illinois, Chicago
                    </li>
                    <li>
                        <a target="_blank" href="https://heise.de/-5027814">heise.de</a>:
                        Browser-Fingerprinting: Favicons als "Super-Cookies"
                    </li>
                </ul>

                <hr>
            </article>
        </div>
    </body>
</html>
Download .txt
gitextract_i4uyxewp/

├── .github/
│   └── FUNDING.yml
├── .gitignore
├── LICENSE
├── README.md
└── server/
    ├── docker-compose.yml
    ├── main.js
    ├── main.ts
    ├── package.json
    ├── tsconfig.json
    └── www/
        ├── 404.html
        ├── identity.html
        ├── index.html
        ├── launch.html
        ├── redirect.html
        ├── referrer-v2.html
        ├── referrer.html
        ├── tsconfig.json
        └── workwise.html
Download .txt
SYMBOL INDEX (80 symbols across 2 files)

FILE: server/main.js
  class Storage (line 21) | class Storage {
    method constructor (line 22) | constructor() {
    method content (line 29) | get content() {
    method content (line 32) | set content(data) {
    method read (line 51) | read() {
    method write (line 54) | write(content) {
    method createPersistent (line 58) | createPersistent() {
    method existsPersistent (line 61) | existsPersistent() {
  constant STORAGE (line 65) | const STORAGE = new Storage().content;
  constant WEBSERVER_DOMAIN_1 (line 67) | const WEBSERVER_DOMAIN_1 = process.env["HOST_MAIN"] ?? "localhost:10080";
  constant WEBSERVER_DOMAIN_2 (line 68) | const WEBSERVER_DOMAIN_2 = process.env["HOST_DEMO"] ?? "localhost:10081";
  constant WEBSERVER_PORT_1 (line 69) | const WEBSERVER_PORT_1 = +process.env["PORT_MAIN"] ?? 10080;
  constant WEBSERVER_PORT_2 (line 70) | const WEBSERVER_PORT_2 = +process.env["PORT_DEMO"] ?? 10081;
  constant CACHE_IDENTIFIER (line 71) | const CACHE_IDENTIFIER = STORAGE.cacheID ?? generateUUID("xxxxxxxx", "01...
  constant FILE (line 73) | const FILE = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42m...
  class Webserver (line 82) | class Webserver {
    method getVector (line 83) | static getVector(identifier) {
    method getIdentifier (line 92) | static getIdentifier(vector, size = vector.size) {
    method hasRoute (line 96) | static hasRoute(route) {
    method getRouteByIndex (line 99) | static getRouteByIndex(index) {
    method getIndexByRoute (line 102) | static getIndexByRoute(route) {
    method getNextRoute (line 105) | static getNextRoute(route) {
    method setCookie (line 111) | static setCookie(res, name, value, options = { httpOnly: false, expire...
    method sendFile (line 114) | static sendFile(res, route, options = {}, type = "html") {
  class Profile (line 137) | class Profile {
    method constructor (line 138) | constructor(uid, identifier = null) {
    method get (line 148) | static get(uid) {
    method has (line 153) | static has(uid) {
    method from (line 156) | static from(uid, identifier) {
    method destructor (line 159) | destructor() {
    method uid (line 162) | get uid() {
    method vector (line 165) | get vector() {
    method visited (line 168) | get visited() {
    method identifier (line 171) | get identifier() {
    method getRouteByIndex (line 174) | getRouteByIndex(index) {
    method _isReading (line 177) | _isReading() {
    method _visitRoute (line 180) | _visitRoute(route) {
    method _calcIdentifier (line 183) | _calcIdentifier() {
    method _setStorageSize (line 186) | _setStorageSize(size) {
    method storageSize (line 189) | get storageSize() {

FILE: server/main.ts
  class Storage (line 49) | class Storage {
    method constructor (line 53) | constructor() {
    method content (line 58) | public get content(): any {
    method content (line 61) | public set content(data: any) {
    method read (line 79) | private read(): Storage {
    method write (line 82) | private write(content: object): Storage {
    method createPersistent (line 86) | private createPersistent() {
    method existsPersistent (line 89) | private existsPersistent() {
  constant STORAGE (line 93) | const STORAGE: any = new Storage().content;
  constant WEBSERVER_DOMAIN_1 (line 100) | const WEBSERVER_DOMAIN_1: string    = process.env["HOST_MAIN"] ?? "local...
  constant WEBSERVER_DOMAIN_2 (line 101) | const WEBSERVER_DOMAIN_2: string    = process.env["HOST_DEMO"] ?? "local...
  constant WEBSERVER_PORT_1 (line 102) | const WEBSERVER_PORT_1: number      = +process.env["PORT_MAIN"] ?? 10080;
  constant WEBSERVER_PORT_2 (line 103) | const WEBSERVER_PORT_2: number      = +process.env["PORT_DEMO"] ?? 10081;
  constant CACHE_IDENTIFIER (line 104) | const CACHE_IDENTIFIER: string      = STORAGE.cacheID ?? generateUUID("x...
  constant FILE (line 110) | const FILE = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42m...
  class Webserver (line 126) | class Webserver {
    method getVector (line 129) | public static getVector(identifier: number): Array<string> {
    method getIdentifier (line 138) | public static getIdentifier(vector: Set<string>, size: number = vector...
    method hasRoute (line 142) | public static hasRoute(route: string): boolean {
    method getRouteByIndex (line 145) | public static getRouteByIndex(index: number): string {
    method getIndexByRoute (line 148) | public static getIndexByRoute(route: string): number {
    method getNextRoute (line 151) | public static getNextRoute(route: string): string | null {
    method setCookie (line 157) | public static setCookie(res: express.Response,
    method sendFile (line 162) | public static sendFile( res: express.Response,
  class Profile (line 190) | class Profile {
    method get (line 192) | public static get(uid: string): Profile {
    method has (line 197) | public static has(uid: string): boolean {
    method from (line 200) | public static from(uid: string, identifier?: number): Profile {
    method constructor (line 210) | constructor(uid: string, identifier: number = null) {
    method destructor (line 217) | public destructor() {
    method uid (line 220) | public get uid(): string {
    method vector (line 223) | public get vector(): Array<string> {
    method visited (line 226) | public get visited(): Set<string> {
    method identifier (line 229) | public get identifier(): number {
    method getRouteByIndex (line 232) | public getRouteByIndex(index: number): string {
    method _isReading (line 235) | public _isReading(): boolean {
    method _visitRoute (line 238) | public _visitRoute(route: string) {
    method _calcIdentifier (line 241) | public _calcIdentifier(): number {
    method _setStorageSize (line 244) | public _setStorageSize(size: number) {
    method storageSize (line 247) | public get storageSize(): number {
Condensed preview — 18 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (143K chars).
[
  {
    "path": ".github/FUNDING.yml",
    "chars": 199,
    "preview": "github: [jonasstrehle]\npatreon: unyt\nopen_collective: #\nko_fi: jonasstrehle\ntidelift: #\ncommunity_bridge: #\nliberapay: #"
  },
  {
    "path": ".gitignore",
    "chars": 28,
    "preview": ".DS_Store\n/server/data.json\n"
  },
  {
    "path": "LICENSE",
    "chars": 1070,
    "preview": "MIT License\n\nCopyright (c) 2021 Jonas Strehle\n\nPermission is hereby granted, free of charge, to any person obtaining a c"
  },
  {
    "path": "README.md",
    "chars": 15488,
    "preview": "<p align=\"center\">\r\n  <a href=\"https://supercookie.me\">\r\n    <img src=\"http://supercookie.me/favicon.ico\" alt=\"supercook"
  },
  {
    "path": "server/docker-compose.yml",
    "chars": 3259,
    "preview": "version: \"3\"\n\nservices:\n  proxy:\n    image: \"traefik:v2.0\"\n    container_name: supercookie-proxy\n    hostname: supercook"
  },
  {
    "path": "server/main.js",
    "chars": 15637,
    "preview": "import express from \"express\";\nimport path from \"path\";\nimport fs from \"fs\";\nimport cookieParser from \"cookie-parser\";\ni"
  },
  {
    "path": "server/main.ts",
    "chars": 19540,
    "preview": "import express from \"express\";\nimport path from \"path\";\nimport fs from \"fs\";\nimport cookieParser from \"cookie-parser\";\ni"
  },
  {
    "path": "server/package.json",
    "chars": 251,
    "preview": "{\n  \"type\": \"module\",\n  \"dependencies\": {\n    \"@types/cookie-parser\": \"^1.4.2\",\n    \"@types/cors\": \"^2.8.9\",\n    \"@types"
  },
  {
    "path": "server/tsconfig.json",
    "chars": 395,
    "preview": "{\n    \"compilerOptions\": {\n        \"allowSyntheticDefaultImports\": true,\n        \"sourceMap\": false,\n        \"removeComm"
  },
  {
    "path": "server/www/404.html",
    "chars": 2529,
    "preview": "<!DOCTYPE html>\n<html>\n    <head>\n        <meta charset=\"utf-8\"/>\n        <title>supercookie • progress</title>\n        "
  },
  {
    "path": "server/www/identity.html",
    "chars": 6593,
    "preview": "<!DOCTYPE html>\n<html>\n    <head>\n        <meta charset=\"utf-8\"/>\n        <title>id • {{hash}}</title>\n        <meta nam"
  },
  {
    "path": "server/www/index.html",
    "chars": 6979,
    "preview": "<!DOCTYPE html>\n<html>\n    <head>\n        <meta charset=\"utf-8\"/>\n        <title>supercookie • welcome</title>\n        <"
  },
  {
    "path": "server/www/launch.html",
    "chars": 1482,
    "preview": "<!DOCTYPE html>\n<html>\n    <head>\n        <meta charset=\"utf-8\"/>\n        <title>supercookie</title>\n        <meta name="
  },
  {
    "path": "server/www/redirect.html",
    "chars": 1035,
    "preview": "<!DOCTYPE html>\n<html>\n    <head>\n        <meta charset=\"utf-8\"/>\n        <title>supercookie • progress</title>\n        "
  },
  {
    "path": "server/www/referrer-v2.html",
    "chars": 1594,
    "preview": "<!DOCTYPE html>\n<html>\n    <head>\n        <meta charset=\"utf-8\"/>\n        <title>supercookie • {{index}}</title>\n       "
  },
  {
    "path": "server/www/referrer.html",
    "chars": 1300,
    "preview": "<!DOCTYPE html>\n<html>\n    <head>\n        <meta charset=\"utf-8\"/>\n        <title>supercookie • {{index}}</title>\n       "
  },
  {
    "path": "server/www/tsconfig.json",
    "chars": 313,
    "preview": "{\n    \"compilerOptions\": {\n        \"sourceMap\": false,\n        \"removeComments\": true,\n        \"target\": \"ESNext\",\n     "
  },
  {
    "path": "server/www/workwise.html",
    "chars": 59181,
    "preview": "<!DOCTYPE html>\n<html>\n    <head>\n        <meta charset=\"utf-8\"/>\n        <title>supercookie • workwise</title>\n        "
  }
]

About this extraction

This page contains the full source code of the jonasstrehle/supercookie GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 18 files (133.7 KB), approximately 32.3k tokens, and a symbol index with 80 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!