dshost-plugin 0.1.1 → 0.1.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/cordis.patch.yml +1 -1
- package/lib/core.js +89 -15
- package/package.json +1 -1
package/cordis.patch.yml
CHANGED
|
@@ -9,6 +9,6 @@
|
|
|
9
9
|
# id -> dsh crashed on startup (systemd restart loop).
|
|
10
10
|
#
|
|
11
11
|
# The plugin is still loaded by cordis because the user profile entry references
|
|
12
|
-
# it by name ('
|
|
12
|
+
# it by name ('dshost-plugin'); this file only needs to exist for
|
|
13
13
|
# the bundle mechanism.
|
|
14
14
|
[]
|
package/lib/core.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import http from 'http';
|
|
6
6
|
import os from 'os';
|
|
7
7
|
import fs from 'fs';
|
|
8
|
+
import path from 'path';
|
|
8
9
|
import crypto from 'crypto';
|
|
9
10
|
import { execFileSync } from 'child_process';
|
|
10
11
|
import WebSocket from 'ws';
|
|
@@ -37,22 +38,31 @@ function localIPv4() {
|
|
|
37
38
|
let cachedInstanceId = null;
|
|
38
39
|
function getInstanceId() {
|
|
39
40
|
if (cachedInstanceId) return cachedInstanceId;
|
|
40
|
-
// 稳定实例 ID:hostname +
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
let
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
41
|
+
// 稳定实例 ID:hostname + systemd machine-id 的哈希前缀。machine-id 由系统
|
|
42
|
+
// 安装时生成、跨重启持久 —— 此前两版(纯随机 / 首个MAC哈希)分别败于 agent
|
|
43
|
+
// 重启与 Docker/虚拟网卡导致的 MAC 集合变化,都会让浏览器标签页的实例引用
|
|
44
|
+
// 失效并触发 503 重试风暴。同名主机由 machine-id 区分。
|
|
45
|
+
let mid = '';
|
|
46
|
+
for (const f of ['/etc/machine-id', '/var/lib/dbus/machine-id']) {
|
|
47
|
+
try {
|
|
48
|
+
const s = fs.readFileSync(f, 'utf8').trim();
|
|
49
|
+
if (s) { mid = s; break; }
|
|
50
|
+
} catch {}
|
|
51
|
+
}
|
|
52
|
+
if (!mid) {
|
|
53
|
+
// 无 machine-id 的兜底:全部非内部 MAC 的联合哈希(排序拼接,不依赖
|
|
54
|
+
// 枚举顺序与单个网卡的存在性)
|
|
55
|
+
try {
|
|
56
|
+
const macs = [];
|
|
57
|
+
for (const addrs of Object.values(os.networkInterfaces())) {
|
|
58
|
+
for (const a of addrs || []) {
|
|
59
|
+
if (a && !a.internal && a.mac && a.mac !== '00:00:00:00:00:00') macs.push(a.mac);
|
|
60
|
+
}
|
|
50
61
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const h = crypto.createHash('sha256').update(os.hostname() + '|' + mac).digest('hex');
|
|
62
|
+
mid = 'mac:' + macs.sort().join(',');
|
|
63
|
+
} catch {}
|
|
64
|
+
}
|
|
65
|
+
const h = crypto.createHash('sha256').update(os.hostname() + '|' + mid).digest('hex');
|
|
56
66
|
cachedInstanceId = os.hostname() + '-' + parseInt(h.slice(0, 8), 16).toString(36).slice(0, 4);
|
|
57
67
|
return cachedInstanceId;
|
|
58
68
|
}
|
|
@@ -170,6 +180,8 @@ export function collectSystemInfo(defaultVersion) {
|
|
|
170
180
|
diskFree,
|
|
171
181
|
dshVersion: defaultVersion || meta.dshVersion,
|
|
172
182
|
plugins: meta.plugins,
|
|
183
|
+
// 无头检测:供 relay 注入脚本决定产物点击行为(新窗口HTTP直出 vs 本地打开)
|
|
184
|
+
headless: !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY,
|
|
173
185
|
};
|
|
174
186
|
}
|
|
175
187
|
|
|
@@ -241,6 +253,7 @@ export function startAgent(opts) {
|
|
|
241
253
|
if (msg.type === 'heartbeat') return;
|
|
242
254
|
|
|
243
255
|
if (msg.type === 'http:request') {
|
|
256
|
+
if (serveArtifactIfRequested(ws, msg)) return;
|
|
244
257
|
handleHttpRequest(ws, msg);
|
|
245
258
|
} else if (msg.type === 'http:body') {
|
|
246
259
|
const stream = streams.get(msg.streamId);
|
|
@@ -316,6 +329,67 @@ export function startAgent(opts) {
|
|
|
316
329
|
}, 15000);
|
|
317
330
|
}
|
|
318
331
|
|
|
332
|
+
// [Artifact Viewer] 无头机产物 HTTP 直出:GET /api/host.artifact?path=...
|
|
333
|
+
// 由注入脚本在浏览器劫持 openPath 点击后以新窗口打开 —— 无显示环境无法
|
|
334
|
+
// "本地打开",改为把文本/代码类产物以对应 MIME 直出给浏览器渲染。
|
|
335
|
+
const ARTIFACT_TEXT_EXT = new Set([
|
|
336
|
+
'.md', '.markdown', '.txt', '.log', '.json', '.yml', '.yaml', '.toml', '.ini',
|
|
337
|
+
'.cfg', '.conf', '.env', '.csv', '.tsv', '.js', '.mjs', '.cjs', '.ts', '.tsx',
|
|
338
|
+
'.jsx', '.py', '.rb', '.go', '.rs', '.java', '.kt', '.c', '.h', '.cpp', '.hpp',
|
|
339
|
+
'.cs', '.php', '.sh', '.bash', '.zsh', '.sql', '.html', '.htm', '.css', '.scss',
|
|
340
|
+
'.less', '.vue', '.svelte', '.swift', '.lua', '.r', '.pl', '.xml', '.svg',
|
|
341
|
+
]);
|
|
342
|
+
const ARTIFACT_MIME = {
|
|
343
|
+
'.md': 'text/markdown; charset=utf-8', '.markdown': 'text/markdown; charset=utf-8',
|
|
344
|
+
'.html': 'text/html; charset=utf-8', '.htm': 'text/html; charset=utf-8',
|
|
345
|
+
'.css': 'text/css; charset=utf-8', '.scss': 'text/x-scss; charset=utf-8',
|
|
346
|
+
'.js': 'text/javascript; charset=utf-8', '.mjs': 'text/javascript; charset=utf-8',
|
|
347
|
+
'.cjs': 'text/javascript; charset=utf-8', '.ts': 'text/typescript; charset=utf-8',
|
|
348
|
+
'.tsx': 'text/typescript; charset=utf-8', '.jsx': 'text/typescript; charset=utf-8',
|
|
349
|
+
'.json': 'application/json; charset=utf-8', '.xml': 'application/xml; charset=utf-8',
|
|
350
|
+
'.svg': 'image/svg+xml', '.py': 'text/x-python; charset=utf-8',
|
|
351
|
+
'.sh': 'text/x-shellscript; charset=utf-8', '.bash': 'text/x-shellscript; charset=utf-8',
|
|
352
|
+
'.csv': 'text/csv; charset=utf-8', '.yml': 'text/yaml; charset=utf-8',
|
|
353
|
+
'.yaml': 'text/yaml; charset=utf-8', '.sql': 'text/x-sql; charset=utf-8',
|
|
354
|
+
};
|
|
355
|
+
const ARTIFACT_MAX_BYTES = 2 * 1024 * 1024;
|
|
356
|
+
|
|
357
|
+
function artifactRespond(ws, streamId, status, headers, body) {
|
|
358
|
+
send(ws, { type: 'http:response', streamId, status, headers });
|
|
359
|
+
if (body) send(ws, { type: 'http:data', streamId, data: b64(body) });
|
|
360
|
+
send(ws, { type: 'http:end', streamId });
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function serveArtifactIfRequested(ws, msg) {
|
|
364
|
+
if (msg.method !== 'GET') return false;
|
|
365
|
+
let u;
|
|
366
|
+
try { u = new URL(msg.path, 'http://local'); } catch { return false; }
|
|
367
|
+
if (u.pathname !== '/api/host.artifact') return false;
|
|
368
|
+
const sid = msg.streamId;
|
|
369
|
+
const fail = (status, text) => artifactRespond(ws, sid, status,
|
|
370
|
+
{ 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' },
|
|
371
|
+
Buffer.from(JSON.stringify({ ok: false, error: { code: 'artifact', message: text } })));
|
|
372
|
+
const rawPath = u.searchParams.get('path') || '';
|
|
373
|
+
if (!path.isAbsolute(rawPath)) return fail(400, 'absolute path required');
|
|
374
|
+
const ext = path.extname(rawPath).toLowerCase();
|
|
375
|
+
const dotfile = /(^|\/)(\.[a-z0-9_.-]+)$/i.test(path.basename(rawPath));
|
|
376
|
+
if (!ARTIFACT_TEXT_EXT.has(ext) && !dotfile) return fail(415, `unsupported artifact type: ${ext || '(none)'}`);
|
|
377
|
+
let st;
|
|
378
|
+
try { st = fs.statSync(rawPath); } catch { return fail(404, 'file not found'); }
|
|
379
|
+
if (!st.isFile()) return fail(400, 'not a regular file');
|
|
380
|
+
if (st.size > ARTIFACT_MAX_BYTES) return fail(413, `artifact too large (${st.size} bytes > ${ARTIFACT_MAX_BYTES})`);
|
|
381
|
+
let buf;
|
|
382
|
+
try { buf = fs.readFileSync(rawPath); } catch (e) { return fail(500, 'read failed: ' + e.message); }
|
|
383
|
+
const mime = ARTIFACT_MIME[ext] || 'text/plain; charset=utf-8';
|
|
384
|
+
artifactRespond(ws, sid, 200, {
|
|
385
|
+
'content-type': mime,
|
|
386
|
+
'content-disposition': `inline; filename="${path.basename(rawPath)}"`,
|
|
387
|
+
'cache-control': 'no-store',
|
|
388
|
+
}, buf);
|
|
389
|
+
log.log?.(`[agent] artifact served: ${rawPath} (${buf.length}B)`);
|
|
390
|
+
return true;
|
|
391
|
+
}
|
|
392
|
+
|
|
319
393
|
function handleHttpRequest(ws, msg) {
|
|
320
394
|
const { streamId, method, path, headers } = msg;
|
|
321
395
|
|
package/package.json
CHANGED