terminal-expose 1.1.0 → 1.2.1
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 +8 -14
- package/index.js +2 -7
- package/package.json +4 -10
- package/src/config/env.js +1 -1
- package/src/core/terminal.js +9 -4
- package/src/core/tunnel.js +25 -81
- package/src/server/app.js +2 -6
- package/src/templates/viewer.js +454 -66
package/README.md
CHANGED
|
@@ -34,15 +34,15 @@ terminal-expose bash -lc "npm test"
|
|
|
34
34
|
The CLI prints a URL like:
|
|
35
35
|
|
|
36
36
|
```text
|
|
37
|
-
Local URL : http://localhost:
|
|
38
|
-
LAN URL : http://192.168.1.10:
|
|
37
|
+
Local URL : http://localhost:5555/s/<token>
|
|
38
|
+
LAN URL : http://192.168.1.10:5555/s/<token>
|
|
39
39
|
```
|
|
40
40
|
|
|
41
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
|
|
|
45
|
-
For quick public sharing, start a
|
|
45
|
+
For quick public sharing, start a secure edge tunnel (powered by Cloudflare Quick Tunnels):
|
|
46
46
|
|
|
47
47
|
```sh
|
|
48
48
|
terminal-expose --public bash
|
|
@@ -51,22 +51,17 @@ terminal-expose --public bash
|
|
|
51
51
|
The CLI prints a URL like:
|
|
52
52
|
|
|
53
53
|
```text
|
|
54
|
-
Public URL: https://random-name.
|
|
55
|
-
Tunnel password: <localtunnel-password>
|
|
54
|
+
Public URL: https://random-name.trycloudflare.com/s/<token>
|
|
56
55
|
```
|
|
57
56
|
|
|
58
|
-
|
|
57
|
+
_(Note: Custom subdomains are not currently supported by free Cloudflare Quick Tunnels.)_
|
|
59
58
|
|
|
60
|
-
|
|
61
|
-
terminal-expose --public --subdomain my-terminal-demo bash
|
|
62
|
-
```
|
|
63
|
-
|
|
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.
|
|
59
|
+
The tunnel URL works outside your Wi-Fi as long as the CLI is still running.
|
|
65
60
|
|
|
66
|
-
For a permanent setup, run it on a public server or forward TCP port
|
|
61
|
+
For a permanent setup, run it on a public server or forward TCP port 5555 to the machine running the CLI:
|
|
67
62
|
|
|
68
63
|
```sh
|
|
69
|
-
EXTERNAL_URL="http://YOUR_PUBLIC_IP:
|
|
64
|
+
EXTERNAL_URL="http://YOUR_PUBLIC_IP:5555" terminal-expose bash
|
|
70
65
|
```
|
|
71
66
|
|
|
72
67
|
## Options
|
|
@@ -77,7 +72,6 @@ Use environment variables:
|
|
|
77
72
|
PORT=4000 terminal-expose bash
|
|
78
73
|
SESSION_TOKEN="change-this-secret" terminal-expose bash
|
|
79
74
|
PUBLIC_TUNNEL=1 terminal-expose bash
|
|
80
|
-
TUNNEL_SUBDOMAIN="my-terminal-demo" PUBLIC_TUNNEL=1 terminal-expose bash
|
|
81
75
|
TERM_COLS=160 TERM_ROWS=48 terminal-expose bash
|
|
82
76
|
```
|
|
83
77
|
|
package/index.js
CHANGED
|
@@ -26,23 +26,18 @@ Examples:
|
|
|
26
26
|
terminal-expose zsh
|
|
27
27
|
terminal-expose bash -lc "cd ~/project && npm test"
|
|
28
28
|
terminal-expose --public bash
|
|
29
|
-
terminal-expose --public --subdomain my-demo bash
|
|
30
29
|
|
|
31
30
|
Environment:
|
|
32
|
-
PORT=
|
|
31
|
+
PORT=5555
|
|
33
32
|
HOST=0.0.0.0
|
|
34
33
|
SESSION_TOKEN=<custom-token>
|
|
35
|
-
EXTERNAL_URL=http://YOUR_PUBLIC_IP:
|
|
34
|
+
EXTERNAL_URL=http://YOUR_PUBLIC_IP:5555
|
|
36
35
|
PUBLIC_TUNNEL=1
|
|
37
|
-
TUNNEL_SUBDOMAIN=my-demo
|
|
38
|
-
TUNNEL_HOST=https://localtunnel.me
|
|
39
36
|
TERM_COLS=120
|
|
40
37
|
TERM_ROWS=40
|
|
41
38
|
|
|
42
39
|
Options:
|
|
43
40
|
--public, --tunnel Create a public HTTPS tunnel.
|
|
44
|
-
--subdomain <name> Request a localtunnel subdomain.
|
|
45
|
-
--tunnel-host <url> Use a different localtunnel server.
|
|
46
41
|
`);
|
|
47
42
|
process.exit(0);
|
|
48
43
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "terminal-expose",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "Interactive terminal session sharing over HTTP and WebSocket.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,8 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"index.js",
|
|
11
|
-
"src"
|
|
12
|
-
"public"
|
|
11
|
+
"src"
|
|
13
12
|
],
|
|
14
13
|
"scripts": {
|
|
15
14
|
"start": "node index.js",
|
|
@@ -33,13 +32,8 @@
|
|
|
33
32
|
},
|
|
34
33
|
"dependencies": {
|
|
35
34
|
"express": "^5.2.1",
|
|
36
|
-
"localtunnel": "^2.0.2",
|
|
37
35
|
"node-pty": "^1.1.0",
|
|
38
|
-
"socket.io": "^4.8.3"
|
|
39
|
-
|
|
40
|
-
"overrides": {
|
|
41
|
-
"localtunnel": {
|
|
42
|
-
"axios": "^1.16.1"
|
|
43
|
-
}
|
|
36
|
+
"socket.io": "^4.8.3",
|
|
37
|
+
"untun": "^0.1.3"
|
|
44
38
|
}
|
|
45
39
|
}
|
package/src/config/env.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const crypto = require("crypto");
|
|
2
2
|
|
|
3
|
-
const PORT = Number(process.env.PORT ||
|
|
3
|
+
const PORT = Number(process.env.PORT || 5555);
|
|
4
4
|
const HOST = process.env.HOST || "0.0.0.0";
|
|
5
5
|
const MAX_BUFFER = Number(process.env.MAX_BUFFER || 100000);
|
|
6
6
|
const ECHO_TO_STDOUT = process.env.ECHO_TO_STDOUT !== "0";
|
package/src/core/terminal.js
CHANGED
|
@@ -15,8 +15,6 @@ class TerminalSession {
|
|
|
15
15
|
|
|
16
16
|
this.shell = pty.spawn(shellCommand, shellArgs, {
|
|
17
17
|
name: "xterm-256color",
|
|
18
|
-
// cols: TERM_COLS,
|
|
19
|
-
// rows: TERM_ROWS,
|
|
20
18
|
cwd: WORKDIR,
|
|
21
19
|
env: {
|
|
22
20
|
...process.env,
|
|
@@ -24,33 +22,40 @@ class TerminalSession {
|
|
|
24
22
|
},
|
|
25
23
|
});
|
|
26
24
|
|
|
25
|
+
// verify if terminal was available or not bro
|
|
27
26
|
if (process.stdin.isTTY) {
|
|
27
|
+
// listening to every stroke of host terminal input
|
|
28
28
|
process.stdin.setRawMode(true);
|
|
29
29
|
}
|
|
30
30
|
process.stdin.resume();
|
|
31
|
+
|
|
32
|
+
// write to shell after getting the input from host terminal
|
|
31
33
|
process.stdin.on("data", (data) => {
|
|
32
34
|
this.shell.write(data);
|
|
33
35
|
});
|
|
34
36
|
|
|
37
|
+
// after getting the data from shell writing it to host terminal
|
|
35
38
|
this.shell.onData((data) => {
|
|
36
39
|
if (ECHO_TO_STDOUT) {
|
|
40
|
+
// this write the user input to the host terminal
|
|
37
41
|
process.stdout.write(data);
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
this.buffer += data;
|
|
41
|
-
|
|
42
45
|
if (this.buffer.length > MAX_BUFFER) {
|
|
43
46
|
this.buffer = this.buffer.slice(-MAX_BUFFER);
|
|
44
47
|
}
|
|
45
48
|
|
|
49
|
+
// ! need to verify it was need or not
|
|
46
50
|
this.emit("data", data);
|
|
47
51
|
});
|
|
48
52
|
|
|
53
|
+
// if the terminal was exited in host machine exit the process and inform the listeing clients
|
|
49
54
|
this.shell.onExit(({ exitCode, signal }) => {
|
|
50
55
|
this.exit = { exitCode, signal };
|
|
51
56
|
this.emit("exit", this.exit);
|
|
52
57
|
|
|
53
|
-
console.log(`Terminal
|
|
58
|
+
console.log(`Terminal-Expose exited: code=${exitCode} signal=${signal}`);
|
|
54
59
|
|
|
55
60
|
if (process.stdin.isTTY) {
|
|
56
61
|
process.stdin.setRawMode(false);
|
package/src/core/tunnel.js
CHANGED
|
@@ -1,96 +1,40 @@
|
|
|
1
|
-
const
|
|
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
|
-
}
|
|
1
|
+
const { startTunnel } = require("untun");
|
|
47
2
|
|
|
48
3
|
async function startPublicTunnel(options, { HOST, PORT, SESSION_TOKEN }) {
|
|
49
4
|
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
5
|
|
|
55
|
-
if (options.tunnelSubdomain) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (options.tunnelHost) {
|
|
60
|
-
tunnelOptions.host = options.tunnelHost;
|
|
6
|
+
if (options.tunnelSubdomain || options.tunnelHost) {
|
|
7
|
+
console.warn(
|
|
8
|
+
"Warning: Custom subdomain and tunnel host are not supported with Cloudflare Quick Tunnels.",
|
|
9
|
+
);
|
|
61
10
|
}
|
|
62
11
|
|
|
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();
|
|
12
|
+
console.log("Public tunnel: starting cloudflared...");
|
|
72
13
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
14
|
+
try {
|
|
15
|
+
const tunnel = await startTunnel({
|
|
16
|
+
port: PORT,
|
|
17
|
+
hostname: localHost,
|
|
18
|
+
acceptCloudflareNotice: true,
|
|
19
|
+
});
|
|
77
20
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
});
|
|
21
|
+
const url = await tunnel.getURL();
|
|
22
|
+
const publicUrl = `${url.replace(/\/$/, "")}/s/${SESSION_TOKEN}`;
|
|
81
23
|
|
|
82
|
-
|
|
83
|
-
console.log("Public tunnel closed.");
|
|
84
|
-
});
|
|
24
|
+
console.log(`Public URL: ${publicUrl}`);
|
|
85
25
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
26
|
+
["SIGINT", "SIGTERM"].forEach((signal) => {
|
|
27
|
+
process.once(signal, async () => {
|
|
28
|
+
await tunnel.close();
|
|
29
|
+
process.kill(process.pid, signal);
|
|
30
|
+
});
|
|
90
31
|
});
|
|
91
|
-
});
|
|
92
32
|
|
|
93
|
-
|
|
33
|
+
return tunnel;
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.error(`Public tunnel error: ${error.message}`);
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
94
38
|
}
|
|
95
39
|
|
|
96
40
|
module.exports = { startPublicTunnel };
|
package/src/server/app.js
CHANGED
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
const express = require("express");
|
|
2
|
-
const path = require("path");
|
|
3
2
|
const { SESSION_TOKEN } = require("../config/env");
|
|
4
|
-
const { VIEWER_HTML } = require("../templates/viewer");
|
|
5
3
|
|
|
6
4
|
function createApp(ioGetter, terminalSession) {
|
|
7
5
|
const app = express();
|
|
8
6
|
|
|
9
|
-
// We are referring to public directory in root
|
|
10
|
-
app.use("/assets", express.static(path.join(__dirname, "../../public")));
|
|
11
|
-
|
|
12
7
|
app.get("/", (_, res) => {
|
|
13
8
|
res.type("html").send(`<!doctype html>
|
|
14
9
|
<html>
|
|
@@ -37,7 +32,8 @@ function createApp(ioGetter, terminalSession) {
|
|
|
37
32
|
return res.status(403).send("Invalid session token");
|
|
38
33
|
}
|
|
39
34
|
|
|
40
|
-
|
|
35
|
+
const viewerHtml = require("../templates/viewer.js");
|
|
36
|
+
res.type("html").send(viewerHtml);
|
|
41
37
|
});
|
|
42
38
|
|
|
43
39
|
return app;
|
package/src/templates/viewer.js
CHANGED
|
@@ -1,95 +1,483 @@
|
|
|
1
|
-
|
|
2
|
-
<html>
|
|
1
|
+
module.exports = `<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
3
|
<head>
|
|
4
|
-
<meta charset="
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
|
|
6
7
|
<title>Terminal Session</title>
|
|
7
|
-
|
|
8
|
+
|
|
9
|
+
<link
|
|
10
|
+
rel="stylesheet"
|
|
11
|
+
href="https://cdn.jsdelivr.net/npm/xterm/css/xterm.css"
|
|
12
|
+
/>
|
|
13
|
+
|
|
8
14
|
<style>
|
|
15
|
+
* {
|
|
16
|
+
box-sizing: border-box;
|
|
17
|
+
}
|
|
18
|
+
|
|
9
19
|
html,
|
|
10
20
|
body {
|
|
11
|
-
margin: 0;
|
|
12
|
-
padding: 0;
|
|
13
|
-
background: #050505;
|
|
14
|
-
color: #f2f2f2;
|
|
15
21
|
width: 100%;
|
|
16
22
|
height: 100%;
|
|
23
|
+
margin: 0;
|
|
24
|
+
|
|
17
25
|
overflow: hidden;
|
|
26
|
+
|
|
27
|
+
background: #09090b;
|
|
28
|
+
color: #ffffff;
|
|
29
|
+
|
|
30
|
+
font-family:
|
|
31
|
+
Inter,
|
|
32
|
+
system-ui,
|
|
33
|
+
sans-serif;
|
|
18
34
|
}
|
|
19
35
|
|
|
20
|
-
|
|
21
|
-
|
|
36
|
+
|
|
37
|
+
/* ======================
|
|
38
|
+
Layout
|
|
39
|
+
====================== */
|
|
40
|
+
|
|
41
|
+
.app {
|
|
22
42
|
height: 100vh;
|
|
43
|
+
|
|
44
|
+
display: flex;
|
|
45
|
+
flex-direction: column;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
/* ======================
|
|
50
|
+
Header
|
|
51
|
+
====================== */
|
|
52
|
+
|
|
53
|
+
.header {
|
|
54
|
+
height: 48px;
|
|
55
|
+
|
|
56
|
+
padding: 0 16px;
|
|
57
|
+
|
|
58
|
+
display: flex;
|
|
59
|
+
align-items: center;
|
|
60
|
+
justify-content: space-between;
|
|
61
|
+
|
|
62
|
+
background:
|
|
63
|
+
linear-gradient(
|
|
64
|
+
180deg,
|
|
65
|
+
#18181b,
|
|
66
|
+
#09090b
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
border-bottom: 1px solid #27272a;
|
|
70
|
+
|
|
71
|
+
user-select: none;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
.title {
|
|
76
|
+
font-size: 14px;
|
|
77
|
+
font-weight: 500;
|
|
78
|
+
|
|
79
|
+
color: #e4e4e7;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
.actions {
|
|
84
|
+
display: flex;
|
|
85
|
+
align-items: center;
|
|
86
|
+
|
|
87
|
+
gap: 14px;
|
|
88
|
+
|
|
89
|
+
font-size: 13px;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
/* ======================
|
|
95
|
+
Status
|
|
96
|
+
====================== */
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
.status {
|
|
100
|
+
display: flex;
|
|
101
|
+
align-items: center;
|
|
102
|
+
|
|
103
|
+
gap: 6px;
|
|
23
104
|
}
|
|
24
105
|
|
|
106
|
+
|
|
107
|
+
.status-dot {
|
|
108
|
+
width: 9px;
|
|
109
|
+
height: 9px;
|
|
110
|
+
|
|
111
|
+
border-radius: 999px;
|
|
112
|
+
|
|
113
|
+
background: currentColor;
|
|
114
|
+
|
|
115
|
+
box-shadow:
|
|
116
|
+
0 0 12px currentColor;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
.connected {
|
|
121
|
+
color: #22c55e;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
.connecting {
|
|
126
|
+
color: #f97316;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
.disconnected {
|
|
131
|
+
color: #ef4444;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
/* ======================
|
|
137
|
+
Buttons
|
|
138
|
+
====================== */
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
button {
|
|
142
|
+
padding: 5px 10px;
|
|
143
|
+
|
|
144
|
+
cursor: pointer;
|
|
145
|
+
|
|
146
|
+
background: #18181b;
|
|
147
|
+
color: #e4e4e7;
|
|
148
|
+
|
|
149
|
+
border:
|
|
150
|
+
1px solid #3f3f46;
|
|
151
|
+
|
|
152
|
+
border-radius: 6px;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
button:hover {
|
|
157
|
+
background: #27272a;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
/* ======================
|
|
164
|
+
Terminal
|
|
165
|
+
====================== */
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
#terminal {
|
|
169
|
+
flex: 1;
|
|
170
|
+
|
|
171
|
+
padding: 12px;
|
|
172
|
+
|
|
173
|
+
overflow: hidden;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
|
|
25
177
|
.xterm {
|
|
26
178
|
height: 100%;
|
|
27
179
|
}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
/* ======================
|
|
184
|
+
Scrollbar
|
|
185
|
+
====================== */
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
.xterm-viewport::-webkit-scrollbar {
|
|
189
|
+
width: 8px;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
.xterm-viewport::-webkit-scrollbar-track {
|
|
194
|
+
background: transparent;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
.xterm-viewport::-webkit-scrollbar-thumb {
|
|
199
|
+
background: #3f3f46;
|
|
200
|
+
|
|
201
|
+
border-radius: 20px;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
.xterm-viewport::-webkit-scrollbar-thumb:hover {
|
|
206
|
+
background: #71717a;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@media(max-width: 600px) {
|
|
212
|
+
|
|
213
|
+
.title {
|
|
214
|
+
font-size: 12px;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
.actions {
|
|
219
|
+
gap: 8px;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
}
|
|
28
223
|
</style>
|
|
29
224
|
</head>
|
|
225
|
+
|
|
30
226
|
<body>
|
|
31
|
-
<div
|
|
227
|
+
<div class="app">
|
|
228
|
+
<header class="header">
|
|
229
|
+
<div class="title">⚡ Remote Terminal</div>
|
|
230
|
+
|
|
231
|
+
<div class="actions">
|
|
232
|
+
<span id="timer"> 00:00 </span>
|
|
233
|
+
|
|
234
|
+
<div id="status" class="status connecting">
|
|
235
|
+
<span class="status-dot"></span>
|
|
236
|
+
|
|
237
|
+
<span id="statusText"> Connecting </span>
|
|
238
|
+
</div>
|
|
239
|
+
|
|
240
|
+
<button id="fullscreen">⛶</button>
|
|
241
|
+
</div>
|
|
242
|
+
</header>
|
|
243
|
+
|
|
244
|
+
<main id="terminal"></main>
|
|
245
|
+
</div>
|
|
246
|
+
|
|
32
247
|
<script src="/socket.io/socket.io.js"></script>
|
|
33
248
|
<script src="https://cdn.jsdelivr.net/npm/xterm/lib/xterm.js"></script>
|
|
34
249
|
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit/lib/xterm-addon-fit.js"></script>
|
|
250
|
+
|
|
35
251
|
<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
252
|
|
|
58
|
-
const fitAddon = new FitAddon.FitAddon();
|
|
59
|
-
term.loadAddon(fitAddon);
|
|
60
253
|
|
|
61
|
-
|
|
254
|
+
const token =
|
|
255
|
+
location.pathname
|
|
256
|
+
.split("/")
|
|
257
|
+
.filter(Boolean)
|
|
258
|
+
.pop();
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
const term =
|
|
263
|
+
new Terminal({
|
|
264
|
+
cursorBlink: true,
|
|
265
|
+
convertEol: true,
|
|
266
|
+
fontFamily:
|
|
267
|
+
\`"JetBrains Mono", monospace\`,
|
|
268
|
+
fontSize: 14,
|
|
269
|
+
lineHeight: 1.3,
|
|
270
|
+
scrollback: 20000,
|
|
271
|
+
theme: {
|
|
272
|
+
background: "#09090b",
|
|
273
|
+
foreground: "#fafafa",
|
|
274
|
+
cursor: "#22c55e",
|
|
275
|
+
selection: "#334155"
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
const fitAddon =
|
|
282
|
+
new FitAddon.FitAddon();
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
term.loadAddon(
|
|
287
|
+
fitAddon
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
term.open(
|
|
292
|
+
document.getElementById(
|
|
293
|
+
"terminal"
|
|
294
|
+
)
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
|
|
62
298
|
fitAddon.fit();
|
|
63
299
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
const socket =
|
|
303
|
+
io({
|
|
304
|
+
auth:{
|
|
305
|
+
token
|
|
306
|
+
},
|
|
307
|
+
reconnection: true,
|
|
308
|
+
reconnectionAttempts:20,
|
|
309
|
+
timeout:5000
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
const statusBox =
|
|
313
|
+
document.getElementById(
|
|
314
|
+
"status"
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
const statusText =
|
|
318
|
+
document.getElementById(
|
|
319
|
+
"statusText"
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
function setStatus(
|
|
324
|
+
text,
|
|
325
|
+
type
|
|
326
|
+
){
|
|
327
|
+
statusBox.className =
|
|
328
|
+
"status " + type;
|
|
329
|
+
statusText.textContent =
|
|
330
|
+
text;
|
|
331
|
+
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
socket.on(
|
|
338
|
+
"connect",
|
|
339
|
+
() => {
|
|
340
|
+
|
|
341
|
+
setStatus(
|
|
342
|
+
"Connected",
|
|
343
|
+
"connected"
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
socket.emit(
|
|
348
|
+
"terminal-resize",
|
|
349
|
+
{
|
|
350
|
+
cols: term.cols,
|
|
351
|
+
rows: term.rows
|
|
352
|
+
}
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
}
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
socket.on(
|
|
363
|
+
"disconnect",
|
|
364
|
+
() => {
|
|
365
|
+
|
|
366
|
+
setStatus(
|
|
367
|
+
"Disconnected",
|
|
368
|
+
"disconnected"
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
}
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
socket.io.on(
|
|
378
|
+
"reconnect_attempt",
|
|
379
|
+
() => {
|
|
380
|
+
|
|
381
|
+
setStatus(
|
|
382
|
+
"Reconnecting",
|
|
383
|
+
"connecting"
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
}
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
socket.on(
|
|
393
|
+
"terminal-output",
|
|
394
|
+
data => {
|
|
395
|
+
|
|
396
|
+
term.write(data);
|
|
397
|
+
|
|
398
|
+
}
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
term.onData(
|
|
406
|
+
data => {
|
|
407
|
+
|
|
408
|
+
socket.emit(
|
|
409
|
+
"terminal-input",
|
|
410
|
+
data
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
}
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
window.addEventListener(
|
|
420
|
+
"resize",
|
|
421
|
+
() => {
|
|
422
|
+
|
|
423
|
+
fitAddon.fit();
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
socket.emit(
|
|
427
|
+
"terminal-resize",
|
|
428
|
+
{
|
|
429
|
+
cols: term.cols,
|
|
430
|
+
rows: term.rows
|
|
431
|
+
}
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
}
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
let seconds = 0;
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
setInterval(
|
|
445
|
+
() => {
|
|
446
|
+
|
|
447
|
+
if(!socket.connected)
|
|
448
|
+
return;
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
seconds++;
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
timer.textContent =
|
|
455
|
+
new Date(
|
|
456
|
+
seconds * 1000
|
|
457
|
+
)
|
|
458
|
+
.toISOString()
|
|
459
|
+
.substring(
|
|
460
|
+
14,
|
|
461
|
+
19
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
},
|
|
466
|
+
1000
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
fullscreen.onclick =
|
|
474
|
+
() => {
|
|
475
|
+
|
|
476
|
+
document.documentElement
|
|
477
|
+
.requestFullscreen();
|
|
478
|
+
|
|
479
|
+
};
|
|
91
480
|
</script>
|
|
92
481
|
</body>
|
|
93
|
-
</html
|
|
94
|
-
|
|
95
|
-
module.exports = { VIEWER_HTML };
|
|
482
|
+
</html>
|
|
483
|
+
`;
|