@goodandready/dsh-lanmode 0.6.1 → 0.6.3
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/bridge.js +27 -3
- package/lib/loopback-source.js +49 -0
- package/lib/tls.js +35 -5
- package/package.json +1 -1
package/lib/bridge.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
import http from 'node:http'
|
|
19
19
|
import https from 'node:https'
|
|
20
|
+
import { forceLoopback, isConnectionBundle } from './loopback-source.js'
|
|
20
21
|
|
|
21
22
|
import { allowed } from './access.js'
|
|
22
23
|
import { REFUSED, isPrivileged } from './privileged.js'
|
|
@@ -109,17 +110,40 @@ export function startDirectBridge(ctx, options) {
|
|
|
109
110
|
req.setTimeout(streamTimeoutMs)
|
|
110
111
|
res.setTimeout(streamTimeoutMs)
|
|
111
112
|
|
|
113
|
+
// Сборку с признаком «своя машина» правим на лету, всё остальное идёт
|
|
114
|
+
// кусками как шло. Для неё просим несжатый ответ: иначе пришлось бы
|
|
115
|
+
// распаковывать ради одной строки.
|
|
116
|
+
const patching = isConnectionBundle(req.url)
|
|
117
|
+
const headers = rewritten(req.headers, authority)
|
|
118
|
+
if (patching) headers['accept-encoding'] = 'identity'
|
|
119
|
+
|
|
112
120
|
const upstream = http.request({
|
|
113
121
|
host: '127.0.0.1',
|
|
114
122
|
port: upstreamPort,
|
|
115
123
|
method: req.method,
|
|
116
124
|
path: req.url,
|
|
117
|
-
headers
|
|
125
|
+
headers,
|
|
118
126
|
timeout: streamTimeoutMs || undefined,
|
|
119
127
|
}, (answer) => {
|
|
120
128
|
answer.setTimeout(streamTimeoutMs)
|
|
121
|
-
|
|
122
|
-
|
|
129
|
+
if (!patching) {
|
|
130
|
+
res.writeHead(answer.statusCode || 502, answer.headers)
|
|
131
|
+
answer.pipe(res)
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
const parts = []
|
|
135
|
+
answer.on('data', (chunk) => parts.push(chunk))
|
|
136
|
+
answer.on('end', () => {
|
|
137
|
+
const done = forceLoopback(Buffer.concat(parts).toString('utf8'))
|
|
138
|
+
if (!done.changed) options.log('сборка соединения пришла без известного вычисления признака: отдаю как есть')
|
|
139
|
+
const body = Buffer.from(done.source, 'utf8')
|
|
140
|
+
const out = { ...answer.headers }
|
|
141
|
+
out['content-length'] = String(body.length)
|
|
142
|
+
delete out['content-encoding']
|
|
143
|
+
res.writeHead(answer.statusCode || 502, out)
|
|
144
|
+
res.end(body)
|
|
145
|
+
})
|
|
146
|
+
answer.on('error', () => { if (!res.writableEnded) res.end() })
|
|
123
147
|
})
|
|
124
148
|
upstream.on('error', () => {
|
|
125
149
|
if (!res.headersSent) res.writeHead(502, { 'content-type': 'text/plain; charset=utf-8' })
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Признак «страница открыта с этой же машины» — в самом источнике.
|
|
2
|
+
//
|
|
3
|
+
// Ядро вычисляет его в браузере один раз, при сборке объекта соединения:
|
|
4
|
+
//
|
|
5
|
+
// isLoopback: pageLocation === void 0 || isLoopbackHostname(pageLocation.hostname)
|
|
6
|
+
//
|
|
7
|
+
// Дальше это обычное свойство обычного объекта, и подменить его на странице
|
|
8
|
+
// можно — плагин так и делает. Но подмена успевает не везде: разделы настроек
|
|
9
|
+
// привязываются в момент своего apply, и тот, кто привязался раньше нашего
|
|
10
|
+
// клиентского кода, уже запомнил «нет». Вкладка «Модели» — как раз такой
|
|
11
|
+
// случай: она читает раздел с провайдерами до нас, получает пустое хранилище
|
|
12
|
+
// вместо файла, и девять заведённых провайдеров исчезают из списка вместе с
|
|
13
|
+
// возможностью их выбрать.
|
|
14
|
+
//
|
|
15
|
+
// Гонку не выиграть перестановкой: порядок загрузки задаёт не плагин. Поэтому
|
|
16
|
+
// значение правится там, где его больше никто не обгонит — в самом файле,
|
|
17
|
+
// пока он идёт через мост. Одна строка, одно вхождение, остальной файл
|
|
18
|
+
// нетронут.
|
|
19
|
+
//
|
|
20
|
+
// Настоящее значение при этом не теряется: клиентская часть плагина отдаёт его
|
|
21
|
+
// тем, кому оно действительно нужно — выдаче результатов работы, где флаг
|
|
22
|
+
// решает, открывать ли путь на своей машине или на серверной.
|
|
23
|
+
|
|
24
|
+
/** Путь, по которому ядро отдаёт сборку с этим вычислением. */
|
|
25
|
+
export const CONNECTION_BUNDLE = '/plugins/@deepseek-ai/dsh-client-connection/client.js'
|
|
26
|
+
|
|
27
|
+
/** Ровно то выражение, которое считает признак. */
|
|
28
|
+
const COMPUTED = 'isLoopback: pageLocation === void 0 || isLoopbackHostname(pageLocation.hostname),'
|
|
29
|
+
|
|
30
|
+
/** Чем его заменяем. */
|
|
31
|
+
const ALWAYS = 'isLoopback: true,'
|
|
32
|
+
|
|
33
|
+
/** Тот ли это запрос: путь сверяем без строки запроса — там висит ?rev=... */
|
|
34
|
+
export function isConnectionBundle(url) {
|
|
35
|
+
const path = String(url ?? '').split('?')[0]
|
|
36
|
+
return path === CONNECTION_BUNDLE
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Заменить вычисление признака на «да».
|
|
41
|
+
*
|
|
42
|
+
* @param source текст сборки
|
|
43
|
+
* @returns `{ source, changed }` — исходник и признак того, что замена нашлась
|
|
44
|
+
*/
|
|
45
|
+
export function forceLoopback(source) {
|
|
46
|
+
const text = String(source)
|
|
47
|
+
if (!text.includes(COMPUTED)) return { source: text, changed: false }
|
|
48
|
+
return { source: text.replace(COMPUTED, ALWAYS), changed: true }
|
|
49
|
+
}
|
package/lib/tls.js
CHANGED
|
@@ -28,20 +28,50 @@ const RENEW_BEFORE_MS = 30 * 24 * 60 * 60 * 1000
|
|
|
28
28
|
/** Сколько живёт выпущенный нами сертификат. */
|
|
29
29
|
const LIFETIME_DAYS = 397
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Интерфейсы, живущие ровно столько, сколько живёт контейнер.
|
|
33
|
+
*
|
|
34
|
+
* Докер заводит виртуальную пару на каждый контейнер и мост на каждую сеть.
|
|
35
|
+
* Адреса на них видны только изнутри контейнера, а появляются и исчезают по
|
|
36
|
+
* десятку за день.
|
|
37
|
+
*/
|
|
38
|
+
const EPHEMERAL_INTERFACE = /^(veth|br-|docker|virbr)/i
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Связь-локальный адрес: браузеру недоступен в принципе.
|
|
42
|
+
*
|
|
43
|
+
* Такому адресу нужен индекс зоны — `[fe80::1%eth0]`, — и ни один человек
|
|
44
|
+
* этого в строку не набирает.
|
|
45
|
+
*/
|
|
46
|
+
function linkLocal(address) {
|
|
47
|
+
return /^fe80:/i.test(address) || /^169\.254\./.test(address)
|
|
48
|
+
}
|
|
49
|
+
|
|
31
50
|
/**
|
|
32
51
|
* Адреса, по которым к этой машине могут обратиться.
|
|
33
52
|
*
|
|
34
|
-
* В сертификат идут
|
|
35
|
-
* и сертификат, выписанный на одно имя, ругается на все остальные —
|
|
36
|
-
* того, как его один раз приняли.
|
|
53
|
+
* В сертификат идут все сразу: браузер сверяет тот адрес, который набрали в
|
|
54
|
+
* строке, и сертификат, выписанный на одно имя, ругается на все остальные —
|
|
55
|
+
* даже после того, как его один раз приняли.
|
|
56
|
+
*
|
|
57
|
+
* Но «все» — это не каждый адрес из системы. Летучие сюда попадать не должны,
|
|
58
|
+
* и дело не в размере списка: сертификат считается негодным, как только на
|
|
59
|
+
* машине появился адрес, которого в нём нет. Один поднятый контейнер — и
|
|
60
|
+
* сертификат перевыпускается, отпечаток меняется, браузер требует подтвердить
|
|
61
|
+
* страницу заново. Обещание «подтвердить один раз» держится ровно до
|
|
62
|
+
* следующего `docker compose up`.
|
|
63
|
+
*
|
|
64
|
+
* Кому нужен адрес, которого здесь нет, — для этого есть `tlsHosts`.
|
|
37
65
|
*/
|
|
38
66
|
export function localAddresses() {
|
|
39
67
|
const found = new Set(['localhost', '127.0.0.1', '::1'])
|
|
40
68
|
const interfaces = os.networkInterfaces()
|
|
41
|
-
for (const list of Object.
|
|
69
|
+
for (const [name, list] of Object.entries(interfaces)) {
|
|
70
|
+
if (EPHEMERAL_INTERFACE.test(name)) continue
|
|
42
71
|
for (const item of list ?? []) {
|
|
43
72
|
if (!item || item.internal) continue
|
|
44
|
-
if (item.address
|
|
73
|
+
if (!item.address || linkLocal(item.address)) continue
|
|
74
|
+
found.add(item.address)
|
|
45
75
|
}
|
|
46
76
|
}
|
|
47
77
|
const hostname = os.hostname()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-lanmode",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
4
4
|
"description": "LAN and reverse-proxy access for the DeepSeek Harness Web UI: returns the settings service on pages that are not localhost, fills in the Web APIs the browser withholds on plain HTTP, and can open a listener of its own so nothing else is needed.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|