terminal-expose 1.0.0 → 1.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.
- package/README.md +5 -5
- package/index.js +38 -440
- package/package.json +3 -2
- package/src/config/env.js +24 -0
- package/src/core/socket.js +56 -0
- package/src/core/terminal.js +88 -0
- package/src/core/tunnel.js +96 -0
- package/src/server/app.js +46 -0
- package/src/templates/viewer.js +95 -0
- package/src/utils/args.js +52 -0
- package/src/utils/system.js +34 -0
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Terminal Expose
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Interactive terminal sharing from your own machine.
|
|
4
4
|
|
|
5
|
-
`terminal-expose` starts a real shell or command on the host where it is run, prints a private
|
|
5
|
+
`terminal-expose` starts a real shell or command on the host where it is run, prints a private URL, and allows collaborative, interactive access to anyone who opens that URL.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -38,7 +38,7 @@ Local URL : http://localhost:3000/s/<token>
|
|
|
38
38
|
LAN URL : http://192.168.1.10:3000/s/<token>
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
Share the full token URL with
|
|
41
|
+
Share the full token URL with your collaborators. They will have full read and write access to the terminal.
|
|
42
42
|
|
|
43
43
|
## Public Internet Access
|
|
44
44
|
|
|
@@ -61,7 +61,7 @@ You can request a custom localtunnel subdomain:
|
|
|
61
61
|
terminal-expose --public --subdomain my-terminal-demo bash
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
-
The tunnel URL works outside your Wi-Fi as long as the CLI is still running. If `loca.lt` asks the
|
|
64
|
+
The tunnel URL works outside your Wi-Fi as long as the CLI is still running. If `loca.lt` asks the user for a tunnel password, share the printed tunnel password too.
|
|
65
65
|
|
|
66
66
|
For a permanent setup, run it on a public server or forward TCP port `3000` to the machine running the CLI:
|
|
67
67
|
|
|
@@ -83,4 +83,4 @@ TERM_COLS=160 TERM_ROWS=48 terminal-expose bash
|
|
|
83
83
|
|
|
84
84
|
## Security
|
|
85
85
|
|
|
86
|
-
Anyone with the full URL can
|
|
86
|
+
Anyone with the full URL can view and interact with the session. Do not type passwords, API keys, or private commands in a shared terminal.
|
package/index.js
CHANGED
|
@@ -1,81 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
const express = require("express");
|
|
4
3
|
const http = require("http");
|
|
5
|
-
const {
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const localtunnel = require("localtunnel");
|
|
4
|
+
const { parseArgs } = require("./src/utils/args");
|
|
5
|
+
const { getDefaultShell, getLanUrls } = require("./src/utils/system");
|
|
6
|
+
const { TerminalSession } = require("./src/core/terminal");
|
|
7
|
+
const { setupSocket } = require("./src/core/socket");
|
|
8
|
+
const { startPublicTunnel } = require("./src/core/tunnel");
|
|
9
|
+
const { createApp } = require("./src/server/app");
|
|
12
10
|
|
|
13
|
-
const
|
|
14
|
-
const server = http.createServer(app);
|
|
11
|
+
const { PORT, HOST, SESSION_TOKEN, EXTERNAL_URL } = require("./src/config/env");
|
|
15
12
|
|
|
16
|
-
|
|
17
|
-
const HOST = process.env.HOST || "0.0.0.0";
|
|
18
|
-
const MAX_BUFFER = Number(process.env.MAX_BUFFER || 100000);
|
|
19
|
-
const ECHO_TO_STDOUT = process.env.ECHO_TO_STDOUT !== "0";
|
|
20
|
-
const SESSION_TOKEN =
|
|
21
|
-
process.env.SESSION_TOKEN || crypto.randomBytes(18).toString("base64url");
|
|
13
|
+
// arguments
|
|
22
14
|
const rawArgs = process.argv.slice(2);
|
|
23
15
|
|
|
24
|
-
function parseArgs(args) {
|
|
25
|
-
const options = {
|
|
26
|
-
publicTunnel: false,
|
|
27
|
-
tunnelSubdomain: process.env.TUNNEL_SUBDOMAIN || null,
|
|
28
|
-
tunnelHost: process.env.TUNNEL_HOST || undefined,
|
|
29
|
-
};
|
|
30
|
-
const commandArgs = [];
|
|
31
|
-
|
|
32
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
33
|
-
const arg = args[index];
|
|
34
|
-
|
|
35
|
-
if (arg === "--public" || arg === "--tunnel") {
|
|
36
|
-
options.publicTunnel = true;
|
|
37
|
-
continue;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (arg === "--subdomain") {
|
|
41
|
-
options.tunnelSubdomain = args[index + 1] || null;
|
|
42
|
-
index += 1;
|
|
43
|
-
continue;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (arg.startsWith("--subdomain=")) {
|
|
47
|
-
options.tunnelSubdomain = arg.slice("--subdomain=".length) || null;
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (arg === "--tunnel-host") {
|
|
52
|
-
options.tunnelHost = args[index + 1] || undefined;
|
|
53
|
-
index += 1;
|
|
54
|
-
continue;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (arg.startsWith("--tunnel-host=")) {
|
|
58
|
-
options.tunnelHost = arg.slice("--tunnel-host=".length) || undefined;
|
|
59
|
-
continue;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
commandArgs.push(arg);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (
|
|
66
|
-
process.env.PUBLIC_TUNNEL === "1" ||
|
|
67
|
-
process.env.PUBLIC_TUNNEL === "true"
|
|
68
|
-
) {
|
|
69
|
-
options.publicTunnel = true;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return { options, commandArgs };
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const { options, commandArgs: command } = parseArgs(rawArgs);
|
|
76
|
-
|
|
77
16
|
if (rawArgs.includes("--help") || rawArgs.includes("-h")) {
|
|
78
17
|
console.log(`Terminal Expose
|
|
18
|
+
Interactive terminal session sharing over HTTP and WebSocket.
|
|
79
19
|
|
|
80
20
|
Usage:
|
|
81
21
|
terminal-expose [options] [command] [args...]
|
|
@@ -113,385 +53,41 @@ if (rawArgs.includes("--version") || rawArgs.includes("-v")) {
|
|
|
113
53
|
process.exit(0);
|
|
114
54
|
}
|
|
115
55
|
|
|
116
|
-
const
|
|
117
|
-
command.length > 0
|
|
118
|
-
? command[0]
|
|
119
|
-
: process.platform === "win32"
|
|
120
|
-
? "powershell.exe"
|
|
121
|
-
: process.env.SHELL || "bash";
|
|
122
|
-
const shellArgs = command.length > 0 ? command.slice(1) : [];
|
|
123
|
-
|
|
124
|
-
function getLanUrls() {
|
|
125
|
-
const urls = [];
|
|
126
|
-
const interfaces = os.networkInterfaces();
|
|
127
|
-
|
|
128
|
-
Object.values(interfaces).forEach((addresses = []) => {
|
|
129
|
-
addresses
|
|
130
|
-
.filter((address) => address.family === "IPv4" && !address.internal)
|
|
131
|
-
.forEach((address) => {
|
|
132
|
-
urls.push(`http://${address.address}:${PORT}/s/${SESSION_TOKEN}`);
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
return urls;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function getLocaltunnelPassword() {
|
|
140
|
-
return new Promise((resolve) => {
|
|
141
|
-
let settled = false;
|
|
142
|
-
|
|
143
|
-
function finish(value) {
|
|
144
|
-
if (settled) {
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
settled = true;
|
|
149
|
-
resolve(value);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const request = https.get("https://loca.lt/mytunnelpassword", (response) => {
|
|
153
|
-
if (response.statusCode !== 200) {
|
|
154
|
-
response.resume();
|
|
155
|
-
finish(null);
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
let body = "";
|
|
160
|
-
response.setEncoding("utf8");
|
|
161
|
-
response.on("data", (chunk) => {
|
|
162
|
-
body += chunk;
|
|
163
|
-
});
|
|
164
|
-
response.on("end", () => {
|
|
165
|
-
finish(body.trim() || null);
|
|
166
|
-
});
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
request.setTimeout(3000, () => {
|
|
170
|
-
request.destroy();
|
|
171
|
-
finish(null);
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
request.on("error", () => {
|
|
175
|
-
finish(null);
|
|
176
|
-
});
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
async function startPublicTunnel() {
|
|
181
|
-
const localHost =
|
|
182
|
-
HOST === "0.0.0.0" || HOST === "::" ? "127.0.0.1" : HOST;
|
|
183
|
-
const tunnelOptions = {
|
|
184
|
-
port: PORT,
|
|
185
|
-
local_host: localHost,
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
if (options.tunnelSubdomain) {
|
|
189
|
-
tunnelOptions.subdomain = options.tunnelSubdomain;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
if (options.tunnelHost) {
|
|
193
|
-
tunnelOptions.host = options.tunnelHost;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
console.log("Public tunnel: starting...");
|
|
197
|
-
|
|
198
|
-
const tunnel = await localtunnel(tunnelOptions);
|
|
199
|
-
const publicUrl = `${tunnel.url.replace(/\/$/, "")}/s/${SESSION_TOKEN}`;
|
|
200
|
-
|
|
201
|
-
console.log(`Public URL: ${publicUrl}`);
|
|
202
|
-
|
|
203
|
-
if (new URL(tunnel.url).hostname.endsWith(".loca.lt")) {
|
|
204
|
-
const password = await getLocaltunnelPassword();
|
|
205
|
-
|
|
206
|
-
if (password) {
|
|
207
|
-
console.log(`Tunnel password: ${password}`);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
tunnel.on("error", (error) => {
|
|
212
|
-
console.error(`Public tunnel error: ${error.message}`);
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
tunnel.on("close", () => {
|
|
216
|
-
console.log("Public tunnel closed.");
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
["SIGINT", "SIGTERM"].forEach((signal) => {
|
|
220
|
-
process.once(signal, () => {
|
|
221
|
-
tunnel.close();
|
|
222
|
-
process.kill(process.pid, signal);
|
|
223
|
-
});
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
return tunnel;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
const VIEWER_HTML = String.raw`<!doctype html>
|
|
230
|
-
<html>
|
|
231
|
-
<head>
|
|
232
|
-
<meta charset="utf-8" />
|
|
233
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
234
|
-
<title>Terminal Viewer</title>
|
|
235
|
-
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm/css/xterm.css" />
|
|
236
|
-
<style>
|
|
237
|
-
html,
|
|
238
|
-
body {
|
|
239
|
-
margin: 0;
|
|
240
|
-
padding: 0;
|
|
241
|
-
background: #050505;
|
|
242
|
-
color: #f2f2f2;
|
|
243
|
-
width: 100%;
|
|
244
|
-
height: 100%;
|
|
245
|
-
overflow: hidden;
|
|
246
|
-
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
.topbar {
|
|
250
|
-
align-items: center;
|
|
251
|
-
background: #111;
|
|
252
|
-
border-bottom: 1px solid #2a2a2a;
|
|
253
|
-
box-sizing: border-box;
|
|
254
|
-
display: flex;
|
|
255
|
-
gap: 12px;
|
|
256
|
-
height: 42px;
|
|
257
|
-
justify-content: space-between;
|
|
258
|
-
padding: 0 14px;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
.title {
|
|
262
|
-
font-size: 13px;
|
|
263
|
-
font-weight: 650;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
.status {
|
|
267
|
-
align-items: center;
|
|
268
|
-
color: #b8b8b8;
|
|
269
|
-
display: flex;
|
|
270
|
-
font-size: 12px;
|
|
271
|
-
gap: 8px;
|
|
272
|
-
min-width: 0;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
.dot {
|
|
276
|
-
background: #9ca3af;
|
|
277
|
-
border-radius: 999px;
|
|
278
|
-
flex: 0 0 auto;
|
|
279
|
-
height: 8px;
|
|
280
|
-
width: 8px;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
.dot.connected {
|
|
284
|
-
background: #22c55e;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
.dot.disconnected {
|
|
288
|
-
background: #ef4444;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
#terminal {
|
|
292
|
-
width: 100vw;
|
|
293
|
-
height: calc(100vh - 42px);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
.xterm {
|
|
297
|
-
height: 100%;
|
|
298
|
-
}
|
|
299
|
-
</style>
|
|
300
|
-
</head>
|
|
301
|
-
<body>
|
|
302
|
-
<header class="topbar">
|
|
303
|
-
<div class="title">Terminal Expose</div>
|
|
304
|
-
<div class="status">
|
|
305
|
-
<span id="dot" class="dot"></span>
|
|
306
|
-
<span id="status">Connecting</span>
|
|
307
|
-
</div>
|
|
308
|
-
</header>
|
|
309
|
-
<div id="terminal"></div>
|
|
310
|
-
<script src="/socket.io/socket.io.js"></script>
|
|
311
|
-
<script src="https://cdn.jsdelivr.net/npm/xterm/lib/xterm.js"></script>
|
|
312
|
-
<script>
|
|
313
|
-
const token = window.location.pathname.split('/').filter(Boolean).pop();
|
|
314
|
-
const statusText = document.getElementById('status');
|
|
315
|
-
const dot = document.getElementById('dot');
|
|
316
|
-
const term = new Terminal({
|
|
317
|
-
disableStdin: true,
|
|
318
|
-
cursorBlink: false,
|
|
319
|
-
convertEol: true,
|
|
320
|
-
fontFamily: '"JetBrains Mono", "SFMono-Regular", Consolas, monospace',
|
|
321
|
-
fontSize: 14,
|
|
322
|
-
scrollback: 10000,
|
|
323
|
-
theme: {
|
|
324
|
-
background: '#050505',
|
|
325
|
-
foreground: '#f4f4f5',
|
|
326
|
-
cursor: '#f4f4f5',
|
|
327
|
-
black: '#18181b',
|
|
328
|
-
red: '#ef4444',
|
|
329
|
-
green: '#22c55e',
|
|
330
|
-
yellow: '#eab308',
|
|
331
|
-
blue: '#3b82f6',
|
|
332
|
-
magenta: '#a855f7',
|
|
333
|
-
cyan: '#06b6d4',
|
|
334
|
-
white: '#e4e4e7'
|
|
335
|
-
}
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
term.open(document.getElementById('terminal'));
|
|
339
|
-
|
|
340
|
-
function setStatus(text, className) {
|
|
341
|
-
statusText.textContent = text;
|
|
342
|
-
dot.className = 'dot ' + (className || '');
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
const socket = io({
|
|
346
|
-
auth: { token },
|
|
347
|
-
reconnectionAttempts: 10,
|
|
348
|
-
timeout: 5000
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
socket.on('connect', () => {
|
|
352
|
-
setStatus('Connected', 'connected');
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
socket.on('terminal-output', (data) => {
|
|
356
|
-
term.write(data);
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
socket.on('terminal-exit', ({ exitCode, signal }) => {
|
|
360
|
-
const suffix = signal ? 'signal ' + signal : 'exit code ' + exitCode;
|
|
361
|
-
setStatus('Command ended: ' + suffix, 'disconnected');
|
|
362
|
-
term.writeln('\r\n[Command ended: ' + suffix + ']');
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
socket.on('connect_error', (error) => {
|
|
366
|
-
setStatus(error.message || 'Connection error', 'disconnected');
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
socket.on('disconnect', () => {
|
|
370
|
-
setStatus('Disconnected', 'disconnected');
|
|
371
|
-
});
|
|
372
|
-
</script>
|
|
373
|
-
</body>
|
|
374
|
-
</html>`;
|
|
375
|
-
|
|
376
|
-
const io = new Server(server, {
|
|
377
|
-
cors: {
|
|
378
|
-
origin: false,
|
|
379
|
-
},
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
io.use((socket, next) => {
|
|
383
|
-
const token = socket.handshake.auth?.token;
|
|
384
|
-
|
|
385
|
-
if (token !== SESSION_TOKEN) {
|
|
386
|
-
return next(new Error("unauthorized"));
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
return next();
|
|
390
|
-
});
|
|
391
|
-
|
|
392
|
-
const shell = pty.spawn(shellCommand, shellArgs, {
|
|
393
|
-
name: "xterm-256color",
|
|
394
|
-
cols: Number(process.env.TERM_COLS || 120),
|
|
395
|
-
rows: Number(process.env.TERM_ROWS || 40),
|
|
396
|
-
cwd: process.env.WORKDIR || process.cwd(),
|
|
397
|
-
env: {
|
|
398
|
-
...process.env,
|
|
399
|
-
TERM: "xterm-256color",
|
|
400
|
-
},
|
|
401
|
-
});
|
|
402
|
-
|
|
403
|
-
let terminalBuffer = "";
|
|
404
|
-
let terminalExit = null;
|
|
405
|
-
|
|
406
|
-
if (process.stdin.isTTY) {
|
|
407
|
-
process.stdin.setRawMode(true);
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
process.stdin.resume();
|
|
411
|
-
process.stdin.on("data", (data) => {
|
|
412
|
-
shell.write(data);
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
shell.onData((data) => {
|
|
416
|
-
if (ECHO_TO_STDOUT) {
|
|
417
|
-
process.stdout.write(data);
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
terminalBuffer += data;
|
|
421
|
-
|
|
422
|
-
if (terminalBuffer.length > MAX_BUFFER) {
|
|
423
|
-
terminalBuffer = terminalBuffer.slice(-MAX_BUFFER);
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
io.emit("terminal-output", data);
|
|
427
|
-
});
|
|
428
|
-
|
|
429
|
-
shell.onExit(({ exitCode, signal }) => {
|
|
430
|
-
terminalExit = { exitCode, signal };
|
|
431
|
-
io.emit("terminal-exit", terminalExit);
|
|
432
|
-
console.log(`Terminal command exited: code=${exitCode} signal=${signal}`);
|
|
56
|
+
const { options, commandArgs: command } = parseArgs(rawArgs);
|
|
433
57
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
}
|
|
58
|
+
const shellCommand = command.length > 0 ? command[0] : getDefaultShell();
|
|
59
|
+
const shellArgs = command.length > 0 ? command.slice(1) : [];
|
|
437
60
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
}, 250);
|
|
441
|
-
});
|
|
61
|
+
// Initialize Terminal
|
|
62
|
+
const terminalSession = new TerminalSession(shellCommand, shellArgs);
|
|
442
63
|
|
|
443
|
-
|
|
64
|
+
// Initialize Express App
|
|
65
|
+
let ioInstance = null;
|
|
66
|
+
const app = createApp(() => ioInstance, terminalSession);
|
|
67
|
+
const server = http.createServer(app);
|
|
444
68
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
<html>
|
|
448
|
-
<head>
|
|
449
|
-
<meta charset="utf-8" />
|
|
450
|
-
<title>Terminal Expose</title>
|
|
451
|
-
</head>
|
|
452
|
-
<body style="font-family: system-ui, sans-serif; line-height: 1.5; padding: 32px;">
|
|
453
|
-
<h1>Terminal Expose</h1>
|
|
454
|
-
<p>This server is running. Use the private session URL printed in the container logs.</p>
|
|
455
|
-
</body>
|
|
456
|
-
</html>`);
|
|
457
|
-
});
|
|
69
|
+
// Initialize Socket.io
|
|
70
|
+
ioInstance = setupSocket(server, terminalSession);
|
|
458
71
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
72
|
+
// Close server and process gracefully on terminal exit
|
|
73
|
+
terminalSession.on("exit", () => {
|
|
74
|
+
console.log("Shutting down server port...");
|
|
75
|
+
server.close(() => {
|
|
76
|
+
// Ensure process is fully killed after server cleanly exits
|
|
77
|
+
process.exit(terminalSession.exit?.exitCode ?? 0);
|
|
464
78
|
});
|
|
465
|
-
});
|
|
466
|
-
|
|
467
|
-
app.get("/s/:token", (req, res) => {
|
|
468
|
-
if (req.params.token !== SESSION_TOKEN) {
|
|
469
|
-
return res.status(403).send("Invalid session token");
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
res.type("html").send(VIEWER_HTML);
|
|
473
|
-
});
|
|
474
79
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
if (terminalExit) {
|
|
483
|
-
socket.emit("terminal-exit", terminalExit);
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
socket.on("disconnect", () => {
|
|
487
|
-
console.log(`Viewer disconnected: ${socket.id}`);
|
|
488
|
-
});
|
|
80
|
+
// Force exit if server.close() hangs (e.g., active keep-alive connections)
|
|
81
|
+
setTimeout(
|
|
82
|
+
() => process.exit(terminalSession.exit?.exitCode ?? 0),
|
|
83
|
+
1000,
|
|
84
|
+
).unref();
|
|
489
85
|
});
|
|
490
86
|
|
|
491
87
|
server.listen(PORT, HOST, () => {
|
|
492
88
|
const localUrl = `http://localhost:${PORT}/s/${SESSION_TOKEN}`;
|
|
493
|
-
const externalUrl =
|
|
494
|
-
? `${
|
|
89
|
+
const externalUrl = EXTERNAL_URL
|
|
90
|
+
? `${EXTERNAL_URL.replace(/\/$/, "")}/s/${SESSION_TOKEN}`
|
|
495
91
|
: null;
|
|
496
92
|
|
|
497
93
|
console.log("\n==============================");
|
|
@@ -500,20 +96,22 @@ server.listen(PORT, HOST, () => {
|
|
|
500
96
|
console.log(`Command : ${[shellCommand, ...shellArgs].join(" ")}`);
|
|
501
97
|
console.log(`Local URL : ${localUrl}`);
|
|
502
98
|
|
|
503
|
-
getLanUrls().forEach((url) =>
|
|
99
|
+
getLanUrls({ port: PORT, session_token: SESSION_TOKEN }).forEach((url) =>
|
|
100
|
+
console.log(`LAN URL : ${url}`),
|
|
101
|
+
);
|
|
504
102
|
|
|
505
103
|
if (externalUrl) {
|
|
506
104
|
console.log(`Public URL: ${externalUrl}`);
|
|
507
105
|
}
|
|
508
106
|
|
|
509
107
|
if (options.publicTunnel) {
|
|
510
|
-
startPublicTunnel().catch((error) => {
|
|
108
|
+
startPublicTunnel(options, { HOST, PORT, SESSION_TOKEN }).catch((error) => {
|
|
511
109
|
console.error(`Public tunnel failed: ${error.message}`);
|
|
512
110
|
});
|
|
513
111
|
}
|
|
514
112
|
|
|
515
113
|
console.log("");
|
|
516
|
-
console.log("Share only the session URL with trusted
|
|
114
|
+
console.log("Share only the session URL with trusted collaborators.");
|
|
517
115
|
console.log("Use --public for internet access without router forwarding.");
|
|
518
116
|
console.log("==============================\n");
|
|
519
117
|
});
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "terminal-expose",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Interactive terminal session sharing over HTTP and WebSocket.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"terminal-expose": "index.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"index.js",
|
|
11
|
+
"src",
|
|
11
12
|
"public"
|
|
12
13
|
],
|
|
13
14
|
"scripts": {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const crypto = require("crypto");
|
|
2
|
+
|
|
3
|
+
const PORT = Number(process.env.PORT || 1111);
|
|
4
|
+
const HOST = process.env.HOST || "0.0.0.0";
|
|
5
|
+
const MAX_BUFFER = Number(process.env.MAX_BUFFER || 100000);
|
|
6
|
+
const ECHO_TO_STDOUT = process.env.ECHO_TO_STDOUT !== "0";
|
|
7
|
+
const SESSION_TOKEN =
|
|
8
|
+
process.env.SESSION_TOKEN || crypto.randomBytes(18).toString("base64url");
|
|
9
|
+
const TERM_COLS = Number(process.env.TERM_COLS || 120);
|
|
10
|
+
const TERM_ROWS = Number(process.env.TERM_ROWS || 40);
|
|
11
|
+
const WORKDIR = process.env.WORKDIR || process.cwd();
|
|
12
|
+
const EXTERNAL_URL = process.env.EXTERNAL_URL || null;
|
|
13
|
+
|
|
14
|
+
module.exports = {
|
|
15
|
+
PORT,
|
|
16
|
+
HOST,
|
|
17
|
+
MAX_BUFFER,
|
|
18
|
+
ECHO_TO_STDOUT,
|
|
19
|
+
SESSION_TOKEN,
|
|
20
|
+
TERM_COLS,
|
|
21
|
+
TERM_ROWS,
|
|
22
|
+
WORKDIR,
|
|
23
|
+
EXTERNAL_URL,
|
|
24
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const { Server } = require("socket.io");
|
|
2
|
+
const { SESSION_TOKEN } = require("../config/env");
|
|
3
|
+
|
|
4
|
+
function setupSocket(server, terminalSession) {
|
|
5
|
+
const io = new Server(server, {
|
|
6
|
+
cors: {
|
|
7
|
+
origin: false,
|
|
8
|
+
},
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
io.use((socket, next) => {
|
|
12
|
+
const token = socket.handshake.auth?.token;
|
|
13
|
+
|
|
14
|
+
if (token !== SESSION_TOKEN) {
|
|
15
|
+
return next(new Error("unauthorized"));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return next();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
io.on("connection", (socket) => {
|
|
22
|
+
console.log(`Client connected: ${socket.id}`);
|
|
23
|
+
|
|
24
|
+
if (terminalSession.buffer) {
|
|
25
|
+
socket.emit("terminal-output", terminalSession.buffer);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (terminalSession.exit) {
|
|
29
|
+
socket.emit("terminal-exit", terminalSession.exit);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
socket.on("terminal-input", (data) => {
|
|
33
|
+
terminalSession.write(data);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
socket.on("terminal-resize", ({ cols, rows }) => {
|
|
37
|
+
terminalSession.resize(cols, rows);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
socket.on("disconnect", () => {
|
|
41
|
+
console.log(`Client disconnected: ${socket.id}`);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
terminalSession.on("data", (data) => {
|
|
46
|
+
io.emit("terminal-output", data);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
terminalSession.on("exit", (exitState) => {
|
|
50
|
+
io.emit("terminal-exit", exitState);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return io;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = { setupSocket };
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
const pty = require("node-pty");
|
|
2
|
+
const {
|
|
3
|
+
ECHO_TO_STDOUT,
|
|
4
|
+
MAX_BUFFER,
|
|
5
|
+
TERM_COLS,
|
|
6
|
+
TERM_ROWS,
|
|
7
|
+
WORKDIR,
|
|
8
|
+
} = require("../config/env");
|
|
9
|
+
|
|
10
|
+
class TerminalSession {
|
|
11
|
+
constructor(shellCommand, shellArgs) {
|
|
12
|
+
this.buffer = "";
|
|
13
|
+
this.exit = null;
|
|
14
|
+
this.listeners = [];
|
|
15
|
+
|
|
16
|
+
this.shell = pty.spawn(shellCommand, shellArgs, {
|
|
17
|
+
name: "xterm-256color",
|
|
18
|
+
// cols: TERM_COLS,
|
|
19
|
+
// rows: TERM_ROWS,
|
|
20
|
+
cwd: WORKDIR,
|
|
21
|
+
env: {
|
|
22
|
+
...process.env,
|
|
23
|
+
TERM: "xterm-256color",
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
if (process.stdin.isTTY) {
|
|
28
|
+
process.stdin.setRawMode(true);
|
|
29
|
+
}
|
|
30
|
+
process.stdin.resume();
|
|
31
|
+
process.stdin.on("data", (data) => {
|
|
32
|
+
this.shell.write(data);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
this.shell.onData((data) => {
|
|
36
|
+
if (ECHO_TO_STDOUT) {
|
|
37
|
+
process.stdout.write(data);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
this.buffer += data;
|
|
41
|
+
|
|
42
|
+
if (this.buffer.length > MAX_BUFFER) {
|
|
43
|
+
this.buffer = this.buffer.slice(-MAX_BUFFER);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
this.emit("data", data);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
this.shell.onExit(({ exitCode, signal }) => {
|
|
50
|
+
this.exit = { exitCode, signal };
|
|
51
|
+
this.emit("exit", this.exit);
|
|
52
|
+
|
|
53
|
+
console.log(`Terminal command exited: code=${exitCode} signal=${signal}`);
|
|
54
|
+
|
|
55
|
+
if (process.stdin.isTTY) {
|
|
56
|
+
process.stdin.setRawMode(false);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
write(data) {
|
|
62
|
+
if (this.shell) {
|
|
63
|
+
this.shell.write(data);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
resize(cols, rows) {
|
|
68
|
+
if (this.shell && cols && rows) {
|
|
69
|
+
try {
|
|
70
|
+
this.shell.resize(cols, rows);
|
|
71
|
+
} catch (err) {
|
|
72
|
+
console.error("Failed to resize terminal", err);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
on(event, callback) {
|
|
78
|
+
this.listeners.push({ event, callback });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
emit(event, data) {
|
|
82
|
+
this.listeners
|
|
83
|
+
.filter((listener) => listener.event === event)
|
|
84
|
+
.forEach((listener) => listener.callback(data));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = { TerminalSession };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
const https = require("https");
|
|
2
|
+
const localtunnel = require("localtunnel");
|
|
3
|
+
|
|
4
|
+
function getLocaltunnelPassword() {
|
|
5
|
+
return new Promise((resolve) => {
|
|
6
|
+
let settled = false;
|
|
7
|
+
|
|
8
|
+
function finish(value) {
|
|
9
|
+
if (settled) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
settled = true;
|
|
14
|
+
resolve(value);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const request = https.get(
|
|
18
|
+
"https://loca.lt/mytunnelpassword",
|
|
19
|
+
(response) => {
|
|
20
|
+
if (response.statusCode !== 200) {
|
|
21
|
+
response.resume();
|
|
22
|
+
finish(null);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let body = "";
|
|
27
|
+
response.setEncoding("utf8");
|
|
28
|
+
response.on("data", (chunk) => {
|
|
29
|
+
body += chunk;
|
|
30
|
+
});
|
|
31
|
+
response.on("end", () => {
|
|
32
|
+
finish(body.trim() || null);
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
request.setTimeout(3000, () => {
|
|
38
|
+
request.destroy();
|
|
39
|
+
finish(null);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
request.on("error", () => {
|
|
43
|
+
finish(null);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function startPublicTunnel(options, { HOST, PORT, SESSION_TOKEN }) {
|
|
49
|
+
const localHost = HOST === "0.0.0.0" || HOST === "::" ? "127.0.0.1" : HOST;
|
|
50
|
+
const tunnelOptions = {
|
|
51
|
+
port: PORT,
|
|
52
|
+
local_host: localHost,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
if (options.tunnelSubdomain) {
|
|
56
|
+
tunnelOptions.subdomain = options.tunnelSubdomain;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (options.tunnelHost) {
|
|
60
|
+
tunnelOptions.host = options.tunnelHost;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
console.log("Public tunnel: starting...");
|
|
64
|
+
|
|
65
|
+
const tunnel = await localtunnel(tunnelOptions);
|
|
66
|
+
const publicUrl = `${tunnel.url.replace(/\/$/, "")}/s/${SESSION_TOKEN}`;
|
|
67
|
+
|
|
68
|
+
console.log(`Public URL: ${publicUrl}`);
|
|
69
|
+
|
|
70
|
+
if (new URL(tunnel.url).hostname.endsWith(".loca.lt")) {
|
|
71
|
+
const password = await getLocaltunnelPassword();
|
|
72
|
+
|
|
73
|
+
if (password) {
|
|
74
|
+
console.log(`Tunnel password: ${password}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
tunnel.on("error", (error) => {
|
|
79
|
+
console.error(`Public tunnel error: ${error.message}`);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
tunnel.on("close", () => {
|
|
83
|
+
console.log("Public tunnel closed.");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
["SIGINT", "SIGTERM"].forEach((signal) => {
|
|
87
|
+
process.once(signal, () => {
|
|
88
|
+
tunnel.close();
|
|
89
|
+
process.kill(process.pid, signal);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return tunnel;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = { startPublicTunnel };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const express = require("express");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { SESSION_TOKEN } = require("../config/env");
|
|
4
|
+
const { VIEWER_HTML } = require("../templates/viewer");
|
|
5
|
+
|
|
6
|
+
function createApp(ioGetter, terminalSession) {
|
|
7
|
+
const app = express();
|
|
8
|
+
|
|
9
|
+
// We are referring to public directory in root
|
|
10
|
+
app.use("/assets", express.static(path.join(__dirname, "../../public")));
|
|
11
|
+
|
|
12
|
+
app.get("/", (_, res) => {
|
|
13
|
+
res.type("html").send(`<!doctype html>
|
|
14
|
+
<html>
|
|
15
|
+
<head>
|
|
16
|
+
<meta charset="utf-8" />
|
|
17
|
+
<title>Terminal Expose</title>
|
|
18
|
+
</head>
|
|
19
|
+
<body style="font-family: system-ui, sans-serif; line-height: 1.5; padding: 32px;">
|
|
20
|
+
<h1>Terminal Expose</h1>
|
|
21
|
+
<p>This server is running. Use the private session URL printed in the container logs.</p>
|
|
22
|
+
</body>
|
|
23
|
+
</html>`);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
app.get("/health", (_, res) => {
|
|
27
|
+
const io = ioGetter();
|
|
28
|
+
res.json({
|
|
29
|
+
ok: true,
|
|
30
|
+
clients: io ? io.engine.clientsCount : 0,
|
|
31
|
+
exited: terminalSession.exit,
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
app.get("/s/:token", (req, res) => {
|
|
36
|
+
if (req.params.token !== SESSION_TOKEN) {
|
|
37
|
+
return res.status(403).send("Invalid session token");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
res.type("html").send(VIEWER_HTML);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return app;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = { createApp };
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
const VIEWER_HTML = String.raw`<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>Terminal Session</title>
|
|
7
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm/css/xterm.css" />
|
|
8
|
+
<style>
|
|
9
|
+
html,
|
|
10
|
+
body {
|
|
11
|
+
margin: 0;
|
|
12
|
+
padding: 0;
|
|
13
|
+
background: #050505;
|
|
14
|
+
color: #f2f2f2;
|
|
15
|
+
width: 100%;
|
|
16
|
+
height: 100%;
|
|
17
|
+
overflow: hidden;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
#terminal {
|
|
21
|
+
width: 100vw;
|
|
22
|
+
height: 100vh;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.xterm {
|
|
26
|
+
height: 100%;
|
|
27
|
+
}
|
|
28
|
+
</style>
|
|
29
|
+
</head>
|
|
30
|
+
<body>
|
|
31
|
+
<div id="terminal"></div>
|
|
32
|
+
<script src="/socket.io/socket.io.js"></script>
|
|
33
|
+
<script src="https://cdn.jsdelivr.net/npm/xterm/lib/xterm.js"></script>
|
|
34
|
+
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit/lib/xterm-addon-fit.js"></script>
|
|
35
|
+
<script>
|
|
36
|
+
const token = window.location.pathname.split('/').filter(Boolean).pop();
|
|
37
|
+
const term = new Terminal({
|
|
38
|
+
cursorBlink: true,
|
|
39
|
+
convertEol: true,
|
|
40
|
+
fontFamily: '"JetBrains Mono", "SFMono-Regular", Consolas, monospace',
|
|
41
|
+
fontSize: 14,
|
|
42
|
+
scrollback: 10000,
|
|
43
|
+
theme: {
|
|
44
|
+
background: '#050505',
|
|
45
|
+
foreground: '#f4f4f5',
|
|
46
|
+
cursor: '#f4f4f5',
|
|
47
|
+
black: '#18181b',
|
|
48
|
+
red: '#ef4444',
|
|
49
|
+
green: '#22c55e',
|
|
50
|
+
yellow: '#eab308',
|
|
51
|
+
blue: '#3b82f6',
|
|
52
|
+
magenta: '#a855f7',
|
|
53
|
+
cyan: '#06b6d4',
|
|
54
|
+
white: '#e4e4e7'
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const fitAddon = new FitAddon.FitAddon();
|
|
59
|
+
term.loadAddon(fitAddon);
|
|
60
|
+
|
|
61
|
+
term.open(document.getElementById('terminal'));
|
|
62
|
+
fitAddon.fit();
|
|
63
|
+
|
|
64
|
+
const socket = io({
|
|
65
|
+
auth: { token },
|
|
66
|
+
reconnectionAttempts: 10,
|
|
67
|
+
timeout: 5000
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
window.addEventListener('resize', () => {
|
|
71
|
+
fitAddon.fit();
|
|
72
|
+
socket.emit('terminal-resize', { cols: term.cols, rows: term.rows });
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
socket.on('connect', () => {
|
|
76
|
+
socket.emit('terminal-resize', { cols: term.cols, rows: term.rows });
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
socket.on('terminal-output', (data) => {
|
|
80
|
+
term.write(data);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
term.onData((data) => {
|
|
84
|
+
socket.emit('terminal-input', data);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
socket.on('terminal-exit', ({ exitCode, signal }) => {
|
|
88
|
+
const suffix = signal ? 'signal ' + signal : 'exit code ' + exitCode;
|
|
89
|
+
term.writeln('\r\n[Command ended: ' + suffix + ']');
|
|
90
|
+
});
|
|
91
|
+
</script>
|
|
92
|
+
</body>
|
|
93
|
+
</html>`;
|
|
94
|
+
|
|
95
|
+
module.exports = { VIEWER_HTML };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
function parseArgs(args) {
|
|
2
|
+
const options = {
|
|
3
|
+
publicTunnel: false,
|
|
4
|
+
tunnelSubdomain: process.env.TUNNEL_SUBDOMAIN || null,
|
|
5
|
+
tunnelHost: process.env.TUNNEL_HOST || undefined,
|
|
6
|
+
};
|
|
7
|
+
const commandArgs = [];
|
|
8
|
+
|
|
9
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
10
|
+
const arg = args[index];
|
|
11
|
+
|
|
12
|
+
if (arg === "--public" || arg === "--tunnel") {
|
|
13
|
+
options.publicTunnel = true;
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (arg === "--subdomain") {
|
|
18
|
+
options.tunnelSubdomain = args[index + 1] || null;
|
|
19
|
+
index += 1;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (arg.startsWith("--subdomain=")) {
|
|
24
|
+
options.tunnelSubdomain = arg.slice("--subdomain=".length) || null;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (arg === "--tunnel-host") {
|
|
29
|
+
options.tunnelHost = args[index + 1] || undefined;
|
|
30
|
+
index += 1;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (arg.startsWith("--tunnel-host=")) {
|
|
35
|
+
options.tunnelHost = arg.slice("--tunnel-host=".length) || undefined;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
commandArgs.push(arg);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (
|
|
43
|
+
process.env.PUBLIC_TUNNEL === "1" ||
|
|
44
|
+
process.env.PUBLIC_TUNNEL === "true"
|
|
45
|
+
) {
|
|
46
|
+
options.publicTunnel = true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return { options, commandArgs };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = { parseArgs };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const os = require("os");
|
|
2
|
+
|
|
3
|
+
const getDefaultShell = () => {
|
|
4
|
+
switch (process.platform) {
|
|
5
|
+
case "win32":
|
|
6
|
+
return process.env.ComSpec || "powershell.exe";
|
|
7
|
+
|
|
8
|
+
case "darwin":
|
|
9
|
+
return process.env.SHELL || "/bin/zsh";
|
|
10
|
+
|
|
11
|
+
case "linux":
|
|
12
|
+
return process.env.SHELL || "/bin/bash";
|
|
13
|
+
|
|
14
|
+
default:
|
|
15
|
+
return process.env.SHELL || "/bin/sh";
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function getLanUrls({ port, session_token }) {
|
|
20
|
+
const urls = [];
|
|
21
|
+
const interfaces = os.networkInterfaces();
|
|
22
|
+
|
|
23
|
+
Object.values(interfaces).forEach((addresses = []) => {
|
|
24
|
+
addresses
|
|
25
|
+
.filter((address) => address.family === "IPv4" && !address.internal)
|
|
26
|
+
.forEach((address) => {
|
|
27
|
+
urls.push(`http://${address.address}:${port}/s/${session_token}`);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
return urls;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = { getDefaultShell, getLanUrls };
|