@goodandready/dsh-lanmode 0.6.0 → 0.6.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/lib/bridge.js CHANGED
@@ -154,7 +154,17 @@ export function startDirectBridge(ctx, options) {
154
154
  const lines = ['HTTP/1.1 101 Switching Protocols']
155
155
  for (const [key, value] of Object.entries(answer.headers)) lines.push(key + ': ' + value)
156
156
  socket.write(lines.join('\r\n') + '\r\n\r\n')
157
- if (upstreamHead && upstreamHead.length) socket.unshift(upstreamHead)
157
+ // Хвосты рукопожатия байты, пришедшие в том же пакете, что и ответ. Их
158
+ // надо переложить в ПРОТИВОПОЛОЖНУЮ сторону, и это то место, где мост
159
+ // ошибался с первого выпуска: upstreamHead уходил в socket.unshift, то
160
+ // есть в читающую сторону клиентского сокета. Мост притворялся, будто эти
161
+ // байты прислал браузер, и по конвейеру возвращал их харнессу. Первый
162
+ // кадр — а харнесс шлёт его сразу после рукопожатия — до браузера не
163
+ // доезжал никогда. Снаружи это выглядело так: разговор открыт, сообщение
164
+ // отправлено, а в чате пусто.
165
+ if (upstreamHead && upstreamHead.length) socket.write(upstreamHead)
166
+ if (head && head.length) upstreamSocket.write(head)
167
+
158
168
  upstreamSocket.pipe(socket)
159
169
  socket.pipe(upstreamSocket)
160
170
  const drop = () => { try { upstreamSocket.destroy() } catch (already) { /* уже мертво */ } }
@@ -162,7 +172,8 @@ export function startDirectBridge(ctx, options) {
162
172
  socket.on('close', drop)
163
173
  })
164
174
  upstream.on('error', () => { try { socket.destroy() } catch (already) { /* уже мертво */ } })
165
- if (head && head.length) upstream.write(head)
175
+ // head сюда не пишем: это байты уже установленного соединения, а не тело
176
+ // запроса. Их место — в upstreamSocket, сразу после рукопожатия.
166
177
  upstream.end()
167
178
  }
168
179
 
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.values(interfaces)) {
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) found.add(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.0",
3
+ "version": "0.6.2",
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",