remote-dsh 0.4.9 → 0.6.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.
- package/dist/bin.js +370 -197
- package/package.json +3 -3
package/dist/bin.js
CHANGED
|
@@ -2,84 +2,58 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* rdsh CLI 入口(手写参数解析,零依赖)。
|
|
4
4
|
*
|
|
5
|
-
* rdsh
|
|
6
|
-
* rdsh join <hub-url> [--token <t>] [--
|
|
5
|
+
* rdsh host setup lan|cloud
|
|
6
|
+
* rdsh host join <hub-url> [--token <t>] [--name <n>] [--dsh <p>] [--insecure]
|
|
7
|
+
* rdsh host serve | service install|status|uninstall | leave | user ...
|
|
7
8
|
* rdsh hub serve|user|host|service ... [--config <path>]
|
|
8
|
-
* rdsh user add|passwd <name> | ls | rm <name> [--config <path>]
|
|
9
|
-
* rdsh service install|uninstall|status [--config <path>]
|
|
10
9
|
* rdsh --version | --help
|
|
11
10
|
*/
|
|
12
11
|
import { createInterface } from "node:readline";
|
|
13
|
-
import {
|
|
12
|
+
import { rm } from "node:fs/promises";
|
|
13
|
+
import { hostname as osHostname } from "node:os";
|
|
14
|
+
import { dirname } from "node:path";
|
|
15
|
+
import { serve, join, registerJoin, findDsh, selfRevoke, readPersistedToken, clearPersistedToken, UserManager, resolveConfigPath, loadConfig, saveConfig, installService, uninstallService, serviceStatus, HOST_SERVICE_NAME, JOIN_SERVICE_NAME, HUB_SERVICE_NAME, } from "rdsh-gateway";
|
|
14
16
|
import { loadHubConfig, resolveHubConfigPath, HubDb, hashPassword, serveHub, } from "rdsh-hub";
|
|
15
17
|
import { VERSION } from "./index.js";
|
|
16
18
|
const HELP = `rdsh — secure remote access for DeepSeek Harness
|
|
17
19
|
|
|
18
20
|
Usage:
|
|
19
|
-
rdsh
|
|
20
|
-
rdsh join <hub-url>
|
|
21
|
-
rdsh
|
|
22
|
-
rdsh
|
|
23
|
-
rdsh
|
|
24
|
-
rdsh
|
|
25
|
-
rdsh hub
|
|
26
|
-
rdsh hub
|
|
27
|
-
rdsh
|
|
28
|
-
rdsh
|
|
21
|
+
rdsh host setup lan|cloud Configure this machine (LAN / cloud HTTPS gateway)
|
|
22
|
+
rdsh host join <hub-url> Connect this machine to a hub (outbound tunnel)
|
|
23
|
+
rdsh host serve Run the configured mode in the foreground
|
|
24
|
+
rdsh host service ... Run as a systemd/launchd service
|
|
25
|
+
rdsh host leave Unregister this machine from the hub
|
|
26
|
+
rdsh host user ... Manage gateway users (LAN/cloud password auth)
|
|
27
|
+
rdsh hub serve Start the hub server (cloud, multi-host)
|
|
28
|
+
rdsh hub user add|passwd|rm|ls|unlock|reset-2fa
|
|
29
|
+
rdsh hub audit ls Audit log (login/2FA/sharing/email events)
|
|
30
|
+
rdsh hub host ls|revoke List / revoke hosts (revoke drops the tunnel instantly)
|
|
31
|
+
rdsh hub service ... Run hub as a systemd/launchd service
|
|
29
32
|
|
|
30
33
|
Global:
|
|
31
|
-
--config <path>
|
|
32
|
-
|
|
33
|
-
--version, -v
|
|
34
|
-
--help, -h
|
|
35
|
-
|
|
36
|
-
Options (serve):
|
|
37
|
-
--port <n> Listen port (overrides config; default 8443)
|
|
38
|
-
--host <ip> Bind address (overrides config; default 0.0.0.0)
|
|
39
|
-
--pair-code <code> Preset pairing code (overrides config)
|
|
40
|
-
--session-ttl <sec> Session cookie lifetime (overrides config; default 43200)
|
|
41
|
-
--dsh <path> dsh executable path (overrides config)
|
|
42
|
-
--reset Rotate session keys (all devices must re-auth)
|
|
43
|
-
--no-code Disable auth (trusted network ONLY!)
|
|
44
|
-
|
|
45
|
-
Options (join):
|
|
46
|
-
--token <t> Use an existing host token (skip pairing-code flow)
|
|
47
|
-
--reset Forget the persisted host token and re-pair
|
|
48
|
-
--dsh <path> dsh executable path
|
|
49
|
-
|
|
50
|
-
Options (hub serve):
|
|
51
|
-
--port <n> Listen port (overrides hub config; default 8443)
|
|
52
|
-
--host <ip> Bind address (overrides hub config; default 0.0.0.0)
|
|
53
|
-
|
|
54
|
-
Config (persistent): host, port, sessionTtlSeconds, tls{cert,key}, behindProxy,
|
|
55
|
-
allowFrom[], auth{mode: pair|password|none, pairCode, users[]}, dshPath.
|
|
56
|
-
Hub config (~/.rdsh/hub.json): host, port, tls{cert,key}, dbPath, jwtKeyPath.
|
|
34
|
+
--config <path> Host config (default ~/.rdsh/host.json; also $RDSH_CONFIG)
|
|
35
|
+
For hub commands: hub config (default ~/.rdsh/hub.json; also $RDSH_HUB_CONFIG)
|
|
36
|
+
--version, -v Print version
|
|
37
|
+
--help, -h Print this help
|
|
57
38
|
`;
|
|
58
39
|
/** 子命令级用法(`rdsh <cmd> --help`)。 */
|
|
59
40
|
const SUB_HELP = {
|
|
60
|
-
|
|
41
|
+
host: `Usage: rdsh host <subcommand>
|
|
61
42
|
|
|
62
|
-
|
|
43
|
+
Configure and run this machine (the DSH host).
|
|
63
44
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
--no-code Disable auth (trusted network ONLY!)
|
|
73
|
-
`,
|
|
74
|
-
join: `Usage: rdsh join <hub-url> [options]
|
|
75
|
-
|
|
76
|
-
Connect to a hub via an outbound tunnel (no ports open on this machine).
|
|
45
|
+
Subcommands:
|
|
46
|
+
setup lan Configure a LAN gateway (pair auth, plain http)
|
|
47
|
+
setup cloud Configure a cloud HTTPS gateway (password + TLS + allowFrom)
|
|
48
|
+
join <hub-url> Connect to a hub (interactive token paste; --token for scripts)
|
|
49
|
+
serve Run the configured mode in the foreground
|
|
50
|
+
service install|status|uninstall Run as a systemd/launchd service
|
|
51
|
+
leave Unregister this machine from the hub
|
|
52
|
+
user add|passwd|ls|rm Manage gateway users
|
|
77
53
|
|
|
78
|
-
Options:
|
|
79
|
-
--
|
|
80
|
-
|
|
81
|
-
--dsh <path> dsh executable path
|
|
82
|
-
--insecure Skip TLS certificate verification (self-signed hub)
|
|
54
|
+
Options (setup lan): --port <n> [--pair-code <code>]
|
|
55
|
+
Options (setup cloud): --tls-cert <path> --tls-key <path> [--port <n>] [--allow-from <cidr,...>]
|
|
56
|
+
Options (join): --token <t> --name <n> --dsh <path> --insecure
|
|
83
57
|
`,
|
|
84
58
|
hub: `Usage: rdsh hub <subcommand> [options]
|
|
85
59
|
|
|
@@ -87,7 +61,8 @@ Run the hub server or manage it.
|
|
|
87
61
|
|
|
88
62
|
Subcommands:
|
|
89
63
|
serve Start the hub server (TLS required)
|
|
90
|
-
user add|passwd|rm|ls
|
|
64
|
+
user add|passwd|rm|ls|unlock|reset-2fa Manage users (admin creates accounts; unlock / reset-2fa)
|
|
65
|
+
audit ls [--user <n>] [--event <e>] [--since 24h|7d] Query the audit log
|
|
91
66
|
host ls|revoke List / revoke hosts (revoke drops the tunnel instantly)
|
|
92
67
|
service install|status|uninstall Run hub as a systemd/launchd service
|
|
93
68
|
|
|
@@ -95,31 +70,6 @@ Options (serve):
|
|
|
95
70
|
--config <path> Hub config (default ~/.rdsh/hub.json; also $RDSH_HUB_CONFIG)
|
|
96
71
|
--port <n> Listen port (overrides hub config; default 8443)
|
|
97
72
|
--host <ip> Bind address (overrides hub config; default 0.0.0.0)
|
|
98
|
-
`,
|
|
99
|
-
user: `Usage: rdsh user <action> [name]
|
|
100
|
-
|
|
101
|
-
Manage gateway users (LAN / cloud HTTPS service).
|
|
102
|
-
|
|
103
|
-
Actions:
|
|
104
|
-
add <name> Add a user (interactive password)
|
|
105
|
-
passwd <name> Change a user's password (revokes all their sessions)
|
|
106
|
-
ls List users
|
|
107
|
-
rm <name> Remove a user
|
|
108
|
-
|
|
109
|
-
Options:
|
|
110
|
-
--config <path> Config file (default ~/.rdsh/config.json; also $RDSH_CONFIG)
|
|
111
|
-
`,
|
|
112
|
-
service: `Usage: rdsh service <action>
|
|
113
|
-
|
|
114
|
-
Manage the gateway as a systemd/launchd service.
|
|
115
|
-
|
|
116
|
-
Actions:
|
|
117
|
-
install Install and start (auto-start on boot, restart on crash)
|
|
118
|
-
status Show service status
|
|
119
|
-
uninstall Stop and remove the service
|
|
120
|
-
|
|
121
|
-
Options:
|
|
122
|
-
--config <path> Config file (default ~/.rdsh/config.json; also $RDSH_CONFIG)
|
|
123
73
|
`,
|
|
124
74
|
};
|
|
125
75
|
/** 参数里是否有 --help / -h。 */
|
|
@@ -151,28 +101,14 @@ async function main() {
|
|
|
151
101
|
return;
|
|
152
102
|
}
|
|
153
103
|
switch (command) {
|
|
154
|
-
case "
|
|
155
|
-
|
|
156
|
-
await serve(opts);
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
case "join": {
|
|
160
|
-
const opts = parseJoinArgs(rest);
|
|
161
|
-
await join(opts);
|
|
104
|
+
case "host": {
|
|
105
|
+
await handleHost(rest, cli.configPath);
|
|
162
106
|
return;
|
|
163
107
|
}
|
|
164
108
|
case "hub": {
|
|
165
109
|
await handleHub(rest, cli.configPath);
|
|
166
110
|
return;
|
|
167
111
|
}
|
|
168
|
-
case "user": {
|
|
169
|
-
await handleUser(rest, cli.configPath);
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
case "service": {
|
|
173
|
-
await handleService(rest, cli.configPath);
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
112
|
default:
|
|
177
113
|
console.error(`rdsh: unknown command '${command}'. Run 'rdsh --help'.`);
|
|
178
114
|
process.exitCode = 1;
|
|
@@ -198,59 +134,118 @@ function parseGlobal(argv) {
|
|
|
198
134
|
flags.push(argv[i]);
|
|
199
135
|
}
|
|
200
136
|
}
|
|
201
|
-
// 返回原始值(undefined 或显式 --config);gateway/hub 子命令分别按自己的默认路径 resolve
|
|
202
137
|
return { configPath, flags };
|
|
203
138
|
}
|
|
204
|
-
function
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
139
|
+
function parsePort(v) {
|
|
140
|
+
const n = Number(v);
|
|
141
|
+
if (!Number.isInteger(n) || n < 0 || n > 65535)
|
|
142
|
+
throw new Error(`invalid port '${v}'`);
|
|
143
|
+
return n;
|
|
144
|
+
}
|
|
145
|
+
/** 服务 unit 的子进程 PATH:node 目录(nvm/自装)+ 系统路径,保证 spawn dsh 时 shebang 能解析 node。 */
|
|
146
|
+
function nodePathEnv() {
|
|
147
|
+
return `${dirname(process.execPath)}:/usr/local/bin:/usr/bin:/bin`;
|
|
148
|
+
}
|
|
149
|
+
async function handleHost(args, configPath) {
|
|
150
|
+
const sub = args[0];
|
|
151
|
+
const rest = args.slice(1);
|
|
152
|
+
if (sub === undefined || hasHelp(rest)) {
|
|
153
|
+
console.log(SUB_HELP["host"] ?? HELP);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
switch (sub) {
|
|
157
|
+
case "setup":
|
|
158
|
+
await handleHostSetup(rest, configPath);
|
|
159
|
+
return;
|
|
160
|
+
case "join":
|
|
161
|
+
await handleHostJoin(rest, configPath);
|
|
162
|
+
return;
|
|
163
|
+
case "serve":
|
|
164
|
+
await handleHostServe(rest, configPath);
|
|
165
|
+
return;
|
|
166
|
+
case "service":
|
|
167
|
+
await handleHostService(rest, configPath);
|
|
168
|
+
return;
|
|
169
|
+
case "leave":
|
|
170
|
+
await handleHostLeave(rest, configPath);
|
|
171
|
+
return;
|
|
172
|
+
case "user":
|
|
173
|
+
await handleHostUser(rest, configPath);
|
|
174
|
+
return;
|
|
175
|
+
default:
|
|
176
|
+
throw new Error("usage: rdsh host setup|join|serve|service|leave|user");
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/** `rdsh host setup lan|cloud`:写 host.json(mode + 预设字段),配置完退出。 */
|
|
180
|
+
async function handleHostSetup(args, configPath) {
|
|
181
|
+
const which = args[0];
|
|
182
|
+
const target = resolveConfigPath(configPath);
|
|
183
|
+
const config = await loadConfig(target);
|
|
184
|
+
if (which === "lan") {
|
|
185
|
+
let port = config.port;
|
|
186
|
+
let pairCode = config.auth.pairCode;
|
|
187
|
+
for (let i = 1; i < args.length; i++) {
|
|
188
|
+
const flag = args[i];
|
|
189
|
+
if (flag === "--port")
|
|
190
|
+
port = parsePort(args[++i]);
|
|
191
|
+
else if (flag === "--pair-code")
|
|
192
|
+
pairCode = args[++i];
|
|
193
|
+
else
|
|
245
194
|
throw new Error(`unknown option '${flag}'`);
|
|
246
195
|
}
|
|
196
|
+
config.mode = "lan";
|
|
197
|
+
config.port = port;
|
|
198
|
+
config.tls = undefined;
|
|
199
|
+
config.behindProxy = false;
|
|
200
|
+
config.allowFrom = [];
|
|
201
|
+
config.auth.mode = "pair";
|
|
202
|
+
config.auth.pairCode = pairCode;
|
|
203
|
+
config.dshPath = config.dshPath ?? findDsh() ?? undefined;
|
|
204
|
+
await saveConfig(target, config);
|
|
205
|
+
console.log(`rdsh: host 配置为 LAN 网关(pair,端口 ${port})→ ${target}`);
|
|
206
|
+
console.log("rdsh: 运行 `rdsh host serve` 前台启动,或 `rdsh host service install` 常驻。");
|
|
207
|
+
return;
|
|
247
208
|
}
|
|
248
|
-
|
|
209
|
+
if (which === "cloud") {
|
|
210
|
+
let port = config.port;
|
|
211
|
+
let cert;
|
|
212
|
+
let key;
|
|
213
|
+
let allowFrom = [];
|
|
214
|
+
for (let i = 1; i < args.length; i++) {
|
|
215
|
+
const flag = args[i];
|
|
216
|
+
if (flag === "--port")
|
|
217
|
+
port = parsePort(args[++i]);
|
|
218
|
+
else if (flag === "--tls-cert")
|
|
219
|
+
cert = args[++i];
|
|
220
|
+
else if (flag === "--tls-key")
|
|
221
|
+
key = args[++i];
|
|
222
|
+
else if (flag === "--allow-from")
|
|
223
|
+
allowFrom = (args[++i] ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
224
|
+
else
|
|
225
|
+
throw new Error(`unknown option '${flag}'`);
|
|
226
|
+
}
|
|
227
|
+
if (cert === undefined || key === undefined) {
|
|
228
|
+
throw new Error("usage: rdsh host setup cloud --tls-cert <path> --tls-key <path> [--port <n>] [--allow-from <cidr,...>]");
|
|
229
|
+
}
|
|
230
|
+
config.mode = "cloud";
|
|
231
|
+
config.port = port;
|
|
232
|
+
config.tls = { cert, key };
|
|
233
|
+
config.behindProxy = false;
|
|
234
|
+
config.allowFrom = allowFrom;
|
|
235
|
+
config.auth.mode = "password";
|
|
236
|
+
config.dshPath = config.dshPath ?? findDsh() ?? undefined;
|
|
237
|
+
await saveConfig(target, config);
|
|
238
|
+
console.log(`rdsh: host 配置为云 HTTPS 网关(password,端口 ${port})→ ${target}`);
|
|
239
|
+
console.log("rdsh: 用 `rdsh host user add <name>` 建用户,再 `rdsh host serve` / `rdsh host service install`。");
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
throw new Error("usage: rdsh host setup lan|cloud");
|
|
249
243
|
}
|
|
250
|
-
|
|
244
|
+
/** `rdsh host join <hub>`:注册(join token)→ 写 host.json + session,配置完退出。 */
|
|
245
|
+
async function handleHostJoin(args, configPath) {
|
|
251
246
|
const hubUrl = args[0];
|
|
252
247
|
if (hubUrl === undefined || !/^https?:\/\//.test(hubUrl)) {
|
|
253
|
-
throw new Error("usage: rdsh join <hub-url> [--token <t>] [--dsh <
|
|
248
|
+
throw new Error("usage: rdsh host join <hub-url> [--token <t>] [--name <n>] [--dsh <p>] [--insecure]");
|
|
254
249
|
}
|
|
255
250
|
const opts = { hubUrl };
|
|
256
251
|
for (let i = 1; i < args.length; i++) {
|
|
@@ -261,45 +256,153 @@ function parseJoinArgs(args) {
|
|
|
261
256
|
throw new Error(`missing value for ${name}`);
|
|
262
257
|
return args[i] ?? "";
|
|
263
258
|
};
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
259
|
+
if (flag === "--token")
|
|
260
|
+
opts.token = value(flag);
|
|
261
|
+
else if (flag === "--name")
|
|
262
|
+
opts.name = value(flag);
|
|
263
|
+
else if (flag === "--dsh")
|
|
264
|
+
opts.dshPath = value(flag);
|
|
265
|
+
else if (flag === "--insecure")
|
|
266
|
+
opts.insecure = true;
|
|
267
|
+
else if (flag === "--reset")
|
|
268
|
+
opts.reset = true;
|
|
269
|
+
else
|
|
270
|
+
throw new Error(`unknown option '${flag}'`);
|
|
271
|
+
}
|
|
272
|
+
if (opts.token === undefined && readPersistedToken(hubUrl) === null) {
|
|
273
|
+
// 无 --token 且无持久化 session:TTY 交互粘贴 token;非 TTY 报错不 hang
|
|
274
|
+
if (process.stdin.isTTY) {
|
|
275
|
+
const pasted = (await promptLine("Paste your join token (hub portal → Add host): ")).trim();
|
|
276
|
+
if (pasted === "")
|
|
277
|
+
throw new Error("需要 join token(hub portal → 添加主机 生成)");
|
|
278
|
+
opts.token = pasted;
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
throw new Error("无持久化 session 且未提供 --token;交互式运行粘贴 token");
|
|
279
282
|
}
|
|
280
283
|
}
|
|
281
|
-
|
|
284
|
+
const outcome = await registerJoin(opts);
|
|
285
|
+
const target = resolveConfigPath(configPath);
|
|
286
|
+
const config = await loadConfig(target);
|
|
287
|
+
config.mode = "join";
|
|
288
|
+
config.hub = hubUrl;
|
|
289
|
+
config.name = opts.name ?? osHostname();
|
|
290
|
+
config.insecure = outcome.insecure;
|
|
291
|
+
config.dshPath = opts.dshPath ?? findDsh() ?? config.dshPath;
|
|
292
|
+
await saveConfig(target, config);
|
|
293
|
+
console.log(`rdsh: host 已接入 ${hubUrl}(${config.name})—— session 已保存`);
|
|
294
|
+
console.log("rdsh: 运行 `rdsh host serve` 前台启动,或 `rdsh host service install` 常驻。");
|
|
295
|
+
}
|
|
296
|
+
/** `rdsh host serve`:前台常驻,读 host.json 按 mode 分发(join→隧道,lan/cloud→网关)。 */
|
|
297
|
+
async function handleHostServe(_args, configPath) {
|
|
298
|
+
const target = resolveConfigPath(configPath);
|
|
299
|
+
const config = await loadConfig(target);
|
|
300
|
+
if (config.mode === "join") {
|
|
301
|
+
if (config.hub === undefined)
|
|
302
|
+
throw new Error("host.json 缺 hub(join 模式);先 `rdsh host join <hub>`");
|
|
303
|
+
await join({ hubUrl: config.hub, name: config.name, insecure: config.insecure, dshPath: config.dshPath });
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
await serve({ configPath: target });
|
|
282
307
|
}
|
|
283
|
-
|
|
308
|
+
/** `rdsh host service install|status|uninstall`:读 host.json mode 装对应服务(unit 不含 token)。 */
|
|
309
|
+
async function handleHostService(args, configPath) {
|
|
310
|
+
const action = args[0];
|
|
311
|
+
const target = resolveConfigPath(configPath);
|
|
312
|
+
switch (action) {
|
|
313
|
+
case "install": {
|
|
314
|
+
await maybeRegisterForServiceInstall(args, target);
|
|
315
|
+
const config = await loadConfig(target);
|
|
316
|
+
const name = config.mode === "join" ? JOIN_SERVICE_NAME : HOST_SERVICE_NAME;
|
|
317
|
+
// host 服务会 spawn dsh(shebang `#!/usr/bin/env node`),nvm/自装 node 下需补 node 目录到 PATH(防 127)
|
|
318
|
+
console.log(await installService({ name, args: ["host", "serve"], configPath: target, pathEnv: nodePathEnv() }));
|
|
319
|
+
console.log(`rdsh: host service installed (${name}) —— 开机自启 + 崩溃重启。`);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
case "status": {
|
|
323
|
+
const config = await loadConfig(target);
|
|
324
|
+
const name = config.mode === "join" ? JOIN_SERVICE_NAME : HOST_SERVICE_NAME;
|
|
325
|
+
console.log(`rdsh host service (${name}): ${await serviceStatus(name)}`);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
case "uninstall": {
|
|
329
|
+
const config = await loadConfig(target);
|
|
330
|
+
const name = config.mode === "join" ? JOIN_SERVICE_NAME : HOST_SERVICE_NAME;
|
|
331
|
+
console.log(await uninstallService(name));
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
default:
|
|
335
|
+
throw new Error("usage: rdsh host service install|status|uninstall");
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
/** 一行接入:`host service install <hub-url> --token <t> [--name <n>]` → 注册 + 写 host.json(再装服务)。 */
|
|
339
|
+
async function maybeRegisterForServiceInstall(args, target) {
|
|
340
|
+
const maybeHub = args[1];
|
|
341
|
+
if (maybeHub === undefined || !/^https?:\/\//.test(maybeHub))
|
|
342
|
+
return; // 无 <hub>,走读 host.json 路径
|
|
343
|
+
const joinOpts = { hubUrl: maybeHub };
|
|
344
|
+
for (let i = 2; i < args.length; i++) {
|
|
345
|
+
const flag = args[i];
|
|
346
|
+
if (flag === "--token")
|
|
347
|
+
joinOpts.token = args[++i];
|
|
348
|
+
else if (flag === "--name")
|
|
349
|
+
joinOpts.name = args[++i];
|
|
350
|
+
else if (flag === "--dsh")
|
|
351
|
+
joinOpts.dshPath = args[++i];
|
|
352
|
+
else if (flag === "--insecure")
|
|
353
|
+
joinOpts.insecure = true;
|
|
354
|
+
else
|
|
355
|
+
throw new Error(`unknown option '${flag}'`);
|
|
356
|
+
}
|
|
357
|
+
const outcome = await registerJoin(joinOpts);
|
|
358
|
+
const cfg = await loadConfig(target);
|
|
359
|
+
cfg.mode = "join";
|
|
360
|
+
cfg.hub = maybeHub;
|
|
361
|
+
cfg.name = joinOpts.name ?? osHostname();
|
|
362
|
+
cfg.insecure = outcome.insecure;
|
|
363
|
+
cfg.dshPath = joinOpts.dshPath ?? findDsh() ?? cfg.dshPath;
|
|
364
|
+
await saveConfig(target, cfg);
|
|
365
|
+
}
|
|
366
|
+
/** `rdsh host leave`:self-revoke + 清 session + 删 host.json → 未配置。 */
|
|
367
|
+
async function handleHostLeave(_args, configPath) {
|
|
368
|
+
const target = resolveConfigPath(configPath);
|
|
369
|
+
const config = await loadConfig(target);
|
|
370
|
+
if (config.mode !== "join" || config.hub === undefined) {
|
|
371
|
+
throw new Error("当前不是 join 模式(或缺 hub),无需 leave");
|
|
372
|
+
}
|
|
373
|
+
const token = readPersistedToken(config.hub);
|
|
374
|
+
if (token === null) {
|
|
375
|
+
console.log("rdsh: 本地无 session 文件,仅清理配置");
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
await selfRevoke(config.hub, token, config.insecure === true);
|
|
379
|
+
console.log("rdsh: 已在 hub 自吊销本机");
|
|
380
|
+
}
|
|
381
|
+
clearPersistedToken(config.hub);
|
|
382
|
+
await rm(target, { force: true });
|
|
383
|
+
console.log(`rdsh: host 已注销(清理 ${target})—— 回到未配置状态`);
|
|
384
|
+
}
|
|
385
|
+
/** `rdsh host user add|passwd|ls|rm`:本机网关用户(写 host.json auth.users)。 */
|
|
386
|
+
async function handleHostUser(args, configPath) {
|
|
284
387
|
const action = args[0];
|
|
285
388
|
const um = new UserManager(resolveConfigPath(configPath));
|
|
286
389
|
switch (action) {
|
|
287
390
|
case "add": {
|
|
288
391
|
const name = args[1];
|
|
289
392
|
if (name === undefined)
|
|
290
|
-
throw new Error("usage: rdsh user add <name>");
|
|
393
|
+
throw new Error("usage: rdsh host user add <name>");
|
|
291
394
|
const password = await promptPassword(`password for ${name}: `);
|
|
292
395
|
const again = await promptPassword("confirm: ");
|
|
293
396
|
if (password !== again)
|
|
294
397
|
throw new Error("passwords do not match");
|
|
295
398
|
await um.add(name, password);
|
|
296
|
-
console.log(`rdsh: user '${name}' added
|
|
399
|
+
console.log(`rdsh: user '${name}' added`);
|
|
297
400
|
return;
|
|
298
401
|
}
|
|
299
402
|
case "passwd": {
|
|
300
403
|
const name = args[1];
|
|
301
404
|
if (name === undefined)
|
|
302
|
-
throw new Error("usage: rdsh user passwd <name>");
|
|
405
|
+
throw new Error("usage: rdsh host user passwd <name>");
|
|
303
406
|
const password = await promptPassword(`new password for ${name}: `);
|
|
304
407
|
const again = await promptPassword("confirm: ");
|
|
305
408
|
if (password !== again)
|
|
@@ -318,7 +421,7 @@ async function handleUser(args, configPath) {
|
|
|
318
421
|
case "rm": {
|
|
319
422
|
const name = args[1];
|
|
320
423
|
if (name === undefined)
|
|
321
|
-
throw new Error("usage: rdsh user rm <name>");
|
|
424
|
+
throw new Error("usage: rdsh host user rm <name>");
|
|
322
425
|
const ok = await um.remove(name);
|
|
323
426
|
if (!ok)
|
|
324
427
|
throw new Error(`user '${name}' not found`);
|
|
@@ -326,24 +429,7 @@ async function handleUser(args, configPath) {
|
|
|
326
429
|
return;
|
|
327
430
|
}
|
|
328
431
|
default:
|
|
329
|
-
throw new Error("usage: rdsh user add|passwd|ls|rm");
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
async function handleService(args, configPath) {
|
|
333
|
-
const action = args[0];
|
|
334
|
-
switch (action) {
|
|
335
|
-
case "install":
|
|
336
|
-
console.log(await installService(resolveConfigPath(configPath)));
|
|
337
|
-
console.log("rdsh: service installed — it starts on boot and restarts on crash.");
|
|
338
|
-
return;
|
|
339
|
-
case "status":
|
|
340
|
-
console.log(`rdsh service: ${await serviceStatus()}`);
|
|
341
|
-
return;
|
|
342
|
-
case "uninstall":
|
|
343
|
-
console.log(await uninstallService());
|
|
344
|
-
return;
|
|
345
|
-
default:
|
|
346
|
-
throw new Error("usage: rdsh service install|status|uninstall");
|
|
432
|
+
throw new Error("usage: rdsh host user add|passwd|ls|rm");
|
|
347
433
|
}
|
|
348
434
|
}
|
|
349
435
|
/** 打开 hub 数据库(读 hub config 定位 dbPath)。 */
|
|
@@ -352,6 +438,56 @@ async function openHubDb(configPath) {
|
|
|
352
438
|
const config = await loadHubConfig(hubConfigPath);
|
|
353
439
|
return new HubDb(config.dbPath);
|
|
354
440
|
}
|
|
441
|
+
/** `rdsh hub audit ls [--user <name>] [--event <e>] [--since 24h|7d]`。 */
|
|
442
|
+
async function handleHubAudit(args, configPath) {
|
|
443
|
+
const db = await openHubDb(configPath);
|
|
444
|
+
try {
|
|
445
|
+
// 兼容 `audit ls` 与 `audit`(ls 为可选位置参数)
|
|
446
|
+
const opts = args[0] === "ls" ? args.slice(1) : args;
|
|
447
|
+
const filter = {};
|
|
448
|
+
for (let i = 0; i < opts.length; i++) {
|
|
449
|
+
const flag = opts[i];
|
|
450
|
+
if (flag === "--user") {
|
|
451
|
+
const name = opts[++i];
|
|
452
|
+
if (name === undefined)
|
|
453
|
+
throw new Error("missing value for --user");
|
|
454
|
+
const u = db.getUserByName(name);
|
|
455
|
+
if (u === null)
|
|
456
|
+
throw new Error(`user '${name}' not found`);
|
|
457
|
+
filter.userId = u.id;
|
|
458
|
+
}
|
|
459
|
+
else if (flag === "--event") {
|
|
460
|
+
filter.event = opts[++i];
|
|
461
|
+
if (filter.event === undefined)
|
|
462
|
+
throw new Error("missing value for --event");
|
|
463
|
+
}
|
|
464
|
+
else if (flag === "--since") {
|
|
465
|
+
const v = opts[++i];
|
|
466
|
+
if (v === undefined)
|
|
467
|
+
throw new Error("missing value for --since");
|
|
468
|
+
const m = /^(\d+)([hd])$/.exec(v);
|
|
469
|
+
if (m === null)
|
|
470
|
+
throw new Error(`invalid --since '${v}' (use like 24h or 7d)`);
|
|
471
|
+
filter.since = Date.now() - Number(m[1]) * (m[2] === "h" ? 3_600_000 : 86_400_000);
|
|
472
|
+
}
|
|
473
|
+
else {
|
|
474
|
+
throw new Error(`unknown option '${flag}'`);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
const events = db.listAudit(filter);
|
|
478
|
+
if (events.length === 0) {
|
|
479
|
+
console.log("(no events)");
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
for (const e of events) {
|
|
483
|
+
const user = e.userId !== null ? db.getUserById(e.userId) : null;
|
|
484
|
+
console.log(`${new Date(e.createdAt).toISOString()} ${user?.name ?? "-"} ${e.event} ip=${e.ip} ${e.detailJson}`);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
finally {
|
|
488
|
+
db.close();
|
|
489
|
+
}
|
|
490
|
+
}
|
|
355
491
|
async function handleHub(args, configPath) {
|
|
356
492
|
const sub = args[0];
|
|
357
493
|
const rest = args.slice(1);
|
|
@@ -373,26 +509,30 @@ async function handleHub(args, configPath) {
|
|
|
373
509
|
await handleHubHost(rest, configPath);
|
|
374
510
|
return;
|
|
375
511
|
}
|
|
512
|
+
case "audit": {
|
|
513
|
+
await handleHubAudit(rest, configPath);
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
376
516
|
case "service": {
|
|
377
517
|
const action = rest[0];
|
|
378
518
|
const hubConfigPath = resolveHubConfigPath(configPath);
|
|
379
519
|
switch (action) {
|
|
380
520
|
case "install":
|
|
381
|
-
console.log(await installService(
|
|
382
|
-
console.log(
|
|
521
|
+
console.log(await installService({ name: HUB_SERVICE_NAME, args: ["hub", "serve"], configPath: hubConfigPath }));
|
|
522
|
+
console.log(`rdsh: hub service installed (${HUB_SERVICE_NAME}) —— 开机自启 + 崩溃重启。`);
|
|
383
523
|
return;
|
|
384
524
|
case "status":
|
|
385
|
-
console.log(`rdsh hub service: ${await serviceStatus()}`);
|
|
525
|
+
console.log(`rdsh hub service (${HUB_SERVICE_NAME}): ${await serviceStatus(HUB_SERVICE_NAME)}`);
|
|
386
526
|
return;
|
|
387
527
|
case "uninstall":
|
|
388
|
-
console.log(await uninstallService());
|
|
528
|
+
console.log(await uninstallService(HUB_SERVICE_NAME));
|
|
389
529
|
return;
|
|
390
530
|
default:
|
|
391
531
|
throw new Error("usage: rdsh hub service install|status|uninstall");
|
|
392
532
|
}
|
|
393
533
|
}
|
|
394
534
|
default:
|
|
395
|
-
throw new Error("usage: rdsh hub serve|user|host|service");
|
|
535
|
+
throw new Error("usage: rdsh hub serve|user|host|audit|service");
|
|
396
536
|
}
|
|
397
537
|
}
|
|
398
538
|
function parseHubServeArgs(args, configPath) {
|
|
@@ -450,7 +590,6 @@ async function handleHubUser(args, configPath) {
|
|
|
450
590
|
if (db.getUserByName(name) !== null)
|
|
451
591
|
throw new Error(`user '${name}' already exists`);
|
|
452
592
|
if (noPassword) {
|
|
453
|
-
// 首次登录自设密码(must_change=1,密码占位不可用)
|
|
454
593
|
db.createUser(name, "scrypt:disabled", new Date().toISOString(), true);
|
|
455
594
|
console.log(`rdsh: user '${name}' created — they must set a password on first sign-in (${hubConfigPath})`);
|
|
456
595
|
}
|
|
@@ -489,12 +628,35 @@ async function handleHubUser(args, configPath) {
|
|
|
489
628
|
console.log(`rdsh: user '${name}' removed (their hosts removed)`);
|
|
490
629
|
return;
|
|
491
630
|
}
|
|
631
|
+
case "unlock": {
|
|
632
|
+
const name = args[1];
|
|
633
|
+
if (name === undefined)
|
|
634
|
+
throw new Error("usage: rdsh hub user unlock <name>");
|
|
635
|
+
const user = db.getUserByName(name);
|
|
636
|
+
if (user === null)
|
|
637
|
+
throw new Error(`user '${name}' not found`);
|
|
638
|
+
db.unlockAccount(user.id);
|
|
639
|
+
console.log(`rdsh: user '${name}' unlocked`);
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
case "reset-2fa": {
|
|
643
|
+
const name = args[1];
|
|
644
|
+
if (name === undefined)
|
|
645
|
+
throw new Error("usage: rdsh hub user reset-2fa <name>");
|
|
646
|
+
const user = db.getUserByName(name);
|
|
647
|
+
if (user === null)
|
|
648
|
+
throw new Error(`user '${name}' not found`);
|
|
649
|
+
db.clearTotpSecret(user.id);
|
|
650
|
+
db.bumpVersion(user.id);
|
|
651
|
+
console.log(`rdsh: 2FA reset for '${name}' (their sessions revoked)`);
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
492
654
|
default:
|
|
493
|
-
throw new Error("usage: rdsh hub user add|passwd|ls|rm");
|
|
655
|
+
throw new Error("usage: rdsh hub user add|passwd|ls|rm|unlock|reset-2fa");
|
|
494
656
|
}
|
|
495
657
|
}
|
|
496
658
|
finally {
|
|
497
|
-
db.close();
|
|
659
|
+
db.close();
|
|
498
660
|
}
|
|
499
661
|
}
|
|
500
662
|
async function handleHubHost(args, configPath) {
|
|
@@ -562,11 +724,21 @@ function promptPassword(prompt) {
|
|
|
562
724
|
});
|
|
563
725
|
}
|
|
564
726
|
// 非 TTY(管道/重定向):一次性读完 stdin(EOF),逐行消费。
|
|
565
|
-
// 注意:不能每次调用新建 readline —— 第二个 interface 会读不到剩余行。
|
|
566
727
|
process.stdout.write(prompt);
|
|
567
728
|
return nextPipeLine();
|
|
568
729
|
}
|
|
569
730
|
let pipeLines = null;
|
|
731
|
+
/** 读取一行(echo,用于粘贴 token 等非敏感输入)。 */
|
|
732
|
+
function promptLine(prompt) {
|
|
733
|
+
process.stdout.write(prompt);
|
|
734
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
735
|
+
return new Promise((resolve) => {
|
|
736
|
+
rl.question("", (answer) => {
|
|
737
|
+
rl.close();
|
|
738
|
+
resolve(answer);
|
|
739
|
+
});
|
|
740
|
+
});
|
|
741
|
+
}
|
|
570
742
|
function nextPipeLine() {
|
|
571
743
|
if (pipeLines !== null) {
|
|
572
744
|
return Promise.resolve(pipeLines.shift() ?? "");
|
|
@@ -578,6 +750,7 @@ function nextPipeLine() {
|
|
|
578
750
|
rl.on("close", () => resolve(pipeLines.shift() ?? ""));
|
|
579
751
|
});
|
|
580
752
|
}
|
|
581
|
-
// 管理命令(
|
|
753
|
+
// 管理命令(host setup/join/leave/service/user 完成后)显式退出:
|
|
754
|
+
// 常驻命令(host serve / hub serve)不 resolve,靠信号退出。
|
|
582
755
|
void main().then(() => process.exit(process.exitCode ?? 0));
|
|
583
756
|
//# sourceMappingURL=bin.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "remote-dsh",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Secure remote access for DeepSeek Harness: rdsh serve / rdsh join / rdsh hub",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
"node": ">=22"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"rdsh-gateway": "0.
|
|
27
|
-
"rdsh-hub": "0.
|
|
26
|
+
"rdsh-gateway": "0.4.0",
|
|
27
|
+
"rdsh-hub": "0.4.0"
|
|
28
28
|
},
|
|
29
29
|
"scripts": {
|
|
30
30
|
"build": "tsc -p tsconfig.json",
|