@sparkelf/dsh-plugin-mobile-gateway 0.9.0 → 0.9.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.
- package/bin/session-cache-proxy.mjs +63 -13
- package/package.json +1 -1
|
@@ -59,7 +59,57 @@ function tokenHashes() {
|
|
|
59
59
|
} catch { return new Set() }
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Write to a socket that may already be gone.
|
|
64
|
+
*
|
|
65
|
+
* `destroyed` is checked before the write but the peer can disappear between the check and the
|
|
66
|
+
* write, and a write to a closed socket emits an 'error' that, unhandled, ends the process — one
|
|
67
|
+
* phone hanging up took this proxy down (EPIPE from FrameReader.onFrame). Every write on a
|
|
68
|
+
* connection the peer controls goes through here.
|
|
69
|
+
*
|
|
70
|
+
* @param socket - the stream to write to; may be destroyed or undefined.
|
|
71
|
+
* @param payload - bytes or text to send.
|
|
72
|
+
* @returns whether the write was attempted.
|
|
73
|
+
*/
|
|
74
|
+
function safeWrite(socket, payload) {
|
|
75
|
+
if (socket === undefined || socket === null || socket.destroyed === true || socket.writable === false) return false
|
|
76
|
+
try {
|
|
77
|
+
socket.write(payload)
|
|
78
|
+
return true
|
|
79
|
+
} catch {
|
|
80
|
+
try { socket.destroy() } catch {}
|
|
81
|
+
return false
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
62
85
|
const frame = {
|
|
86
|
+
/**
|
|
87
|
+
* Wrap an already-serialized payload in one unmasked text frame.
|
|
88
|
+
*
|
|
89
|
+
* The upstream socket from an 'upgrade' event is a raw TCP stream, not a WebSocket object, so
|
|
90
|
+
* anything written to it must already be framed. Writing the bare JSON text instead is a silent
|
|
91
|
+
* corruption: the payload's first byte becomes the frame header, and '{' is 0x7B = FIN 0, RSV1 1,
|
|
92
|
+
* RSV2 1, RSV3 1, opcode 0xB — which the receiving parser rejects as "RSV2 and RSV3 must be
|
|
93
|
+
* clear" and closes the connection over.
|
|
94
|
+
*/
|
|
95
|
+
encodeText(text) {
|
|
96
|
+
const payload = Buffer.from(text, 'utf8')
|
|
97
|
+
if (payload.length < 126) {
|
|
98
|
+
return Buffer.concat([Buffer.from([0x81, payload.length]), payload])
|
|
99
|
+
}
|
|
100
|
+
if (payload.length < 65536) {
|
|
101
|
+
const header = Buffer.alloc(4)
|
|
102
|
+
header[0] = 0x81
|
|
103
|
+
header[1] = 126
|
|
104
|
+
header.writeUInt16BE(payload.length, 2)
|
|
105
|
+
return Buffer.concat([header, payload])
|
|
106
|
+
}
|
|
107
|
+
const header = Buffer.alloc(10)
|
|
108
|
+
header[0] = 0x81
|
|
109
|
+
header[1] = 127
|
|
110
|
+
header.writeBigUInt64BE(BigInt(payload.length), 2)
|
|
111
|
+
return Buffer.concat([header, payload])
|
|
112
|
+
},
|
|
63
113
|
/** Encode one server frame. Payloads are text by construction. */
|
|
64
114
|
encode(value) {
|
|
65
115
|
const payload = Buffer.from(JSON.stringify(value), 'utf8')
|
|
@@ -269,7 +319,7 @@ server.on('upgrade', (request, socket, head) => {
|
|
|
269
319
|
// asked for dsh-mobile-v1 must see it echoed or it will not treat the socket as established.
|
|
270
320
|
const accept = response2.headers['sec-websocket-accept']
|
|
271
321
|
const protocol = response2.headers['sec-websocket-protocol']
|
|
272
|
-
socket
|
|
322
|
+
safeWrite(socket,
|
|
273
323
|
'HTTP/1.1 101 Switching Protocols\r\n'
|
|
274
324
|
+ 'Upgrade: websocket\r\n'
|
|
275
325
|
+ 'Connection: Upgrade\r\n'
|
|
@@ -277,11 +327,14 @@ server.on('upgrade', (request, socket, head) => {
|
|
|
277
327
|
+ (protocol === undefined ? '' : 'Sec-WebSocket-Protocol: ' + String(protocol) + '\r\n')
|
|
278
328
|
+ '\r\n',
|
|
279
329
|
)
|
|
280
|
-
|
|
281
|
-
if (
|
|
330
|
+
// `head` holds leftover bytes the client sent after its handshake, already framed.
|
|
331
|
+
if (head.length > 0) safeWrite(liveSocket, head)
|
|
332
|
+
if (liveHead.length > 0) safeWrite(socket, liveHead)
|
|
282
333
|
|
|
283
334
|
const liveReader = new FrameReader((text) => {
|
|
284
|
-
|
|
335
|
+
let parsed
|
|
336
|
+
try { parsed = JSON.parse(text) } catch { return }
|
|
337
|
+
safeWrite(socket, frame.encode(parsed))
|
|
285
338
|
}, () => {})
|
|
286
339
|
liveSocket.on('data', (chunk) => liveReader.push(chunk))
|
|
287
340
|
liveSocket.on('close', () => { if (!socket.destroyed) socket.destroy() })
|
|
@@ -291,16 +344,16 @@ server.on('upgrade', (request, socket, head) => {
|
|
|
291
344
|
// local storage, and every other frame is forwarded unchanged.
|
|
292
345
|
const clientReader = new FrameReader((text) => {
|
|
293
346
|
let message
|
|
294
|
-
try { message = JSON.parse(text) } catch { liveSocket.
|
|
347
|
+
try { message = JSON.parse(text) } catch { safeWrite(liveSocket, frame.encodeText(text)); return }
|
|
295
348
|
if (message?.type === 'history' && typeof message.sessionId === 'string') {
|
|
296
349
|
let cached
|
|
297
350
|
try { cached = cachedEvents(message.sessionId) } catch { cached = undefined }
|
|
298
351
|
if (cached !== undefined && cached.events.length > 0) {
|
|
299
|
-
socket
|
|
352
|
+
safeWrite(socket, frame.encode(historyFrame(message, cached)))
|
|
300
353
|
return
|
|
301
354
|
}
|
|
302
355
|
}
|
|
303
|
-
liveSocket.
|
|
356
|
+
safeWrite(liveSocket, frame.encodeText(text))
|
|
304
357
|
}, (kind) => { if (kind === 'close') liveSocket.destroy() })
|
|
305
358
|
socket.on('data', (chunk) => clientReader.push(chunk))
|
|
306
359
|
})
|
|
@@ -313,20 +366,17 @@ server.on('upgrade', (request, socket, head) => {
|
|
|
313
366
|
response2.on('end', () => {
|
|
314
367
|
if (socket.destroyed) return
|
|
315
368
|
const body = Buffer.concat(chunks)
|
|
316
|
-
socket
|
|
317
|
-
socket.end(body)
|
|
369
|
+
if (safeWrite(socket, 'HTTP/1.1 ' + String(response2.statusCode ?? 502) + ' ' + String(response2.statusMessage ?? 'Bad Gateway') + '\r\nContent-Length: ' + String(body.length) + '\r\nConnection: close\r\n\r\n')) socket.end(body)
|
|
318
370
|
})
|
|
319
371
|
})
|
|
320
372
|
live.on('error', () => {
|
|
321
373
|
if (socket.destroyed) return
|
|
322
|
-
socket
|
|
323
|
-
socket.end()
|
|
374
|
+
if (safeWrite(socket, 'HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\n\r\n')) socket.end()
|
|
324
375
|
})
|
|
325
376
|
live.setTimeout(20000, () => {
|
|
326
377
|
live.destroy()
|
|
327
378
|
if (!socket.destroyed) {
|
|
328
|
-
socket
|
|
329
|
-
socket.end()
|
|
379
|
+
if (safeWrite(socket, 'HTTP/1.1 504 Gateway Timeout\r\nConnection: close\r\n\r\n')) socket.end()
|
|
330
380
|
}
|
|
331
381
|
})
|
|
332
382
|
live.end()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sparkelf/dsh-plugin-mobile-gateway",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "Mobile gateway for DSH 0.1.7 (Session format 4) with an entry-VPS Session cache. Reads DSH 0.1.7 Session format 4 and lifts retired v3 message shapes on read; a companion cache service plus gateway proxy let a phone fetch history from the entry host at 109 MB/s instead of through the desktop's 0.9 MB/s upstream. Per-session Agent preset selection, live preset updates, and the upstream LAN/pairing protocol are unchanged.",
|
|
5
5
|
"main": "lib/index.mjs",
|
|
6
6
|
"files": [
|