quickwin 1.0.2 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.en.md +204 -204
  2. package/README.md +203 -203
  3. package/examples/PdfCanvas.tsx +83 -83
  4. package/examples/exec_server.ts +35 -35
  5. package/examples/pdf_preview2.ts +375 -375
  6. package/examples/pdf_viewer.tsx +279 -279
  7. package/examples/test_gallery.tsx +285 -285
  8. package/examples/test_react_lazy.tsx +63 -63
  9. package/examples/test_react_listbox.tsx +71 -71
  10. package/examples/test_root_window.tsx +41 -41
  11. package/examples/test_tab.tsx +75 -75
  12. package/examples/tray_demo.tsx +81 -81
  13. package/lib/assert.ts +6 -6
  14. package/lib/cache-utils.ts +40 -40
  15. package/lib/dumpRects.ts +25 -25
  16. package/lib/fetch.ts +502 -502
  17. package/lib/http-common.ts +399 -399
  18. package/lib/http-server.ts +424 -424
  19. package/lib/polyfill.ts +171 -171
  20. package/lib/react-qw/components/index.ts +17 -17
  21. package/lib/react-qw/index.ts +90 -90
  22. package/lib/react-qw/jsx.d.ts +34 -34
  23. package/lib/react-qw/layout.ts +111 -111
  24. package/lib/react-qw/props.ts +36 -36
  25. package/lib/react-qw/reconciler.ts +373 -373
  26. package/lib/stream.ts +21 -21
  27. package/lib/text-codec.ts +171 -171
  28. package/lib/text-measure.ts +54 -54
  29. package/lib/url.ts +201 -201
  30. package/lib/vendor/web-streams/ponyfill.d.mts +829 -829
  31. package/lib/vendor/web-streams/ponyfill.mjs +8 -8
  32. package/lib/websocket.js +8 -0
  33. package/lib/websocket.ts +563 -555
  34. package/package.json +50 -44
  35. package/qwin-nowasm-x86.exe +0 -0
  36. package/qwin-nowasm.exe +0 -0
  37. package/qwin-x86.exe +0 -0
  38. package/qwin.exe +0 -0
  39. package/test/run.ts +96 -96
  40. package/test/test_basic.ts +9 -9
  41. package/test/test_basic_fetch.ts +33 -33
  42. package/test/test_brotli.ts +52 -52
  43. package/test/test_fetch_cache.ts +187 -187
  44. package/test/test_ffi.ts +174 -174
  45. package/test/test_frame_encoding.ts +132 -132
  46. package/test/test_helper.ts +80 -80
  47. package/test/test_http_import.ts +74 -74
  48. package/test/test_http_server.ts +136 -136
  49. package/test/test_import_dir.ts +5 -5
  50. package/test/test_ipv6.ts +129 -129
  51. package/test/test_meta_url.ts +4 -4
  52. package/test/test_mupdf_render.ts +74 -74
  53. package/test/test_mupdf_twice.ts +81 -81
  54. package/test/test_mupdf_wasm.ts +30 -30
  55. package/test/test_net_event.ts +55 -55
  56. package/test/test_net_fetch.ts +238 -238
  57. package/test/test_net_websocket.ts +164 -164
  58. package/test/test_polyfill.ts +159 -159
  59. package/test/test_url.ts +183 -183
  60. package/test/test_wasm_basic.ts +73 -73
  61. package/test/test_wasm_import_global.ts +45 -45
  62. package/test/test_wasm_sjlj.ts +125 -125
  63. package/test/test_wasm_types.ts +124 -124
  64. package/test/test_worker.ts +24 -24
  65. package/test/test_worker_fetch.ts +27 -27
  66. package/test/test_worker_net.ts +54 -54
  67. package/test/test_worker_wasm_concurrent.ts +45 -45
  68. package/test/worker_fetch.ts +13 -13
  69. package/test/worker_helper.ts +1 -1
  70. package/test/worker_http.ts +20 -20
  71. package/test/worker_import_lib.ts +13 -13
  72. package/test/worker_module.ts +17 -17
  73. package/test/worker_wasm_concurrent.ts +22 -22
  74. package/tsconfig.json +27 -27
  75. package/vendor/mupdf-wasm/mupdf-wasm.d.ts +571 -571
  76. package/vendor/mupdf-wasm/mupdf-wasm.js +2749 -2749
  77. package/vendor/mupdf-wasm/mupdf.d.ts +939 -939
  78. package/vendor/mupdf-wasm/mupdf.js +3317 -3317
package/lib/websocket.ts CHANGED
@@ -1,555 +1,563 @@
1
- import '../lib/polyfill.js'
2
- import * as sock from 'sock'
3
- import * as wolfssl from 'wolfssl'
4
- import * as os from 'os'
5
-
6
- // ── Constants ──
7
-
8
- const MAGIC_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
9
-
10
- enum State {
11
- CONNECTING = 0,
12
- OPEN = 1,
13
- CLOSING = 2,
14
- CLOSED = 3,
15
- }
16
-
17
- enum Opcode {
18
- CONTINUATION = 0x0,
19
- TEXT = 0x1,
20
- BINARY = 0x2,
21
- CLOSE = 0x8,
22
- PING = 0x9,
23
- PONG = 0xA,
24
- }
25
-
26
- // ── Random key generation ──
27
-
28
- function generateKey(): string {
29
- const bytes = new Uint8Array(16)
30
- crypto.getRandomValues(bytes)
31
- return btoa(String.fromCharCode(...bytes))
32
- }
33
-
34
- async function computeAccept(key: string): Promise<string> {
35
- const data = new TextEncoder().encode(key + MAGIC_GUID)
36
- const hash = await crypto.subtle.digest('SHA-1', data)
37
- const hashBytes = new Uint8Array(hash)
38
- return btoa(String.fromCharCode(...hashBytes))
39
- }
40
-
41
- // ── Frame helpers ──
42
-
43
- function createFrame(opcode: number, payload: Uint8Array): ArrayBuffer {
44
- const maskingKey = new Uint8Array(4)
45
- for (let i = 0; i < 4; i++) maskingKey[i] = (Math.random() * 256) | 0
46
-
47
- let headerSize: number
48
- if (payload.length < 126) {
49
- headerSize = 6
50
- } else if (payload.length < 65536) {
51
- headerSize = 8
52
- } else {
53
- headerSize = 14
54
- }
55
-
56
- const buf = new ArrayBuffer(headerSize + payload.length)
57
- const view = new Uint8Array(buf)
58
- let off = 0
59
-
60
- view[off++] = 0x80 | opcode
61
- view[off++] = 0x80 | (payload.length < 126 ? payload.length :
62
- payload.length < 65536 ? 126 : 127)
63
-
64
- if (payload.length >= 126 && payload.length < 65536) {
65
- view[off++] = (payload.length >> 8) & 0xFF
66
- view[off++] = payload.length & 0xFF
67
- } else if (payload.length >= 65536) {
68
- const len = payload.length
69
- const hi = (len / 0x100000000) >>> 0
70
- const lo = len >>> 0
71
- view[off++] = (hi >> 24) & 0xFF
72
- view[off++] = (hi >> 16) & 0xFF
73
- view[off++] = (hi >> 8) & 0xFF
74
- view[off++] = hi & 0xFF
75
- view[off++] = (lo >> 24) & 0xFF
76
- view[off++] = (lo >> 16) & 0xFF
77
- view[off++] = (lo >> 8) & 0xFF
78
- view[off++] = lo & 0xFF
79
- }
80
-
81
- view[off++] = maskingKey[0]!
82
- view[off++] = maskingKey[1]!
83
- view[off++] = maskingKey[2]!
84
- view[off++] = maskingKey[3]!
85
-
86
- for (let i = 0; i < payload.length; i++) {
87
- view[off++] = payload[i]! ^ maskingKey[i % 4]!
88
- }
89
-
90
- return buf
91
- }
92
-
93
- function createCloseFrame(code: number, reason: string): ArrayBuffer {
94
- const reasonBytes = new Uint8Array([(code >> 8) & 0xFF, code & 0xFF, ...new TextEncoder().encode(reason)])
95
- return createFrame(Opcode.CLOSE, reasonBytes)
96
- }
97
-
98
- // ── Frame parser (returns null if incomplete) ──
99
-
100
- interface ParsedFrame {
101
- opcode: number
102
- payload: Uint8Array
103
- fin: boolean
104
- totalLen: number
105
- }
106
-
107
- function tryParseFrame(buffer: Uint8Array): ParsedFrame | null {
108
- if (buffer.length < 2) return null
109
-
110
- const b0 = buffer[0]!
111
- const b1 = buffer[1]!
112
- const fin = (b0 & 0x80) !== 0
113
- const opcode = b0 & 0x0F
114
- const masked = (b1 & 0x80) !== 0
115
- let payloadLen = b1 & 0x7F
116
- let offset = 2
117
-
118
- if (payloadLen === 126) {
119
- if (buffer.length < 4) return null
120
- payloadLen = (buffer[2]! << 8) | buffer[3]!
121
- offset = 4
122
- } else if (payloadLen === 127) {
123
- if (buffer.length < 10) return null
124
- let hi = 0, lo = 0
125
- for (let i = 0; i < 4; i++) hi = (hi * 256 + buffer[offset + i]!) >>> 0
126
- for (let i = 4; i < 8; i++) lo = (lo * 256 + buffer[offset + i]!) >>> 0
127
- payloadLen = hi * 0x100000000 + lo
128
- offset = 10
129
- }
130
-
131
- if (masked) offset += 4
132
-
133
- const totalLen = offset + payloadLen
134
- if (buffer.length < totalLen) return null
135
-
136
- let payload: Uint8Array
137
- if (masked) {
138
- const maskKey = buffer.slice(offset - 4, offset)
139
- payload = new Uint8Array(payloadLen)
140
- for (let i = 0; i < payloadLen; i++) {
141
- payload[i] = buffer[offset + i]! ^ maskKey[i % 4]!
142
- }
143
- } else {
144
- payload = buffer.slice(offset, offset + payloadLen)
145
- }
146
-
147
- return { opcode, payload, fin, totalLen }
148
- }
149
-
150
- // ── WebSocket class ──
151
-
152
- interface WebSocketOptions {
153
- headers?: Record<string, string>
154
- }
155
-
156
-
157
-
158
- class WebSocketImpl {
159
- readonly url: string
160
- readyState: State = State.CONNECTING
161
- onopen: ((event: Event) => void) | null = null
162
- onclose: ((event: CloseEvent) => void) | null = null
163
- onerror: ((event: Event) => void) | null = null
164
- onmessage: ((event: MessageEvent) => void) | null = null
165
-
166
- private _options: WebSocketOptions
167
- private _sock: sock.SockHandle | null = null
168
- private _ssl: wolfssl.WOLFSSL | null = null
169
- private _ctx: wolfssl.WOLFSSL_CTX | null = null
170
- private _readBuffer: Uint8Array = new Uint8Array(0)
171
- private _state: State = State.CONNECTING
172
- private _processingHandshake: boolean = false
173
- static readonly CONNECTING = State.CONNECTING
174
- static readonly OPEN = State.OPEN
175
- static readonly CLOSING = State.CLOSING
176
- static readonly CLOSED = State.CLOSED
177
-
178
- private _closeCode: number = 1005
179
- private _closeReason: string = ''
180
-
181
- constructor(url: string, options?: WebSocketOptions) {
182
- this.url = url
183
- this._options = options || {}
184
- this._connect()
185
- }
186
-
187
- send(data: string | ArrayBuffer | Uint8Array): void {
188
- if (this._state !== State.OPEN) return
189
-
190
- let payload: Uint8Array
191
- if (typeof data === 'string') {
192
- payload = new TextEncoder().encode(data)
193
- const frame = createFrame(Opcode.TEXT, payload)
194
- this._sendRaw(frame)
195
- } else if (data instanceof ArrayBuffer) {
196
- payload = new Uint8Array(data)
197
- const frame = createFrame(Opcode.BINARY, payload)
198
- this._sendRaw(frame)
199
- } else if (data instanceof Uint8Array) {
200
- const frame = createFrame(Opcode.BINARY, data)
201
- this._sendRaw(frame)
202
- }
203
- }
204
-
205
- close(code?: number, reason?: string): void {
206
- if (this._state === State.CLOSED || this._state === State.CLOSING) return
207
- this._state = State.CLOSING
208
-
209
- const closeCode = code || 1000
210
- const closeReason = reason || ''
211
- this._closeCode = closeCode
212
- this._closeReason = closeReason
213
- const frame = createCloseFrame(closeCode, closeReason)
214
- this._sendRaw(frame)
215
-
216
- this._cleanup()
217
- this._setState(State.CLOSED)
218
- }
219
-
220
- private _connect(): void {
221
- let url: URL
222
- try { url = new URL(this.url) } catch {
223
- const self = this
224
- os.setTimeout(() => {
225
- self._fireError(new Error('Invalid WebSocket URL: ' + self.url))
226
- self._setState(State.CLOSED)
227
- }, 0)
228
- return
229
- }
230
- const host = url.hostname
231
- const hostHeader = host.includes(':') ? `[${host}]` : host
232
- const isWSS = url.protocol === 'wss:'
233
- const port = url.port ? parseInt(url.port, 10) : (isWSS ? 443 : 80)
234
- const path = url.pathname + (url.search || '')
235
-
236
- const requestKey = generateKey()
237
-
238
- const customHeadersList: string[] = []
239
- let hasUserAgent = false
240
- if (this._options.headers) {
241
- for (const name in this._options.headers) {
242
- const value = this._options.headers[name]
243
- customHeadersList.push(name + ': ' + value + '\r\n')
244
- if (name.toLowerCase() === 'user-agent') hasUserAgent = true
245
- }
246
- }
247
- const customHeaders = customHeadersList.join('')
248
-
249
- const request = (
250
- 'GET ' + path + ' HTTP/1.1\r\n' +
251
- 'Host: ' + hostHeader + ':' + port + '\r\n' +
252
- 'Upgrade: websocket\r\n' +
253
- 'Connection: Upgrade\r\n' +
254
- 'Sec-WebSocket-Key: ' + requestKey + '\r\n' +
255
- 'Sec-WebSocket-Version: 13\r\n' +
256
- (hasUserAgent ? '' : 'User-Agent: ' + navigator.userAgent + '\r\n') +
257
- customHeaders +
258
- '\r\n'
259
- )
260
-
261
- let state: 'resolve' | 'connect' | 'handshake' | 'request' | 'open' = 'resolve'
262
- let headerBuffer = ''
263
- let resolved = false
264
-
265
- const cleanup = (): void => {
266
- if (this._ssl) { wolfssl.wolfSSL_free(this._ssl); this._ssl = null }
267
- if (this._ctx) { wolfssl.wolfSSL_CTX_free(this._ctx); this._ctx = null }
268
- if (this._sock !== null && this._sock >= 0) { sock.closesocket(this._sock); this._sock = null }
269
- }
270
-
271
- const doError = (err: Error): void => {
272
- if (resolved) return
273
- resolved = true
274
- cleanup()
275
- this._fireError(err)
276
- if (this._state !== State.CLOSED) this._setState(State.CLOSED)
277
- }
278
-
279
- this._sock = sock.socket()
280
- if (this._sock < 0) {
281
- const self = this
282
- os.setTimeout(() => {
283
- self._fireError(new Error('Failed to create socket'))
284
- if (self._state !== State.CLOSED) self._setState(State.CLOSED)
285
- }, 0)
286
- return
287
- }
288
- const fd = this._sock
289
-
290
- sock.set_on_event(fd, async (event: { lNetworkEvents: number; iErrorCode: number[] }) => {
291
- if (this._processingHandshake) return
292
- if (resolved && state === 'open') {
293
- if (event.lNetworkEvents & sock.FdEvent.FD_READ) {
294
- this._onData()
295
- }
296
- if (event.lNetworkEvents & sock.FdEvent.FD_CLOSE) {
297
- this._cleanup()
298
- this._setState(State.CLOSED)
299
- }
300
- return
301
- }
302
-
303
- if (event.lNetworkEvents & sock.FdEvent.FD_CONNECT) {
304
- const errCode = event.iErrorCode[0]
305
- if (errCode !== 0) {
306
- doError(new Error('Connection failed: ' + errCode))
307
- return
308
- }
309
- if (isWSS) {
310
- const method = wolfssl.wolfTLSv1_2_client_method()
311
- this._ctx = wolfssl.wolfSSL_CTX_new(method)
312
- if (!this._ctx) { doError(new Error('SSL_CTX_new failed')); return }
313
- wolfssl.wolfSSL_CTX_set_verify(this._ctx, wolfssl.VerifyMode.SSL_VERIFY_NONE)
314
- this._ssl = wolfssl.wolfSSL_new(this._ctx)
315
- if (!this._ssl) { doError(new Error('SSL_new failed')); return }
316
- wolfssl.wolfSSL_set_fd(this._ssl, sock.get_fd(fd))
317
- const sniHost = host
318
- if (sniHost) wolfssl.wolfSSL_UseSNI(this._ssl, wolfssl.SniType.WOLFSSL_SNI_HOST_NAME, sniHost)
319
- state = 'handshake'
320
- } else {
321
- this._sendRawStr(request)
322
- state = 'request'
323
- }
324
- }
325
-
326
- if ((event.lNetworkEvents & sock.FdEvent.FD_READ) || (event.lNetworkEvents & sock.FdEvent.FD_WRITE)) {
327
- if (state === 'handshake') {
328
- if (!this._ssl) { doError(new Error('TLS not initialized')); return }
329
- const ret = wolfssl.wolfSSL_connect(this._ssl)
330
- if (ret === wolfssl.ReturnCode.SSL_SUCCESS) {
331
- this._sendRawStr(request)
332
- state = 'request'
333
- } else {
334
- const err = wolfssl.wolfSSL_get_error(this._ssl, ret)
335
- if (err !== wolfssl.ErrorCode.WOLFSSL_ERROR_WANT_READ &&
336
- err !== wolfssl.ErrorCode.WOLFSSL_ERROR_WANT_WRITE) {
337
- doError(new Error('TLS handshake failed: ' + err))
338
- }
339
- }
340
- }
341
- else if (state === 'request') {
342
- while (true) {
343
- let data: ArrayBuffer | null
344
- if (isWSS && this._ssl) {
345
- data = wolfssl.wolfSSL_read(this._ssl, 8192)
346
- } else if (this._sock !== null && this._sock >= 0) {
347
- data = sock.recv(this._sock, 8192)
348
- } else { break }
349
- if (!data || data.byteLength === 0) break
350
- headerBuffer += _ab2str(data)
351
- const headerEnd = headerBuffer.indexOf('\r\n\r\n')
352
- if (headerEnd >= 0) {
353
- const headerPart = headerBuffer.slice(0, headerEnd)
354
- const lines = headerPart.split('\r\n')
355
- const statusLine = lines[0]!
356
- const statusParts = statusLine.split(' ')
357
- if (statusParts.length < 2) {
358
- doError(new Error('Invalid HTTP status line: ' + statusLine))
359
- return
360
- }
361
- const statusCode = parseInt(statusParts[1]!, 10)
362
- if (statusCode !== 101) {
363
- doError(new Error('WebSocket handshake failed: HTTP ' + statusCode))
364
- return
365
- }
366
-
367
- let acceptHeader = ''
368
- for (let i = 1; i < lines.length; i++) {
369
- const line = lines[i]!
370
- const colonIdx2 = line.indexOf(':')
371
- if (colonIdx2 >= 0) {
372
- const name = line.slice(0, colonIdx2).toLowerCase().trim()
373
- if (name === 'sec-websocket-accept') {
374
- acceptHeader = line.slice(colonIdx2 + 1).trim()
375
- break
376
- }
377
- }
378
- }
379
- if (!acceptHeader) {
380
- doError(new Error('WebSocket handshake missing Sec-WebSocket-Accept'))
381
- return
382
- }
383
-
384
- this._processingHandshake = true
385
- const expectedAccept = await computeAccept(requestKey)
386
- if (acceptHeader !== expectedAccept) {
387
- doError(new Error('WebSocket handshake invalid Sec-WebSocket-Accept'))
388
- this._processingHandshake = false
389
- return
390
- }
391
-
392
- state = 'open'
393
- resolved = true
394
- this._processingHandshake = false
395
- this._setState(State.OPEN)
396
- break
397
- }
398
- }
399
- }
400
- }
401
-
402
- if (event.lNetworkEvents & sock.FdEvent.FD_CLOSE) {
403
- if (state !== 'open') {
404
- doError(new Error('Connection closed'))
405
- }
406
- }
407
- })
408
-
409
- const ip = sock.resolve(host)
410
- if (!ip) {
411
- cleanup()
412
- const self = this
413
- os.setTimeout(() => {
414
- self._fireError(new Error('DNS resolution failed for: ' + host))
415
- if (self._state !== State.CLOSED) self._setState(State.CLOSED)
416
- }, 0)
417
- return
418
- }
419
- sock.connect(fd, ip, port)
420
- }
421
-
422
- private _sendRaw(buf: ArrayBuffer): void {
423
- if (this._sock === null || this._sock < 0) return
424
- if (this._ssl) {
425
- wolfssl.wolfSSL_write(this._ssl, buf)
426
- } else {
427
- sock.send(this._sock, buf)
428
- }
429
- }
430
-
431
- private _sendRawStr(str: string): void {
432
- const buf = new ArrayBuffer(str.length)
433
- const view = new Uint8Array(buf)
434
- for (let i = 0; i < str.length; i++) view[i] = str.charCodeAt(i) & 0xFF
435
- this._sendRaw(buf)
436
- }
437
-
438
- private _onData(): void {
439
- while (true) {
440
- let data: ArrayBuffer | null
441
- if (this._ssl) {
442
- data = wolfssl.wolfSSL_read(this._ssl, 8192)
443
- } else if (this._sock !== null && this._sock >= 0) {
444
- data = sock.recv(this._sock, 8192)
445
- } else { break }
446
- if (!data || data.byteLength === 0) break
447
-
448
- const newBuf = new Uint8Array(this._readBuffer.length + data.byteLength)
449
- newBuf.set(this._readBuffer)
450
- newBuf.set(new Uint8Array(data), this._readBuffer.length)
451
- this._readBuffer = newBuf
452
-
453
- while (true) {
454
- const frame = tryParseFrame(this._readBuffer)
455
- if (!frame) break
456
-
457
- this._readBuffer = this._readBuffer.slice(frame.totalLen)
458
-
459
- if (frame.opcode === Opcode.TEXT || frame.opcode === Opcode.BINARY) {
460
- const event = new MessageEvent('message', { data: frame.opcode === Opcode.TEXT ? _ab2str(frame.payload.slice(0).buffer) : frame.payload.slice(0).buffer })
461
- this._dispatchEvent('message', event)
462
- } else if (frame.opcode === Opcode.PING) {
463
- const pong = createFrame(Opcode.PONG, frame.payload)
464
- this._sendRaw(pong)
465
- } else if (frame.opcode === Opcode.PONG) {
466
- } else if (frame.opcode === Opcode.CLOSE) {
467
- if (frame.payload.length >= 2) {
468
- this._closeCode = (frame.payload[0]! << 8) | frame.payload[1]!
469
- this._closeReason = _ab2str(frame.payload.slice(2).buffer)
470
- }
471
- this._cleanup()
472
- this._setState(State.CLOSED)
473
- return
474
- }
475
- }
476
- }
477
- }
478
-
479
- private _setState(state: State): void {
480
- if (this._state === state) return
481
- this.readyState = state
482
- this._state = state
483
-
484
- if (state === State.OPEN) {
485
- this._dispatchEvent('open', new Event('open'))
486
- } else if (state === State.CLOSED) {
487
- const event = new CloseEvent('close', { code: this._closeCode, reason: this._closeReason, wasClean: true })
488
- this._dispatchEvent('close', event)
489
- }
490
- }
491
-
492
- private _dispatchEvent(type: string, event: Event | MessageEvent | CloseEvent): void {
493
- if (type === 'open' && this.onopen) this.onopen(event as Event)
494
- else if (type === 'close' && this.onclose) this.onclose(event as CloseEvent)
495
- else if (type === 'error' && this.onerror) this.onerror(event as Event)
496
- else if (type === 'message' && this.onmessage) this.onmessage(event as MessageEvent)
497
- }
498
-
499
- private _fireError(err: Error): void {
500
- console.error('WebSocket error:', err.message)
501
- this._dispatchEvent('error', new Event('error'))
502
- }
503
-
504
- private _cleanup(): void {
505
- if (this._ssl) { wolfssl.wolfSSL_free(this._ssl); this._ssl = null }
506
- if (this._ctx) { wolfssl.wolfSSL_CTX_free(this._ctx); this._ctx = null }
507
- if (this._sock !== null && this._sock >= 0) { sock.closesocket(this._sock); this._sock = null }
508
- }
509
- }
510
-
511
- // ── Event/MessageEvent/CloseEvent polyfills ──
512
-
513
- class Event {
514
- readonly type: string
515
- constructor(type: string, _init?: unknown) { this.type = type }
516
- }
517
-
518
- class MessageEvent {
519
- readonly type: string
520
- readonly data: unknown
521
- constructor(type: string, init: { data: unknown }) {
522
- this.type = type
523
- this.data = init.data
524
- }
525
- }
526
-
527
- class CloseEvent {
528
- readonly type: string
529
- readonly code: number
530
- readonly reason: string
531
- readonly wasClean: boolean
532
- constructor(type: string, init: { code?: number; reason?: string; wasClean?: boolean }) {
533
- this.type = type
534
- this.code = init.code || 0
535
- this.reason = init.reason || ''
536
- this.wasClean = init.wasClean || false
537
- }
538
- }
539
-
540
- // ── Utility ──
541
-
542
- function _ab2str(buf: ArrayBuffer): string {
543
- return new TextDecoder('utf-8').decode(buf)
544
- }
545
-
546
- // ── Global declarations ──
547
-
548
- declare global {
549
- interface WebSocket extends WebSocketImpl {}
550
- var WebSocket: typeof WebSocketImpl
551
- }
552
-
553
- // ── Register globals ──
554
-
555
- globalThis.WebSocket = WebSocketImpl
1
+ import '../lib/polyfill.js'
2
+ import * as sock from 'sock'
3
+ import * as wolfssl from 'wolfssl'
4
+ import * as os from 'os'
5
+
6
+ // ── Constants ──
7
+
8
+ const MAGIC_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
9
+
10
+ enum State {
11
+ CONNECTING = 0,
12
+ OPEN = 1,
13
+ CLOSING = 2,
14
+ CLOSED = 3,
15
+ }
16
+
17
+ enum Opcode {
18
+ CONTINUATION = 0x0,
19
+ TEXT = 0x1,
20
+ BINARY = 0x2,
21
+ CLOSE = 0x8,
22
+ PING = 0x9,
23
+ PONG = 0xA,
24
+ }
25
+
26
+ // ── Random key generation ──
27
+
28
+ function generateKey(): string {
29
+ const bytes = new Uint8Array(16)
30
+ crypto.getRandomValues(bytes)
31
+ return btoa(String.fromCharCode(...bytes))
32
+ }
33
+
34
+ async function computeAccept(key: string): Promise<string> {
35
+ const data = new TextEncoder().encode(key + MAGIC_GUID)
36
+ const hash = await crypto.subtle.digest('SHA-1', data)
37
+ const hashBytes = new Uint8Array(hash)
38
+ return btoa(String.fromCharCode(...hashBytes))
39
+ }
40
+
41
+ // ── Frame helpers ──
42
+
43
+ function createFrame(opcode: number, payload: Uint8Array): ArrayBuffer {
44
+ const maskingKey = new Uint8Array(4)
45
+ for (let i = 0; i < 4; i++) maskingKey[i] = (Math.random() * 256) | 0
46
+
47
+ let headerSize: number
48
+ if (payload.length < 126) {
49
+ headerSize = 6
50
+ } else if (payload.length < 65536) {
51
+ headerSize = 8
52
+ } else {
53
+ headerSize = 14
54
+ }
55
+
56
+ const buf = new ArrayBuffer(headerSize + payload.length)
57
+ const view = new Uint8Array(buf)
58
+ let off = 0
59
+
60
+ view[off++] = 0x80 | opcode
61
+ view[off++] = 0x80 | (payload.length < 126 ? payload.length :
62
+ payload.length < 65536 ? 126 : 127)
63
+
64
+ if (payload.length >= 126 && payload.length < 65536) {
65
+ view[off++] = (payload.length >> 8) & 0xFF
66
+ view[off++] = payload.length & 0xFF
67
+ } else if (payload.length >= 65536) {
68
+ const len = payload.length
69
+ const hi = (len / 0x100000000) >>> 0
70
+ const lo = len >>> 0
71
+ view[off++] = (hi >> 24) & 0xFF
72
+ view[off++] = (hi >> 16) & 0xFF
73
+ view[off++] = (hi >> 8) & 0xFF
74
+ view[off++] = hi & 0xFF
75
+ view[off++] = (lo >> 24) & 0xFF
76
+ view[off++] = (lo >> 16) & 0xFF
77
+ view[off++] = (lo >> 8) & 0xFF
78
+ view[off++] = lo & 0xFF
79
+ }
80
+
81
+ view[off++] = maskingKey[0]!
82
+ view[off++] = maskingKey[1]!
83
+ view[off++] = maskingKey[2]!
84
+ view[off++] = maskingKey[3]!
85
+
86
+ for (let i = 0; i < payload.length; i++) {
87
+ view[off++] = payload[i]! ^ maskingKey[i % 4]!
88
+ }
89
+
90
+ return buf
91
+ }
92
+
93
+ function createCloseFrame(code: number, reason: string): ArrayBuffer {
94
+ const reasonBytes = new Uint8Array([(code >> 8) & 0xFF, code & 0xFF, ...new TextEncoder().encode(reason)])
95
+ return createFrame(Opcode.CLOSE, reasonBytes)
96
+ }
97
+
98
+ // ── Frame parser (returns null if incomplete) ──
99
+
100
+ interface ParsedFrame {
101
+ opcode: number
102
+ payload: Uint8Array
103
+ fin: boolean
104
+ totalLen: number
105
+ }
106
+
107
+ function tryParseFrame(buffer: Uint8Array): ParsedFrame | null {
108
+ if (buffer.length < 2) return null
109
+
110
+ const b0 = buffer[0]!
111
+ const b1 = buffer[1]!
112
+ const fin = (b0 & 0x80) !== 0
113
+ const opcode = b0 & 0x0F
114
+ const masked = (b1 & 0x80) !== 0
115
+ let payloadLen = b1 & 0x7F
116
+ let offset = 2
117
+
118
+ if (payloadLen === 126) {
119
+ if (buffer.length < 4) return null
120
+ payloadLen = (buffer[2]! << 8) | buffer[3]!
121
+ offset = 4
122
+ } else if (payloadLen === 127) {
123
+ if (buffer.length < 10) return null
124
+ let hi = 0, lo = 0
125
+ for (let i = 0; i < 4; i++) hi = (hi * 256 + buffer[offset + i]!) >>> 0
126
+ for (let i = 4; i < 8; i++) lo = (lo * 256 + buffer[offset + i]!) >>> 0
127
+ payloadLen = hi * 0x100000000 + lo
128
+ offset = 10
129
+ }
130
+
131
+ if (masked) offset += 4
132
+
133
+ const totalLen = offset + payloadLen
134
+ if (buffer.length < totalLen) return null
135
+
136
+ let payload: Uint8Array
137
+ if (masked) {
138
+ const maskKey = buffer.slice(offset - 4, offset)
139
+ payload = new Uint8Array(payloadLen)
140
+ for (let i = 0; i < payloadLen; i++) {
141
+ payload[i] = buffer[offset + i]! ^ maskKey[i % 4]!
142
+ }
143
+ } else {
144
+ payload = buffer.slice(offset, offset + payloadLen)
145
+ }
146
+
147
+ return { opcode, payload, fin, totalLen }
148
+ }
149
+
150
+ // ── WebSocket class ──
151
+
152
+ interface WebSocketOptions {
153
+ headers?: Record<string, string>
154
+ }
155
+
156
+
157
+
158
+ class WebSocketImpl {
159
+ readonly url: string
160
+ readyState: State = State.CONNECTING
161
+ onopen: ((event: Event) => void) | null = null
162
+ onclose: ((event: CloseEvent) => void) | null = null
163
+ onerror: ((event: Event) => void) | null = null
164
+ onmessage: ((event: MessageEvent) => void) | null = null
165
+
166
+ private _options: WebSocketOptions
167
+ private _sock: sock.SockHandle | null = null
168
+ private _ssl: wolfssl.WOLFSSL | null = null
169
+ private _ctx: wolfssl.WOLFSSL_CTX | null = null
170
+ private _readBuffer: Uint8Array = new Uint8Array(0)
171
+ private _state: State = State.CONNECTING
172
+ private _processingHandshake: boolean = false
173
+ static readonly CONNECTING = State.CONNECTING
174
+ static readonly OPEN = State.OPEN
175
+ static readonly CLOSING = State.CLOSING
176
+ static readonly CLOSED = State.CLOSED
177
+
178
+ private _closeCode: number = 1005
179
+ private _closeReason: string = ''
180
+
181
+ constructor(url: string, options?: WebSocketOptions) {
182
+ this.url = url
183
+ this._options = options || {}
184
+ this._connect()
185
+ }
186
+
187
+ send(data: string | ArrayBuffer | Uint8Array): void {
188
+ if (this._state !== State.OPEN) return
189
+
190
+ let payload: Uint8Array
191
+ if (typeof data === 'string') {
192
+ payload = new TextEncoder().encode(data)
193
+ const frame = createFrame(Opcode.TEXT, payload)
194
+ this._sendRaw(frame)
195
+ } else if (data instanceof ArrayBuffer) {
196
+ payload = new Uint8Array(data)
197
+ const frame = createFrame(Opcode.BINARY, payload)
198
+ this._sendRaw(frame)
199
+ } else if (data instanceof Uint8Array) {
200
+ const frame = createFrame(Opcode.BINARY, data)
201
+ this._sendRaw(frame)
202
+ }
203
+ }
204
+
205
+ close(code?: number, reason?: string): void {
206
+ if (this._state === State.CLOSED || this._state === State.CLOSING) return
207
+ this._state = State.CLOSING
208
+
209
+ const closeCode = code || 1000
210
+ const closeReason = reason || ''
211
+ this._closeCode = closeCode
212
+ this._closeReason = closeReason
213
+ const frame = createCloseFrame(closeCode, closeReason)
214
+ this._sendRaw(frame)
215
+
216
+ this._cleanup()
217
+ this._setState(State.CLOSED)
218
+ }
219
+
220
+ private _connect(): void {
221
+ let url: URL
222
+ try { url = new URL(this.url) } catch {
223
+ const self = this
224
+ os.setTimeout(() => {
225
+ self._fireError(new Error('Invalid WebSocket URL: ' + self.url))
226
+ self._setState(State.CLOSED)
227
+ }, 0)
228
+ return
229
+ }
230
+ if (url.protocol !== 'ws:' && url.protocol !== 'wss:') {
231
+ const self = this
232
+ os.setTimeout(() => {
233
+ self._fireError(new Error('Unsupported WebSocket scheme: ' + url.protocol))
234
+ self._setState(State.CLOSED)
235
+ }, 0)
236
+ return
237
+ }
238
+ const host = url.hostname
239
+ const hostHeader = host.includes(':') ? `[${host}]` : host
240
+ const isWSS = url.protocol === 'wss:'
241
+ const port = url.port ? parseInt(url.port, 10) : (isWSS ? 443 : 80)
242
+ const path = url.pathname + (url.search || '')
243
+
244
+ const requestKey = generateKey()
245
+
246
+ const customHeadersList: string[] = []
247
+ let hasUserAgent = false
248
+ if (this._options.headers) {
249
+ for (const name in this._options.headers) {
250
+ const value = this._options.headers[name]
251
+ customHeadersList.push(name + ': ' + value + '\r\n')
252
+ if (name.toLowerCase() === 'user-agent') hasUserAgent = true
253
+ }
254
+ }
255
+ const customHeaders = customHeadersList.join('')
256
+
257
+ const request = (
258
+ 'GET ' + path + ' HTTP/1.1\r\n' +
259
+ 'Host: ' + hostHeader + ':' + port + '\r\n' +
260
+ 'Upgrade: websocket\r\n' +
261
+ 'Connection: Upgrade\r\n' +
262
+ 'Sec-WebSocket-Key: ' + requestKey + '\r\n' +
263
+ 'Sec-WebSocket-Version: 13\r\n' +
264
+ (hasUserAgent ? '' : 'User-Agent: ' + navigator.userAgent + '\r\n') +
265
+ customHeaders +
266
+ '\r\n'
267
+ )
268
+
269
+ let state: 'resolve' | 'connect' | 'handshake' | 'request' | 'open' = 'resolve'
270
+ let headerBuffer = ''
271
+ let resolved = false
272
+
273
+ const cleanup = (): void => {
274
+ if (this._ssl) { wolfssl.wolfSSL_free(this._ssl); this._ssl = null }
275
+ if (this._ctx) { wolfssl.wolfSSL_CTX_free(this._ctx); this._ctx = null }
276
+ if (this._sock !== null && this._sock >= 0) { sock.closesocket(this._sock); this._sock = null }
277
+ }
278
+
279
+ const doError = (err: Error): void => {
280
+ if (resolved) return
281
+ resolved = true
282
+ cleanup()
283
+ this._fireError(err)
284
+ if (this._state !== State.CLOSED) this._setState(State.CLOSED)
285
+ }
286
+
287
+ this._sock = sock.socket()
288
+ if (this._sock < 0) {
289
+ const self = this
290
+ os.setTimeout(() => {
291
+ self._fireError(new Error('Failed to create socket'))
292
+ if (self._state !== State.CLOSED) self._setState(State.CLOSED)
293
+ }, 0)
294
+ return
295
+ }
296
+ const fd = this._sock
297
+
298
+ sock.set_on_event(fd, async (event: { lNetworkEvents: number; iErrorCode: number[] }) => {
299
+ if (this._processingHandshake) return
300
+ if (resolved && state === 'open') {
301
+ if (event.lNetworkEvents & sock.FdEvent.FD_READ) {
302
+ this._onData()
303
+ }
304
+ if (event.lNetworkEvents & sock.FdEvent.FD_CLOSE) {
305
+ this._cleanup()
306
+ this._setState(State.CLOSED)
307
+ }
308
+ return
309
+ }
310
+
311
+ if (event.lNetworkEvents & sock.FdEvent.FD_CONNECT) {
312
+ const errCode = event.iErrorCode[0]
313
+ if (errCode !== 0) {
314
+ doError(new Error('Connection failed: ' + errCode))
315
+ return
316
+ }
317
+ if (isWSS) {
318
+ const method = wolfssl.wolfTLSv1_2_client_method()
319
+ this._ctx = wolfssl.wolfSSL_CTX_new(method)
320
+ if (!this._ctx) { doError(new Error('SSL_CTX_new failed')); return }
321
+ wolfssl.wolfSSL_CTX_set_verify(this._ctx, wolfssl.VerifyMode.SSL_VERIFY_NONE)
322
+ this._ssl = wolfssl.wolfSSL_new(this._ctx)
323
+ if (!this._ssl) { doError(new Error('SSL_new failed')); return }
324
+ wolfssl.wolfSSL_set_fd(this._ssl, sock.get_fd(fd))
325
+ const sniHost = host
326
+ if (sniHost) wolfssl.wolfSSL_UseSNI(this._ssl, wolfssl.SniType.WOLFSSL_SNI_HOST_NAME, sniHost)
327
+ state = 'handshake'
328
+ } else {
329
+ this._sendRawStr(request)
330
+ state = 'request'
331
+ }
332
+ }
333
+
334
+ if ((event.lNetworkEvents & sock.FdEvent.FD_READ) || (event.lNetworkEvents & sock.FdEvent.FD_WRITE)) {
335
+ if (state === 'handshake') {
336
+ if (!this._ssl) { doError(new Error('TLS not initialized')); return }
337
+ const ret = wolfssl.wolfSSL_connect(this._ssl)
338
+ if (ret === wolfssl.ReturnCode.SSL_SUCCESS) {
339
+ this._sendRawStr(request)
340
+ state = 'request'
341
+ } else {
342
+ const err = wolfssl.wolfSSL_get_error(this._ssl, ret)
343
+ if (err !== wolfssl.ErrorCode.WOLFSSL_ERROR_WANT_READ &&
344
+ err !== wolfssl.ErrorCode.WOLFSSL_ERROR_WANT_WRITE) {
345
+ doError(new Error('TLS handshake failed: ' + err))
346
+ }
347
+ }
348
+ }
349
+ else if (state === 'request') {
350
+ while (true) {
351
+ let data: ArrayBuffer | null
352
+ if (isWSS && this._ssl) {
353
+ data = wolfssl.wolfSSL_read(this._ssl, 8192)
354
+ } else if (this._sock !== null && this._sock >= 0) {
355
+ data = sock.recv(this._sock, 8192)
356
+ } else { break }
357
+ if (!data || data.byteLength === 0) break
358
+ headerBuffer += _ab2str(data)
359
+ const headerEnd = headerBuffer.indexOf('\r\n\r\n')
360
+ if (headerEnd >= 0) {
361
+ const headerPart = headerBuffer.slice(0, headerEnd)
362
+ const lines = headerPart.split('\r\n')
363
+ const statusLine = lines[0]!
364
+ const statusParts = statusLine.split(' ')
365
+ if (statusParts.length < 2) {
366
+ doError(new Error('Invalid HTTP status line: ' + statusLine))
367
+ return
368
+ }
369
+ const statusCode = parseInt(statusParts[1]!, 10)
370
+ if (statusCode !== 101) {
371
+ doError(new Error('WebSocket handshake failed: HTTP ' + statusCode))
372
+ return
373
+ }
374
+
375
+ let acceptHeader = ''
376
+ for (let i = 1; i < lines.length; i++) {
377
+ const line = lines[i]!
378
+ const colonIdx2 = line.indexOf(':')
379
+ if (colonIdx2 >= 0) {
380
+ const name = line.slice(0, colonIdx2).toLowerCase().trim()
381
+ if (name === 'sec-websocket-accept') {
382
+ acceptHeader = line.slice(colonIdx2 + 1).trim()
383
+ break
384
+ }
385
+ }
386
+ }
387
+ if (!acceptHeader) {
388
+ doError(new Error('WebSocket handshake missing Sec-WebSocket-Accept'))
389
+ return
390
+ }
391
+
392
+ this._processingHandshake = true
393
+ const expectedAccept = await computeAccept(requestKey)
394
+ if (acceptHeader !== expectedAccept) {
395
+ doError(new Error('WebSocket handshake invalid Sec-WebSocket-Accept'))
396
+ this._processingHandshake = false
397
+ return
398
+ }
399
+
400
+ state = 'open'
401
+ resolved = true
402
+ this._processingHandshake = false
403
+ this._setState(State.OPEN)
404
+ break
405
+ }
406
+ }
407
+ }
408
+ }
409
+
410
+ if (event.lNetworkEvents & sock.FdEvent.FD_CLOSE) {
411
+ if (state !== 'open') {
412
+ doError(new Error('Connection closed'))
413
+ }
414
+ }
415
+ })
416
+
417
+ const ip = sock.resolve(host)
418
+ if (!ip) {
419
+ cleanup()
420
+ const self = this
421
+ os.setTimeout(() => {
422
+ self._fireError(new Error('DNS resolution failed for: ' + host))
423
+ if (self._state !== State.CLOSED) self._setState(State.CLOSED)
424
+ }, 0)
425
+ return
426
+ }
427
+ sock.connect(fd, ip, port)
428
+ }
429
+
430
+ private _sendRaw(buf: ArrayBuffer): void {
431
+ if (this._sock === null || this._sock < 0) return
432
+ if (this._ssl) {
433
+ wolfssl.wolfSSL_write(this._ssl, buf)
434
+ } else {
435
+ sock.send(this._sock, buf)
436
+ }
437
+ }
438
+
439
+ private _sendRawStr(str: string): void {
440
+ const buf = new ArrayBuffer(str.length)
441
+ const view = new Uint8Array(buf)
442
+ for (let i = 0; i < str.length; i++) view[i] = str.charCodeAt(i) & 0xFF
443
+ this._sendRaw(buf)
444
+ }
445
+
446
+ private _onData(): void {
447
+ while (true) {
448
+ let data: ArrayBuffer | null
449
+ if (this._ssl) {
450
+ data = wolfssl.wolfSSL_read(this._ssl, 8192)
451
+ } else if (this._sock !== null && this._sock >= 0) {
452
+ data = sock.recv(this._sock, 8192)
453
+ } else { break }
454
+ if (!data || data.byteLength === 0) break
455
+
456
+ const newBuf = new Uint8Array(this._readBuffer.length + data.byteLength)
457
+ newBuf.set(this._readBuffer)
458
+ newBuf.set(new Uint8Array(data), this._readBuffer.length)
459
+ this._readBuffer = newBuf
460
+
461
+ while (true) {
462
+ const frame = tryParseFrame(this._readBuffer)
463
+ if (!frame) break
464
+
465
+ this._readBuffer = this._readBuffer.slice(frame.totalLen)
466
+
467
+ if (frame.opcode === Opcode.TEXT || frame.opcode === Opcode.BINARY) {
468
+ const event = new MessageEvent('message', { data: frame.opcode === Opcode.TEXT ? _ab2str(frame.payload.slice(0).buffer) : frame.payload.slice(0).buffer })
469
+ this._dispatchEvent('message', event)
470
+ } else if (frame.opcode === Opcode.PING) {
471
+ const pong = createFrame(Opcode.PONG, frame.payload)
472
+ this._sendRaw(pong)
473
+ } else if (frame.opcode === Opcode.PONG) {
474
+ } else if (frame.opcode === Opcode.CLOSE) {
475
+ if (frame.payload.length >= 2) {
476
+ this._closeCode = (frame.payload[0]! << 8) | frame.payload[1]!
477
+ this._closeReason = _ab2str(frame.payload.slice(2).buffer)
478
+ }
479
+ this._cleanup()
480
+ this._setState(State.CLOSED)
481
+ return
482
+ }
483
+ }
484
+ }
485
+ }
486
+
487
+ private _setState(state: State): void {
488
+ if (this._state === state) return
489
+ this.readyState = state
490
+ this._state = state
491
+
492
+ if (state === State.OPEN) {
493
+ this._dispatchEvent('open', new Event('open'))
494
+ } else if (state === State.CLOSED) {
495
+ const event = new CloseEvent('close', { code: this._closeCode, reason: this._closeReason, wasClean: true })
496
+ this._dispatchEvent('close', event)
497
+ }
498
+ }
499
+
500
+ private _dispatchEvent(type: string, event: Event | MessageEvent | CloseEvent): void {
501
+ if (type === 'open' && this.onopen) this.onopen(event as Event)
502
+ else if (type === 'close' && this.onclose) this.onclose(event as CloseEvent)
503
+ else if (type === 'error' && this.onerror) this.onerror(event as Event)
504
+ else if (type === 'message' && this.onmessage) this.onmessage(event as MessageEvent)
505
+ }
506
+
507
+ private _fireError(err: Error): void {
508
+ console.error('WebSocket error:', err.message)
509
+ this._dispatchEvent('error', new Event('error'))
510
+ }
511
+
512
+ private _cleanup(): void {
513
+ if (this._ssl) { wolfssl.wolfSSL_free(this._ssl); this._ssl = null }
514
+ if (this._ctx) { wolfssl.wolfSSL_CTX_free(this._ctx); this._ctx = null }
515
+ if (this._sock !== null && this._sock >= 0) { sock.closesocket(this._sock); this._sock = null }
516
+ }
517
+ }
518
+
519
+ // ── Event/MessageEvent/CloseEvent polyfills ──
520
+
521
+ class Event {
522
+ readonly type: string
523
+ constructor(type: string, _init?: unknown) { this.type = type }
524
+ }
525
+
526
+ class MessageEvent {
527
+ readonly type: string
528
+ readonly data: unknown
529
+ constructor(type: string, init: { data: unknown }) {
530
+ this.type = type
531
+ this.data = init.data
532
+ }
533
+ }
534
+
535
+ class CloseEvent {
536
+ readonly type: string
537
+ readonly code: number
538
+ readonly reason: string
539
+ readonly wasClean: boolean
540
+ constructor(type: string, init: { code?: number; reason?: string; wasClean?: boolean }) {
541
+ this.type = type
542
+ this.code = init.code || 0
543
+ this.reason = init.reason || ''
544
+ this.wasClean = init.wasClean || false
545
+ }
546
+ }
547
+
548
+ // ── Utility ──
549
+
550
+ function _ab2str(buf: ArrayBuffer): string {
551
+ return new TextDecoder('utf-8').decode(buf)
552
+ }
553
+
554
+ // ── Global declarations ──
555
+
556
+ declare global {
557
+ interface WebSocket extends WebSocketImpl {}
558
+ var WebSocket: typeof WebSocketImpl
559
+ }
560
+
561
+ // ── Register globals ──
562
+
563
+ globalThis.WebSocket = WebSocketImpl