@wenbin_wb/dsh-bridge 2.8.4 → 2.8.6
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/CHANGELOG.md +28 -0
- package/client/client.js +302 -32
- package/client/index.js +319 -39
- package/docs/fix-tunnel-sse-and-session-list.md +134 -0
- package/lib/cloudflared-manager.mjs +75 -12
- package/lib/feishu/gateway.js +1 -0
- package/lib/feishu/node.js +1 -0
- package/lib/index.js +65 -9
- package/lib/platform/conversation-bridge.js +201 -197
- package/lib/qq/gateway.js +26 -2
- package/lib/qq/node.js +2 -0
- package/lib/telegram/gateway.js +3 -0
- package/lib/telegram/node.js +1 -0
- package/lib/tunnel-client.mjs +402 -289
- package/lib/wechat/gateway.js +2 -0
- package/lib/wechat/node.js +1 -0
- package/package.json +2 -2
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { spawn } from 'node:child_process';
|
|
4
|
-
import { createWriteStream, existsSync, mkdirSync } from 'node:fs';
|
|
1
|
+
import { spawn, execSync } from 'node:child_process';
|
|
2
|
+
import { createWriteStream, existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
5
3
|
import { chmod, stat, unlink, rename } from 'node:fs/promises';
|
|
6
4
|
import { homedir, platform, arch } from 'node:os';
|
|
7
5
|
import { join } from 'node:path';
|
|
@@ -73,6 +71,27 @@ async function downloadFile(url, dest, onProgress) {
|
|
|
73
71
|
});
|
|
74
72
|
}
|
|
75
73
|
|
|
74
|
+
function findSystemCloudflared() {
|
|
75
|
+
const isWin = platform() === 'win32';
|
|
76
|
+
const candidates = [];
|
|
77
|
+
if (isWin) {
|
|
78
|
+
candidates.push('cloudflared.exe', 'cloudflared', 'C:\\Program Files (x86)\\cloudflared\\cloudflared.exe', 'C:\\Program Files\\cloudflared\\cloudflared.exe');
|
|
79
|
+
} else {
|
|
80
|
+
candidates.push('cloudflared', '/opt/homebrew/bin/cloudflared', '/usr/local/bin/cloudflared', '/usr/bin/cloudflared', '/bin/cloudflared');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
for (const bin of candidates) {
|
|
84
|
+
try {
|
|
85
|
+
if (bin.includes('/') || bin.includes('\\')) {
|
|
86
|
+
if (!existsSync(bin)) continue;
|
|
87
|
+
}
|
|
88
|
+
execSync(`"${bin}" --version`, { stdio: 'ignore', timeout: 3000 });
|
|
89
|
+
return bin;
|
|
90
|
+
} catch {}
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
76
95
|
export class CloudflaredManager {
|
|
77
96
|
constructor({ port, home, token, hostname, onStateChange, logger }) {
|
|
78
97
|
this.port = port;
|
|
@@ -104,19 +123,47 @@ export class CloudflaredManager {
|
|
|
104
123
|
}
|
|
105
124
|
|
|
106
125
|
async _ensureBinary() {
|
|
126
|
+
// 1. 优先使用系统环境变量或 Homebrew / 包管理器已安装的全局二进制
|
|
127
|
+
const systemBin = findSystemCloudflared();
|
|
128
|
+
if (systemBin) {
|
|
129
|
+
this.binaryPath = systemBin;
|
|
130
|
+
this.logger?.info('优先使用系统全局 cloudflared: %s', systemBin);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
107
134
|
const { url, name } = getCloudflaredInfo();
|
|
108
135
|
const binDir = join(this.home, 'bin');
|
|
109
136
|
const binPath = join(binDir, name);
|
|
110
137
|
this.binaryPath = binPath;
|
|
111
138
|
|
|
139
|
+
// 2. 检查本地 ~/.dsh-bridge/bin/cloudflared 是否已存在且可用
|
|
112
140
|
if (existsSync(binPath)) {
|
|
113
141
|
try {
|
|
114
142
|
const s = await stat(binPath);
|
|
115
143
|
if (s.size > MIN_BINARY_SIZE) { // >5MB 才视为有效二进制
|
|
116
|
-
|
|
117
|
-
|
|
144
|
+
// 检查是否为历史残留未解压的 gzip 压缩包 (0x1f 0x8b)
|
|
145
|
+
const fd = readFileSync(binPath);
|
|
146
|
+
const isGzip = fd.length >= 2 && fd[0] === 0x1f && fd[1] === 0x8b;
|
|
147
|
+
if (isGzip) {
|
|
148
|
+
this.logger?.warn('检测到历史残留的未解压 cloudflared.tgz 压缩包,正在清理重新准备...');
|
|
149
|
+
await unlink(binPath).catch(() => {});
|
|
150
|
+
} else {
|
|
151
|
+
// macOS / Linux 赋予可执行权限并清除 Gatekeeper 隔离属性
|
|
152
|
+
if (platform() !== 'win32') {
|
|
153
|
+
await chmod(binPath, 0o755).catch(() => {});
|
|
154
|
+
if (platform() === 'darwin') {
|
|
155
|
+
try { execSync(`xattr -d com.apple.quarantine "${binPath}"`, { stdio: 'ignore' }); } catch {}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// 执行一次 --version 验证是否能正常 spawn
|
|
159
|
+
execSync(`"${binPath}" --version`, { stdio: 'ignore', timeout: 3000 });
|
|
160
|
+
this.logger?.info('cloudflared 已存在且验证通过: %s', binPath);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
118
163
|
}
|
|
119
|
-
} catch {
|
|
164
|
+
} catch (verifyErr) {
|
|
165
|
+
this.logger?.warn('现有 cloudflared 二进制验证失败 (%s),准备重新下载', verifyErr.message);
|
|
166
|
+
}
|
|
120
167
|
// 损坏文件,删掉重下
|
|
121
168
|
await unlink(binPath).catch(() => {});
|
|
122
169
|
}
|
|
@@ -135,16 +182,32 @@ export class CloudflaredManager {
|
|
|
135
182
|
this._setState('downloading', `下载 cloudflared: ${mb}/${totalMb} MB (${percent}%)`);
|
|
136
183
|
});
|
|
137
184
|
|
|
138
|
-
if (
|
|
139
|
-
|
|
185
|
+
if (url.endsWith('.tgz') || url.endsWith('.tar.gz')) {
|
|
186
|
+
try {
|
|
187
|
+
execSync(`tar -xzf "${tempPath}" -C "${binDir}"`);
|
|
188
|
+
await unlink(tempPath).catch(() => {});
|
|
189
|
+
} catch (tarErr) {
|
|
190
|
+
this.logger?.error('解压 cloudflared 压缩包失败: %s', tarErr.message);
|
|
191
|
+
throw new Error(`解压 cloudflared 失败: ${tarErr.message}`);
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
if (existsSync(binPath)) await unlink(binPath).catch(() => {});
|
|
195
|
+
await rename(tempPath, binPath);
|
|
196
|
+
}
|
|
140
197
|
|
|
141
198
|
if (platform() !== 'win32') {
|
|
142
|
-
await chmod(binPath, 0o755);
|
|
199
|
+
await chmod(binPath, 0o755).catch(() => {});
|
|
200
|
+
if (platform() === 'darwin') {
|
|
201
|
+
try { execSync(`xattr -d com.apple.quarantine "${binPath}"`, { stdio: 'ignore' }); } catch {}
|
|
202
|
+
}
|
|
143
203
|
}
|
|
144
|
-
|
|
204
|
+
|
|
205
|
+
// 执行 --version 最终确认
|
|
206
|
+
execSync(`"${binPath}" --version`, { stdio: 'ignore', timeout: 3000 });
|
|
207
|
+
this.logger?.info('cloudflared 下载并准备完成');
|
|
145
208
|
} catch (err) {
|
|
146
209
|
await unlink(tempPath).catch(() => {});
|
|
147
|
-
throw new Error(
|
|
210
|
+
throw new Error(`准备 cloudflared 失败: ${err.message}`);
|
|
148
211
|
}
|
|
149
212
|
}
|
|
150
213
|
|
package/lib/feishu/gateway.js
CHANGED
|
@@ -205,6 +205,7 @@ export class FeishuGateway extends Service {
|
|
|
205
205
|
// ---- 消息收发 ----
|
|
206
206
|
|
|
207
207
|
async _handleMessageReceive(data) {
|
|
208
|
+
if (this._closing || this.status === 'offline') return
|
|
208
209
|
if (!data?.message) return
|
|
209
210
|
const { message, sender } = data
|
|
210
211
|
const messageId = message.message_id
|
package/lib/feishu/node.js
CHANGED
|
@@ -107,6 +107,7 @@ export class FeishuConversationNode extends ConversationBridge {
|
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
async _handleInbound(event) {
|
|
110
|
+
if (this.gateway?._closing || this.gateway?.status === 'offline') return
|
|
110
111
|
const { peerId, senderId, isGroup, text, messageId, messageType, contentObj } = event
|
|
111
112
|
this._lastPeer = { peerId, senderId, isGroup }
|
|
112
113
|
|
package/lib/index.js
CHANGED
|
@@ -10,9 +10,10 @@ import { get as httpsGet } from 'node:https';
|
|
|
10
10
|
import { networkInterfaces, homedir, totalmem, freemem, cpus, loadavg, platform, arch, release, hostname, uptime } from 'node:os';
|
|
11
11
|
import { join, dirname, basename, resolve, normalize } from 'node:path';
|
|
12
12
|
import { fileURLToPath } from 'node:url';
|
|
13
|
-
import { readFileSync } from 'node:fs';
|
|
13
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
14
14
|
import { readFile, writeFile, mkdir, unlink, readdir, stat, access } from 'node:fs/promises';
|
|
15
15
|
import { spawn } from 'node:child_process';
|
|
16
|
+
import { createHash, createHmac } from 'node:crypto';
|
|
16
17
|
import QRCode from 'qrcode';
|
|
17
18
|
import { installBridgeRpc } from './bridge-rpc.js';
|
|
18
19
|
import { CustomTunnelClient } from './tunnel-client.mjs';
|
|
@@ -42,23 +43,34 @@ function selectLanIPv4() {
|
|
|
42
43
|
let best = null;
|
|
43
44
|
let bestScore = -1;
|
|
44
45
|
|
|
46
|
+
const VIRTUAL_KEYWORDS = [
|
|
47
|
+
'vethernet', 'wsl', 'hyper-v', 'virtual', 'vmware', 'vbox', 'docker',
|
|
48
|
+
'tailscale', 'zerotier', 'tap', 'tun', 'utun', 'wireguard', 'loopback', 'bridge',
|
|
49
|
+
];
|
|
50
|
+
|
|
45
51
|
for (const [ifname, addrs] of Object.entries(interfaces)) {
|
|
46
52
|
if (!addrs) continue;
|
|
53
|
+
const lower = ifname.toLowerCase();
|
|
54
|
+
const isVirtual = VIRTUAL_KEYWORDS.some((kw) => lower.includes(kw));
|
|
47
55
|
|
|
48
56
|
for (const addr of addrs) {
|
|
49
57
|
if (addr.family !== 'IPv4' || addr.internal) continue;
|
|
50
58
|
|
|
51
59
|
let score = 0;
|
|
60
|
+
// 1. IP 网段优先(家庭/企业物理局域网最常用网段)
|
|
52
61
|
if (addr.address.startsWith('192.168.')) score += 100;
|
|
53
62
|
else if (addr.address.startsWith('10.')) score += 90;
|
|
54
|
-
else if (addr.address.match(/^172\.(1[6-9]|2[0-9]|3[0-1])\./)) score +=
|
|
63
|
+
else if (addr.address.match(/^172\.(1[6-9]|2[0-9]|3[0-1])\./)) score += 70;
|
|
64
|
+
else score += 10;
|
|
55
65
|
|
|
56
|
-
|
|
57
|
-
if (
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
66
|
+
// 2. 物理网卡与名称特征优先
|
|
67
|
+
if (isVirtual) {
|
|
68
|
+
score -= 200; // 虚拟网卡大幅降权
|
|
69
|
+
} else {
|
|
70
|
+
score += 100;
|
|
71
|
+
if (lower.includes('wi-fi') || lower.includes('wlan') || lower.includes('wireless')) score += 50;
|
|
72
|
+
else if (lower.includes('ethernet') || lower.includes('以太网') || lower.includes('eth') || lower.includes('en')) score += 40;
|
|
73
|
+
}
|
|
62
74
|
|
|
63
75
|
if (score > bestScore) {
|
|
64
76
|
bestScore = score;
|
|
@@ -161,6 +173,37 @@ function isCompressed(headers) {
|
|
|
161
173
|
return /(^|,\s*)(gzip|br|deflate)(\s*,|$)/i.test(String(headers['content-encoding'] ?? ''));
|
|
162
174
|
}
|
|
163
175
|
|
|
176
|
+
function encodeBase64Url(value) {
|
|
177
|
+
return Buffer.from(value).toString('base64url');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* 读取 DSH 本地凭证并生成 loopback dsh-auth 认证签名 Cookie (适配 DSH 新版原生认证)
|
|
182
|
+
*/
|
|
183
|
+
function getDshLoopbackCookie(targetPort) {
|
|
184
|
+
try {
|
|
185
|
+
const dshHome = process.env.DSH_HOME ?? join(homedir(), '.dsh');
|
|
186
|
+
const credPath = join(dshHome, '.credentials.yaml');
|
|
187
|
+
if (!existsSync(credPath)) return '';
|
|
188
|
+
const content = readFileSync(credPath, 'utf8');
|
|
189
|
+
const match = content.match(/secret:\s*([A-Za-z0-9_-]+)/);
|
|
190
|
+
if (!match) return '';
|
|
191
|
+
const secret = Buffer.from(match[1], 'base64url');
|
|
192
|
+
|
|
193
|
+
const authority = `127.0.0.1:${targetPort}`;
|
|
194
|
+
const name = 'dsh-auth-' + encodeBase64Url(createHash('sha256').update(authority).digest());
|
|
195
|
+
const issuedAt = Date.now() - 1000;
|
|
196
|
+
const expiresAt = issuedAt + 30 * 24 * 3600 * 1000;
|
|
197
|
+
const body = encodeBase64Url(Buffer.from(JSON.stringify({
|
|
198
|
+
version: 1, authority, issuedAt, expiresAt,
|
|
199
|
+
}), 'utf8'));
|
|
200
|
+
const sig = encodeBase64Url(createHmac('sha256', secret).update(body).digest());
|
|
201
|
+
return `${name}=v1.${body}.${sig}`;
|
|
202
|
+
} catch {
|
|
203
|
+
return '';
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
164
207
|
/** 把请求头中的 Host 和 Origin 改写成 loopback,让 DSH 的安全栅栏放行 */
|
|
165
208
|
function loopbackHeaders(headers, targetPort) {
|
|
166
209
|
const authority = `127.0.0.1:${targetPort}`;
|
|
@@ -168,6 +211,19 @@ function loopbackHeaders(headers, targetPort) {
|
|
|
168
211
|
out['host'] = authority;
|
|
169
212
|
if (out['origin']) out['origin'] = `http://${authority}`;
|
|
170
213
|
if (out['Origin']) out['Origin'] = `http://${authority}`;
|
|
214
|
+
|
|
215
|
+
// 1. 注入 DSH 本地认证签名(若有)
|
|
216
|
+
const dshCookie = getDshLoopbackCookie(targetPort);
|
|
217
|
+
if (dshCookie) {
|
|
218
|
+
const existing = out['cookie'] || out['Cookie'] || '';
|
|
219
|
+
out['cookie'] = existing ? `${existing}; ${dshCookie}` : dshCookie;
|
|
220
|
+
delete out['Cookie'];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// 2. 禁用内部代理流量压缩,确保代理层拿到未压缩 HTML 以稳定注入 ownsHost 和 Polyfill
|
|
224
|
+
delete out['accept-encoding'];
|
|
225
|
+
delete out['Accept-Encoding'];
|
|
226
|
+
|
|
171
227
|
return out;
|
|
172
228
|
}
|
|
173
229
|
|
|
@@ -1254,7 +1310,7 @@ class BridgeService {
|
|
|
1254
1310
|
*/
|
|
1255
1311
|
function apply(ctx, config = {}) {
|
|
1256
1312
|
const logger = ctx.logger(name);
|
|
1257
|
-
const dshPort = ctx.webServer.
|
|
1313
|
+
const dshPort = ctx.webServer?.port ?? config.targetPort ?? 3080;
|
|
1258
1314
|
|
|
1259
1315
|
if (!dshPort) {
|
|
1260
1316
|
logger.error('webServer port unavailable');
|