mc-tunnel 0.0.0-stage → 0.1.0

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.
Files changed (76) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +261 -3
  3. package/THIRD_PARTY_NOTICES.md +40 -0
  4. package/dist/cjs/constants.js +21 -0
  5. package/dist/cjs/constants.js.map +1 -0
  6. package/dist/cjs/coord.js +244 -0
  7. package/dist/cjs/coord.js.map +1 -0
  8. package/dist/cjs/easytier.js +287 -0
  9. package/dist/cjs/easytier.js.map +1 -0
  10. package/dist/cjs/electron.js +71 -0
  11. package/dist/cjs/electron.js.map +1 -0
  12. package/dist/cjs/errors.js +43 -0
  13. package/dist/cjs/errors.js.map +1 -0
  14. package/dist/cjs/guest.js +247 -0
  15. package/dist/cjs/guest.js.map +1 -0
  16. package/dist/cjs/host.js +195 -0
  17. package/dist/cjs/host.js.map +1 -0
  18. package/dist/cjs/identity.js +62 -0
  19. package/dist/cjs/identity.js.map +1 -0
  20. package/dist/cjs/index.js +62 -0
  21. package/dist/cjs/index.js.map +1 -0
  22. package/dist/cjs/net/lan.js +224 -0
  23. package/dist/cjs/net/lan.js.map +1 -0
  24. package/dist/cjs/net/mc-ping.js +150 -0
  25. package/dist/cjs/net/mc-ping.js.map +1 -0
  26. package/dist/cjs/net/ports.js +74 -0
  27. package/dist/cjs/net/ports.js.map +1 -0
  28. package/dist/cjs/package.json +3 -0
  29. package/dist/cjs/room-code.js +178 -0
  30. package/dist/cjs/room-code.js.map +1 -0
  31. package/dist/cjs/types.js +3 -0
  32. package/dist/cjs/types.js.map +1 -0
  33. package/dist/esm/constants.js +18 -0
  34. package/dist/esm/constants.js.map +1 -0
  35. package/dist/esm/coord.js +239 -0
  36. package/dist/esm/coord.js.map +1 -0
  37. package/dist/esm/easytier.js +278 -0
  38. package/dist/esm/easytier.js.map +1 -0
  39. package/dist/esm/electron.js +62 -0
  40. package/dist/esm/electron.js.map +1 -0
  41. package/dist/esm/errors.js +36 -0
  42. package/dist/esm/errors.js.map +1 -0
  43. package/dist/esm/guest.js +243 -0
  44. package/dist/esm/guest.js.map +1 -0
  45. package/dist/esm/host.js +191 -0
  46. package/dist/esm/host.js.map +1 -0
  47. package/dist/esm/identity.js +54 -0
  48. package/dist/esm/identity.js.map +1 -0
  49. package/dist/esm/index.js +33 -0
  50. package/dist/esm/index.js.map +1 -0
  51. package/dist/esm/net/lan.js +216 -0
  52. package/dist/esm/net/lan.js.map +1 -0
  53. package/dist/esm/net/mc-ping.js +146 -0
  54. package/dist/esm/net/mc-ping.js.map +1 -0
  55. package/dist/esm/net/ports.js +70 -0
  56. package/dist/esm/net/ports.js.map +1 -0
  57. package/dist/esm/package.json +3 -0
  58. package/dist/esm/room-code.js +169 -0
  59. package/dist/esm/room-code.js.map +1 -0
  60. package/dist/esm/types.js +2 -0
  61. package/dist/esm/types.js.map +1 -0
  62. package/dist/types/constants.d.ts +17 -0
  63. package/dist/types/coord.d.ts +53 -0
  64. package/dist/types/easytier.d.ts +60 -0
  65. package/dist/types/electron.d.ts +38 -0
  66. package/dist/types/errors.d.ts +13 -0
  67. package/dist/types/guest.d.ts +41 -0
  68. package/dist/types/host.d.ts +35 -0
  69. package/dist/types/identity.d.ts +9 -0
  70. package/dist/types/index.d.ts +30 -0
  71. package/dist/types/net/lan.d.ts +59 -0
  72. package/dist/types/net/mc-ping.d.ts +16 -0
  73. package/dist/types/net/ports.d.ts +4 -0
  74. package/dist/types/room-code.d.ts +26 -0
  75. package/dist/types/types.d.ts +102 -0
  76. package/package.json +57 -6
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 mc-tunnel contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,261 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
1
+ # mc-tunnel
2
+
3
+ 面向 **Electron / Node.js** 的 Minecraft 局域网联机模块。
4
+
5
+ 房主在游戏里「对局域网开放」,把 **房间码** 发给朋友;朋友输入房间码后,
6
+ 游戏「多人游戏 → 局域网世界」里就会出现这条房间,双击即可进入。
7
+
8
+ > [EasyTier](https://github.com/EasyTier/EasyTier) 以**外部子进程**方式调用完成 P2P 组网
9
+ > (详见 [合规说明](#合规说明))。
10
+
11
+ ---
12
+
13
+ ## 特性
14
+
15
+ | | |
16
+ |---|---|
17
+ | 零依赖 | 运行时只使用 Node 内置模块(`http` / `dgram` / `net` / `child_process`),不引入任何 npm 依赖 |
18
+ | 免管理员权限 | EasyTier 以 `--no-tun` 运行,不创建虚拟网卡 |
19
+ | 无需自建服务器 | 房间码自包含凭据,节点发现交给 EasyTier 公共节点;也支持完全离线直连 |
20
+ | 无需 mod | 通过伪造 Minecraft 局域网多播让原版客户端直接可见 |
21
+ | 安全 | 房间码含 CRC-12 校验;协调服务使用握手指纹 + 令牌鉴权(恒定时间比较) |
22
+ | 双格式 | 同时产出 ESM 与 CJS,附带 `.d.ts` 类型声明 |
23
+ | 可观测 | 全流程 `state` / `players` / `error` 事件,失败时携带 EasyTier 日志尾部 |
24
+
25
+ ---
26
+
27
+ ## 安装
28
+
29
+ ```bash
30
+ npm install mc-tunnel
31
+ ```
32
+
33
+ 同时需要准备 EasyTier 的两个可执行文件(本包**不打包**它们,避免体积与许可问题):
34
+
35
+ ```
36
+ easytier-core(.exe)
37
+ easytier-cli(.exe)
38
+ ```
39
+
40
+ 放置位置三选一:
41
+
42
+ 1. Electron:`resources/easytier/`(配合 electron-builder 的 `extraResources`)
43
+ 2. 任意目录 + 环境变量 `MC_TUNNEL_EASYTIER=/path/to/dir`
44
+ 3. 代码里传 `easyTier: { binDir: '/path/to/dir' }`
45
+
46
+ ---
47
+
48
+ ## 快速开始
49
+
50
+ ```ts
51
+ import { createHost, createGuest } from 'mc-tunnel';
52
+
53
+ // ---------- 房主 ----------
54
+ const host = await createHost({
55
+ mcPort: 25565, // 不传则自动监听局域网广播发现
56
+ playerName: 'Alice',
57
+ });
58
+
59
+ console.log('房间码:', host.roomCode); // 例如 7Q2M-K9XP-3RTF-WV4D
60
+
61
+ host.on('player-join', (player) => console.log(`${player.name} 加入了`));
62
+ host.on('mc-closed', () => console.log('存档已关闭,房间失效'));
63
+
64
+ // ---------- 客人 ----------
65
+ const guest = await createGuest({
66
+ roomCode: '7Q2M-K9XP-3RTF-WV4D',
67
+ playerName: 'Bob',
68
+ });
69
+
70
+ console.log('在游戏里连接:', guest.address); // 127.0.0.1:25565
71
+ // 此时「局域网世界」里已经出现房间,双击即可进入
72
+
73
+ // 退出时务必关闭,否则会残留 easytier-core 进程
74
+ await guest.close();
75
+ await host.close();
76
+ ```
77
+
78
+ ### Electron 集成
79
+
80
+ ```ts
81
+ import { app, BrowserWindow } from 'electron';
82
+ import { createHostInElectron, manageSessions } from 'mc-tunnel/electron';
83
+
84
+ const host = await createHostInElectron(app, { mcPort: 25565 });
85
+
86
+ // 应用退出时自动关闭会话,避免孤儿进程
87
+ manageSessions(app, host);
88
+ ```
89
+
90
+ `electronDefaults()` 会自动把:
91
+ - 设备标识放到 `app.getPath('userData')`
92
+ - EasyTier 目录指向 `process.resourcesPath/easytier`
93
+
94
+ `electron-builder` 配置示例:
95
+
96
+ ```jsonc
97
+ {
98
+ "extraResources": [
99
+ { "from": "vendor/easytier", "to": "easytier" }
100
+ ]
101
+ }
102
+ ```
103
+
104
+ > ⚠️ 如果使用 ASAR 打包,EasyTier 必须放在 `extraResources`(ASAR 内的可执行文件无法 `spawn`)。
105
+
106
+ ---
107
+
108
+ ## API
109
+
110
+ ### `createHost(options?): Promise<TunnelHost>`
111
+
112
+ | 参数 | 类型 | 说明 |
113
+ |---|---|---|
114
+ | `roomCode` | `string` | 指定房间码,不传则随机生成 |
115
+ | `mcPort` | `number` | 指定端口;不传则监听局域网广播自动发现 |
116
+ | `playerName` | `string` | 昵称,默认 `Host` |
117
+ | `motd` | `string` | 局域网列表显示名 |
118
+ | `coordPort` | `number` | 协调服务端口,默认 `25991`(双方必须一致) |
119
+ | `listeners` | `string[]` | 额外监听地址,如 `["tcp://0.0.0.0:11010"]` |
120
+ | `peers` | `string[]` | 直连对端,如 `["tcp://1.2.3.4:11010"]` |
121
+ | `scanTimeoutMs` | `number` | 自动发现超时,默认 `30000` |
122
+ | `dataDir` | `string` | 设备标识存储目录 |
123
+ | `easyTier` | `EasyTierOptions` | `{ binDir, publicNodes, p2pOnly }` |
124
+
125
+ 会话属性:`roomCode` `mcPort` `coordPort` `virtualIp` `phase`,方法:`players()` `close()`。
126
+
127
+ 事件:`state` `players` `player-join` `player-leave` `mc-closed` `error`。
128
+
129
+ ### `createGuest(options): Promise<TunnelGuest>`
130
+
131
+ | 参数 | 类型 | 说明 |
132
+ |---|---|---|
133
+ | `roomCode` | `string` | **必填** |
134
+ | `playerName` | `string` | 昵称,默认 `Guest` |
135
+ | `preferredLocalPort` | `number` | 期望本地端口,默认与房主端口同号 |
136
+ | `peers` | `string[]` | 直连对端,可完全绕开公共节点 |
137
+ | `timeoutMs` | `number` | 建链总超时,默认 `90000` |
138
+ | `dataDir` / `easyTier` | | 同上 |
139
+
140
+ 会话属性:`roomCode` `mcPort` `localPort` `address` `natDifficulty` `phase`,
141
+ 方法:`hostPlayers()` `close()`。
142
+
143
+ 事件:`state` `players` `server-status` `error`。
144
+
145
+ ### 工具函数
146
+
147
+ ```ts
148
+ import {
149
+ generateRoomCode, // () => string
150
+ verifyRoomCode, // (code) => boolean
151
+ normalizeRoomCode, // (input) => string | null
152
+ parseRoomCode, // (code) => { code, networkName, networkSecret }
153
+ pingMc, // (port, host?, timeoutMs?) => Promise<McStatus | null>
154
+ isPortOpen, // (port, host?, timeoutMs?) => Promise<boolean>
155
+ resolveBinaries, // (binDir?) => { core, cli }
156
+ } from 'mc-tunnel';
157
+ ```
158
+
159
+ ---
160
+
161
+ ## 工作原理
162
+
163
+ ```
164
+ 房主 客人
165
+ ──── ────
166
+ 扫描局域网广播 ← 房间码解析出网络名/密钥
167
+ ↓ ↓
168
+ EasyTier 固定虚拟 IP 10.144.144.1 EasyTier DHCP 拿 IP
169
+ ↓ ↓
170
+ 协调服务 0.0.0.0:25991 ← 端口映射 127.0.0.1:x → 10.144.144.1:25991
171
+ ↓ ↓
172
+ 提供 /v1/session 返回 MC 端口 HTTP 握手校验 + 取端口
173
+ ↓ ↓
174
+ Minecraft 0.0.0.0:25565 ← 端口映射 TCP/UDP → 10.144.144.1:25565
175
+ ↓
176
+ 多播广播「局域网世界」
177
+ ```
178
+
179
+ ### 房间码格式(自研)
180
+
181
+ 16 个字符 × 5 bit = 80 bit:
182
+
183
+ ```
184
+ [4 bit 版本][64 bit 随机种子][12 bit CRC]
185
+ ```
186
+
187
+ - 字符集:Crockford Base32(`0-9A-Z` 去掉 `I/L/O/U`)
188
+ - 展示:`XXXX-XXXX-XXXX-XXXX`
189
+ - 输入容错:小写、空格、`MT-` 前缀、`I→1` `L→1` `O→0` `U→V` 自动纠正
190
+ - 网络名/密钥由种子经 **HMAC-SHA256 域分离派生**(`mc-tunnel/v1/*`),房间码本身不含明文凭据
191
+
192
+ ### 协调协议(自研)
193
+
194
+ HTTP/1.1 + JSON,跑在隧道内:
195
+
196
+ | 方法 | 路径 | 鉴权 | 说明 |
197
+ |---|---|---|---|
198
+ | GET | `/v1/hello` | 否 | 返回握手指纹,客人据此确认对端身份 |
199
+ | GET | `/v1/session` | 是 | 返回房主 MC 端口与玩家列表 |
200
+ | POST | `/v1/players` | 是 | 玩家心跳 / 昵称上报 |
201
+
202
+ 鉴权用房间码派生的令牌(`x-mct-token`),比较用 `timingSafeEqual`。
203
+
204
+ ---
205
+
206
+ ## 常见问题
207
+
208
+ **为什么不能把 `127.0.0.1` 写进 `peers`?**
209
+ EasyTier 连接对端时会绑定本地地址,回环地址会报 `10049 AddrNotAvailable`。
210
+ 直连请使用真实网卡 IP(可参考 `test/helpers.mjs` 的 `primaryIpv4()`)。
211
+
212
+ **客人提前退出了房主仍在?**
213
+ 房主侧玩家列表 12 秒无心跳自动剔除,不阻塞后续加入。
214
+
215
+ **端口冲突怎么办?**
216
+ 协调服务默认 `25991`,被占用会直接报错(不会静默换端口,否则客人找不到)。
217
+ Minecraft 映射端口优先与房主同号,被占用时自动改用随机端口,功能不受影响,
218
+ 但依赖固定 UDP 端口的 mod(如 SimpleVoiceChat)可能需要手动指定 `preferredLocalPort`。
219
+
220
+ **对称 NAT 连不上?**
221
+ 默认 `p2pOnly: true`,不走中继。若需要中继,请自建 EasyTier 公共节点并设置
222
+ `easyTier: { p2pOnly: false, publicNodes: ['tcp://your-node:11010'] }`。
223
+
224
+ **支持哪些平台?**
225
+ Node.js >= 18.17(使用内置 `fetch` 与 `AbortSignal.timeout`)。Windows / macOS / Linux 均可用。
226
+
227
+ ---
228
+
229
+ ## 开发
230
+
231
+ ```bash
232
+ npm install # 国内镜像已写入 .npmrc
233
+ npm run build # 产出 dist/{esm,cjs,types}
234
+ npm test # node:test,无需额外框架
235
+ ```
236
+
237
+ 端到端测试需要 EasyTier 二进制,放到 `test/fixtures/bin/` 或设置
238
+ `MC_TUNNEL_EASYTIER`,缺失时自动跳过。
239
+
240
+ ---
241
+
242
+ ## 合规说明
243
+
244
+ 本包为**独立实现**:
245
+
246
+ - 房间码编码、校验算法、协调协议、状态机均为本项目原创;
247
+ - 不含任何第三方项目的源代码、常量或协议格式;
248
+ - EasyTier 仅在运行时以**独立子进程**调用(`spawn`),不做静态/动态链接,
249
+ 也不修改其可执行文件。
250
+
251
+ 因此:
252
+
253
+ - 你可以把本包用于**闭源商业软件**;
254
+ - 本包自身采用 MIT 许可,你可以自由修改、私有化(也可以把 `license` 改成 `UNLICENSED`);
255
+ - 分发时请遵守 EasyTier 自身的许可要求,详见 `THIRD_PARTY_NOTICES.md`。
256
+
257
+ ---
258
+
259
+ ## 许可
260
+
261
+ MIT
@@ -0,0 +1,40 @@
1
+ # 第三方组件声明
2
+
3
+ 本包(`mc-tunnel`)自身不包含任何第三方源代码或二进制文件。
4
+ 以下组件由使用者自行提供,并在运行时以独立进程方式调用。
5
+
6
+ ---
7
+
8
+ ## EasyTier
9
+
10
+ - 项目地址:https://github.com/EasyTier/EasyTier
11
+ - 许可:请以你所使用的发行版附带的 `LICENSE` 为准(上游为 Apache-2.0)
12
+ - 使用方式:
13
+ - 本包通过 `child_process.spawn` 启动 `easytier-core`,
14
+ 通过 `easytier-cli` 的 JSON 输出读取节点信息;
15
+ - **不做静态或动态链接**,不修改其可执行文件,不重新分发其二进制。
16
+
17
+ ### 你需要做什么
18
+
19
+ 如果你在分发产品时捆绑了 EasyTier 的可执行文件,请:
20
+
21
+ 1. 保留其 `LICENSE` / `NOTICE` 文件(如发行包内有);
22
+ 2. 在产品的「开源许可 / 关于」页面列出 EasyTier 及其许可与项目地址;
23
+ 3. 若你对 EasyTier 二进制做了任何修改,需按 Apache-2.0 第 4 条标注修改说明
24
+ (本包不要求你修改,建议直接使用官方发布版)。
25
+
26
+ 示例文案:
27
+
28
+ ```
29
+ 本产品使用 EasyTier 提供 P2P 网络能力。
30
+ EasyTier: https://github.com/EasyTier/EasyTier
31
+ Licensed under the Apache License, Version 2.0.
32
+ ```
33
+
34
+ ---
35
+
36
+ ## 关于 Minecraft
37
+
38
+ 本项目实现了 Minecraft 客户端的公开协议行为(局域网多播发现、服务器列表 Ping),
39
+ 仅用于本机与玩家之间的连接建立,不包含 Mojang / Microsoft 的任何代码或资源。
40
+ 请遵守 Minecraft 最终用户许可协议(EULA)。
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ /** 全局常量。 */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.MC_WATCH_INTERVAL_MS = exports.HEARTBEAT_INTERVAL_MS = exports.DEFAULT_MOTD = exports.DEFAULT_COORD_PORT = exports.HOST_VIRTUAL_IP = exports.VERSION = void 0;
5
+ /** 与 package.json 的 version 保持一致(发布前请同步)。 */
6
+ exports.VERSION = '0.1.0';
7
+ /**
8
+ * 房主在虚拟网络中的固定地址。
9
+ * 10.144.144.0/24 是 EasyTier 的默认虚拟网段,固定 .1 可以让客人无需任何发现协议
10
+ * 就能直接找到协调服务——这是本实现相对「编码到 hostname」方案的主要简化。
11
+ */
12
+ exports.HOST_VIRTUAL_IP = '10.144.144.1';
13
+ /** 协调服务默认端口。房主与客人必须一致。 */
14
+ exports.DEFAULT_COORD_PORT = 25991;
15
+ /** 局域网列表里显示的房间名。 */
16
+ exports.DEFAULT_MOTD = '§b§lMC Tunnel 联机房间(请保持程序运行)';
17
+ /** 客人向房主上报心跳的间隔。 */
18
+ exports.HEARTBEAT_INTERVAL_MS = 5000;
19
+ /** 房主检查 Minecraft 是否存活的间隔。 */
20
+ exports.MC_WATCH_INTERVAL_MS = 5000;
21
+ //# sourceMappingURL=constants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.js","sourceRoot":"","sources":["../../src/constants.ts"],"names":[],"mappings":";AAAA,YAAY;;;AAEZ,6CAA6C;AAChC,QAAA,OAAO,GAAG,OAAO,CAAC;AAE/B;;;;GAIG;AACU,QAAA,eAAe,GAAG,cAAc,CAAC;AAE9C,0BAA0B;AACb,QAAA,kBAAkB,GAAG,KAAK,CAAC;AAExC,oBAAoB;AACP,QAAA,YAAY,GAAG,6BAA6B,CAAC;AAE1D,oBAAoB;AACP,QAAA,qBAAqB,GAAG,IAAI,CAAC;AAE1C,8BAA8B;AACjB,QAAA,oBAAoB,GAAG,IAAI,CAAC"}
@@ -0,0 +1,244 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CoordClient = exports.CoordServer = exports.COORD_TOKEN_HEADER = exports.COORD_PROTOCOL_VERSION = void 0;
4
+ /**
5
+ * 房主侧协调服务(跑在隧道内,客人通过端口映射访问)。
6
+ *
7
+ * 协议:HTTP/1.1 + JSON,全部为本包自定义,与任何既有项目无关。
8
+ *
9
+ * GET /v1/hello 公开接口,返回握手探针,客人据此确认对端身份
10
+ * GET /v1/session 需要令牌,返回房主的 Minecraft 端口与玩家列表
11
+ * POST /v1/players 需要令牌,客人上报心跳 / 昵称
12
+ *
13
+ * 之所以选 HTTP/JSON 而不是自定义二进制帧:调试成本低(curl 就能测),
14
+ * 而且 Node 内置 http 模块足够,不需要引入任何依赖。
15
+ */
16
+ const node_http_1 = require("node:http");
17
+ const node_events_1 = require("node:events");
18
+ const room_code_js_1 = require("./room-code.js");
19
+ exports.COORD_PROTOCOL_VERSION = 1;
20
+ exports.COORD_TOKEN_HEADER = 'x-mct-token';
21
+ /** 客人心跳间隔的 2 倍仍没消息就从列表里剔除 */
22
+ const PLAYER_TIMEOUT_MS = 12000;
23
+ const BODY_LIMIT = 4096;
24
+ function readBody(request) {
25
+ return new Promise((resolve, reject) => {
26
+ const chunks = [];
27
+ let size = 0;
28
+ request.on('data', (chunk) => {
29
+ size += chunk.length;
30
+ if (size > BODY_LIMIT) {
31
+ reject(new Error('Body too large'));
32
+ request.destroy();
33
+ return;
34
+ }
35
+ chunks.push(chunk);
36
+ });
37
+ request.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
38
+ request.on('error', reject);
39
+ });
40
+ }
41
+ function sendJson(response, status, data) {
42
+ const body = Buffer.from(JSON.stringify(data), 'utf8');
43
+ response.writeHead(status, {
44
+ 'content-type': 'application/json; charset=utf-8',
45
+ 'content-length': body.length,
46
+ 'cache-control': 'no-store',
47
+ });
48
+ response.end(body);
49
+ }
50
+ class CoordServer extends node_events_1.EventEmitter {
51
+ options;
52
+ playerTimeoutMs;
53
+ server;
54
+ players = new Map();
55
+ pruneTimer;
56
+ constructor(options) {
57
+ super();
58
+ this.options = options;
59
+ this.playerTimeoutMs = options.playerTimeoutMs ?? PLAYER_TIMEOUT_MS;
60
+ }
61
+ static async start(options) {
62
+ const instance = new CoordServer(options);
63
+ instance.players.set(options.hostId, {
64
+ id: options.hostId,
65
+ name: options.hostName,
66
+ role: 'host',
67
+ lastSeen: Date.now(),
68
+ });
69
+ const server = (0, node_http_1.createServer)((request, response) => {
70
+ instance.handle(request, response).catch(() => {
71
+ if (!response.headersSent) {
72
+ sendJson(response, 500, { error: 'internal' });
73
+ }
74
+ });
75
+ });
76
+ await new Promise((resolve, reject) => {
77
+ server.once('error', (error) => {
78
+ reject(error.code === 'EADDRINUSE'
79
+ ? new Error(`Coord port ${options.port} is already in use`)
80
+ : error);
81
+ });
82
+ server.listen(options.port, '0.0.0.0', () => resolve());
83
+ });
84
+ instance.server = server;
85
+ // 剔除周期跟着超时走,避免超时很短时仍要等 4 秒
86
+ const sweep = Math.max(200, Math.min(4000, Math.floor(instance.playerTimeoutMs / 2)));
87
+ instance.pruneTimer = setInterval(() => instance.prune(), sweep);
88
+ instance.pruneTimer.unref?.();
89
+ return instance;
90
+ }
91
+ prune() {
92
+ const now = Date.now();
93
+ let changed = false;
94
+ for (const [id, record] of this.players) {
95
+ if (record.role === 'host') {
96
+ continue;
97
+ }
98
+ if (now - record.lastSeen > this.playerTimeoutMs) {
99
+ this.players.delete(id);
100
+ changed = true;
101
+ this.emit('player-leave', { id: record.id, name: record.name });
102
+ }
103
+ }
104
+ if (changed) {
105
+ this.emit('players', this.list());
106
+ }
107
+ }
108
+ list() {
109
+ return [...this.players.values()]
110
+ .map(({ id, name, role }) => ({ id, name, role }))
111
+ .sort((a, b) => (a.role === b.role ? a.id.localeCompare(b.id) : a.role === 'host' ? -1 : 1));
112
+ }
113
+ authorized(request) {
114
+ const header = request.headers[exports.COORD_TOKEN_HEADER];
115
+ const token = Array.isArray(header) ? header[0] : header;
116
+ return typeof token === 'string' && (0, room_code_js_1.safeEqual)(token, this.options.token);
117
+ }
118
+ async handle(request, response) {
119
+ const url = new URL(request.url ?? '/', 'http://localhost');
120
+ if (request.method === 'GET' && url.pathname === '/v1/hello') {
121
+ sendJson(response, 200, {
122
+ app: 'mc-tunnel',
123
+ protocol: exports.COORD_PROTOCOL_VERSION,
124
+ probe: this.options.probe,
125
+ });
126
+ return;
127
+ }
128
+ if (!this.authorized(request)) {
129
+ sendJson(response, 401, { error: 'unauthorized' });
130
+ return;
131
+ }
132
+ if (request.method === 'GET' && url.pathname === '/v1/session') {
133
+ sendJson(response, 200, {
134
+ room: this.options.roomCode,
135
+ mcPort: this.options.mcPort,
136
+ host: { id: this.options.hostId, name: this.options.hostName },
137
+ players: this.list(),
138
+ });
139
+ return;
140
+ }
141
+ if (request.method === 'POST' && url.pathname === '/v1/players') {
142
+ const raw = await readBody(request);
143
+ let body;
144
+ try {
145
+ body = JSON.parse(raw);
146
+ }
147
+ catch {
148
+ sendJson(response, 400, { error: 'bad-json' });
149
+ return;
150
+ }
151
+ const id = typeof body.id === 'string' ? body.id.trim() : '';
152
+ const name = typeof body.name === 'string' ? body.name.trim().slice(0, 32) : '';
153
+ if (!/^[0-9a-z]{8,64}$/i.test(id) || name.length === 0) {
154
+ sendJson(response, 400, { error: 'bad-player' });
155
+ return;
156
+ }
157
+ const existing = this.players.get(id);
158
+ if (existing && existing.role === 'host') {
159
+ sendJson(response, 409, { error: 'id-conflict' });
160
+ return;
161
+ }
162
+ const isNew = existing === undefined;
163
+ this.players.set(id, {
164
+ id,
165
+ name,
166
+ role: 'guest',
167
+ lastSeen: Date.now(),
168
+ });
169
+ if (isNew) {
170
+ this.emit('player-join', { id, name });
171
+ this.emit('players', this.list());
172
+ }
173
+ sendJson(response, 200, { players: this.list() });
174
+ return;
175
+ }
176
+ sendJson(response, 404, { error: 'not-found' });
177
+ }
178
+ get port() {
179
+ return this.options.port;
180
+ }
181
+ async stop() {
182
+ if (this.pruneTimer) {
183
+ clearInterval(this.pruneTimer);
184
+ this.pruneTimer = undefined;
185
+ }
186
+ const server = this.server;
187
+ if (!server) {
188
+ return;
189
+ }
190
+ this.server = undefined;
191
+ await new Promise((resolve) => {
192
+ server.close(() => resolve());
193
+ // 关闭后不再处理新连接,已在途连接由 Node 自行收尾
194
+ server.closeAllConnections?.();
195
+ });
196
+ }
197
+ }
198
+ exports.CoordServer = CoordServer;
199
+ /** 客人侧客户端。 */
200
+ class CoordClient {
201
+ base;
202
+ token;
203
+ constructor(base, token) {
204
+ this.base = base.replace(/\/$/, '');
205
+ this.token = token;
206
+ }
207
+ async request(path, init, timeoutMs = 5000) {
208
+ return fetch(`${this.base}${path}`, {
209
+ ...init,
210
+ headers: { ...(init?.headers ?? {}) },
211
+ signal: AbortSignal.timeout(timeoutMs),
212
+ });
213
+ }
214
+ async hello() {
215
+ const response = await this.request('/v1/hello');
216
+ if (!response.ok) {
217
+ throw new Error(`hello failed: HTTP ${response.status}`);
218
+ }
219
+ return (await response.json());
220
+ }
221
+ async session() {
222
+ const response = await this.request('/v1/session', {
223
+ headers: { [exports.COORD_TOKEN_HEADER]: this.token },
224
+ });
225
+ if (!response.ok) {
226
+ throw new Error(`session failed: HTTP ${response.status}`);
227
+ }
228
+ return (await response.json());
229
+ }
230
+ async heartbeat(id, name) {
231
+ const response = await this.request('/v1/players', {
232
+ method: 'POST',
233
+ headers: { [exports.COORD_TOKEN_HEADER]: this.token, 'content-type': 'application/json' },
234
+ body: JSON.stringify({ id, name }),
235
+ });
236
+ if (!response.ok) {
237
+ throw new Error(`heartbeat failed: HTTP ${response.status}`);
238
+ }
239
+ const data = (await response.json());
240
+ return data.players;
241
+ }
242
+ }
243
+ exports.CoordClient = CoordClient;
244
+ //# sourceMappingURL=coord.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"coord.js","sourceRoot":"","sources":["../../src/coord.ts"],"names":[],"mappings":"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"}