├── .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 |
--------------------------------------------------------------------------------