@sparkelf/dsh-plugin-mobile-gateway 0.9.0 → 0.9.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/bin/protocol-harness.mjs +187 -0
- package/bin/session-cache-proxy.mjs +88 -15
- package/package.json +1 -1
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Mobile gateway protocol harness.
|
|
4
|
+
*
|
|
5
|
+
* Drives a full pairing and Session load the way the App does, so a change can be checked without
|
|
6
|
+
* a phone in hand. It speaks the wire protocol directly: the App is a shipped binary and the point
|
|
7
|
+
* is to reproduce its behaviour, not the gateway's own tests.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* protocol-harness.mjs --host 127.0.0.1 --port 7091 --code <pairingCode> [--json]
|
|
11
|
+
* [--expect-history] [--timeout 30000]
|
|
12
|
+
*
|
|
13
|
+
* Exit codes: 0 the flow completed, 1 a protocol failure, 2 the flow timed out.
|
|
14
|
+
*/
|
|
15
|
+
import net from 'node:net'
|
|
16
|
+
import { randomBytes } from 'node:crypto'
|
|
17
|
+
|
|
18
|
+
const argv = process.argv.slice(2)
|
|
19
|
+
let host = '127.0.0.1'
|
|
20
|
+
let port = 7091
|
|
21
|
+
let code = ''
|
|
22
|
+
let json = false
|
|
23
|
+
let expectHistory = false
|
|
24
|
+
let sessionId = ''
|
|
25
|
+
let timeoutMs = 30000
|
|
26
|
+
let path = '/ws/mobile'
|
|
27
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
28
|
+
const arg = argv[index]
|
|
29
|
+
if (arg === '--host') { host = argv[++index]; continue }
|
|
30
|
+
if (arg === '--port') { port = Number(argv[++index]); continue }
|
|
31
|
+
if (arg === '--code') { code = argv[++index]; continue }
|
|
32
|
+
if (arg === '--path') { path = argv[++index]; continue }
|
|
33
|
+
if (arg === '--timeout') { timeoutMs = Number(argv[++index]); continue }
|
|
34
|
+
if (arg === '--json') { json = true; continue }
|
|
35
|
+
if (arg === '--expect-history') { expectHistory = true; continue }
|
|
36
|
+
if (arg === '--session') { sessionId = argv[++index]; continue }
|
|
37
|
+
if (arg === '--help' || arg === '-h') {
|
|
38
|
+
console.log('usage: protocol-harness.mjs --code <pairingCode> [--host 127.0.0.1] [--port 7091] [--path /ws/mobile] [--json] [--expect-history] [--timeout 30000]')
|
|
39
|
+
process.exit(0)
|
|
40
|
+
}
|
|
41
|
+
throw new Error('unknown option: ' + arg)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const report = { steps: [], frames: [], errors: [], ok: false }
|
|
45
|
+
|
|
46
|
+
function note(step) { report.steps.push(step); if (!json) console.log(' ' + step) }
|
|
47
|
+
function fail(message) { report.errors.push(message); if (!json) console.log(' FAIL ' + message) }
|
|
48
|
+
|
|
49
|
+
/** One masked client text frame, per RFC 6455 — an unmasked frame is rejected as EXPECTED_MASK. */
|
|
50
|
+
function encodeClientText(text) {
|
|
51
|
+
const payload = Buffer.from(text, 'utf8')
|
|
52
|
+
const mask = randomBytes(4)
|
|
53
|
+
const length = payload.length
|
|
54
|
+
let header
|
|
55
|
+
if (length < 126) {
|
|
56
|
+
header = Buffer.alloc(2)
|
|
57
|
+
header[1] = 0x80 | length
|
|
58
|
+
} else if (length < 65536) {
|
|
59
|
+
header = Buffer.alloc(4)
|
|
60
|
+
header[1] = 0x80 | 126
|
|
61
|
+
header.writeUInt16BE(length, 2)
|
|
62
|
+
} else {
|
|
63
|
+
header = Buffer.alloc(10)
|
|
64
|
+
header[1] = 0x80 | 127
|
|
65
|
+
header.writeBigUInt64BE(BigInt(length), 2)
|
|
66
|
+
}
|
|
67
|
+
header[0] = 0x81
|
|
68
|
+
const masked = Buffer.from(payload)
|
|
69
|
+
for (let index = 0; index < masked.length; index += 1) masked[index] ^= mask[index % 4]
|
|
70
|
+
return Buffer.concat([header, mask, masked])
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const socket = net.connect(port, host)
|
|
74
|
+
let carry = Buffer.alloc(0)
|
|
75
|
+
let handshakeDone = false
|
|
76
|
+
let finished = false
|
|
77
|
+
const sentHistory = new Set()
|
|
78
|
+
|
|
79
|
+
function finish(ok, why) {
|
|
80
|
+
if (finished) return
|
|
81
|
+
finished = true
|
|
82
|
+
report.ok = ok
|
|
83
|
+
if (why !== undefined) fail(why)
|
|
84
|
+
if (json) console.log(JSON.stringify(report, null, 2))
|
|
85
|
+
else {
|
|
86
|
+
console.log()
|
|
87
|
+
console.log(' 帧序列: ' + report.frames.map((frame) => frame.kind).join(' -> '))
|
|
88
|
+
console.log(' 结果: ' + (ok ? 'PASS' : 'FAIL'))
|
|
89
|
+
if (report.errors.length > 0) for (const error of report.errors) console.log(' - ' + error)
|
|
90
|
+
}
|
|
91
|
+
try { socket.destroy() } catch {}
|
|
92
|
+
process.exit(ok ? 0 : 1)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Decode server frames; the server never masks, so the payload follows the header directly. */
|
|
96
|
+
function readFrames() {
|
|
97
|
+
for (;;) {
|
|
98
|
+
if (carry.length < 2) return
|
|
99
|
+
const opcode = carry[0] & 0x0f
|
|
100
|
+
const masked = (carry[1] & 0x80) !== 0
|
|
101
|
+
let length = carry[1] & 0x7f
|
|
102
|
+
let offset = 2
|
|
103
|
+
if (length === 126) { if (carry.length < 4) return; length = carry.readUInt16BE(2); offset = 4 }
|
|
104
|
+
else if (length === 127) { if (carry.length < 10) return; length = Number(carry.readBigUInt64BE(2)); offset = 10 }
|
|
105
|
+
const maskLength = masked ? 4 : 0
|
|
106
|
+
if (carry.length < offset + maskLength + length) return
|
|
107
|
+
let payload = carry.subarray(offset + maskLength, offset + maskLength + length)
|
|
108
|
+
if (masked) {
|
|
109
|
+
const mask = carry.subarray(offset, offset + 4)
|
|
110
|
+
payload = Buffer.from(payload)
|
|
111
|
+
for (let index = 0; index < payload.length; index += 1) payload[index] ^= mask[index % 4]
|
|
112
|
+
}
|
|
113
|
+
carry = carry.subarray(offset + maskLength + length)
|
|
114
|
+
if (opcode === 0x8) { note('server closed the connection'); finish(false, 'the server closed before the flow completed'); return }
|
|
115
|
+
if (opcode === 0x9) { socket.write(Buffer.concat([Buffer.from([0x8a, 0x80]), randomBytes(4)])); continue }
|
|
116
|
+
if (opcode !== 0x1) continue
|
|
117
|
+
onFrame(payload.toString('utf8'))
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function onFrame(text) {
|
|
122
|
+
let frame
|
|
123
|
+
try { frame = JSON.parse(text) } catch { fail('the server sent a frame that is not JSON'); return }
|
|
124
|
+
report.frames.push({ kind: frame.kind, bytes: text.length })
|
|
125
|
+
if (!json) console.log(' <- ' + frame.kind + (frame.kind === 'history' ? ' events=' + String(frame.events?.length ?? 0) + ' cursor=' + String(frame.cursor) + ' bytes=' + String(frame.bytes) : ''))
|
|
126
|
+
if (frame.kind === 'error') { fail('server error frame: ' + JSON.stringify(frame).slice(0, 160)); return }
|
|
127
|
+
if (frame.kind === 'paired') { note('paired'); socket.write(encodeClientText(JSON.stringify({ type: 'sessions' }))); return }
|
|
128
|
+
if (frame.kind === 'sessions') {
|
|
129
|
+
note('sessions: ' + String(frame.sessions?.length ?? 0))
|
|
130
|
+
// A named Session lets a caller exercise a Session known to hold events, which the default
|
|
131
|
+
// first-two choice cannot guarantee.
|
|
132
|
+
const ids = sessionId !== '' ? [sessionId] : (frame.sessions ?? []).map((session) => session.sessionId).slice(0, 2)
|
|
133
|
+
for (const id of ids) { sentHistory.add(id); socket.write(encodeClientText(JSON.stringify({ type: 'history', sessionId: id, view: 'conversation' }))) }
|
|
134
|
+
if (ids.length === 0) { note('no Sessions to request'); finish(true) }
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
if (frame.kind === 'history') {
|
|
138
|
+
note('history for ' + String(frame.sessionId).slice(0, 24) + ': ' + String(frame.events?.length ?? 0) + ' events'
|
|
139
|
+
+ ' cursor=' + String(frame.cursor) + ' hasMore=' + String(frame.hasMore)
|
|
140
|
+
+ ' bytes=' + String(frame.bytes) + ' formatVersion=' + String(frame.historyFormatVersion)
|
|
141
|
+
+ ' view=' + String(frame.view))
|
|
142
|
+
if ((frame.events?.length ?? 0) > 0) {
|
|
143
|
+
const sequenced = frame.events.every((event) => typeof event.seq === 'number')
|
|
144
|
+
if (!sequenced) fail('history carried an event without a seq')
|
|
145
|
+
}
|
|
146
|
+
const done = [...sentHistory].every((id) => report.frames.some((seen) => seen.kind === 'history'))
|
|
147
|
+
if (done) finish(report.errors.length === 0)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
socket.on('connect', () => {
|
|
152
|
+
const key = randomBytes(16).toString('base64')
|
|
153
|
+
const protocols = code === '' ? 'dsh-mobile-v1' : 'dsh-mobile-v1, dsh-pair.' + code
|
|
154
|
+
socket.write(
|
|
155
|
+
'GET ' + path + ' HTTP/1.1\r\n'
|
|
156
|
+
+ 'Host: ' + host + ':' + String(port) + '\r\n'
|
|
157
|
+
+ 'Upgrade: websocket\r\n'
|
|
158
|
+
+ 'Connection: Upgrade\r\n'
|
|
159
|
+
+ 'Sec-WebSocket-Version: 13\r\n'
|
|
160
|
+
+ 'Sec-WebSocket-Key: ' + key + '\r\n'
|
|
161
|
+
+ 'Sec-WebSocket-Protocol: ' + protocols + '\r\n'
|
|
162
|
+
+ 'X-DSH-Device-ID: harness-' + randomBytes(8).toString('hex') + '\r\n'
|
|
163
|
+
+ '\r\n',
|
|
164
|
+
)
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
socket.on('data', (chunk) => {
|
|
168
|
+
if (!handshakeDone) {
|
|
169
|
+
carry = Buffer.concat([carry, chunk])
|
|
170
|
+
const end = carry.indexOf('\r\n\r\n')
|
|
171
|
+
if (end < 0) return
|
|
172
|
+
const head = carry.subarray(0, end).toString('utf8')
|
|
173
|
+
const status = head.split('\r\n')[0]
|
|
174
|
+
report.handshake = status
|
|
175
|
+
if (!json) console.log(' handshake: ' + status)
|
|
176
|
+
if (!head.startsWith('HTTP/1.1 101')) { finish(false, 'handshake refused: ' + status); return }
|
|
177
|
+
handshakeDone = true
|
|
178
|
+
note('upgraded')
|
|
179
|
+
carry = carry.subarray(end + 4)
|
|
180
|
+
} else {
|
|
181
|
+
carry = Buffer.concat([carry, chunk])
|
|
182
|
+
}
|
|
183
|
+
readFrames()
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
socket.on('error', (error) => finish(false, 'socket error: ' + error.message))
|
|
187
|
+
setTimeout(() => finish(false, 'timed out after ' + String(timeoutMs) + 'ms'), timeoutMs)
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* session-cache-proxy.mjs [--port 7091] [--root /var/lib/dsh-mobile-cache]
|
|
20
20
|
* [--upstream http://127.0.0.1:3080] [--ws-path /ws/mobile]
|
|
21
21
|
*/
|
|
22
|
-
import { createHash } from 'node:crypto'
|
|
22
|
+
import { createHash, randomBytes } from 'node:crypto'
|
|
23
23
|
import { execFileSync } from 'node:child_process'
|
|
24
24
|
import { existsSync, readFileSync, statSync } from 'node:fs'
|
|
25
25
|
import { createServer, request as httpRequest } from 'node:http'
|
|
@@ -34,6 +34,9 @@ let upstream = process.env.DSH_UPSTREAM ?? 'http://127.0.0.1:3080'
|
|
|
34
34
|
let upstreamHost = process.env.DSH_UPSTREAM_HOST ?? ''
|
|
35
35
|
let wsPath = process.env.DSH_WS_PATH ?? '/ws/mobile'
|
|
36
36
|
let tokensFile = process.env.DSH_CACHE_TOKENS ?? ''
|
|
37
|
+
// When set, every frame's kind and size is logged. Used to see what a client actually asked for and
|
|
38
|
+
// what it was given, which is the only way to tell a client that is waiting from one that is retrying.
|
|
39
|
+
const trace = process.env.DSH_PROXY_TRACE === '1'
|
|
37
40
|
for (let index = 0; index < argv.length; index += 1) {
|
|
38
41
|
const arg = argv[index]
|
|
39
42
|
if (arg === '--port') { port = Number(argv[++index]); continue }
|
|
@@ -59,7 +62,64 @@ function tokenHashes() {
|
|
|
59
62
|
} catch { return new Set() }
|
|
60
63
|
}
|
|
61
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Write to a socket that may already be gone.
|
|
67
|
+
*
|
|
68
|
+
* `destroyed` is checked before the write but the peer can disappear between the check and the
|
|
69
|
+
* write, and a write to a closed socket emits an 'error' that, unhandled, ends the process — one
|
|
70
|
+
* phone hanging up took this proxy down (EPIPE from FrameReader.onFrame). Every write on a
|
|
71
|
+
* connection the peer controls goes through here.
|
|
72
|
+
*
|
|
73
|
+
* @param socket - the stream to write to; may be destroyed or undefined.
|
|
74
|
+
* @param payload - bytes or text to send.
|
|
75
|
+
* @returns whether the write was attempted.
|
|
76
|
+
*/
|
|
77
|
+
function safeWrite(socket, payload) {
|
|
78
|
+
if (socket === undefined || socket === null || socket.destroyed === true || socket.writable === false) return false
|
|
79
|
+
try {
|
|
80
|
+
socket.write(payload)
|
|
81
|
+
return true
|
|
82
|
+
} catch {
|
|
83
|
+
try { socket.destroy() } catch {}
|
|
84
|
+
return false
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
62
88
|
const frame = {
|
|
89
|
+
/**
|
|
90
|
+
* Wrap an already-serialized payload in one masked text frame.
|
|
91
|
+
*
|
|
92
|
+
* The upstream socket from an 'upgrade' event is a raw TCP stream, not a WebSocket object, so
|
|
93
|
+
* anything written to it must already be framed. Bare JSON corrupts the header instead: the first
|
|
94
|
+
* payload byte reads as the frame header, and '{' is 0x7B = FIN 0, RSV1 1, RSV2 1, RSV3 1,
|
|
95
|
+
* opcode 0xB, which the receiver rejects as "RSV2 and RSV3 must be clear".
|
|
96
|
+
*
|
|
97
|
+
* It must also be *masked*. In this direction the proxy is the client, and RFC 6455 requires
|
|
98
|
+
* client-to-server frames to carry a mask — a server rejects an unmasked one with
|
|
99
|
+
* WS_ERR_EXPECTED_MASK and closes the connection, which the phone sees as a failed login.
|
|
100
|
+
*/
|
|
101
|
+
encodeClientText(text) {
|
|
102
|
+
const payload = Buffer.from(text, 'utf8')
|
|
103
|
+
const mask = randomBytes(4)
|
|
104
|
+
const length = payload.length
|
|
105
|
+
let header
|
|
106
|
+
if (length < 126) {
|
|
107
|
+
header = Buffer.alloc(2)
|
|
108
|
+
header[1] = 0x80 | length
|
|
109
|
+
} else if (length < 65536) {
|
|
110
|
+
header = Buffer.alloc(4)
|
|
111
|
+
header[1] = 0x80 | 126
|
|
112
|
+
header.writeUInt16BE(length, 2)
|
|
113
|
+
} else {
|
|
114
|
+
header = Buffer.alloc(10)
|
|
115
|
+
header[1] = 0x80 | 127
|
|
116
|
+
header.writeBigUInt64BE(BigInt(length), 2)
|
|
117
|
+
}
|
|
118
|
+
header[0] = 0x81
|
|
119
|
+
const masked = Buffer.from(payload)
|
|
120
|
+
for (let index = 0; index < masked.length; index += 1) masked[index] ^= mask[index % 4]
|
|
121
|
+
return Buffer.concat([header, mask, masked])
|
|
122
|
+
},
|
|
63
123
|
/** Encode one server frame. Payloads are text by construction. */
|
|
64
124
|
encode(value) {
|
|
65
125
|
const payload = Buffer.from(JSON.stringify(value), 'utf8')
|
|
@@ -146,6 +206,11 @@ function cachedEvents(sessionId) {
|
|
|
146
206
|
|
|
147
207
|
// --- history shaping, mirroring the gateway so a phone cannot tell the difference ---
|
|
148
208
|
//
|
|
209
|
+
// The gateway announces its Session format in the hello frame and the client checks the history
|
|
210
|
+
// answer against it. This must match that announcement, not the newest format the cache happens to
|
|
211
|
+
// hold: the cache stores whatever the desktop wrote, while the handshake is what the client trusts.
|
|
212
|
+
const GATEWAY_HISTORY_FORMAT_VERSION = 3
|
|
213
|
+
//
|
|
149
214
|
// The gateway caps a page at 256 KB on its opening read because every byte travels the desktop's
|
|
150
215
|
// upstream. Here the bytes are already local and the wire is the VPS's own 8.3 MB/s downstream, so
|
|
151
216
|
// the cap only costs round trips: a 10866-event Session would take 118 pages, and at 173 ms RTT
|
|
@@ -225,7 +290,11 @@ function historyFrame(message, cached) {
|
|
|
225
290
|
...(message.view === 'conversation' ? { view: 'conversation' } : {}),
|
|
226
291
|
...(capped.hasMore && oldest !== undefined ? { nextBeforeSeq: oldest } : {}),
|
|
227
292
|
cursor: lastSeq,
|
|
228
|
-
|
|
293
|
+
// Must equal what the gateway announced in its own hello frame, which is its
|
|
294
|
+
// SESSION_FORMAT_VERSION. The client validates the version on the history answer against the one
|
|
295
|
+
// it was told, so announcing 4 while the gateway says 3 makes it reject every page and stay on
|
|
296
|
+
// "loading history". The wire can carry either; the handshake is what must agree.
|
|
297
|
+
historyFormatVersion: GATEWAY_HISTORY_FORMAT_VERSION,
|
|
229
298
|
}
|
|
230
299
|
}
|
|
231
300
|
|
|
@@ -269,7 +338,7 @@ server.on('upgrade', (request, socket, head) => {
|
|
|
269
338
|
// asked for dsh-mobile-v1 must see it echoed or it will not treat the socket as established.
|
|
270
339
|
const accept = response2.headers['sec-websocket-accept']
|
|
271
340
|
const protocol = response2.headers['sec-websocket-protocol']
|
|
272
|
-
socket
|
|
341
|
+
safeWrite(socket,
|
|
273
342
|
'HTTP/1.1 101 Switching Protocols\r\n'
|
|
274
343
|
+ 'Upgrade: websocket\r\n'
|
|
275
344
|
+ 'Connection: Upgrade\r\n'
|
|
@@ -277,11 +346,15 @@ server.on('upgrade', (request, socket, head) => {
|
|
|
277
346
|
+ (protocol === undefined ? '' : 'Sec-WebSocket-Protocol: ' + String(protocol) + '\r\n')
|
|
278
347
|
+ '\r\n',
|
|
279
348
|
)
|
|
280
|
-
|
|
281
|
-
if (
|
|
349
|
+
// `head` holds leftover bytes the client sent after its handshake, already framed.
|
|
350
|
+
if (head.length > 0) safeWrite(liveSocket, head)
|
|
351
|
+
if (liveHead.length > 0) safeWrite(socket, liveHead)
|
|
282
352
|
|
|
283
353
|
const liveReader = new FrameReader((text) => {
|
|
284
|
-
if (
|
|
354
|
+
if (trace) console.log('trace upstream->client ' + text.slice(0, 200))
|
|
355
|
+
let parsed
|
|
356
|
+
try { parsed = JSON.parse(text) } catch { return }
|
|
357
|
+
safeWrite(socket, frame.encode(parsed))
|
|
285
358
|
}, () => {})
|
|
286
359
|
liveSocket.on('data', (chunk) => liveReader.push(chunk))
|
|
287
360
|
liveSocket.on('close', () => { if (!socket.destroyed) socket.destroy() })
|
|
@@ -290,17 +363,20 @@ server.on('upgrade', (request, socket, head) => {
|
|
|
290
363
|
// The client's frames are examined: a history request for a cached Session is answered from
|
|
291
364
|
// local storage, and every other frame is forwarded unchanged.
|
|
292
365
|
const clientReader = new FrameReader((text) => {
|
|
366
|
+
if (trace) console.log('trace client->upstream ' + text.slice(0, 200))
|
|
293
367
|
let message
|
|
294
|
-
try { message = JSON.parse(text) } catch { liveSocket.
|
|
368
|
+
try { message = JSON.parse(text) } catch { safeWrite(liveSocket, frame.encodeClientText(text)); return }
|
|
295
369
|
if (message?.type === 'history' && typeof message.sessionId === 'string') {
|
|
296
370
|
let cached
|
|
297
371
|
try { cached = cachedEvents(message.sessionId) } catch { cached = undefined }
|
|
298
372
|
if (cached !== undefined && cached.events.length > 0) {
|
|
299
|
-
|
|
373
|
+
const answer = historyFrame(message, cached)
|
|
374
|
+
if (trace) console.log('trace cache->client history sessionId=' + message.sessionId + ' events=' + String(answer.events.length) + ' bytes=' + String(answer.bytes))
|
|
375
|
+
safeWrite(socket, frame.encode(answer))
|
|
300
376
|
return
|
|
301
377
|
}
|
|
302
378
|
}
|
|
303
|
-
liveSocket.
|
|
379
|
+
safeWrite(liveSocket, frame.encodeClientText(text))
|
|
304
380
|
}, (kind) => { if (kind === 'close') liveSocket.destroy() })
|
|
305
381
|
socket.on('data', (chunk) => clientReader.push(chunk))
|
|
306
382
|
})
|
|
@@ -313,20 +389,17 @@ server.on('upgrade', (request, socket, head) => {
|
|
|
313
389
|
response2.on('end', () => {
|
|
314
390
|
if (socket.destroyed) return
|
|
315
391
|
const body = Buffer.concat(chunks)
|
|
316
|
-
socket
|
|
317
|
-
socket.end(body)
|
|
392
|
+
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
393
|
})
|
|
319
394
|
})
|
|
320
395
|
live.on('error', () => {
|
|
321
396
|
if (socket.destroyed) return
|
|
322
|
-
socket
|
|
323
|
-
socket.end()
|
|
397
|
+
if (safeWrite(socket, 'HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\n\r\n')) socket.end()
|
|
324
398
|
})
|
|
325
399
|
live.setTimeout(20000, () => {
|
|
326
400
|
live.destroy()
|
|
327
401
|
if (!socket.destroyed) {
|
|
328
|
-
socket
|
|
329
|
-
socket.end()
|
|
402
|
+
if (safeWrite(socket, 'HTTP/1.1 504 Gateway Timeout\r\nConnection: close\r\n\r\n')) socket.end()
|
|
330
403
|
}
|
|
331
404
|
})
|
|
332
405
|
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.2",
|
|
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": [
|