api-ape 2.0.0 → 2.2.2
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.
- package/README.md +203 -124
- package/client/README.md +37 -30
- package/client/browser.js +10 -8
- package/client/connectSocket.js +662 -381
- package/client/index.js +171 -0
- package/client/transports/streaming.js +240 -0
- package/dist/ape.js +2 -699
- package/dist/ape.js.map +7 -0
- package/dist/api-ape.min.js +2 -0
- package/dist/api-ape.min.js.map +7 -0
- package/index.d.ts +71 -18
- package/package.json +50 -15
- package/server/README.md +99 -13
- package/server/lib/broadcast.js +25 -8
- package/server/lib/bun.js +122 -0
- package/server/lib/longPolling.js +226 -0
- package/server/lib/main.js +381 -38
- package/server/lib/wiring.js +19 -12
- package/server/lib/ws/adapters/bun.js +225 -0
- package/server/lib/ws/adapters/deno.js +186 -0
- package/server/lib/ws/frames.js +217 -0
- package/server/lib/ws/index.js +15 -0
- package/server/lib/ws/server.js +109 -0
- package/server/lib/ws/socket.js +222 -0
- package/server/lib/wsProvider.js +135 -0
- package/server/security/origin.js +16 -4
- package/server/socket/receive.js +14 -1
- package/server/socket/send.js +6 -6
- package/server/utils/deepRequire.js +25 -10
- package/server/utils/parseUserAgent.js +286 -0
- package/example/Bun/README.md +0 -74
- package/example/Bun/api/message.ts +0 -11
- package/example/Bun/index.html +0 -76
- package/example/Bun/package.json +0 -9
- package/example/Bun/server.ts +0 -59
- package/example/Bun/styles.css +0 -128
- package/example/ExpressJs/README.md +0 -95
- package/example/ExpressJs/api/message.js +0 -11
- package/example/ExpressJs/backend.js +0 -39
- package/example/ExpressJs/index.html +0 -88
- package/example/ExpressJs/package-lock.json +0 -834
- package/example/ExpressJs/package.json +0 -10
- package/example/ExpressJs/styles.css +0 -128
- package/example/NextJs/.dockerignore +0 -29
- package/example/NextJs/Dockerfile +0 -52
- package/example/NextJs/Dockerfile.dev +0 -27
- package/example/NextJs/README.md +0 -113
- package/example/NextJs/ape/client.js +0 -66
- package/example/NextJs/ape/embed.js +0 -12
- package/example/NextJs/ape/index.js +0 -23
- package/example/NextJs/ape/logic/chat.js +0 -62
- package/example/NextJs/ape/onConnect.js +0 -69
- package/example/NextJs/ape/onDisconnect.js +0 -13
- package/example/NextJs/ape/onError.js +0 -9
- package/example/NextJs/ape/onReceive.js +0 -15
- package/example/NextJs/ape/onSend.js +0 -15
- package/example/NextJs/api/message.js +0 -44
- package/example/NextJs/docker-compose.yml +0 -22
- package/example/NextJs/next-env.d.ts +0 -5
- package/example/NextJs/next.config.js +0 -8
- package/example/NextJs/package-lock.json +0 -6400
- package/example/NextJs/package.json +0 -24
- package/example/NextJs/pages/Info.tsx +0 -153
- package/example/NextJs/pages/_app.tsx +0 -6
- package/example/NextJs/pages/index.tsx +0 -275
- package/example/NextJs/public/favicon.ico +0 -0
- package/example/NextJs/public/vercel.svg +0 -4
- package/example/NextJs/server.js +0 -36
- package/example/NextJs/styles/Chat.module.css +0 -448
- package/example/NextJs/styles/Home.module.css +0 -129
- package/example/NextJs/styles/globals.css +0 -26
- package/example/NextJs/tsconfig.json +0 -20
- package/example/README.md +0 -117
- package/example/Vite/README.md +0 -68
- package/example/Vite/ape/client.ts +0 -66
- package/example/Vite/ape/onConnect.ts +0 -52
- package/example/Vite/api/message.ts +0 -57
- package/example/Vite/index.html +0 -16
- package/example/Vite/package.json +0 -19
- package/example/Vite/server.ts +0 -62
- package/example/Vite/src/App.vue +0 -170
- package/example/Vite/src/components/Info.vue +0 -352
- package/example/Vite/src/main.ts +0 -5
- package/example/Vite/src/style.css +0 -200
- package/example/Vite/src/vite-env.d.ts +0 -7
- package/example/Vite/vite.config.ts +0 -20
- package/todo.md +0 -85
- package/utils/jss.test.js +0 -261
- package/utils/messageHash.test.js +0 -56
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bun Native WebSocket Adapter
|
|
3
|
+
* Wraps Bun's ServerWebSocket to be compatible with the ws library API
|
|
4
|
+
*
|
|
5
|
+
* Bun uses a different pattern:
|
|
6
|
+
* - Bun.serve({ websocket: { open, message, close, ... } })
|
|
7
|
+
* - server.upgrade(req) to upgrade connections
|
|
8
|
+
*
|
|
9
|
+
* This adapter provides a ws-compatible WebSocketServer that works
|
|
10
|
+
* with the existing api-ape code.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { EventEmitter } = require('events')
|
|
14
|
+
|
|
15
|
+
// WebSocket ready states (matching ws library)
|
|
16
|
+
const READY_STATES = {
|
|
17
|
+
CONNECTING: 0,
|
|
18
|
+
OPEN: 1,
|
|
19
|
+
CLOSING: 2,
|
|
20
|
+
CLOSED: 3
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Wrapper around Bun's ServerWebSocket to provide ws-compatible API
|
|
25
|
+
*/
|
|
26
|
+
class BunWebSocket extends EventEmitter {
|
|
27
|
+
constructor(bunSocket) {
|
|
28
|
+
super()
|
|
29
|
+
this._socket = bunSocket
|
|
30
|
+
this._readyState = READY_STATES.OPEN
|
|
31
|
+
|
|
32
|
+
// Define constants on instance (matching ws library API)
|
|
33
|
+
this.CONNECTING = READY_STATES.CONNECTING
|
|
34
|
+
this.OPEN = READY_STATES.OPEN
|
|
35
|
+
this.CLOSING = READY_STATES.CLOSING
|
|
36
|
+
this.CLOSED = READY_STATES.CLOSED
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
get readyState() {
|
|
40
|
+
return this._readyState
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Send data to the client
|
|
45
|
+
* @param {string|Buffer|ArrayBuffer} data - Data to send
|
|
46
|
+
*/
|
|
47
|
+
send(data) {
|
|
48
|
+
if (this._readyState !== READY_STATES.OPEN) {
|
|
49
|
+
throw new Error('WebSocket is not open')
|
|
50
|
+
}
|
|
51
|
+
this._socket.send(data)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Close the WebSocket connection
|
|
56
|
+
* @param {number} code - Status code
|
|
57
|
+
* @param {string} reason - Close reason
|
|
58
|
+
*/
|
|
59
|
+
close(code = 1000, reason = '') {
|
|
60
|
+
if (this._readyState === READY_STATES.CLOSING ||
|
|
61
|
+
this._readyState === READY_STATES.CLOSED) {
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
this._readyState = READY_STATES.CLOSING
|
|
65
|
+
this._socket.close(code, reason)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Called by BunWebSocketServer when message received
|
|
70
|
+
* @internal
|
|
71
|
+
*/
|
|
72
|
+
_onMessage(data) {
|
|
73
|
+
// Convert to Buffer for consistency with ws library
|
|
74
|
+
const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data)
|
|
75
|
+
this.emit('message', buffer)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Called by BunWebSocketServer when connection closed
|
|
80
|
+
* @internal
|
|
81
|
+
*/
|
|
82
|
+
_onClose(code, reason) {
|
|
83
|
+
this._readyState = READY_STATES.CLOSED
|
|
84
|
+
this.emit('close', code, reason)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Called by BunWebSocketServer on error
|
|
89
|
+
* @internal
|
|
90
|
+
*/
|
|
91
|
+
_onError(error) {
|
|
92
|
+
this.emit('error', error)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* WebSocketServer compatible with Bun's server.upgrade() pattern
|
|
98
|
+
*
|
|
99
|
+
* Usage in main.js:
|
|
100
|
+
* - Create BunWebSocketServer
|
|
101
|
+
* - It provides the websocket handlers for Bun.serve()
|
|
102
|
+
* - Call handleUpgrade() when upgrade request received
|
|
103
|
+
*/
|
|
104
|
+
class BunWebSocketServer extends EventEmitter {
|
|
105
|
+
constructor(options = {}) {
|
|
106
|
+
super()
|
|
107
|
+
this._noServer = options.noServer || false
|
|
108
|
+
this._clients = new Map() // Map socket -> BunWebSocket wrapper
|
|
109
|
+
|
|
110
|
+
// Bun websocket handler configuration
|
|
111
|
+
// This will be used by the integration in main.js
|
|
112
|
+
this.websocketHandlers = {
|
|
113
|
+
open: (ws) => this._handleOpen(ws),
|
|
114
|
+
message: (ws, message) => this._handleMessage(ws, message),
|
|
115
|
+
close: (ws, code, reason) => this._handleClose(ws, code, reason),
|
|
116
|
+
error: (ws, error) => this._handleError(ws, error)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Get all connected clients
|
|
122
|
+
* @returns {Set<BunWebSocket>}
|
|
123
|
+
*/
|
|
124
|
+
get clients() {
|
|
125
|
+
return new Set(this._clients.values())
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Handle upgrade request - called from main.js
|
|
130
|
+
* For Bun, we need to return info for the upgrade
|
|
131
|
+
* @param {Request} req - Bun Request object
|
|
132
|
+
* @param {*} server - Bun server instance
|
|
133
|
+
* @param {*} head - Not used in Bun
|
|
134
|
+
* @param {function} callback - Called with wrapped WebSocket
|
|
135
|
+
*/
|
|
136
|
+
handleUpgrade(req, server, head, callback) {
|
|
137
|
+
// In Bun, we store the callback and req info
|
|
138
|
+
// The actual upgrade happens via server.upgrade()
|
|
139
|
+
// The callback will be called in _handleOpen when Bun calls our open handler
|
|
140
|
+
|
|
141
|
+
// Store pending upgrade info keyed by some identifier
|
|
142
|
+
// Bun's server.upgrade() succeeds/fails synchronously
|
|
143
|
+
const upgraded = server.upgrade(req, {
|
|
144
|
+
data: { callback, req }
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
if (!upgraded) {
|
|
148
|
+
// Upgrade failed
|
|
149
|
+
return false
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return true
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Handle Bun websocket open event
|
|
157
|
+
* @internal
|
|
158
|
+
*/
|
|
159
|
+
_handleOpen(bunSocket) {
|
|
160
|
+
const wrapper = new BunWebSocket(bunSocket)
|
|
161
|
+
this._clients.set(bunSocket, wrapper)
|
|
162
|
+
|
|
163
|
+
// Get the callback from upgrade data
|
|
164
|
+
const { callback, req } = bunSocket.data || {}
|
|
165
|
+
|
|
166
|
+
if (callback) {
|
|
167
|
+
callback(wrapper)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Emit connection event
|
|
171
|
+
this.emit('connection', wrapper, req)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Handle Bun websocket message event
|
|
176
|
+
* @internal
|
|
177
|
+
*/
|
|
178
|
+
_handleMessage(bunSocket, message) {
|
|
179
|
+
const wrapper = this._clients.get(bunSocket)
|
|
180
|
+
if (wrapper) {
|
|
181
|
+
wrapper._onMessage(message)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Handle Bun websocket close event
|
|
187
|
+
* @internal
|
|
188
|
+
*/
|
|
189
|
+
_handleClose(bunSocket, code, reason) {
|
|
190
|
+
const wrapper = this._clients.get(bunSocket)
|
|
191
|
+
if (wrapper) {
|
|
192
|
+
wrapper._onClose(code, reason)
|
|
193
|
+
this._clients.delete(bunSocket)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Handle Bun websocket error event
|
|
199
|
+
* @internal
|
|
200
|
+
*/
|
|
201
|
+
_handleError(bunSocket, error) {
|
|
202
|
+
const wrapper = this._clients.get(bunSocket)
|
|
203
|
+
if (wrapper) {
|
|
204
|
+
wrapper._onError(error)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Close server and all connections
|
|
210
|
+
*/
|
|
211
|
+
close(callback) {
|
|
212
|
+
for (const [bunSocket, wrapper] of this._clients) {
|
|
213
|
+
wrapper.close(1001, 'Server shutting down')
|
|
214
|
+
}
|
|
215
|
+
this._clients.clear()
|
|
216
|
+
if (callback) {
|
|
217
|
+
callback()
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
module.exports = {
|
|
223
|
+
BunWebSocket,
|
|
224
|
+
BunWebSocketServer
|
|
225
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deno Native WebSocket Adapter
|
|
3
|
+
* Wraps Deno's native WebSocket to be compatible with the ws library API
|
|
4
|
+
*
|
|
5
|
+
* Deno uses a different pattern:
|
|
6
|
+
* - Deno.serve() with Deno.upgradeWebSocket(req)
|
|
7
|
+
* - Returns { socket, response } where socket is a standard WebSocket
|
|
8
|
+
*
|
|
9
|
+
* This adapter provides a ws-compatible WebSocketServer that works
|
|
10
|
+
* with the existing api-ape code.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { EventEmitter } = require('events')
|
|
14
|
+
|
|
15
|
+
// WebSocket ready states (matching ws library)
|
|
16
|
+
const READY_STATES = {
|
|
17
|
+
CONNECTING: 0,
|
|
18
|
+
OPEN: 1,
|
|
19
|
+
CLOSING: 2,
|
|
20
|
+
CLOSED: 3
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Wrapper around Deno's native WebSocket to provide ws-compatible API
|
|
25
|
+
* Deno's WebSocket uses onmessage/onclose properties instead of EventEmitter
|
|
26
|
+
*/
|
|
27
|
+
class DenoWebSocket extends EventEmitter {
|
|
28
|
+
constructor(denoSocket) {
|
|
29
|
+
super()
|
|
30
|
+
this._socket = denoSocket
|
|
31
|
+
this._readyState = READY_STATES.OPEN
|
|
32
|
+
|
|
33
|
+
// Define constants on instance (matching ws library API)
|
|
34
|
+
this.CONNECTING = READY_STATES.CONNECTING
|
|
35
|
+
this.OPEN = READY_STATES.OPEN
|
|
36
|
+
this.CLOSING = READY_STATES.CLOSING
|
|
37
|
+
this.CLOSED = READY_STATES.CLOSED
|
|
38
|
+
|
|
39
|
+
// Wire up Deno's event properties to our EventEmitter
|
|
40
|
+
this._setupDenoEvents()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
get readyState() {
|
|
44
|
+
return this._readyState
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Setup Deno WebSocket event handlers
|
|
49
|
+
* @internal
|
|
50
|
+
*/
|
|
51
|
+
_setupDenoEvents() {
|
|
52
|
+
this._socket.onmessage = (event) => {
|
|
53
|
+
// Convert to Buffer for consistency with ws library
|
|
54
|
+
const data = event.data
|
|
55
|
+
const buffer = typeof data === 'string'
|
|
56
|
+
? Buffer.from(data)
|
|
57
|
+
: Buffer.from(data)
|
|
58
|
+
this.emit('message', buffer)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
this._socket.onclose = (event) => {
|
|
62
|
+
this._readyState = READY_STATES.CLOSED
|
|
63
|
+
this.emit('close', event.code, event.reason)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
this._socket.onerror = (event) => {
|
|
67
|
+
this.emit('error', event)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Send data to the client
|
|
73
|
+
* @param {string|Buffer|ArrayBuffer} data - Data to send
|
|
74
|
+
*/
|
|
75
|
+
send(data) {
|
|
76
|
+
if (this._readyState !== READY_STATES.OPEN) {
|
|
77
|
+
throw new Error('WebSocket is not open')
|
|
78
|
+
}
|
|
79
|
+
// Deno's WebSocket.send() accepts string, ArrayBuffer, or Blob
|
|
80
|
+
if (Buffer.isBuffer(data)) {
|
|
81
|
+
this._socket.send(data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength))
|
|
82
|
+
} else {
|
|
83
|
+
this._socket.send(data)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Close the WebSocket connection
|
|
89
|
+
* @param {number} code - Status code
|
|
90
|
+
* @param {string} reason - Close reason
|
|
91
|
+
*/
|
|
92
|
+
close(code = 1000, reason = '') {
|
|
93
|
+
if (this._readyState === READY_STATES.CLOSING ||
|
|
94
|
+
this._readyState === READY_STATES.CLOSED) {
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
this._readyState = READY_STATES.CLOSING
|
|
98
|
+
this._socket.close(code, reason)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* WebSocketServer compatible with Deno's Deno.upgradeWebSocket() pattern
|
|
104
|
+
*
|
|
105
|
+
* Usage in main.js:
|
|
106
|
+
* - Create DenoWebSocketServer
|
|
107
|
+
* - Call handleUpgrade() when upgrade request received
|
|
108
|
+
* - It uses Deno.upgradeWebSocket() internally
|
|
109
|
+
*/
|
|
110
|
+
class DenoWebSocketServer extends EventEmitter {
|
|
111
|
+
constructor(options = {}) {
|
|
112
|
+
super()
|
|
113
|
+
this._noServer = options.noServer || false
|
|
114
|
+
this._clients = new Set()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get all connected clients
|
|
119
|
+
* @returns {Set<DenoWebSocket>}
|
|
120
|
+
*/
|
|
121
|
+
get clients() {
|
|
122
|
+
return this._clients
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Handle upgrade request using Deno.upgradeWebSocket
|
|
127
|
+
* @param {Request} req - Deno Request object
|
|
128
|
+
* @param {*} _socket - Not used in Deno (placeholder for API compat)
|
|
129
|
+
* @param {*} _head - Not used in Deno (placeholder for API compat)
|
|
130
|
+
* @param {function} callback - Called with wrapped WebSocket
|
|
131
|
+
* @returns {{ response: Response } | null} - Response to return from handler
|
|
132
|
+
*/
|
|
133
|
+
handleUpgrade(req, _socket, _head, callback) {
|
|
134
|
+
// Check for upgrade header
|
|
135
|
+
const upgrade = req.headers.get('upgrade')
|
|
136
|
+
if (!upgrade || upgrade.toLowerCase() !== 'websocket') {
|
|
137
|
+
return null
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
// Use Deno's built-in upgrade
|
|
142
|
+
const { socket: denoSocket, response } = Deno.upgradeWebSocket(req)
|
|
143
|
+
|
|
144
|
+
// Wrap with our adapter
|
|
145
|
+
const wrapper = new DenoWebSocket(denoSocket)
|
|
146
|
+
this._clients.add(wrapper)
|
|
147
|
+
|
|
148
|
+
// Remove from clients on close
|
|
149
|
+
wrapper.on('close', () => {
|
|
150
|
+
this._clients.delete(wrapper)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
// Call the callback with wrapped socket
|
|
154
|
+
if (callback) {
|
|
155
|
+
callback(wrapper)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Emit connection event
|
|
159
|
+
this.emit('connection', wrapper, req)
|
|
160
|
+
|
|
161
|
+
// Return the response for Deno's handler
|
|
162
|
+
return { response }
|
|
163
|
+
} catch (err) {
|
|
164
|
+
console.error('[api-ape] Deno WebSocket upgrade failed:', err)
|
|
165
|
+
return null
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Close server and all connections
|
|
171
|
+
*/
|
|
172
|
+
close(callback) {
|
|
173
|
+
for (const client of this._clients) {
|
|
174
|
+
client.close(1001, 'Server shutting down')
|
|
175
|
+
}
|
|
176
|
+
this._clients.clear()
|
|
177
|
+
if (callback) {
|
|
178
|
+
callback()
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
module.exports = {
|
|
184
|
+
DenoWebSocket,
|
|
185
|
+
DenoWebSocketServer
|
|
186
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC 6455 WebSocket Frame Encoding/Decoding
|
|
3
|
+
*
|
|
4
|
+
* Frame format:
|
|
5
|
+
* 0 1 2 3
|
|
6
|
+
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
|
7
|
+
* +-+-+-+-+-------+-+-------------+-------------------------------+
|
|
8
|
+
* |F|R|R|R| opcode|M| Payload len | Extended payload length |
|
|
9
|
+
* |I|S|S|S| (4) |A| (7) | (16/64) |
|
|
10
|
+
* |N|V|V|V| |S| | (if payload len==126/127) |
|
|
11
|
+
* | |1|2|3| |K| | |
|
|
12
|
+
* +-+-+-+-+-------+-+-------------+-------------------------------+
|
|
13
|
+
* | Extended payload length continued, if payload len == 127 |
|
|
14
|
+
* +-------------------------------+-------------------------------+
|
|
15
|
+
* | | Masking-key, if MASK set to 1 |
|
|
16
|
+
* +-------------------------------+-------------------------------+
|
|
17
|
+
* | Masking-key (continued) | Payload Data |
|
|
18
|
+
* +-------------------------------+-------------------------------+
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const crypto = require('crypto')
|
|
22
|
+
|
|
23
|
+
// WebSocket GUID for handshake (RFC 6455 Section 1.3)
|
|
24
|
+
const WS_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
|
|
25
|
+
|
|
26
|
+
// Opcodes
|
|
27
|
+
const OPCODES = {
|
|
28
|
+
CONTINUATION: 0x00,
|
|
29
|
+
TEXT: 0x01,
|
|
30
|
+
BINARY: 0x02,
|
|
31
|
+
CLOSE: 0x08,
|
|
32
|
+
PING: 0x09,
|
|
33
|
+
PONG: 0x0A
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Generate the Sec-WebSocket-Accept header value from client key
|
|
38
|
+
* @param {string} clientKey - The Sec-WebSocket-Key from client
|
|
39
|
+
* @returns {string} Base64 encoded accept key
|
|
40
|
+
*/
|
|
41
|
+
function generateAcceptKey(clientKey) {
|
|
42
|
+
return crypto
|
|
43
|
+
.createHash('sha1')
|
|
44
|
+
.update(clientKey + WS_GUID)
|
|
45
|
+
.digest('base64')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Unmask payload data (client → server messages are always masked)
|
|
50
|
+
* @param {Buffer} payload - The masked payload
|
|
51
|
+
* @param {Buffer} maskKey - 4-byte mask key
|
|
52
|
+
* @returns {Buffer} Unmasked payload
|
|
53
|
+
*/
|
|
54
|
+
function unmaskPayload(payload, maskKey) {
|
|
55
|
+
const result = Buffer.alloc(payload.length)
|
|
56
|
+
for (let i = 0; i < payload.length; i++) {
|
|
57
|
+
result[i] = payload[i] ^ maskKey[i & 3]
|
|
58
|
+
}
|
|
59
|
+
return result
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Parse a WebSocket frame from buffer
|
|
64
|
+
* @param {Buffer} buffer - Raw data buffer
|
|
65
|
+
* @returns {{ frame: Object, bytesConsumed: number } | null} Parsed frame or null if incomplete
|
|
66
|
+
*/
|
|
67
|
+
function parseFrame(buffer) {
|
|
68
|
+
if (buffer.length < 2) return null
|
|
69
|
+
|
|
70
|
+
let offset = 0
|
|
71
|
+
const firstByte = buffer[offset++]
|
|
72
|
+
const secondByte = buffer[offset++]
|
|
73
|
+
|
|
74
|
+
const fin = (firstByte & 0x80) !== 0
|
|
75
|
+
const opcode = firstByte & 0x0F
|
|
76
|
+
const masked = (secondByte & 0x80) !== 0
|
|
77
|
+
let payloadLength = secondByte & 0x7F
|
|
78
|
+
|
|
79
|
+
// Extended payload length
|
|
80
|
+
if (payloadLength === 126) {
|
|
81
|
+
if (buffer.length < offset + 2) return null
|
|
82
|
+
payloadLength = buffer.readUInt16BE(offset)
|
|
83
|
+
offset += 2
|
|
84
|
+
} else if (payloadLength === 127) {
|
|
85
|
+
if (buffer.length < offset + 8) return null
|
|
86
|
+
// JavaScript can't handle full 64-bit, use lower 32 bits
|
|
87
|
+
const high = buffer.readUInt32BE(offset)
|
|
88
|
+
const low = buffer.readUInt32BE(offset + 4)
|
|
89
|
+
if (high !== 0) {
|
|
90
|
+
throw new Error('Payload too large')
|
|
91
|
+
}
|
|
92
|
+
payloadLength = low
|
|
93
|
+
offset += 8
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Mask key (4 bytes, only if masked)
|
|
97
|
+
let maskKey = null
|
|
98
|
+
if (masked) {
|
|
99
|
+
if (buffer.length < offset + 4) return null
|
|
100
|
+
maskKey = buffer.slice(offset, offset + 4)
|
|
101
|
+
offset += 4
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Payload
|
|
105
|
+
if (buffer.length < offset + payloadLength) return null
|
|
106
|
+
let payload = buffer.slice(offset, offset + payloadLength)
|
|
107
|
+
offset += payloadLength
|
|
108
|
+
|
|
109
|
+
// Unmask if needed
|
|
110
|
+
if (masked && maskKey) {
|
|
111
|
+
payload = unmaskPayload(payload, maskKey)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
frame: { fin, opcode, payload },
|
|
116
|
+
bytesConsumed: offset
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Build a WebSocket frame
|
|
122
|
+
* Server → client frames are never masked (per RFC 6455)
|
|
123
|
+
* @param {Buffer|string} data - Payload data
|
|
124
|
+
* @param {number} opcode - Frame opcode
|
|
125
|
+
* @param {boolean} fin - Is this the final frame?
|
|
126
|
+
* @returns {Buffer} Complete frame buffer
|
|
127
|
+
*/
|
|
128
|
+
function buildFrame(data, opcode = OPCODES.TEXT, fin = true) {
|
|
129
|
+
const payload = Buffer.isBuffer(data) ? data : Buffer.from(data)
|
|
130
|
+
const payloadLength = payload.length
|
|
131
|
+
|
|
132
|
+
// Calculate header size
|
|
133
|
+
let headerSize = 2 // First two bytes
|
|
134
|
+
let extendedLengthSize = 0
|
|
135
|
+
|
|
136
|
+
if (payloadLength > 65535) {
|
|
137
|
+
extendedLengthSize = 8
|
|
138
|
+
} else if (payloadLength > 125) {
|
|
139
|
+
extendedLengthSize = 2
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const frame = Buffer.alloc(headerSize + extendedLengthSize + payloadLength)
|
|
143
|
+
let offset = 0
|
|
144
|
+
|
|
145
|
+
// First byte: FIN + opcode
|
|
146
|
+
frame[offset++] = (fin ? 0x80 : 0x00) | opcode
|
|
147
|
+
|
|
148
|
+
// Second byte: mask bit (0) + payload length
|
|
149
|
+
if (payloadLength > 65535) {
|
|
150
|
+
frame[offset++] = 127
|
|
151
|
+
// Write 64-bit length (high 32 bits = 0)
|
|
152
|
+
frame.writeUInt32BE(0, offset)
|
|
153
|
+
offset += 4
|
|
154
|
+
frame.writeUInt32BE(payloadLength, offset)
|
|
155
|
+
offset += 4
|
|
156
|
+
} else if (payloadLength > 125) {
|
|
157
|
+
frame[offset++] = 126
|
|
158
|
+
frame.writeUInt16BE(payloadLength, offset)
|
|
159
|
+
offset += 2
|
|
160
|
+
} else {
|
|
161
|
+
frame[offset++] = payloadLength
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Payload (no masking for server → client)
|
|
165
|
+
payload.copy(frame, offset)
|
|
166
|
+
|
|
167
|
+
return frame
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Build a close frame with optional status code and reason
|
|
172
|
+
* @param {number} code - Status code (1000 = normal)
|
|
173
|
+
* @param {string} reason - Close reason
|
|
174
|
+
* @returns {Buffer} Close frame
|
|
175
|
+
*/
|
|
176
|
+
function buildCloseFrame(code = 1000, reason = '') {
|
|
177
|
+
const reasonBuffer = Buffer.from(reason)
|
|
178
|
+
const payload = Buffer.alloc(2 + reasonBuffer.length)
|
|
179
|
+
payload.writeUInt16BE(code, 0)
|
|
180
|
+
reasonBuffer.copy(payload, 2)
|
|
181
|
+
return buildFrame(payload, OPCODES.CLOSE)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Build a pong frame in response to ping
|
|
186
|
+
* @param {Buffer} data - Ping payload to echo back
|
|
187
|
+
* @returns {Buffer} Pong frame
|
|
188
|
+
*/
|
|
189
|
+
function buildPongFrame(data) {
|
|
190
|
+
return buildFrame(data, OPCODES.PONG)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Parse close frame payload
|
|
195
|
+
* @param {Buffer} payload - Close frame payload
|
|
196
|
+
* @returns {{ code: number, reason: string }}
|
|
197
|
+
*/
|
|
198
|
+
function parseClosePayload(payload) {
|
|
199
|
+
if (payload.length >= 2) {
|
|
200
|
+
return {
|
|
201
|
+
code: payload.readUInt16BE(0),
|
|
202
|
+
reason: payload.slice(2).toString('utf8')
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return { code: 1005, reason: '' }
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
module.exports = {
|
|
209
|
+
OPCODES,
|
|
210
|
+
generateAcceptKey,
|
|
211
|
+
parseFrame,
|
|
212
|
+
buildFrame,
|
|
213
|
+
buildCloseFrame,
|
|
214
|
+
buildPongFrame,
|
|
215
|
+
parseClosePayload,
|
|
216
|
+
unmaskPayload
|
|
217
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket polyfill entry point
|
|
3
|
+
* Provides WebSocketServer compatible with the ws library API
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { WebSocketServer } = require('./server')
|
|
7
|
+
const { WebSocket, READY_STATES } = require('./socket')
|
|
8
|
+
const { OPCODES } = require('./frames')
|
|
9
|
+
|
|
10
|
+
module.exports = {
|
|
11
|
+
WebSocketServer,
|
|
12
|
+
WebSocket,
|
|
13
|
+
READY_STATES,
|
|
14
|
+
OPCODES
|
|
15
|
+
}
|