├── .prettierrc ├── LICENSE ├── README.md ├── dist └── bundle.js ├── docs ├── CNAME ├── bundle.js ├── example_page.html ├── favicon.ico ├── iframe.html └── index.html ├── gen.py ├── package.json ├── requirements.txt ├── sample_output.json ├── sample_output_headless.json ├── server ├── README.md ├── __init__.py ├── client.py ├── db.py ├── files │ ├── bundle.js │ ├── example_page.html │ ├── favicon.png │ └── iframe.html └── serve.py └── src ├── constants.js ├── fingerprint.js └── index.js /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "useTabs": false, 4 | "semi": true, 5 | "singleQuote": false, 6 | "trailingComma": "all", 7 | "bracketSpacing": true, 8 | "arrowParens": "always", 9 | "printWidth": 120, 10 | "plugins": [], 11 | "quoteProps": "consistent" 12 | } 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Aurin Aegerter 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FP-Collector 2 | 3 | Simple script for collecting some unique information from browsers 4 | 5 | **checkout the [demo](https://fp.totallysafe.ch/)** 6 | 7 | - fetch fingerprint for [`selenium-driverless`](https://github.com/kaliiiiiiiiii/Selenium-Driverless) (applying fingerprints not yet implemented) 8 | 9 | ### Feel free to contribute! 10 | 11 | See dev-branch for the latest features. 12 | 13 | ### Usage 14 | 15 | You can embed the script into your website using a free CDN. 16 | 17 | ```html 18 | 34 |

Welcome

35 |
Simple list
36 | 42 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliiiiiiiiii/driverless-fp-collector/00c983cb03cae0e44c0de686ef145ded370a198f/docs/favicon.ico -------------------------------------------------------------------------------- /docs/iframe.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 |

driverless-fp-collector demo

10 | Source-Code 11 |
12 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | driverless-fp-collector demo 6 | 7 | 8 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |

driverless-fp-collector demo

21 | Source-Code 22 |

23 |         
24 | 50 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /gen.py: -------------------------------------------------------------------------------- 1 | # https://github.com/kaliiiiiiiiii/driverless-fp-collector 2 | 3 | from selenium_driverless import webdriver 4 | from selenium_driverless.types.by import By 5 | import os 6 | import asyncio 7 | import json 8 | 9 | 10 | async def get_fp(driver, script): 11 | await driver.get(os.getcwd() + "/docs/index.html") 12 | await asyncio.sleep(1) 13 | res = asyncio.create_task(driver.execute_async_script(script, timeout=120)) 14 | elem = await driver.find_element(By.ID, "get-fp") 15 | await asyncio.sleep(0.5) 16 | await elem.click(move_to=False) 17 | await asyncio.sleep(0.5) 18 | await elem.click(move_to=False) 19 | res = await res 20 | res = json.loads(res) 21 | return res 22 | 23 | 24 | async def get_fp_native(script): 25 | async with webdriver.Chrome(debug=False) as driver: 26 | res = await get_fp(driver, script) 27 | return res 28 | 29 | 30 | async def get_fp_headless(script): 31 | options = webdriver.ChromeOptions() 32 | options.add_argument("--headless=new") 33 | async with webdriver.Chrome(debug=False, options=options) as headles_driver: 34 | res = await get_fp(headles_driver, script) 35 | return res 36 | 37 | 38 | async def main(): 39 | os.system("npm run build") 40 | script = """ 41 | // execute 42 | async function handler(){ 43 | var elem = document.documentElement; 44 | function callback(e){ 45 | window.fp_click_callback(e) 46 | elem.removeEventListener("mousedown", this); 47 | elem.removeEventListener("touchstart", this); 48 | } 49 | var data = getFingerprint(true, true); 50 | elem.addEventListener("mousedown", callback); 51 | elem.addEventListener("touchstart", callback); 52 | data = await data 53 | console.log(data); 54 | return JSON.stringify(data) 55 | debugger 56 | } 57 | res = handler() 58 | res.catch((e) => {throw e}) 59 | res.then(arguments[arguments.length-1]) 60 | """ 61 | 62 | fp_native, fp_headless = await asyncio.gather( 63 | get_fp_native(script), get_fp_headless(script) 64 | ) 65 | with open(os.getcwd() + "/sample_output.json", "w+", encoding="utf-8") as f: 66 | f.write(json.dumps(fp_native, indent=4)) 67 | with open( 68 | os.getcwd() + "/sample_output_headless.json", "w+", encoding="utf-8" 69 | ) as f: 70 | f.write(json.dumps(fp_headless, indent=4)) 71 | 72 | 73 | asyncio.run(main()) 74 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fp-collector", 3 | "version": "0.0.5", 4 | "description": "**checkout the [demo](https://kaliiiiiiiiii.github.io/driverless-fp-collector/)**", 5 | "main": "./dist/bundle.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "npx browserify src/index.js -o dist/bundle.js && npx uglifyjs dist/bundle.js --output dist/bundle.js && npx shx cp dist/bundle.js docs/bundle.js && npx shx cp dist/bundle.js server/files/bundle.js" 9 | }, 10 | "keywords": [], 11 | "author": "kaliii & peet", 12 | "license": "MIT", 13 | "dependencies": { 14 | "browserify": "^17.0.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | selenium-driverless 2 | aiohttp~=3.8.6 3 | orjson~=3.9.10 4 | motor~=3.3.2 5 | pymongo~=4.6.1 -------------------------------------------------------------------------------- /server/README.md: -------------------------------------------------------------------------------- 1 | Run server: 2 | 3 | 1. Start a mongodb server locally 4 | 2. `python3 server.py` 5 | 6 | - more than 30 entries in the last hour on a IP will make you ignored and add a flag-point 7 | - more than 10 flag-points will get your IP flagged permanently 8 | 9 | # DataBase endpoints: 10 | `/api/v1/compile?q={"type":"windows"}` 11 | valid types are: 12 | ```json 13 | [ 14 | "a_paths","bots","windows", 15 | "linux","ios","android","mac","other" 16 | ] 17 | ``` 18 | or just query by json. Internally uses `MongoDb.collection.find(q)` 19 | `/api/v1/get_val?id=658ca35031cee1347dad5478` 20 | valid collections are: 21 | ```json 22 | [ 23 | "a_paths","bots","windows", 24 | "linux","ios","android","mac","other" 25 | ] 26 | ``` 27 | 28 | 29 | ## Todos 30 | - [x] add path endpoints for each platform 31 | - [ ] add similarity endpoint -------------------------------------------------------------------------------- /server/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliiiiiiiiii/driverless-fp-collector/00c983cb03cae0e44c0de686ef145ded370a198f/server/__init__.py -------------------------------------------------------------------------------- /server/client.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import copy 3 | import json 4 | import traceback 5 | import typing 6 | from concurrent import futures 7 | 8 | import aiohttp 9 | import orjson 10 | 11 | 12 | class Client: 13 | def __init__(self, host: str = "http://localhost:80", max_workers=5): 14 | self._host = host 15 | self._max_workers = max_workers 16 | self._val_cache = {} 17 | 18 | @staticmethod 19 | async def _get(url: str, params: dict = None) -> bytes: 20 | async with aiohttp.ClientSession() as r: 21 | async with r.get(url, params=params) as resp: 22 | assert resp.status == 200 23 | return await resp.read() 24 | 25 | async def compile(self, query: dict = None): 26 | if not query: 27 | query = {} 28 | res = await self._get(self.api_v1 + "/compile", params={"q": json.dumps(query)}) 29 | return await self._load_json(res) 30 | 31 | @staticmethod 32 | def path2dict(paths: typing.Dict[str, typing.Dict[str, int]], 33 | callback: typing.Callable[[str, typing.Dict[typing.Union[str, int], int]], typing.Union[str, int]]): 34 | _dict = {} 35 | 36 | def add_value(d, _path, _value): 37 | curr = d 38 | _path = json.loads(_path) 39 | k = _path[-1] 40 | _path = _path[:-1] 41 | for _key in _path: 42 | if _key not in curr: 43 | curr[_key] = {} 44 | curr = curr[_key] 45 | curr[k] = _value 46 | return d 47 | 48 | _paths = [] 49 | for path, values in paths.items(): 50 | values = copy.deepcopy(values) 51 | 52 | # get n times, if is list 53 | n:typing.Union[typing.Dict[int, int], None] = values.get("l") 54 | if n: 55 | del values["l"] 56 | n:int = int(callback(path, n)) 57 | _list = [] 58 | for _ in range(n): 59 | # todo: improve how list frequencies are calculated//handled 60 | # more than once if is list 61 | value = callback(path, values) 62 | del values[value] 63 | value = json.loads(value) 64 | _list.append(value) 65 | add_value(_dict, path, _list) 66 | else: 67 | value = callback(path, values) 68 | if not value: 69 | breakpoint() 70 | del values[value] 71 | value = json.loads(value) 72 | try: 73 | add_value(_dict, path, value) 74 | except TypeError: 75 | traceback.print_exc() 76 | breakpoint() 77 | return _dict 78 | 79 | @staticmethod 80 | def opt_choose(path: str, values: typing.Dict[str, int]): 81 | _id = None 82 | count = 0 83 | for __id, _count in reversed(values.items()): 84 | if _count > count: 85 | _id = __id 86 | count = _count 87 | return _id 88 | 89 | async def __aenter__(self): 90 | self._pool = futures.ThreadPoolExecutor(max_workers=self._max_workers) 91 | self._loop = asyncio.get_running_loop() 92 | return self 93 | 94 | async def __aexit__(self, exc_type, exc_val, exc_tb): 95 | self._pool.shutdown() 96 | 97 | async def _load_json(self, data: bytes): 98 | return await self._loop.run_in_executor(self._pool, lambda: orjson.loads(data)) 99 | 100 | async def _dump_json(self, data) -> bytes: 101 | return await self._loop.run_in_executor(self._pool, lambda: orjson.dumps(data)) 102 | 103 | @property 104 | def host(self) -> str: 105 | return self._host 106 | 107 | @property 108 | def api_v1(self) -> str: 109 | return self.host + "/api/v1" 110 | 111 | 112 | async def main(): 113 | async with Client() as c: 114 | paths = await c.compile({"type": "windows", "mainVersion": 120}) 115 | _dict = c.path2dict(paths, c.opt_choose) 116 | _dict = c.path2dict(paths, c.opt_choose) 117 | print(_dict) 118 | 119 | 120 | if __name__ == "__main__": 121 | asyncio.run(main()) 122 | -------------------------------------------------------------------------------- /server/db.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import motor.motor_asyncio 4 | import pymongo.errors 5 | import json 6 | 7 | import orjson 8 | import asyncio 9 | import os 10 | import time 11 | import math 12 | import typing 13 | import bson 14 | import logging 15 | from concurrent import futures 16 | from collections import defaultdict 17 | 18 | _dir = os.path.dirname(os.path.abspath(__file__)) 19 | 20 | logger = logging.getLogger("driverless-fp-collector") 21 | logging.basicConfig() 22 | 23 | 24 | class DataBase: 25 | 26 | async def __aenter__(self, host:str=None,max_workers: int = 10): 27 | args = sys.argv 28 | if not host: 29 | if len(args) > 1: 30 | url = args[1] 31 | else: 32 | url = 'localhost:27017' 33 | host = [url] 34 | self._client = motor.motor_asyncio.AsyncIOMotorClient(host=host) 35 | self._db = self.client["fingerprints"] 36 | self._entries = self.db["entries"] 37 | self._fingerprints = self.db["fingerprints"] 38 | self._val_map = {} 39 | 40 | self._ips = self.db["ips"] 41 | try: 42 | await self.db.validate_collection("entries") # Try to validate a collection 43 | except pymongo.errors.OperationFailure: # If the collection doesn't exist 44 | await self.entries.create_index("cookie", unique=True, name="cookie") 45 | await self.ips.create_index("ip", unique=True, name="ip") 46 | 47 | self._loop = asyncio.get_running_loop() 48 | self._pool = futures.ThreadPoolExecutor(max_workers=max_workers) 49 | self._paths_map = {} 50 | return self 51 | 52 | async def __aexit__(self, exc_type, exc_val, exc_tb): 53 | self._pool.shutdown() 54 | self.client.close() 55 | 56 | async def _load_json(self, data: bytes): 57 | return await self._loop.run_in_executor(self._pool, lambda: orjson.loads(data)) 58 | 59 | async def _dump_json(self, data) -> bytes: 60 | return await self._loop.run_in_executor(self._pool, lambda: orjson.dumps(data)) 61 | 62 | async def add_fp_entry(self, ip: str, cookie: str, fp: bytes): 63 | _time = math.floor(time.time()) 64 | ip_doc = await self.ips.find_one({"ip": ip}) 65 | if ip_doc: 66 | if ip_doc["flag"] > 10: 67 | return 68 | timestamps: typing.List[int] = ip_doc["timestamps"] 69 | for idx, stamp in enumerate(timestamps): 70 | if stamp < (_time - 3_600): # 60s*60min => 1h 71 | timestamps.pop(idx) 72 | await self.ips.update_one({"ip": ip}, {"$push": {"timestamps": _time}}) 73 | if len(timestamps) > 20: 74 | await self.ips.update_one({"ip": ip}, {"$inc": {"flag": 1}}) 75 | return 76 | else: 77 | try: 78 | await self.ips.insert_one( 79 | {"ip": ip, "timestamps": [_time], "flag": 0}) 80 | except pymongo.errors.DuplicateKeyError: 81 | pass 82 | 83 | _id = bson.ObjectId() 84 | try: 85 | await self.entries.insert_one( 86 | {"ip": ip, "cookie": cookie, "fp": _id, "timestamp": _time}) 87 | except pymongo.errors.DuplicateKeyError: 88 | pass 89 | else: 90 | pre_json = time.monotonic() 91 | fp = await self._load_json(fp) 92 | logger.debug(f"loading json took: {time.monotonic() - pre_json:_} s") 93 | if fp.get("status") != "pass": 94 | return 95 | platform = fp["HighEntropyValues"]["platform"] 96 | mobile = fp["HighEntropyValues"]["mobile"] 97 | is_bot = fp["is_bot"] 98 | fp["mainVersion"] = int(fp["HighEntropyValues"]["uaFullVersion"].split(".")[0]) 99 | 100 | if is_bot: 101 | fp["type"] = "bot" 102 | if is_bot is True: 103 | pass 104 | elif mobile: 105 | if platform in ["Android", "null", "Linux", "Linux aarch64"] or platform[:10] == "Linux armv": 106 | fp["type"] = "android" 107 | elif platform in ["iPhone", "iPod", "iPad"]: 108 | fp["type"] = "ios" 109 | else: 110 | fp["type"] = "other" 111 | elif platform in ["OS/2", "Pocket PC", "Windows", "Win16", "Win32", "WinCE"]: 112 | fp["type"] = "windows" 113 | elif platform in ["Macintosh", "MacIntel", "MacPPC", "Mac68K"]: 114 | fp["type"] = "mac" 115 | elif platform in ["Linux", "Linux aarch64", "Linux i686", "Linux i686 on x86_64", 116 | "Linux ppc64", "Linux x86_64"] or platform[:10] == "Linux armv": 117 | fp["type"] = "linux" 118 | fp["type"] = "other" 119 | fp["_id"] = _id 120 | await self.fingerprints.insert_one(fp) 121 | 122 | def val2paths(self, values, path: list = None) -> typing.Iterable[typing.Union[str, any]]: 123 | _type = type(values) 124 | if _type is dict: 125 | if path is None: 126 | path = [] 127 | 128 | for key, value in values.items(): 129 | curr_path = path + [key] 130 | yield from self.val2paths(value, curr_path) 131 | else: 132 | if path: 133 | yield json.dumps(path), values 134 | 135 | async def compile_paths(self, query:dict=None): 136 | if not query: 137 | query = {} 138 | 139 | def parse_entry(_entry:dict, _paths:dict): 140 | for path, values in self.val2paths(_entry): 141 | if type(values) is list: 142 | for value in values: 143 | _paths[path][json.dumps(value)] += 1 144 | if "l" not in _paths[path]: 145 | _paths[path]["l"] = defaultdict(lambda: 0) 146 | _paths[path]["l"][str(len(values))] += 1 147 | else: 148 | _paths[path][json.dumps(values)] += 1 149 | paths = defaultdict(lambda:defaultdict(lambda: 0)) 150 | 151 | coro = [] 152 | async for entry in self.fingerprints.find(query): 153 | del entry["_id"] 154 | coro.append(self._loop.run_in_executor(self._pool, lambda: parse_entry(entry, paths))) 155 | await asyncio.gather(*coro) 156 | return paths 157 | 158 | @property 159 | def client(self) -> motor.motor_asyncio.AsyncIOMotorClient: 160 | return self._client 161 | 162 | @property 163 | def db(self) -> motor.motor_asyncio.AsyncIOMotorDatabase: 164 | return self._db 165 | 166 | @property 167 | def entries(self) -> motor.motor_asyncio.AsyncIOMotorCollection: 168 | return self._entries 169 | 170 | @property 171 | def fingerprints(self) -> motor.motor_asyncio.AsyncIOMotorCollection: 172 | return self._fingerprints 173 | 174 | @property 175 | def ips(self) -> motor.motor_asyncio.AsyncIOMotorCollection: 176 | return self._ips 177 | -------------------------------------------------------------------------------- /server/files/bundle.js: -------------------------------------------------------------------------------- 1 | (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i{if(typeof value==="object"){value=j(value,max_depth-1)}var _type=typeof value;if(!["function"].includes(_type)){res.push(value)}});return res}else if(obj){var res={};get_obj_keys(obj).forEach(key=>{var value=obj[key];if(typeof value==="object"){value=j(value,max_depth-1)}var _type=typeof value;if(obj!==undefined&&!["function"].includes(_type)){res[key]=value}});return res}}function get_worker_response(fn){try{const URL=window.URL||window.webkitURL;var fn="self.onmessage=async function(e){postMessage(await ("+fn.toString()+")())}";var blob;try{blob=new Blob([fn],{type:"application/javascript"})}catch(e){window.BlobBuilder=window.BlobBuilder||window.WebKitBlobBuilder||window.MozBlobBuilder;blob=new BlobBuilder;blob.append(response);blob=blob.getBlob()}var url=URL.createObjectURL(blob);var worker=new Worker(url);var _promise=new Promise((resolve,reject)=>{worker.onmessage=m=>{worker.terminate();resolve(m.data)}});worker.postMessage("call");return _promise}catch(e){return new Promise((resolve,reject)=>{reject(e)})}}async function get_permission_state(permission){try{var result=await navigator.permissions.query({name:permission});return result.state}catch(e){}}function get_obj_keys(obj){var res=new Set(Object.getOwnPropertyNames(obj));for(var prop in obj){res.add(prop)}return[...res]}function get_speech(){return new Promise(function(resolve,reject){var speech=speechSynthesis.getVoices();if(speech.length===0){setTimeout(()=>{resolve([])},2e3);speechSynthesis.addEventListener("voiceschanged",()=>{resolve(speechSynthesis.getVoices())})}else{resolve(speech)}})}function checkAudioType(_type){const audio=document.createElement("audio");return audio.canPlayType(_type)}function get_audio_types(){var res={};audioTypes.forEach(t=>{res[t]=checkAudioType(t)});return res}async function listFonts(){await document.fonts.ready;const fontAvailable=new Set;for(const font of fonts.values()){if(document.fonts.check(`12px "${font}"`))fontAvailable.add(font)}return[...fontAvailable.values()]}async function get_permissions(){var res={};permissions.forEach(async function(permission){res[permission]=await get_permission_state(permission)});return res}function get_stack(){var sizeA=0;var sizeB=0;var counter=0;try{var fn_1=function(){counter+=1;fn_1()};fn_1()}catch(_a){sizeA=counter;try{counter=0;var fn_2=function(){var local=1;counter+=local;fn_2()};fn_2()}catch(_b){sizeB=counter}}var bytes=sizeB*8/(sizeA-sizeB);return[sizeA,sizeB,bytes]}function getTimingResolution(){var runs=5e3;var valA=1;var valB=1;var res;for(var i=0;ivalA&&res{globalThis.fp_click_callback=resolve});var is_touch=false;if(e.type=="touchstart"){is_touch=true;e=e.touches[0]||e.changedTouches[0]}var is_bot=e.pageY==e.screenY&&e.pageX==e.screenX;if(is_bot&&1>=outerHeight-innerHeight){is_bot=false}if(is_bot&&is_touch&&navigator.userAgentData.mobile){is_bot="maybe"}if(is_touch==false&&navigator.userAgentData.mobile===true){is_bot="maybe"}if(e.isTrusted===false){is_bot=true}if(check_worker){worker_ua=await get_worker_response(function(){return navigator.userAgent});if(worker_ua!==navigator.userAgent){is_bot=true}}return is_bot}function get_gl_infos(gl){if(gl){const get=gl.getParameter.bind(gl);const ext=gl.getExtension("WEBGL_debug_renderer_info");const parameters={};for(const parameter in gl){var param=gl[parameter];if(!isNaN(parseInt(param))){var _res=get(param);if(_res!==null){parameters[parameter]=[_res,param]}}}if(ext){parameters["UNMASKED_VENDOR_WEBGL"]=[get(ext.UNMASKED_VENDOR_WEBGL),ext.UNMASKED_VENDOR_WEBGL];parameters["UNMASKED_RENDERER_WEBGL"]=[get(ext.UNMASKED_RENDERER_WEBGL),ext.UNMASKED_RENDERER_WEBGL]}return parameters}}async function get_voices(){if(window.speechSynthesis&&speechSynthesis.addEventListener){var res=[];var voices=await get_speech();voices.forEach(value=>{res.push(j(value))});return res}}async function get_keyboard(){if(navigator.keyboard){var res={};const layout=await navigator.keyboard.getLayoutMap();layout.forEach((key,value)=>{res[key]=value});return res}}function audio_context(){const audioCtx=new AudioContext;return j(audioCtx,4)}function get_video(){var video=document.createElement("video");if(video.canPlayType){var res={};videoTypes.forEach(v=>{res[v]=video.canPlayType(v)});return res}}async function get_webrtc_infos(){var res={video:j(RTCRtpReceiver.getCapabilities("video"),3),audio:j(RTCRtpReceiver.getCapabilities("audio"),3)};return res}async function get_webgpu_infos(){if(navigator.gpu){const adapter=await navigator.gpu.requestAdapter();var info={};if(adapter && adapter.requestAdapterInfo){info=await adapter.requestAdapterInfo()}var res={...j(adapter),...j(info)};return res}}async function get_media_devices(){if(navigator.mediaDevices){var res=await navigator.mediaDevices.enumerateDevices();return j(res)}}if(window.chrome){const iframe=document.createElement("iframe");iframe.src="about:blank";iframe.height="0";iframe.width="0";var promise=new Promise(resolve=>{iframe.addEventListener("load",()=>{resolve()})});document.body.appendChild(iframe);await promise;const data={appCodeName:navigator.appCodeName,appName:navigator.appName,appVersion:navigator.appVersion,cookieEnabled:navigator.cookieEnabled,deviceMemory:navigator.deviceMemory,doNotTrack:navigator.doNotTrack,hardwareConcurrency:navigator.hardwareConcurrency,language:navigator.language,languages:navigator.languages,maxTouchPoints:navigator.maxTouchPoints,pdfViewerEnabled:navigator.pdfViewerEnabled,platform:navigator.platform,product:navigator.product,productSub:navigator.productSub,userAgent:navigator.userAgent,vendor:navigator.vendor,vendorSub:navigator.vendorSub,webdiver:navigator.webdriver,devicePixelRatio:window.devicePixelRatio,innerWidth:window.innerWidth,innerHeight:window.innerHeight,outerWidth:window.outerHeight,outerHeight:window.outerHeight,screen:j(screen),connection:j(navigator.connection),plugins:j(navigator.plugins,3),userActivation:j(navigator.userActivation),"chrome.app":chrome.app?j(chrome.app):undefined,wow64:navigator.userAgent.indexOf("WOW64")>-1,HighEntropyValues:j(await navigator.userAgentData.getHighEntropyValues(["architecture","model","platformVersion","bitness","uaFullVersion","fullVersionList"]),3),darkmode:window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches,availabeFonts:await listFonts(),stack_native:get_stack(),timing_native:getTimingResolution(),permissions:await get_permissions(),navigator:get_obj_keys(navigator),window:get_obj_keys(iframe.contentWindow),document:get_obj_keys(iframe.contentWindow.document),documentElement:get_obj_keys(iframe.contentWindow.document.documentElement),speechSynthesis:await get_voices(),css:j(iframe.contentWindow.getComputedStyle(iframe.contentWindow.document.documentElement,"")),keyboard:await get_keyboard(),audioTypes:get_audio_types(),videoTypes:get_video(),audioContext:audio_context(),webrtc:await get_webrtc_infos(),webgpu:await get_webgpu_infos(),mediaDevices:await get_media_devices(),is_bot:undefined,status:"pass"};document.body.removeChild(iframe);if(check_worker){data["stack_worker"]=await get_worker_response(get_stack);data["timing_worker"]=await get_worker_response(getTimingResolution)}data["is_bot"]=await ensure_no_bot(check_worker);if(get_gl){const gl=document.createElement("canvas").getContext("webgl");const gl2=document.createElement("canvas").getContext("webgl2");const gl_experimental=document.createElement("canvas").getContext("experimental-webgl");data["gl"]=get_gl_infos(gl),data["gl2"]=get_gl_infos(gl2);data["gl_experimental"]=get_gl_infos(gl2)}if(globalThis.on_fp_result){globalThis.on_fp_result(data)}return data}else{return{status:"not chromium"}}}globalThis.fp_click_promise=new Promise((resolve,reject)=>{globalThis.fp_click_callback=resolve});module.exports=getFingerprint},{"./constants":1}],3:[function(require,module,exports){const collect=require("./fingerprint.js");window.getFingerprint=collect},{"./fingerprint.js":2}]},{},[3]); 2 | -------------------------------------------------------------------------------- /server/files/example_page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Example HTML 6 | 7 | 8 | 9 | 10 | 13 | 35 |

Welcome

36 |
Simple list
37 |
    38 |
  • alpha
  • 39 |
  • beta
  • 40 |
  • gamma
  • 41 |
  • delta
  • 42 |
43 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /server/files/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaliiiiiiiiii/driverless-fp-collector/00c983cb03cae0e44c0de686ef145ded370a198f/server/files/favicon.png -------------------------------------------------------------------------------- /server/files/iframe.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 |

driverless-fp-collector demo

10 | Source-Code 11 |
12 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /server/serve.py: -------------------------------------------------------------------------------- 1 | import json 2 | import uuid 3 | import faulthandler 4 | 5 | import bson 6 | import orjson 7 | from aiohttp import web 8 | from db import DataBase, _dir, logger 9 | import logging 10 | 11 | 12 | async def logger_middleware(request, handler): 13 | try: 14 | return await handler(request) 15 | except Exception as Argument: 16 | logger.exception("Error while handling request:") 17 | 18 | 19 | class Server: 20 | 21 | def __init__(self): 22 | pass 23 | 24 | # noinspection PyUnusedLocal 25 | async def _init(self, app): 26 | self._db = await DataBase().__aenter__() 27 | 28 | # noinspection PyUnusedLocal 29 | async def _cleanup(self, app): 30 | await self.db.__aexit__(None, None, None) 31 | 32 | def run(self): 33 | app = web.Application() 34 | app.add_routes([ 35 | web.get("/", self.root), 36 | web.get("/iframe.html", self.iframe), 37 | web.get("/favicon.ico", self.favicon), 38 | web.get("/example_page.html", self.example_page), 39 | web.get("/bundle.js", self.bundle), 40 | web.post('/api/v1/logger', self.api_log), 41 | web.get('/api/v1/compile', self.compile) 42 | ]) 43 | 44 | app.on_cleanup.append(self._cleanup) 45 | app.on_startup.append(self._init) 46 | web.run_app(app, host="0.0.0.0", port=80) 47 | 48 | async def root(self, request: web.BaseRequest): 49 | raise web.HTTPFound('example_page.html') 50 | 51 | @staticmethod 52 | async def bundle(request: web.BaseRequest): 53 | return web.FileResponse(f"{_dir}/files/bundle.js") 54 | 55 | @staticmethod 56 | async def example_page(request: web.BaseRequest): 57 | return web.FileResponse(f"{_dir}/files/example_page.html") 58 | 59 | @staticmethod 60 | async def iframe(request: web.BaseRequest): 61 | response = web.FileResponse(f"{_dir}/files/iframe.html") 62 | if not request.cookies.get("driverless-fp-collector"): 63 | response.set_cookie("driverless-fp-collector", uuid.uuid4().hex, samesite='Lax') 64 | return response 65 | 66 | @staticmethod 67 | async def favicon(request: web.BaseRequest): 68 | return web.FileResponse(f"{_dir}/files/favicon.png") 69 | 70 | async def api_log(self, request: web.BaseRequest): 71 | data = await request.read() 72 | if len(data) > 500_000: 73 | raise ValueError("Got more than 500_000 data, aborting") 74 | ip = request.remote 75 | cookie = request.cookies.get("driverless-fp-collector") 76 | await self.db.add_fp_entry(ip, cookie, data) 77 | return web.Response(text='OK') 78 | 79 | async def compile(self, request: web.BaseRequest): 80 | query = request.query.get("q", {}) 81 | if query: 82 | query = json.loads(query) 83 | if "_id" in query: 84 | del query["_id"] 85 | paths = await self.db.compile_paths(query) 86 | return web.Response(body=orjson.dumps(paths), content_type="application/json") 87 | 88 | 89 | @property 90 | def db(self) -> DataBase: 91 | return self._db 92 | 93 | 94 | if __name__ == "__main__": 95 | logger.setLevel(logging.DEBUG) 96 | faulthandler.enable() 97 | server = Server() 98 | server.run() 99 | -------------------------------------------------------------------------------- /src/fingerprint.js: -------------------------------------------------------------------------------- 1 | // https://github.com/kaliiiiiiiiii/driverless-fp-collector 2 | 3 | /* 4 | MIT License 5 | 6 | Copyright (c) 2023 Aurin Aegerter 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | */ 26 | 27 | const { fonts, permissions, audioTypes, videoTypes } = require("./constants"); 28 | 29 | // main function 30 | async function getFingerprint( 31 | get_gl = true, 32 | check_worker = true, 33 | ) { 34 | // utils 35 | function j(obj, max_depth = 2) { 36 | // to json 37 | if (max_depth === 0) { 38 | return undefined; 39 | } 40 | if ( 41 | obj && 42 | obj.constructor && 43 | typeof obj.constructor.length == "number" && 44 | obj.constructor.name.includes("Array") 45 | ) { 46 | var res = []; 47 | Object.values(obj).forEach((value) => { 48 | if (typeof value === "object") { 49 | value = j(value, max_depth - 1); 50 | } 51 | var _type = typeof value; 52 | if (!["function"].includes(_type)) { 53 | res.push(value); 54 | } 55 | }); 56 | return res; 57 | } else if (obj) { 58 | var res = {}; 59 | get_obj_keys(obj).forEach((key) => { 60 | var value = obj[key]; 61 | if (typeof value === "object") { 62 | value = j(value, max_depth - 1); 63 | } 64 | var _type = typeof value; 65 | if (obj !== undefined && !["function"].includes(_type)) { 66 | res[key] = value; 67 | } 68 | }); 69 | return res; 70 | } 71 | } 72 | 73 | function get_worker_response(fn) { 74 | try { 75 | const URL = window.URL || window.webkitURL; 76 | var fn = "self.onmessage=async function(e){postMessage(await (" + fn.toString() + ")())}"; 77 | var blob; 78 | try { 79 | blob = new Blob([fn], { type: "application/javascript" }); 80 | } catch (e) { 81 | // Backwards-compatibility 82 | window.BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder; 83 | blob = new BlobBuilder(); 84 | blob.append(response); 85 | blob = blob.getBlob(); 86 | } 87 | var url = URL.createObjectURL(blob); 88 | var worker = new Worker(url); 89 | var _promise = new Promise((resolve, reject) => { 90 | worker.onmessage = (m) => { 91 | worker.terminate(); 92 | resolve(m.data); 93 | }; 94 | }); 95 | worker.postMessage("call"); 96 | return _promise; 97 | } catch (e) { 98 | return new Promise((resolve, reject) => { 99 | reject(e); 100 | }); 101 | } 102 | } 103 | 104 | async function get_permission_state(permission) { 105 | try { 106 | var result = await navigator.permissions.query({ name: permission }); 107 | return result.state; 108 | } catch (e) {} 109 | } 110 | 111 | function get_obj_keys(obj) { 112 | var res = new Set(Object.getOwnPropertyNames(obj)); 113 | for (var prop in obj) { 114 | res.add(prop); 115 | } 116 | return [...res]; 117 | } 118 | 119 | function get_speech() { 120 | return new Promise(function (resolve, reject) { 121 | var speech = speechSynthesis.getVoices(); 122 | if (speech.length === 0) { 123 | setTimeout(() => { 124 | resolve([]); 125 | }, 2000); // in case voices are actually 0 and already have been loaded 126 | speechSynthesis.addEventListener("voiceschanged", () => { 127 | resolve(speechSynthesis.getVoices()); 128 | }); 129 | } else { 130 | resolve(speech); 131 | } 132 | }); 133 | } 134 | 135 | function checkAudioType(_type) { 136 | const audio = document.createElement("audio"); 137 | return audio.canPlayType(_type); 138 | } 139 | 140 | // functions 141 | function get_audio_types() { 142 | var res = {}; 143 | audioTypes.forEach((t) => { 144 | res[t] = checkAudioType(t); 145 | }); 146 | return res; 147 | } 148 | async function listFonts() { 149 | await document.fonts.ready; 150 | 151 | const fontAvailable = new Set(); 152 | 153 | for (const font of fonts.values()) { 154 | if (document.fonts.check(`12px "${font}"`)) fontAvailable.add(font); 155 | } 156 | return [...fontAvailable.values()]; 157 | } 158 | 159 | async function get_permissions() { 160 | var res = {}; 161 | permissions.forEach(async function (permission) { 162 | res[permission] = await get_permission_state(permission); 163 | }); 164 | return res; 165 | } 166 | 167 | function get_stack() { 168 | var sizeA = 0; 169 | var sizeB = 0; 170 | var counter = 0; 171 | try { 172 | var fn_1 = function () { 173 | counter += 1; 174 | fn_1(); 175 | }; 176 | fn_1(); 177 | } catch (_a) { 178 | sizeA = counter; 179 | try { 180 | counter = 0; 181 | var fn_2 = function () { 182 | var local = 1; 183 | counter += local; 184 | fn_2(); 185 | }; 186 | fn_2(); 187 | } catch (_b) { 188 | sizeB = counter; 189 | } 190 | } 191 | var bytes = (sizeB * 8) / (sizeA - sizeB); 192 | return [sizeA, sizeB, bytes]; 193 | } 194 | 195 | function getTimingResolution() { 196 | var runs = 5000; 197 | var valA = 1; 198 | var valB = 1; 199 | var res; 200 | for (var i = 0; i < runs; i++) { 201 | var a = performance.now(); 202 | var b = performance.now(); 203 | if (a < b) { 204 | res = b - a; 205 | if (res > valA && res < valB) { 206 | valB = res; 207 | } else if (res < valA) { 208 | valB = valA; 209 | valA = res; 210 | } 211 | } 212 | } 213 | return valA; 214 | } 215 | 216 | async function ensure_no_bot(check_worker, click_promise) { 217 | e = await globalThis.fp_click_promise 218 | globalThis.fp_click_promise = new Promise((resolve, reject)=>{globalThis.fp_click_callback = resolve}) 219 | var is_touch = false; 220 | if (e.type == "touchstart") { 221 | is_touch = true; 222 | e = e.touches[0] || e.changedTouches[0]; 223 | } 224 | var is_bot = e.pageY == e.screenY && e.pageX == e.screenX; 225 | if (is_bot && 1 >= outerHeight - innerHeight) { 226 | // fullscreen 227 | is_bot = false; 228 | } 229 | if (is_bot && is_touch && navigator.userAgentData.mobile) { 230 | is_bot = "maybe"; // mobile touch can have e.pageY == e.screenY && e.pageX == e.screenX 231 | } 232 | if (is_touch == false && navigator.userAgentData.mobile === true) { 233 | is_bot = "maybe"; // mouse on mobile is suspicious 234 | } 235 | if (e.isTrusted === false) { 236 | is_bot = true; 237 | } 238 | if (check_worker) { 239 | worker_ua = await get_worker_response(function(){return navigator.userAgent}); 240 | if (worker_ua !== navigator.userAgent) { 241 | is_bot = true; 242 | } 243 | }; 244 | return is_bot 245 | } 246 | 247 | function get_gl_infos(gl) { 248 | if (gl) { 249 | const get = gl.getParameter.bind(gl); 250 | const ext = gl.getExtension("WEBGL_debug_renderer_info"); 251 | const parameters = {}; 252 | 253 | for (const parameter in gl) { 254 | var param = gl[parameter]; 255 | if (!isNaN(parseInt(param))) { 256 | var _res = get(param); 257 | if (_res !== null) { 258 | parameters[parameter] = [_res, param]; 259 | } 260 | } 261 | } 262 | 263 | if (ext) { 264 | parameters["UNMASKED_VENDOR_WEBGL"] = [get(ext.UNMASKED_VENDOR_WEBGL), ext.UNMASKED_VENDOR_WEBGL]; 265 | parameters["UNMASKED_RENDERER_WEBGL"] = [get(ext.UNMASKED_RENDERER_WEBGL), ext.UNMASKED_RENDERER_WEBGL]; 266 | } 267 | 268 | return parameters; 269 | } 270 | } 271 | 272 | async function get_voices() { 273 | if (window.speechSynthesis && speechSynthesis.addEventListener) { 274 | var res = []; 275 | var voices = await get_speech(); 276 | voices.forEach((value) => { 277 | res.push(j(value)); 278 | }); 279 | return res; 280 | } 281 | } 282 | 283 | async function get_keyboard() { 284 | if (navigator.keyboard) { 285 | var res = {}; 286 | const layout = await navigator.keyboard.getLayoutMap(); 287 | layout.forEach((key, value) => { 288 | res[key] = value; 289 | }); 290 | return res; 291 | } 292 | } 293 | function audio_context() { 294 | const audioCtx = new AudioContext(); 295 | return j(audioCtx, 4); 296 | } 297 | 298 | function get_video() { 299 | var video = document.createElement("video"); 300 | if (video.canPlayType) { 301 | var res = {}; 302 | videoTypes.forEach((v) => { 303 | res[v] = video.canPlayType(v); 304 | }); 305 | return res; 306 | } 307 | } 308 | 309 | async function get_webrtc_infos() { 310 | var res = { 311 | video: j(RTCRtpReceiver.getCapabilities("video"), 3), 312 | audio: j(RTCRtpReceiver.getCapabilities("audio"), 3), 313 | }; 314 | return res; 315 | } 316 | async function get_webgpu_infos() { 317 | if (navigator.gpu) { 318 | const adapter = await navigator.gpu.requestAdapter(); 319 | var info = {}; 320 | if (adapter && adapter.requestAdapterInfo) { 321 | info = await adapter.requestAdapterInfo() 322 | } else { 323 | info = { error: 'deprecated' }; 324 | } 325 | var res = { ...j(adapter), ...j(info) }; 326 | return res; 327 | } 328 | } 329 | 330 | async function get_media_devices() { 331 | if (navigator.mediaDevices) { 332 | var res = await navigator.mediaDevices.enumerateDevices(); 333 | return j(res); 334 | } 335 | } 336 | 337 | if (window.chrome) { 338 | const iframe = document.createElement("iframe"); 339 | iframe.src = "about:blank"; 340 | iframe.height = "0"; 341 | iframe.width = "0"; 342 | var promise = new Promise((resolve) => { 343 | iframe.addEventListener("load", () => { 344 | resolve(); 345 | }); 346 | }); 347 | document.body.appendChild(iframe); 348 | await promise; 349 | 350 | const data = { 351 | // navigator 352 | "appCodeName": navigator.appCodeName, 353 | "appName": navigator.appName, 354 | "appVersion": navigator.appVersion, 355 | "cookieEnabled": navigator.cookieEnabled, 356 | "deviceMemory": navigator.deviceMemory, 357 | "doNotTrack": navigator.doNotTrack, 358 | "hardwareConcurrency": navigator.hardwareConcurrency, 359 | "language": navigator.language, 360 | "languages": navigator.languages, 361 | "maxTouchPoints": navigator.maxTouchPoints, 362 | "pdfViewerEnabled": navigator.pdfViewerEnabled, 363 | "platform": navigator.platform, 364 | "product": navigator.product, 365 | "productSub": navigator.productSub, 366 | "userAgent": navigator.userAgent, 367 | "vendor": navigator.vendor, 368 | "vendorSub": navigator.vendorSub, 369 | "webdiver": navigator.webdriver, 370 | "devicePixelRatio": window.devicePixelRatio, 371 | "innerWidth": window.innerWidth, 372 | "innerHeight": window.innerHeight, 373 | "outerWidth": window.outerHeight, 374 | "outerHeight": window.outerHeight, 375 | // jsonified 376 | "screen": j(screen), 377 | "connection": j(navigator.connection), 378 | "plugins": j(navigator.plugins, 3), 379 | "userActivation": j(navigator.userActivation), 380 | "chrome.app": chrome.app ? j(chrome.app) : undefined, 381 | // processed 382 | "wow64": navigator.userAgent.indexOf("WOW64") > -1, 383 | "HighEntropyValues": j( 384 | await navigator.userAgentData.getHighEntropyValues([ 385 | "architecture", 386 | "model", 387 | "platformVersion", 388 | "bitness", 389 | "uaFullVersion", 390 | "fullVersionList", 391 | ]), 392 | 3, 393 | ), 394 | "darkmode": window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches, 395 | "availabeFonts": await listFonts(), 396 | "stack_native": get_stack(), 397 | "timing_native": getTimingResolution(), 398 | "permissions": await get_permissions(), 399 | "navigator": get_obj_keys(navigator), 400 | "window": get_obj_keys(iframe.contentWindow), 401 | "document": get_obj_keys(iframe.contentWindow.document), 402 | "documentElement": get_obj_keys(iframe.contentWindow.document.documentElement), 403 | "speechSynthesis": await get_voices(), 404 | "css": j(iframe.contentWindow.getComputedStyle(iframe.contentWindow.document.documentElement, "")), 405 | "keyboard": await get_keyboard(), 406 | "audioTypes": get_audio_types(), 407 | "videoTypes": get_video(), 408 | "audioContext": audio_context(), 409 | "webrtc": await get_webrtc_infos(), 410 | "webgpu": await get_webgpu_infos(), 411 | "mediaDevices": await get_media_devices(), 412 | "is_bot": undefined, 413 | "status": "pass", 414 | }; 415 | document.body.removeChild(iframe); 416 | if (check_worker) { 417 | data["stack_worker"] = await get_worker_response(get_stack); 418 | data["timing_worker"] = await get_worker_response(getTimingResolution); 419 | } 420 | data["is_bot"] = await ensure_no_bot(check_worker); 421 | if (get_gl) { 422 | const gl = document.createElement("canvas").getContext("webgl"); 423 | const gl2 = document.createElement("canvas").getContext("webgl2"); 424 | const gl_experimental = document.createElement("canvas").getContext("experimental-webgl"); 425 | (data["gl"] = get_gl_infos(gl)), (data["gl2"] = get_gl_infos(gl2)); 426 | data["gl_experimental"] = get_gl_infos(gl2); 427 | } 428 | if(globalThis.on_fp_result){globalThis.on_fp_result(data)} 429 | return data; 430 | } else { 431 | return { status: "not chromium" }; 432 | } 433 | } 434 | 435 | globalThis.fp_click_promise = new Promise((resolve, reject)=>{globalThis.fp_click_callback = resolve}) 436 | 437 | module.exports = getFingerprint; 438 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const collect = require("./fingerprint.js"); 2 | 3 | window.getFingerprint = collect; 4 | --------------------------------------------------------------------------------