├── .gitignore ├── server.js ├── README.md ├── package.json ├── LICENSE ├── style.css ├── index.html └── fpworker.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | .vscode -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const path = require('path') 3 | const staticPath = path.join(__dirname, '/') 4 | const app = express() 5 | 6 | app.use(express.static(staticPath)) 7 | 8 | app.listen(8000, () => console.log('⚡')) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fpworker 2 | 3 | https://abrahamjuliot.github.io/fpworker/ 4 | 5 | 1 Fingerprinting script, 4 scopes 6 | - `Window` 7 | - `DedicatedWorkerGlobalScope` 8 | - `SharedWorkerGlobalScope` 9 | - `ServiceWorkerGlobalScope` 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fpworker", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "fpworker.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node server.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/abrahamjuliot/fpworker.git" 13 | }, 14 | "author": "", 15 | "license": "ISC", 16 | "bugs": { 17 | "url": "https://github.com/abrahamjuliot/fpworker/issues" 18 | }, 19 | "homepage": "https://github.com/abrahamjuliot/fpworker#readme", 20 | "dependencies": { 21 | "express": "^4.17.1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Abraham 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 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | /* Reset */ 2 | html, body, div, span, applet, object, iframe, 3 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 4 | a, abbr, acronym, address, big, cite, code, 5 | del, dfn, em, img, ins, kbd, q, s, samp, 6 | small, strike, strong, sub, sup, tt, var, 7 | b, u, i, center, 8 | dl, dt, dd, ol, ul, li, 9 | fieldset, form, label, legend, 10 | table, caption, tbody, tfoot, thead, tr, th, td, 11 | article, aside, canvas, details, embed, 12 | figure, figcaption, footer, header, hgroup, 13 | menu, nav, output, ruby, section, summary, 14 | time, mark, audio, video { 15 | margin: 0; 16 | padding: 0; 17 | border: 0; 18 | font-size: 100% !important; 19 | font: inherit; 20 | vertical-align: baseline; 21 | } 22 | /* HTML5 display-role reset for older browsers */ 23 | article, aside, details, figcaption, figure, 24 | footer, header, hgroup, menu, nav, section { 25 | display: block; 26 | } 27 | body { 28 | line-height: 1; 29 | } 30 | ol, ul { 31 | list-style: none; 32 | } 33 | blockquote, q { 34 | quotes: none; 35 | } 36 | blockquote:before, blockquote:after, 37 | q:before, q:after { 38 | content: ''; 39 | content: none; 40 | } 41 | table { 42 | border-collapse: collapse; 43 | border-spacing: 0; 44 | } 45 | 46 | /* base */ 47 | body { 48 | font-family: monospace; 49 | line-height: 1.2; 50 | } 51 | *, *::before, *::after { 52 | box-sizing: border-box; 53 | } 54 | 55 | h1, h2, h3, h4, h5, h6 { 56 | font-family: sans-serif; 57 | font-weight: bold; 58 | line-height: 1.5; 59 | } 60 | h1 { 61 | font-size: 32px !important; 62 | } 63 | h2 { 64 | font-size: 16px !important; 65 | } 66 | h3 { 67 | font-size: 14px !important; 68 | } 69 | 70 | 71 | /* grid */ 72 | .grid { 73 | display: grid; 74 | grid-template-columns: repeat(1fr, 1fr); 75 | gap: 0; 76 | padding: 20px 0; 77 | grid-auto-rows: minmax(100px, auto); 78 | } 79 | .header, 80 | .window-scope, 81 | .dedicated-worker, 82 | .service-worker, 83 | .shared-worker { 84 | padding: 20px; 85 | } 86 | 87 | .header h1 { 88 | color: #6a3fff; 89 | } 90 | .header .perf { 91 | color: #bf3ab6; 92 | } 93 | 94 | .window-scope { 95 | background: #6a3fff; 96 | color: #fff; 97 | } 98 | .dedicated-worker { 99 | background: #d7fffb; 100 | color: #8a3dfb 101 | } 102 | .service-worker { 103 | background: #bf3ab6; 104 | color: #fff; 105 | } 106 | .shared-worker { 107 | background: #e8d6ff; 108 | color: #7b1c82 109 | } 110 | 111 | .card { 112 | box-shadow: 0 6px 15px rgba(36, 37, 38, 0.17); 113 | padding: 10px; 114 | margin: 5px auto; 115 | border-radius: 3px; 116 | } 117 | .mismatch { 118 | background: #fff; 119 | } 120 | .service-worker .mismatch { 121 | color: #bf3ab6; 122 | } 123 | 124 | @media (min-width: 650px) { 125 | h1 { 126 | font-size: 40px !important; 127 | } 128 | h2 { 129 | font-size: 20px !important; 130 | } 131 | h3 { 132 | font-size: 16px !important; 133 | } 134 | .grid { 135 | grid-template-columns: repeat(2, 1fr); 136 | gap: 20px; 137 | padding: 20px; 138 | } 139 | .header, 140 | .window-scope, 141 | .dedicated-worker, 142 | .service-worker, 143 | .shared-worker { 144 | padding: 10px; 145 | } 146 | .header, 147 | .window-scope { 148 | grid-column: 1; 149 | } 150 | .dedicated-worker, 151 | .service-worker, 152 | .shared-worker { 153 | grid-column: 2; 154 | } 155 | .header { 156 | grid-row: 1; 157 | } 158 | .window-scope { 159 | grid-row: 2 / 7; 160 | } 161 | .dedicated-worker { 162 | grid-row: 1 / span 2; 163 | } 164 | .service-worker { 165 | grid-row: 3 / span 2; 166 | } 167 | .shared-worker { 168 | grid-row: 5 / span 2; 169 | } 170 | } 171 | 172 | @media (min-width: 900px) { 173 | .grid { 174 | grid-template-columns: repeat(3, 1fr); 175 | } 176 | .header, 177 | .window-scope { 178 | grid-column: 1; 179 | } 180 | .dedicated-worker, 181 | .service-worker, 182 | .shared-worker { 183 | grid-column: 2 / span 2; 184 | } 185 | } 186 | 187 | @media (min-width: 1200px) { 188 | .dedicated-worker, 189 | .service-worker { 190 | grid-row: 1 / span 4; 191 | } 192 | .dedicated-worker { 193 | grid-column: 2; 194 | } 195 | .service-worker { 196 | grid-column: 3; 197 | } 198 | .shared-worker { 199 | grid-column: 2 / span 2; 200 | } 201 | } 202 | 203 | 204 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | fpworker 9 | 10 | 11 |
12 | 13 | 141 | 142 | 143 | -------------------------------------------------------------------------------- /fpworker.js: -------------------------------------------------------------------------------- 1 | const fpworker = (async () => { 2 | // Compute all scopes 3 | const ask = fn => { try { return fn() } catch (e) { return } } 4 | const getFingerprint = async () => { 5 | const getGPU = (canvas1, canvas2) => { 6 | const getRenderer = gl => gl.getParameter( 7 | gl.getExtension('WEBGL_debug_renderer_info').UNMASKED_RENDERER_WEBGL 8 | ) 9 | const gpuSet = new Set([ 10 | ask(() => getRenderer(canvas1.getContext('webgl'))), 11 | ask(() => getRenderer(canvas2.getContext('webgl2'))) 12 | ]) 13 | gpuSet.delete() // discard undefined 14 | // find 1st trusted if size > 1 15 | // need to discard unknown gpus 16 | return [...gpuSet] 17 | } 18 | 19 | const getCanvasData = async ({ canvas, ctx, width = 186, height = 30 }) => { 20 | if (!canvas || !ctx) return 21 | const getData = async blob => { 22 | if (!blob) return 23 | const getRead = (method, blob) => new Promise(resolve => { 24 | const reader = new FileReader() 25 | reader[method](blob) 26 | return reader.addEventListener('loadend', () => resolve(reader.result)) 27 | }) 28 | const [ 29 | canvasReadAsArrayBuffer, canvasReadAsBinaryString, canvasReadAsDataURL, canvasReadAsText 30 | ] = await Promise.all([ 31 | getRead('readAsArrayBuffer', blob), 32 | getRead('readAsBinaryString', blob), 33 | getRead('readAsDataURL', blob), 34 | getRead('readAsText', blob), 35 | ]) 36 | return { 37 | canvasReadAsArrayBuffer: String.fromCharCode.apply(null, new Uint8Array(canvasReadAsArrayBuffer)), 38 | canvasReadAsBinaryString, 39 | canvasReadAsDataURL, 40 | canvasReadAsText 41 | } 42 | } 43 | canvas.width = width 44 | canvas.height = height 45 | ctx.font = '14px Arial' 46 | ctx.fillText(`😃🙌🧠🦄🐉🌊🍧🏄‍♀️🌠🔮`, 0, 20) 47 | ctx.fillStyle = 'rgba(0, 0, 0, 0)' 48 | ctx.fillRect(0, 0, width-50, height) 49 | if (canvas.constructor.name === 'OffscreenCanvas') { 50 | return getData(await canvas.convertToBlob()) 51 | } 52 | return new Promise(resolve => { 53 | return canvas.toBlob(async blob => resolve(getData(blob))) 54 | }) 55 | } 56 | 57 | const getEmojis = ctx => { 58 | if (!ctx) return 59 | const emojis = [ 60 | [128512],[9786],[129333, 8205, 9794, 65039],[9832],[9784],[9895],[8265],[8505],[127987, 65039, 8205, 9895, 65039],[129394],[9785],[9760],[129489, 8205, 129456],[129487, 8205, 9794, 65039],[9975],[129489, 8205, 129309, 8205, 129489],[9752],[9968],[9961],[9972],[9992],[9201],[9928],[9730],[9969],[9731],[9732],[9976],[9823],[9937],[9000],[9993],[9999],[10002],[9986],[9935],[9874],[9876],[9881],[9939],[9879],[9904],[9905],[9888],[9762],[9763],[11014],[8599],[10145],[11013],[9883],[10017],[10013],[9766],[9654],[9197],[9199],[9167],[9792],[9794],[10006],[12336],[9877],[9884],[10004],[10035],[10055],[9724],[9642],[10083],[10084],[9996],[9757],[9997],[10052],[9878],[8618],[9775],[9770],[9774],[9745],[10036],[127344],[127359] 61 | ].map(emojiCode => String.fromCodePoint(...emojiCode)) 62 | const getSum = textMetrics => ( 63 | +(textMetrics.actualBoundingBoxAscent||0) 64 | +(textMetrics.actualBoundingBoxDescent||0) 65 | +(textMetrics.actualBoundingBoxLeft||0) 66 | +(textMetrics.actualBoundingBoxRight||0) 67 | +(textMetrics.fontBoundingBoxAscent||0) 68 | +(textMetrics.fontBoundingBoxDescent||0) 69 | +(textMetrics.width||0) 70 | ) 71 | const emojiSumSet = new Set() 72 | const emojiSet = new Set() 73 | ask(() => emojis.forEach(emoji => { 74 | const sum = getSum(ctx.measureText(emoji)) 75 | if (!emojiSumSet.has(sum)) { 76 | emojiSumSet.add(sum) 77 | return emojiSet.add(emoji) 78 | } 79 | return 80 | })) 81 | return { 82 | emojiUnique: [...emojiSet].join('') 83 | } 84 | } 85 | 86 | const getSystemFontLists = () => ({ 87 | windowsFonts: { 88 | // https://docs.microsoft.com/en-us/typography/fonts/windows_11_font_list 89 | '7': [ 90 | 'Cambria Math', 91 | 'Lucida Console' 92 | ], 93 | '8': [ 94 | 'Aldhabi', 95 | 'Gadugi', 96 | 'Myanmar Text', 97 | 'Nirmala UI' 98 | ], 99 | '8.1': [ 100 | 'Leelawadee UI', 101 | 'Javanese Text', 102 | 'Segoe UI Emoji' 103 | ], 104 | '10': [ 105 | 'HoloLens MDL2 Assets', // 10 (v1507) + 106 | 'Segoe MDL2 Assets', // 10 (v1507) + 107 | 'Bahnschrift', // 10 (v1709) +- 108 | 'Ink Free', // 10 (v1803) +- 109 | ], 110 | '11': ['Segoe Fluent Icons'] 111 | }, 112 | appleFonts: ['Helvetica Neue'], 113 | linuxFonts: [ 114 | 'Arimo', // ubuntu, chrome os 115 | 'Jomolhari', // chrome os 116 | 'Ubuntu' // ubuntu 117 | ], 118 | miscFonts: [ 119 | 'Dancing Script', // android 120 | 'Droid Sans Mono', // android 121 | 'Roboto' // android, chrome OS 122 | ] 123 | }) 124 | 125 | const detectFonts = ctx => { 126 | if (!ctx) return 127 | const { windowsFonts, appleFonts, linuxFonts, miscFonts } = getSystemFontLists() 128 | const fontList = [ 129 | ...Object.keys(windowsFonts).map(key => windowsFonts[key]).flat(), 130 | ...appleFonts, 131 | ...linuxFonts, 132 | ...miscFonts 133 | ] 134 | const getTextMetrics = (ctx, font) => { 135 | ctx.font = `256px ${font}` 136 | return ctx.measureText('mmmmmmmmmmlli') 137 | } 138 | const baseFonts = ['monospace', 'sans-serif', 'serif'] 139 | const base = baseFonts.reduce((acc, font) => { 140 | acc[font] = getTextMetrics(ctx, font) 141 | return acc 142 | }, {}) 143 | const families = fontList.reduce((acc, font) => { 144 | baseFonts.forEach(baseFont => acc.push(`'${font}', ${baseFont}`)) 145 | return acc 146 | }, []) 147 | const detectedFonts = families.reduce((acc, family) => { 148 | const basefont = /, (.+)/.exec(family)[1] 149 | const dimensions = getTextMetrics(ctx, family) 150 | const font = /\'(.+)\'/.exec(family)[1] 151 | const detected = dimensions.width != base[basefont].width 152 | return !isNaN(dimensions.width) && detected ? acc.add(font) : acc 153 | }, new Set()) 154 | return { fontsDetected: [...detectedFonts].sort() } 155 | } 156 | 157 | const getFonts = () => ask(() => { 158 | const { windowsFonts, appleFonts, linuxFonts, miscFonts } = getSystemFontLists() 159 | const fontList = [ 160 | ...Object.keys(windowsFonts).map(key => windowsFonts[key]).flat(), 161 | ...appleFonts, 162 | ...linuxFonts, 163 | ...miscFonts 164 | ] 165 | 166 | const fontFaceSet = globalThis.document ? document.fonts : fonts 167 | const getRandomValues = n => [...crypto.getRandomValues(new Uint32Array(n))] 168 | .map(n => n.toString(36)).join('') 169 | if (fontFaceSet.check(`0 '${getRandomValues(1)}'`)) return 170 | fontFaceSet.clear() 171 | const fontsChecked = fontList.filter(font => fontFaceSet.check(`0 '${font}'`)) 172 | return { fontsChecked: fontsChecked.sort() } 173 | }) 174 | 175 | const getFontSystem = ({ supportedFonts, windowsFonts, appleFonts, linuxFonts, miscFonts }) => { 176 | const getWindowsVersion = (windowsFonts, fonts) => { 177 | const fontVersion = { 178 | ['11']: windowsFonts['11'].find(x => fonts.includes(x)), 179 | ['10']: windowsFonts['10'].find(x => fonts.includes(x)), 180 | ['8.1']: windowsFonts['8.1'].find(x => fonts.includes(x)), 181 | ['8']: windowsFonts['8'].find(x => fonts.includes(x)), 182 | // require complete set of Windows 7 fonts 183 | ['7']: windowsFonts['7'].filter(x => fonts.includes(x)).length == windowsFonts['7'].length 184 | } 185 | const hash = ( 186 | ''+Object.keys(fontVersion).sort().filter(key => !!fontVersion[key]) 187 | ) 188 | const hashMap = { 189 | '10,11,7,8,8.1': '11', 190 | '10,7,8,8.1': '10', 191 | '7,8,8.1': '8.1', 192 | '11,7,8,8.1': '8.1', // missing 10 193 | '7,8': '8', 194 | '10,7,8': '8', // missing 8.1 195 | '10,11,7,8': '8', // missing 8.1 196 | '7': '7', 197 | '7,8.1': '7', 198 | '10,7,8.1': '7', // missing 8 199 | '10,11,7,8.1': '7', // missing 8 200 | } 201 | const version = hashMap[hash] 202 | return version ? `Windows ${version}` : undefined 203 | } 204 | const systemHashMap = { 205 | 'Arimo,Jomolhari,Roboto': 'Chrome OS', 206 | 'Arimo,Ubuntu': 'Ubuntu', 207 | 'Dancing Script,Droid Sans Mono,Roboto': 'Android' 208 | } 209 | const hasAppleFonts = supportedFonts.find(x => appleFonts.includes(x)) 210 | const hasLinuxFonts = supportedFonts.find(x => linuxFonts.includes(x)) 211 | const windowsFontSystem = getWindowsVersion(windowsFonts, supportedFonts) 212 | const fontSystem = ( 213 | windowsFontSystem || ( 214 | hasLinuxFonts ? (systemHashMap[''+supportedFonts] || 'Linux') : 215 | hasAppleFonts ? 'Apple' : 216 | (systemHashMap[''+supportedFonts] || 'unknown') 217 | ) 218 | ) 219 | return fontSystem 220 | } 221 | 222 | const loadFonts = () => ask(async () => { 223 | if (!globalThis.FontFace) return 224 | const { windowsFonts, appleFonts, linuxFonts, miscFonts } = getSystemFontLists() 225 | const fontList = [ 226 | ...Object.keys(windowsFonts).map(key => windowsFonts[key]).flat(), 227 | ...appleFonts, 228 | ...linuxFonts, 229 | ...miscFonts 230 | ] 231 | const fontFaceList = fontList.map(font => new FontFace(font, `local("${font}")`)) 232 | const responseCollection = await Promise.allSettled(fontFaceList.map(font => font.load())) 233 | const fontsLoaded = responseCollection.reduce((acc, font) => { 234 | return font.status == 'fulfilled' ? [...acc, font.value.family] : acc 235 | }, []) 236 | return { 237 | fontsLoaded: fontsLoaded.sort(), 238 | fontSystem: getFontSystem({ 239 | supportedFonts: fontsLoaded, 240 | windowsFonts, 241 | appleFonts, 242 | linuxFonts, 243 | miscFonts 244 | }) 245 | } 246 | }) 247 | 248 | const getUserAgentData = () => { 249 | if (!navigator.userAgentData) return 250 | return navigator.userAgentData.getHighEntropyValues([ 251 | 'platform', 252 | 'platformVersion', 253 | 'architecture', 254 | 'bitness', 255 | 'model', 256 | 'uaFullVersion' 257 | ]) 258 | } 259 | 260 | const canvas2d = ( 261 | ask(() => new OffscreenCanvas(186, 30)) || 262 | ask(() => document.createElement('canvas')) 263 | ) 264 | const ctx2d = ask(() => canvas2d.getContext('2d')) 265 | const canvasGl = ( 266 | ask(() => new OffscreenCanvas(30, 30)) || 267 | ask(() => document.createElement('canvas')) 268 | ) 269 | const canvasGl2 = ( 270 | ask(() => new OffscreenCanvas(30, 30)) || 271 | ask(() => document.createElement('canvas')) 272 | ) 273 | 274 | const [ 275 | canvasData, 276 | userAgentData, 277 | loadedFonts 278 | ] = await Promise.all([ 279 | getCanvasData({ canvas: canvas2d, ctx: ctx2d }), 280 | getUserAgentData(), 281 | loadFonts() 282 | ]).catch(error => console.error(error)) 283 | 284 | const getEngine = () => { 285 | const hashMap = { 286 | '1.9275814160560204e-50': 'Blink', 287 | '1.9275814160560185e-50': 'Gecko', 288 | '1.9275814160560206e-50': 'WebKit' 289 | } 290 | const mathPI = 3.141592653589793 291 | return hashMap[mathPI ** -100] || 'unknown' 292 | } 293 | 294 | const { 295 | architecture: uaArchitecture, 296 | model: uaModel, 297 | platform: uaPlatform, 298 | platformVersion: uaPlatformVersion, 299 | uaFullVersion 300 | } = userAgentData || {} 301 | 302 | return { 303 | // Blink 304 | ...canvasData, 305 | ...getEmojis(ctx2d), 306 | ...getEmojis(), 307 | ...getFonts(), 308 | ...loadedFonts, 309 | ...detectFonts(ctx2d), 310 | uaArchitecture, 311 | uaModel, 312 | uaPlatform, 313 | uaPlatformVersion, 314 | uaFullVersion, 315 | gpu: getGPU(canvasGl, canvasGl2), 316 | deviceMemory: navigator.deviceMemory, 317 | // Blink/Gecko 318 | hardwareConcurrency: navigator.hardwareConcurrency, 319 | // Blink/Gecko/WebKit 320 | timeZone: ask(() => Intl.DateTimeFormat().resolvedOptions().timeZone), 321 | language: navigator.language, 322 | languages: ''+navigator.languages, 323 | userAgent: navigator.userAgent, 324 | platform: navigator.platform, 325 | engine: getEngine() 326 | } 327 | } 328 | 329 | // Compute and communicate from worker scopes 330 | const onEvent = (eventType, fn) => addEventListener(eventType, fn) 331 | const send = async source => source.postMessage(await getFingerprint()) 332 | if (!globalThis.document && globalThis.WorkerGlobalScope) return ( 333 | globalThis.ServiceWorkerGlobalScope ? onEvent('message', async e => send(e.source)) : 334 | globalThis.SharedWorkerGlobalScope ? onEvent('connect', async e => send(e.ports[0])) : 335 | await send(self) // DedicatedWorkerGlobalScope 336 | ) 337 | 338 | // Compute and communicate from window scope 339 | const resolveWorkerData = (target, resolve, fn) => target.addEventListener('message', event => { 340 | fn(); return resolve(event.data) 341 | }) 342 | const getDedicatedWorker = ({ scriptSource }) => new Promise(resolve => { 343 | const dedicatedWorker = ask(() => new Worker(scriptSource)) 344 | if (!dedicatedWorker) return resolve() 345 | return resolveWorkerData(dedicatedWorker, resolve, () => dedicatedWorker.terminate()) 346 | }) 347 | 348 | const getSharedWorker = ({ scriptSource }) => new Promise(resolve => { 349 | const sharedWorker = ask(() => new SharedWorker(scriptSource)) 350 | if (!sharedWorker) return resolve() 351 | sharedWorker.port.start() 352 | return resolveWorkerData(sharedWorker.port, resolve, () => sharedWorker.port.close()) 353 | }) 354 | 355 | const getServiceWorker = ({ scriptSource, scope }) => new Promise(async resolve => { 356 | const registration = await ask(() => navigator.serviceWorker.register(scriptSource, { scope }).catch(e => {})) 357 | if (!registration) return resolve() 358 | return navigator.serviceWorker.ready.then(registration => { 359 | registration.active.postMessage(undefined) 360 | return resolveWorkerData(navigator.serviceWorker, resolve, () => registration.unregister()) 361 | }) 362 | }) 363 | 364 | const scriptSource = './fpworker.js' 365 | console.log(location.pathname) 366 | const start = performance.now() 367 | const [ windowScope, dedicatedWorker, sharedWorker, serviceWorker ] = await Promise.all([ 368 | getFingerprint(), 369 | getDedicatedWorker({ scriptSource }), 370 | getSharedWorker({ scriptSource }), 371 | getServiceWorker({ scriptSource, scope: location.pathname }) 372 | ]).catch(error => console.error(error.message)) 373 | 374 | const data = { 375 | perf: (performance.now() - start).toFixed(2), 376 | windowScope, 377 | dedicatedWorker, 378 | sharedWorker, 379 | serviceWorker 380 | } 381 | return data 382 | })() 383 | 384 | 385 | /* 386 | await new FontFace('ZWAdobeF', `local("ZWAdobeF")`).load().catch(e => {}) 387 | 388 | engine version estimate 389 | textMetrics fonts 390 | 391 | matchmedia 392 | timezone 393 | 394 | more textMetrics 395 | more fonts 396 | canvas pixels? 397 | permissions 398 | */ 399 | --------------------------------------------------------------------------------