@goodandready/dsh-voice 0.9.42 → 0.9.43
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/lib/http-util.js +133 -40
- package/package.json +1 -1
package/lib/http-util.js
CHANGED
|
@@ -102,11 +102,117 @@ export function readBody(req, maxBytes) {
|
|
|
102
102
|
export function isLoopback(addr) {
|
|
103
103
|
if (!addr) return false
|
|
104
104
|
const clean = String(addr).replace(/^::ffff:/, '')
|
|
105
|
-
return clean === '127.0.0.1' || clean === '::1' || clean === 'localhost'
|
|
105
|
+
return clean === '127.0.0.1' || clean === '::1' || clean === 'localhost' || clean === '[::1]'
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
/**
|
|
109
|
-
* Check whether
|
|
109
|
+
* Check whether an incoming HTTP request is from the DeepSeek desktop shell (#12, #295).
|
|
110
|
+
* Matches patterns from @deepseek-ai/dsh-desktop and official DSH shell wrappers.
|
|
111
|
+
* @param {import('node:http').IncomingMessage} req
|
|
112
|
+
* @returns {boolean}
|
|
113
|
+
*/
|
|
114
|
+
export function isDesktopShellRequest(req) {
|
|
115
|
+
if (!req || !req.headers) return false
|
|
116
|
+
const ua = String(req.headers['user-agent'] || '')
|
|
117
|
+
const cookie = String(req.headers.cookie || '')
|
|
118
|
+
if (ua.includes('@deepseek-ai/dsh-desktop/') && cookie.includes('dsh-auth-')) return true
|
|
119
|
+
const remote = req.socket?.remoteAddress || req.connection?.remoteAddress || ''
|
|
120
|
+
if (ua.includes('dsh-desktop') && isLoopback(remote)) return true
|
|
121
|
+
return false
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Check whether an incoming HTTP request carries a DeepSeek Harness session cookie (#295).
|
|
126
|
+
* DSH core sets 'dsh-auth-<hash>=v1...' as a SameSite=Strict HttpOnly cookie.
|
|
127
|
+
* @param {import('node:http').IncomingMessage} req
|
|
128
|
+
* @returns {boolean}
|
|
129
|
+
*/
|
|
130
|
+
export function hasDshAuthCookie(req) {
|
|
131
|
+
if (!req || !req.headers) return false
|
|
132
|
+
const cookie = String(req.headers.cookie || '')
|
|
133
|
+
return cookie.includes('dsh-auth-')
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Split host header or URL host into lowercase hostname and port string.
|
|
138
|
+
* @param {string} rawHost
|
|
139
|
+
* @returns {{ hostname: string, port: string }}
|
|
140
|
+
*/
|
|
141
|
+
function parseHost(rawHost) {
|
|
142
|
+
if (!rawHost) return { hostname: '', port: '' }
|
|
143
|
+
const clean = String(rawHost).trim().toLowerCase()
|
|
144
|
+
const ipv6Match = clean.match(/^\[([^\]]+)\](?::(\d+))?$/)
|
|
145
|
+
let hostname = ''
|
|
146
|
+
let port = ''
|
|
147
|
+
if (ipv6Match) {
|
|
148
|
+
hostname = ipv6Match[1]
|
|
149
|
+
port = ipv6Match[2] || ''
|
|
150
|
+
} else {
|
|
151
|
+
const parts = clean.split(':')
|
|
152
|
+
hostname = parts[0]
|
|
153
|
+
port = parts[1] || ''
|
|
154
|
+
}
|
|
155
|
+
if (port === '80' || port === '443') {
|
|
156
|
+
port = ''
|
|
157
|
+
}
|
|
158
|
+
return { hostname, port }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Verify whether an Origin or Referer URL is allowed for the given Host header (#12, #292, #295).
|
|
163
|
+
* @param {string} urlStr
|
|
164
|
+
* @param {string} hostHeader
|
|
165
|
+
* @param {import('node:http').IncomingMessage} req
|
|
166
|
+
* @returns {boolean}
|
|
167
|
+
*/
|
|
168
|
+
export function isOriginOrRefererAllowed(urlStr, hostHeader, req) {
|
|
169
|
+
if (!urlStr || typeof urlStr !== 'string') return false
|
|
170
|
+
const trimmed = urlStr.trim()
|
|
171
|
+
if (!trimmed) return false
|
|
172
|
+
|
|
173
|
+
const remote = req?.socket?.remoteAddress || req?.connection?.remoteAddress || ''
|
|
174
|
+
const remoteIsLoopback = isLoopback(remote)
|
|
175
|
+
|
|
176
|
+
// 1. Literal 'null' origin (sent by Chromium/Electron for local files or sandboxed views)
|
|
177
|
+
if (trimmed === 'null') {
|
|
178
|
+
return remoteIsLoopback && (isDesktopShellRequest(req) || hasDshAuthCookie(req))
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
const u = new URL(trimmed)
|
|
183
|
+
|
|
184
|
+
// 2. Official DeepSeek desktop custom app scheme (e.g. dsh-app://app or dsh-app://localhost)
|
|
185
|
+
if (u.protocol === 'dsh-app:') {
|
|
186
|
+
const appHost = u.host.toLowerCase()
|
|
187
|
+
const allowedAppHost = !appHost || appHost === 'app' || appHost === 'localhost'
|
|
188
|
+
return allowedAppHost && remoteIsLoopback
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// 3. Web schemes: HTTP and HTTPS
|
|
192
|
+
if (u.protocol === 'http:' || u.protocol === 'https:') {
|
|
193
|
+
if (!hostHeader) return false
|
|
194
|
+
const target = parseHost(hostHeader)
|
|
195
|
+
const origin = parseHost(u.host)
|
|
196
|
+
|
|
197
|
+
// Exact match
|
|
198
|
+
if (origin.hostname === target.hostname && origin.port === target.port) {
|
|
199
|
+
return true
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Loopback equivalence (e.g. localhost:3080 vs 127.0.0.1:3080 or ::1:3080)
|
|
203
|
+
if (isLoopback(origin.hostname) && isLoopback(target.hostname) && origin.port === target.port) {
|
|
204
|
+
return true
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return false
|
|
209
|
+
} catch {
|
|
210
|
+
return false
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Check whether a request originates from the same origin as the Host header or trusted desktop shell (#12, #292, #295).
|
|
110
216
|
* @param {import('node:http').IncomingMessage} req
|
|
111
217
|
* @returns {boolean}
|
|
112
218
|
*/
|
|
@@ -118,38 +224,33 @@ export function hasSameOrigin(req) {
|
|
|
118
224
|
const host = String(req.headers.host || '').trim().toLowerCase()
|
|
119
225
|
if (!host) return false
|
|
120
226
|
|
|
121
|
-
const origin = String(req.headers.origin || '').trim()
|
|
227
|
+
const origin = String(req.headers.origin || '').trim()
|
|
122
228
|
if (origin) {
|
|
123
|
-
|
|
124
|
-
const u = new URL(origin)
|
|
125
|
-
return u.host.toLowerCase() === host
|
|
126
|
-
} catch {
|
|
127
|
-
return false
|
|
128
|
-
}
|
|
229
|
+
return isOriginOrRefererAllowed(origin, host, req)
|
|
129
230
|
}
|
|
130
231
|
|
|
131
|
-
const referer = String(req.headers.referer || '').trim()
|
|
232
|
+
const referer = String(req.headers.referer || '').trim()
|
|
132
233
|
if (referer) {
|
|
133
|
-
|
|
134
|
-
const u = new URL(referer)
|
|
135
|
-
return u.host.toLowerCase() === host
|
|
136
|
-
} catch {
|
|
137
|
-
return false
|
|
138
|
-
}
|
|
234
|
+
return isOriginOrRefererAllowed(referer, host, req)
|
|
139
235
|
}
|
|
140
236
|
|
|
237
|
+
// Same-origin metadata from browser Fetch API
|
|
141
238
|
if (secSite === 'same-origin') return true
|
|
142
239
|
|
|
240
|
+
// DeepSeek Desktop shell or authenticated DSH session on loopback/same-host
|
|
241
|
+
const remote = req.socket?.remoteAddress || req.connection?.remoteAddress || ''
|
|
242
|
+
if (isDesktopShellRequest(req)) return true
|
|
243
|
+
if (hasDshAuthCookie(req) && (isLoopback(remote) || host)) return true
|
|
244
|
+
|
|
143
245
|
return false
|
|
144
246
|
}
|
|
145
247
|
|
|
146
248
|
/**
|
|
147
249
|
* Verify that the HTTP request originates from a trusted caller.
|
|
148
|
-
* Fail-closed security guard for state-changing or quota-consuming routes (#117, #173, #277, #292).
|
|
250
|
+
* Fail-closed security guard for state-changing or quota-consuming routes (#117, #173, #277, #292, #295).
|
|
149
251
|
* - Routes marked loopbackOnly strictly reject non-loopback callers.
|
|
150
|
-
* - Mutating requests (POST, PUT, PATCH) or WS upgrades require validated same-origin or valid session token (#292).
|
|
151
|
-
*
|
|
152
|
-
* - Non-loopback LAN requests require valid session token for mutating operations (#277).
|
|
252
|
+
* - Mutating requests (POST, PUT, PATCH) or WS upgrades require validated same-origin or valid session token (#12, #292, #295).
|
|
253
|
+
* Raw LAN callers or reverse bridges without Origin/Referer/Cookie and without token are fail-closed rejected.
|
|
153
254
|
* - Non-mutating GET requests require loopback or same-origin.
|
|
154
255
|
* @param {import('node:http').IncomingMessage} req
|
|
155
256
|
* @param {{ loopbackOnly?: boolean }} [options]
|
|
@@ -162,27 +263,18 @@ export function isTrustedCaller(req, options = {}) {
|
|
|
162
263
|
const secSite = String(req.headers?.['sec-fetch-site'] || '').toLowerCase()
|
|
163
264
|
if (secSite === 'cross-site' || secSite === 'same-site') return false
|
|
164
265
|
|
|
165
|
-
// 2. If Origin header is present, it must strictly match the Host header
|
|
166
266
|
const host = String(req.headers?.host || '').toLowerCase()
|
|
167
|
-
|
|
267
|
+
|
|
268
|
+
// 2. If Origin header is present, verify against host or trusted desktop scheme (#12, #292, #295)
|
|
269
|
+
const origin = String(req.headers?.origin || '').trim()
|
|
168
270
|
if (origin) {
|
|
169
|
-
|
|
170
|
-
const u = new URL(origin)
|
|
171
|
-
if (u.host !== host) return false
|
|
172
|
-
} catch {
|
|
173
|
-
return false
|
|
174
|
-
}
|
|
271
|
+
if (!isOriginOrRefererAllowed(origin, host, req)) return false
|
|
175
272
|
}
|
|
176
273
|
|
|
177
|
-
// 3. If Referer header is present,
|
|
178
|
-
const referer = String(req.headers?.referer || '').trim()
|
|
274
|
+
// 3. If Referer header is present, verify against host or trusted desktop scheme (#12, #292, #295)
|
|
275
|
+
const referer = String(req.headers?.referer || '').trim()
|
|
179
276
|
if (referer) {
|
|
180
|
-
|
|
181
|
-
const u = new URL(referer)
|
|
182
|
-
if (u.host !== host) return false
|
|
183
|
-
} catch {
|
|
184
|
-
return false
|
|
185
|
-
}
|
|
277
|
+
if (!isOriginOrRefererAllowed(referer, host, req)) return false
|
|
186
278
|
}
|
|
187
279
|
|
|
188
280
|
// 4. For state-changing or data-carrying requests (POST/PUT/PATCH), require application/json or octet-stream
|
|
@@ -200,7 +292,7 @@ export function isTrustedCaller(req, options = {}) {
|
|
|
200
292
|
const remote = req.socket?.remoteAddress || req.connection?.remoteAddress || ''
|
|
201
293
|
const remoteIsLoopback = isLoopback(remote)
|
|
202
294
|
const hostName = host.replace(/:\d+$/, '')
|
|
203
|
-
const hostIsLoopback = hostName
|
|
295
|
+
const hostIsLoopback = isLoopback(hostName)
|
|
204
296
|
const isPureLocal = remoteIsLoopback && hostIsLoopback
|
|
205
297
|
|
|
206
298
|
// 5. If route strictly requires loopback (e.g. host model installer or hardware benchmark)
|
|
@@ -212,6 +304,7 @@ export function isTrustedCaller(req, options = {}) {
|
|
|
212
304
|
// 6. Mutating requests (POST/PUT/PATCH):
|
|
213
305
|
// Even from loopback (which may be proxied via dsh-lanmode bridge), requests without Origin/Referer
|
|
214
306
|
// must present a valid x-dsh-voice-token (#292).
|
|
307
|
+
// Non-loopback LAN callers strictly require a valid session token to prevent forged LAN mutations (#277).
|
|
215
308
|
if (isMutating) {
|
|
216
309
|
const incomingToken = String(req.headers?.['x-dsh-voice-token'] || req.headers?.['x-voice-token'] || '')
|
|
217
310
|
const hasValidToken = Boolean(incomingToken && verifyVoiceSessionToken(incomingToken))
|
|
@@ -221,8 +314,8 @@ export function isTrustedCaller(req, options = {}) {
|
|
|
221
314
|
return hasValidToken
|
|
222
315
|
}
|
|
223
316
|
|
|
224
|
-
// Loopback callers (or reverse bridge dsh-lanmode):
|
|
225
|
-
// If request has verified same-origin
|
|
317
|
+
// Loopback callers (local browser, desktop shell, or reverse bridge dsh-lanmode):
|
|
318
|
+
// If request has verified same-origin or desktop shell, allow it (#12, #295).
|
|
226
319
|
if (hasSameOrigin(req)) return true
|
|
227
320
|
// If request has valid session token, allow it.
|
|
228
321
|
if (hasValidToken) return true
|
|
@@ -233,7 +326,7 @@ export function isTrustedCaller(req, options = {}) {
|
|
|
233
326
|
// 7. Loopback remote address for non-mutating GET requests
|
|
234
327
|
if (remoteIsLoopback) return true
|
|
235
328
|
|
|
236
|
-
// 8. Non-loopback LAN GET requests: require same-origin
|
|
329
|
+
// 8. Non-loopback LAN GET requests: require same-origin or authenticated session
|
|
237
330
|
if (secSite === 'same-origin') return true
|
|
238
331
|
if (hasSameOrigin(req)) return true
|
|
239
332
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-voice",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.43",
|
|
4
4
|
"description": "Voice input for DeepSeek Harness: dictation chunked by pauses and voice messages, each with its own provider fallback chain (Deepgram, Groq, HuggingFace, local whisper.cpp, plus any OpenAI-compatible API of your own).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|