├── .gitattributes ├── .gitignore ├── README.md ├── cmd └── fingerprint │ └── main.go ├── dist ├── script.min.wasm ├── script.wasm └── script.wat ├── go.mod ├── go.work ├── internal ├── canvas │ ├── canvasfp.go │ └── picasso.go ├── crypto │ ├── hex.go │ └── murmur.go ├── protobuf │ ├── proto.go │ └── proto_test.go ├── types │ └── default.go └── webgl │ ├── engine.go │ ├── fingerprint.go │ └── params.go ├── scripts └── Makefile └── test ├── serve.go └── static ├── index.html └── js ├── gzip.min.js └── init.min.js /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wasm Web Fingerprinting library 2 | Js/Wasm Obfuscated fingerprinting, bot detection & API protection library 3 | 4 | state: `Pre-alpha` 5 | 6 | ### To implement: 7 | - [x] Canvas Fp 8 | - [x] Implement ProtoBuf protocol for communication 9 | - [ ] If not switching to another languge/compiling method, optimise wasm loading and glue code 10 | - [ ] Advanced Canvas Fp 11 | - [x] WebGl Fp & Params Fp 12 | - [ ] Screen Fp / Browser properties 13 | - [ ] Audio Fp 14 | - [ ] Css / Js and other fp techniques 15 | - [ ] Bot / Automation detection 16 | - [ ] Use mouse movements & bezier 17 | - [ ] Tls and Ja3 Fingerprinting 18 | - [ ] Make a Browser fp (finegrained) and Device fp (large grained, targets device) 19 | - [ ] Implement Obfuscation (although wasm is a first step) and Encryption 20 | - [ ] Implement an api that gets the fingerprint and processes data 21 | - [ ] Implement all fp's natively (without go) to increase speed (rust ?) 22 | - [ ] Train a model on recognising bad fp's 23 | - [ ] Implement techniques to make fp as authentic as possible and difficult to fake 24 | 25 | ### Compiling golang to wasm 26 | navigate to `./scripts` and run: 27 | ```sh 28 | make 29 | ``` 30 | 31 | image 32 | 33 | ### Running the script 34 | navigate to `./test` and run: 35 | ```sh 36 | go run serve.go 37 | ``` 38 | 39 | You can then open [localhost:8080](http://localhost:8080) and the Fingerprint will be logged to console 40 | The fingerprints can be Accessible through calling `getFp()` 41 | 42 | image 43 | 44 | ### Performance 45 | - Fp takes `~ 15ms` to compute (on `apple m2 air`) 46 | - Wasm size is `80kb` and `28kb`, before and after compression 47 | 48 | ### Optimizing compiled wasm from golang (both tinygo and gzip are used here) 49 | 50 | - using [tinygo](https://github.com/tinygo-org/tinygo) ~ `75%` filesize reduction 51 | ```js 52 | // using -no-debug and -opt=z to strip debug info and minimize filesize 53 | tinygo build -o output.wasm -target wasm -no-debug -opt=z input.go 54 | ``` 55 | drawback: limited library implementation - solution: implement libraries natively like [`HexEncode`](https://github.com/onlpsec/fingerprint/blob/main/internal/crypto/hex.go). 56 | 57 | - using [gzip](https://www.gnu.org/software/gzip/) ~ `50%` filesize reduction 58 | ```sh 59 | gzip -9 -v -c input.wasm > output.min.wasm 60 | ``` 61 | drawbacks: + `21kb` from [gzip](https://github.com/onlpsec/fingerprint/blob/main/test/static/gzip.min.js) javascript library 62 | 63 | VsCode settings (for gopls): 64 | ```sh 65 | GOOS=js GOARCH=wasm 66 | ``` 67 | - if not, you will get an annoying (fake) error for including `syscall/js` 68 | 69 | ### Credits 70 | - https://newassets.hcaptcha.com/c/ac578c1/hsw.js 71 | - https://github.com/fingerprintjs/fingerprintjs 72 | -------------------------------------------------------------------------------- /cmd/fingerprint/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "syscall/js" 5 | "time" 6 | 7 | "github.com/onlpsec/fingerprint/internal/canvas" 8 | "github.com/onlpsec/fingerprint/internal/crypto" 9 | "github.com/onlpsec/fingerprint/internal/protobuf" 10 | "github.com/onlpsec/fingerprint/internal/webgl" 11 | ) 12 | 13 | func ProtoTest(this js.Value, inputs []js.Value) interface{} { 14 | hash, winding := canvas.CanvasFp() 15 | 16 | proto_bean := map[int]interface{}{ 17 | 1: "field_ome", 18 | 2: 2, 19 | 3: map[int]interface{}{ 20 | 1: hash, 21 | 2: winding, 22 | }, 23 | 4: "abcdef", 24 | 5: "field_ome", 25 | 6: 2, 26 | 7: map[int]interface{}{ 27 | 1: 1, 28 | 2: 2, 29 | 3: map[int]interface{}{ 30 | 1: 1, 31 | 2: 2, 32 | }, 33 | }, 34 | 8: "abcdef", 35 | } 36 | 37 | return crypto.HexEncode(protobuf.ProtoBuf(proto_bean)) 38 | } 39 | 40 | func getFp(this js.Value, inputs []js.Value) interface{} { 41 | start := time.Now() 42 | canvas_fp, canvas_winding := canvas.CanvasFp() 43 | webgl_fp := webgl.WebglFp() 44 | webgl_params := webgl.GetWebGLParameters() 45 | 46 | performance := time.Since(start).Seconds() 47 | 48 | return js.ValueOf(map[string]interface{}{ 49 | "canvas": map[string]interface{}{ 50 | "fp": canvas_fp, 51 | "winding": canvas_winding, 52 | }, 53 | "webgl": map[string]interface{}{ 54 | "fp": webgl_fp, 55 | "params": map[string]interface{}{ 56 | "extensions": webgl_params.Extensions, 57 | "general": webgl_params.General, 58 | "shaderprecision": webgl_params.ShaderPrecision, 59 | }, 60 | }, 61 | "elapsed": performance, 62 | }) 63 | } 64 | 65 | func main() { 66 | c := make(chan struct{}, 0) 67 | 68 | js.Global().Set("ProtoTest", js.FuncOf(ProtoTest)) 69 | js.Global().Set("getFp", js.FuncOf(getFp)) 70 | <-c 71 | } 72 | -------------------------------------------------------------------------------- /dist/script.min.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onlpsec/fingerprint/19371a9f83d1aad0bd147efa3a0f7b9d9d9d5c97/dist/script.min.wasm -------------------------------------------------------------------------------- /dist/script.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onlpsec/fingerprint/19371a9f83d1aad0bd147efa3a0f7b9d9d9d5c97/dist/script.wasm -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/onlpsec/fingerprint 2 | 3 | go 1.21.4 -------------------------------------------------------------------------------- /go.work: -------------------------------------------------------------------------------- 1 | go 1.21.4 2 | 3 | use ( 4 | ./ 5 | ) -------------------------------------------------------------------------------- /internal/canvas/canvasfp.go: -------------------------------------------------------------------------------- 1 | package canvas 2 | 3 | import ( 4 | "syscall/js" 5 | 6 | "github.com/onlpsec/fingerprint/internal/crypto" 7 | ) 8 | 9 | func drawCircle(ctx js.Value, x, y, radius int) { 10 | Pi := 3.14159265358979323846264338327950288419716939937510582097494459 11 | ctx.Call("beginPath") 12 | ctx.Call("arc", x, y, radius, 0, Pi*2, true) 13 | ctx.Call("closePath") 14 | ctx.Call("fill") 15 | } 16 | 17 | func CanvasFp() (string, int) { 18 | 19 | canvas := js.Global().Get("document").Call("createElement", "canvas") 20 | canvas.Set("width", 2000) 21 | canvas.Set("height", 200) 22 | canvas.Get("style").Set("display", "inline") 23 | 24 | ctx := canvas.Call("getContext", "2d") 25 | ctx.Call("rect", 0, 0, 10, 10) 26 | ctx.Call("rect", 2, 2, 6, 6) 27 | 28 | has_winding := 0 29 | winding := ctx.Call("isPointInPath", 5, 5, "evenodd").Bool() == false 30 | if winding == true { 31 | has_winding = 1 32 | } 33 | 34 | ctx.Set("textBaseline", "alphabetic") 35 | ctx.Set("fillStyle", "#f60") 36 | ctx.Call("fillRect", 125, 1, 62, 20) 37 | ctx.Set("fillStyle", "#069") 38 | ctx.Set("font", "11pt Arial") 39 | ctx.Call("fillText", "Cwm fjordbank glyphs vext quiz, 😂😂", 2, 15) 40 | 41 | ctx.Set("fillStyle", "rgba(102, 204, 0, 0.2)") 42 | ctx.Set("font", "18pt Arial") 43 | ctx.Call("fillText", "Cwm fjordbank glyphs vext quiz, 😂😂", 4, 45) 44 | 45 | ctx.Set("globalCompositeOperation", "multiply") 46 | ctx.Set("fillStyle", "rgb(255,0,255)") 47 | drawCircle(ctx, 50, 50, 50) 48 | ctx.Set("fillStyle", "rgb(0,255,255)") 49 | drawCircle(ctx, 100, 50, 50) 50 | ctx.Set("fillStyle", "rgb(255,255,0)") 51 | drawCircle(ctx, 75, 100, 50) 52 | ctx.Set("fillStyle", "rgb(255,0,255)") 53 | 54 | drawCircle(ctx, 75, 75, 75) 55 | drawCircle(ctx, 75, 75, 25) 56 | ctx.Call("fill", "evenodd") 57 | 58 | hash := "" 59 | if canvas.Get("toDataURL").Truthy() { 60 | // x64hash128 unavailable in GO 61 | hash = crypto.X64hash128(canvas.Call("toDataURL").String()) 62 | } else { 63 | return "", -1 64 | } 65 | 66 | return hash, has_winding 67 | } 68 | -------------------------------------------------------------------------------- /internal/canvas/picasso.go: -------------------------------------------------------------------------------- 1 | package canvas 2 | 3 | import ( 4 | "github.com/onlpsec/fingerprint/internal/crypto" 5 | ) 6 | 7 | func PicassoCanvasFp(roundNumber, params int) string { 8 | 9 | return crypto.X64hash128("") 10 | } 11 | -------------------------------------------------------------------------------- /internal/crypto/hex.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | const hextable = "0123456789abcdef" 4 | 5 | func EncodedLen(n int) int { return n * 2 } 6 | 7 | func Encode(dst, src []byte) int { 8 | j := 0 9 | for _, v := range src { 10 | dst[j] = hextable[v>>4] 11 | dst[j+1] = hextable[v&0x0f] 12 | j += 2 13 | } 14 | return len(src) * 2 15 | } 16 | 17 | func HexEncode(src []byte) string { 18 | dst := make([]byte, EncodedLen(len(src))) 19 | Encode(dst, src) 20 | return string(dst) 21 | } 22 | -------------------------------------------------------------------------------- /internal/crypto/murmur.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | const ( 4 | c1 uint64 = 0x87c37b91114253d5 5 | c2 uint64 = 0x4cf5ad432745937f 6 | ) 7 | 8 | type Hash interface { 9 | Write([]byte) (int, error) 10 | Sum([]byte) []byte 11 | Reset() 12 | Size() int 13 | BlockSize() int 14 | } 15 | 16 | type ( 17 | sum128 struct { 18 | digest [2]uint64 19 | seed uint64 20 | } 21 | 22 | Hash128 interface { 23 | Hash 24 | Sum128() [2]uint64 25 | } 26 | ) 27 | 28 | func rotl64(n uint64, count uint) uint64 { 29 | return (n << count) | (n >> (64 - count)) 30 | } 31 | 32 | func New128() Hash128 { 33 | return New128WithSeed(0) 34 | } 35 | 36 | func New128WithSeed(seed uint64) Hash128 { 37 | return &sum128{digest: [2]uint64{seed, seed}, seed: seed} 38 | } 39 | 40 | func (s *sum128) Sum128() [2]uint64 { 41 | return s.digest 42 | } 43 | 44 | func (s *sum128) Write(data []byte) (int, error) { 45 | tail := s.blockInterMix(data) 46 | s.blockInterMixTail(tail) 47 | s.finalizeMix(data) 48 | 49 | return len(data), nil 50 | } 51 | 52 | func (s *sum128) blockInterMix(data []byte) []byte { 53 | tailStart := 0 54 | for i := 0; i+s.Size() <= len(data); i += s.Size() { 55 | var k1, k2 uint64 56 | 57 | tailStart += s.Size() 58 | 59 | x := data[i : i+s.Size()/2] 60 | y := data[i+s.Size()/2:] 61 | 62 | for j := 0; j < s.Size()/2; j++ { 63 | k1 |= uint64(x[j]) << uint(j*8) 64 | k2 |= uint64(y[j]) << uint(j*8) 65 | } 66 | 67 | k1 *= c1 68 | k1 = rotl64(k1, 31) 69 | k1 *= c2 70 | 71 | s.digest[0] ^= k1 72 | s.digest[0] = rotl64(s.digest[0], 27) 73 | s.digest[0] += s.digest[1] 74 | s.digest[0] = s.digest[0]*5 + 0x52dce729 75 | 76 | k2 *= c2 77 | k2 = rotl64(k2, 33) 78 | k2 *= c1 79 | 80 | s.digest[1] ^= k2 81 | s.digest[1] = rotl64(s.digest[1], 31) 82 | s.digest[1] += s.digest[0] 83 | s.digest[1] = s.digest[1]*5 + 0x38495ab5 84 | } 85 | 86 | return data[tailStart:] 87 | } 88 | 89 | func (s *sum128) blockInterMixTail(tail []byte) { 90 | var k1, k2 uint64 91 | 92 | switch len(tail) & 15 { 93 | case 15: 94 | k2 ^= uint64(tail[14]) << 48 95 | fallthrough 96 | case 14: 97 | k2 ^= uint64(tail[13]) << 40 98 | fallthrough 99 | case 13: 100 | k2 ^= uint64(tail[12]) << 32 101 | fallthrough 102 | case 12: 103 | k2 ^= uint64(tail[11]) << 24 104 | fallthrough 105 | case 11: 106 | k2 ^= uint64(tail[10]) << 16 107 | fallthrough 108 | case 10: 109 | k2 ^= uint64(tail[9]) << 8 110 | fallthrough 111 | case 9: 112 | k2 ^= uint64(tail[8]) << 0 113 | k2 *= c2 114 | k2 = rotl64(k2, 33) 115 | k2 *= c1 116 | 117 | s.digest[1] ^= k2 118 | fallthrough 119 | case 8: 120 | k1 ^= uint64(tail[7]) << 56 121 | fallthrough 122 | case 7: 123 | k1 ^= uint64(tail[6]) << 48 124 | fallthrough 125 | case 6: 126 | k1 ^= uint64(tail[5]) << 40 127 | fallthrough 128 | case 5: 129 | k1 ^= uint64(tail[4]) << 32 130 | fallthrough 131 | case 4: 132 | k1 ^= uint64(tail[3]) << 24 133 | fallthrough 134 | case 3: 135 | k1 ^= uint64(tail[2]) << 16 136 | fallthrough 137 | case 2: 138 | k1 ^= uint64(tail[1]) << 8 139 | fallthrough 140 | case 1: 141 | k1 ^= uint64(tail[0]) << 0 142 | k1 *= c1 143 | k1 = rotl64(k1, 31) 144 | k1 *= c2 145 | 146 | s.digest[0] ^= k1 147 | } 148 | } 149 | 150 | func (s *sum128) finalizeMix(data []byte) { 151 | s.digest[0] ^= uint64(len(data)) 152 | s.digest[1] ^= uint64(len(data)) 153 | 154 | s.digest[0] += s.digest[1] 155 | s.digest[1] += s.digest[0] 156 | 157 | s.digest[0] ^= s.digest[0] >> 33 158 | s.digest[0] *= 0xff51afd7ed558ccd 159 | s.digest[0] ^= s.digest[0] >> 33 160 | s.digest[0] *= 0xc4ceb9fe1a85ec53 161 | s.digest[0] ^= s.digest[0] >> 33 162 | 163 | s.digest[1] ^= s.digest[1] >> 33 164 | s.digest[1] *= 0xff51afd7ed558ccd 165 | s.digest[1] ^= s.digest[1] >> 33 166 | s.digest[1] *= 0xc4ceb9fe1a85ec53 167 | s.digest[1] ^= s.digest[1] >> 33 168 | 169 | s.digest[0] += s.digest[1] 170 | s.digest[1] += s.digest[0] 171 | 172 | } 173 | 174 | func (s *sum128) Sum(in []byte) []byte { 175 | return append(in, 176 | byte(s.digest[0]>>56), 177 | byte(s.digest[0]>>48), 178 | byte(s.digest[0]>>40), 179 | byte(s.digest[0]>>32), 180 | byte(s.digest[0]>>24), 181 | byte(s.digest[0]>>16), 182 | byte(s.digest[0]>>8), 183 | byte(s.digest[0]), 184 | byte(s.digest[1]>>56), 185 | byte(s.digest[1]>>48), 186 | byte(s.digest[1]>>40), 187 | byte(s.digest[1]>>32), 188 | byte(s.digest[1]>>24), 189 | byte(s.digest[1]>>16), 190 | byte(s.digest[1]>>8), 191 | byte(s.digest[1]), 192 | ) 193 | 194 | } 195 | 196 | func (s *sum128) Reset() { 197 | s.digest[0] = s.seed 198 | s.digest[1] = s.seed 199 | } 200 | 201 | func (s *sum128) Size() int { 202 | return 16 203 | } 204 | 205 | func (s *sum128) BlockSize() int { 206 | return 1 207 | } 208 | 209 | func X64hash128(content string) string { 210 | hash := New128WithSeed(1234) 211 | data := []byte(content) 212 | hash.Write(data) 213 | hashedValue := hash.Sum(nil) 214 | 215 | hexStr := make([]byte, EncodedLen(len(hashedValue))) 216 | Encode(hexStr, hashedValue) 217 | 218 | return string(hexStr) 219 | } 220 | -------------------------------------------------------------------------------- /internal/protobuf/proto.go: -------------------------------------------------------------------------------- 1 | package protobuf 2 | 3 | import ( 4 | "bytes" 5 | "sort" 6 | ) 7 | 8 | type ProtoFieldType int 9 | 10 | const ( 11 | VARINT ProtoFieldType = iota 12 | INT64 13 | STRING 14 | ) 15 | 16 | func write0(data *bytes.Buffer, byte byte) { 17 | data.WriteByte(byte & 0xFF) 18 | } 19 | 20 | func write(data *bytes.Buffer, bytes []byte) { 21 | data.Write(bytes) 22 | } 23 | 24 | func writeVarint(data *bytes.Buffer, vint uint32) { 25 | vint = vint & 0xFFFFFFFF 26 | for vint > 0x80 { 27 | write0(data, byte((vint&0x7F)|0x80)) 28 | vint >>= 7 29 | } 30 | write0(data, byte(vint&0x7F)) 31 | } 32 | 33 | func writeString(data *bytes.Buffer, str string) { 34 | writeVarint(data, uint32(len(str))) 35 | write(data, []byte(str)) 36 | } 37 | 38 | func ProtoBuf(data map[int]interface{}) []byte { 39 | result := new(bytes.Buffer) 40 | 41 | // Get the keys and sort them 42 | keys := make([]int, 0, len(data)) 43 | for k := range data { 44 | keys = append(keys, k) 45 | } 46 | sort.Ints(keys) 47 | 48 | // Iterate over the sorted keys 49 | for _, k := range keys { 50 | v := data[k] 51 | switch v := v.(type) { 52 | case int: 53 | key := (k << 3) | int(VARINT&7) 54 | writeVarint(result, uint32(key)) 55 | writeVarint(result, uint32(v)) 56 | 57 | case string: 58 | key := (k << 3) | int(STRING&7) 59 | writeVarint(result, uint32(key)) 60 | writeString(result, v) 61 | 62 | case map[int]interface{}: 63 | key := (k << 3) | int(STRING&7) 64 | writeVarint(result, uint32(key)) 65 | writeString(result, string(ProtoBuf(v))) 66 | 67 | default: 68 | panic("") 69 | } 70 | } 71 | return result.Bytes() 72 | } 73 | -------------------------------------------------------------------------------- /internal/protobuf/proto_test.go: -------------------------------------------------------------------------------- 1 | package protobuf_test 2 | -------------------------------------------------------------------------------- /internal/types/default.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | type FpStruct struct { 4 | Hash string 5 | Winding int 6 | } 7 | -------------------------------------------------------------------------------- /internal/webgl/engine.go: -------------------------------------------------------------------------------- 1 | package webgl 2 | 3 | import "syscall/js" 4 | 5 | func getWebglEngine() js.Value { 6 | doc := js.Global().Get("document") 7 | canvas := doc.Call("createElement", "canvas") 8 | var gl js.Value 9 | defer func() { 10 | if r := recover(); r != nil { 11 | gl = js.Null() 12 | } 13 | }() 14 | gl = canvas.Call("getContext", "webgl") 15 | if gl.IsNull() { 16 | gl = canvas.Call("getContext", "experimental-webgl") 17 | } 18 | return gl 19 | } 20 | -------------------------------------------------------------------------------- /internal/webgl/fingerprint.go: -------------------------------------------------------------------------------- 1 | package webgl 2 | 3 | import ( 4 | "syscall/js" 5 | 6 | "github.com/onlpsec/fingerprint/internal/crypto" 7 | ) 8 | 9 | func WebglFp() interface{} { 10 | webglContext := getWebglEngine() 11 | if webglContext.IsNull() { 12 | return -1 13 | } 14 | 15 | vShaderTemplate := "attribute vec2 attrVertex;varying vec2 varyinTexCoordinate;uniform vec2 uniformOffset;void main(){varyinTexCoordinate=attrVertex+uniformOffset;gl_Position=vec4(attrVertex,0,1);}" 16 | fShaderTemplate := "precision mediump float;varying vec2 varyinTexCoordinate;void main() {gl_FragColor=vec4(varyinTexCoordinate,0,1);}" 17 | 18 | vertexPosBuffer := webglContext.Call("createBuffer") 19 | webglContext.Call("bindBuffer", webglContext.Get("ARRAY_BUFFER"), vertexPosBuffer) 20 | 21 | vertices := []float32{-0.2, -0.9, 0, 0.4, -0.26, 0, 0, 0.732134444, 0} 22 | array := make([]interface{}, len(vertices)) 23 | for i, v := range vertices { 24 | array[i] = v 25 | } 26 | jsVertices := js.ValueOf(array) 27 | float32Array := js.Global().Get("Float32Array").New(jsVertices) 28 | webglContext.Call("bufferData", webglContext.Get("ARRAY_BUFFER"), float32Array, webglContext.Get("STATIC_DRAW")) 29 | 30 | program := webglContext.Call("createProgram") 31 | vshader := webglContext.Call("createShader", webglContext.Get("VERTEX_SHADER")) 32 | webglContext.Call("shaderSource", vshader, vShaderTemplate) 33 | webglContext.Call("compileShader", vshader) 34 | fshader := webglContext.Call("createShader", webglContext.Get("FRAGMENT_SHADER")) 35 | webglContext.Call("shaderSource", fshader, fShaderTemplate) 36 | webglContext.Call("compileShader", fshader) 37 | 38 | webglContext.Call("attachShader", program, vshader) 39 | webglContext.Call("attachShader", program, fshader) 40 | webglContext.Call("linkProgram", program) 41 | webglContext.Call("useProgram", program) 42 | 43 | attrVertex := webglContext.Call("getAttribLocation", program, "attrVertex") 44 | webglContext.Call("enableVertexAttribArray", attrVertex) 45 | webglContext.Call("vertexAttribPointer", attrVertex, 2, webglContext.Get("FLOAT"), false, 0, 0) 46 | 47 | uniformOffset := webglContext.Call("getUniformLocation", program, "uniformOffset") 48 | webglContext.Call("uniform2f", uniformOffset, 1, 1) 49 | 50 | webglContext.Call("drawArrays", webglContext.Get("TRIANGLE_STRIP"), 0, len(vertices)/2) 51 | 52 | result := webglContext.Get("canvas").Call("toDataURL").String() 53 | 54 | return crypto.X64hash128(result) 55 | } 56 | -------------------------------------------------------------------------------- /internal/webgl/params.go: -------------------------------------------------------------------------------- 1 | package webgl 2 | 3 | import ( 4 | "syscall/js" 5 | ) 6 | 7 | type WebGLParameters struct { 8 | Extensions []interface{} 9 | General map[string]interface{} 10 | ShaderPrecision map[string]interface{} 11 | } 12 | 13 | func GetWebGLParameters() WebGLParameters { 14 | 15 | glContext := getWebglEngine() 16 | 17 | maxAnisotropy := func(e js.Value) js.Value { 18 | t := js.Value{} 19 | i := e.Call("getExtension", "EXT_texture_filter_anisotropic") 20 | if !i.Truthy() { 21 | i = e.Call("getExtension", "WEBKIT_EXT_texture_filter_anisotropic") 22 | } 23 | if !i.Truthy() { 24 | i = e.Call("getExtension", "MOZ_EXT_texture_filter_anisotropic") 25 | } 26 | 27 | if i.Truthy() { 28 | t = e.Call("getParameter", i.Get("MAX_TEXTURE_MAX_ANISOTROPY_EXT")) 29 | if t.Int() == 0 { 30 | t = js.ValueOf(2) 31 | } 32 | } 33 | 34 | return t 35 | }(glContext) 36 | 37 | webglParameters := map[string]interface{}{ 38 | "MAX_ANISOTROPY": maxAnisotropy, 39 | "ANTIALIAS": "no", 40 | } 41 | if glContext.Call("getContextAttributes").Get("antialias").Truthy() { 42 | webglParameters["ANTIALIAS"] = "yes" 43 | } 44 | 45 | parameters := []string{ 46 | "ALPHA_BITS", 47 | "BLUE_BITS", 48 | "DEPTH_BITS", 49 | "GREEN_BITS", 50 | "MAX_COMBINED_TEXTURE_IMAGE_UNITS", 51 | "MAX_CUBE_MAP_TEXTURE_SIZE", 52 | "MAX_FRAGMENT_UNIFORM_VECTORS", 53 | "MAX_RENDERBUFFER_SIZE", 54 | "MAX_TEXTURE_IMAGE_UNITS", 55 | "MAX_TEXTURE_SIZE", 56 | "MAX_VARYING_VECTORS", 57 | "MAX_VERTEX_ATTRIBS", 58 | "MAX_VERTEX_TEXTURE_IMAGE_UNITS", 59 | "MAX_VERTEX_UNIFORM_VECTORS", 60 | "RED_BITS", 61 | "RENDERER", 62 | "SHADING_LANGUAGE_VERSION", 63 | "STENCIL_BITS", 64 | "VENDOR", 65 | "VERSION", 66 | } 67 | 68 | arrayParams := []string{ 69 | "ALIASED_LINE_WIDTH_RANGE", 70 | "ALIASED_POINT_SIZE_RANGE", 71 | "MAX_VIEWPORT_DIMS", 72 | } 73 | 74 | shaders := []string{ 75 | "VERTEX_SHADER", 76 | "FRAGMENT_SHADER", 77 | } 78 | 79 | precisions := []string{ 80 | "HIGH_FLOAT", 81 | "MEDIUM_FLOAT", 82 | "LOW_FLOAT", 83 | "HIGH_INT", 84 | "MEDIUM_INT", 85 | "LOW_INT", 86 | } 87 | 88 | precisionAttributesKeys := []string{"rangeMin", "rangeMax", "precision"} 89 | 90 | precisionAttributes := map[string]interface{}{} 91 | 92 | for _, shader := range shaders { 93 | for _, precision := range precisions { 94 | for _, attrKey := range precisionAttributesKeys { 95 | precisionAttributes[shader+"_"+precision+"_"+attrKey] = 96 | glContext.Call("getShaderPrecisionFormat", glContext.Get(shader), glContext.Get(precision)).Get(attrKey) 97 | } 98 | } 99 | } 100 | 101 | for _, parameter := range parameters { 102 | webglParameters[parameter] = glContext.Call("getParameter", glContext.Get(parameter)) 103 | } 104 | 105 | for _, parameter := range arrayParams { 106 | jsonParam := glContext.Call("getParameter", glContext.Get(parameter)) 107 | webglParameters[parameter] = "[" + jsonParam.Index(0).String() + "," + jsonParam.Index(1).String() + "]" 108 | } 109 | 110 | webglExtensions := glContext.Call("getSupportedExtensions") 111 | 112 | extensions := make([]interface{}, webglExtensions.Length()) 113 | for i := 0; i < webglExtensions.Length(); i++ { 114 | extensions[i] = webglExtensions.Index(i).String() 115 | } 116 | 117 | return WebGLParameters{ 118 | Extensions: extensions, 119 | General: webglParameters, 120 | ShaderPrecision: precisionAttributes, 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /scripts/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: compile 2 | 3 | compile: 4 | @echo "compiling to wasm ..." 5 | cd ../cmd/fingerprint/ && tinygo build -o ../../dist/script.wasm -target wasm -no-debug -opt=z main.go 6 | @size=$$(($$(wc -c < ../dist/script.wasm)/1024)) ; \ 7 | printf "wasm size: %s kB\n" $$size 8 | @echo "compressing wasm with gzip..." 9 | gzip -9 -v -c ../dist/script.wasm > ../dist/script.min.wasm 10 | @size=$$(($$(wc -c < ../dist/script.min.wasm)/1024)) ; \ 11 | printf "compressed wasm size: %s kB\n" $$size 12 | @echo "done. wasm is in ./dist/script.wasm" -------------------------------------------------------------------------------- /test/serve.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | ) 7 | 8 | func main() { 9 | fs := http.FileServer(http.Dir("./static")) 10 | http.Handle("/static/", http.StripPrefix("/static/", fs)) 11 | 12 | fs1 := http.FileServer(http.Dir("./../dist")) 13 | http.Handle("/dist/", http.StripPrefix("/dist/", fs1)) 14 | 15 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 16 | http.ServeFile(w, r, "./static/index.html") 17 | }) 18 | 19 | log.Println("Listening on http://localhost:8080...") 20 | err := http.ListenAndServe(":8080", nil) 21 | if err != nil { 22 | log.Fatal(err) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 22 | 23 | -------------------------------------------------------------------------------- /test/static/js/gzip.min.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).gzip={})}(this,function(e){"use strict";var t=(e,t,i,n)=>{let a=65535&e|0,r=e>>>16&65535|0,o=0;for(;0!==i;){i-=o=i>2e3?2e3:i;do{r=r+(a=a+t[n++]|0)|0}while(--o);a%=65521,r%=65521}return a|r<<16|0};const i=new Uint32Array((()=>{let e,t=[];for(var i=0;i<256;i++){e=i;for(var n=0;n<8;n++)e=1&e?3988292384^e>>>1:e>>>1;t[i]=e}return t})());var n=(e,t,n,a)=>{const r=i,o=a+n;e^=-1;for(let i=a;i>>8^r[255&(e^t[i])];return-1^e};const a=16209;var r=function(e,t){let i,n,r,o,s,l,d,f,c,h,u,w,b,m,k,_,g,p,v,x,y,E,R,A;const Z=e.state;i=e.next_in,R=e.input,n=i+(e.avail_in-5),r=e.next_out,A=e.output,o=r-(t-e.avail_out),s=r+(e.avail_out-257),l=Z.dmax,d=Z.wsize,f=Z.whave,c=Z.wnext,h=Z.window,u=Z.hold,w=Z.bits,b=Z.lencode,m=Z.distcode,k=(1<>>=p=g>>>24,w-=p,0===(p=g>>>16&255))A[r++]=65535&g;else{if(!(16&p)){if(0==(64&p)){g=b[(65535&g)+(u&(1<>>=p,w-=p),w<15&&(u+=R[i++]<>>=p=g>>>24,w-=p,!(16&(p=g>>>16&255))){if(0==(64&p)){g=m[(65535&g)+(u&(1<l){e.msg="invalid distance too far back",Z.mode=a;break e}if(u>>>=p,w-=p,x>(p=r-o)){if((p=x-p)>f&&Z.sane){e.msg="invalid distance too far back",Z.mode=a;break e}if(y=0,E=h,0===c){if(y+=d-p,p2;)A[r++]=E[y++],A[r++]=E[y++],A[r++]=E[y++],v-=3;v&&(A[r++]=E[y++],v>1&&(A[r++]=E[y++]))}else{y=r-x;do{A[r++]=A[y++],A[r++]=A[y++],A[r++]=A[y++],v-=3}while(v>2);v&&(A[r++]=A[y++],v>1&&(A[r++]=A[y++]))}break}}break}}while(i>3,u&=(1<<(w-=v<<3))-1,e.next_in=i,e.next_out=r,e.avail_in=i{const h=c.bits;let u,w,b,m,k,_,g=0,p=0,v=0,x=0,y=0,E=0,R=0,A=0,Z=0,S=0,T=null;const O=new Uint16Array(16),U=new Uint16Array(16);let D,I,B,N=null;for(g=0;g<=15;g++)O[g]=0;for(p=0;p=1&&0===O[x];x--);if(y>x&&(y=x),0===x)return a[r++]=20971520,a[r++]=20971520,c.bits=1,0;for(v=1;v0&&(0===e||1!==x))return-1;for(U[1]=0,g=1;g<15;g++)U[g+1]=U[g]+O[g];for(p=0;p852||2===e&&Z>592)return 1;for(;;){D=g-R,f[p]+1<_?(I=0,B=f[p]):f[p]>=_?(I=N[f[p]-_],B=T[f[p]-_]):(I=96,B=0),u=1<>R)+(w-=u)]=D<<24|I<<16|B|0}while(0!==w);for(u=1<>=1;if(0!==u?(S&=u-1,S+=u):S=0,p++,0==--O[g]){if(g===x)break;g=t[i+f[p]]}if(g>y&&(S&m)!==b){for(0===R&&(R=y),k+=v,A=1<<(E=g-R);E+R852||2===e&&Z>592)return 1;a[b=S&m]=y<<24|E<<16|k-r|0}}return 0!==S&&(a[k+S]=g-R<<24|64<<16|0),c.bits=y,0},c={Z_NO_FLUSH:0,Z_PARTIAL_FLUSH:1,Z_SYNC_FLUSH:2,Z_FULL_FLUSH:3,Z_FINISH:4,Z_BLOCK:5,Z_TREES:6,Z_OK:0,Z_STREAM_END:1,Z_NEED_DICT:2,Z_ERRNO:-1,Z_STREAM_ERROR:-2,Z_DATA_ERROR:-3,Z_MEM_ERROR:-4,Z_BUF_ERROR:-5,Z_NO_COMPRESSION:0,Z_BEST_SPEED:1,Z_BEST_COMPRESSION:9,Z_DEFAULT_COMPRESSION:-1,Z_FILTERED:1,Z_HUFFMAN_ONLY:2,Z_RLE:3,Z_FIXED:4,Z_DEFAULT_STRATEGY:0,Z_BINARY:0,Z_TEXT:1,Z_UNKNOWN:2,Z_DEFLATED:8};const{Z_FINISH:h,Z_BLOCK:u,Z_TREES:w,Z_OK:b,Z_STREAM_END:m,Z_NEED_DICT:k,Z_STREAM_ERROR:_,Z_DATA_ERROR:g,Z_MEM_ERROR:p,Z_BUF_ERROR:v,Z_DEFLATED:x}=c,y=16180,E=16190,R=16191,A=16192,Z=16194,S=16199,T=16200,O=16206,U=16209,D=e=>(e>>>24&255)+(e>>>8&65280)+((65280&e)<<8)+((255&e)<<24);const I=e=>{if(!e)return 1;const t=e.state;return!t||t.strm!==e||t.mode16211?1:0},B=e=>{if(I(e))return _;const t=e.state;return e.total_in=e.total_out=t.total=0,e.msg="",t.wrap&&(e.adler=1&t.wrap),t.mode=y,t.last=0,t.havedict=0,t.flags=-1,t.dmax=32768,t.head=null,t.hold=0,t.bits=0,t.lencode=t.lendyn=new Int32Array(852),t.distcode=t.distdyn=new Int32Array(592),t.sane=1,t.back=-1,b},N=e=>{if(I(e))return _;const t=e.state;return t.wsize=0,t.whave=0,t.wnext=0,B(e)},C=(e,t)=>{let i;if(I(e))return _;const n=e.state;return t<0?(i=0,t=-t):(i=5+(t>>4),t<48&&(t&=15)),t&&(t<8||t>15)?_:(null!==n.window&&n.wbits!==t&&(n.window=null),n.wrap=i,n.wbits=t,N(e))},z=(e,t)=>{if(!e)return _;const i=new function(){this.strm=null,this.mode=0,this.last=!1,this.wrap=0,this.havedict=!1,this.flags=0,this.dmax=0,this.check=0,this.total=0,this.head=null,this.wbits=0,this.wsize=0,this.whave=0,this.wnext=0,this.window=null,this.hold=0,this.bits=0,this.length=0,this.offset=0,this.extra=0,this.lencode=null,this.distcode=null,this.lenbits=0,this.distbits=0,this.ncode=0,this.nlen=0,this.ndist=0,this.have=0,this.next=null,this.lens=new Uint16Array(320),this.work=new Uint16Array(288),this.lendyn=null,this.distdyn=null,this.sane=0,this.back=0,this.was=0};e.state=i,i.strm=e,i.window=null,i.mode=y;const n=C(e,t);return n!==b&&(e.state=null),n};let F,L,M=!0;const H=e=>{if(M){F=new Int32Array(512),L=new Int32Array(32);let t=0;for(;t<144;)e.lens[t++]=8;for(;t<256;)e.lens[t++]=9;for(;t<280;)e.lens[t++]=7;for(;t<288;)e.lens[t++]=8;for(f(1,e.lens,0,288,F,0,e.work,{bits:9}),t=0;t<32;)e.lens[t++]=5;f(2,e.lens,0,32,L,0,e.work,{bits:5}),M=!1}e.lencode=F,e.lenbits=9,e.distcode=L,e.distbits=5},j=(e,t,i,n)=>{let a;const r=e.state;return null===r.window&&(r.wsize=1<=r.wsize?(r.window.set(t.subarray(i-r.wsize,i),0),r.wnext=0,r.whave=r.wsize):((a=r.wsize-r.wnext)>n&&(a=n),r.window.set(t.subarray(i-n,i-n+a),r.wnext),(n-=a)?(r.window.set(t.subarray(i-n,i),0),r.wnext=n,r.whave=r.wsize):(r.wnext+=a,r.wnext===r.wsize&&(r.wnext=0),r.whavez(e,15),inflateInit2:z,inflate:(e,i)=>{let a,o,s,l,d,c,B,N,C,z,F,L,M,K,P,Y,G,X,W,q,J,Q,V=0;const $=new Uint8Array(4);let ee,te;const ie=new Uint8Array([16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15]);if(I(e)||!e.output||!e.input&&0!==e.avail_in)return _;(a=e.state).mode===R&&(a.mode=A),d=e.next_out,s=e.output,B=e.avail_out,l=e.next_in,o=e.input,c=e.avail_in,N=a.hold,C=a.bits,z=c,F=B,Q=b;e:for(;;)switch(a.mode){case y:if(0===a.wrap){a.mode=A;break}for(;C<16;){if(0===c)break e;c--,N+=o[l++]<>>8&255,a.check=n(a.check,$,2,0),N=0,C=0,a.mode=16181;break}if(a.head&&(a.head.done=!1),!(1&a.wrap)||(((255&N)<<8)+(N>>8))%31){e.msg="incorrect header check",a.mode=U;break}if((15&N)!==x){e.msg="unknown compression method",a.mode=U;break}if(C-=4,J=8+(15&(N>>>=4)),0===a.wbits&&(a.wbits=J),J>15||J>a.wbits){e.msg="invalid window size",a.mode=U;break}a.dmax=1<>8&1),512&a.flags&&4&a.wrap&&($[0]=255&N,$[1]=N>>>8&255,a.check=n(a.check,$,2,0)),N=0,C=0,a.mode=16182;case 16182:for(;C<32;){if(0===c)break e;c--,N+=o[l++]<>>8&255,$[2]=N>>>16&255,$[3]=N>>>24&255,a.check=n(a.check,$,4,0)),N=0,C=0,a.mode=16183;case 16183:for(;C<16;){if(0===c)break e;c--,N+=o[l++]<>8),512&a.flags&&4&a.wrap&&($[0]=255&N,$[1]=N>>>8&255,a.check=n(a.check,$,2,0)),N=0,C=0,a.mode=16184;case 16184:if(1024&a.flags){for(;C<16;){if(0===c)break e;c--,N+=o[l++]<>>8&255,a.check=n(a.check,$,2,0)),N=0,C=0}else a.head&&(a.head.extra=null);a.mode=16185;case 16185:if(1024&a.flags&&((L=a.length)>c&&(L=c),L&&(a.head&&(J=a.head.extra_len-a.length,a.head.extra||(a.head.extra=new Uint8Array(a.head.extra_len)),a.head.extra.set(o.subarray(l,l+L),J)),512&a.flags&&4&a.wrap&&(a.check=n(a.check,o,L,l)),c-=L,l+=L,a.length-=L),a.length))break e;a.length=0,a.mode=16186;case 16186:if(2048&a.flags){if(0===c)break e;L=0;do{J=o[l+L++],a.head&&J&&a.length<65536&&(a.head.name+=String.fromCharCode(J))}while(J&&L>9&1,a.head.done=!0),e.adler=a.check=0,a.mode=R;break;case 16189:for(;C<32;){if(0===c)break e;c--,N+=o[l++]<>>=7&C,C-=7&C,a.mode=O;break}for(;C<3;){if(0===c)break e;c--,N+=o[l++]<>>=1)){case 0:a.mode=16193;break;case 1:if(H(a),a.mode=S,i===w){N>>>=2,C-=2;break e}break;case 2:a.mode=16196;break;case 3:e.msg="invalid block type",a.mode=U}N>>>=2,C-=2;break;case 16193:for(N>>>=7&C,C-=7&C;C<32;){if(0===c)break e;c--,N+=o[l++]<>>16^65535)){e.msg="invalid stored block lengths",a.mode=U;break}if(a.length=65535&N,N=0,C=0,a.mode=Z,i===w)break e;case Z:a.mode=16195;case 16195:if(L=a.length){if(L>c&&(L=c),L>B&&(L=B),0===L)break e;s.set(o.subarray(l,l+L),d),c-=L,l+=L,B-=L,d+=L,a.length-=L;break}a.mode=R;break;case 16196:for(;C<14;){if(0===c)break e;c--,N+=o[l++]<>>=5,C-=5,a.ndist=1+(31&N),N>>>=5,C-=5,a.ncode=4+(15&N),N>>>=4,C-=4,a.nlen>286||a.ndist>30){e.msg="too many length or distance symbols",a.mode=U;break}a.have=0,a.mode=16197;case 16197:for(;a.have>>=3,C-=3}for(;a.have<19;)a.lens[ie[a.have++]]=0;if(a.lencode=a.lendyn,a.lenbits=7,ee={bits:a.lenbits},Q=f(0,a.lens,0,19,a.lencode,0,a.work,ee),a.lenbits=ee.bits,Q){e.msg="invalid code lengths set",a.mode=U;break}a.have=0,a.mode=16198;case 16198:for(;a.have>>16&255,G=65535&V,!((P=V>>>24)<=C);){if(0===c)break e;c--,N+=o[l++]<>>=P,C-=P,a.lens[a.have++]=G;else{if(16===G){for(te=P+2;C>>=P,C-=P,0===a.have){e.msg="invalid bit length repeat",a.mode=U;break}J=a.lens[a.have-1],L=3+(3&N),N>>>=2,C-=2}else if(17===G){for(te=P+3;C>>=P)),N>>>=3,C-=3}else{for(te=P+7;C>>=P)),N>>>=7,C-=7}if(a.have+L>a.nlen+a.ndist){e.msg="invalid bit length repeat",a.mode=U;break}for(;L--;)a.lens[a.have++]=J}}if(a.mode===U)break;if(0===a.lens[256]){e.msg="invalid code -- missing end-of-block",a.mode=U;break}if(a.lenbits=9,ee={bits:a.lenbits},Q=f(1,a.lens,0,a.nlen,a.lencode,0,a.work,ee),a.lenbits=ee.bits,Q){e.msg="invalid literal/lengths set",a.mode=U;break}if(a.distbits=6,a.distcode=a.distdyn,ee={bits:a.distbits},Q=f(2,a.lens,a.nlen,a.ndist,a.distcode,0,a.work,ee),a.distbits=ee.bits,Q){e.msg="invalid distances set",a.mode=U;break}if(a.mode=S,i===w)break e;case S:a.mode=T;case T:if(c>=6&&B>=258){e.next_out=d,e.avail_out=B,e.next_in=l,e.avail_in=c,a.hold=N,a.bits=C,r(e,F),d=e.next_out,s=e.output,B=e.avail_out,l=e.next_in,o=e.input,c=e.avail_in,N=a.hold,C=a.bits,a.mode===R&&(a.back=-1);break}for(a.back=0;Y=(V=a.lencode[N&(1<>>16&255,G=65535&V,!((P=V>>>24)<=C);){if(0===c)break e;c--,N+=o[l++]<>X)])>>>16&255,G=65535&V,!(X+(P=V>>>24)<=C);){if(0===c)break e;c--,N+=o[l++]<>>=X,C-=X,a.back+=X}if(N>>>=P,C-=P,a.back+=P,a.length=G,0===Y){a.mode=16205;break}if(32&Y){a.back=-1,a.mode=R;break}if(64&Y){e.msg="invalid literal/length code",a.mode=U;break}a.extra=15&Y,a.mode=16201;case 16201:if(a.extra){for(te=a.extra;C>>=a.extra,C-=a.extra,a.back+=a.extra}a.was=a.length,a.mode=16202;case 16202:for(;Y=(V=a.distcode[N&(1<>>16&255,G=65535&V,!((P=V>>>24)<=C);){if(0===c)break e;c--,N+=o[l++]<>X)])>>>16&255,G=65535&V,!(X+(P=V>>>24)<=C);){if(0===c)break e;c--,N+=o[l++]<>>=X,C-=X,a.back+=X}if(N>>>=P,C-=P,a.back+=P,64&Y){e.msg="invalid distance code",a.mode=U;break}a.offset=G,a.extra=15&Y,a.mode=16203;case 16203:if(a.extra){for(te=a.extra;C>>=a.extra,C-=a.extra,a.back+=a.extra}if(a.offset>a.dmax){e.msg="invalid distance too far back",a.mode=U;break}a.mode=16204;case 16204:if(0===B)break e;if(L=F-B,a.offset>L){if((L=a.offset-L)>a.whave&&a.sane){e.msg="invalid distance too far back",a.mode=U;break}L>a.wnext?(L-=a.wnext,M=a.wsize-L):M=a.wnext-L,L>a.length&&(L=a.length),K=a.window}else K=s,M=d-a.offset,L=a.length;L>B&&(L=B),B-=L,a.length-=L;do{s[d++]=K[M++]}while(--L);0===a.length&&(a.mode=T);break;case 16205:if(0===B)break e;s[d++]=a.length,B--,a.mode=T;break;case O:if(a.wrap){for(;C<32;){if(0===c)break e;c--,N|=o[l++]<{if(I(e))return _;let t=e.state;return t.window&&(t.window=null),e.state=null,b},inflateGetHeader:(e,t)=>{if(I(e))return _;const i=e.state;return 0==(2&i.wrap)?_:(i.head=t,t.done=!1,b)},inflateSetDictionary:(e,i)=>{const n=i.length;let a,r,o;return I(e)?_:0!==(a=e.state).wrap&&a.mode!==E?_:a.mode===E&&(r=t(r=1,i,n,0))!==a.check?g:(o=j(e,i,n,n))?(a.mode=16210,p):(a.havedict=1,b)},inflateInfo:"pako inflate (from Nodeca project)"};const P=(e,t)=>Object.prototype.hasOwnProperty.call(e,t);var Y=function(e){const t=Array.prototype.slice.call(arguments,1);for(;t.length;){const i=t.shift();if(i){if("object"!=typeof i)throw new TypeError(i+"must be non-object");for(const t in i)P(i,t)&&(e[t]=i[t])}}return e};let G=!0;try{String.fromCharCode.apply(null,new Uint8Array(1))}catch(e){G=!1}const X=new Uint8Array(256);for(let ke=0;ke<256;ke++)X[ke]=ke>=252?6:ke>=248?5:ke>=240?4:ke>=224?3:ke>=192?2:1;X[254]=X[254]=1;var W=e=>{if("function"==typeof TextEncoder&&TextEncoder.prototype.encode)return(new TextEncoder).encode(e);let t,i,n,a,r,o=e.length,s=0;for(a=0;a>>6,t[r++]=128|63&i):i<65536?(t[r++]=224|i>>>12,t[r++]=128|i>>>6&63,t[r++]=128|63&i):(t[r++]=240|i>>>18,t[r++]=128|i>>>12&63,t[r++]=128|i>>>6&63,t[r++]=128|63&i);return t},q=(e,t)=>{const i=t||e.length;if("function"==typeof TextDecoder&&TextDecoder.prototype.decode)return(new TextDecoder).decode(e.subarray(0,t));let n,a;const r=new Array(2*i);for(a=0,n=0;n4)r[a++]=65533,n+=o-1;else{for(t&=2===o?31:3===o?15:7;o>1&&n1?r[a++]=65533:t<65536?r[a++]=t:(t-=65536,r[a++]=55296|t>>10&1023,r[a++]=56320|1023&t)}}return((e,t)=>{if(t<65534&&e.subarray&&G)return String.fromCharCode.apply(null,e.length===t?e:e.subarray(0,t));let i="";for(let n=0;n{(t=t||e.length)>e.length&&(t=e.length);let i=t-1;for(;i>=0&&128==(192&e[i]);)i--;return i<0||0===i?t:i+X[e[i]]>t?i:t},Q={2:"need dictionary",1:"stream end",0:"","-1":"file error","-2":"stream error","-3":"data error","-4":"insufficient memory","-5":"buffer error","-6":"incompatible version"},V=function(){this.input=null,this.next_in=0,this.avail_in=0,this.total_in=0,this.output=null,this.next_out=0,this.avail_out=0,this.total_out=0,this.msg="",this.state=null,this.data_type=2,this.adler=0},$=function(){this.text=0,this.time=0,this.xflags=0,this.os=0,this.extra=null,this.extra_len=0,this.name="",this.comment="",this.hcrc=0,this.done=!1};const ee=Object.prototype.toString,{Z_NO_FLUSH:te,Z_FINISH:ie,Z_OK:ne,Z_STREAM_END:ae,Z_NEED_DICT:re,Z_STREAM_ERROR:oe,Z_DATA_ERROR:se,Z_MEM_ERROR:le}=c;function de(e){this.options=Y({chunkSize:65536,windowBits:15,to:""},e||{});const t=this.options;t.raw&&t.windowBits>=0&&t.windowBits<16&&(t.windowBits=-t.windowBits,0===t.windowBits&&(t.windowBits=-15)),!(t.windowBits>=0&&t.windowBits<16)||e&&e.windowBits||(t.windowBits+=32),t.windowBits>15&&t.windowBits<48&&0==(15&t.windowBits)&&(t.windowBits|=15),this.err=0,this.msg="",this.ended=!1,this.chunks=[],this.strm=new V,this.strm.avail_out=0;let i=K.inflateInit2(this.strm,t.windowBits);if(i!==ne)throw new Error(Q[i]);if(this.header=new $,K.inflateGetHeader(this.strm,this.header),t.dictionary&&("string"==typeof t.dictionary?t.dictionary=W(t.dictionary):"[object ArrayBuffer]"===ee.call(t.dictionary)&&(t.dictionary=new Uint8Array(t.dictionary)),t.raw&&(i=K.inflateSetDictionary(this.strm,t.dictionary))!==ne))throw new Error(Q[i])}function fe(e,t){const i=new de(t);if(i.push(e),i.err)throw i.msg||Q[i.err];return i.result}de.prototype.push=function(e,t){const i=this.strm,n=this.options.chunkSize,a=this.options.dictionary;let r,o,s;if(this.ended)return!1;for(o=t===~~t?t:!0===t?ie:te,"[object ArrayBuffer]"===ee.call(e)?i.input=new Uint8Array(e):i.input=e,i.next_in=0,i.avail_in=i.input.length;;){for(0===i.avail_out&&(i.output=new Uint8Array(n),i.next_out=0,i.avail_out=n),(r=K.inflate(i,o))===re&&a&&((r=K.inflateSetDictionary(i,a))===ne?r=K.inflate(i,o):r===se&&(r=re));i.avail_in>0&&r===ae&&i.state.wrap>0&&0!==e[i.next_in];)K.inflateReset(i),r=K.inflate(i,o);switch(r){case oe:case se:case re:case le:return this.onEnd(r),this.ended=!0,!1}if(s=i.avail_out,i.next_out&&(0===i.avail_out||r===ae))if("string"===this.options.to){let e=J(i.output,i.next_out),t=i.next_out-e,a=q(i.output,e);i.next_out=t,i.avail_out=n-t,t&&i.output.set(i.output.subarray(e,e+t),0),this.onData(a)}else this.onData(i.output.length===i.next_out?i.output:i.output.subarray(0,i.next_out));if(r!==ne||0!==s){if(r===ae)return r=K.inflateEnd(this.strm),this.onEnd(r),this.ended=!0,!0;if(0===i.avail_in)break}}return!0},de.prototype.onData=function(e){this.chunks.push(e)},de.prototype.onEnd=function(e){e===ne&&("string"===this.options.to?this.result=this.chunks.join(""):this.result=(e=>{let t=0;for(let n=0,a=e.length;n{if("undefined"!=typeof global);else if("undefined"!=typeof window)window.global=window;else if("undefined"!=typeof self)self.global=self;else throw Error("cannot export Go (neither global, window nor self is defined)");global.require||"undefined"==typeof require||(global.require=require),!global.fs&&global.require&&(global.fs=require("fs"));let e=()=>{let e=Error("not implemented");return e.code="ENOSYS",e};if(!global.fs){let t="";global.fs={constants:{O_WRONLY:-1,O_RDWR:-1,O_CREAT:-1,O_TRUNC:-1,O_APPEND:-1,O_EXCL:-1},writeSync(e,s){t+=i.decode(s);let n=t.lastIndexOf("\n");return -1!=n&&(console.log(t.substr(0,n)),t=t.substr(n+1)),s.length},write(t,s,n,i,r,l){if(0!==n||i!==s.length||null!==r){l(e());return}let o=this.writeSync(t,s);l(null,o)},chmod(t,s,n){n(e())},chown(t,s,n,i){i(e())},close(t,s){s(e())},fchmod(t,s,n){n(e())},fchown(t,s,n,i){i(e())},fstat(t,s){s(e())},fsync(e,t){t(null)},ftruncate(t,s,n){n(e())},lchown(t,s,n,i){i(e())},link(t,s,n){n(e())},lstat(t,s){s(e())},mkdir(t,s,n){n(e())},open(t,s,n,i){i(e())},read(t,s,n,i,r,l){l(e())},readdir(t,s){s(e())},readlink(t,s){s(e())},rename(t,s,n){n(e())},rmdir(t,s){s(e())},stat(t,s){s(e())},symlink(t,s,n){n(e())},truncate(t,s,n){n(e())},unlink(t,s){s(e())},utimes(t,s,n,i){i(e())}}}if(global.process||(global.process={getuid:()=>-1,getgid:()=>-1,geteuid:()=>-1,getegid:()=>-1,getgroups(){throw e()},pid:-1,ppid:-1,umask(){throw e()},cwd(){throw e()},chdir(){throw e()}}),!global.crypto){let s=require("crypto");global.crypto={getRandomValues(e){s.randomFillSync(e)}}}global.performance||(global.performance={now(){let[e,t]=process.hrtime();return 1e3*e+t/1e6}}),global.TextEncoder||(global.TextEncoder=require("util").TextEncoder),global.TextDecoder||(global.TextDecoder=require("util").TextDecoder);let n=new TextEncoder("utf-8"),i=new TextDecoder("utf-8"),r=new DataView(new ArrayBuffer(8));var l=[];global.WasmLoader=class{constructor(){this._callbackTimeouts=new Map,this._nextCallbackTimeoutID=1;let e=()=>new DataView(this._inst.exports.memory.buffer),t=e=>{r.setBigInt64(0,e,!0);let t=r.getFloat64(0,!0);if(0===t)return;if(!isNaN(t))return t;let s=4294967295n&e;return this._values[s]},s=s=>{let n=e().getBigUint64(s,!0);return t(n)},o=e=>{if("number"==typeof e)return isNaN(e)?2146959360n<<32n:0===e?2146959360n<<32n|1n:(r.setFloat64(0,e,!0),r.getBigInt64(0,!0));switch(e){case void 0:return 0n;case null:return 2146959360n<<32n|2n;case!0:return 2146959360n<<32n|3n;case!1:return 2146959360n<<32n|4n}let t=this._ids.get(e);void 0===t&&(void 0===(t=this._idPool.pop())&&(t=BigInt(this._values.length)),this._values[t]=e,this._goRefCounts[t]=0,this._ids.set(e,t)),this._goRefCounts[t]++;let s=1n;switch(typeof e){case"string":s=2n;break;case"symbol":s=3n;break;case"function":s=4n}return t|(2146959360n|s)<<32n},a=(t,s)=>{let n=o(s);e().setBigUint64(t,n,!0)},c=(e,t,s)=>new Uint8Array(this._inst.exports.memory.buffer,e,t),u=(e,t,n)=>{let i=Array(t);for(let r=0;ri.decode(new DataView(this._inst.exports.memory.buffer,e,t)),f=Date.now()-performance.now();this.importObject={wasi_snapshot_preview1:{fd_write:function(t,s,n,r){let o=0;if(1==t)for(let a=0;a0,fd_fdstat_get:()=>0,fd_seek:()=>0,proc_exit(e){if(global.process)process.exit(e);else throw"trying to exit with code "+e},random_get:(e,t)=>(crypto.getRandomValues(c(e,t)),0)},gojs:{"runtime.ticks":()=>f+performance.now(),"runtime.sleepTicks":e=>{setTimeout(this._inst.exports.go_scheduler,e)},"syscall/js.finalizeRef"(e){},"syscall/js.stringVal"(e,t){let s=$(e,t);return o(s)},"syscall/js.valueGet"(e,s,n){let i=$(s,n),r=t(e),l=Reflect.get(r,i);return o(l)},"syscall/js.valueSet"(e,s,n,i){let r=t(e),l=$(s,n),o=t(i);Reflect.set(r,l,o)},"syscall/js.valueDelete"(e,s,n){let i=t(e),r=$(s,n);Reflect.deleteProperty(i,r)},"syscall/js.valueIndex":(e,s)=>o(Reflect.get(t(e),s)),"syscall/js.valueSetIndex"(e,s,n){Reflect.set(t(e),s,t(n))},"syscall/js.valueCall"(s,n,i,r,l,o,c){let f=t(n),d=$(i,r),h=u(l,o,c);try{let g=Reflect.get(f,d);a(s,Reflect.apply(g,f,h)),e().setUint8(s+8,1)}catch(p){a(s,p),e().setUint8(s+8,0)}},"syscall/js.valueInvoke"(s,n,i,r,l){try{let o=t(n),c=u(i,r,l);a(s,Reflect.apply(o,void 0,c)),e().setUint8(s+8,1)}catch($){a(s,$),e().setUint8(s+8,0)}},"syscall/js.valueNew"(s,n,i,r,l){let o=t(n),c=u(i,r,l);try{a(s,Reflect.construct(o,c)),e().setUint8(s+8,1)}catch($){a(s,$),e().setUint8(s+8,0)}},"syscall/js.valueLength":e=>t(e).length,"syscall/js.valuePrepareString"(s,i){let r=String(t(i)),l=n.encode(r);a(s,l),e().setInt32(s+8,l.length,!0)},"syscall/js.valueLoadString"(e,s,n,i){let r=t(e);c(s,n,i).set(r)},"syscall/js.valueInstanceOf":(e,s)=>t(e) instanceof t(s),"syscall/js.copyBytesToGo"(s,n,i,r,l){let o=s+4,a=c(n,i),u=t(l);if(!(u instanceof Uint8Array||u instanceof Uint8ClampedArray)){e().setUint8(o,0);return}let $=u.subarray(0,a.length);a.set($),e().setUint32(s,$.length,!0),e().setUint8(o,1)},"syscall/js.copyBytesToJS"(s,n,i,r,l){let o=s+4,a=t(n),u=c(i,r);if(!(a instanceof Uint8Array||a instanceof Uint8ClampedArray)){e().setUint8(o,0);return}let $=u.subarray(0,a.length);a.set($),e().setUint32(s,$.length,!0),e().setUint8(o,1)}}},this.importObject.env=this.importObject.gojs}async run(e){for(this._inst=e,this._values=[NaN,0,null,!0,!1,global,this,],this._goRefCounts=[],this._ids=new Map,this._idPool=[],this.exited=!1,new DataView(this._inst.exports.memory.buffer);;){let t=new Promise(e=>{this._resolveCallbackPromise=()=>{if(this.exited)throw Error("bad callback: Go program has already exited");setTimeout(e,0)}});if(this._inst.exports._start(),this.exited)break;await t}}_resume(){if(this.exited)throw Error("Go program has already exited");this._inst.exports.resume(),this.exited&&this._resolveExitPromise()}_makeFuncWrapper(e){let t=this;return function(){let s={id:e,this:this,args:arguments};return t._pendingEvent=s,t._resume(),s.result}}}})(); --------------------------------------------------------------------------------