dshost-plugin 0.1.2 → 0.1.4
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 +92 -0
- 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';
|
|
@@ -179,6 +180,8 @@ export function collectSystemInfo(defaultVersion) {
|
|
|
179
180
|
diskFree,
|
|
180
181
|
dshVersion: defaultVersion || meta.dshVersion,
|
|
181
182
|
plugins: meta.plugins,
|
|
183
|
+
// 无头检测:供 relay 注入脚本决定产物点击行为(新窗口HTTP直出 vs 本地打开)
|
|
184
|
+
headless: !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY,
|
|
182
185
|
};
|
|
183
186
|
}
|
|
184
187
|
|
|
@@ -250,11 +253,16 @@ export function startAgent(opts) {
|
|
|
250
253
|
if (msg.type === 'heartbeat') return;
|
|
251
254
|
|
|
252
255
|
if (msg.type === 'http:request') {
|
|
256
|
+
if (serveArtifactIfRequested(ws, msg)) return;
|
|
253
257
|
handleHttpRequest(ws, msg);
|
|
254
258
|
} else if (msg.type === 'http:body') {
|
|
259
|
+
const ab = artifactFormBodies.get(msg.streamId);
|
|
260
|
+
if (ab) { ab.push(un64(msg.data)); return; }
|
|
255
261
|
const stream = streams.get(msg.streamId);
|
|
256
262
|
if (stream && stream.dshReq) stream.dshReq.write(un64(msg.data));
|
|
257
263
|
} else if (msg.type === 'http:end') {
|
|
264
|
+
const ab = artifactFormBodies.get(msg.streamId);
|
|
265
|
+
if (ab) { artifactFormBodies.delete(msg.streamId); serveArtifactFromForm(ws, msg.streamId, Buffer.concat(ab)); return; }
|
|
258
266
|
const stream = streams.get(msg.streamId);
|
|
259
267
|
if (stream && stream.dshReq) stream.dshReq.end();
|
|
260
268
|
} else if (msg.type === 'ws:open') {
|
|
@@ -325,6 +333,90 @@ export function startAgent(opts) {
|
|
|
325
333
|
}, 15000);
|
|
326
334
|
}
|
|
327
335
|
|
|
336
|
+
// [Artifact Viewer] 无头机产物 HTTP 直出:GET /api/host.artifact?path=...
|
|
337
|
+
// 由注入脚本在浏览器劫持 openPath 点击后以新窗口打开 —— 无显示环境无法
|
|
338
|
+
// "本地打开",改为把文本/代码类产物以对应 MIME 直出给浏览器渲染。
|
|
339
|
+
const ARTIFACT_TEXT_EXT = new Set([
|
|
340
|
+
'.md', '.markdown', '.txt', '.log', '.json', '.yml', '.yaml', '.toml', '.ini',
|
|
341
|
+
'.cfg', '.conf', '.env', '.csv', '.tsv', '.js', '.mjs', '.cjs', '.ts', '.tsx',
|
|
342
|
+
'.jsx', '.py', '.rb', '.go', '.rs', '.java', '.kt', '.c', '.h', '.cpp', '.hpp',
|
|
343
|
+
'.cs', '.php', '.sh', '.bash', '.zsh', '.sql', '.html', '.htm', '.css', '.scss',
|
|
344
|
+
'.less', '.vue', '.svelte', '.swift', '.lua', '.r', '.pl', '.xml', '.svg',
|
|
345
|
+
]);
|
|
346
|
+
const ARTIFACT_MIME = {
|
|
347
|
+
'.md': 'text/markdown; charset=utf-8', '.markdown': 'text/markdown; charset=utf-8',
|
|
348
|
+
'.html': 'text/html; charset=utf-8', '.htm': 'text/html; charset=utf-8',
|
|
349
|
+
'.css': 'text/css; charset=utf-8', '.scss': 'text/x-scss; charset=utf-8',
|
|
350
|
+
'.js': 'text/javascript; charset=utf-8', '.mjs': 'text/javascript; charset=utf-8',
|
|
351
|
+
'.cjs': 'text/javascript; charset=utf-8', '.ts': 'text/typescript; charset=utf-8',
|
|
352
|
+
'.tsx': 'text/typescript; charset=utf-8', '.jsx': 'text/typescript; charset=utf-8',
|
|
353
|
+
'.json': 'application/json; charset=utf-8', '.xml': 'application/xml; charset=utf-8',
|
|
354
|
+
'.svg': 'image/svg+xml', '.py': 'text/x-python; charset=utf-8',
|
|
355
|
+
'.sh': 'text/x-shellscript; charset=utf-8', '.bash': 'text/x-shellscript; charset=utf-8',
|
|
356
|
+
'.csv': 'text/csv; charset=utf-8', '.yml': 'text/yaml; charset=utf-8',
|
|
357
|
+
'.yaml': 'text/yaml; charset=utf-8', '.sql': 'text/x-sql; charset=utf-8',
|
|
358
|
+
};
|
|
359
|
+
const ARTIFACT_MAX_BYTES = 2 * 1024 * 1024;
|
|
360
|
+
|
|
361
|
+
function artifactRespond(ws, streamId, status, headers, body) {
|
|
362
|
+
send(ws, { type: 'http:response', streamId, status, headers });
|
|
363
|
+
if (body) send(ws, { type: 'http:data', streamId, data: b64(body) });
|
|
364
|
+
send(ws, { type: 'http:end', streamId });
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const artifactFormBodies = new Map();
|
|
368
|
+
|
|
369
|
+
function serveArtifactFromForm(ws, sid, body) {
|
|
370
|
+
let pth = '';
|
|
371
|
+
const ct = 'application/x-www-form-urlencoded';
|
|
372
|
+
try { pth = new URLSearchParams(body.toString('utf8')).get('path') || ''; } catch {}
|
|
373
|
+
serveArtifactResolved(ws, sid, pth);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function serveArtifactResolved(ws, sid, rawPath) {
|
|
377
|
+
const fail = (status, text) => artifactRespond(ws, sid, status,
|
|
378
|
+
{ 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' },
|
|
379
|
+
Buffer.from(JSON.stringify({ ok: false, error: { code: 'artifact', message: text } })));
|
|
380
|
+
return serveArtifactFile(ws, sid, rawPath, fail);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function serveArtifactIfRequested(ws, msg) {
|
|
384
|
+
let u;
|
|
385
|
+
try { u = new URL(msg.path, 'http://local'); } catch { return false; }
|
|
386
|
+
if (u.pathname !== '/api/host.artifact') return false;
|
|
387
|
+
const sid = msg.streamId;
|
|
388
|
+
if (msg.method === 'POST') {
|
|
389
|
+
// [EO规避] 该URL的GET响应会被EdgeOne静默吞杀(三出口实测),改走POST表单体
|
|
390
|
+
artifactFormBodies.set(sid, []);
|
|
391
|
+
return true;
|
|
392
|
+
}
|
|
393
|
+
if (msg.method === 'GET') {
|
|
394
|
+
const pth = u.searchParams.get('path') || '';
|
|
395
|
+
return serveArtifactResolved(ws, sid, pth);
|
|
396
|
+
}
|
|
397
|
+
return false;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function serveArtifactFile(ws, sid, rawPath, fail) {
|
|
401
|
+
const ext = path.extname(rawPath).toLowerCase();
|
|
402
|
+
const dotfile = /(^|\/)(\.[a-z0-9_.-]+)$/i.test(path.basename(rawPath));
|
|
403
|
+
if (!ARTIFACT_TEXT_EXT.has(ext) && !dotfile) return fail(415, `unsupported artifact type: ${ext || '(none)'}`);
|
|
404
|
+
let st;
|
|
405
|
+
try { st = fs.statSync(rawPath); } catch { return fail(404, 'file not found'); }
|
|
406
|
+
if (!st.isFile()) return fail(400, 'not a regular file');
|
|
407
|
+
if (st.size > ARTIFACT_MAX_BYTES) return fail(413, `artifact too large (${st.size} bytes > ${ARTIFACT_MAX_BYTES})`);
|
|
408
|
+
let buf;
|
|
409
|
+
try { buf = fs.readFileSync(rawPath); } catch (e) { return fail(500, 'read failed: ' + e.message); }
|
|
410
|
+
const mime = ARTIFACT_MIME[ext] || 'text/plain; charset=utf-8';
|
|
411
|
+
artifactRespond(ws, sid, 200, {
|
|
412
|
+
'content-type': mime,
|
|
413
|
+
'content-disposition': `inline; filename="${path.basename(rawPath)}"`,
|
|
414
|
+
'cache-control': 'no-store',
|
|
415
|
+
}, buf);
|
|
416
|
+
log.log?.(`[agent] artifact served: ${rawPath} (${buf.length}B)`);
|
|
417
|
+
return true;
|
|
418
|
+
}
|
|
419
|
+
|
|
328
420
|
function handleHttpRequest(ws, msg) {
|
|
329
421
|
const { streamId, method, path, headers } = msg;
|
|
330
422
|
|
package/package.json
CHANGED